Files
ConfigKit/internal/engine/inheritance_origin_test.go

303 lines
12 KiB
Go

package engine
import (
"io/fs"
"reflect"
"slices"
"testing"
"testing/fstest"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
func TestInheritedOriginsKeepSourcePathsPresenceAndIndependentReplacement(t *testing.T) {
parent := parseCompositionDocument(t, "source-1", `parent:
enabled: false
count: 0
optional: null
items: [{file: message.txt}]
settings: {shared: parent}
`)
child := parseCompositionDocument(t, "source-2", `child:
extends: parent
count: 3
settings: {own: child}
`)
composed, diagnostics := composeDocuments(parent, child)
if len(diagnostics) != 0 {
t.Fatal(diagnostics)
}
result, diagnostics := expandInheritanceWithOrigins(composed.root, composed.origins, defaultProjectionLimits())
if len(diagnostics) != 0 {
t.Fatal(diagnostics)
}
for _, key := range []string{"enabled", "optional"} {
path := Keys("child", key)
value, found := documentAtPath(result.root, path)
origin, hasOrigin := result.origins[path.String()]
if !found || !hasOrigin || value == nil || len(origin.Locations) != 1 || origin.Locations[0].SourceID != "source-1" ||
len(projectInheritanceSteps(origin)) != 1 {
t.Fatalf("inherited %s = %#v, %#v", key, value, origin)
}
}
count := result.origins["/child/count"]
if len(count.Locations) != 1 || count.Locations[0].SourceID != "source-2" || len(projectInheritanceSteps(count)) != 0 {
t.Fatalf("direct replacement origin = %#v", count)
}
if len(result.replacements) != 1 || result.replacements[0].path.String() != "/child/count" ||
result.replacements[0].before.Text != "0" || result.replacements[0].after.Text != "3" ||
len(result.replacements[0].previous) != 1 ||
len(projectInheritanceSteps(result.replacements[0].previous[0].origin)) != 1 {
t.Fatalf("displaced inheritance = %#v", result.replacements)
}
settings := result.origins["/child/settings"]
if len(settings.Locations) != 2 || settings.Locations[0].SourceID != "source-1" ||
settings.Locations[1].SourceID != "source-2" || len(projectInheritanceSteps(settings)) != 1 {
t.Fatalf("composite mapping origin = %#v", settings)
}
if originContainedBy(result.origins["/child/enabled"], result.origins["/parent/enabled"]) ||
originContainedBy(result.origins["/parent/enabled"], result.origins["/child/enabled"]) {
t.Fatal("direct and inherited evidence collapsed despite matching source coordinates")
}
original := result.origins["/child/enabled"]
detached := cloneOrigin(original)
detached.Locations[0].SourceID = "changed"
detached.contributors[0].source.segments[0].key = "changed"
stepsCopy := projectInheritanceSteps(original)
stepsCopy[0].Locations[0].SourceID = "changed"
if original.Locations[0].SourceID != "source-1" ||
original.contributors[0].source.String() != "/parent/enabled" ||
projectInheritanceSteps(original)[0].Locations[0].SourceID != "source-2" {
t.Fatal("cloned origin or projected steps changed retained evidence")
}
resource := result.origins["/child/items/0/file"]
steps := projectInheritanceSteps(resource)
if len(steps) != 1 || steps[0].From.String() != "/parent/items/0/file" ||
steps[0].To.String() != "/child/items/0/file" || steps[0].Reference.String() != "/child/extends" ||
len(resource.Locations) != 1 || resource.Locations[0].SourceID != "source-1" {
t.Fatalf("sequence resource origin = %#v, steps = %#v", resource, steps)
}
contexts := map[string]fs.FS{
"source-1": fstest.MapFS{"message.txt": &fstest.MapFile{Data: []byte("from parent")}},
"source-2": fstest.MapFS{"message.txt": &fstest.MapFile{Data: []byte("from child")}},
}
data, err := fs.ReadFile(contexts[resource.Locations[0].SourceID], "message.txt")
if err != nil || string(data) != "from parent" {
t.Fatalf("inherited resource = %q, %v", data, err)
}
}
func TestOriginMembershipPreservesLocationAndSourceCoverageRules(t *testing.T) {
location := Location{SourceID: "source", Line: 4, Column: 2}
otherCoordinates := location
otherCoordinates.Line++
container := Origin{
Layer: LayerOverlay,
Operation: ChangeNormalization,
Locations: []Location{location},
}
if !originContainedBy(container, container) {
t.Fatal("identical location evidence was not contained")
}
coordinateDifference := container
coordinateDifference.Locations = []Location{otherCoordinates}
if originContainedBy(coordinateDifference, container) {
t.Fatal("location containment ignored differing coordinates")
}
parent := Keys("parent")
child := parent.Key("child")
change := Change{Path: parent, Origin: container}
descendantOrigin := Origin{
Layer: container.Layer,
Operation: container.Operation,
Locations: []Location{otherCoordinates},
}
if !originRepresentedByChange(child, descendantOrigin, change) {
t.Fatal("descendant source coverage rejected matching SourceID with different coordinates")
}
descendantOrigin.Locations[0].SourceID = "another-source"
if originRepresentedByChange(child, descendantOrigin, change) {
t.Fatal("descendant source coverage accepted an unrecorded SourceID")
}
withContributor := container
withContributor.contributors = []inheritanceContributor{{source: parent, location: location}}
if originContainedBy(withContributor, container) {
t.Fatal("exact location containment ignored the contributor-empty mismatch")
}
if !originRepresentedByChange(child, withContributor, change) {
t.Fatal("descendant source coverage was conflated with contributor containment")
}
}
func TestProjectedInheritanceStepsMatchScanAndRetainOwnership(t *testing.T) {
sourcePath := Keys("source", "a/b", "01")
firstLocations := []Location{{SourceID: "first;:source", Line: -1, Column: 2}}
secondLocations := []Location{
{SourceID: "second:;source", Line: 3, Column: -4},
{SourceID: "", Line: -5, Column: 6},
}
firstTransfer := inheritanceTransfer{
from: Keys("source"), to: Keys("middle"), reference: Keys("middle", "extends"), locations: firstLocations,
}
secondTransfer := inheritanceTransfer{
from: Keys("middle"), to: Keys("target"), reference: Keys("target", "extends"), locations: secondLocations,
}
firstTrace := &inheritanceTrace{transfer: &firstTransfer}
chain := &inheritanceTrace{previous: firstTrace, transfer: &secondTransfer}
firstStep := InheritanceStep{
From: sourcePath, To: Keys("middle", "a/b", "01"), Reference: firstTransfer.reference, Locations: firstLocations,
}
duplicateFirstTransfer := firstTransfer
duplicateSecondTransfer := secondTransfer
duplicateChain := &inheritanceTrace{
previous: &inheritanceTrace{transfer: &duplicateFirstTransfer},
transfer: &duplicateSecondTransfer,
}
reversedLocations := []Location{secondLocations[1], secondLocations[0]}
reorderedSecondTransfer := secondTransfer
reorderedSecondTransfer.locations = reversedLocations
reorderedChain := &inheritanceTrace{
previous: &inheritanceTrace{transfer: &duplicateFirstTransfer},
transfer: &reorderedSecondTransfer,
}
origin := Origin{
Inheritance: []InheritanceStep{firstStep, firstStep},
contributors: []inheritanceContributor{
{source: sourcePath, trace: chain},
{source: sourcePath, trace: duplicateChain},
{source: sourcePath, trace: reorderedChain},
{source: sourcePath, trace: chain},
},
}
want := projectInheritanceStepsByScan(origin)
got := projectInheritanceSteps(origin)
if !reflect.DeepEqual(got, want) {
t.Fatalf("projected steps differ from ordered scan:\n got: %#v\nwant: %#v", got, want)
}
if len(got) != 4 || got[0].From.String() != "/source/a~1b/01" || got[0].To.String() != "/middle/a~1b/01" ||
!reflect.DeepEqual(got[0], got[1]) || got[2].To.String() != "/target/a~1b/01" ||
!reflect.DeepEqual(got[2].Locations, secondLocations) || !reflect.DeepEqual(got[3].Locations, reversedLocations) {
t.Fatalf("unexpected duplicate/order/escape projection: %#v", got)
}
got[0].From.segments[0].key = "mutated"
got[2].Locations[0].SourceID = "mutated"
if repeated := projectInheritanceSteps(origin); !reflect.DeepEqual(repeated, want) {
t.Fatalf("returned steps mutated retained evidence:\n got: %#v\nwant: %#v", repeated, want)
}
}
func TestInheritanceLocationsKeyEncodesOrderedLengthPrefixedValues(t *testing.T) {
if inheritanceLocationsKey(nil) != inheritanceLocationsKey([]Location{}) {
t.Fatal("nil and empty location lists should encode equally")
}
values := [][]Location{
{{SourceID: "a;1;-2;", Line: -3, Column: 4}},
{{SourceID: "a", Line: 1, Column: -2}, {SourceID: ";1;-2;", Line: -3, Column: 4}},
{{SourceID: ";1;-2;", Line: -3, Column: 4}, {SourceID: "a", Line: 1, Column: -2}},
{{SourceID: "é", Line: -3, Column: 4}},
{{SourceID: "e", Line: -3, Column: 4}},
}
keys := make(map[string]int, len(values))
for index, locations := range values {
key := inheritanceLocationsKey(locations)
if previous, found := keys[key]; found {
t.Fatalf("location lists %d and %d encoded identically", previous, index)
}
keys[key] = index
}
}
func projectInheritanceStepsByScan(origin Origin) []InheritanceStep {
steps := cloneInheritanceSteps(origin.Inheritance)
for _, contributor := range origin.contributors {
var reverse []*inheritanceTrace
for link := contributor.trace; link != nil; link = link.previous {
reverse = append(reverse, link)
}
current := contributor.source
for index := len(reverse) - 1; index >= 0; index-- {
transfer := reverse[index].transfer
relative, ok := relativeHistoryPath(transfer.from, current)
if !ok {
continue
}
next := appendInheritancePath(transfer.to, relative)
step := InheritanceStep{
From: current.clone(), To: next.clone(), Reference: transfer.reference.clone(),
Locations: cloneLocations(transfer.locations),
}
if !slices.ContainsFunc(steps, func(existing InheritanceStep) bool {
return existing.From.String() == step.From.String() &&
existing.To.String() == step.To.String() &&
existing.Reference.String() == step.Reference.String() &&
slices.Equal(existing.Locations, step.Locations)
}) {
steps = append(steps, step)
}
current = next
}
}
return steps
}
func TestInheritedOriginsRetainChainsAndNestedDestinations(t *testing.T) {
first := parseCompositionDocument(t, "source-1", `first:
enabled: false
settings:
primary: {value: original}
secondary: {extends: primary}
`)
second := parseCompositionDocument(t, "source-2", `first: {other: present}
middle: {extends: first}
last:
extends: middle
settings:
primary: {value: replaced}
`)
composed, diagnostics := composeDocuments(first, second)
if len(diagnostics) != 0 {
t.Fatal(diagnostics)
}
result, diagnostics := expandInheritanceWithOrigins(composed.root, composed.origins, defaultProjectionLimits())
if len(diagnostics) != 0 {
t.Fatal(diagnostics)
}
chain := projectInheritanceSteps(result.origins["/last/enabled"])
if len(chain) != 2 || chain[0].From.String() != "/first/enabled" ||
chain[0].To.String() != "/middle/enabled" || chain[1].From.String() != "/middle/enabled" ||
chain[1].To.String() != "/last/enabled" {
t.Fatalf("chain = %#v", chain)
}
composite := result.origins["/last"]
if len(composite.Locations) < 2 || len(composite.contributors) != len(composite.Locations) {
t.Fatalf("composite contributors = %#v", composite)
}
seenSources := make(map[string]bool)
for _, contributor := range composite.contributors {
seenSources[contributor.location.SourceID] = true
}
if !seenSources["source-1"] || !seenSources["source-2"] {
t.Fatalf("same-layer source contributors = %#v", composite.contributors)
}
nested := projectInheritanceSteps(result.origins["/last/settings/secondary/value"])
if len(nested) != 1 || nested[0].From.String() != "/last/settings/primary/value" ||
nested[0].To.String() != "/last/settings/secondary/value" {
t.Fatalf("destination-specific nested transfer = %#v", nested)
}
if got := result.origins["/last/settings/secondary/value"].Locations[0].SourceID; got != "source-2" {
t.Fatalf("nested replacement source = %q", got)
}
if original, found := documentAtPath(first, Keys("first", "settings", "primary", "value")); !found || original.Text != "original" {
t.Fatal("expansion changed its input")
}
if inherited, found := documentAtPath(result.root, Keys("last", "settings", "secondary", "value")); !found || inherited.Kind != document.String || inherited.Text != "replaced" {
t.Fatalf("nested inherited value = %#v, %v", inherited, found)
}
}