379 lines
16 KiB
Go
379 lines
16 KiB
Go
package engine
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"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 := 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 := sequencePolicy{identityFields: map[string]string{"/records": "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 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", wantCode: CodeComposeInvalid},
|
|
{name: "null identity", body: "records: [{id: null}]\n", path: "/records/0/id", wantCode: CodeComposeInvalid},
|
|
{name: "numeric identity", body: "records: [{id: 12}]\n", path: "/records/0/id", wantCode: CodeComposeInvalid},
|
|
{name: "boolean identity", body: "records: [{id: false}]\n", path: "/records/0/id", wantCode: CodeComposeInvalid},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
_, diagnostics := composeDocumentsWithPolicy(
|
|
sequencePolicy{identityFields: map[string]string{"/records": "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 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(
|
|
sequencePolicy{identityFields: map[string]string{"/records": "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 := sequencePolicy{identityFields: map[string]string{
|
|
"/absent": "id", "/not-a-sequence": "id", "/catalog": "id", "/catalog/records": "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(
|
|
sequencePolicy{identityFields: map[string]string{"/records": "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(
|
|
sequencePolicy{identityFields: map[string]string{"/registered": "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(
|
|
sequencePolicy{identityFields: map[string]string{"/records": "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)
|
|
}
|
|
}
|