package configkit import ( "errors" "math" "reflect" "strings" "testing" "time" ) func TestLoadRunsRecordedResolutionWithMapWriteback(t *testing.T) { t.Parallel() type artifact struct { Output string `yaml:"output"` Timeout time.Duration `yaml:"timeout"` } type config struct { PollInterval time.Duration `yaml:"poll_interval"` Timeout time.Duration `yaml:"timeout"` Artifacts map[string]artifact `yaml:"artifacts"` } definition := Definition[config]{ Defaults: func() config { return config{PollInterval: time.Minute, Timeout: 5 * time.Second} }, Resolve: func(configuration *config, resolution *Resolution) error { Default(resolution, Keys("timeout"), &configuration.Timeout, configuration.PollInterval/10, "derived from poll interval", Keys("poll_interval")) for name, current := range configuration.Artifacts { path := Keys("artifacts", name) Default(resolution, path.Key("timeout"), ¤t.Timeout, configuration.Timeout, "inherited configuration timeout", Keys("timeout")) Normalize(resolution, path.Key("output"), ¤t.Output, strings.TrimSpace(current.Output), "trimmed surrounding whitespace") configuration.Artifacts[name] = current } resolution.Warning(Keys("timeout"), "using derived timeout", Keys("poll_interval")) return nil }, } input := "poll_interval: 1m\nartifacts:\n second: {output: ' second '}\n first: {output: ' first '}\n" result, err := Load(Bytes("resolution.yml", []byte(input)), Options[config]{Definition: definition}) if err != nil { t.Fatal(err) } if result.Config.Timeout != 6*time.Second { t.Fatalf("timeout = %s", result.Config.Timeout) } for name, want := range map[string]string{"first": "first", "second": "second"} { current := result.Config.Artifacts[name] if current.Timeout != 6*time.Second || current.Output != want { t.Errorf("artifact %q = %#v", name, current) } } if diagnostics := result.Diagnostics(); len(diagnostics) != 1 || diagnostics[0].Code != CodeResolveWarning || diagnostics[0].Primary == nil || len(diagnostics[0].Related) != 1 { t.Fatalf("diagnostics = %#v", diagnostics) } metadata := result.Metadata() timeout, ok := metadata.Lookup(Keys("timeout")) if !ok || timeout.Explicit || timeout.Origin.Layer != LayerResolution || timeout.Origin.Operation != ChangeDerivedDefault { t.Fatalf("timeout metadata = %#v, %v", timeout, ok) } output, ok := metadata.Lookup(Keys("artifacts", "first", "output")) if !ok || !output.Explicit || output.Origin.Operation != ChangeNormalization { t.Fatalf("output metadata = %#v, %v", output, ok) } history := metadata.History(Keys("timeout")) last := history[len(history)-1] if last.Origin.Operation != ChangeDerivedDefault || !reflect.DeepEqual(last.Reasons, []string{"derived from poll interval"}) || len(last.Dependencies) != 1 || last.Dependencies[0].String() != "/poll_interval" { t.Fatalf("timeout history = %#v", history) } wantOrder := []string{ "/artifacts/first/output", "/artifacts/first/timeout", "/artifacts/second/output", "/artifacts/second/timeout", "/timeout", } gotOrder := make([]string, len(result.resolution)) for index, snapshot := range result.resolution { gotOrder[index] = snapshot.change.Path.String() } if !reflect.DeepEqual(gotOrder, wantOrder) { t.Fatalf("resolution order = %#v", gotOrder) } } func TestResolutionPreservesExplicitNullAndZeroWhileReplacingAbsentBaseline(t *testing.T) { t.Parallel() type config struct { Optional *int `yaml:"optional"` Count int `yaml:"count"` Absent int `yaml:"absent"` } five := 5 result, err := Load(Bytes("explicit.yml", []byte("optional: null\ncount: 0\n")), Options[config]{Definition: Definition[config]{ Defaults: func() config { return config{Optional: &five, Count: 5, Absent: 5} }, Resolve: func(configuration *config, resolution *Resolution) error { if !resolution.IsExplicit(Keys("optional")) || !resolution.IsExplicit(Keys("count")) || resolution.IsExplicit(Keys("absent")) { return errors.New("unexpected presence") } Default(resolution, Keys("optional"), &configuration.Optional, &five, "would derive optional") Default(resolution, Keys("count"), &configuration.Count, 9, "would derive count") Default(resolution, Keys("absent"), &configuration.Absent, 9, "derived absent value") return nil }, }}) if err != nil { var loadError *LoadError _ = errors.As(err, &loadError) t.Fatalf("%v: %#v", err, loadError.Diagnostics()) } if result.Config.Optional != nil || result.Config.Count != 0 || result.Config.Absent != 9 { t.Fatalf("config = %#v", result.Config) } if len(result.Metadata().History(Keys("optional"))) != 2 || len(result.Metadata().History(Keys("count"))) != 2 || len(result.Metadata().History(Keys("absent"))) != 2 { t.Fatal("resolution history disagrees with explicitness") } } func TestResolutionCoalescesEqualAndRepeatedHelperIntent(t *testing.T) { t.Parallel() type config struct { Value int `yaml:"value"` Other int `yaml:"other"` } result, err := Load(NoSource(), Options[config]{Definition: Definition[config]{ Defaults: func() config { return config{Value: 5} }, Resolve: func(configuration *config, resolution *Resolution) error { Default(resolution, Keys("value"), &configuration.Value, 5, "equal derived intent", Keys("other")) Default(resolution, Keys("value"), &configuration.Value, 7, "updated derived intent", Keys("other")) Normalize(resolution, Keys("value"), &configuration.Value, 7, "canonicalized value", Keys("other")) return nil }, }}) if err != nil { t.Fatal(err) } history := result.Metadata().History(Keys("value")) if len(history) != 2 { t.Fatalf("history = %#v", history) } change := history[1] if change.Origin.Operation != ChangeNormalization || !reflect.DeepEqual(change.Reasons, []string{"equal derived intent", "updated derived intent", "canonicalized value"}) || len(change.Dependencies) != 1 { t.Fatalf("coalesced change = %#v", change) } } func TestResolutionContainerRecordCoversSubtreeRemovalAndSnapshotsAreDetached(t *testing.T) { t.Parallel() type item struct { Value string `yaml:"value"` } type config struct { Items map[string]item `yaml:"items"` } result, err := Load(Bytes("remove.yml", []byte("items:\n keep: {value: 'yes'}\n remove: {value: 'no'}\n")), Options[config]{Definition: Definition[config]{ Resolve: func(configuration *config, resolution *Resolution) error { delete(configuration.Items, "remove") resolution.Record(Keys("items"), ChangeNormalization, "removed disabled entries") return nil }, }}) if err != nil { t.Fatal(err) } if _, exists := result.Config.Items["remove"]; exists { t.Fatal("removed entry remains") } if _, ok := result.Metadata().Lookup(Keys("items", "remove")); ok { t.Fatal("removed entry remains effective metadata") } if len(result.resolution) != 1 || result.resolution[0].before == nil || result.resolution[0].after == nil { t.Fatalf("snapshots = %#v", result.resolution) } result.Config.Items["keep"] = item{Value: "changed"} if mappingValue(t, result.resolution[0].after, "keep").Mapping[0].Value.Text != "yes" { t.Fatal("caller mutation changed private resolution snapshot") } } func TestResolutionSequenceReplacementUpdatesDescendantOrigins(t *testing.T) { t.Parallel() type config struct { Values []int `yaml:"values"` } result, err := Load(Bytes("sequence.yml", []byte("values: [1, 2]\n")), Options[config]{Definition: Definition[config]{ Resolve: func(configuration *config, resolution *Resolution) error { Normalize(resolution, Keys("values"), &configuration.Values, []int{3}, "replaced values") return nil }, }}) if err != nil { t.Fatal(err) } info, ok := result.Metadata().Lookup(Keys("values").Index(0)) if !ok || !info.Explicit || info.Origin.Layer != LayerResolution || info.Origin.Operation != ChangeNormalization { t.Fatalf("element metadata = %#v, %v", info, ok) } if _, ok := result.Metadata().Lookup(Keys("values").Index(1)); ok { t.Fatal("removed sequence element remains effective") } } func TestResolutionRejectsUnrecordedAndMismatchedMutations(t *testing.T) { t.Parallel() type config struct { Value int `yaml:"value"` } tests := []struct { name string resolve func(*config, *Resolution) error code Code }{ {name: "unrecorded", resolve: func(configuration *config, _ *Resolution) error { configuration.Value = 2 return nil }, code: CodeResolveUnrecordedChange}, {name: "unrelated target", resolve: func(_ *config, resolution *Resolution) error { unrelated := 1 Normalize(resolution, Keys("value"), &unrelated, 2, "changed value") return nil }, code: CodeResolveInvalidTarget}, {name: "invalid record kind", resolve: func(_ *config, resolution *Resolution) error { resolution.Record(Keys("value"), ChangeKind("other"), "invalid kind") return nil }, code: CodeResolveInvalidKind}, {name: "invalid path", resolve: func(_ *config, resolution *Resolution) error { resolution.Record(Keys("unknown"), ChangeNormalization, "unknown field") return nil }, code: CodeResolveInvalidPath}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { result, err := Load(Bytes("invalid-resolution.yml", []byte("value: 1\n")), Options[config]{Definition: Definition[config]{Resolve: test.resolve}}) assertLoadDiagnostic(t, result, err, test.code) }) } } func TestResolutionRejectsInvalidProjectedValuesAndSuppressesReturnedErrors(t *testing.T) { t.Parallel() type config struct { Number float64 `yaml:"number"` Value int `yaml:"value"` } tests := []struct { name string resolve func(*config, *Resolution) error code Code }{ {name: "invalid helper value", resolve: func(configuration *config, resolution *Resolution) error { Normalize(resolution, Keys("number"), &configuration.Number, math.NaN(), "invalid number") return nil }, code: CodeResolveInvalidValue}, {name: "reported error", resolve: func(_ *config, resolution *Resolution) error { resolution.Error(Keys("value"), "value cannot be resolved") return errors.New("CANARY_RESOLUTION_ERROR") }, code: CodeResolveError}, {name: "returned error", resolve: func(_ *config, _ *Resolution) error { return errors.New("CANARY_RESOLUTION_ERROR") }, code: CodeResolveHookFailed}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { result, err := Load(Bytes("resolution-errors.yml", []byte("value: 1\nnumber: 1\n")), Options[config]{Definition: Definition[config]{Resolve: test.resolve}}) loadError := assertLoadDiagnostic(t, result, err, test.code) for _, diagnostic := range loadError.Diagnostics() { if strings.Contains(diagnostic.Message, "CANARY") { t.Fatalf("diagnostic exposed returned error: %#v", diagnostic) } } }) } } func TestResolutionContextPanicsAfterCallback(t *testing.T) { t.Parallel() type config struct { Value int `yaml:"value"` } var retained *Resolution if _, err := Load(NoSource(), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error { retained = resolution return nil }}}); err != nil { t.Fatal(err) } defer func() { if recovered := recover(); recovered != closedCallbackContextMessage { t.Fatalf("panic = %#v", recovered) } }() retained.IsExplicit(Keys("value")) } func TestValidationContextPanicsAfterVersionCallback(t *testing.T) { t.Parallel() type config struct { Version string `yaml:"version"` } var retained *Validation if _, err := Load(Bytes("version-context.yml", []byte("version: one\n")), Options[config]{Definition: Definition[config]{ Version: &VersionRule{Path: Keys("version"), Check: func(_ Version, validation *Validation) error { retained = validation return nil }}, }}); err != nil { t.Fatal(err) } defer func() { if recovered := recover(); recovered != closedCallbackContextMessage { t.Fatalf("panic = %#v", recovered) } }() retained.Warning(Keys("version"), "too late") } func assertLoadDiagnostic[T any](t *testing.T, result *Result[T], err error, code Code) *LoadError { t.Helper() if result != nil || err == nil { t.Fatalf("result, err = %#v, %v", result, err) } var loadError *LoadError if !errors.As(err, &loadError) { t.Fatalf("error type = %T", err) } diagnostics := loadError.Diagnostics() if len(diagnostics) == 0 || diagnostics[0].Code != code { t.Fatalf("diagnostics = %#v", diagnostics) } return loadError }