Snapshot import defaults at load preflight

This commit is contained in:
2026-10-26 00:13:34 +00:00
parent c25cf39d19
commit 643f43ce6a
10 changed files with 73 additions and 43 deletions

View File

@@ -81,31 +81,14 @@ func toEngineOverrides(values []Override) []engine.Override {
func toEngineImportDefaults(value ImportDefaults) engine.ImportDefaults { func toEngineImportDefaults(value ImportDefaults) engine.ImportDefaults {
return engine.ImportDefaults{ return engine.ImportDefaults{
Required: cloneImportDefaultBool(value.Required), Required: value.Required,
Follow: cloneImportDefaultBool(value.Follow), Follow: value.Follow,
Hidden: cloneImportDefaultBool(value.Hidden), Hidden: value.Hidden,
Recursive: cloneImportDefaultBool(value.Recursive), Recursive: value.Recursive,
Pattern: cloneImportDefaultStrings(value.Pattern), Pattern: value.Pattern,
} }
} }
func cloneImportDefaultBool(value *bool) *bool {
if value == nil {
return nil
}
cloned := *value
return &cloned
}
func cloneImportDefaultStrings(values []string) []string {
if values == nil {
return nil
}
cloned := make([]string, len(values))
copy(cloned, values)
return cloned
}
func fromEngineSourceVersionCheck(value *engine.SourceVersionCheck) *SourceVersionCheck { func fromEngineSourceVersionCheck(value *engine.SourceVersionCheck) *SourceVersionCheck {
if value == nil { if value == nil {
return nil return nil

View File

@@ -151,7 +151,8 @@ building the package here; do not report that deferred check as passed.
Finding: **F07**, defaults handoff. Owners: root `conversion.go`, `lifecycle.go`, Finding: **F07**, defaults handoff. Owners: root `conversion.go`, `lifecycle.go`,
`layered.go`; engine defaults/preflight/coordinator owners; `facade_import_defaults_test.go` `layered.go`; engine defaults/preflight/coordinator owners; `facade_import_defaults_test.go`
and `internal/engine/import_spec_test.go`. Status: **not started**. and `internal/engine/import_spec_test.go`. Status: **complete**. Validation passed:
`env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -run 'Import|Glob|Directory|SourceVersion'`, `env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test . -run 'ImportDefaults|FacadeImport|SourceVersion'`, `env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./...`, `env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test -race ./...`, and `git diff --check`.
1. Set engine `ImportDefaults` fields directly in root `toEngineOptions` and 1. Set engine `ImportDefaults` fields directly in root `toEngineOptions` and
`toEngineLayeredOptions`. Retain `toEngineImportDefaults` as the explicit `toEngineLayeredOptions`. Retain `toEngineImportDefaults` as the explicit

View File

@@ -6,8 +6,6 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
"gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
) )
func TestImportDepthChecksSuffixAfterSaturation(t *testing.T) { func TestImportDepthChecksSuffixAfterSaturation(t *testing.T) {
@@ -125,20 +123,61 @@ func TestImportDefaultsPreserveEmptyPatternAndRequiredness(t *testing.T) {
if sources := loadError.Sources(); len(sources) != 1 || len(sources[0].ImportSelections) != 0 { if sources := loadError.Sources(); len(sources) != 1 || len(sources[0].ImportSelections) != 0 {
t.Fatalf("required empty selection was recorded as optional: %#v", sources) t.Fatalf("required empty selection was recorded as optional: %#v", sources)
} }
}
recursiveMain := writeFacadeImportFile(t, directory, "recursive.yml", `configkit: func TestImportDefaultsAreSnapshottedBeforeOrdinaryAndLayeredCallbacks(t *testing.T) {
type config struct {
Version string `yaml:"version"`
Imported string `yaml:"imported"`
}
for _, layered := range []bool{false, true} {
t.Run(map[bool]string{false: "ordinary", true: "layered"}[layered], func(t *testing.T) {
directory := t.TempDir()
mainPath := writeFacadeImportFile(t, directory, "main.yml", `version: v1
configkit:
imports: imports:
- dir: recursive-files - dir: parts
`) `)
writeFacadeImportFile(t, directory, "recursive-files/nested/value.cfg", "value: selected\n") writeFacadeImportFile(t, directory, "parts/nested/value.cfg", "imported: selected\n")
recursive := true
patterns := []string{"*.cfg"} recursive := true
engineOptions := toEngineOptions(Options[config]{Imports: true, ImportDefaults: ImportDefaults{Recursive: &recursive, Pattern: patterns}}) patterns := []string{"*.cfg"}
recursive = false definition := Definition[config]{Version: &VersionRule{
patterns[0] = "[" Path: Keys("version"),
loadedEngine, err := engine.Load(File(recursiveMain).toEngine(), secretBinding(), engineOptions) Check: func(version Version, _ *Validation) error {
if err != nil || loadedEngine.Config.Value != "selected" { if version != (Version{Present: true, Text: "v1"}) {
t.Fatalf("load after mutating caller defaults = %#v, %v", loadedEngine, err) t.Errorf("version callback received %#v", version)
}
recursive = false
patterns[0] = "*.skip"
return nil
},
}}
load := func() (*Result[config], error) {
defaults := ImportDefaults{Recursive: &recursive, Pattern: patterns}
if layered {
return LoadLayered(Input{Source: File(mainPath), Imports: true}, LayeredOptions[config]{
Definition: definition, SourceVersionMode: SourceVersionModeIfPresent, ImportDefaults: defaults,
})
}
return Load(File(mainPath), Options[config]{
Definition: definition, SourceVersionMode: SourceVersionModeIfPresent, Imports: true, ImportDefaults: defaults,
})
}
first, err := load()
if err != nil || first.Config.Imported != "selected" {
t.Fatalf("load with pre-callback defaults = %#v, %v", first, err)
}
if recursive || patterns[0] != "*.skip" {
t.Fatalf("version callback did not mutate caller defaults: recursive=%v pattern=%q", recursive, patterns[0])
}
second, err := load()
if err != nil || second.Config.Imported != "" {
t.Fatalf("subsequent load reused earlier defaults: %#v, %v", second, err)
}
})
} }
} }

View File

@@ -458,7 +458,7 @@ func testAssembleLayeredInput[T any](request loadRequest[T], input assembledInpu
accounting = &importAccounting{} accounting = &importAccounting{}
} }
policy := acquisitionPolicy{ policy := acquisitionPolicy{
importDefaults: snapshotImportDefaults(request.importDefaults), importDefaults: request.importDefaults,
sequencePolicy: request.sequencePolicy, sequencePolicy: request.sequencePolicy,
importLimits: defaultImportLimits(), importLimits: defaultImportLimits(),
} }

View File

@@ -104,7 +104,6 @@ func validateImportDefaults(value ImportDefaults) []Diagnostic {
} }
func resolveImportPolicy(spec importDeclarationSpec, defaults ImportDefaults) (resolvedImportPolicy, []Diagnostic) { func resolveImportPolicy(spec importDeclarationSpec, defaults ImportDefaults) (resolvedImportPolicy, []Diagnostic) {
defaults = snapshotImportDefaults(defaults)
policy := resolvedImportPolicy{ policy := resolvedImportPolicy{
required: boolOr(spec.required, defaults.Required, false), required: boolOr(spec.required, defaults.Required, false),
follow: true, follow: true,

View File

@@ -189,10 +189,18 @@ func TestResolveImportPolicyAppliesDefaultsIndependently(t *testing.T) {
snapshot := snapshotImportDefaults(defaults) snapshot := snapshotImportDefaults(defaults)
*defaults.Required = false *defaults.Required = false
*defaults.Follow = false
*defaults.Hidden = false
*defaults.Recursive = false
defaults.Pattern[0] = "*.toml" defaults.Pattern[0] = "*.toml"
if snapshot.Required == nil || !*snapshot.Required || snapshot.Pattern[0] != "*.yaml" { if snapshot.Required == nil || !*snapshot.Required || snapshot.Follow == nil || !*snapshot.Follow ||
snapshot.Hidden == nil || !*snapshot.Hidden || snapshot.Recursive == nil || !*snapshot.Recursive || snapshot.Pattern[0] != "*.yaml" {
t.Fatalf("snapshot shares mutable inputs: %#v", snapshot) t.Fatalf("snapshot shares mutable inputs: %#v", snapshot)
} }
omittedSnapshot := snapshotImportDefaults(ImportDefaults{})
if omittedSnapshot.Required != nil || omittedSnapshot.Follow != nil || omittedSnapshot.Hidden != nil || omittedSnapshot.Recursive != nil {
t.Fatal("snapshot changed omitted booleans into explicit values")
}
empty := snapshotImportDefaults(ImportDefaults{Pattern: []string{}}) empty := snapshotImportDefaults(ImportDefaults{Pattern: []string{}})
if empty.Pattern == nil || len(empty.Pattern) != 0 { if empty.Pattern == nil || len(empty.Pattern) != 0 {
t.Fatalf("snapshot lost explicit empty pattern: %#v", empty) t.Fatalf("snapshot lost explicit empty pattern: %#v", empty)

View File

@@ -41,7 +41,7 @@ func LoadLayered[T any](main Input, secrets SecretBinding, supplied ...LayeredOp
destination: main.Destination.clone(), placementMode: main.placementMode, destination: main.Destination.clone(), placementMode: main.placementMode,
}, },
definition: options.Definition, sourceVersionMode: options.SourceVersionMode, definition: options.Definition, sourceVersionMode: options.SourceVersionMode,
importDefaults: snapshotImportDefaults(options.ImportDefaults), importDefaults: options.ImportDefaults,
overlay: options.Overlay, overlay: options.Overlay,
overlayFilesystem: options.OverlayFilesystem, overlayFilesystem: options.OverlayFilesystem,
overrides: append([]Override(nil), options.Overrides...), overrides: append([]Override(nil), options.Overrides...),

View File

@@ -185,7 +185,7 @@ func executeLoadRequest[T any](request loadRequest[T], secrets SecretBinding, lo
accumulator := diagnosticAccumulator{} accumulator := diagnosticAccumulator{}
accounting := &importAccounting{} accounting := &importAccounting{}
policy := acquisitionPolicy{ policy := acquisitionPolicy{
importDefaults: snapshotImportDefaults(request.importDefaults), importDefaults: request.importDefaults,
sequencePolicy: request.sequencePolicy, sequencePolicy: request.sequencePolicy,
importLimits: defaultImportLimits(), importLimits: defaultImportLimits(),
} }

View File

@@ -75,8 +75,8 @@ func toEngineLayeredOptions[T any](options LayeredOptions[T]) engine.LayeredOpti
SourceVersionMode: engine.SourceVersionModeFromFacade(uint8(options.SourceVersionMode)), SourceVersionMode: engine.SourceVersionModeFromFacade(uint8(options.SourceVersionMode)),
Overlay: options.Overlay, Overlay: options.Overlay,
OverlayFilesystem: toEngineFilesystemContext(options.OverlayFilesystem), OverlayFilesystem: toEngineFilesystemContext(options.OverlayFilesystem),
ImportDefaults: toEngineImportDefaults(options.ImportDefaults),
} }
result = engine.LayeredOptionsWithImportDefaults(result, toEngineImportDefaults(options.ImportDefaults))
if options.Underlays != nil { if options.Underlays != nil {
result.Underlays = make([]engine.Underlay, len(options.Underlays)) result.Underlays = make([]engine.Underlay, len(options.Underlays))
for index, group := range options.Underlays { for index, group := range options.Underlays {

View File

@@ -317,9 +317,9 @@ func toEngineOptions[T any](options Options[T]) engine.Options[T] {
Imports: options.Imports, Imports: options.Imports,
Overlay: options.Overlay, Overlay: options.Overlay,
SourceVersionMode: engine.SourceVersionModeFromFacade(uint8(options.SourceVersionMode)), SourceVersionMode: engine.SourceVersionModeFromFacade(uint8(options.SourceVersionMode)),
ImportDefaults: toEngineImportDefaults(options.ImportDefaults),
} }
result = engine.OptionsWithPlacement(result, toEnginePath(options.Destination.clone()), uint8(options.PlacementMode)) result = engine.OptionsWithPlacement(result, toEnginePath(options.Destination.clone()), uint8(options.PlacementMode))
result = engine.OptionsWithImportDefaults(result, toEngineImportDefaults(options.ImportDefaults))
if options.Filesystem != nil { if options.Filesystem != nil {
result.Filesystem = toEngineFilesystemContext(options.Filesystem) result.Filesystem = toEngineFilesystemContext(options.Filesystem)
} }