Resolve sibling inheritance with memoized parent templates
This commit is contained in:
141
internal/engine/inheritance_test.go
Normal file
141
internal/engine/inheritance_test.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||
)
|
||||
|
||||
func TestSiblingInheritanceResolvesReferencesAndKeepsInputs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name, input string
|
||||
path Path
|
||||
text string
|
||||
transfers int
|
||||
controls int
|
||||
}{
|
||||
{
|
||||
name: "forward reference", input: "child: {extends: parent, value: own}\nparent: {value: parent, kept: preserved}\n",
|
||||
path: Keys("child", "kept"), text: "preserved", transfers: 1, controls: 1,
|
||||
},
|
||||
{
|
||||
name: "backward reference", input: "parent: {value: parent, kept: preserved}\nchild: {extends: parent, value: own}\n",
|
||||
path: Keys("child", "value"), text: "own", transfers: 1, controls: 1,
|
||||
},
|
||||
{
|
||||
name: "chain and shared parent", input: "last: {extends: middle, enabled: false}\nfirst: {value: original}\nmiddle: {extends: first, extra: middle}\nother: {extends: first}\n",
|
||||
path: Keys("last", "value"), text: "original", transfers: 3, controls: 3,
|
||||
},
|
||||
{
|
||||
name: "exact empty sibling name", input: "child: {extends: \"\"}\n\"\": {value: empty}\n",
|
||||
path: Keys("child", "value"), text: "empty", transfers: 1, controls: 1,
|
||||
},
|
||||
{
|
||||
name: "exact unusual sibling name", input: "child: {extends: \"a/b~c\"}\n\"a/b~c\": {value: unusual}\n",
|
||||
path: Keys("child", "value"), text: "unusual", transfers: 1, controls: 1,
|
||||
},
|
||||
{
|
||||
name: "null cancellation", input: "child: {extends: null, value: own}\nparent: {value: parent}\n",
|
||||
path: Keys("child", "value"), text: "own", controls: 1,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
input := parseCompositionDocument(t, "source-1", test.input)
|
||||
before := document.Clone(input)
|
||||
result, diagnostics := expandSiblingDefinitions(input, Path{})
|
||||
if len(diagnostics) != 0 {
|
||||
t.Fatalf("diagnostics = %#v", diagnostics)
|
||||
}
|
||||
if len(result.transfers) != test.transfers || len(result.controls) != test.controls {
|
||||
t.Fatalf("evidence = %#v, %#v", result.transfers, result.controls)
|
||||
}
|
||||
node, found := documentAtPath(result.root, test.path)
|
||||
if !found || node.Text != test.text {
|
||||
t.Fatalf("%s = %#v, %v", test.path.String(), node, found)
|
||||
}
|
||||
if _, found := documentAtPath(result.root, Keys("child", inheritanceKey)); found {
|
||||
t.Fatal("child directive remained in expanded payload")
|
||||
}
|
||||
if !reflect.DeepEqual(input, before) {
|
||||
t.Fatal("resolver changed input")
|
||||
}
|
||||
node.Text = "changed"
|
||||
if !reflect.DeepEqual(input, before) {
|
||||
t.Fatal("expanded payload aliases input")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSiblingInheritanceTracksTransferAndIndependentChildren(t *testing.T) {
|
||||
input := parseCompositionDocument(t, "source-1", "first: {value: parent}\nsecond: {extends: first}\nthird: {extends: first}\n")
|
||||
result, diagnostics := expandSiblingDefinitions(input, Keys("catalog"))
|
||||
if len(diagnostics) != 0 {
|
||||
t.Fatal(diagnostics)
|
||||
}
|
||||
if len(result.transfers) != 2 || result.transfers[0].from.String() != "/catalog/first" ||
|
||||
result.transfers[0].to.String() != "/catalog/second" ||
|
||||
result.transfers[0].reference.String() != "/catalog/second/extends" ||
|
||||
len(result.transfers[0].locations) != 1 || result.transfers[0].locations[0].SourceID != "source-1" {
|
||||
t.Fatalf("transfers = %#v", result.transfers)
|
||||
}
|
||||
second, _ := documentAtPath(result.root, Keys("second", "value"))
|
||||
third, _ := documentAtPath(result.root, Keys("third", "value"))
|
||||
second.Text = "changed"
|
||||
if third.Text != "parent" {
|
||||
t.Fatal("siblings share mutable expanded values")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSiblingInheritanceRejectsInvalidRelationships(t *testing.T) {
|
||||
tests := []struct {
|
||||
name, input, path string
|
||||
code Code
|
||||
related bool
|
||||
}{
|
||||
{"missing", "child: {extends: private-reference-canary}\n", "/child/extends", CodeInheritanceMissing, false},
|
||||
{"nonmapping parent", "child: {extends: parent}\nparent: null\n", "/child/extends", CodeInheritanceInvalid, true},
|
||||
{"boolean reference", "child: {extends: true}\n", "/child/extends", CodeInheritanceInvalid, false},
|
||||
{"sequence reference", "child: {extends: [parent]}\n", "/child/extends", CodeInheritanceInvalid, false},
|
||||
{"self reference", "child: {extends: child}\n", "/child/extends", CodeInheritanceCycle, true},
|
||||
{"longer cycle", "a: {extends: b}\nb: {extends: c}\nc: {extends: a}\n", "/c/extends", CodeInheritanceCycle, true},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
input := parseCompositionDocument(t, "source-1", test.input)
|
||||
result, diagnostics := expandSiblingDefinitions(input, Path{})
|
||||
if result != nil || len(diagnostics) != 1 {
|
||||
t.Fatalf("result and diagnostics = %#v, %#v", result, diagnostics)
|
||||
}
|
||||
problem := diagnostics[0]
|
||||
if problem.Code != test.code || problem.Path.String() != test.path || problem.Primary == nil ||
|
||||
problem.Primary.SourceID != "source-1" || problem.Primary.Line < 1 ||
|
||||
(len(problem.Related) != 0) != test.related || strings.Contains(problem.Message, "private-reference-canary") {
|
||||
t.Fatalf("diagnostic = %#v", problem)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSiblingInheritanceDetectsDeepCycleWithoutRecursiveDependencyWalk(t *testing.T) {
|
||||
const count = 2048
|
||||
entries := make([]document.Entry, count)
|
||||
for index := range entries {
|
||||
location := document.Location{SourceID: "source-1", Line: index + 1, Column: 1}
|
||||
parent := strconv.Itoa((index + 1) % count)
|
||||
entries[index] = document.Entry{
|
||||
Key: strconv.Itoa(index), Value: &document.Node{Kind: document.Mapping, Location: location,
|
||||
Mapping: []document.Entry{{Key: inheritanceKey, Value: &document.Node{
|
||||
Kind: document.String, Text: parent, Location: location,
|
||||
}}}},
|
||||
}
|
||||
}
|
||||
result, diagnostics := expandSiblingDefinitions(&document.Node{Kind: document.Mapping, Mapping: entries}, Path{})
|
||||
if result != nil || len(diagnostics) != 1 || diagnostics[0].Code != CodeInheritanceCycle {
|
||||
t.Fatalf("deep cycle result = %#v, diagnostics = %#v", result, diagnostics)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user