Preserve sibling inheritance in configuration history and explanations
This commit is contained in:
271
internal/engine/inheritance_history_test.go
Normal file
271
internal/engine/inheritance_history_test.go
Normal file
@@ -0,0 +1,271 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||
)
|
||||
|
||||
func inheritanceInspectionForTest(t *testing.T, sources ...*document.Node) (*Inspection, *inheritanceScope) {
|
||||
t.Helper()
|
||||
composed, diagnostics := composeDocuments(sources...)
|
||||
if len(diagnostics) != 0 {
|
||||
t.Fatal(diagnostics)
|
||||
}
|
||||
return inheritanceInspectionFromComposition(t, composed)
|
||||
}
|
||||
|
||||
func inheritanceInspectionFromComposition(t *testing.T, composed *composedDocument) (*Inspection, *inheritanceScope) {
|
||||
t.Helper()
|
||||
expanded, diagnostics := expandInheritanceWithOrigins(composed.root, composed.origins, defaultProjectionLimits())
|
||||
if len(diagnostics) != 0 {
|
||||
t.Fatal(diagnostics)
|
||||
}
|
||||
baseline := &document.Node{Kind: document.Mapping}
|
||||
presence := make(map[string]inputPresence)
|
||||
collectPresence(expanded.root, Path{}, presence)
|
||||
decoded := decodedConfiguration{
|
||||
baseline: baseline, final: expanded.root, presence: presence, origins: expanded.origins,
|
||||
}
|
||||
metadata := metadataFromDecoded(decoded)
|
||||
applyInheritanceMetadata(&metadata, composed.root, composed.origins, composed.overlays, expanded)
|
||||
finalizeMetadataPaths(&metadata, expanded.root, composed.applications, baseline, composed.root)
|
||||
return &Inspection{
|
||||
metadata: metadata, baseline: baseline, input: expanded.root,
|
||||
inputOrigins: expanded.origins, final: expanded.root,
|
||||
applications: composed.applications, inheritance: expanded.operations,
|
||||
}, expanded
|
||||
}
|
||||
|
||||
func TestInheritanceHistoryFollowsFinalParentAfterSourceOverlay(t *testing.T) {
|
||||
base, diagnostics := composeDocuments(parseCompositionDocument(t, "source-1", "parent: {value: original}\nchild: {extends: parent}\n"))
|
||||
if len(diagnostics) != 0 {
|
||||
t.Fatal(diagnostics)
|
||||
}
|
||||
assembly := newAssemblyAccumulator(base)
|
||||
if diagnostics := assembly.applyOverlay(parseCompositionDocument(t, "source-2", "parent: {value: latest}\n"), nil, nil, LayerOverlay); len(diagnostics) != 0 {
|
||||
t.Fatal(diagnostics)
|
||||
}
|
||||
inspection, _ := inheritanceInspectionFromComposition(t, assembly.current)
|
||||
path := Keys("child", "value")
|
||||
explanation, found := inspection.Explain(path)
|
||||
if !found || explanation.Effective.YAML != "latest\n" ||
|
||||
len(explanation.History) == 0 || explanation.History[len(explanation.History)-1].Origin.Locations[0].SourceID != "source-2" {
|
||||
t.Fatalf("overlaid parent contribution = %#v, %v", explanation, found)
|
||||
}
|
||||
parentHistory := inspection.Metadata().History(Keys("parent", "value"))
|
||||
if len(parentHistory) < 2 || parentHistory[0].Origin.Layer != LayerBase ||
|
||||
parentHistory[len(parentHistory)-1].Origin.Layer != LayerOverlay {
|
||||
t.Fatalf("parent source history = %#v", parentHistory)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInheritanceHistoryPreservesCancellationAndSupersededDirective(t *testing.T) {
|
||||
base, diagnostics := composeDocuments(parseCompositionDocument(t, "source-1", "parent: {value: one}\nchild: {extends: missing, value: own}\n"))
|
||||
if len(diagnostics) != 0 {
|
||||
t.Fatal(diagnostics)
|
||||
}
|
||||
assembly := newAssemblyAccumulator(base)
|
||||
if diagnostics := assembly.applyOverlay(parseCompositionDocument(t, "source-2", "child: {extends: null}\n"), nil, nil, LayerOverlay); len(diagnostics) != 0 {
|
||||
t.Fatal(diagnostics)
|
||||
}
|
||||
inspection, _ := inheritanceInspectionFromComposition(t, assembly.current)
|
||||
control := Keys("child", inheritanceKey)
|
||||
history := inspection.Metadata().History(control)
|
||||
if len(history) < 3 || history[0].Origin.Locations[0].SourceID != "source-1" ||
|
||||
history[1].Origin.Locations[0].SourceID != "source-2" ||
|
||||
history[len(history)-1].Origin.Operation != ChangeInheritance {
|
||||
t.Fatalf("superseded and cancelled directive history = %#v", history)
|
||||
}
|
||||
explanation, found := inspection.Explain(control)
|
||||
if !found || explanation.Effective.Present || len(explanation.History) < 3 ||
|
||||
explanation.History[len(explanation.History)-1].After.Present {
|
||||
t.Fatalf("cancelled directive explanation = %#v, %v", explanation, found)
|
||||
}
|
||||
value, found := inspection.Explain(Keys("child", "value"))
|
||||
if !found || value.Effective.YAML != "own\n" {
|
||||
t.Fatalf("cancelled relationship changed own value = %#v, %v", value, found)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInheritanceHistoryProjectsNestedDestinationValues(t *testing.T) {
|
||||
inspection, _ := inheritanceInspectionForTest(t, parseCompositionDocument(t, "source-1", `base:
|
||||
settings:
|
||||
primary: {value: old}
|
||||
secondary: {extends: primary}
|
||||
custom:
|
||||
extends: base
|
||||
settings:
|
||||
primary: {value: new}
|
||||
`))
|
||||
path := Keys("custom", "settings", "secondary", "value")
|
||||
history := inspection.Metadata().History(path)
|
||||
structural := 0
|
||||
for _, change := range history {
|
||||
if change.Origin.Operation == ChangeInheritance {
|
||||
structural++
|
||||
}
|
||||
}
|
||||
if structural == 0 {
|
||||
t.Fatalf("nested destination history = %#v", history)
|
||||
}
|
||||
explanation, found := inspection.Explain(path)
|
||||
if !found || explanation.Effective.YAML != "new\n" ||
|
||||
len(explanation.Origin.Inheritance) == 0 ||
|
||||
explanation.Origin.Inheritance[len(explanation.Origin.Inheritance)-1].To.String() != path.String() {
|
||||
t.Fatalf("nested destination explanation = %#v, %v", explanation, found)
|
||||
}
|
||||
for _, change := range explanation.History {
|
||||
if change.Origin.Operation == ChangeDecoding {
|
||||
t.Fatalf("nested inheritance was attributed to decoding: %#v", explanation.History)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInheritanceHistoryShowsParentAndEqualChildContribution(t *testing.T) {
|
||||
inspection, _ := inheritanceInspectionForTest(t,
|
||||
parseCompositionDocument(t, "source-1", "parent: {value: same}\n"),
|
||||
parseCompositionDocument(t, "source-2", "child: {extends: parent, value: same}\n"))
|
||||
path := Keys("child", "value")
|
||||
history := inspection.Metadata().History(path)
|
||||
var structural []Change
|
||||
for _, change := range history {
|
||||
if change.Origin.Operation == ChangeInheritance {
|
||||
structural = append(structural, change)
|
||||
}
|
||||
}
|
||||
if len(structural) != 2 || structural[0].Origin.Locations[0].SourceID != "source-1" ||
|
||||
structural[1].Origin.Locations[0].SourceID != "source-2" ||
|
||||
len(structural[0].Dependencies) != 2 || structural[0].Dependencies[0].String() != "/parent" ||
|
||||
len(structural[0].Origin.Inheritance) != 1 {
|
||||
t.Fatalf("equal-valued structural history = %#v", history)
|
||||
}
|
||||
explanation, found := inspection.Explain(path)
|
||||
if !found || explanation.Effective.YAML != "same\n" {
|
||||
t.Fatalf("explanation = %#v, %v", explanation, found)
|
||||
}
|
||||
var equalEvents int
|
||||
for _, change := range explanation.History {
|
||||
if change.Origin.Operation == ChangeDecoding {
|
||||
t.Fatalf("inheritance was attributed to decoding: %#v", explanation.History)
|
||||
}
|
||||
if change.Origin.Operation == ChangeInheritance && change.Before.YAML == change.After.YAML {
|
||||
equalEvents++
|
||||
}
|
||||
}
|
||||
if equalEvents != 2 {
|
||||
t.Fatalf("equal parent and child operations = %#v", explanation.History)
|
||||
}
|
||||
parentHistory := inspection.Metadata().History(Keys("parent", "value"))
|
||||
for _, change := range parentHistory {
|
||||
if change.Origin.Operation == ChangeInheritance {
|
||||
t.Fatalf("source definition acquired a destination operation: %#v", parentHistory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInheritanceHistoryRetainsControlsAndDisplacedPaths(t *testing.T) {
|
||||
inspection, _ := inheritanceInspectionForTest(t,
|
||||
parseCompositionDocument(t, "source-1", "parent: {branch: {detail: inherited}}\n"),
|
||||
parseCompositionDocument(t, "source-2", "child: {extends: parent, branch: replaced}\n"))
|
||||
control := Keys("child", inheritanceKey)
|
||||
controlHistory := inspection.Metadata().History(control)
|
||||
if len(controlHistory) < 2 || controlHistory[len(controlHistory)-1].Origin.Operation != ChangeInheritance {
|
||||
t.Fatalf("control history = %#v", controlHistory)
|
||||
}
|
||||
controlExplanation, found := inspection.Explain(control)
|
||||
if !found || controlExplanation.Effective.Present ||
|
||||
controlExplanation.History[len(controlExplanation.History)-1].After.Present {
|
||||
t.Fatalf("consumed control = %#v, %v", controlExplanation, found)
|
||||
}
|
||||
path := Keys("child", "branch", "detail")
|
||||
if _, found := inspection.Metadata().Lookup(path); found {
|
||||
t.Fatal("displaced descendant remained effective")
|
||||
}
|
||||
listed := false
|
||||
for _, historical := range inspection.Metadata().HistoricalPaths() {
|
||||
if historical.String() == path.String() {
|
||||
listed = true
|
||||
}
|
||||
}
|
||||
if !listed {
|
||||
t.Fatal("displaced inherited descendant was not enumerable")
|
||||
}
|
||||
history := inspection.Metadata().History(path)
|
||||
if len(history) < 2 || history[len(history)-1].Origin.Operation != ChangeInheritance {
|
||||
t.Fatalf("displaced descendant history = %#v", history)
|
||||
}
|
||||
explanation, found := inspection.Explain(path)
|
||||
if !found || explanation.Effective.Present {
|
||||
t.Fatalf("displaced descendant explanation = %#v, %v", explanation, found)
|
||||
}
|
||||
var sawInherited, sawRemoval bool
|
||||
for _, change := range explanation.History {
|
||||
if change.Origin.Operation != ChangeInheritance {
|
||||
continue
|
||||
}
|
||||
sawInherited = sawInherited || change.After.Present
|
||||
sawRemoval = sawRemoval || (change.Before.Present && !change.After.Present)
|
||||
}
|
||||
if !sawInherited || !sawRemoval {
|
||||
t.Fatalf("displaced value sequence = %#v", explanation.History)
|
||||
}
|
||||
var rendered bytes.Buffer
|
||||
if err := inspection.WriteExplanation(&rendered, path); err != nil ||
|
||||
!strings.Contains(rendered.String(), "structural inheritance") {
|
||||
t.Fatalf("rendered structural explanation = %q, %v", rendered.String(), err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInheritanceHistoryPrecedesApplicationResolution(t *testing.T) {
|
||||
inspection, _ := inheritanceInspectionForTest(t,
|
||||
parseCompositionDocument(t, "source-1", "parent: {value: original}\nchild: {extends: parent}\n"))
|
||||
path := Keys("child", "value")
|
||||
replacement := &document.Node{Kind: document.String, Text: "derived"}
|
||||
snapshot := resolutionSnapshot{
|
||||
change: Change{Path: path, Origin: Origin{Layer: LayerResolution, Operation: ChangeNormalization}},
|
||||
after: replacement,
|
||||
}
|
||||
final := document.Clone(inspection.final)
|
||||
replaceHistoryPath(&final, path, replacement)
|
||||
inspection.final = final
|
||||
inspection.resolution = []resolutionSnapshot{snapshot}
|
||||
inspection.metadata.history[path.String()] = append(inspection.metadata.history[path.String()], snapshot.change)
|
||||
inspection.metadata.fields[path.String()] = FieldInfo{Explicit: true, Origin: snapshot.change.Origin}
|
||||
history := inspection.Metadata().History(path)
|
||||
if len(history) < 2 || history[len(history)-2].Origin.Operation != ChangeInheritance ||
|
||||
history[len(history)-1].Origin.Operation != ChangeNormalization {
|
||||
t.Fatalf("resolution order = %#v", history)
|
||||
}
|
||||
explanation, found := inspection.Explain(path)
|
||||
if !found || explanation.Effective.YAML != "derived\n" ||
|
||||
explanation.History[len(explanation.History)-1].Origin.Operation != ChangeNormalization {
|
||||
t.Fatalf("resolved explanation = %#v, %v", explanation, found)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHistoryNodeBudgetCountsSharedInheritanceEvidenceOnce(t *testing.T) {
|
||||
shared := &document.Node{Kind: document.String, Text: "shared"}
|
||||
extra := &document.Node{Kind: document.String, Text: "extra"}
|
||||
expansion := &inheritanceScope{
|
||||
root: shared,
|
||||
operations: []inheritanceOperation{{parent: shared, effective: shared}},
|
||||
replacements: []inheritanceReplacement{{before: shared, after: extra}},
|
||||
}
|
||||
if !historyNodesWithin(2, nil, nil, nil, nil, nil, expansion) ||
|
||||
historyNodesWithin(1, nil, nil, nil, nil, nil, expansion) {
|
||||
t.Fatal("shared evidence node accounting is incorrect")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInheritanceExpansionEnforcesRetainedHistoryBudget(t *testing.T) {
|
||||
input := parseCompositionDocument(t, "source-1", "parent: {value: original}\nchild: {extends: parent, value: replacement}\n")
|
||||
if _, diagnostics := expandInheritanceWithBudgets(input, nil, defaultProjectionLimits(), 2); len(diagnostics) != 1 || diagnostics[0].Code != CodeExplainLimitExceeded {
|
||||
t.Fatalf("retained history limit = %#v", diagnostics)
|
||||
}
|
||||
if _, diagnostics := expandInheritanceWithBudgets(input, nil, defaultProjectionLimits(), 100); len(diagnostics) != 0 {
|
||||
t.Fatalf("sufficient retained history budget = %#v", diagnostics)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user