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) }