256 lines
9.9 KiB
Go
256 lines
9.9 KiB
Go
package engine
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
)
|
|
|
|
func TestInheritanceGraphSchedulesDirectRelationshipsAndAppliesTemplates(t *testing.T) {
|
|
root := parseCompositionDocument(t, "schedule", `
|
|
child:
|
|
extends: base
|
|
own: child-value
|
|
base:
|
|
inherited: base-value
|
|
cleared:
|
|
extends: null
|
|
own: cleared-value
|
|
scalar: kept
|
|
`)
|
|
original := document.Clone(root)
|
|
graph, diagnostics := analyzeInheritanceGraph(root, Path{})
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("analyze diagnostics = %#v", diagnostics)
|
|
}
|
|
if len(graph.edges) != 1 {
|
|
t.Fatalf("edges = %#v, want one direct parent edge", graph.edges)
|
|
}
|
|
order, diagnostics := graph.schedule()
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("schedule diagnostics = %#v", diagnostics)
|
|
}
|
|
orderNames := make([]string, len(order))
|
|
for i, index := range order {
|
|
orderNames[i] = graph.nodes[index].name
|
|
}
|
|
if want := []string{"base", "child", "cleared"}; !reflect.DeepEqual(orderNames, want) {
|
|
t.Fatalf("mapping schedule = %v, want %v", orderNames, want)
|
|
}
|
|
|
|
result, diagnostics := applyInheritanceTemplates(graph, order, defaultProjectionLimits(), nil)
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("apply diagnostics = %#v", diagnostics)
|
|
}
|
|
if !reflect.DeepEqual(root, original) {
|
|
t.Fatal("applying templates mutated the input mapping")
|
|
}
|
|
if len(result.controls) != 2 || result.controls[0].path.String() != "/child/extends" || result.controls[0].cancelled ||
|
|
result.controls[1].path.String() != "/cleared/extends" || !result.controls[1].cancelled {
|
|
t.Fatalf("controls = %#v, want scheduled ordinary and null relationships", result.controls)
|
|
}
|
|
if len(result.transfers) != 1 || result.transfers[0].from.String() != "/base" || result.transfers[0].to.String() != "/child" {
|
|
t.Fatalf("transfers = %#v, want the direct parent transfer", result.transfers)
|
|
}
|
|
if got, want := result.root.Mapping[0].Key, "child"; got != want {
|
|
t.Fatalf("root order begins with %q, want %q", got, want)
|
|
}
|
|
child := result.root.Mapping[0].Value
|
|
inherited, found := documentAtPath(child, Path{}.Key("inherited"))
|
|
if !found || inherited.Text != "base-value" {
|
|
t.Fatalf("inherited value = %#v, found %t", inherited, found)
|
|
}
|
|
childValue, found := documentAtPath(child, Path{}.Key("own"))
|
|
if !found || childValue.Text != "child-value" {
|
|
t.Fatalf("child value = %#v, found %t", childValue, found)
|
|
}
|
|
cleared := result.root.Mapping[2].Value
|
|
clearedValue, found := documentAtPath(cleared, Path{}.Key("own"))
|
|
if !found || clearedValue.Text != "cleared-value" {
|
|
t.Fatalf("null relationship value = %#v, found %t", clearedValue, found)
|
|
}
|
|
if _, found := inheritanceDirective(cleared); found {
|
|
t.Fatal("null relationship directive remained in the template")
|
|
}
|
|
|
|
child.Mapping[0].Value.Text = "changed-result"
|
|
inherited, found = documentAtPath(root.Mapping[1].Value, Path{}.Key("inherited"))
|
|
if !found || inherited.Text != "base-value" {
|
|
t.Fatalf("mutating result changed input: inherited value = %#v, found %t", inherited, found)
|
|
}
|
|
}
|
|
|
|
func TestInheritanceGraphReportsCycleAtAuthoredRelationship(t *testing.T) {
|
|
root := parseCompositionDocument(t, "cycle-source", `alpha:
|
|
extends: beta
|
|
beta:
|
|
extends: alpha
|
|
`)
|
|
graph, diagnostics := analyzeInheritanceGraph(root, Path{})
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("analyze diagnostics = %#v", diagnostics)
|
|
}
|
|
_, diagnostics = graph.schedule()
|
|
if len(diagnostics) != 1 || diagnostics[0].Code != CodeInheritanceCycle {
|
|
t.Fatalf("cycle diagnostics = %#v", diagnostics)
|
|
}
|
|
diagnostic := diagnostics[0]
|
|
if diagnostic.Primary == nil || diagnostic.Primary.SourceID != "cycle-source" {
|
|
t.Fatalf("cycle primary = %#v, want authored source", diagnostic.Primary)
|
|
}
|
|
if diagnostic.Primary.Line != 4 {
|
|
t.Fatalf("cycle primary line = %d, want line 4", diagnostic.Primary.Line)
|
|
}
|
|
if len(diagnostic.Related) != 1 || diagnostic.Related[0].SourceID != "cycle-source" {
|
|
t.Fatalf("cycle related sites = %#v, want the referenced mapping", diagnostic.Related)
|
|
}
|
|
if diagnostic.Message != "sibling inheritance contains a cycle" {
|
|
t.Fatalf("cycle message exposes unexpected details: %q", diagnostic.Message)
|
|
}
|
|
}
|
|
|
|
func TestInheritanceTransportsReusableCollectionPolicyTemplates(t *testing.T) {
|
|
root := parseItemPolicyDocument(t, "templates", `base:
|
|
prompts:
|
|
- id: shared
|
|
children:
|
|
derived:
|
|
extends: nested-base
|
|
own: destination-local
|
|
nested-base:
|
|
inherited: source-value
|
|
first:
|
|
extends: base
|
|
second:
|
|
extends: base
|
|
`)
|
|
|
|
policyLocation := Location{SourceID: "base-resource", Line: 2, Column: 3}
|
|
nestedLocation := Location{SourceID: "nested-resource", Line: 5, Column: 11}
|
|
nested := collectionPolicy{identityFields: []string{"key"}, identityPresent: true, identityLocation: &nestedLocation}
|
|
recordScope := collectionPolicyScope{}
|
|
recordScope.set(Keys("children", "prompts"), nested)
|
|
basePolicy := collectionPolicy{identityFields: []string{"id"}, identityPresent: true, identityLocation: &policyLocation}
|
|
basePolicy.setRecordPolicyAt(0, collectionRecordPolicy{owner: newCollectionRecordPolicy().owner, scope: recordScope})
|
|
policies := collectionPolicyScope{}
|
|
policies.set(Keys("base", "prompts"), basePolicy)
|
|
|
|
got, diagnostics := expandSiblingDefinitionsWithPolicies(root, Path{}, defaultProjectionLimits(), nil, policies)
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("expand sibling templates: %#v", diagnostics)
|
|
}
|
|
withoutPolicies, diagnostics := expandSiblingDefinitionsWithOrigins(root, Path{}, defaultProjectionLimits(), nil)
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("expand sibling templates without policy: %#v", diagnostics)
|
|
}
|
|
if !reflect.DeepEqual(got.root, withoutPolicies.root) {
|
|
t.Fatalf("policy transport changed inherited values:\n got %#v\nwant %#v", got.root, withoutPolicies.root)
|
|
}
|
|
var transfers []inheritanceOperation
|
|
for _, operation := range got.operations {
|
|
if operation.transfer != nil {
|
|
transfers = append(transfers, operation)
|
|
}
|
|
}
|
|
if len(transfers) != 2 {
|
|
t.Fatalf("transfer operations = %d, want two destination transfers", len(transfers))
|
|
}
|
|
|
|
for index, destination := range []string{"/first", "/second"} {
|
|
operation := transfers[index]
|
|
if operation.transfer == nil || len(operation.transfer.projectedPolicies) != 2 {
|
|
t.Fatalf("transfer %d projected policies = %#v, want collection and record-local policy", index, operation.transfer)
|
|
}
|
|
projected := operation.transfer.projectedPolicies
|
|
if projected[0].path.String() != destination+"/prompts" || projected[1].path.String() != destination+"/prompts/0/children/prompts" {
|
|
t.Fatalf("transfer %d policy paths = %q, %q", index, projected[0].path, projected[1].path)
|
|
}
|
|
if got := projected[0].policy.identityLocation; got == nil || *got != policyLocation {
|
|
t.Fatalf("transfer %d collection declaration location = %#v", index, got)
|
|
}
|
|
if got := projected[1].policy.identityLocation; got == nil || *got != nestedLocation {
|
|
t.Fatalf("transfer %d record-local declaration location = %#v", index, got)
|
|
}
|
|
if len(operation.templatePolicies.collections) != 0 {
|
|
t.Fatalf("transfer %d unexpectedly copied parent YAML policy onto child template", index)
|
|
}
|
|
}
|
|
|
|
first, second := transfers[0].transfer.projectedPolicies[0].policy, transfers[1].transfer.projectedPolicies[0].policy
|
|
if first.recordOrder[0] == second.recordOrder[0] {
|
|
t.Fatal("destination policy templates share a mutable record owner")
|
|
}
|
|
first.identityFields[0] = "changed"
|
|
if second.identityFields[0] != "id" {
|
|
t.Fatalf("mutating one destination changed another: %v", second.identityFields)
|
|
}
|
|
if basePolicy.identityFields[0] != "id" {
|
|
t.Fatalf("mutating a destination changed the source template: %v", basePolicy.identityFields)
|
|
}
|
|
|
|
firstValue, found := documentAtPath(got.root.Mapping[1].Value, Keys("prompts").Index(0).Key("children").Key("derived").Key("extends"))
|
|
if !found || firstValue.Text != "nested-base" {
|
|
t.Fatalf("nested template instruction = %#v, found %t; want original instruction retained", firstValue, found)
|
|
}
|
|
}
|
|
|
|
func TestInheritanceExpansionInterleavesUnequalBranches(t *testing.T) {
|
|
root := parseCompositionDocument(t, "branches", `a-grandchild:
|
|
extends: z-child
|
|
own: grandchild
|
|
b-base:
|
|
base: b
|
|
c-base:
|
|
base: c
|
|
d-child:
|
|
extends: c-base
|
|
own: d
|
|
z-child:
|
|
extends: b-base
|
|
own: z
|
|
`)
|
|
result, diagnostics := expandInheritanceTree(root, defaultProjectionLimits())
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("expand diagnostics = %#v", diagnostics)
|
|
}
|
|
got := make([]string, len(result.transfers))
|
|
for i, transfer := range result.transfers {
|
|
got[i] = transfer.from.String() + " -> " + transfer.to.String()
|
|
}
|
|
want := []string{"/c-base -> /d-child", "/b-base -> /z-child", "/z-child -> /a-grandchild"}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("transfer order = %v, want %v", got, want)
|
|
}
|
|
if value, found := documentAtPath(result.root, Path{}.Key("a-grandchild").Key("base")); !found || value.Text != "b" {
|
|
t.Fatalf("grandchild inherited value = %#v, found %t", value, found)
|
|
}
|
|
if value, found := documentAtPath(result.root, Path{}.Key("d-child").Key("base")); !found || value.Text != "c" {
|
|
t.Fatalf("independent branch inherited value = %#v, found %t", value, found)
|
|
}
|
|
}
|
|
|
|
func TestInheritanceGraphValidatesRelationshipKinds(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
text string
|
|
code Code
|
|
}{
|
|
{name: "missing sibling", text: "child:\n extends: absent\n", code: CodeInheritanceMissing},
|
|
{name: "non-mapping sibling", text: "base: scalar\nchild:\n extends: base\n", code: CodeInheritanceInvalid},
|
|
{name: "invalid relationship", text: "child:\n extends: 3\n", code: CodeInheritanceInvalid},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
root := parseCompositionDocument(t, "invalid", test.text)
|
|
_, diagnostics := analyzeInheritanceGraph(root, Path{})
|
|
if len(diagnostics) != 1 || diagnostics[0].Code != test.code {
|
|
t.Fatalf("diagnostics = %#v, want code %q", diagnostics, test.code)
|
|
}
|
|
if diagnostics[0].Primary == nil || diagnostics[0].Primary.SourceID != "invalid" {
|
|
t.Fatalf("primary site = %#v, want authored declaration", diagnostics[0].Primary)
|
|
}
|
|
})
|
|
}
|
|
}
|