package engine import ( "errors" "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) } } 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) } } }