Add source-local version extraction

This commit is contained in:
2026-10-15 15:15:05 +00:00
parent 2fcf892324
commit 49218a836b
3 changed files with 740 additions and 0 deletions

View File

@@ -0,0 +1,339 @@
package engine
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
type sourceVersionMode uint8
const (
sourceVersionModeDefault sourceVersionMode = iota
sourceVersionModeIfPresent
sourceVersionModeRequired
)
type sourceVersionRole uint8
const (
sourceVersionMain sourceVersionRole = iota
sourceVersionContribution
)
type sourceVersionAncestor struct {
path Path
kind document.Kind
}
type sourceVersionEvidence struct {
path Path
resolvedPath Path
destination Path
version Version
location *Location
ancestors []sourceVersionAncestor
absence sourceVersionPathAbsence
checked bool
redacted bool
}
type sourceVersionProcessingResult struct {
payload *document.Node
imports []importDeclaration
evidence *sourceVersionEvidence
diagnostics []Diagnostic
}
type sourceVersionPathAbsence uint8
const (
sourceVersionPathPresent sourceVersionPathAbsence = iota
sourceVersionPathInvalid
sourceVersionMemberMissing
sourceVersionIndexUnavailable
sourceVersionPathObstructed
)
type sourceVersionPath struct {
node *document.Node
present bool
absence sourceVersionPathAbsence
nearest document.Location
resolvedPath Path
ancestors []sourceVersionAncestor
}
func processSourceVersion(root *document.Node, rule *VersionRule, role sourceVersionRole, destination Path, mode sourceVersionMode) sourceVersionProcessingResult {
if role != sourceVersionMain && role != sourceVersionContribution {
return sourceVersionProcessingResult{diagnostics: []Diagnostic{sourceVersionOptionsDiagnostic("source role is invalid")}}
}
if mode != sourceVersionModeDefault && mode != sourceVersionModeIfPresent && mode != sourceVersionModeRequired {
return sourceVersionProcessingResult{diagnostics: []Diagnostic{sourceVersionOptionsDiagnostic("source-version mode is invalid")}}
}
if mode != sourceVersionModeDefault && (rule == nil || rule.Check == nil || !rule.Path.valid()) {
return sourceVersionProcessingResult{diagnostics: []Diagnostic{sourceVersionOptionsDiagnostic("source-version checking requires a valid version rule and checker")}}
}
if root == nil {
if role == sourceVersionMain && mode != sourceVersionModeDefault {
return sourceVersionProcessingResult{diagnostics: canonicalDiagnostics(reportVersion(rule, Version{}, nil))}
}
return sourceVersionProcessingResult{}
}
payload, imports, diagnostics := splitImportDirectives(root)
if len(diagnostics) != 0 {
return sourceVersionProcessingResult{diagnostics: diagnostics}
}
result := sourceVersionProcessingResult{payload: payload, imports: imports}
if mode == sourceVersionModeDefault {
return result
}
resolved := resolveSourceVersionPath(root, rule.Path)
evidence := &sourceVersionEvidence{
path: rule.Path.clone(), resolvedPath: resolved.resolvedPath,
destination: destination.clone(), ancestors: resolved.ancestors,
absence: resolved.absence,
}
location := resolved.nearest
if resolved.present {
location = resolved.node.Location
}
if locationKnown(location) {
public := publicLocation(location)
evidence.location = &public
}
result.evidence = evidence
interpreted := interpretVersion(rule, root)
evidence.version = interpreted.version
if sourceVersionControlOverlap(root, evidence.resolvedPath) {
evidence.redactText()
result.payload = nil
result.diagnostics = []Diagnostic{sourceVersionOverlapDiagnostic(rule, evidence)}
return result
}
if hasErrors(interpreted.diagnostics) {
evidence.redactText()
result.payload = nil
result.diagnostics = canonicalDiagnostics(interpreted.diagnostics)
return result
}
if !interpreted.version.Present {
if mode == sourceVersionModeRequired {
evidence.redactText()
result.payload = nil
result.diagnostics = []Diagnostic{sourceVersionMissingDiagnostic(rule, evidence)}
return result
}
if role == sourceVersionContribution {
return result
}
}
evidence.checked = true
result.diagnostics = canonicalDiagnostics(reportVersion(rule, interpreted.version, root))
if hasErrors(result.diagnostics) {
evidence.redactText()
result.payload = nil
return result
}
if role == sourceVersionContribution && interpreted.version.Present {
if !removeSourceVersionNode(result.payload, evidence.resolvedPath) {
evidence.redactText()
result.payload = nil
result.diagnostics = []Diagnostic{sourceVersionOptionsDiagnostic("selected source version could not be removed from its owned payload")}
return result
}
}
return result
}
func sourceVersionOptionsDiagnostic(message string) Diagnostic {
return Diagnostic{Code: CodeOptionsInvalid, Severity: SeverityError, Message: message}
}
func sourceVersionMissingDiagnostic(rule *VersionRule, evidence *sourceVersionEvidence) Diagnostic {
return Diagnostic{
Code: CodeVersionError, Severity: SeverityError, Path: rule.Path.clone(),
Message: "required schema version is missing", Primary: cloneLocation(evidence.location),
}
}
func sourceVersionOverlapDiagnostic(rule *VersionRule, evidence *sourceVersionEvidence) Diagnostic {
return Diagnostic{
Code: CodeVersionError, Severity: SeverityError, Path: rule.Path.clone(),
Message: "schema version path overlaps a structural directive", Primary: cloneLocation(evidence.location),
}
}
func (evidence *sourceVersionEvidence) redactText() {
if evidence == nil {
return
}
evidence.version.Text = ""
evidence.redacted = true
}
func resolveSourceVersionPath(root *document.Node, path Path) sourceVersionPath {
resolved := sourceVersionPath{absence: sourceVersionPathInvalid}
if root == nil || !path.valid() {
return resolved
}
current := root
resolved.nearest = current.Location
resolved.ancestors = append(resolved.ancestors, sourceVersionAncestor{kind: current.Kind})
for _, segment := range path.segments {
switch current.Kind {
case document.Mapping:
if segment.kind == pathSegmentIndex {
resolved.absence = sourceVersionPathObstructed
return resolved
}
resolved.resolvedPath = resolved.resolvedPath.Key(segment.key)
found := false
for _, entry := range current.Mapping {
if entry.Key == segment.key {
current = entry.Value
found = true
break
}
}
if !found {
resolved.absence = sourceVersionMemberMissing
return resolved
}
case document.Sequence:
index, ok := schemaIndex(segment)
if !ok {
resolved.absence = sourceVersionIndexUnavailable
return resolved
}
resolved.resolvedPath = resolved.resolvedPath.Index(index)
if index >= len(current.Sequence) {
resolved.absence = sourceVersionIndexUnavailable
return resolved
}
current = current.Sequence[index]
default:
resolved.absence = sourceVersionPathObstructed
return resolved
}
resolved.nearest = current.Location
resolved.ancestors = append(resolved.ancestors, sourceVersionAncestor{
path: resolved.resolvedPath.clone(), kind: current.Kind,
})
}
resolved.node = current
resolved.present = true
resolved.absence = sourceVersionPathPresent
return resolved
}
func sourceVersionControlOverlap(root *document.Node, path Path) bool {
if root == nil || !path.valid() {
return false
}
if root.Kind == document.Mapping {
for _, entry := range root.Mapping {
if entry.Key == directiveKey && pathHasPrefix(path, Keys(directiveKey)) {
return true
}
}
}
return sourceVersionInheritanceOverlap(root, Path{}, path)
}
func sourceVersionInheritanceOverlap(node *document.Node, current, path Path) bool {
if node == nil {
return false
}
if node.Kind == document.Mapping {
if _, found := inheritanceDirective(node); found {
controlPath := current.Key(inheritanceKey)
if pathHasPrefix(path, controlPath) {
return true
}
}
for _, entry := range node.Mapping {
if entry.Key == inheritanceKey {
continue
}
if sourceVersionInheritanceOverlap(entry.Value, current.Key(entry.Key), path) {
return true
}
}
} else if node.Kind == document.Sequence {
for index, child := range node.Sequence {
if sourceVersionInheritanceOverlap(child, current.Index(index), path) {
return true
}
}
}
return false
}
func pathHasPrefix(path, prefix Path) bool {
if !path.valid() || !prefix.valid() || len(prefix.segments) > len(path.segments) {
return false
}
for index, segment := range prefix.segments {
candidate := path.segments[index]
if candidate.kind != segment.kind || candidate.key != segment.key || candidate.index != segment.index {
return false
}
}
return true
}
func removeSourceVersionNode(root *document.Node, path Path) bool {
if root == nil || !path.valid() || len(path.segments) == 0 {
return false
}
current := root
for _, segment := range path.segments[:len(path.segments)-1] {
switch current.Kind {
case document.Mapping:
if segment.kind == pathSegmentIndex {
return false
}
found := false
for _, entry := range current.Mapping {
if entry.Key == segment.key {
current = entry.Value
found = true
break
}
}
if !found {
return false
}
case document.Sequence:
index, ok := schemaIndex(segment)
if !ok || index >= len(current.Sequence) {
return false
}
current = current.Sequence[index]
default:
return false
}
}
last := path.segments[len(path.segments)-1]
switch current.Kind {
case document.Mapping:
if last.kind == pathSegmentIndex {
return false
}
for index, entry := range current.Mapping {
if entry.Key == last.key {
current.Mapping = append(current.Mapping[:index], current.Mapping[index+1:]...)
return true
}
}
case document.Sequence:
index, ok := schemaIndex(last)
if !ok || index >= len(current.Sequence) {
return false
}
current.Sequence = append(current.Sequence[:index], current.Sequence[index+1:]...)
return true
}
return false
}

View File

@@ -0,0 +1,384 @@
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
}