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 { for _, snapshot := range result.history.resolutionPayload.snapshots { if snapshot.change.Path.String() == "/items/1/value" && (snapshot.before == nil || snapshot.before.Text != "old") { t.Fatalf("paired before = %#v", snapshot.before) } } } 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()) } }