Files
ConfigKit/internal/engine/compose_test.go

729 lines
31 KiB
Go

package engine
import (
"fmt"
"reflect"
"strings"
"testing"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
func TestComposeWideRegisteredCollectionsPreservesEvidenceAndReplay(t *testing.T) {
registrations := make([]sequenceIdentityRegistration, 0, 128)
var inputs []*document.Node
for source := 0; source < 3; source++ {
var text strings.Builder
for collection := 0; collection < 128; collection++ {
key := fmt.Sprintf("records%d", collection)
registrations = append(registrations, sequenceIdentityRegistration{
selector: sequenceIdentitySelector{literalSequenceSelector(key)}, identityFields: []string{"id"},
})
fmt.Fprintf(&text, "%s: [{id: source%d}]\n", key, source)
}
inputs = append(inputs, parseCompositionDocument(t, fmt.Sprintf("source-%d", source), text.String()))
}
policy := mustCompileSequenceIdentityPolicy(t, registrations)
composed, diagnostics := composeDocumentsWithPolicy(policy, inputs...)
if len(diagnostics) != 0 {
t.Fatal(diagnostics)
}
working := &document.Node{Kind: document.Mapping}
for _, application := range composed.applications {
applyHistoryApplication(&working, application, new(mappingIndex))
}
if !configurationValuesEqual(working, composed.root) {
t.Fatal("wide registered collections replay differs from assembly")
}
for collection := 0; collection < 128; collection++ {
path := Keys(fmt.Sprintf("records%d", collection)).Index(2).Key("id")
origin := composed.origins[path.String()]
if len(origin.Locations) != 1 || origin.Locations[0].SourceID != "source-2" {
t.Fatalf("shifted origin at %s = %#v", path.String(), origin)
}
local, ok := historyApplicationLocalPath(composed.applications[2], path)
if !ok || local.String() != Keys(fmt.Sprintf("records%d", collection)).Index(0).Key("id").String() {
t.Fatal("wide local path projection failed")
}
}
clone := cloneComposedDocument(composed)
for _, original := range composed.applications[0].sequences {
pointer := original.path.String()
copy := clone.applications[0].sequencesByPath[pointer]
copy.path.segments[0].key = "changed"
if composed.applications[0].sequencesByPath[pointer].path.String() != pointer {
t.Fatal("history path index aliases cloned state")
}
break
}
}
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 := compareConfigurationValues(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 TestComposeDocumentsKeepsFirstConflictAndAppendedEntryOrder(t *testing.T) {
first := parseCompositionDocument(t, "source-1", "catalog:\n first: 1\n shared: one\n later: one\n")
second := parseCompositionDocument(t, "source-2", "catalog:\n second: 2\n shared: two\n later: two\n")
third := parseCompositionDocument(t, "source-3", "catalog:\n third: 3\n shared: three\n")
_, diagnostics := composeDocuments(first, second, third)
assertCompositionConflict(t, diagnostics, "/catalog/shared", []string{"source-1", "source-2", "source-3"})
composed, diagnostics := composeDocuments(
parseCompositionDocument(t, "source-1", "catalog:\n first: 1\n"),
parseCompositionDocument(t, "source-2", "catalog:\n second: 2\n"),
parseCompositionDocument(t, "source-3", "catalog:\n third: 3\n"),
)
if len(diagnostics) != 0 {
t.Fatalf("composeDocuments() diagnostics = %#v", diagnostics)
}
entries := composed.root.Mapping[0].Value.Mapping
got := make([]string, len(entries))
for index, entry := range entries {
got[index] = entry.Key
}
if want := []string{"first", "second", "third"}; !reflect.DeepEqual(got, want) {
t.Fatalf("catalog keys = %#v, want %#v", got, want)
}
}
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 TestComposeDocumentsAppendsRegisteredSequencesAndReturnsRanges(t *testing.T) {
first := parseCompositionDocument(t, "source-1", "records: [{id: '', left: 1}, {id: ' spaced ', right: 1}]\nother: [one]\n")
second := parseCompositionDocument(t, "source-2", "records: [{id: ' ', text: second}]\n")
firstBefore, secondBefore := document.Clone(first), document.Clone(second)
policy := mustCompileSequenceIdentityPolicy(t, []sequenceIdentityRegistration{{selector: sequenceIdentitySelector{literalSequenceSelector("records")}, identityFields: []string{"id"}}})
composed, diagnostics := composeDocumentsWithPolicy(policy, first, second)
if len(diagnostics) != 0 {
t.Fatalf("composeDocumentsWithPolicy() diagnostics = %#v", diagnostics)
}
records, ok := documentAtPath(composed.root, Keys("records"))
if !ok || records.Kind != document.Sequence || len(records.Sequence) != 3 {
t.Fatalf("records = %#v, want three appended records", records)
}
if got := composed.sequenceRanges; !reflect.DeepEqual(got, []sequenceRange{
{sourceIndex: 0, path: Keys("records"), start: 0, end: 2},
{sourceIndex: 1, path: Keys("records"), start: 2, end: 3},
}) {
t.Fatalf("sequence ranges = %#v", got)
}
if !reflect.DeepEqual(first, firstBefore) || !reflect.DeepEqual(second, secondBefore) {
t.Fatal("registered composition mutated an input")
}
composed.root.Mapping[0].Value.Sequence[0].Mapping[1].Value.Text = "changed"
if !reflect.DeepEqual(first, firstBefore) || !reflect.DeepEqual(second, secondBefore) {
t.Fatal("composed sequence aliases an input")
}
}
func TestComposeDocumentsProjectsSequenceOriginsToAssignedIndexes(t *testing.T) {
first := parseCompositionDocument(t, "source-1", `mounted:
records:
- id: first
name: first-name
escaped/~:
"7":
- id: escaped-first
name: escaped-first-name
`)
second := parseCompositionDocument(t, "source-2", `mounted:
records:
- id: second
name: second-name
escaped/~:
"7":
- id: escaped-second
name: escaped-second-name
`)
firstBefore, secondBefore := document.Clone(first), document.Clone(second)
policy := mustCompileSequenceIdentityPolicy(t, []sequenceIdentityRegistration{
{selector: sequenceIdentitySelector{literalSequenceSelector("mounted"), literalSequenceSelector("records")}, identityFields: []string{"id"}},
{selector: sequenceIdentitySelector{literalSequenceSelector("escaped/~"), literalSequenceSelector("7")}, identityFields: []string{"id"}},
})
composed, diagnostics := composeDocumentsWithPolicy(policy, first, second)
if len(diagnostics) != 0 {
t.Fatalf("composeDocumentsWithPolicy() diagnostics = %#v", diagnostics)
}
for _, test := range []struct {
path Path
want Location
}{
{path: Keys("mounted", "records").Index(1).Key("name"), want: Location{SourceID: "source-2", Line: 4, Column: 13}},
{path: Keys("escaped/~", "7").Index(1).Key("name"), want: Location{SourceID: "source-2", Line: 8, Column: 13}},
} {
path, want := test.path, test.want
origin, ok := composed.origins[path.String()]
if !ok || !reflect.DeepEqual(origin.Locations, []Location{want}) {
t.Errorf("origin at %s = %#v, want source-specific location %#v", path, origin, want)
}
}
for _, test := range []struct {
path Path
wantSources []string
}{
{path: Keys("mounted", "records"), wantSources: []string{"source-1", "source-2"}},
{path: Keys("escaped/~", "7"), wantSources: []string{"source-1", "source-2"}},
} {
path, wantSources := test.path, test.wantSources
origin := composed.origins[path.String()]
gotSources := make([]string, len(origin.Locations))
for index, location := range origin.Locations {
gotSources[index] = location.SourceID
}
if !reflect.DeepEqual(gotSources, wantSources) {
t.Errorf("container origin at %s = %#v, want contributors %#v", path, gotSources, wantSources)
}
}
if !reflect.DeepEqual(first, firstBefore) || !reflect.DeepEqual(second, secondBefore) {
t.Fatal("composition projection mutated an input")
}
copy := cloneComposedDocument(composed)
copy.origins[Keys("mounted", "records").Index(1).Key("name").String()].Locations[0].SourceID = "changed"
for index := range copy.claims {
if copy.claims[index].path.String() == "/mounted/records" {
copy.claims[index].path.segments[0].key = "changed"
break
}
}
copy.sequenceRanges[0].path.segments[0].key = "changed"
if got := composed.origins[Keys("mounted", "records").Index(1).Key("name").String()].Locations[0].SourceID; got != "source-2" {
t.Fatalf("copied origin mutation changed source evidence: %q", got)
}
foundClaim := false
for _, claim := range composed.claims {
if claim.path.String() == "/mounted/records" {
foundClaim = true
break
}
}
if !foundClaim {
t.Fatal("copied claim mutation changed source path")
}
if got := composed.sequenceRanges[0].path.String(); got != "/mounted/records" {
t.Fatalf("copied range mutation changed assigned path: %q", got)
}
}
func TestProjectedCompositionOriginsUseNoncontiguousItemMappings(t *testing.T) {
lowerRoot := parseCompositionDocument(t, "lower", `records:
- id: first
label: equal
- id: middle
- id: last
nested: {old: lower}
`)
higherRoot := parseCompositionDocument(t, "higher", `records:
- id: last
nested: {new: higher}
- id: first
label: equal
- id: added
`)
lower, _ := documentAtPath(lowerRoot, Keys("records"))
higher, _ := documentAtPath(higherRoot, Keys("records"))
combined, diagnostics := combineSequence(
lower, higher, sequenceCombinationPolicy(sequenceModeUpsert, "id"),
sequenceCombinationContext{destination: Keys("records")},
)
if len(diagnostics) != 0 {
t.Fatalf("combine diagnostics = %#v", diagnostics)
}
inputs := []*document.Node{lowerRoot, higherRoot}
ranges := []sequenceRange{
{
sourceIndex: 0, path: Keys("records"), start: 0, end: 3,
items: combined.lowerItems,
}, {
sourceIndex: 1, path: Keys("records"), start: 0, end: 3,
items: combined.higherItems,
},
}
origins := projectedCompositionOrigins(inputs, ranges)
if got, want := sequenceIdentityValues(combined.value, "id"), []string{"first", "middle", "last", "added"}; !reflect.DeepEqual(got, want) {
t.Fatalf("combined IDs = %#v, want %#v", got, want)
}
for path, want := range map[string][]string{
"/records/0/label": {"higher"},
"/records/1/id": {"lower"},
"/records/2/nested/new": {"higher"},
"/records/3/id": {"higher"},
"/records/0/nested/new": nil,
"/records/2/nested/old": nil,
} {
origin := origins[path]
var got []string
for _, location := range origin.Locations {
got = append(got, location.SourceID)
}
if !reflect.DeepEqual(got, want) {
t.Errorf("origin sources at %s = %#v, want %#v", path, got, want)
}
}
}
func TestCloneSequenceRangesPreservesPreparedProjection(t *testing.T) {
ranges := []sequenceRange{{
start: 0, end: 1,
projection: newSequenceItemProjectionIndex([]sequenceItemProjection{{inputIndex: 0, outputIndex: 4}}),
}}
cloned := cloneSequenceRanges(ranges)
if output, ok := cloned[0].outputForInput(0); !ok || output != 4 {
t.Fatalf("cloned projection = %d, %v; want output index 4", output, ok)
}
cloned[0].projection.outputByInput[0] = 9
if output, ok := ranges[0].outputForInput(0); !ok || output != 4 {
t.Fatalf("cloned projection aliases original: %d, %v", output, ok)
}
}
func TestComposeDocumentsValidatesRegisteredSequenceSingletons(t *testing.T) {
tests := []struct {
name string
body string
path string
wantCode Code
}{
{name: "nonmapping item", body: "records: [plain]\n", path: "/records/0", wantCode: CodeComposeInvalid},
{name: "missing identity", body: "records: [{name: plain}]\n", path: "/records/0/id", wantCode: CodeComposeInvalid},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, diagnostics := composeDocumentsWithPolicy(
mustCompileSequenceIdentityPolicy(t, []sequenceIdentityRegistration{{selector: sequenceIdentitySelector{literalSequenceSelector("records")}, identityFields: []string{"id"}}}),
parseCompositionDocument(t, "source-1", test.body),
)
if len(diagnostics) != 1 || diagnostics[0].Code != test.wantCode || diagnostics[0].Path.String() != test.path {
t.Fatalf("diagnostics = %#v, want %s at %s", diagnostics, test.wantCode, test.path)
}
if diagnostics[0].Primary == nil {
t.Fatal("malformed sequence diagnostic has no source location")
}
})
}
}
func TestComposeDocumentsUsesLowerIdentityForStrictPeers(t *testing.T) {
path := Keys("records")
lower := collectionPolicyScope{collections: map[string]collectionPolicyEntry{
path.String(): {
destination: path,
policy: collectionPolicy{identityFields: []string{"id"}, identityPresent: true},
},
}}
composed, diagnostics := composeDocumentsWithCollectionPolicy(
sequencePolicy{}, lower, nil,
parseCompositionDocument(t, "source-1", "records: [{id: first}]\n"),
parseCompositionDocument(t, "source-2", "records: [{id: second}]\n"),
)
if len(diagnostics) != 0 {
t.Fatalf("compose strict peers with lower identity: %#v", diagnostics)
}
value, present := documentAtPath(composed.root, path)
if !present || value.Kind != document.Sequence || len(value.Sequence) != 2 {
t.Fatalf("composed records = %#v, %v; want two strict peer records", value, present)
}
resolved, present := composed.collectionPolicies.collections[path.String()]
if !present || !resolved.policy.identityPresent || !reflect.DeepEqual(resolved.policy.identityFields, []string{"id"}) {
t.Fatalf("composed collection policy = %#v, %v; want retained lower identity", resolved, present)
}
clone := cloneComposedDocument(composed)
resolved.policy.identityFields[0] = "mutated"
if !reflect.DeepEqual(clone.collectionPolicies.collections[path.String()].policy.identityFields, []string{"id"}) {
t.Fatal("cloned composition shares mutable collection identity fields")
}
}
func TestComposeDocumentsResolvesDeclarationsBeforePeerComposition(t *testing.T) {
path := Keys("records")
declaration := func(source string, line int, fields ...string) sequenceDeclarationSpec {
location := Location{SourceID: source, Line: line}
return sequenceDeclarationSpec{
destination: path, identityFields: fields, identityPresent: true,
identityLocation: &location,
}
}
_, diagnostics := composeDocumentsWithCollectionPolicy(
sequencePolicy{}, collectionPolicyScope{},
[]sequenceDeclarationSpec{declaration("source-1", 2, "id"), declaration("source-2", 4, "name")},
parseCompositionDocument(t, "source-1", "left: true\n"),
parseCompositionDocument(t, "source-2", "right: true\n"),
)
if len(diagnostics) != 1 || diagnostics[0].Code != CodeSequenceConflict || diagnostics[0].Path.String() != "/records" {
t.Fatalf("conflicting policy-only declarations = %#v", diagnostics)
}
if diagnostics[0].Primary == nil || len(diagnostics[0].Related) != 1 {
t.Fatalf("declaration conflict lacks both locations: %#v", diagnostics[0])
}
composed, diagnostics := composeDocumentsWithCollectionPolicy(
sequencePolicy{}, collectionPolicyScope{},
[]sequenceDeclarationSpec{declaration("source-1", 2, "tenant", "id"), declaration("source-2", 4, "id", "tenant")},
parseCompositionDocument(t, "source-1", "records: [{tenant: true, id: null}, {tenant: \"alpha\", id: 1}]\n"),
parseCompositionDocument(t, "source-2", "records: [{id: 1.0, tenant: \"beta\"}, {id: \"1\", tenant: \"alpha\"}]\n"),
)
if len(diagnostics) != 0 {
t.Fatalf("compose normalized compound identities: %#v", diagnostics)
}
value, present := documentAtPath(composed.root, path)
if !present || len(value.Sequence) != 4 {
t.Fatalf("compound identity records = %#v, %v; want four distinct records", value, present)
}
_, diagnostics = composeDocumentsWithCollectionPolicy(
sequencePolicy{}, collectionPolicyScope{},
[]sequenceDeclarationSpec{declaration("source-1", 2, "id")},
parseCompositionDocument(t, "source-1", "records: [{id: 1}]\n"),
parseCompositionDocument(t, "source-2", "records: [{id: 1.0}]\n"),
)
if len(diagnostics) != 1 || diagnostics[0].Code != CodeComposeConflict || diagnostics[0].Path.String() != "/records" {
t.Fatalf("equivalent numeric peer identities = %#v", diagnostics)
}
}
func TestComposeDocumentsReplacementDoesNotBorrowLowerYAMLIdentity(t *testing.T) {
path := Keys("records")
lower := collectionPolicyScope{collections: map[string]collectionPolicyEntry{
path.String(): {
destination: path,
policy: collectionPolicy{identityFields: []string{"id"}, identityPresent: true},
},
}}
declaration := sequenceDeclarationSpec{destination: path, mode: sequenceModeReplace, modePresent: true}
_, diagnostics := composeDocumentsWithCollectionPolicy(
sequencePolicy{}, lower, []sequenceDeclarationSpec{declaration},
parseCompositionDocument(t, "source-1", "records: [{name: first}]\n"),
parseCompositionDocument(t, "source-2", "records: [{name: second}]\n"),
)
if len(diagnostics) != 1 || diagnostics[0].Code != CodeComposeConflict || diagnostics[0].Path.String() != "/records" {
t.Fatalf("replacement peers borrowing lower identity = %#v", diagnostics)
}
identityReplacement := sequenceDeclarationSpec{
destination: path, identityFields: []string{"name"}, identityPresent: true,
mode: sequenceModeReplace, modePresent: true,
}
composed, diagnostics := composeDocumentsWithCollectionPolicy(
sequencePolicy{}, lower, []sequenceDeclarationSpec{identityReplacement},
parseCompositionDocument(t, "source-1", "records: [{name: first}]\n"),
parseCompositionDocument(t, "source-2", "records: [{name: second}]\n"),
)
if len(diagnostics) != 0 {
t.Fatalf("replacement with a new identity declaration: %#v", diagnostics)
}
resolved := composed.collectionPolicies.collections[path.String()].policy
if !reflect.DeepEqual(resolved.identityFields, []string{"name"}) || !resolved.identityPresent {
t.Fatalf("replacement collection identity = %#v; want name", resolved)
}
_, diagnostics = composeDocumentsWithCollectionPolicy(
mustCompileSequenceIdentityPolicy(t, []sequenceIdentityRegistration{{selector: sequenceIdentitySelector{literalSequenceSelector("records")}, identityFields: []string{"id"}}}), lower,
[]sequenceDeclarationSpec{declaration},
parseCompositionDocument(t, "source-1", "records: [{name: replacement}]\n"),
)
if len(diagnostics) != 1 || diagnostics[0].Code != CodeComposeInvalid || diagnostics[0].Path.String() != "/records/0/id" {
t.Fatalf("replacement ignored application identity constraint = %#v", diagnostics)
}
}
func TestComposeDocumentsReportsAssignedPathForMalformedRegisteredSequenceContribution(t *testing.T) {
_, diagnostics := composeDocumentsWithPolicy(
mustCompileSequenceIdentityPolicy(t, []sequenceIdentityRegistration{{selector: sequenceIdentitySelector{literalSequenceSelector("records")}, identityFields: []string{"id"}}}),
parseCompositionDocument(t, "source-1", "records: [{id: first}]\n"),
parseCompositionDocument(t, "source-2", "records: [{name: malformed}]\n"),
)
if len(diagnostics) != 1 || diagnostics[0].Code != CodeComposeInvalid || diagnostics[0].Path.String() != "/records/1/id" {
t.Fatalf("diagnostics = %#v, want invalid identity field at /records/1/id", diagnostics)
}
if diagnostics[0].Primary == nil || diagnostics[0].Primary.SourceID != "source-2" {
t.Fatalf("malformed item location = %#v, want source-2", diagnostics[0].Primary)
}
}
func TestComposeDocumentsRejectsRegisteredDuplicateIdentities(t *testing.T) {
tests := []struct {
name string
first string
second string
within bool
}{
{name: "within identical bodies", first: "records: [{id: '', value: same}, {id: '', value: same}]\n", within: true},
{name: "within disjoint bodies", first: "records: [{id: same, left: 1}, {id: same, right: 2}]\n", within: true},
{name: "across identical bodies", first: "records: [{id: same, value: same}]\n", second: "records: [{id: same, value: same}]\n"},
{name: "across disjoint bodies", first: "records: [{id: same, left: 1}]\n", second: "records: [{id: same, right: 2}]\n"},
{name: "empty identity across lists", first: "records: [{id: ''}]\n", second: "records: [{id: ''}]\n"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
inputs := []*document.Node{parseCompositionDocument(t, "source-1", test.first)}
if !test.within {
inputs = append(inputs, parseCompositionDocument(t, "source-2", test.second))
}
_, diagnostics := composeDocumentsWithPolicy(
mustCompileSequenceIdentityPolicy(t, []sequenceIdentityRegistration{{selector: sequenceIdentitySelector{literalSequenceSelector("records")}, identityFields: []string{"id"}}}), inputs...,
)
if len(diagnostics) != 1 || diagnostics[0].Code != CodeComposeConflict || diagnostics[0].Path.String() != "/records" {
t.Fatalf("diagnostics = %#v, want duplicate conflict at /records", diagnostics)
}
if diagnostics[0].Primary == nil || len(diagnostics[0].Related) != 1 {
t.Fatalf("duplicate locations = %#v", diagnostics[0])
}
for _, secret := range []string{"same", "left", "right", "value"} {
if strings.Contains(diagnostics[0].Message, secret) {
t.Fatalf("diagnostic leaks %q: %q", secret, diagnostics[0].Message)
}
}
})
}
}
func TestComposeDocumentsSkipsMissingAndNonSequenceRegistrations(t *testing.T) {
policy := mustCompileSequenceIdentityPolicy(t, []sequenceIdentityRegistration{
{selector: sequenceIdentitySelector{literalSequenceSelector("absent")}, identityFields: []string{"id"}},
{selector: sequenceIdentitySelector{literalSequenceSelector("not-a-sequence")}, identityFields: []string{"id"}},
{selector: sequenceIdentitySelector{literalSequenceSelector("catalog")}, identityFields: []string{"id"}},
{selector: sequenceIdentitySelector{literalSequenceSelector("catalog"), literalSequenceSelector("records")}, identityFields: []string{"id"}},
})
composed, diagnostics := composeDocumentsWithPolicy(policy, parseCompositionDocument(t, "source-1", `
not-a-sequence: null
catalog:
records: [{id: present}]
`))
if len(diagnostics) != 0 {
t.Fatalf("composeDocumentsWithPolicy() diagnostics = %#v", diagnostics)
}
if len(composed.sequenceRanges) != 1 || composed.sequenceRanges[0].path.String() != "/catalog/records" {
t.Fatalf("sequence ranges = %#v, want only the reached descendant sequence", composed.sequenceRanges)
}
composed, diagnostics = composeDocumentsWithPolicy(
mustCompileSequenceIdentityPolicy(t, []sequenceIdentityRegistration{{selector: sequenceIdentitySelector{literalSequenceSelector("records")}, identityFields: []string{"id"}}}),
parseCompositionDocument(t, "source-1", "records: []\n"),
parseCompositionDocument(t, "source-2", "records: []\n"),
)
if len(diagnostics) != 0 || len(composed.root.Mapping[0].Value.Sequence) != 0 {
t.Fatalf("empty registered sequences = (%#v, %#v)", composed, diagnostics)
}
if got := composed.sequenceRanges; !reflect.DeepEqual(got, []sequenceRange{
{sourceIndex: 0, path: Keys("records"), start: 0, end: 0},
{sourceIndex: 1, path: Keys("records"), start: 0, end: 0},
}) {
t.Fatalf("empty sequence ranges = %#v", got)
}
}
func TestComposeDocumentsKeepsUnregisteredSequenceConflicts(t *testing.T) {
_, diagnostics := composeDocumentsWithPolicy(
mustCompileSequenceIdentityPolicy(t, []sequenceIdentityRegistration{{selector: sequenceIdentitySelector{literalSequenceSelector("registered")}, identityFields: []string{"id"}}}),
parseCompositionDocument(t, "source-1", "unregistered: [one]\n"),
parseCompositionDocument(t, "source-2", "unregistered: [two]\n"),
)
assertCompositionConflict(t, diagnostics, "/unregistered", []string{"source-1", "source-2"})
}
func TestComposeDocumentsKeepsNonSequenceRegisteredOverlapsConflicting(t *testing.T) {
tests := []struct {
name string
first string
second string
}{
{name: "mapping and sequence", first: "records: {id: one}\n", second: "records: [{id: two}]\n"},
{name: "null and sequence", first: "records: null\n", second: "records: [{id: two}]\n"},
{name: "scalar and sequence", first: "records: value\n", second: "records: [{id: two}]\n"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, diagnostics := composeDocumentsWithPolicy(
mustCompileSequenceIdentityPolicy(t, []sequenceIdentityRegistration{{selector: sequenceIdentitySelector{literalSequenceSelector("records")}, identityFields: []string{"id"}}}),
parseCompositionDocument(t, "source-1", test.first),
parseCompositionDocument(t, "source-2", test.second),
)
assertCompositionConflict(t, diagnostics, "/records", []string{"source-1", "source-2"})
})
}
}
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)
}
}