Integrate source version checks for main inputs
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Implement shared source version checks
|
# Implement shared source version checks
|
||||||
|
|
||||||
Status: Stages 1–2 complete; source-local extraction has not started.
|
Status: Stages 1–4 complete; recursive import integration has not started.
|
||||||
|
|
||||||
Audience: a Luna coding agent implementing one numbered stage per prompt, in
|
Audience: a Luna coding agent implementing one numbered stage per prompt, in
|
||||||
order. The [feature roadmap](source-version-checks.md) owns target behavior and
|
order. The [feature roadmap](source-version-checks.md) owns target behavior and
|
||||||
@@ -305,6 +305,26 @@ No environmental limitation affected validation; checks ran on Linux/amd64.
|
|||||||
Validate: load/grouped/version/destination/hook tests and everyday check. Done:
|
Validate: load/grouped/version/destination/hook tests and everyday check. Done:
|
||||||
private enabled main behavior works without changing old callers.
|
private enabled main behavior works without changing old callers.
|
||||||
|
|
||||||
|
Completion evidence: added the private mode to ordinary and layered engine
|
||||||
|
options and the shared load request. Both entry points validate enabled modes
|
||||||
|
and their shared checker before source acquisition; enabled loads build schema
|
||||||
|
before invoking the main checker. The parsed main document is checked at its
|
||||||
|
source-local path before import traversal or layered composition, and its node
|
||||||
|
remains in the application payload. A single semantic destination-prefixed rule
|
||||||
|
now protects mounted main ownership during decoding, inheritance, acquired-input
|
||||||
|
placement, overlays, and CLI overrides; declaration callbacks and diagnostics
|
||||||
|
retain local paths. Default mode keeps its prior post-composition check timing.
|
||||||
|
The per-load assembled input retains main evidence and successful warning
|
||||||
|
diagnostics. NoSource invokes the existing absent-main callback once in either
|
||||||
|
enabled mode. These checks passed with Go `go1.26.8 linux/amd64`:
|
||||||
|
|
||||||
|
- `GOTOOLCHAIN=local GOPROXY=off go test ./internal/engine -run 'SourceVersion|Version|Load|Grouped|Destination|Hook'`
|
||||||
|
- `GOTOOLCHAIN=local ./scripts/check.sh`
|
||||||
|
- `git diff --check`
|
||||||
|
|
||||||
|
No environmental limitation affected validation; checks ran on Linux/amd64.
|
||||||
|
The engine mode remains private; public options are scheduled separately.
|
||||||
|
|
||||||
## Stage 5 — Integrate recursive imports, warnings, and accounting
|
## Stage 5 — Integrate recursive imports, warnings, and accounting
|
||||||
|
|
||||||
1. Thread shared state through import entry/traversal. Check each acquired child
|
1. Thread shared state through import entry/traversal. Check each acquired child
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type Underlay struct{ Inputs []Input }
|
|||||||
// LayeredOptions selects lower-priority tiers and final application rules.
|
// LayeredOptions selects lower-priority tiers and final application rules.
|
||||||
type LayeredOptions[T any] struct {
|
type LayeredOptions[T any] struct {
|
||||||
Definition Definition[T]
|
Definition Definition[T]
|
||||||
|
SourceVersionMode sourceVersionMode
|
||||||
Underlays []Underlay
|
Underlays []Underlay
|
||||||
Overlay string
|
Overlay string
|
||||||
OverlayFilesystem *FilesystemContext
|
OverlayFilesystem *FilesystemContext
|
||||||
@@ -37,7 +38,8 @@ func LoadLayered[T any](main Input, secrets SecretBinding, supplied ...LayeredOp
|
|||||||
source: main.Source, imports: main.Imports, filesystem: main.Filesystem,
|
source: main.Source, imports: main.Imports, filesystem: main.Filesystem,
|
||||||
destination: main.Destination.clone(),
|
destination: main.Destination.clone(),
|
||||||
},
|
},
|
||||||
definition: options.Definition, overlay: options.Overlay,
|
definition: options.Definition, sourceVersionMode: options.SourceVersionMode,
|
||||||
|
overlay: options.Overlay,
|
||||||
overlayFilesystem: options.OverlayFilesystem,
|
overlayFilesystem: options.OverlayFilesystem,
|
||||||
overrides: append([]Override(nil), options.Overrides...),
|
overrides: append([]Override(nil), options.Overrides...),
|
||||||
underlays: make([]loadGroup, len(options.Underlays)),
|
underlays: make([]loadGroup, len(options.Underlays)),
|
||||||
|
|||||||
@@ -22,11 +22,12 @@ type Definition[T any] struct {
|
|||||||
// Imports enables explicit path and glob imports; Overlay selects one fixed overlay;
|
// Imports enables explicit path and glob imports; Overlay selects one fixed overlay;
|
||||||
// Overrides contains only values explicitly supplied by an application CLI.
|
// Overrides contains only values explicitly supplied by an application CLI.
|
||||||
type Options[T any] struct {
|
type Options[T any] struct {
|
||||||
Definition Definition[T]
|
Definition Definition[T]
|
||||||
Imports bool
|
Imports bool
|
||||||
Overlay string
|
Overlay string
|
||||||
Filesystem *FilesystemContext
|
Filesystem *FilesystemContext
|
||||||
Overrides []Override
|
Overrides []Override
|
||||||
|
SourceVersionMode sourceVersionMode
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilesystemContext supplies filesystem roots for imports and overlays.
|
// FilesystemContext supplies filesystem roots for imports and overlays.
|
||||||
@@ -143,6 +144,8 @@ func loadWithSecretLookup[T any](source Source, secrets SecretBinding, lookup se
|
|||||||
main: loadInput{source: source, imports: options.Imports, filesystem: options.Filesystem},
|
main: loadInput{source: source, imports: options.Imports, filesystem: options.Filesystem},
|
||||||
definition: options.Definition, overlay: options.Overlay,
|
definition: options.Definition, overlay: options.Overlay,
|
||||||
overrides: options.Overrides, ordinary: true,
|
overrides: options.Overrides, ordinary: true,
|
||||||
|
sourceVersionMode: options.SourceVersionMode,
|
||||||
|
effectiveVersion: effectiveSourceVersionRule(options.Definition.Version, Path{}, options.SourceVersionMode),
|
||||||
}
|
}
|
||||||
return executeLoadRequest(request, secrets, lookup)
|
return executeLoadRequest(request, secrets, lookup)
|
||||||
}
|
}
|
||||||
@@ -151,28 +154,46 @@ func executeLoadRequest[T any](request loadRequest[T], secrets SecretBinding, lo
|
|||||||
accumulator := diagnosticAccumulator{}
|
accumulator := diagnosticAccumulator{}
|
||||||
accounting := &importAccounting{}
|
accounting := &importAccounting{}
|
||||||
mainOptions := Options[T]{Definition: request.definition, Imports: request.main.imports, Filesystem: request.main.filesystem}
|
mainOptions := Options[T]{Definition: request.definition, Imports: request.main.imports, Filesystem: request.main.filesystem}
|
||||||
|
mainOptions.SourceVersionMode = request.sourceVersionMode
|
||||||
|
mainOptions.Definition.Version = request.protectionVersion()
|
||||||
|
mainOptions.Overrides = request.overrides
|
||||||
if request.ordinary {
|
if request.ordinary {
|
||||||
mainOptions.Overlay = request.overlay
|
mainOptions.Overlay = request.overlay
|
||||||
}
|
}
|
||||||
|
|
||||||
input, diagnostics := assembleBaseInputTracked(request.main.source, mainOptions, request.main.destination, accounting, nil, nil)
|
var schema *schemaDescriptor
|
||||||
|
if request.sourceVersionMode != sourceVersionModeDefault {
|
||||||
|
var schemaIssue *schemaIssue
|
||||||
|
schema, schemaIssue = buildSchema(reflect.TypeFor[T](), secrets)
|
||||||
|
if schemaIssue != nil {
|
||||||
|
accumulator.add(diagnosticFromSchemaIssue(schemaIssue))
|
||||||
|
return nil, accumulator.loadError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input, diagnostics := assembleBaseInputTrackedWithVersion(request.main.source, mainOptions, request.main.destination, accounting, nil, nil, request.definition.Version, request.sourceVersionMode)
|
||||||
accumulator.sources = input.sources
|
accumulator.sources = input.sources
|
||||||
|
accumulator.add(input.versionDiagnostics...)
|
||||||
accumulator.add(diagnostics...)
|
accumulator.add(diagnostics...)
|
||||||
if len(diagnostics) != 0 {
|
if hasErrors(diagnostics) {
|
||||||
return nil, accumulator.loadError()
|
return nil, accumulator.loadError()
|
||||||
}
|
}
|
||||||
schema, schemaIssue := buildSchema(reflect.TypeFor[T](), secrets)
|
if schema == nil {
|
||||||
if schemaIssue != nil {
|
var schemaIssue *schemaIssue
|
||||||
accumulator.add(diagnosticFromSchemaIssue(schemaIssue))
|
schema, schemaIssue = buildSchema(reflect.TypeFor[T](), secrets)
|
||||||
return nil, accumulator.loadError()
|
if schemaIssue != nil {
|
||||||
|
accumulator.add(diagnosticFromSchemaIssue(schemaIssue))
|
||||||
|
return nil, accumulator.loadError()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
versionDiagnostics := checkVersion(request.definition.Version, input.document)
|
if request.sourceVersionMode == sourceVersionModeDefault {
|
||||||
accumulator.add(versionDiagnostics...)
|
versionDiagnostics := checkVersion(request.definition.Version, input.document)
|
||||||
if hasErrors(versionDiagnostics) {
|
accumulator.add(versionDiagnostics...)
|
||||||
return nil, accumulator.loadError()
|
if hasErrors(versionDiagnostics) {
|
||||||
|
return nil, accumulator.loadError()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if request.ordinary {
|
if request.ordinary {
|
||||||
input, diagnostics = applyInputLayers(request.main.source, Options[T]{Definition: request.definition, Overlay: request.overlay, Filesystem: request.main.filesystem, Overrides: request.overrides}, input)
|
input, diagnostics = applyInputLayers(request.main.source, mainOptions, input)
|
||||||
} else {
|
} else {
|
||||||
input, diagnostics = assembleLayeredInput(request, input, accounting)
|
input, diagnostics = assembleLayeredInput(request, input, accounting)
|
||||||
}
|
}
|
||||||
@@ -208,7 +229,7 @@ func executeLoadRequest[T any](request loadRequest[T], secrets SecretBinding, lo
|
|||||||
accumulator.add(problems...)
|
accumulator.add(problems...)
|
||||||
return nil, accumulator.loadError()
|
return nil, accumulator.loadError()
|
||||||
}
|
}
|
||||||
if problem := inheritanceVersionDiagnostic(request.definition.Version, expanded); problem != nil {
|
if problem := inheritanceVersionDiagnostic(request.protectionVersion(), expanded); problem != nil {
|
||||||
accumulator.add(*problem)
|
accumulator.add(*problem)
|
||||||
return nil, accumulator.loadError()
|
return nil, accumulator.loadError()
|
||||||
}
|
}
|
||||||
@@ -217,7 +238,9 @@ func executeLoadRequest[T any](request loadRequest[T], secrets SecretBinding, lo
|
|||||||
input.document = expanded.root
|
input.document = expanded.root
|
||||||
}
|
}
|
||||||
|
|
||||||
decoded, diagnostics := decodeLoadInput(schema, request.definition, input)
|
decodeDefinition := request.definition
|
||||||
|
decodeDefinition.Version = request.protectionVersion()
|
||||||
|
decoded, diagnostics := decodeLoadInput(schema, decodeDefinition, input)
|
||||||
accumulator.add(diagnostics...)
|
accumulator.add(diagnostics...)
|
||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
return nil, accumulator.loadError()
|
return nil, accumulator.loadError()
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ import (
|
|||||||
|
|
||||||
// assembledInput is the source/document handoff across schema and version checks.
|
// assembledInput is the source/document handoff across schema and version checks.
|
||||||
type assembledInput struct {
|
type assembledInput struct {
|
||||||
parsed parsedSource
|
parsed parsedSource
|
||||||
document *document.Node
|
document *document.Node
|
||||||
composition *composedDocument
|
composition *composedDocument
|
||||||
inheritance *inheritanceScope
|
inheritance *inheritanceScope
|
||||||
sources []SourceInfo
|
sources []SourceInfo
|
||||||
contexts map[string]ResourceContext
|
contexts map[string]ResourceContext
|
||||||
|
versionEvidence *sourceVersionEvidence
|
||||||
|
versionDiagnostics []Diagnostic
|
||||||
}
|
}
|
||||||
|
|
||||||
func assembleBaseInput[T any](source Source, options Options[T]) (assembledInput, []Diagnostic) {
|
func assembleBaseInput[T any](source Source, options Options[T]) (assembledInput, []Diagnostic) {
|
||||||
@@ -21,6 +23,10 @@ func assembleBaseInput[T any](source Source, options Options[T]) (assembledInput
|
|||||||
}
|
}
|
||||||
|
|
||||||
func assembleBaseInputTracked[T any](source Source, options Options[T], destination Path, accounting *importAccounting, visited map[resourceIdentity]importRoute, hostToken *namespaceToken) (assembledInput, []Diagnostic) {
|
func assembleBaseInputTracked[T any](source Source, options Options[T], destination Path, accounting *importAccounting, visited map[resourceIdentity]importRoute, hostToken *namespaceToken) (assembledInput, []Diagnostic) {
|
||||||
|
return assembleBaseInputTrackedWithVersion(source, options, destination, accounting, visited, hostToken, nil, sourceVersionModeDefault)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assembleBaseInputTrackedWithVersion[T any](source Source, options Options[T], destination Path, accounting *importAccounting, visited map[resourceIdentity]importRoute, hostToken *namespaceToken, localVersion *VersionRule, mode sourceVersionMode) (assembledInput, []Diagnostic) {
|
||||||
limits := defaultImportLimits()
|
limits := defaultImportLimits()
|
||||||
parseLimits := defaultParseLimits()
|
parseLimits := defaultParseLimits()
|
||||||
parseLimits.source.remaining = limits.bytes - accounting.bytes
|
parseLimits.source.remaining = limits.bytes - accounting.bytes
|
||||||
@@ -42,6 +48,14 @@ func assembleBaseInputTracked[T any](source Source, options Options[T], destinat
|
|||||||
}
|
}
|
||||||
return input, diagnostics
|
return input, diagnostics
|
||||||
}
|
}
|
||||||
|
if mode != sourceVersionModeDefault {
|
||||||
|
processed := processSourceVersion(parsed.document, localVersion, sourceVersionMain, destination, mode)
|
||||||
|
input.versionEvidence = processed.evidence
|
||||||
|
input.versionDiagnostics = processed.diagnostics
|
||||||
|
if hasErrors(processed.diagnostics) {
|
||||||
|
return input, processed.diagnostics
|
||||||
|
}
|
||||||
|
}
|
||||||
if options.Imports {
|
if options.Imports {
|
||||||
input.composition, input.sources, diagnostics = loadImportedDocumentsTracked(source, options.Filesystem, parsed, destination, options.Definition.Version, limits, accounting, visited, hostToken)
|
input.composition, input.sources, diagnostics = loadImportedDocumentsTracked(source, options.Filesystem, parsed, destination, options.Definition.Version, limits, accounting, visited, hostToken)
|
||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
|
|||||||
@@ -10,12 +10,21 @@ type loadRequest[T any] struct {
|
|||||||
main loadInput
|
main loadInput
|
||||||
underlays []loadGroup
|
underlays []loadGroup
|
||||||
definition Definition[T]
|
definition Definition[T]
|
||||||
|
sourceVersionMode sourceVersionMode
|
||||||
|
effectiveVersion *VersionRule
|
||||||
overlay string
|
overlay string
|
||||||
overlayFilesystem *FilesystemContext
|
overlayFilesystem *FilesystemContext
|
||||||
overrides []Override
|
overrides []Override
|
||||||
ordinary bool
|
ordinary bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (request loadRequest[T]) protectionVersion() *VersionRule {
|
||||||
|
if request.effectiveVersion != nil {
|
||||||
|
return request.effectiveVersion
|
||||||
|
}
|
||||||
|
return request.definition.Version
|
||||||
|
}
|
||||||
|
|
||||||
type loadInput struct {
|
type loadInput struct {
|
||||||
source Source
|
source Source
|
||||||
imports bool
|
imports bool
|
||||||
@@ -33,8 +42,10 @@ func loadGroupedWithSecretLookup[T any](request loadRequest[T], secrets SecretBi
|
|||||||
} else {
|
} else {
|
||||||
request.main.destination = normalized
|
request.main.destination = normalized
|
||||||
}
|
}
|
||||||
|
request.effectiveVersion = effectiveSourceVersionRule(request.definition.Version, request.main.destination, request.sourceVersionMode)
|
||||||
_, mainDiagnostics := selectOptions(request.main.source, []Options[T]{{
|
_, mainDiagnostics := selectOptions(request.main.source, []Options[T]{{
|
||||||
Definition: request.definition, Imports: request.main.imports, Filesystem: request.main.filesystem,
|
Definition: request.definition, Imports: request.main.imports, Filesystem: request.main.filesystem,
|
||||||
|
SourceVersionMode: request.sourceVersionMode,
|
||||||
}})
|
}})
|
||||||
diagnostics = append(diagnostics, mainDiagnostics...)
|
diagnostics = append(diagnostics, mainDiagnostics...)
|
||||||
for groupIndex := range request.underlays {
|
for groupIndex := range request.underlays {
|
||||||
@@ -68,7 +79,7 @@ func loadGroupedWithSecretLookup[T any](request loadRequest[T], secrets SecretBi
|
|||||||
func assembleLayeredInput[T any](request loadRequest[T], input assembledInput, accounting *importAccounting) (assembledInput, []Diagnostic) {
|
func assembleLayeredInput[T any](request loadRequest[T], input assembledInput, accounting *importAccounting) (assembledInput, []Diagnostic) {
|
||||||
groups := make([]*composedDocument, 0, len(request.underlays))
|
groups := make([]*composedDocument, 0, len(request.underlays))
|
||||||
for index, group := range request.underlays {
|
for index, group := range request.underlays {
|
||||||
composition, sources, contexts, diagnostics := acquireUnderlayGroup(group, index+1, request.definition.Version, accounting)
|
composition, sources, contexts, diagnostics := acquireUnderlayGroup(group, index+1, request.protectionVersion(), accounting)
|
||||||
input.sources = append(input.sources, sources...)
|
input.sources = append(input.sources, sources...)
|
||||||
mergeResourceContexts(&input.contexts, contexts)
|
mergeResourceContexts(&input.contexts, contexts)
|
||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
@@ -115,7 +126,7 @@ func assembleLayeredInput[T any](request loadRequest[T], input assembledInput, a
|
|||||||
selection := Source{kind: sourceBytes}
|
selection := Source{kind: sourceBytes}
|
||||||
selected := parsedSource{document: &document.Node{Kind: document.Mapping}, source: &SourceInfo{}}
|
selected := parsedSource{document: &document.Node{Kind: document.Mapping}, source: &SourceInfo{}}
|
||||||
var diagnostics []Diagnostic
|
var diagnostics []Diagnostic
|
||||||
input.sources, diagnostics = loadSelectedOverlayInto(selection, request.overlayFilesystem, selected, assembly, input.sources, request.overlay, request.definition.Version, defaultImportLimits())
|
input.sources, diagnostics = loadSelectedOverlayInto(selection, request.overlayFilesystem, selected, assembly, input.sources, request.overlay, request.protectionVersion(), defaultImportLimits())
|
||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
return input, diagnostics
|
return input, diagnostics
|
||||||
}
|
}
|
||||||
@@ -127,7 +138,7 @@ func assembleLayeredInput[T any](request loadRequest[T], input assembledInput, a
|
|||||||
}
|
}
|
||||||
if len(request.overrides) != 0 {
|
if len(request.overrides) != 0 {
|
||||||
var diagnostics []Diagnostic
|
var diagnostics []Diagnostic
|
||||||
input.sources, diagnostics = assembly.applyOverrides(input.sources, request.overrides, request.definition.Version, defaultImportLimits())
|
input.sources, diagnostics = assembly.applyOverrides(input.sources, request.overrides, request.protectionVersion(), defaultImportLimits())
|
||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
return input, diagnostics
|
return input, diagnostics
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ func selectOptions[T any](source Source, supplied []Options[T]) (Options[T], []D
|
|||||||
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "schema-version rule requires a checker"))
|
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "schema-version rule requires a checker"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if options.SourceVersionMode != sourceVersionModeDefault && options.SourceVersionMode != sourceVersionModeIfPresent && options.SourceVersionMode != sourceVersionModeRequired {
|
||||||
|
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "source-version mode is invalid"))
|
||||||
|
} else if options.SourceVersionMode != sourceVersionModeDefault && (definition.Version == nil || !definition.Version.Path.valid() || definition.Version.Check == nil) {
|
||||||
|
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "source-version checking requires a valid version rule and checker"))
|
||||||
|
}
|
||||||
return options, canonicalDiagnostics(diagnostics)
|
return options, canonicalDiagnostics(diagnostics)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,21 @@ type sourceVersionProcessingResult struct {
|
|||||||
diagnostics []Diagnostic
|
diagnostics []Diagnostic
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func effectiveSourceVersionRule(local *VersionRule, destination Path, mode sourceVersionMode) *VersionRule {
|
||||||
|
if local == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if mode == sourceVersionModeDefault {
|
||||||
|
return local
|
||||||
|
}
|
||||||
|
effective := *local
|
||||||
|
effective.Path = Path{
|
||||||
|
segments: append(clonePathSegments(destination.segments, len(local.Path.segments)), local.Path.segments...),
|
||||||
|
invalid: destination.invalid || local.Path.invalid,
|
||||||
|
}
|
||||||
|
return &effective
|
||||||
|
}
|
||||||
|
|
||||||
type sourceVersionPathAbsence uint8
|
type sourceVersionPathAbsence uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -1,11 +1,24 @@
|
|||||||
package engine
|
package engine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type sourceVersionMountedService struct {
|
||||||
|
Version string `yaml:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type sourceVersionMountedConfig struct {
|
||||||
|
Service sourceVersionMountedService `yaml:"service"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type sourceVersionNullConfig struct {
|
||||||
|
Version *string `yaml:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
func sourceVersionTestDocument(t *testing.T, input string) *document.Node {
|
func sourceVersionTestDocument(t *testing.T, input string) *document.Node {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
parsed, diagnostics := parseSource(Bytes("source-version.yml", []byte(input)), "source-version", defaultParseLimits())
|
parsed, diagnostics := parseSource(Bytes("source-version.yml", []byte(input)), "source-version", defaultParseLimits())
|
||||||
@@ -374,6 +387,211 @@ func TestSourceVersionNoSourceMainRetainsAbsentCallback(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSourceVersionLoadChecksMountedMainLocallyAndProtectsEffectivePath(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
checks := 0
|
||||||
|
definition := Definition[sourceVersionMountedConfig]{Version: &VersionRule{
|
||||||
|
Path: Keys("version"),
|
||||||
|
Check: func(version Version, validation *Validation) error {
|
||||||
|
checks++
|
||||||
|
if !version.Present || version.Null || version.Text != "9007199254740993" {
|
||||||
|
return errors.New("unexpected source-local version")
|
||||||
|
}
|
||||||
|
validation.Warning(Path{}, "advisory")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
main := Input{Source: Bytes("main.yml", []byte("version: 9007199254740993\n")), Destination: Keys("service")}
|
||||||
|
loaded, err := LoadLayered(main, testSecretBinding(), LayeredOptions[sourceVersionMountedConfig]{
|
||||||
|
Definition: definition, SourceVersionMode: sourceVersionModeRequired,
|
||||||
|
})
|
||||||
|
if err != nil || loaded == nil {
|
||||||
|
t.Fatalf("mounted main load = %#v, %v", loaded, err)
|
||||||
|
}
|
||||||
|
if loaded.Config.Service.Version != "9007199254740993" || checks != 1 {
|
||||||
|
t.Fatalf("mounted main config/callbacks = %#v, %d", loaded.Config, checks)
|
||||||
|
}
|
||||||
|
warnings := testLoaded(loaded).Diagnostics()
|
||||||
|
if len(warnings) != 1 || warnings[0].Code != CodeVersionWarning || warnings[0].Severity != SeverityWarning {
|
||||||
|
t.Fatalf("mounted main warnings = %#v", warnings)
|
||||||
|
}
|
||||||
|
|
||||||
|
checks = 0
|
||||||
|
_, err = LoadLayered(main, testSecretBinding(), LayeredOptions[sourceVersionMountedConfig]{
|
||||||
|
Definition: definition, SourceVersionMode: sourceVersionModeRequired,
|
||||||
|
Overrides: []Override{{Path: Keys("service").Key("version"), Value: "4", Flag: "--version"}},
|
||||||
|
})
|
||||||
|
var loadErr *LoadError
|
||||||
|
if !errors.As(err, &loadErr) {
|
||||||
|
t.Fatalf("mounted protected override error = %T %v, want *LoadError", err, err)
|
||||||
|
}
|
||||||
|
requireSourceVersionDiagnostic(t, loadErr.Diagnostics(), CodeOverrideVersion, "/service/version")
|
||||||
|
if checks != 1 {
|
||||||
|
t.Fatalf("mounted protected override callbacks = %d, want 1", checks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSourceVersionLoadChecksNullAndNoSourceMain(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("required actual main absence fails before the checker", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
checks := 0
|
||||||
|
_, err := Load(Bytes("main.yml", []byte("other: value\n")), testSecretBinding(), Options[sourceVersionNullConfig]{
|
||||||
|
SourceVersionMode: sourceVersionModeRequired,
|
||||||
|
Definition: Definition[sourceVersionNullConfig]{Version: &VersionRule{
|
||||||
|
Path: Keys("version"), Check: func(Version, *Validation) error { checks++; return nil },
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
var loadErr *LoadError
|
||||||
|
if !errors.As(err, &loadErr) {
|
||||||
|
t.Fatalf("missing required main version = %T %v, want *LoadError", err, err)
|
||||||
|
}
|
||||||
|
requireSourceVersionDiagnostic(t, loadErr.Diagnostics(), CodeVersionError, "/version")
|
||||||
|
if checks != 0 {
|
||||||
|
t.Fatalf("missing required main invoked checker %d times", checks)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("required null is passed to the checker", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
checks := 0
|
||||||
|
loaded, err := Load(Bytes("main.yml", []byte("version: null\n")), testSecretBinding(), Options[sourceVersionNullConfig]{
|
||||||
|
SourceVersionMode: sourceVersionModeRequired,
|
||||||
|
Definition: Definition[sourceVersionNullConfig]{Version: &VersionRule{
|
||||||
|
Path: Keys("version"),
|
||||||
|
Check: func(version Version, _ *Validation) error {
|
||||||
|
checks++
|
||||||
|
if !version.Present || !version.Null || version.Text != "" {
|
||||||
|
return errors.New("unexpected null version")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
if err != nil || loaded == nil || checks != 1 || loaded.Config.Version != nil {
|
||||||
|
t.Fatalf("required null load = %#v, callbacks %d, %v", loaded, checks, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, mode := range []sourceVersionMode{sourceVersionModeIfPresent, sourceVersionModeRequired} {
|
||||||
|
mode := mode
|
||||||
|
t.Run(map[sourceVersionMode]string{sourceVersionModeIfPresent: "if-present", sourceVersionModeRequired: "required"}[mode], func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
checks := 0
|
||||||
|
loaded, err := Load(NoSource(), testSecretBinding(), Options[sourceVersionNullConfig]{
|
||||||
|
SourceVersionMode: mode,
|
||||||
|
Definition: Definition[sourceVersionNullConfig]{Version: &VersionRule{
|
||||||
|
Path: Keys("version"), Check: func(version Version, _ *Validation) error {
|
||||||
|
checks++
|
||||||
|
if version.Present || version.Null {
|
||||||
|
return errors.New("NoSource should report absence")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
if err != nil || loaded == nil || checks != 1 || len(testLoaded(loaded).Sources()) != 0 {
|
||||||
|
t.Fatalf("NoSource load = %#v, callbacks %d, %v", loaded, checks, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSourceVersionLoadReportsMountedMainErrorsAtLocalPath(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
checks := 0
|
||||||
|
_, err := LoadLayered(Input{
|
||||||
|
Source: Bytes("main.yml", []byte("version: 5\n")), Destination: Keys("service"),
|
||||||
|
}, testSecretBinding(), LayeredOptions[sourceVersionMountedConfig]{
|
||||||
|
SourceVersionMode: sourceVersionModeRequired,
|
||||||
|
Definition: Definition[sourceVersionMountedConfig]{Version: &VersionRule{
|
||||||
|
Path: Keys("version"),
|
||||||
|
Check: func(Version, *Validation) error { checks++; return errors.New("rejected") },
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
var loadErr *LoadError
|
||||||
|
if !errors.As(err, &loadErr) {
|
||||||
|
t.Fatalf("mounted main rejection = %T %v, want *LoadError", err, err)
|
||||||
|
}
|
||||||
|
requireSourceVersionDiagnostic(t, loadErr.Diagnostics(), CodeVersionHookFailed, "/version")
|
||||||
|
if checks != 1 {
|
||||||
|
t.Fatalf("mounted main checker calls = %d, want 1", checks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSourceVersionLoadChecksMainBeforeLayeredAcquisition(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
checks := 0
|
||||||
|
_, err := LoadLayered(Input{Source: Bytes("main.yml", []byte("version: 5\n"))}, testSecretBinding(), LayeredOptions[sourceVersionNullConfig]{
|
||||||
|
SourceVersionMode: sourceVersionModeIfPresent,
|
||||||
|
Definition: Definition[sourceVersionNullConfig]{Version: &VersionRule{
|
||||||
|
Path: Keys("version"), Check: func(Version, *Validation) error { checks++; return errors.New("rejected") },
|
||||||
|
}},
|
||||||
|
Underlays: []Underlay{{Inputs: []Input{{Source: Bytes("unread.yml", []byte("[malformed"))}}}},
|
||||||
|
})
|
||||||
|
var loadErr *LoadError
|
||||||
|
if !errors.As(err, &loadErr) {
|
||||||
|
t.Fatalf("main rejection = %T %v, want *LoadError", err, err)
|
||||||
|
}
|
||||||
|
requireSourceVersionDiagnostic(t, loadErr.Diagnostics(), CodeVersionHookFailed, "/version")
|
||||||
|
if checks != 1 {
|
||||||
|
t.Fatalf("main checker calls = %d, want 1", checks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSourceVersionEnabledBuildsSchemaBeforeMainCallback(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
checks := 0
|
||||||
|
_, err := Load(Bytes("main.yml", []byte("version: 5\n")), testSecretBinding(), Options[int]{
|
||||||
|
SourceVersionMode: sourceVersionModeIfPresent,
|
||||||
|
Definition: Definition[int]{Version: &VersionRule{
|
||||||
|
Path: Keys("version"), Check: func(Version, *Validation) error { checks++; return nil },
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
if err == nil || checks != 0 {
|
||||||
|
t.Fatalf("invalid schema load = %v, checker calls %d; want schema failure before callback", err, checks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSourceVersionLoadRejectsOptionsBeforeReadingMain(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
checkerCalls := 0
|
||||||
|
definition := Definition[sourceVersionNullConfig]{Version: &VersionRule{
|
||||||
|
Path: Keys("version"), Check: func(Version, *Validation) error { checkerCalls++; return nil },
|
||||||
|
}}
|
||||||
|
_, err := Load(Bytes("malformed.yml", []byte("[not valid")), testSecretBinding(), Options[sourceVersionNullConfig]{
|
||||||
|
Definition: definition, SourceVersionMode: sourceVersionMode(255),
|
||||||
|
})
|
||||||
|
var loadErr *LoadError
|
||||||
|
if !errors.As(err, &loadErr) {
|
||||||
|
t.Fatalf("invalid-mode error = %T %v, want *LoadError", err, err)
|
||||||
|
}
|
||||||
|
requireSourceVersionDiagnostic(t, loadErr.Diagnostics(), CodeOptionsInvalid, "")
|
||||||
|
if checkerCalls != 0 {
|
||||||
|
t.Fatalf("invalid mode invoked checker %d times", checkerCalls)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = Load(Bytes("malformed.yml", []byte("[not valid")), testSecretBinding(), Options[sourceVersionNullConfig]{SourceVersionMode: sourceVersionModeRequired})
|
||||||
|
if !errors.As(err, &loadErr) {
|
||||||
|
t.Fatalf("missing-rule error = %T %v, want *LoadError", err, err)
|
||||||
|
}
|
||||||
|
requireSourceVersionDiagnostic(t, loadErr.Diagnostics(), CodeOptionsInvalid, "")
|
||||||
|
|
||||||
|
_, err = LoadLayered(Input{Source: Bytes("malformed.yml", []byte("[not valid"))}, testSecretBinding(), LayeredOptions[sourceVersionNullConfig]{
|
||||||
|
SourceVersionMode: sourceVersionModeRequired,
|
||||||
|
})
|
||||||
|
if !errors.As(err, &loadErr) {
|
||||||
|
t.Fatalf("layered missing-rule error = %T %v, want *LoadError", err, err)
|
||||||
|
}
|
||||||
|
requireSourceVersionDiagnostic(t, loadErr.Diagnostics(), CodeOptionsInvalid, "")
|
||||||
|
}
|
||||||
|
|
||||||
func mustParseSourceVersionPath(t *testing.T, input string) Path {
|
func mustParseSourceVersionPath(t *testing.T, input string) Path {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
path, err := ParsePath(input)
|
path, err := ParsePath(input)
|
||||||
|
|||||||
Reference in New Issue
Block a user