Files
configkit/internal/engine/resolution_compare_test.go

536 lines
19 KiB
Go

package engine
import (
"errors"
"strings"
"testing"
)
func TestResolutionAuditsOrdinaryListFields(t *testing.T) {
type item struct {
Count int `yaml:"count"`
Enabled bool `yaml:"enabled"`
Nested []*int `yaml:"nested"`
}
type config struct {
Items []item `yaml:"items"`
}
for _, test := range []struct {
name string
other, resize bool
wantPath string
}{
{name: "covered field"},
{name: "uncovered field", other: true, wantPath: "/items/0/enabled"},
{name: "length change", resize: true, wantPath: "/items"},
} {
t.Run(test.name, func(t *testing.T) {
result, err := testLoad(Bytes("items", []byte("items: [{count: 3, enabled: true, nested: [null]}]\n")), Options[config]{Definition: Definition[config]{
Resolve: func(value *config, r *Resolution) error {
value.Items[0].Count = 0
r.RecordNormalization(Keys("items").Index(0).Key("count"), "clear count")
if test.other {
value.Items[0].Enabled = false
}
if test.resize {
value.Items = append(value.Items, item{})
}
return nil
},
}})
if test.wantPath == "" {
if err != nil || result.Config.Items[0].Count != 0 {
t.Fatalf("result = %#v, error = %v", result, err)
}
return
}
var failure *LoadError
if !errors.As(err, &failure) {
t.Fatalf("error = %v", err)
}
found := false
for _, diagnostic := range failure.Diagnostics() {
if diagnostic.Code == CodeResolveUnrecordedChange && diagnostic.Path.String() == test.wantPath {
found = true
}
}
if !found {
t.Fatalf("diagnostics = %#v", failure.Diagnostics())
}
})
}
}
func TestResolutionComparisonKeepsDiffAtomic(t *testing.T) {
before := parseCompositionDocument(t, "before", "items: [{values: [true, null]}]")
after := parseCompositionDocument(t, "after", "items: [{values: [false, 0]}]")
deltas := compareResolutionValues(before, after)
if len(deltas) != 2 || deltas[0].path.String() != "/items/0/values/0" || deltas[1].path.String() != "/items/0/values/1" {
t.Fatalf("resolution deltas = %#v", deltas)
}
atomic := compareConfigurationValues(before, after)
if len(atomic) != 1 || atomic[0].path.String() != "/items" {
t.Fatalf("atomic deltas = %#v", atomic)
}
}
func TestResolutionAuditsRegisteredCollectionChanges(t *testing.T) {
type item struct {
ID string `yaml:"id"`
Value string `yaml:"value"`
}
type config struct {
Items []item `yaml:"items"`
}
for _, test := range []struct {
name string
move, container, rename bool
wantPath string
}{
{name: "field edit"},
{name: "uncovered move", move: true, wantPath: "/items"},
{name: "covered move", move: true, container: true},
{name: "identity replacement", rename: true, wantPath: "/items"},
} {
t.Run(test.name, func(t *testing.T) {
definition := Definition[config]{Resolve: func(value *config, r *Resolution) error {
value.Items[0].Value = "changed"
if test.move {
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
}
if test.rename {
value.Items[0].ID = "new"
}
index := 0
if test.move {
index = 1
}
r.RecordNormalization(Keys("items").Index(index).Key("value"), "change value")
if test.container {
r.RecordNormalization(Keys("items"), "reorder")
}
return nil
}}
definition = DefinitionWithSequenceIdentityRegistrations(definition, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
result, err := testLoad(Bytes("items", []byte("items: [{id: a, value: old}, {id: b, value: other}]\n")), Options[config]{Definition: definition})
if test.wantPath == "" {
if err != nil {
t.Fatal(err)
}
if test.move {
correspondence := result.history.resolutionPayload.correspondence
entry, ok := correspondence.entryPath(Keys("items").Index(1).Key("value"))
if !ok || entry.String() != "/items/0/value" {
t.Fatalf("retained entry = %s, %t", entry.String(), ok)
}
for _, snapshot := range result.history.resolutionPayload.snapshots {
if snapshot.change.Path.String() == "/items/1/value" && (snapshot.before == nil || snapshot.before.Text != "old" || !snapshot.hasEntry || snapshot.entryPath.String() != "/items/0/value") {
t.Fatalf("paired before = %#v", snapshot.before)
}
if snapshot.before != nil {
found := false
for _, root := range result.history.documentRoots() {
if root == snapshot.before {
found = true
}
}
if !found {
t.Fatal("paired snapshot omitted from retained roots")
}
}
}
}
return
}
var failure *LoadError
if !errors.As(err, &failure) {
t.Fatalf("error = %v", err)
}
found := false
for _, diagnostic := range failure.Diagnostics() {
if diagnostic.Code == CodeResolveUnrecordedChange && diagnostic.Path.String() == test.wantPath {
found = true
}
}
if !found {
t.Fatalf("diagnostics = %#v", failure.Diagnostics())
}
})
}
}
func TestRegisteredResolutionFallbackAndHelperTarget(t *testing.T) {
type item struct {
ID string `yaml:"id"`
Value string `yaml:"value"`
}
type config struct {
Items []item `yaml:"items"`
}
registration := []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}}
makeDefinition := func(resolve func(*config, *Resolution) error) Definition[config] {
return DefinitionWithSequenceIdentityRegistrations(Definition[config]{Defaults: func() config { return config{Items: []item{{ID: "a"}, {ID: "a"}}} }, Resolve: resolve}, registration)
}
_, err := testLoad(NoSource(), Options[config]{Definition: makeDefinition(func(value *config, r *Resolution) error {
value.Items[1].ID = "b"
r.RecordNormalization(Keys("items"), "repair identities")
return nil
})})
if err != nil {
t.Fatalf("repair rejected: %v", err)
}
_, err = testLoad(NoSource(), Options[config]{Definition: makeDefinition(func(*config, *Resolution) error { return nil })})
if err == nil {
t.Fatal("invalid final identities accepted")
}
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{Resolve: func(value *config, r *Resolution) error {
Normalize(r, Keys("items").Index(0).Key("value"), &value.Items[0].Value, "changed", "normalize")
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
r.RecordNormalization(Keys("items"), "reorder")
return nil
}}, registration)
_, err = testLoad(Bytes("items", []byte("items: [{id: a, value: first}, {id: b, value: second}]\n")), Options[config]{Definition: definition})
var failure *LoadError
if !errors.As(err, &failure) {
t.Fatalf("helper error = %v", err)
}
found := false
for _, diagnostic := range failure.Diagnostics() {
if diagnostic.Code == CodeResolveInvalidTarget && diagnostic.Path.String() == "/items/0/value" {
found = true
}
}
if !found {
t.Fatalf("diagnostics = %#v", failure.Diagnostics())
}
}
func TestRegisteredResolutionMetadataFollowsEntryIdentity(t *testing.T) {
type item struct {
ID string `yaml:"id"`
Enabled bool `yaml:"enabled"`
Count int `yaml:"count"`
}
type config struct {
Items []item `yaml:"items"`
}
entryFixed := false
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{Resolve: func(value *config, r *Resolution) error {
entryFixed = r.IsExplicit(Keys("items").Index(0).Key("enabled")) && !r.IsExplicit(Keys("items").Index(1).Key("enabled"))
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
value.Items = append(value.Items, item{ID: "new"})
r.RecordNormalization(Keys("items"), "reorder and add")
return nil
}}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
result, err := testLoad(Bytes("records", []byte("items: [{id: a, enabled: false}, {id: b, count: 0}]\n")), Options[config]{Definition: definition})
if err != nil || !entryFixed {
t.Fatalf("load = %#v, fixed entry = %t, error = %v", result, entryFixed, err)
}
for _, check := range []struct {
index int
key string
explicit bool
}{{0, "enabled", false}, {0, "count", true}, {1, "enabled", true}, {1, "count", false}, {2, "id", false}} {
info, ok := result.Metadata().Lookup(Keys("items").Index(check.index).Key(check.key))
if !ok || info.Explicit != check.explicit || info.Origin.Layer != LayerResolution {
t.Errorf("item %d %s = %#v, %t", check.index, check.key, info, ok)
}
}
}
func TestRegisteredResolutionValidationLocationFollowsMovedRecord(t *testing.T) {
type item struct {
ID string `yaml:"id"`
Value string `yaml:"value"`
}
type config struct {
Items []item `yaml:"items"`
}
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{
Resolve: func(value *config, r *Resolution) error {
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
r.RecordNormalization(Keys("items"), "reorder")
return nil
},
Validate: func(_ *config, v *Validation) { v.Error(Keys("items").Index(0).Key("value"), "invalid") },
}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
_, err := testLoad(Bytes("records", []byte("items:\n - id: a\n value: first\n - id: b\n value: second\n")), Options[config]{Definition: definition})
var failure *LoadError
if !errors.As(err, &failure) {
t.Fatalf("error = %v", err)
}
for _, diagnostic := range failure.Diagnostics() {
if diagnostic.Path.String() == "/items/0/value" && diagnostic.Primary != nil && diagnostic.Primary.Line == 5 {
return
}
}
t.Fatalf("diagnostics = %#v", failure.Diagnostics())
}
func TestRegisteredResolutionHistoryFollowsMatchedSource(t *testing.T) {
type item struct {
ID string `yaml:"id"`
Value string `yaml:"value"`
}
type config struct {
Items []item `yaml:"items"`
}
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{Resolve: func(value *config, r *Resolution) error {
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
value.Items = append(value.Items, item{ID: "new", Value: "added"})
r.RecordNormalization(Keys("items"), "reorder and add")
return nil
}}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
result, err := testLoad(Bytes("records", []byte("items:\n - id: a\n value: first\n - id: b\n value: second\n")), Options[config]{Definition: definition})
if err != nil {
t.Fatal(err)
}
for _, check := range []struct{ index, line int }{{0, 5}, {1, 3}} {
path := Keys("items").Index(check.index).Key("value")
if location, ok := result.history.latestHistoricalLocation(path); !ok || location.Line != check.line {
t.Errorf("historical location for %s = %#v, %t", path.String(), location, ok)
}
history := result.Metadata().History(path)
found := false
for _, change := range history {
for _, location := range change.Origin.Locations {
if location.Line == check.line {
found = true
} else if location.Line == 3 || location.Line == 5 {
t.Errorf("wrong record in %s: %#v", path.String(), history)
}
}
}
if !found {
t.Errorf("missing source line %d in %#v", check.line, history)
}
explanation, ok := result.Inspection.Explain(path)
if !ok {
t.Fatalf("missing explanation for %s", path.String())
}
want := "second\n"
if check.index == 1 {
want = "first\n"
}
foundValue := false
for _, change := range explanation.History {
if change.Origin.Layer == LayerMain && change.After.YAML == want {
foundValue = true
}
}
if !foundValue {
t.Errorf("missing matched input %q in %#v", want, explanation.History)
}
}
for _, change := range result.Metadata().History(Keys("items").Index(2).Key("value")) {
if change.Origin.Layer == LayerMain {
t.Fatalf("added record inherited input: %#v", change)
}
}
}
func TestMovedRecordExplanationIncludesInheritedValue(t *testing.T) {
type item struct {
ID string `yaml:"id"`
Value string `yaml:"value"`
}
type config struct {
Items []item `yaml:"items"`
}
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{Resolve: func(value *config, r *Resolution) error {
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
r.RecordNormalization(Keys("items"), "reorder")
return nil
}}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
result, err := testLoad(Bytes("records", []byte("items: [{id: parent, value: inherited}, {id: child, extends: {id: parent}}]\n")), Options[config]{Definition: definition})
if err != nil {
t.Fatal(err)
}
explanation, ok := result.Inspection.Explain(Keys("items").Index(0).Key("value"))
if !ok {
t.Fatal("missing explanation")
}
found := false
for _, change := range explanation.History {
if change.After.YAML == "inherited\n" {
found = true
}
}
if !found {
t.Fatalf("inherited history = %#v", explanation.History)
}
}
func TestMovedRecordExplanationIncludesCLIValue(t *testing.T) {
type item struct {
ID string `yaml:"id"`
Value string `yaml:"value"`
}
type config struct {
Items []item `yaml:"items"`
}
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{Resolve: func(value *config, r *Resolution) error {
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
r.RecordNormalization(Keys("items"), "reorder")
return nil
}}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
result, err := testLoad(NoSource(), Options[config]{Definition: definition, Overrides: []Override{{
Path: Keys("items"), Flag: "--items", Value: []any{map[string]any{"id": "a", "value": "first"}, map[string]any{"id": "b", "value": "second"}},
}}})
if err != nil {
t.Fatal(err)
}
explanation, ok := result.Inspection.Explain(Keys("items").Index(1).Key("value"))
if !ok {
t.Fatal("missing explanation")
}
for _, change := range explanation.History {
if change.Origin.Layer == LayerCLI && change.After.YAML == "first\n" {
return
}
}
t.Fatalf("CLI history = %#v", explanation.History)
}
func TestRegisteredResolutionExplainsRemovalAndReusedPositions(t *testing.T) {
type item struct {
ID string `yaml:"id"`
Value string `yaml:"value"`
}
type config struct {
Items []item `yaml:"items"`
}
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{Resolve: func(value *config, r *Resolution) error {
value.Items = value.Items[1:]
r.RecordNormalization(Keys("items"), "remove first")
return nil
}}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
result, err := testLoad(Bytes("records", []byte("items:\n - id: reports\n value: report\n - id: invoices\n value: invoice\n")), Options[config]{Definition: definition})
if err != nil {
t.Fatal(err)
}
path := Keys("items").Index(0).Key("value")
explanation, ok := result.Inspection.Explain(path)
if !ok {
t.Fatal("survivor explanation missing")
}
found := false
for _, change := range explanation.History {
if change.Origin.Layer == LayerResolution {
if change.Before.YAML != "invoice\n" || change.After.YAML != "invoice\n" {
t.Fatalf("survivor operation = %#v", change)
}
found = true
}
if change.Origin.Layer == LayerMain && change.After.YAML == "report\n" {
t.Fatalf("removed occupant leaked into survivor: %#v", change)
}
}
if !found {
t.Fatal("missing collection operation")
}
container, ok := result.Inspection.Explain(Keys("items"))
if !ok {
t.Fatal("collection explanation missing")
}
found = false
for _, change := range container.History {
if change.Origin.Layer == LayerResolution && strings.Contains(change.Before.YAML, "reports") && !strings.Contains(change.After.YAML, "reports") {
found = true
}
}
if !found {
t.Fatalf("missing removed collection evidence: %#v", container.History)
}
mainEvidence := false
for _, change := range result.Metadata().History(Keys("items")) {
if change.Origin.Layer == LayerMain && len(change.Origin.Locations) != 0 {
mainEvidence = true
}
}
if !mainEvidence {
t.Fatal("collection history lost source attribution")
}
if _, ok := result.Inspection.Explain(Keys("items").Index(1)); !ok {
t.Fatal("absent historical position missing")
}
if _, ok := result.Inspection.Explain(Keys("items").Index(8)); ok {
t.Fatal("unknown position reported")
}
}
func TestRegisteredResolutionExplainsAddedAndChangedIdentities(t *testing.T) {
type item struct {
ID string `yaml:"id"`
Value string `yaml:"value"`
}
type config struct {
Items []item `yaml:"items"`
}
for _, replace := range []bool{false, true} {
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{Resolve: func(value *config, r *Resolution) error {
if replace {
value.Items[0] = item{ID: "new", Value: "added"}
} else {
value.Items = append([]item{{ID: "new", Value: "added"}}, value.Items...)
}
r.RecordNormalization(Keys("items"), "update collection")
return nil
}}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
result, err := testLoad(Bytes("records", []byte("items: [{id: old, value: original}]\n")), Options[config]{Definition: definition})
if err != nil {
t.Fatal(err)
}
explanation, ok := result.Inspection.Explain(Keys("items").Index(0).Key("value"))
if !ok {
t.Fatal("added explanation missing")
}
for _, change := range explanation.History {
if change.Origin.Layer == LayerMain || (change.Origin.Layer == LayerResolution && change.Before.Present) {
t.Fatalf("added record inherited previous value: %#v", explanation.History)
}
}
if !replace {
moved, ok := result.Inspection.Explain(Keys("items").Index(1).Key("value"))
if !ok {
t.Fatal("shifted survivor missing")
}
found := false
for _, change := range moved.History {
if change.Origin.Layer == LayerResolution && change.Before.YAML == "original\n" && change.After.YAML == "original\n" {
found = true
}
}
if !found {
t.Fatalf("shifted survivor = %#v", moved.History)
}
}
}
}
func TestRegisteredResolutionExplainsRemovedFieldOnMovedRecord(t *testing.T) {
type config struct {
Items []map[string]any `yaml:"items"`
}
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{Resolve: func(value *config, r *Resolution) error {
delete(value.Items[0], "extra")
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
r.RecordNormalization(Keys("items"), "remove and reorder")
return nil
}}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
result, err := testLoad(Bytes("records", []byte("items: [{id: a, extra: own}, {id: b, extra: other}]\n")), Options[config]{Definition: definition})
if err != nil {
t.Fatal(err)
}
explanation, ok := result.Inspection.Explain(Keys("items").Index(1).Key("extra"))
if !ok {
t.Fatal("removed field explanation missing")
}
for _, change := range explanation.History {
if change.Origin.Layer == LayerResolution && change.Before.YAML == "own\n" && !change.After.Present {
return
}
}
t.Fatalf("removed field history = %#v", explanation.History)
}