300 lines
12 KiB
Go
300 lines
12 KiB
Go
package configkit
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestImportDepthChecksSuffixAfterSaturation(t *testing.T) {
|
|
for _, test := range []struct {
|
|
depth string
|
|
valid bool
|
|
}{
|
|
{"999999999999999999999999.5", false},
|
|
{"999999999999999999999999e1", false},
|
|
{"999999999999999999999999", true},
|
|
{"-0", true},
|
|
} {
|
|
t.Run(test.depth, func(t *testing.T) {
|
|
main := writeFacadeImportFile(t, t.TempDir(), "main.yml", "value: true\nconfigkit:\n imports:\n - dir: absent\n recursive: true\n depth: "+test.depth+"\n")
|
|
result, err := Load[map[string]any](File(main), Options[map[string]any]{Imports: true})
|
|
if test.valid {
|
|
if err != nil || result.Config["value"] != true {
|
|
t.Fatalf("valid depth: %v", err)
|
|
}
|
|
return
|
|
}
|
|
var loadError *LoadError
|
|
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 {
|
|
t.Fatalf("invalid depth accepted: %v", err)
|
|
}
|
|
if diagnostic := loadError.Diagnostics()[0]; diagnostic.Code != CodeImportInvalid || diagnostic.Path.String() != "/configkit/imports/0/depth" {
|
|
t.Fatalf("invalid depth diagnostic = %#v", diagnostic)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestImportDefaultsApplyToTransitiveAndUnderlayImports(t *testing.T) {
|
|
type config struct {
|
|
Main string `yaml:"main"`
|
|
Transitive string `yaml:"transitive"`
|
|
Exception string `yaml:"exception"`
|
|
Underlay string `yaml:"underlay"`
|
|
IgnoredDeep string `yaml:"ignored_deep"`
|
|
}
|
|
directory := t.TempDir()
|
|
mainPath := writeFacadeImportFile(t, directory, "main.yml", `configkit:
|
|
imports:
|
|
- path: parent/parent.yml
|
|
main: main
|
|
`)
|
|
writeFacadeImportFile(t, directory, "parent/parent.yml", `configkit:
|
|
imports:
|
|
- dir: generated
|
|
- dir: explicit
|
|
pattern: '*.yaml'
|
|
recursive: false
|
|
`)
|
|
transitivePath := writeFacadeImportFile(t, directory, "parent/generated/deep/transitive.cfg", "transitive: selected\n")
|
|
writeFacadeImportFile(t, directory, "parent/generated/ignored.yaml", "ignored_deep: wrong-pattern\n")
|
|
explicitPath := writeFacadeImportFile(t, directory, "parent/explicit/exception.yaml", "exception: declaration\n")
|
|
writeFacadeImportFile(t, directory, "parent/explicit/deep/ignored.yaml", "ignored_deep: wrong-depth\n")
|
|
writeFacadeImportFile(t, directory, "parent/explicit/ignored.cfg", "ignored_deep: wrong-explicit-pattern\n")
|
|
underlayPath := writeFacadeImportFile(t, directory, "underlay.yml", `configkit:
|
|
imports:
|
|
- dir: underlay-files
|
|
`)
|
|
underlayImportPath := writeFacadeImportFile(t, directory, "underlay-files/underlay.cfg", "underlay: selected\n")
|
|
recursive := true
|
|
loaded, err := LoadLayered(Input{Source: File(mainPath), Imports: true}, LayeredOptions[config]{
|
|
ImportDefaults: ImportDefaults{Recursive: &recursive, Pattern: []string{"*.cfg"}},
|
|
Underlays: []Underlay{{Inputs: []Input{{Source: File(underlayPath), Imports: true}}}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("LoadLayered: %v", err)
|
|
}
|
|
if loaded.Config.Main != "main" || loaded.Config.Transitive != "selected" || loaded.Config.Exception != "declaration" || loaded.Config.Underlay != "selected" || loaded.Config.IgnoredDeep != "" {
|
|
t.Fatalf("configuration = %#v", loaded.Config)
|
|
}
|
|
paths := make(map[string]bool)
|
|
for _, source := range loaded.Sources() {
|
|
paths[source.Path] = true
|
|
}
|
|
for _, selected := range []string{transitivePath, explicitPath, underlayImportPath} {
|
|
if !paths[canonicalFacadeImportPath(t, selected)] {
|
|
t.Errorf("selected source %q missing from source records", selected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestImportDefaultsPreserveEmptyPatternAndRequiredness(t *testing.T) {
|
|
type config struct {
|
|
Value string `yaml:"value"`
|
|
}
|
|
directory := t.TempDir()
|
|
mainPath := writeFacadeImportFile(t, directory, "main.yml", `configkit:
|
|
imports:
|
|
- dir: snippets
|
|
`)
|
|
writeFacadeImportFile(t, directory, "snippets/value.cfg", "value: imported\n")
|
|
|
|
unrestricted, err := Load(File(mainPath), Options[config]{Imports: true})
|
|
if err != nil || unrestricted.Config.Value != "imported" {
|
|
t.Fatalf("unspecified pattern = %#v, %v", unrestricted, err)
|
|
}
|
|
empty, err := Load(File(mainPath), Options[config]{Imports: true, ImportDefaults: ImportDefaults{Pattern: []string{}}})
|
|
if err != nil || empty.Config.Value != "" {
|
|
t.Fatalf("empty pattern = %#v, %v", empty, err)
|
|
}
|
|
emptySources := empty.Sources()
|
|
if len(emptySources) != 1 || len(emptySources[0].ImportSelections) != 1 || emptySources[0].ImportSelections[0].Path.String() != "/configkit/imports/0" || emptySources[0].ImportSelections[0].Selector != ImportSelectorDirectory || emptySources[0].ImportSelections[0].Outcome != ImportSelectionNone {
|
|
t.Fatalf("empty-pattern evidence = %#v", emptySources)
|
|
}
|
|
required := true
|
|
_, err = Load(File(mainPath), Options[config]{Imports: true, ImportDefaults: ImportDefaults{Required: &required, Pattern: []string{}}})
|
|
var loadError *LoadError
|
|
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 || loadError.Diagnostics()[0].Code != CodeImportMissing {
|
|
t.Fatalf("required empty selection error = %T %v", err, err)
|
|
}
|
|
if sources := loadError.Sources(); len(sources) != 1 || len(sources[0].ImportSelections) != 0 {
|
|
t.Fatalf("required empty selection was recorded as optional: %#v", sources)
|
|
}
|
|
}
|
|
|
|
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:
|
|
- dir: parts
|
|
`)
|
|
writeFacadeImportFile(t, directory, "parts/nested/value.cfg", "imported: selected\n")
|
|
|
|
recursive := true
|
|
patterns := []string{"*.cfg"}
|
|
definition := Definition[config]{Version: &VersionRule{
|
|
Path: Keys("version"),
|
|
Check: func(version Version, _ *Validation) error {
|
|
if version != (Version{Present: true, Text: "v1"}) {
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInvalidImportDefaultsPreflightBeforeOrdinaryAndLayeredReads(t *testing.T) {
|
|
type config struct {
|
|
Value string `yaml:"value"`
|
|
}
|
|
for _, layered := range []bool{false, true} {
|
|
input := strings.NewReader("value: read\n")
|
|
underlayInput := strings.NewReader("value: underlay\n")
|
|
defaultsCalled := false
|
|
resolveCalled := false
|
|
definition := Definition[config]{
|
|
Defaults: func() config { defaultsCalled = true; return config{} },
|
|
Resolve: func(*config, *Resolution) error { resolveCalled = true; return nil },
|
|
}
|
|
defaults := ImportDefaults{Pattern: []string{"["}}
|
|
var err error
|
|
if layered {
|
|
_, err = LoadLayered(Input{Source: Reader("reader.yml", input)}, LayeredOptions[config]{
|
|
Definition: definition, ImportDefaults: defaults,
|
|
Underlays: []Underlay{{Inputs: []Input{{Source: Reader("underlay.yml", underlayInput)}}}},
|
|
})
|
|
} else {
|
|
_, err = Load(Reader("reader.yml", input), Options[config]{Definition: definition, ImportDefaults: defaults})
|
|
}
|
|
var loadError *LoadError
|
|
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 || loadError.Diagnostics()[0].Code != CodeOptionsInvalid {
|
|
if loadError != nil {
|
|
t.Fatalf("layered=%v diagnostics = %#v", layered, loadError.Diagnostics())
|
|
}
|
|
t.Fatalf("layered=%v error = %T %v", layered, err, err)
|
|
}
|
|
if input.Len() != len("value: read\n") || underlayInput.Len() != len("value: underlay\n") || defaultsCalled || resolveCalled {
|
|
t.Fatalf("layered=%v consumed input or called application hooks: remaining=%d underlay=%d defaults=%v resolve=%v", layered, input.Len(), underlayInput.Len(), defaultsCalled, resolveCalled)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPublicImportSelectionSnapshotsAreDetachedAcrossResultErrorAndResolution(t *testing.T) {
|
|
type config struct {
|
|
Value string `yaml:"value"`
|
|
}
|
|
directory := t.TempDir()
|
|
mainPath := writeFacadeImportFile(t, directory, "main.yml", `configkit:
|
|
imports:
|
|
- dir: absent
|
|
value: loaded
|
|
`)
|
|
callbackPath := ""
|
|
loaded, err := Load(File(mainPath), Options[config]{Imports: true, Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
|
|
first, ok := resolution.Source("source-1")
|
|
if !ok || len(first.ImportSelections) != 1 {
|
|
t.Fatalf("resolution source = %#v, %v", first, ok)
|
|
}
|
|
first.ImportSelections[0].Path = Keys("callback mutation")
|
|
second, ok := resolution.Source("source-1")
|
|
if !ok || second.ImportSelections[0].Path.String() != "/configkit/imports/0" {
|
|
t.Fatalf("resolution snapshot changed after mutation: %#v", second)
|
|
}
|
|
callbackPath = second.ImportSelections[0].Path.String()
|
|
return nil
|
|
}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if callbackPath != "/configkit/imports/0" {
|
|
t.Fatalf("callback path = %q", callbackPath)
|
|
}
|
|
sources := loaded.Sources()
|
|
if len(sources) != 1 || len(sources[0].ImportSelections) != 1 || sources[0].ImportSelections[0].Location.SourceID != sources[0].ID || sources[0].ImportSelections[0].Selector != ImportSelectorDirectory {
|
|
t.Fatalf("result source evidence = %#v", sources)
|
|
}
|
|
sources[0].ImportSelections[0].Path = Keys("result mutation")
|
|
if got := loaded.Sources()[0].ImportSelections[0].Path.String(); got != "/configkit/imports/0" {
|
|
t.Fatalf("result snapshot changed after mutation: %q", got)
|
|
}
|
|
|
|
failurePath := writeFacadeImportFile(t, directory, "failure.yml", `configkit:
|
|
imports:
|
|
- dir: absent
|
|
- path: required-missing.yml
|
|
required: true
|
|
`)
|
|
_, err = Load(File(failurePath), Options[config]{Imports: true})
|
|
var loadError *LoadError
|
|
if !errors.As(err, &loadError) {
|
|
t.Fatalf("failed load error = %T %v", err, err)
|
|
}
|
|
errorSources := loadError.Sources()
|
|
if len(errorSources) != 1 || len(errorSources[0].ImportSelections) != 1 || errorSources[0].ImportSelections[0].Path.String() != "/configkit/imports/0" {
|
|
t.Fatalf("error source evidence = %#v", errorSources)
|
|
}
|
|
errorSources[0].ImportSelections[0].Path = Keys("error mutation")
|
|
if got := loadError.Sources()[0].ImportSelections[0].Path.String(); got != "/configkit/imports/0" {
|
|
t.Fatalf("error snapshot changed after mutation: %q", got)
|
|
}
|
|
}
|
|
|
|
func writeFacadeImportFile(t *testing.T, root, name, contents string) string {
|
|
t.Helper()
|
|
filename := filepath.Join(root, name)
|
|
if err := os.MkdirAll(filepath.Dir(filename), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filename, []byte(contents), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return filename
|
|
}
|
|
|
|
func canonicalFacadeImportPath(t *testing.T, filename string) string {
|
|
t.Helper()
|
|
canonical, err := filepath.EvalSymlinks(filename)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return filepath.ToSlash(canonical)
|
|
}
|