220 lines
7.3 KiB
Go
220 lines
7.3 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|