211 lines
9.2 KiB
Go
211 lines
9.2 KiB
Go
package engine
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
)
|
|
|
|
type groupedConfig struct {
|
|
Version string `yaml:"version"`
|
|
Settings map[string]string `yaml:"settings"`
|
|
Port int `yaml:"port"`
|
|
Derived string `yaml:"derived"`
|
|
}
|
|
|
|
func testGroupedLoad[T any](request loadRequest[T]) (*testResult[T], error) {
|
|
loaded, err := loadGroupedWithSecretLookup(request, testSecretBinding(), func(string) (string, bool) { return "", false })
|
|
return testLoaded(loaded), err
|
|
}
|
|
|
|
func TestGroupedLoadPrecedenceSourceOrderAndSingleLifecycle(t *testing.T) {
|
|
filesystem := NewFilesystem("bundle", fstest.MapFS{
|
|
"main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: imported.yml}]}\nversion: v1\nport: 5\nsettings: {shared: main}\n")},
|
|
"imported.yml": &fstest.MapFile{Data: []byte("settings: {imported: included}\n")},
|
|
"overlays/test.yml": &fstest.MapFile{Data: []byte("settings: {shared: overlay}\n")},
|
|
})
|
|
checks := struct{ version, defaults, resolution, validation int }{}
|
|
request := loadRequest[groupedConfig]{
|
|
main: loadInput{source: filesystem.File("main.yml"), imports: true},
|
|
underlays: []loadGroup{
|
|
{inputs: []loadInput{
|
|
{source: Mapping("low-a", map[string]any{"settings": map[string]any{"shared": "low", "a": "one"}, "port": "invalid until replaced"})},
|
|
{source: Bytes("low-b", []byte("settings: {b: two}\n"))},
|
|
}},
|
|
{inputs: []loadInput{{source: Mapping("middle", map[string]any{"settings": map[string]any{"shared": "middle", "c": "three"}})}}},
|
|
},
|
|
definition: Definition[groupedConfig]{
|
|
Version: &VersionRule{Path: Keys("version"), Check: func(version Version, validation *Validation) error {
|
|
checks.version++
|
|
if version.Text != "v1" {
|
|
t.Errorf("version = %#v", version)
|
|
}
|
|
return nil
|
|
}},
|
|
Defaults: func() groupedConfig { checks.defaults++; return groupedConfig{Derived: "initial"} },
|
|
Resolve: func(config *groupedConfig, resolution *Resolution) error {
|
|
checks.resolution++
|
|
Default(resolution, Keys("derived"), &config.Derived, "resolved", "set derived")
|
|
return nil
|
|
},
|
|
Validate: func(config *groupedConfig, validation *Validation) { checks.validation++ },
|
|
},
|
|
overlay: "test", overlayFilesystem: &FilesystemContext{Root: ".", BaseDir: ".", FS: &filesystem},
|
|
overrides: []Override{{Path: Keys("settings", "shared"), Value: "cli", Flag: "--shared"}},
|
|
}
|
|
result, err := testGroupedLoad(request)
|
|
if err != nil {
|
|
t.Fatalf("load: %v, diagnostics %#v", err, err.(*LoadError).Diagnostics())
|
|
}
|
|
wantSettings := map[string]string{"shared": "cli", "a": "one", "b": "two", "c": "three", "imported": "included"}
|
|
if result.Config.Port != 5 || !reflect.DeepEqual(result.Config.Settings, wantSettings) {
|
|
t.Fatalf("config = %#v", result.Config)
|
|
}
|
|
if checks != (struct{ version, defaults, resolution, validation int }{1, 1, 1, 1}) {
|
|
t.Fatalf("lifecycle calls = %#v", checks)
|
|
}
|
|
sources := result.Sources()
|
|
if len(sources) != 7 {
|
|
t.Fatalf("sources = %#v", sources)
|
|
}
|
|
wantLayers := []Layer{LayerBase, LayerBase, LayerUnderlay, LayerUnderlay, LayerUnderlay, LayerOverlay, LayerCLI}
|
|
wantIndexes := []int{0, 0, 1, 1, 2, 0, 0}
|
|
for index, source := range sources {
|
|
if source.ID != "source-"+string(rune('1'+index)) || source.Layer != wantLayers[index] || source.UnderlayIndex != wantIndexes[index] {
|
|
t.Errorf("source %d = %#v", index, source)
|
|
}
|
|
}
|
|
info, ok := result.Metadata().Lookup(Keys("settings", "shared"))
|
|
if !ok || info.Origin.Layer != LayerCLI {
|
|
t.Fatalf("effective shared metadata = %#v, %v", info, ok)
|
|
}
|
|
explanation, ok := result.Explain(Keys("settings", "shared"))
|
|
if !ok || len(explanation.History) < 5 {
|
|
t.Fatalf("shared explanation = %#v, %v", explanation, ok)
|
|
}
|
|
var layers []Layer
|
|
for _, change := range explanation.History {
|
|
if change.Origin.Layer != LayerDefault && (len(layers) == 0 || layers[len(layers)-1] != change.Origin.Layer) {
|
|
layers = append(layers, change.Origin.Layer)
|
|
}
|
|
}
|
|
if !reflect.DeepEqual(layers, []Layer{LayerUnderlay, LayerBase, LayerOverlay, LayerCLI}) {
|
|
t.Fatalf("history layers = %#v", layers)
|
|
}
|
|
}
|
|
|
|
func TestGroupedLoadAbsentMainAndVersionProtection(t *testing.T) {
|
|
var observed Version
|
|
request := loadRequest[groupedConfig]{
|
|
main: loadInput{source: NoSource()},
|
|
underlays: []loadGroup{{}, {inputs: []loadInput{{source: Bytes("underlay", []byte("port: 7\n"))}}}},
|
|
definition: Definition[groupedConfig]{Version: &VersionRule{Path: Keys("version"), Check: func(version Version, validation *Validation) error {
|
|
observed = version
|
|
return nil
|
|
}}},
|
|
}
|
|
result, err := testGroupedLoad(request)
|
|
if err != nil || result.Config.Port != 7 || observed.Present || len(result.Sources()) != 1 || result.Sources()[0].UnderlayIndex != 2 {
|
|
t.Fatalf("underlay-only load = %#v, version %#v, %v", result, observed, err)
|
|
}
|
|
request.underlays[1].inputs[0].source = Bytes("underlay", []byte("version: forbidden\n"))
|
|
_, err = testGroupedLoad(request)
|
|
if got := groupedErrorCode(t, err); got != CodeUnderlayVersion {
|
|
t.Fatalf("underlay version code = %s", got)
|
|
}
|
|
filesystem := NewFilesystem("bundle", fstest.MapFS{
|
|
"main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: fragment.yml}]}\n")},
|
|
"fragment.yml": &fstest.MapFile{Data: []byte("version: forbidden\n")},
|
|
})
|
|
request.underlays[1].inputs[0] = loadInput{source: filesystem.File("main.yml"), imports: true}
|
|
_, err = testGroupedLoad(request)
|
|
if got := groupedErrorCode(t, err); got != CodeImportVersion {
|
|
t.Fatalf("import version code = %s", got)
|
|
}
|
|
request.underlays = []loadGroup{{}}
|
|
request.definition.Defaults = func() groupedConfig { return groupedConfig{Port: 11} }
|
|
result, err = testGroupedLoad(request)
|
|
if err != nil || result.Config.Port != 11 || len(result.Sources()) != 0 {
|
|
t.Fatalf("defaults-only load = %#v, %v", result, err)
|
|
}
|
|
}
|
|
|
|
func TestGroupedLoadStrictConflictsRepetitionAndBudgets(t *testing.T) {
|
|
main := loadInput{source: Bytes("main", []byte("port: 5\n"))}
|
|
request := loadRequest[groupedConfig]{
|
|
main: main,
|
|
underlays: []loadGroup{{inputs: []loadInput{
|
|
{source: Bytes("left", []byte("port: 1\n"))},
|
|
{source: Bytes("right", []byte("port: 2\n"))},
|
|
}}},
|
|
}
|
|
_, err := testGroupedLoad(request)
|
|
if got := groupedErrorCode(t, err); got != CodeComposeConflict {
|
|
t.Fatalf("strict conflict code = %s", got)
|
|
}
|
|
conflicting := NewFilesystem("bundle", fstest.MapFS{
|
|
"main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: fragment.yml}]}\nport: 1\n")},
|
|
"fragment.yml": &fstest.MapFile{Data: []byte("port: 2\n")},
|
|
})
|
|
request.main = loadInput{source: conflicting.File("main.yml"), imports: true}
|
|
request.underlays = []loadGroup{{inputs: []loadInput{{source: Bytes("lower", []byte("port: 3\n"))}}}}
|
|
_, err = testGroupedLoad(request)
|
|
if got := groupedErrorCode(t, err); got != CodeComposeConflict {
|
|
t.Fatalf("main/import conflict code = %s", got)
|
|
}
|
|
request.main = main
|
|
filesystem := NewFilesystem("bundle", fstest.MapFS{"same.yml": &fstest.MapFile{Data: []byte("port: 1\n")}})
|
|
file := filesystem.File("same.yml")
|
|
request.underlays = []loadGroup{{inputs: []loadInput{{source: file}, {source: file}}}}
|
|
_, err = testGroupedLoad(request)
|
|
if got := groupedErrorCode(t, err); got != CodeImportRepeated {
|
|
t.Fatalf("same-group repetition code = %s", got)
|
|
}
|
|
request.underlays = []loadGroup{{inputs: []loadInput{{source: file}}}, {inputs: []loadInput{{source: file}}}}
|
|
if result, err := testGroupedLoad(request); err != nil || result.Config.Port != 5 || len(result.Sources()) != 3 {
|
|
t.Fatalf("cross-group reuse = %#v, %v", result, err)
|
|
}
|
|
|
|
accounting := &importAccounting{sources: maxImportSources - 1}
|
|
group := loadGroup{inputs: []loadInput{{source: Mapping("first", map[string]any{})}, {source: Mapping("second", map[string]any{})}}}
|
|
_, _, diagnostics := acquireUnderlayGroup(group, 1, nil, accounting)
|
|
if len(diagnostics) == 0 || diagnostics[0].Code != CodeSourceLimitExceeded {
|
|
t.Fatalf("shared source budget = %#v", diagnostics)
|
|
}
|
|
accounting = &importAccounting{bytes: maxTotalBytes - 2}
|
|
group.inputs = []loadInput{{source: Mapping("first", map[string]any{"port": "one"})}}
|
|
_, _, diagnostics = acquireUnderlayGroup(group, 1, nil, accounting)
|
|
if len(diagnostics) == 0 || diagnostics[0].Code != CodeSourceLimitExceeded {
|
|
t.Fatalf("shared byte budget = %#v", diagnostics)
|
|
}
|
|
}
|
|
|
|
func TestGroupedLoadInvalidIntermediateTypeCanBeReplaced(t *testing.T) {
|
|
request := loadRequest[groupedConfig]{
|
|
main: loadInput{source: Bytes("main", []byte("port: 9\n"))},
|
|
underlays: []loadGroup{{inputs: []loadInput{{source: Bytes("low", []byte("port: wrong\n"))}}}},
|
|
}
|
|
result, err := testGroupedLoad(request)
|
|
if err != nil || result.Config.Port != 9 {
|
|
t.Fatalf("final typed decode = %#v, %v", result, err)
|
|
}
|
|
request.main.source = Bytes("main", []byte("port: wrong\n"))
|
|
_, err = testGroupedLoad(request)
|
|
if got := groupedErrorCode(t, err); got != CodeDecodeType && got != CodeDecodeRange {
|
|
t.Fatalf("final invalid type code = %s", got)
|
|
}
|
|
}
|
|
|
|
func groupedErrorCode(t *testing.T, err error) Code {
|
|
t.Helper()
|
|
loadError, ok := err.(*LoadError)
|
|
if !ok || len(loadError.Diagnostics()) == 0 {
|
|
t.Fatalf("error = %#v", err)
|
|
}
|
|
if strings.Contains(loadError.Diagnostics()[0].Message, "wrong") {
|
|
t.Fatal("raw value escaped diagnostics")
|
|
}
|
|
return loadError.Diagnostics()[0].Code
|
|
}
|