Retain main version warnings in layered loads
This commit is contained in:
@@ -293,7 +293,13 @@ resolution, including a null component, without adding a broader name ban.
|
|||||||
|
|
||||||
## 8. Retain Nonfatal Main-Version Diagnostics In Layered Loads — P2
|
## 8. Retain Nonfatal Main-Version Diagnostics In Layered Loads — P2
|
||||||
|
|
||||||
Status: [ ] Open.
|
Status: [x] Complete.
|
||||||
|
|
||||||
|
Completion: layered assembly returns nonfatal main-version diagnostics on success
|
||||||
|
and alongside later failures; the shared caller stops only for errors. Regression
|
||||||
|
coverage verifies ordinary/layered warning parity, exactly one checker call, and
|
||||||
|
warnings retained with later structural or decoding errors. Verified through
|
||||||
|
`./scripts/check.sh`, including existing source-version ordering coverage.
|
||||||
|
|
||||||
Owner: [`assembleLayeredInput`](../../internal/engine/load_request.go), lines 143–152.
|
Owner: [`assembleLayeredInput`](../../internal/engine/load_request.go), lines 143–152.
|
||||||
|
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ func executeLoadRequest[T any](request loadRequest[T], secrets SecretBinding, lo
|
|||||||
accumulator.add(versionState.diagnostics[reportedVersionDiagnostics:]...)
|
accumulator.add(versionState.diagnostics[reportedVersionDiagnostics:]...)
|
||||||
accumulator.sources = input.sources
|
accumulator.sources = input.sources
|
||||||
accumulator.add(diagnostics...)
|
accumulator.add(diagnostics...)
|
||||||
if len(diagnostics) != 0 {
|
if hasErrors(diagnostics) {
|
||||||
return nil, accumulator.loadError()
|
return nil, accumulator.loadError()
|
||||||
}
|
}
|
||||||
if request.ordinary && input.parsed.source != nil {
|
if request.ordinary && input.parsed.source != nil {
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ func assembleLayeredInput[T any](request loadRequest[T], input assembledInput, a
|
|||||||
return input, diagnostics
|
return input, diagnostics
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var versionDiagnostics []Diagnostic
|
||||||
if request.sourceVersionMode == sourceVersionModeDefault {
|
if request.sourceVersionMode == sourceVersionModeDefault {
|
||||||
var mainRoot *document.Node
|
var mainRoot *document.Node
|
||||||
if mainComposition != nil {
|
if mainComposition != nil {
|
||||||
@@ -153,8 +154,9 @@ func assembleLayeredInput[T any](request loadRequest[T], input assembledInput, a
|
|||||||
} else if input.document != nil {
|
} else if input.document != nil {
|
||||||
mainRoot = input.document
|
mainRoot = input.document
|
||||||
}
|
}
|
||||||
if diagnostics := checkVersion(request.definition.Version, mainRoot); hasErrors(diagnostics) {
|
versionDiagnostics = checkVersion(request.definition.Version, mainRoot)
|
||||||
return input, diagnostics
|
if hasErrors(versionDiagnostics) {
|
||||||
|
return input, versionDiagnostics
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if mainComposition != nil {
|
if mainComposition != nil {
|
||||||
@@ -170,7 +172,7 @@ func assembleLayeredInput[T any](request loadRequest[T], input assembledInput, a
|
|||||||
request.sequencePolicy, declarations,
|
request.sequencePolicy, declarations,
|
||||||
)
|
)
|
||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
return input, diagnostics
|
return input, append(versionDiagnostics, diagnostics...)
|
||||||
}
|
}
|
||||||
projectTierSourceDestinations(input.sources[:mainSourceCount], mainEvidence, operations)
|
projectTierSourceDestinations(input.sources[:mainSourceCount], mainEvidence, operations)
|
||||||
}
|
}
|
||||||
@@ -180,7 +182,7 @@ func assembleLayeredInput[T any](request loadRequest[T], input assembledInput, a
|
|||||||
assembly = newAssemblyAccumulator(composition)
|
assembly = newAssemblyAccumulator(composition)
|
||||||
}
|
}
|
||||||
if assembly == nil {
|
if assembly == nil {
|
||||||
return input, nil
|
return input, versionDiagnostics
|
||||||
}
|
}
|
||||||
assembly.current.inputBytes = accounting.bytes
|
assembly.current.inputBytes = accounting.bytes
|
||||||
assembly.current.inputNodes = accounting.nodes
|
assembly.current.inputNodes = accounting.nodes
|
||||||
@@ -193,17 +195,17 @@ func assembleLayeredInput[T any](request loadRequest[T], input assembledInput, a
|
|||||||
overlayPolicy := []sequencePolicy{request.sequencePolicy}
|
overlayPolicy := []sequencePolicy{request.sequencePolicy}
|
||||||
input.sources, diagnostics = loadSelectedOverlayIntoWithVersion(selection, request.overlayFilesystem, selected, assembly, input.sources, request.overlay, request.protectionVersion(), defaultImportLimits(), input.versionState, overlayPolicy...)
|
input.sources, diagnostics = loadSelectedOverlayIntoWithVersion(selection, request.overlayFilesystem, selected, assembly, input.sources, request.overlay, request.protectionVersion(), defaultImportLimits(), input.versionState, overlayPolicy...)
|
||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
return input, diagnostics
|
return input, append(versionDiagnostics, diagnostics...)
|
||||||
}
|
}
|
||||||
contexts, diagnostic := contextsForAcquiredSources(selection, request.overlayFilesystem, selected.source, input.sources[len(input.sources)-1:])
|
contexts, diagnostic := contextsForAcquiredSources(selection, request.overlayFilesystem, selected.source, input.sources[len(input.sources)-1:])
|
||||||
if diagnostic != nil {
|
if diagnostic != nil {
|
||||||
return input, []Diagnostic{*diagnostic}
|
return input, append(versionDiagnostics, *diagnostic)
|
||||||
}
|
}
|
||||||
mergeResourceContexts(&input.contexts, contexts)
|
mergeResourceContexts(&input.contexts, contexts)
|
||||||
}
|
}
|
||||||
input.composition = assembly.current
|
input.composition = assembly.current
|
||||||
input.document = assembly.current.root
|
input.document = assembly.current.root
|
||||||
return input, nil
|
return input, versionDiagnostics
|
||||||
}
|
}
|
||||||
|
|
||||||
func acquireUnderlayGroupScopes(group loadGroup, index int, rule *VersionRule, accounting *importAccounting, versionState *sourceVersionState, policy sequencePolicy) (*acquiredUnderlayGroup, []Diagnostic) {
|
func acquireUnderlayGroupScopes(group loadGroup, index int, rule *VersionRule, accounting *importAccounting, versionState *sourceVersionState, policy sequencePolicy) (*acquiredUnderlayGroup, []Diagnostic) {
|
||||||
|
|||||||
@@ -9,6 +9,77 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestDefaultMainVersionWarningSurvivesLayeredLoading(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
type config struct {
|
||||||
|
Version string `yaml:"version"`
|
||||||
|
Count int `yaml:"count"`
|
||||||
|
Items []map[string]any `yaml:"items"`
|
||||||
|
}
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
ordinary bool
|
||||||
|
body, lower string
|
||||||
|
failure bool
|
||||||
|
}{
|
||||||
|
{name: "ordinary", ordinary: true},
|
||||||
|
{name: "layered", lower: "{}"},
|
||||||
|
{name: "later structural failure", body: "configkit:\n sequences: [{destination: [items], key: id}]\nitems: []\n", lower: "items: null", failure: true},
|
||||||
|
{name: "later decoding failure", body: "count: invalid\n", lower: "{}", failure: true},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
calls := 0
|
||||||
|
definition := Definition[config]{Version: &VersionRule{
|
||||||
|
Path: Keys("version"),
|
||||||
|
Check: func(version Version, validation *Validation) error {
|
||||||
|
calls++
|
||||||
|
if version.Text != "current" {
|
||||||
|
t.Errorf("main checker received %q", version.Text)
|
||||||
|
}
|
||||||
|
validation.Warning(Keys("version"), "accepted main version")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
source := Bytes("main", []byte("version: current\n"+test.body))
|
||||||
|
var loaded *testResult[config]
|
||||||
|
var err error
|
||||||
|
if test.ordinary {
|
||||||
|
loaded, err = testLoad(source, Options[config]{Definition: definition})
|
||||||
|
} else {
|
||||||
|
loaded, err = testGroupedLoad(loadRequest[config]{
|
||||||
|
main: loadInput{source: source}, definition: definition,
|
||||||
|
underlays: []loadGroup{{inputs: []loadInput{{source: Bytes("lower", []byte(test.lower))}}}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
var diagnostics []Diagnostic
|
||||||
|
if test.failure {
|
||||||
|
var loadErr *LoadError
|
||||||
|
if !errors.As(err, &loadErr) || loaded != nil {
|
||||||
|
t.Fatalf("expected failed load: %#v, %v", loaded, err)
|
||||||
|
}
|
||||||
|
diagnostics = loadErr.Diagnostics()
|
||||||
|
if !hasErrors(diagnostics) {
|
||||||
|
t.Fatal("later failure was not reported")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
diagnostics = loaded.Diagnostics()
|
||||||
|
}
|
||||||
|
warnings := 0
|
||||||
|
for _, diagnostic := range diagnostics {
|
||||||
|
if diagnostic.Code == CodeVersionWarning && diagnostic.Severity == SeverityWarning {
|
||||||
|
warnings++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if calls != 1 || warnings != 1 {
|
||||||
|
t.Fatalf("main checker calls = %d, retained warnings = %d; diagnostics = %#v", calls, warnings, diagnostics)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSourceVersionAbsentPathKeepsUnresolvedSegmentIntent(t *testing.T) {
|
func TestSourceVersionAbsentPathKeepsUnresolvedSegmentIntent(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user