173 lines
8.2 KiB
Go
173 lines
8.2 KiB
Go
package engine
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
)
|
|
|
|
func TestParseSequenceDeclarationPresenceAndRelativeDestination(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
declaration string
|
|
wantIdentity bool
|
|
wantMode bool
|
|
wantIdentityList []string
|
|
wantModeValue sequenceMode
|
|
}{
|
|
{name: "destination only is a no-op", declaration: "destination: [prompts]"},
|
|
{name: "key only", declaration: "destination: [prompts]\n key: id", wantIdentity: true, wantIdentityList: []string{"id"}},
|
|
{name: "mode only", declaration: "destination: [prompts]\n mode: append", wantMode: true, wantModeValue: sequenceModeAppend},
|
|
{name: "combined", declaration: "destination: [prompts]\n key: [namespace, id, id]\n mode: upsert", wantIdentity: true, wantMode: true, wantIdentityList: []string{"id", "namespace"}, wantModeValue: sequenceModeUpsert},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
extracted := extractSequenceDeclarationsYAML(t, "configkit:\n sequences:\n - "+test.declaration+"\n", rootDirectiveContextSource)
|
|
if len(extracted.sequences) != 1 {
|
|
t.Fatalf("extraction = %#v", extracted)
|
|
}
|
|
declaration := extracted.sequences[0]
|
|
if declaration.destination.String() != "/prompts" || declaration.path.String() != "/configkit/sequences/0" {
|
|
t.Fatalf("relative destination or declaration path changed: %#v", declaration)
|
|
}
|
|
if declaration.location.SourceID != "sequence-declarations" || declaration.location.Line == 0 {
|
|
t.Fatalf("declaration source locations were not retained: %#v", declaration)
|
|
}
|
|
if declaration.identityPresent != test.wantIdentity || declaration.modePresent != test.wantMode {
|
|
t.Fatalf("presence = key:%t mode:%t, want key:%t mode:%t", declaration.identityPresent, declaration.modePresent, test.wantIdentity, test.wantMode)
|
|
}
|
|
if !reflect.DeepEqual(declaration.identityFields, test.wantIdentityList) || declaration.mode != test.wantModeValue {
|
|
t.Fatalf("parsed fields/mode = %#v/%d", declaration.identityFields, declaration.mode)
|
|
}
|
|
if declaration.identityPresent && (declaration.identityLocation == nil || declaration.identityLocation.SourceID != "sequence-declarations" || declaration.identityLocation.Line == 0) {
|
|
t.Fatalf("identity location missing: %#v", declaration)
|
|
}
|
|
if declaration.modePresent && (declaration.modeLocation == nil || declaration.modeLocation.SourceID != "sequence-declarations" || declaration.modeLocation.Line == 0) {
|
|
t.Fatalf("mode location missing: %#v", declaration)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseSequenceDeclarationNormalizesEquivalentFieldSets(t *testing.T) {
|
|
extracted := extractSequenceDeclarationsYAML(t, `configkit:
|
|
sequences:
|
|
- destination: [prompts]
|
|
key: [namespace, id, id]
|
|
- destination: [prompts]
|
|
key: [id, namespace]
|
|
`, rootDirectiveContextSource)
|
|
if len(extracted.sequences) != 2 || !reflect.DeepEqual(extracted.sequences[0].identityFields, extracted.sequences[1].identityFields) {
|
|
t.Fatalf("equivalent declarations were not normalized: %#v", extracted.sequences)
|
|
}
|
|
}
|
|
|
|
func TestExtractRootDirectivesCombinesAndRemovesImportsAndSequences(t *testing.T) {
|
|
extracted := extractSequenceDeclarationsYAML(t, `configkit:
|
|
imports: [{path: fragments/base.yaml}]
|
|
sequences: []
|
|
value: true
|
|
`, rootDirectiveContextSource)
|
|
if len(extracted.imports) != 1 || len(extracted.sequences) != 0 {
|
|
t.Fatalf("mixed extraction = %#v", extracted)
|
|
}
|
|
if extracted.body == nil || extracted.body.Kind != document.Mapping || len(extracted.body.Mapping) != 1 || extracted.body.Mapping[0].Key != "value" {
|
|
t.Fatalf("directive controls were not removed from payload: %#v", extracted.body)
|
|
}
|
|
}
|
|
|
|
func TestExtractRootDirectivesAllowsSequencesButRejectsOverlayImports(t *testing.T) {
|
|
_, diagnostics := extractRootDirectives(parseRootDirectiveDocument(t, `configkit:
|
|
imports: [{path: fragments/base.yaml}]
|
|
sequences: [{destination: [prompts], key: id}]
|
|
`), rootDirectiveContextOverlay)
|
|
if len(diagnostics) != 1 || diagnostics[0].Code != CodeOverlayDirective || diagnostics[0].Path.String() != "/configkit/imports" {
|
|
t.Fatalf("overlay diagnostics = %#v, want imports-only rejection", diagnostics)
|
|
}
|
|
|
|
extracted, diagnostics := extractRootDirectives(parseRootDirectiveDocument(t, `configkit:
|
|
sequences: [{destination: [prompts], key: id}]
|
|
`), rootDirectiveContextOverlay)
|
|
if len(diagnostics) != 0 || len(extracted.sequences) != 1 {
|
|
t.Fatalf("overlay sequence extraction = %#v, %#v", extracted, diagnostics)
|
|
}
|
|
}
|
|
|
|
func TestParseSequenceDeclarationRejectsMalformedForms(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
body string
|
|
path string
|
|
}{
|
|
{name: "sequences wrong type", body: "configkit: {sequences: nope}\n", path: "/configkit/sequences"},
|
|
{name: "declaration wrong type", body: "configkit: {sequences: [nope]}\n", path: "/configkit/sequences/0"},
|
|
{name: "missing destination", body: "configkit: {sequences: [{key: id}]}\n", path: "/configkit/sequences/0/destination"},
|
|
{name: "destination wrong type", body: "configkit: {sequences: [{destination: nope}]}\n", path: "/configkit/sequences/0/destination"},
|
|
{name: "reserved destination", body: "configkit: {sequences: [{destination: [configkit]}]}\n", path: "/configkit/sequences/0/destination/0"},
|
|
{name: "key wrong type", body: "configkit: {sequences: [{destination: [prompts], key: true}]}\n", path: "/configkit/sequences/0/key"},
|
|
{name: "empty key set", body: "configkit: {sequences: [{destination: [prompts], key: []}]}\n", path: "/configkit/sequences/0/key"},
|
|
{name: "key member wrong type", body: "configkit: {sequences: [{destination: [prompts], key: [id, null]}]}\n", path: "/configkit/sequences/0/key/1"},
|
|
{name: "mode wrong type", body: "configkit: {sequences: [{destination: [prompts], mode: true}]}\n", path: "/configkit/sequences/0/mode"},
|
|
{name: "unknown mode", body: "configkit: {sequences: [{destination: [prompts], mode: merge}]}\n", path: "/configkit/sequences/0/mode"},
|
|
{name: "unknown declaration field", body: "configkit: {sequences: [{destination: [prompts], append: true}]}\n", path: "/configkit/sequences/0/append"},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
_, diagnostics := extractRootDirectives(parseRootDirectiveDocument(t, test.body), rootDirectiveContextSource)
|
|
if len(diagnostics) == 0 || diagnostics[0].Code != CodeSequenceInvalid || diagnostics[0].Path.String() != test.path {
|
|
t.Fatalf("diagnostics = %#v, want %s at %s", diagnostics, CodeSequenceInvalid, test.path)
|
|
}
|
|
if diagnostics[0].Primary == nil || diagnostics[0].Primary.Line == 0 {
|
|
t.Fatalf("diagnostic has no source location: %#v", diagnostics[0])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSequenceDeclarationsDoNotTriggerVersionOverlapWhenHeaderIsAbsent(t *testing.T) {
|
|
root := parseRootDirectiveDocument(t, `configkit:
|
|
sequences:
|
|
- destination: [prompts]
|
|
key: id
|
|
version: v1
|
|
`)
|
|
presentPath, err := ParsePath("/configkit/sequences/0/destination")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
present := resolveSourceVersionPath(root, presentPath)
|
|
if !sourceVersionHeaderControlOverlap(root, present) {
|
|
t.Fatal("present version header beneath a sequence control did not overlap")
|
|
}
|
|
absentPath, err := ParsePath("/configkit/sequences/1/destination")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
absent := resolveSourceVersionPath(root, absentPath)
|
|
if absent.present {
|
|
t.Fatal("missing version header was reported present")
|
|
}
|
|
if sourceVersionHeaderControlOverlap(root, absent) {
|
|
t.Fatal("absent version header beneath a sequence control was treated as overlap")
|
|
}
|
|
}
|
|
|
|
func extractSequenceDeclarationsYAML(t *testing.T, input string, context rootDirectiveContext) rootDirectiveExtraction {
|
|
t.Helper()
|
|
extracted, diagnostics := extractRootDirectives(parseRootDirectiveDocument(t, input), context)
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("root directive diagnostics = %#v", diagnostics)
|
|
}
|
|
return extracted
|
|
}
|
|
|
|
func parseRootDirectiveDocument(t *testing.T, input string) *document.Node {
|
|
t.Helper()
|
|
parsed, diagnostics := parseSource(Bytes("sequence-declarations.yml", []byte(input)), "sequence-declarations", defaultParseLimits())
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("source parse diagnostics = %#v", diagnostics)
|
|
}
|
|
return parsed.document
|
|
}
|