Add source-local version extraction
This commit is contained in:
@@ -270,6 +270,23 @@ No environmental limitation affected validation; checks ran on Linux/amd64.
|
||||
Validate: helper/version-path/directive/inheritance-control tests and everyday
|
||||
check. Done: the processor is tested independently of public loading.
|
||||
|
||||
Completion evidence: added a private source-local processor that resolves the
|
||||
configured path against the original document, separates imports, records
|
||||
private path/ancestor/location evidence, and consumes accepted contribution
|
||||
headers only from an owned payload copy. Missing and obstructed paths retain
|
||||
their cause and nearest available location; required absence fails before the
|
||||
checker, while optional main absence still invokes it. Structural directive
|
||||
overlap is rejected before payload consumption. Focused coverage includes the
|
||||
version vocabulary, malformed nodes, contextual numeric keys and indices,
|
||||
empty ancestors, sequence deletion, and actual versus unrelated structural
|
||||
overlap. These checks passed with Go `go1.26.8 linux/amd64`:
|
||||
|
||||
- `GOTOOLCHAIN=local GOPROXY=off go test ./internal/engine -run 'SourceVersion|Version|Import|Underlay|Overlay|Destination|Inheritance'`
|
||||
- `GOTOOLCHAIN=local ./scripts/check.sh`
|
||||
- `git diff --check`
|
||||
|
||||
No environmental limitation affected validation; checks ran on Linux/amd64.
|
||||
|
||||
## Stage 4 — Add private mode state and main-document integration
|
||||
|
||||
1. Add the engine mode to ordinary/layered options and loadRequest. Validate it at
|
||||
|
||||
339
internal/engine/source_version.go
Normal file
339
internal/engine/source_version.go
Normal 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
|
||||
}
|
||||
384
internal/engine/source_version_test.go
Normal file
384
internal/engine/source_version_test.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user