package engine import ( "reflect" "testing" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) func TestNestedInheritanceUsesEachDestinationScope(t *testing.T) { input := parseCompositionDocument(t, "source-1", `presets: base: settings: primary: {width: 10} secondary: {extends: primary} custom: extends: base settings: primary: {width: 20} `) before := document.Clone(input) result, diagnostics := expandInheritanceTree(input, defaultProjectionLimits()) if len(diagnostics) != 0 { t.Fatal(diagnostics) } for path, want := range map[string]string{ "/presets/base/settings/secondary/width": "10", "/presets/custom/settings/secondary/width": "20", } { parsed, _ := ParsePath(path) node, found := documentAtPath(result.root, parsed) if !found || node.Text != want { t.Errorf("%s = %#v, found %v", path, node, found) } } if len(result.transfers) != 3 || !reflect.DeepEqual(input, before) { t.Fatalf("transfers = %#v; input changed = %v", result.transfers, !reflect.DeepEqual(input, before)) } if node, found := documentAtPath(result.root, Keys("presets", "custom", "settings", "secondary", inheritanceKey)); found { t.Fatalf("nested directive survived: %#v", node) } } func TestNestedInheritanceSupportsChainsCancellationAndContainerReplacement(t *testing.T) { input := parseCompositionDocument(t, "source-1", `catalog: alpha: settings: first: {value: one} second: {extends: first} beta: extends: alpha settings: first: {value: two} gamma: extends: beta settings: first: {value: three} cancelled: extends: alpha settings: second: {extends: null, value: own} replaced: extends: alpha settings: none `) result, diagnostics := expandInheritanceTree(input, defaultProjectionLimits()) if len(diagnostics) != 0 { t.Fatal(diagnostics) } checks := map[string]string{ "/catalog/alpha/settings/second/value": "one", "/catalog/beta/settings/second/value": "two", "/catalog/gamma/settings/second/value": "three", "/catalog/cancelled/settings/second/value": "own", "/catalog/replaced/settings": "none", } for pointer, want := range checks { path, _ := ParsePath(pointer) node, found := documentAtPath(result.root, path) if !found || node.Text != want { t.Errorf("%s = %#v, found %v", pointer, node, found) } } if _, found := documentAtPath(result.root, Keys("catalog", "replaced", "settings", "second")); found { t.Fatal("replaced container retained an active child") } if _, found := documentAtPath(result.root, Keys("catalog", "cancelled", "settings", "second", inheritanceKey)); found { t.Fatal("cancelled nested directive survived") } } func TestNestedInheritanceCanReplaceAnInheritedRelationship(t *testing.T) { input := parseCompositionDocument(t, "source-1", `base: settings: first: {value: old} other: {value: new} selected: {extends: first} custom: extends: base settings: selected: {extends: other} `) result, diagnostics := expandInheritanceTree(input, defaultProjectionLimits()) if len(diagnostics) != 0 { t.Fatal(diagnostics) } base, found := documentAtPath(result.root, Keys("base", "settings", "selected", "value")) if !found || base.Text != "old" { t.Fatalf("base relationship = %#v, %v", base, found) } custom, found := documentAtPath(result.root, Keys("custom", "settings", "selected", "value")) if !found || custom.Text != "new" { t.Fatalf("replaced relationship = %#v, %v", custom, found) } } func TestNestedInheritanceRejectsDestinationCycleAndInvalidPositions(t *testing.T) { tests := []struct { name, input, path string code Code }{ { name: "destination-only cycle", input: `base: settings: a: {value: one} b: {extends: a} custom: extends: base settings: a: {extends: b} `, path: "/custom/settings/b/extends", code: CodeInheritanceCycle, }, {"root reference", "extends: anything\nentry: {value: one}\n", "/extends", CodeInheritanceInvalid}, {"list element reference", "items: [{extends: anything, value: one}]\n", "/items/0/extends", CodeInheritanceInvalid}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { input := parseCompositionDocument(t, "source-1", test.input) result, diagnostics := expandInheritanceTree(input, defaultProjectionLimits()) if result != nil || len(diagnostics) != 1 || diagnostics[0].Code != test.code || diagnostics[0].Path.String() != test.path || diagnostics[0].Primary == nil { t.Fatalf("result = %#v, diagnostics = %#v", result, diagnostics) } }) } } func TestNestedInheritanceResolvesScopesInsideSequences(t *testing.T) { input := parseCompositionDocument(t, "source-1", `items: - primary: {value: first} secondary: {extends: primary} - extends: null zeta: {extends: alpha} alpha: {value: second} `) result, diagnostics := expandInheritanceTree(input, defaultProjectionLimits()) if len(diagnostics) != 0 { t.Fatal(diagnostics) } first, found := documentAtPath(result.root, Keys("items").Index(0).Key("secondary").Key("value")) if !found || first.Text != "first" { t.Fatalf("first sequence scope = %#v, %v", first, found) } second, found := documentAtPath(result.root, Keys("items").Index(1).Key("zeta").Key("value")) if !found || second.Text != "second" { t.Fatalf("second sequence scope = %#v, %v", second, found) } if _, found := documentAtPath(result.root, Keys("items").Index(1).Key(inheritanceKey)); found { t.Fatal("sequence-element cancellation survived") } } func TestInheritanceExpansionHonorsOutputNodeAndDepthBudgets(t *testing.T) { input := parseCompositionDocument(t, "source-1", "base: {value: one}\nchild: {extends: base}\n") for _, test := range []struct { name string limits projectionLimits valid bool }{ {name: "at node boundary", limits: projectionLimits{nodes: 9, depth: 2}, valid: true}, {name: "beyond node boundary", limits: projectionLimits{nodes: 8, depth: 2}}, {name: "at depth boundary", limits: projectionLimits{nodes: 20, depth: 2}, valid: true}, {name: "beyond depth boundary", limits: projectionLimits{nodes: 20, depth: 1}}, } { t.Run(test.name, func(t *testing.T) { result, diagnostics := expandInheritanceTree(input, test.limits) if test.valid { if result == nil || len(diagnostics) != 0 { t.Fatalf("result = %#v, diagnostics = %#v", result, diagnostics) } } else if result != nil || len(diagnostics) != 1 || diagnostics[0].Code != CodeSchemaProjectionLimit { t.Fatalf("result = %#v, diagnostics = %#v", result, diagnostics) } }) } } func TestInheritanceBudgetIncludesOtherScopesAndSequenceItems(t *testing.T) { input := parseCompositionDocument(t, "source-1", `extends: null groups: - a: {x: one} b: {extends: a} - a: {x: two} b: {extends: a} unchanged: {x: three} `) // Root (1), groups key/list (2), two expanded scopes (18), and // unchanged key/mapping/key/value (4). Consumed controls cost nothing. for _, limit := range []int{24, 25, 26} { result, diagnostics := expandInheritanceTree(input, projectionLimits{nodes: limit, depth: 5}) if limit < 25 { if result != nil || len(diagnostics) != 1 || diagnostics[0].Code != CodeSchemaProjectionLimit { t.Fatalf("limit %d accepted or wrong error: %#v", limit, diagnostics) } } else if result == nil || len(diagnostics) != 0 { t.Fatalf("limit %d rejected: %#v", limit, diagnostics) } } }