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
|
Validate: helper/version-path/directive/inheritance-control tests and everyday
|
||||||
check. Done: the processor is tested independently of public loading.
|
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
|
## Stage 4 — Add private mode state and main-document integration
|
||||||
|
|
||||||
1. Add the engine mode to ordinary/layered options and loadRequest. Validate it at
|
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