729 lines
31 KiB
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)
|
|
}
|
|
}
|