package engine import ( "errors" "math" "reflect" "strings" "testing" "time" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) type mutableText struct { data []byte } func (value mutableText) MarshalText() ([]byte, error) { return value.data, nil } func (value *mutableText) UnmarshalText(text []byte) error { value.data = append(value.data[:0], text...) return nil } type failingText struct { failUnmarshal bool } func (value failingText) MarshalText() ([]byte, error) { return []byte("CANARY_CODEC_TEXT"), errors.New("CANARY_MARSHAL_ERROR") } func (value *failingText) UnmarshalText([]byte) error { if value.failUnmarshal { return errors.New("CANARY_UNMARSHAL_ERROR") } return nil } type reconstructFailText string func (value reconstructFailText) MarshalText() ([]byte, error) { return []byte(value), nil } func (value *reconstructFailText) UnmarshalText([]byte) error { return errors.New("CANARY_RECONSTRUCT_ERROR") } func TestProjectionPreservesCanonicalTypesAndNilEmptyDistinctions(t *testing.T) { t.Parallel() type nested struct { Value string `yaml:"value"` } type config struct { Zero int `yaml:"zero"` Wide uint64 `yaml:"wide"` Delay time.Duration `yaml:"delay"` Pointer *nested `yaml:"pointer"` NilPointer *nested `yaml:"nil_pointer"` EmptyMap map[string]string `yaml:"empty_map"` NilMap map[string]string `yaml:"nil_map"` EmptySlice []string `yaml:"empty_slice"` NilSlice []string `yaml:"nil_slice"` Objects map[string]*nested `yaml:"objects"` Fixed [2]int `yaml:"fixed"` Custom mutableText `yaml:"custom"` Dynamic any `yaml:"dynamic"` Ignored *nested `yaml:"-"` private *nested } value := config{ Wide: math.MaxUint64, Delay: 1500 * time.Millisecond, Pointer: &nested{Value: "pointer"}, EmptyMap: map[string]string{}, EmptySlice: []string{}, Objects: map[string]*nested{ "z": {Value: "last"}, "a": {Value: "first"}, }, Fixed: [2]int{1, 2}, Custom: mutableText{data: []byte("canonical")}, Dynamic: map[string]any{"count": int64(9), "items": []any{"x", false}}, Ignored: &nested{Value: "ignored"}, private: &nested{Value: "private"}, } schema := mustSchemaFor[config](t) projected, issue := projectConfiguration(schema, reflect.ValueOf(value), defaultProjectionLimits()) if issue != nil { t.Fatalf("projectConfiguration() issue = %#v", issue) } assertDocument(t, mappingValue(t, projected, "zero"), document.Number, "0") assertDocument(t, mappingValue(t, projected, "wide"), document.Number, "18446744073709551615") assertDocument(t, mappingValue(t, projected, "delay"), document.String, "1.5s") assertDocument(t, mappingValue(t, projected, "custom"), document.String, "canonical") if mappingValue(t, projected, "nil_pointer").Kind != document.Null || mappingValue(t, projected, "nil_map").Kind != document.Null || mappingValue(t, projected, "nil_slice").Kind != document.Null { t.Fatal("nil values did not project as null") } if empty := mappingValue(t, projected, "empty_map"); empty.Kind != document.Mapping || empty.Mapping == nil || len(empty.Mapping) != 0 { t.Fatalf("empty map = %#v", empty) } if empty := mappingValue(t, projected, "empty_slice"); empty.Kind != document.Sequence || empty.Sequence == nil || len(empty.Sequence) != 0 { t.Fatalf("empty slice = %#v", empty) } objects := mappingValue(t, projected, "objects") if len(objects.Mapping) != 2 || objects.Mapping[0].Key != "a" || objects.Mapping[1].Key != "z" { t.Fatalf("map order = %#v", objects.Mapping) } if hasMappingKey(projected, "ignored") || hasMappingKey(projected, "private") { t.Fatal("ignored field entered projection") } clonedValue, cloneIssue := cloneConfiguration(schema, reflect.ValueOf(value), defaultProjectionLimits()) if cloneIssue != nil { t.Fatal(cloneIssue.message) } cloned := clonedValue.Interface().(config) if cloned.NilMap != nil || cloned.NilSlice != nil || cloned.NilPointer != nil || cloned.EmptyMap == nil || cloned.EmptySlice == nil { t.Fatalf("clone lost nil/empty distinctions: %#v", cloned) } } func TestCloneConfigurationDetachesAllSupportedMutableValues(t *testing.T) { t.Parallel() type nested struct { Values []string `yaml:"values"` } type config struct { First *nested `yaml:"first"` Second *nested `yaml:"second"` Map map[string]*nested `yaml:"map"` Slice []map[string]int `yaml:"slice"` Custom mutableText `yaml:"custom"` Dynamic any `yaml:"dynamic"` Ignored *nested `yaml:"-"` } shared := &nested{Values: []string{"original"}} value := config{ First: shared, Second: shared, Map: map[string]*nested{"entry": {Values: []string{"map"}}}, Slice: []map[string]int{{"count": 1}}, Custom: mutableText{data: []byte("text")}, Dynamic: map[string]any{ "items": []any{map[string]any{"name": "dynamic"}}, }, Ignored: &nested{Values: []string{"ignored"}}, } schema := mustSchemaFor[config](t) clonedValue, issue := cloneConfiguration(schema, reflect.ValueOf(value), defaultProjectionLimits()) if issue != nil { t.Fatalf("cloneConfiguration() issue = %#v", issue) } cloned := clonedValue.Interface().(config) if cloned.First == cloned.Second || cloned.First == shared || cloned.Custom.data == nil || &cloned.Custom.data[0] == &value.Custom.data[0] { t.Fatal("clone retained mutable identity") } if cloned.Ignored != nil { t.Fatal("ignored field was retained") } value.First.Values[0] = "changed" value.Map["entry"].Values[0] = "changed" value.Slice[0]["count"] = 2 value.Custom.data[0] = 'X' value.Dynamic.(map[string]any)["items"].([]any)[0].(map[string]any)["name"] = "changed" if cloned.First.Values[0] != "original" || cloned.Map["entry"].Values[0] != "map" || cloned.Slice[0]["count"] != 1 || string(cloned.Custom.data) != "text" || cloned.Dynamic.(map[string]any)["items"].([]any)[0].(map[string]any)["name"] != "dynamic" { t.Fatalf("clone changed after source mutation: %#v", cloned) } } func TestProjectionAndCloneRejectCyclesAndInvalidDynamicValues(t *testing.T) { t.Parallel() type recursive struct { Next *recursive `yaml:"next"` } type config struct { Tree *recursive `yaml:"tree"` Dynamic any `yaml:"dynamic"` } cyclic := &recursive{} cyclic.Next = cyclic schema := mustSchemaFor[config](t) acyclic := config{Tree: &recursive{Next: &recursive{}}} if _, issue := projectConfiguration(schema, reflect.ValueOf(acyclic), defaultProjectionLimits()); issue != nil { t.Fatalf("acyclic recursive projection issue = %#v", issue) } if _, issue := cloneConfiguration(schema, reflect.ValueOf(acyclic), defaultProjectionLimits()); issue != nil { t.Fatalf("acyclic recursive clone issue = %#v", issue) } value := config{Tree: cyclic} if _, issue := projectConfiguration(schema, reflect.ValueOf(value), defaultProjectionLimits()); issue == nil || issue.code != CodeSchemaCycle || issue.path.String() != "/tree/next" { t.Fatalf("projection cycle issue = %#v", issue) } if _, issue := cloneConfiguration(schema, reflect.ValueOf(value), defaultProjectionLimits()); issue == nil || issue.code != CodeSchemaCycle { t.Fatalf("clone cycle issue = %#v", issue) } dynamicCycle := map[string]any{} dynamicCycle["self"] = dynamicCycle value = config{Dynamic: dynamicCycle} if _, issue := projectConfiguration(schema, reflect.ValueOf(value), defaultProjectionLimits()); issue == nil || issue.code != CodeSchemaCycle { t.Fatalf("dynamic cycle issue = %#v", issue) } value = config{Dynamic: make(chan int)} if _, issue := projectConfiguration(schema, reflect.ValueOf(value), defaultProjectionLimits()); issue == nil || issue.code != CodeSchemaDynamicType { t.Fatalf("dynamic type issue = %#v", issue) } if _, issue := cloneConfiguration(schema, reflect.ValueOf(value), defaultProjectionLimits()); issue == nil || issue.code != CodeSchemaDynamicType { t.Fatalf("dynamic clone issue = %#v", issue) } } func TestProjectionRejectsNonFiniteLimitsAndCodecFailuresSafely(t *testing.T) { t.Parallel() type numbers struct { Value float64 `yaml:"value"` } numberSchema := mustSchemaFor[numbers](t) for _, value := range []float64{math.NaN(), math.Inf(1), math.Inf(-1)} { if _, issue := projectConfiguration(numberSchema, reflect.ValueOf(numbers{Value: value}), defaultProjectionLimits()); issue == nil || issue.code != CodeSchemaNonFinite { t.Fatalf("non-finite issue = %#v", issue) } } type codecConfig struct { Value failingText `yaml:"value"` } codecSchema := mustSchemaFor[codecConfig](t) if _, issue := projectConfiguration(codecSchema, reflect.ValueOf(codecConfig{}), defaultProjectionLimits()); issue == nil || issue.code != CodeSchemaCodec || strings.Contains(issue.message, "CANARY") { t.Fatalf("codec projection issue = %#v", issue) } type reconstructConfig struct { Value reconstructFailText `yaml:"value"` } reconstructSchema := mustSchemaFor[reconstructConfig](t) if _, issue := cloneConfiguration(reconstructSchema, reflect.ValueOf(reconstructConfig{Value: "safe-name"}), defaultProjectionLimits()); issue == nil || issue.code != CodeSchemaCodec || strings.Contains(issue.message, "CANARY") { t.Fatalf("codec clone issue = %#v", issue) } type simple struct { Value string `yaml:"value"` } simpleSchema := mustSchemaFor[simple](t) if _, issue := projectConfiguration(simpleSchema, reflect.ValueOf(simple{}), projectionLimits{depth: 1, nodes: 3}); issue != nil { t.Fatalf("exact node limit rejected: %#v", issue) } if _, issue := projectConfiguration(simpleSchema, reflect.ValueOf(simple{}), projectionLimits{depth: 1, nodes: 2}); issue == nil || issue.code != CodeSchemaProjectionLimit { t.Fatalf("node overflow issue = %#v", issue) } if _, issue := projectConfiguration(simpleSchema, reflect.ValueOf(simple{}), projectionLimits{depth: 0, nodes: 3}); issue == nil || issue.code != CodeSchemaProjectionLimit { t.Fatalf("depth overflow issue = %#v", issue) } } func TestInlineProjectionAndCloneUsePhysicalMembers(t *testing.T) { t.Parallel() type Embedded struct { Name string `yaml:"name"` Items map[string]string `yaml:"items"` } type config struct { *Embedded `yaml:",inline"` Extras map[string]any `yaml:",inline"` } value := config{ Embedded: &Embedded{Name: "value", Items: map[string]string{"a": "b"}}, Extras: map[string]any{"extra": []string{"one"}}, } schema := mustSchemaFor[config](t) projected, issue := projectConfiguration(schema, reflect.ValueOf(value), defaultProjectionLimits()) if issue != nil || !hasMappingKey(projected, "name") || !hasMappingKey(projected, "items") || !hasMappingKey(projected, "extra") { t.Fatalf("inline projection = %#v, issue=%#v", projected, issue) } clonedValue, issue := cloneConfiguration(schema, reflect.ValueOf(value), defaultProjectionLimits()) if issue != nil { t.Fatal(issue.message) } cloned := clonedValue.Interface().(config) value.Items["a"] = "changed" value.Extras["extra"].([]string)[0] = "changed" if cloned.Items["a"] != "b" || cloned.Extras["extra"].([]string)[0] != "one" { t.Fatal("inline clone retained aliases") } value.Extras["name"] = "collision" if _, issue := projectConfiguration(schema, reflect.ValueOf(value), defaultProjectionLimits()); issue == nil || issue.code != CodeSchemaAmbiguousField || issue.path.String() != "/name" { t.Fatalf("inline collision issue = %#v", issue) } } func TestCompareDocumentTreesTracksSemanticChanges(t *testing.T) { t.Parallel() type nested struct { Value string `yaml:"value"` } type config struct { Nested nested `yaml:"nested"` Mapping map[string]int `yaml:"mapping"` Sequence []string `yaml:"sequence"` Maybe map[string]int `yaml:"maybe"` } schema := mustSchemaFor[config](t) before, issue := projectConfiguration(schema, reflect.ValueOf(config{ Nested: nested{Value: "before"}, Mapping: map[string]int{"remove": 1, "same": 2}, Sequence: []string{"a", "b"}, }), defaultProjectionLimits()) if issue != nil { t.Fatal(issue.message) } after, issue := projectConfiguration(schema, reflect.ValueOf(config{ Nested: nested{Value: "after"}, Mapping: map[string]int{"add": 3, "same": 2}, Sequence: []string{"a", "changed"}, Maybe: map[string]int{}, }), defaultProjectionLimits()) if issue != nil { t.Fatal(issue.message) } deltas := compareConfigurationValues(before, after) want := []struct { path string kind documentDeltaKind }{ {path: "/mapping/add", kind: documentAdded}, {path: "/mapping/remove", kind: documentRemoved}, {path: "/maybe", kind: documentReplaced}, {path: "/nested/value", kind: documentReplaced}, {path: "/sequence", kind: documentReplaced}, } if len(deltas) != len(want) { t.Fatalf("deltas = %#v", deltas) } for i := range want { if deltas[i].path.String() != want[i].path || deltas[i].kind != want[i].kind { t.Fatalf("delta[%d] = %#v, want %#v", i, deltas[i], want[i]) } } for _, delta := range deltas { if strings.HasPrefix(delta.path.String(), "/sequence/") { t.Fatal("sequence comparison created index correspondence") } } if equal := compareConfigurationValues(after, document.Clone(after)); len(equal) != 0 { t.Fatalf("equal trees changed = %#v", equal) } } func mustSchemaFor[T any](t *testing.T) *schemaDescriptor { t.Helper() schema, issue := testBuildSchema(reflect.TypeFor[T]()) if issue != nil { t.Fatalf("testBuildSchema() issue = %#v", issue) } return schema } func mappingValue(t *testing.T, node *document.Node, key string) *document.Node { t.Helper() for _, entry := range node.Mapping { if entry.Key == key { return entry.Value } } t.Fatalf("mapping key %q missing", key) return nil } func hasMappingKey(node *document.Node, key string) bool { for _, entry := range node.Mapping { if entry.Key == key { return true } } return false } func assertDocument(t *testing.T, node *document.Node, kind document.Kind, text string) { t.Helper() if node.Kind != kind || node.Text != text { t.Fatalf("node = %#v, want kind=%v text=%q", node, kind, text) } }