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 } func TestGroupedAccountingChargesMixedOccurrencesOnce(t *testing.T) { const ( one = "configkit: {imports: [{path: child-one.yml}]}\nleft: first\n" child = "extra: imported\n" two = "configkit: {imports: [{path: child-two.yml}]}\nright: second\n" ) filesystem := NewFilesystem("bundle", fstest.MapFS{ "one.yml": &fstest.MapFile{Data: []byte(one)}, "child-one.yml": &fstest.MapFile{Data: []byte(child)}, "two.yml": &fstest.MapFile{Data: []byte(two)}, "child-two.yml": &fstest.MapFile{Data: []byte(child)}, }) accounting := &importAccounting{} first, sources, _, diagnostics := acquireUnderlayGroup(loadGroup{inputs: []loadInput{{source: filesystem.File("one.yml"), imports: true}}}, 1, nil, accounting) if len(diagnostics) != 0 || first == nil || len(sources) != 2 || accounting.sources != 2 || accounting.bytes != int64(len(one)+len(child)) { t.Fatalf("first acquisition = %#v, %#v, %#v, accounting %#v", first, sources, diagnostics, accounting) } second, sources, _, diagnostics := acquireUnderlayGroup(loadGroup{inputs: []loadInput{{source: filesystem.File("two.yml"), imports: true}}}, 2, nil, accounting) if len(diagnostics) != 0 || second == nil || len(sources) != 2 || sources[0].ID != "source-3" || sources[1].ID != "source-4" || accounting.sources != 4 || accounting.bytes != int64(len(one)+len(two)+2*len(child)) { t.Fatalf("second acquisition = %#v, %#v, %#v, accounting %#v", second, sources, diagnostics, accounting) } var wantNodes int for _, text := range []string{one, child, two, child} { parsed, diagnostics := parseSource(Bytes("fixture", []byte(text)), "source-1", defaultParseLimits()) if len(diagnostics) != 0 { t.Fatal(diagnostics) } wantNodes += countInputNodes(parsed.document) } if accounting.nodes != wantNodes { t.Fatalf("charged nodes = %d, want %d", accounting.nodes, wantNodes) } if first.inputBytes != int64(len(one)+len(child)) || second.inputBytes != int64(len(two)+len(child)) { t.Fatalf("composition costs = %d, %d, accounting %d", first.inputBytes, second.inputBytes, accounting.bytes) } } func TestGroupedGlobEntryBudgetIsShared(t *testing.T) { filesystem := NewFilesystem("bundle", fstest.MapFS{ "main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{glob: 'parts/*.yml'}]}\n")}, "parts/a.yml": &fstest.MapFile{Data: []byte("left: one\n")}, "parts/b.yml": &fstest.MapFile{Data: []byte("right: two\n")}, }) accounting := &importAccounting{entries: maxGlobEntries - 1} _, _, _, diagnostics := acquireUnderlayGroup(loadGroup{inputs: []loadInput{{source: filesystem.File("main.yml"), imports: true}}}, 1, nil, accounting) if len(diagnostics) == 0 || diagnostics[0].Code != CodeImportLimitExceeded || accounting.entries <= maxGlobEntries { t.Fatalf("shared glob budget = %#v, accounting %#v", diagnostics, accounting) } } func TestGroupedMixedSourcesShareCLIRecordLimit(t *testing.T) { filesystem := NewFilesystem("bundle", fstest.MapFS{"main.yml": &fstest.MapFile{Data: []byte("value: base\n")}}) inputs := make([]Input, maxImportSources-1) inputs[0] = Input{Source: Bytes("empty-bytes", []byte("{}\n"))} for index := 1; index < len(inputs); index++ { inputs[index] = Input{Source: Mapping("empty-map", map[string]any{})} } type config struct { Value string `yaml:"value"` } _, err := LoadLayered(Input{Source: filesystem.File("main.yml")}, testSecretBinding(), LayeredOptions[config]{ Underlays: []Underlay{{Inputs: inputs}}, Overrides: []Override{{Path: Keys("value"), Value: "cli", Flag: "--value"}}, }) loadError, ok := err.(*LoadError) if !ok || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != CodeOverrideLimitExceeded || len(loadError.Sources()) != maxImportSources { t.Fatalf("mixed source/CLI limit = %#v", err) } }