385 lines
17 KiB
Go
385 lines
17 KiB
Go
package engine
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
)
|
|
|
|
func sourceVersionTestDocument(t *testing.T, input string) *document.Node {
|
|
t.Helper()
|
|
parsed, diagnostics := parseSource(Bytes("source-version.yml", []byte(input)), "source-version", defaultParseLimits())
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("parse diagnostics = %#v", diagnostics)
|
|
}
|
|
return parsed.document
|
|
}
|
|
|
|
func requireSourceVersionDiagnostic(t *testing.T, diagnostics []Diagnostic, code Code, path string) Diagnostic {
|
|
t.Helper()
|
|
for _, diagnostic := range diagnostics {
|
|
if diagnostic.Code == code && diagnostic.Path.String() == path {
|
|
return diagnostic
|
|
}
|
|
}
|
|
t.Fatalf("diagnostics = %#v, want %s at %q", diagnostics, code, path)
|
|
return Diagnostic{}
|
|
}
|
|
|
|
func TestSourceVersionExtractionPreservesVersionVocabulary(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want Version
|
|
}{
|
|
{name: "string", input: "version: \"app.v7\"\n", want: Version{Present: true, Text: "app.v7"}},
|
|
{name: "integer", input: "version: 123456789012345678901234567890\n", want: Version{Present: true, Text: "123456789012345678901234567890"}},
|
|
{name: "null", input: "version: null\n", want: Version{Present: true, Null: true}},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
root := sourceVersionTestDocument(t, test.input)
|
|
var checked Version
|
|
rule := &VersionRule{Path: Keys("version"), Check: func(version Version, _ *Validation) error {
|
|
checked = version
|
|
return nil
|
|
}}
|
|
result := processSourceVersion(root, rule, sourceVersionContribution, Keys("collections", "weather"), sourceVersionModeIfPresent)
|
|
if len(result.diagnostics) != 0 || !result.evidence.checked || result.evidence.version != test.want || checked != test.want {
|
|
t.Fatalf("result = %#v, checked = %#v", result, checked)
|
|
}
|
|
if _, found := documentAtPath(result.payload, rule.Path); found {
|
|
t.Fatal("accepted non-main version remained in the payload")
|
|
}
|
|
if node, found := documentAtPath(root, rule.Path); !found || node == nil {
|
|
t.Fatal("source document was mutated while consuming its version")
|
|
}
|
|
if result.evidence.destination.String() != "/collections/weather" {
|
|
t.Fatalf("evidence destination = %q", result.evidence.destination.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionExtractionHandlesMissingAndMalformedValues(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
path Path
|
|
mode sourceVersionMode
|
|
role sourceVersionRole
|
|
wantCode Code
|
|
wantPresent bool
|
|
wantCallback bool
|
|
wantAbsence sourceVersionPathAbsence
|
|
}{
|
|
{
|
|
name: "missing mapping member is optional absence", input: "settings: {}\n",
|
|
path: Keys("settings", "version"), mode: sourceVersionModeIfPresent,
|
|
role: sourceVersionContribution, wantPresent: false, wantAbsence: sourceVersionMemberMissing,
|
|
},
|
|
{
|
|
name: "missing below scalar is optional absence", input: "settings: scalar\n",
|
|
path: Keys("settings", "version"), mode: sourceVersionModeIfPresent,
|
|
role: sourceVersionContribution, wantPresent: false, wantAbsence: sourceVersionPathObstructed,
|
|
},
|
|
{
|
|
name: "required absence reports nearest location", input: "settings: scalar\n",
|
|
path: Keys("settings", "version"), mode: sourceVersionModeRequired,
|
|
role: sourceVersionContribution, wantCode: CodeVersionError, wantAbsence: sourceVersionPathObstructed,
|
|
},
|
|
{
|
|
name: "unavailable sequence index is absence", input: "versions: [1]\n",
|
|
path: Keys("versions").Index(3), mode: sourceVersionModeRequired,
|
|
role: sourceVersionContribution, wantCode: CodeVersionError, wantAbsence: sourceVersionIndexUnavailable,
|
|
},
|
|
{
|
|
name: "required actual main absence", input: "name: demo\n", path: Keys("version"),
|
|
mode: sourceVersionModeRequired, role: sourceVersionMain, wantCode: CodeVersionError, wantAbsence: sourceVersionMemberMissing,
|
|
},
|
|
{
|
|
name: "main optional absence invokes checker", input: "name: demo\n", path: Keys("version"),
|
|
mode: sourceVersionModeIfPresent, role: sourceVersionMain, wantCallback: true, wantAbsence: sourceVersionMemberMissing,
|
|
},
|
|
{name: "boolean", input: "version: true\n", path: Keys("version"), mode: sourceVersionModeIfPresent, role: sourceVersionContribution, wantCode: CodeVersionType, wantPresent: true},
|
|
{name: "fraction", input: "version: 4.5\n", path: Keys("version"), mode: sourceVersionModeIfPresent, role: sourceVersionContribution, wantCode: CodeVersionType, wantPresent: true},
|
|
{name: "mapping", input: "version: {}\n", path: Keys("version"), mode: sourceVersionModeIfPresent, role: sourceVersionContribution, wantCode: CodeVersionType, wantPresent: true},
|
|
{name: "sequence", input: "version: []\n", path: Keys("version"), mode: sourceVersionModeIfPresent, role: sourceVersionContribution, wantCode: CodeVersionType, wantPresent: true},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
root := sourceVersionTestDocument(t, test.input)
|
|
called := false
|
|
rule := &VersionRule{Path: test.path, Check: func(Version, *Validation) error {
|
|
called = true
|
|
return nil
|
|
}}
|
|
result := processSourceVersion(root, rule, test.role, Path{}, test.mode)
|
|
if called != test.wantCallback {
|
|
t.Fatalf("checker called = %v, want %v", called, test.wantCallback)
|
|
}
|
|
if test.wantCode != "" {
|
|
diagnostic := requireSourceVersionDiagnostic(t, result.diagnostics, test.wantCode, test.path.String())
|
|
if result.payload != nil {
|
|
t.Fatalf("failed processing returned payload %#v", result.payload)
|
|
}
|
|
if diagnostic.Primary == nil || diagnostic.Primary.SourceID != "source-version" || diagnostic.Primary.Line != 1 {
|
|
t.Fatalf("diagnostic location = %#v", diagnostic.Primary)
|
|
}
|
|
} else if len(result.diagnostics) != 0 {
|
|
t.Fatalf("diagnostics = %#v", result.diagnostics)
|
|
}
|
|
if result.evidence == nil || result.evidence.version.Present != test.wantPresent {
|
|
t.Fatalf("evidence = %#v", result.evidence)
|
|
}
|
|
if result.evidence.absence != test.wantAbsence {
|
|
t.Fatalf("absence cause = %v, want %v", result.evidence.absence, test.wantAbsence)
|
|
}
|
|
if test.wantCode == CodeVersionType && (!result.evidence.redacted || result.evidence.version.Text != "") {
|
|
t.Fatalf("malformed value was retained: %#v", result.evidence)
|
|
}
|
|
if !test.wantCallback && result.evidence.checked {
|
|
t.Fatalf("evidence incorrectly records a callback: %#v", result.evidence)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionExtractionConsumesOnlySelectedNodes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
destination := Keys("collections", "weather")
|
|
root := sourceVersionTestDocument(t, "settings:\n version: 8\n")
|
|
originalSettings, _ := documentAtPath(root, Keys("settings"))
|
|
result := processSourceVersion(root, &VersionRule{
|
|
Path: Keys("settings", "version"), Check: func(Version, *Validation) error { return nil },
|
|
}, sourceVersionContribution, destination, sourceVersionModeIfPresent)
|
|
settings, found := documentAtPath(result.payload, Keys("settings"))
|
|
if len(result.diagnostics) != 0 || !found || settings.Kind != document.Mapping || len(settings.Mapping) != 0 {
|
|
t.Fatalf("payload = %#v, diagnostics = %#v", result.payload, result.diagnostics)
|
|
}
|
|
if settings.Location != originalSettings.Location {
|
|
t.Fatalf("authored ancestor location changed: got %#v, want %#v", settings.Location, originalSettings.Location)
|
|
}
|
|
if original, found := documentAtPath(root, Keys("settings", "version")); !found || original.Text != "8" {
|
|
t.Fatalf("original source changed: %#v, found %v", original, found)
|
|
}
|
|
if len(result.evidence.ancestors) != 3 || result.evidence.ancestors[1].kind != document.Mapping {
|
|
t.Fatalf("ancestor shape = %#v", result.evidence.ancestors)
|
|
}
|
|
|
|
sequenceRoot := sourceVersionTestDocument(t, "versions: [5, 7, 9]\n")
|
|
originalVersions, _ := documentAtPath(sequenceRoot, Keys("versions"))
|
|
originalLastLocation := originalVersions.Sequence[2].Location
|
|
sequencePath, err := ParsePath("/versions/1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sequenceResult := processSourceVersion(sequenceRoot, &VersionRule{
|
|
Path: sequencePath, Check: func(version Version, _ *Validation) error {
|
|
if version.Text != "7" {
|
|
t.Fatalf("checked version = %#v", version)
|
|
}
|
|
return nil
|
|
},
|
|
}, sourceVersionContribution, Path{}, sourceVersionModeRequired)
|
|
versions, found := documentAtPath(sequenceResult.payload, Keys("versions"))
|
|
if len(sequenceResult.diagnostics) != 0 || !found || versions.Kind != document.Sequence || len(versions.Sequence) != 2 || versions.Sequence[0].Text != "5" || versions.Sequence[1].Text != "9" {
|
|
t.Fatalf("sequence payload = %#v, diagnostics = %#v", sequenceResult.payload, sequenceResult.diagnostics)
|
|
}
|
|
if versions.Sequence[1].Location != originalLastLocation {
|
|
t.Fatalf("remaining sequence location changed: got %#v, want %#v", versions.Sequence[1].Location, originalLastLocation)
|
|
}
|
|
if len(sequenceResult.evidence.resolvedPath.segments) != 2 || sequenceResult.evidence.resolvedPath.segments[1].kind != pathSegmentIndex || sequenceResult.evidence.resolvedPath.segments[1].index != 1 {
|
|
t.Fatalf("original sequence path = %#v", sequenceResult.evidence.resolvedPath)
|
|
}
|
|
|
|
itemRoot := sourceVersionTestDocument(t, "items:\n - name: first\n - version: 12\n")
|
|
itemResult := processSourceVersion(itemRoot, &VersionRule{
|
|
Path: Keys("items").Index(1).Key("version"), Check: func(Version, *Validation) error { return nil },
|
|
}, sourceVersionContribution, Path{}, sourceVersionModeIfPresent)
|
|
items, found := documentAtPath(itemResult.payload, Keys("items"))
|
|
if len(itemResult.diagnostics) != 0 || !found || len(items.Sequence) != 2 || items.Sequence[1].Kind != document.Mapping || len(items.Sequence[1].Mapping) != 0 {
|
|
t.Fatalf("sequence mapping payload = %#v, diagnostics = %#v", itemResult.payload, itemResult.diagnostics)
|
|
}
|
|
|
|
emptySequenceRoot := sourceVersionTestDocument(t, "versions: [7]\n")
|
|
emptySequenceResult := processSourceVersion(emptySequenceRoot, &VersionRule{
|
|
Path: Keys("versions").Index(0), Check: func(Version, *Validation) error { return nil },
|
|
}, sourceVersionContribution, Path{}, sourceVersionModeIfPresent)
|
|
emptySequence, found := documentAtPath(emptySequenceResult.payload, Keys("versions"))
|
|
if len(emptySequenceResult.diagnostics) != 0 || !found || emptySequence.Kind != document.Sequence || len(emptySequence.Sequence) != 0 {
|
|
t.Fatalf("empty sequence payload = %#v, diagnostics = %#v", emptySequenceResult.payload, emptySequenceResult.diagnostics)
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionExtractionResolvesContextualPathSegments(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
path Path
|
|
wantRemainingPath bool
|
|
wantRemainingText string
|
|
}{
|
|
{name: "literal numeric mapping key", input: "versions:\n \"0\": 3\n other: 4\n", path: Keys("versions", "0")},
|
|
{name: "parsed numeric mapping key", input: "versions:\n \"0\": 3\n other: 4\n", path: mustParseSourceVersionPath(t, "/versions/0")},
|
|
{name: "parsed sequence index", input: "versions: [3, 4]\n", path: mustParseSourceVersionPath(t, "/versions/0"), wantRemainingPath: true, wantRemainingText: "4"},
|
|
{name: "explicit sequence index", input: "versions: [3, 4]\n", path: Keys("versions").Index(0), wantRemainingPath: true, wantRemainingText: "4"},
|
|
{name: "escaped mapping key", input: "a/b:\n version: 3\n", path: mustParseSourceVersionPath(t, "/a~1b/version")},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
root := sourceVersionTestDocument(t, test.input)
|
|
result := processSourceVersion(root, &VersionRule{
|
|
Path: test.path, Check: func(version Version, _ *Validation) error {
|
|
if version.Text != "3" {
|
|
t.Errorf("checked version = %#v", version)
|
|
}
|
|
return nil
|
|
},
|
|
}, sourceVersionContribution, Path{}, sourceVersionModeIfPresent)
|
|
if len(result.diagnostics) != 0 {
|
|
t.Fatalf("diagnostics = %#v", result.diagnostics)
|
|
}
|
|
remaining, found := documentAtPath(result.payload, test.path)
|
|
if found != test.wantRemainingPath || found && remaining.Text != test.wantRemainingText {
|
|
t.Fatalf("remaining selected path = %#v, found = %v", remaining, found)
|
|
}
|
|
if !result.evidence.version.Present || result.evidence.version.Text != "3" {
|
|
t.Fatalf("evidence = %#v", result.evidence)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionExtractionRejectsStructuralOverlap(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
path Path
|
|
wantCode Code
|
|
}{
|
|
{
|
|
name: "inside import declaration", input: "configkit:\n imports:\n - path: child.yml\n",
|
|
path: Keys("configkit", "imports").Index(0).Key("path"), wantCode: CodeVersionError,
|
|
},
|
|
{
|
|
name: "extends control", input: "settings:\n extends: parent\n",
|
|
path: Keys("settings", "extends"), wantCode: CodeVersionError,
|
|
},
|
|
{
|
|
name: "invalid extends subtree", input: "settings:\n extends:\n version: 1\n",
|
|
path: Keys("settings", "extends", "version"), wantCode: CodeVersionError,
|
|
},
|
|
{
|
|
name: "malformed directive cannot be consumed", input: "configkit:\n imports: true\n",
|
|
path: Keys("configkit", "imports"), wantCode: CodeImportInvalid,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
root := sourceVersionTestDocument(t, test.input)
|
|
called := false
|
|
result := processSourceVersion(root, &VersionRule{Path: test.path, Check: func(Version, *Validation) error {
|
|
called = true
|
|
return nil
|
|
}}, sourceVersionContribution, Path{}, sourceVersionModeIfPresent)
|
|
diagnostic := requireSourceVersionDiagnostic(t, result.diagnostics, test.wantCode, test.path.String())
|
|
if called || result.payload != nil {
|
|
t.Fatalf("invalid overlap produced payload %#v or called checker %v", result.payload, called)
|
|
}
|
|
if test.wantCode == CodeVersionError && (result.evidence == nil || !result.evidence.redacted || result.evidence.version.Text != "") {
|
|
t.Fatalf("overlap evidence was not conservatively redacted: %#v", result.evidence)
|
|
}
|
|
if diagnostic.Primary == nil || diagnostic.Primary.SourceID != "source-version" {
|
|
t.Fatalf("diagnostic location = %#v", diagnostic.Primary)
|
|
}
|
|
})
|
|
}
|
|
|
|
unrelatedRoot := sourceVersionTestDocument(t, "configkit:\n imports:\n - path: child.yml\nmetadata:\n version: 4\n")
|
|
unrelatedResult := processSourceVersion(unrelatedRoot, &VersionRule{
|
|
Path: Keys("metadata", "version"), Check: func(Version, *Validation) error { return nil },
|
|
}, sourceVersionContribution, Path{}, sourceVersionModeIfPresent)
|
|
metadata, found := documentAtPath(unrelatedResult.payload, Keys("metadata"))
|
|
if len(unrelatedResult.diagnostics) != 0 || !found || len(metadata.Mapping) != 0 || len(unrelatedResult.imports) != 1 {
|
|
t.Fatalf("unrelated directive result = %#v", unrelatedResult)
|
|
}
|
|
|
|
ordinaryRoot := sourceVersionTestDocument(t, "extends_version: 4\n")
|
|
ordinaryResult := processSourceVersion(ordinaryRoot, &VersionRule{
|
|
Path: Keys("extends_version"), Check: func(Version, *Validation) error { return nil },
|
|
}, sourceVersionContribution, Path{}, sourceVersionModeIfPresent)
|
|
if len(ordinaryResult.diagnostics) != 0 || ordinaryResult.payload == nil {
|
|
t.Fatalf("ordinary extends_version key was rejected: %#v", ordinaryResult)
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionDefaultModeDoesNotCheckOrConsume(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
root := sourceVersionTestDocument(t, "version: 2\n")
|
|
called := false
|
|
result := processSourceVersion(root, &VersionRule{Path: Keys("version"), Check: func(Version, *Validation) error {
|
|
called = true
|
|
return nil
|
|
}}, sourceVersionContribution, Path{}, sourceVersionModeDefault)
|
|
if len(result.diagnostics) != 0 || called || result.evidence != nil {
|
|
t.Fatalf("default result = %#v, checker called = %v", result, called)
|
|
}
|
|
if node, found := documentAtPath(result.payload, Keys("version")); !found || node.Text != "2" {
|
|
t.Fatalf("default payload = %#v", result.payload)
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionNoSourceMainRetainsAbsentCallback(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
mode sourceVersionMode
|
|
}{
|
|
{name: "if present", mode: sourceVersionModeIfPresent},
|
|
{name: "required", mode: sourceVersionModeRequired},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
called := false
|
|
result := processSourceVersion(nil, &VersionRule{
|
|
Path: Keys("version"),
|
|
Check: func(version Version, _ *Validation) error {
|
|
called = true
|
|
if version != (Version{}) {
|
|
t.Errorf("NoSource version = %#v", version)
|
|
}
|
|
return nil
|
|
},
|
|
}, sourceVersionMain, Path{}, test.mode)
|
|
if !called || len(result.diagnostics) != 0 || result.evidence != nil || result.payload != nil {
|
|
t.Fatalf("NoSource result = %#v, callback called = %v", result, called)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func mustParseSourceVersionPath(t *testing.T, input string) Path {
|
|
t.Helper()
|
|
path, err := ParsePath(input)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|