package configkit import ( "reflect" "testing" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) func TestComposeDocumentsJoinsDisjointMappingsAndTracksClaims(t *testing.T) { base := parseCompositionDocument(t, "source-1", `services: api: enabled: true empty: {} `) fragment := parseCompositionDocument(t, "source-2", `services: worker: enabled: false values: [one, two] `) baseBefore := document.Clone(base) fragmentBefore := document.Clone(fragment) composed, diagnostics := composeDocuments(base, fragment) if len(diagnostics) != 0 { t.Fatalf("composeDocuments() diagnostics = %#v", diagnostics) } for _, path := range []Path{ Keys("services", "api", "enabled"), Keys("services", "worker", "enabled"), Keys("empty"), Keys("values"), } { if _, ok := documentAtPath(composed.root, path); !ok { t.Errorf("composed document is missing %s", path.String()) } } serviceOrigin := composed.origins[Keys("services").String()] wantLocations := []Location{ {SourceID: "source-1", Line: 2, Column: 3}, {SourceID: "source-2", Line: 2, Column: 3}, } if !reflect.DeepEqual(serviceOrigin, Origin{Layer: LayerBase, Locations: wantLocations}) { t.Fatalf("services origin = %#v, want composite origin", serviceOrigin) } wantClaimPaths := []string{"", "/services", "/services/api", "/services/api/enabled", "/empty", "", "/services", "/services/worker", "/services/worker/enabled", "/values"} gotClaimPaths := make([]string, len(composed.claims)) for index, claim := range composed.claims { gotClaimPaths[index] = claim.path.String() } if !reflect.DeepEqual(gotClaimPaths, wantClaimPaths) { t.Fatalf("claim paths = %#v, want %#v", gotClaimPaths, wantClaimPaths) } reversed, reversedDiagnostics := composeDocuments(fragment, base) if len(reversedDiagnostics) != 0 { t.Fatalf("reversed composeDocuments() diagnostics = %#v", reversedDiagnostics) } if deltas := compareDocumentTrees(composed.root, reversed.root); len(deltas) != 0 { t.Fatalf("input order changed effective values: %#v", deltas) } if !reflect.DeepEqual(base, baseBefore) || !reflect.DeepEqual(fragment, fragmentBefore) { t.Fatal("composition mutated an input document") } composed.root.Mapping[0].Value.Mapping[0].Value.Mapping[0].Value.Text = "changed" if !reflect.DeepEqual(base, baseBefore) || !reflect.DeepEqual(fragment, fragmentBefore) { t.Fatal("composed document aliases an input document") } } func TestComposeDocumentsRejectsEveryPresentNonMappingOverlap(t *testing.T) { tests := []struct { name string left string right string path string }{ {name: "equal scalar", left: "value: true\n", right: "value: true\n", path: "/value"}, {name: "different scalar", left: "value: true\n", right: "value: false\n", path: "/value"}, {name: "null", left: "value: null\n", right: "value: null\n", path: "/value"}, {name: "sequence", left: "value: [one]\n", right: "value: [two]\n", path: "/value"}, {name: "mapping and scalar", left: "value:\n nested: true\n", right: "value: text\n", path: "/value"}, {name: "scalar and mapping", left: "value: text\n", right: "value:\n nested: true\n", path: "/value"}, {name: "nested keyed entry", left: "items:\n shared:\n left: 1\n", right: "items:\n shared:\n left: 2\n", path: "/items/shared/left"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { left := parseCompositionDocument(t, "source-1", test.left) right := parseCompositionDocument(t, "source-2", test.right) _, diagnostics := composeDocuments(left, right) assertCompositionConflict(t, diagnostics, test.path, []string{"source-1", "source-2"}) }) } } func TestComposeDocumentsReportsAllClaimantsAtExactLocations(t *testing.T) { first := parseCompositionDocument(t, "source-1", "group:\n value: 1\n") second := parseCompositionDocument(t, "source-2", "group:\n value: 2\n") third := parseCompositionDocument(t, "source-3", "group:\n value: 3\n") _, diagnostics := composeDocuments(first, second, third) assertCompositionConflict(t, diagnostics, "/group/value", []string{"source-1", "source-2", "source-3"}) locations := append([]Location{*diagnostics[0].Primary}, diagnostics[0].Related...) for _, location := range locations { if location.Line != 2 || location.Column != 10 { t.Fatalf("claim location = %#v, want line 2 column 10", location) } } _, diagnostics = composeDocuments( parseCompositionDocument(t, "source-1", "group:\n left: 1\n"), parseCompositionDocument(t, "source-2", "group:\n right: 2\n"), parseCompositionDocument(t, "source-3", "group: blocked\n"), ) assertCompositionConflict(t, diagnostics, "/group", []string{"source-1", "source-2", "source-3"}) } func TestComposeDocumentsTreatsEmptyMappingsAsChildlessContributions(t *testing.T) { empty, diagnostics := composeDocuments( parseCompositionDocument(t, "source-1", "{}\n"), parseCompositionDocument(t, "source-2", "{}\n"), ) if len(diagnostics) != 0 || len(empty.root.Mapping) != 0 || len(empty.origins[""].Locations) != 2 { t.Fatalf("empty root composition = (%#v, %#v)", empty, diagnostics) } tests := []struct { name string left string right string }{ {name: "two empty mappings", left: "value: {}\n", right: "value: {}\n"}, {name: "empty then populated", left: "value: {}\n", right: "value: {nested: true}\n"}, {name: "populated then empty", left: "value: {nested: true}\n", right: "value: {}\n"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { composed, diagnostics := composeDocuments( parseCompositionDocument(t, "source-1", test.left), parseCompositionDocument(t, "source-2", test.right), ) if len(diagnostics) != 0 { t.Fatalf("composeDocuments() diagnostics = %#v", diagnostics) } if origin := composed.origins["/value"]; len(origin.Locations) != 2 { t.Fatalf("value origin = %#v, want both mapping locations", origin) } if test.name != "two empty mappings" { if _, ok := documentAtPath(composed.root, Keys("value", "nested")); !ok { t.Fatal("populated mapping contribution was not retained") } } }) } } func TestComposeDocumentsRejectsInvalidBoundaries(t *testing.T) { if composed, diagnostics := composeDocuments(); composed != nil || len(diagnostics) != 1 || diagnostics[0].Code != CodeComposeInvalid { t.Fatalf("empty composition = (%#v, %#v)", composed, diagnostics) } nonMapping := &document.Node{Kind: document.String, Location: document.Location{SourceID: "source-1", Line: 4, Column: 2}} if composed, diagnostics := composeDocuments(nonMapping); composed != nil || len(diagnostics) != 1 || diagnostics[0].Primary == nil || diagnostics[0].Primary.Line != 4 { t.Fatalf("non-mapping composition = (%#v, %#v)", composed, diagnostics) } } func parseCompositionDocument(t *testing.T, sourceID, input string) *document.Node { t.Helper() parsed, diagnostics := parseSource(Bytes(sourceID, []byte(input)), sourceID, defaultParseLimits()) if len(diagnostics) != 0 { t.Fatalf("parse diagnostics = %#v", diagnostics) } return parsed.document } func assertCompositionConflict(t *testing.T, diagnostics []Diagnostic, path string, sources []string) { t.Helper() if len(diagnostics) != 1 { t.Fatalf("diagnostics = %#v, want one conflict", diagnostics) } diagnostic := diagnostics[0] if diagnostic.Code != CodeComposeConflict || diagnostic.Path.String() != path || diagnostic.Primary == nil { t.Fatalf("diagnostic = %#v, want compose conflict at %s", diagnostic, path) } locations := append([]Location{*diagnostic.Primary}, diagnostic.Related...) gotSources := make([]string, len(locations)) for index, location := range locations { gotSources[index] = location.SourceID } if !reflect.DeepEqual(gotSources, sources) { t.Fatalf("claim sources = %#v, want %#v", gotSources, sources) } }