Files
ConfigKit/internal/engine/inheritance_identified_test.go

446 lines
20 KiB
Go

package engine
import (
"strings"
"testing"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
func parseIdentifiedInheritanceSequence(t *testing.T, sourceID, input string) *document.Node {
t.Helper()
var indented strings.Builder
indented.WriteString("jobs:\n")
for _, line := range strings.Split(strings.TrimSuffix(input, "\n"), "\n") {
indented.WriteString(" ")
indented.WriteString(line)
indented.WriteByte('\n')
}
root := parseItemPolicyDocument(t, sourceID, indented.String())
sequence, found := documentAtPath(root, Keys("jobs"))
if !found || sequence.Kind != document.Sequence {
t.Fatalf("identified sequence missing from test document: %#v", root)
}
return sequence
}
func TestPreserveInheritanceIdentityIndexesCompoundFields(t *testing.T) {
t.Parallel()
mergedEntries := []document.Entry{
{Key: "before", Value: &document.Node{Kind: document.String, Text: "keep"}},
{Key: "id", KeyLocation: document.Location{SourceID: "parent", Line: 2}, ValueLocation: document.Location{SourceID: "parent", Line: 2}, Value: &document.Node{Kind: document.String, Text: "parent-id"}},
}
for index := 0; index < 64; index++ {
key := "setting-" + string(rune('a'+index))
mergedEntries = append(mergedEntries, document.Entry{Key: key, Value: &document.Node{Kind: document.String, Text: key}})
}
mergedEntries = append(mergedEntries,
document.Entry{Key: "a/b~c", KeyLocation: document.Location{SourceID: "parent", Line: 70}, ValueLocation: document.Location{SourceID: "parent", Line: 70}, Value: &document.Node{Kind: document.String, Text: "parent-literal"}},
document.Entry{Key: "missing", Value: &document.Node{Kind: document.String, Text: "retained"}},
document.Entry{Key: "after", Value: &document.Node{Kind: document.String, Text: "keep"}},
)
merged := &document.Node{Kind: document.Mapping, Mapping: mergedEntries}
child := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{
{Key: "a/b~c", KeyLocation: document.Location{SourceID: "child", Line: 9, Column: 3}, ValueLocation: document.Location{SourceID: "child", Line: 9, Column: 10}, Value: &document.Node{Kind: document.String, Text: "child-literal", Location: document.Location{SourceID: "child", Line: 9, Column: 10}}},
{Key: "id", KeyLocation: document.Location{SourceID: "child", Line: 3, Column: 3}, ValueLocation: document.Location{SourceID: "child", Line: 3, Column: 7}, Value: &document.Node{Kind: document.String, Text: "child-id", Location: document.Location{SourceID: "child", Line: 3, Column: 7}}},
}}
beforeKeys := make([]string, len(merged.Mapping))
for index, entry := range merged.Mapping {
beforeKeys[index] = entry.Key
}
untouched := merged.Mapping[2].Value
preserveInheritanceIdentityFields(merged, child, []string{"id", "a/b~c", "missing"})
for index, entry := range merged.Mapping {
if entry.Key != beforeKeys[index] {
t.Fatalf("mapping order changed at %d: %q, want %q", index, entry.Key, beforeKeys[index])
}
}
for _, want := range []struct {
key string
text string
keyLine int
keyColumn int
valueLine int
valueColumn int
}{
{key: "id", text: "child-id", keyLine: 3, keyColumn: 3, valueLine: 3, valueColumn: 7},
{key: "a/b~c", text: "child-literal", keyLine: 9, keyColumn: 3, valueLine: 9, valueColumn: 10},
} {
var got document.Entry
for _, entry := range merged.Mapping {
if entry.Key == want.key {
got = entry
break
}
}
if got.Value == nil || got.Value.Text != want.text || got.KeyLocation.SourceID != "child" || got.KeyLocation.Line != want.keyLine || got.KeyLocation.Column != want.keyColumn ||
got.ValueLocation.SourceID != "child" || got.ValueLocation.Line != want.valueLine || got.ValueLocation.Column != want.valueColumn ||
got.Value.Location.SourceID != "child" || got.Value.Location.Line != want.valueLine || got.Value.Location.Column != want.valueColumn {
t.Fatalf("restored identity %q = %#v", want.key, got)
}
if got.Value == child.Mapping[0].Value || (want.key == "id" && got.Value == child.Mapping[1].Value) {
t.Fatalf("restored identity %q borrows its child value", want.key)
}
}
if merged.Mapping[2].Value != untouched {
t.Fatal("nonidentity sibling was replaced")
}
for _, entry := range merged.Mapping {
if entry.Key == "missing" && entry.Value.Text != "retained" {
t.Fatalf("identity absent from child was changed: %#v", entry)
}
}
child.Mapping[0].Value.Text = "mutated"
for _, entry := range merged.Mapping {
if entry.Key == "a/b~c" && entry.Value.Text != "child-literal" {
t.Fatal("restored identity value aliases the child template")
}
}
}
func TestIdentifiedInheritanceGraphSchedulesForwardReferencesInPlace(t *testing.T) {
sequence := parseIdentifiedInheritanceSequence(t, "identified-graph", `- id: 2
tenant: acme
extends: {id: 1.0, tenant: acme}
- id: 1e0
tenant: acme
extends: {id: 0, tenant: acme}
- id: 0
tenant: acme
- id: 3
tenant: acme
extends: null
`)
path := Keys("jobs")
graph, diagnostics := analyzeIdentifiedInheritanceGraph(sequence, path, []string{"tenant", "id"})
if len(diagnostics) != 0 {
t.Fatalf("analyze identified graph: %#v", diagnostics)
}
if len(graph.edges) != 2 {
t.Fatalf("edges = %#v, want the two links in the inheritance chain", graph.edges)
}
order, diagnostics := graph.schedule()
if len(diagnostics) != 0 {
t.Fatalf("schedule identified graph: %#v", diagnostics)
}
if len(order) != 4 || order[0] != 2 || order[1] != 1 || order[2] != 0 {
t.Fatalf("schedule = %v, want the forward parent chain before its children", order)
}
if graph.nodes[0].path.String() != "/jobs/0" || graph.nodes[0].item != sequence.Sequence[0] ||
!graph.nodes[3].cancelled || len(graph.nodes[3].directivePath.segments) == 0 {
t.Fatalf("graph nodes changed authored positions or cancellation: %#v", graph.nodes)
}
if _, found := graph.byIdentity[graph.nodes[1].tuple]; !found {
t.Fatal("identity index does not contain the parent record")
}
}
func TestIdentifiedInheritanceScopeAdapterReusesTemplateExecution(t *testing.T) {
sequence := parseIdentifiedInheritanceSequence(t, "identified-scope", `- id: child-a
tenant: team-a
extends: {id: parent-a, tenant: team-a}
settings: {child: a}
- id: parent-a
tenant: team-a
settings: {parent: a}
- id: child-b
tenant: team-b
extends: {id: parent-b, tenant: team-b}
settings: {child: b}
- id: parent-b
tenant: team-b
settings: {parent: b}
`)
path := Keys("jobs")
graph, diagnostics := analyzeIdentifiedInheritanceGraph(sequence, path, []string{"tenant", "id"})
if len(diagnostics) != 0 {
t.Fatalf("analyze identified graph: %#v", diagnostics)
}
policies := collectionPolicyScope{}
policies.set(path.Index(1).Key("steps"), collectionPolicy{
identityFields: []string{"step-id"}, identityPresent: true,
})
result, diagnostics := applyIdentifiedInheritanceTemplates(graph, defaultProjectionLimits(), nil, policies, sequencePolicy{})
if len(diagnostics) != 0 {
t.Fatalf("apply identified templates: %#v", diagnostics)
}
if got := len(result.root.Sequence); got != 4 {
t.Fatalf("result sequence length = %d, want 4", got)
}
for index, want := range []string{"child-a", "parent-a", "child-b", "parent-b"} {
id, found := documentAtPath(result.root.Sequence[index], Path{}.Key("id"))
if !found || id.Text != want {
t.Fatalf("result identity at index %d = %#v, found %t; want %q", index, id, found, want)
}
}
for index, want := range []string{"a", "b"} {
value, found := documentAtPath(result.root.Sequence[index*2], Path{}.Key("settings").Key("parent"))
if !found || value.Text != want {
t.Fatalf("inherited value at record %d = %#v, found %t; want %q", index*2, value, found, want)
}
}
if len(result.transfers) != 2 || result.transfers[0].from.String() != "/jobs/1" || result.transfers[0].to.String() != "/jobs/0" ||
result.transfers[1].from.String() != "/jobs/3" || result.transfers[1].to.String() != "/jobs/2" {
t.Fatalf("record transfers = %#v, want independent transfers at authored positions", result.transfers)
}
if len(result.controls) != 2 || result.controls[0].path.String() != "/jobs/0/extends" || result.controls[1].path.String() != "/jobs/2/extends" {
t.Fatalf("relationship controls = %#v, want paths from child positions", result.controls)
}
if len(result.operations) != 4 || result.operations[1].transfer == nil || result.operations[1].transfer.to.String() != "/jobs/0" ||
result.operations[3].transfer == nil || result.operations[3].transfer.to.String() != "/jobs/2" {
t.Fatalf("relationship history operations = %#v, want transfer evidence at child positions", result.operations)
}
if _, found := result.origins[path.Index(0).Key("id").String()]; !found {
t.Fatal("resolved sequence output did not retain the child identity origin path")
}
projectedPolicy, found := result.collectionPolicies.at(path.Index(0).Key("steps"))
if !found || !projectedPolicy.identityPresent || len(projectedPolicy.identityFields) != 1 || projectedPolicy.identityFields[0] != "step-id" {
t.Fatalf("inherited nested collection policy = %#v, found %t", projectedPolicy, found)
}
adapter := identifiedSequenceInheritanceScope{graph: graph, policies: policies}
fields := adapter.templates()[0].identityFields
if len(fields) != 2 || fields[0] != "id" || fields[1] != "tenant" {
t.Fatalf("payload identity fields = %v, want [id tenant]", fields)
}
templates := adapter.templates()
parentPolicy, found := templates[1].collectionPolicies.at(Keys("steps"))
if !found || len(parentPolicy.identityFields) != 1 || parentPolicy.identityFields[0] != "step-id" {
t.Fatalf("parent nested policy = %#v, found %t", parentPolicy, found)
}
if _, found := templates[0].collectionPolicies.at(Keys("steps")); found {
t.Fatal("nested policy from one record leaked into a sibling template")
}
parentPolicy.identityFields[0] = "changed"
templates[1].collectionPolicies.set(Keys("steps"), parentPolicy)
reused, found := adapter.templates()[1].collectionPolicies.at(Keys("steps"))
if !found || len(reused.identityFields) != 1 || reused.identityFields[0] != "step-id" {
t.Fatal("reusing the adapter observed a mutation to a prior template snapshot")
}
if sequence.Sequence[0].Mapping[0].Value.Text != "child-a" {
t.Fatal("executing the sequence adapter mutated its source scope")
}
}
func TestIdentifiedInheritanceResolvesNestedCollectionsAtEachDestination(t *testing.T) {
root := parseItemPolicyDocument(t, "identified-nested", `jobs:
- id: child
extends: {id: parent}
- id: parent
extends: {id: base}
steps:
- id: child-step
extends: {id: parent-step}
value: child-value
- id: parent-step
value: parent-value
- id: base
steps:
- id: base-step
value: base-value
`)
identity := collectionPolicy{identityFields: []string{"id"}, identityPresent: true}
policies := collectionPolicyScope{}
policies.set(Keys("jobs"), identity)
for index := 0; index < 3; index++ {
policies.set(Keys("jobs").Index(index).Key("steps"), identity)
}
expanded, diagnostics := expandInheritanceWithCollectionRules(root, nil, policies, sequencePolicy{}, defaultProjectionLimits())
if len(diagnostics) != 0 {
t.Fatalf("expand nested identified inheritance: %#v", diagnostics)
}
steps, found := documentAtPath(expanded.root, Keys("jobs").Index(0).Key("steps"))
if !found || steps.Kind != document.Sequence || len(steps.Sequence) != 3 {
t.Fatalf("child nested steps = %#v, found %t", steps, found)
}
for index, want := range []string{"base-step", "child-step", "parent-step"} {
id, exists := documentAtPath(steps.Sequence[index], Keys("id"))
if !exists || id.Text != want {
t.Fatalf("child step %d identity = %#v, found %t; want %q", index, id, exists, want)
}
}
value, found := documentAtPath(steps.Sequence[1], Keys("value"))
if !found || value.Text != "child-value" {
t.Fatalf("nested record inheritance = %#v, found %t", value, found)
}
if containsInheritanceDirective(expanded.root) {
t.Fatal("a nested record relationship remained after destination-local expansion")
}
childIdentity, found := expanded.origins["/jobs/0/id"]
if !found || len(childIdentity.Locations) == 0 || childIdentity.Locations[0].SourceID != "identified-nested" {
t.Fatalf("child identity origin = %#v, found %t", childIdentity, found)
}
if _, found := expanded.origins["/jobs/0/steps/1/value"]; !found {
t.Fatal("nested inherited value did not retain its destination origin")
}
}
func TestIdentifiedInheritanceRespectsOutputNodeBudget(t *testing.T) {
root := parseItemPolicyDocument(t, "identified-budget", `jobs:
- id: child
extends: {id: parent}
- id: parent
value: inherited
`)
policies := collectionPolicyScope{}
policies.set(Keys("jobs"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true})
baseline, diagnostics := expandInheritanceWithCollectionRules(root, nil, policies, sequencePolicy{}, defaultProjectionLimits())
if len(diagnostics) != 0 {
t.Fatalf("unbounded identified inheritance: %#v", diagnostics)
}
outputNodes, within := inheritanceLowerBoundCount(baseline.root, -1)
if !within || outputNodes < 2 {
t.Fatalf("identified output node count = %d, within %t", outputNodes, within)
}
limits := defaultProjectionLimits()
limits.nodes = outputNodes
if result, diagnostics := expandInheritanceWithCollectionRules(root, nil, policies, sequencePolicy{}, limits); result == nil || len(diagnostics) != 0 {
t.Fatalf("at output-node boundary: result = %#v, diagnostics = %#v", result, diagnostics)
}
limits.nodes = outputNodes - 1
if result, diagnostics := expandInheritanceWithCollectionRules(root, nil, policies, sequencePolicy{}, limits); result != nil ||
len(diagnostics) != 1 || diagnostics[0].Code != CodeSchemaProjectionLimit {
t.Fatalf("below output-node boundary: result = %#v, diagnostics = %#v", result, diagnostics)
}
}
func TestIdentifiedInheritanceRequiresChildIdentityBeforeParentLookup(t *testing.T) {
sequence := parseIdentifiedInheritanceSequence(t, "missing-child-field", `- id: child
extends: {id: parent, tenant: acme}
- id: parent
tenant: acme
`)
_, diagnostics := analyzeIdentifiedInheritanceGraph(sequence, Keys("jobs"), []string{"id", "tenant"})
if len(diagnostics) != 1 || diagnostics[0].Code != CodeSequenceInvalid || diagnostics[0].Path.String() != "/jobs/0/tenant" {
t.Fatalf("missing child identity diagnostics = %#v", diagnostics)
}
}
func TestIdentifiedInheritanceRejectsMalformedAndMissingReferences(t *testing.T) {
tests := []struct {
name string
value string
wantCode Code
}{
{name: "string shorthand", value: "extends: parent\n", wantCode: CodeInheritanceInvalid},
{name: "sequence shorthand", value: "extends: [{id: parent, tenant: acme}]\n", wantCode: CodeInheritanceInvalid},
{name: "missing component", value: "extends: {id: parent}\n", wantCode: CodeInheritanceInvalid},
{name: "extra component", value: "extends: {id: parent, tenant: acme, extra: value}\n", wantCode: CodeInheritanceInvalid},
{name: "mapping component", value: "extends: {id: {nested: value}, tenant: acme}\n", wantCode: CodeInheritanceInvalid},
{name: "missing parent", value: "extends: {id: missing, tenant: acme}\n", wantCode: CodeInheritanceMissing},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
sequence := parseIdentifiedInheritanceSequence(t, "bad-reference", "- id: child\n tenant: acme\n "+test.value)
_, diagnostics := analyzeIdentifiedInheritanceGraph(sequence, Keys("jobs"), []string{"id", "tenant"})
if len(diagnostics) != 1 || diagnostics[0].Code != test.wantCode || diagnostics[0].Path.String() != "/jobs/0/extends" && diagnostics[0].Code != CodeInheritanceInvalid {
t.Fatalf("diagnostics = %#v", diagnostics)
}
if diagnostics[0].Code == CodeInheritanceMissing && diagnostics[0].Path.String() != "/jobs/0/extends" {
t.Fatalf("missing-parent path = %s", diagnostics[0].Path)
}
})
}
}
func TestIdentifiedInheritanceGraphFindsSelfAndLongerCycles(t *testing.T) {
for _, input := range []string{
`- id: self
tenant: acme
extends: {id: self, tenant: acme}
`,
`- id: alpha
tenant: acme
extends: {id: beta, tenant: acme}
- id: beta
tenant: acme
extends: {id: gamma, tenant: acme}
- id: gamma
tenant: acme
extends: {id: alpha, tenant: acme}
`,
} {
graph, diagnostics := analyzeIdentifiedInheritanceGraph(parseIdentifiedInheritanceSequence(t, "cycle", input), Keys("jobs"), []string{"id", "tenant"})
if len(diagnostics) != 0 {
t.Fatalf("analyze cycle graph: %#v", diagnostics)
}
_, diagnostics = graph.schedule()
if len(diagnostics) != 1 || diagnostics[0].Code != CodeInheritanceCycle || diagnostics[0].Path.String() == "" {
t.Fatalf("cycle diagnostics = %#v", diagnostics)
}
if diagnostics[0].Primary == nil || diagnostics[0].Primary.SourceID != "cycle" {
t.Fatalf("cycle primary location = %#v", diagnostics[0].Primary)
}
}
}
func TestIdentifiedInheritanceScopesCannotReferenceOtherCollections(t *testing.T) {
other := parseIdentifiedInheritanceSequence(t, "other-collection", `- id: parent
tenant: acme
`)
_, diagnostics := analyzeIdentifiedInheritanceGraph(other, Keys("other"), []string{"id", "tenant"})
if len(diagnostics) != 0 {
t.Fatal(diagnostics)
}
first := parseIdentifiedInheritanceSequence(t, "first-collection", `- id: child
tenant: acme
extends: {id: parent, tenant: acme}
`)
_, diagnostics = analyzeIdentifiedInheritanceGraph(first, Keys("first"), []string{"id", "tenant"})
if len(diagnostics) != 1 || diagnostics[0].Code != CodeInheritanceMissing {
t.Fatalf("cross-collection reference diagnostics = %#v", diagnostics)
}
}
func TestIdentifiedInheritanceAcceptsStrictlyComposedParentReference(t *testing.T) {
left := parseCompositionDocument(t, "reference-left", "extends: {id: parent}\n")
right := parseCompositionDocument(t, "reference-right", "extends: {tenant: acme}\n")
reference, diagnostics := composeDocuments(left, right)
if len(diagnostics) != 0 {
t.Fatalf("compose disjoint reference helpers: %#v", diagnostics)
}
child, diagnostics := composeDocuments(parseCompositionDocument(t, "child-fields", "id: child\ntenant: acme\n"), reference.root)
if len(diagnostics) != 0 {
t.Fatalf("compose child fields and reference: %#v", diagnostics)
}
parent := parseCompositionDocument(t, "parent", "id: parent\ntenant: acme\n")
sequence := &document.Node{Kind: document.Sequence, Sequence: []*document.Node{child.root, parent}}
graph, diagnostics := analyzeIdentifiedInheritanceGraph(sequence, Keys("jobs"), []string{"id", "tenant"})
if len(diagnostics) != 0 {
t.Fatalf("analyze composed reference: %#v", diagnostics)
}
if len(graph.edges) != 1 || graph.edges[0] != (dependencyEdge{prerequisite: 1, dependent: 0}) {
t.Fatalf("composed reference edge = %#v", graph.edges)
}
_, diagnostics = composeDocuments(
parseCompositionDocument(t, "overlap-a", "extends: {id: first}\n"),
parseCompositionDocument(t, "overlap-b", "extends: {id: second}\n"),
)
if len(diagnostics) != 1 || diagnostics[0].Code != CodeComposeConflict {
t.Fatalf("overlapping reference fields should conflict during strict composition: %#v", diagnostics)
}
}
func TestIdentifiedInheritanceValidatesOnlySurvivingRelationships(t *testing.T) {
lower := parseIdentifiedInheritanceSequence(t, "superseded", `- id: shared
extends: {id: absent, tenant: acme}
`)
higher := parseIdentifiedInheritanceSequence(t, "cancelled", `- id: shared
extends: null
`)
resolution := collectionPolicyResolution{
operation: sequenceModeUpsert, effectiveIdentityFields: []string{"id"},
}
combined, diagnostics := combineSequence(lower, higher, resolution, sequenceCombinationContext{destination: Keys("jobs")})
if len(diagnostics) != 0 {
t.Fatalf("combine surviving reference: %#v", diagnostics)
}
graph, diagnostics := analyzeIdentifiedInheritanceGraph(combined.value, Keys("jobs"), []string{"id"})
if len(diagnostics) != 0 || len(graph.edges) != 0 || !graph.nodes[0].cancelled {
t.Fatalf("superseded missing reference was validated: graph=%#v diagnostics=%#v", graph, diagnostics)
}
}