package engine import ( "errors" "path/filepath" "reflect" "testing" ) func TestOptionalEmptyImportSelectionsRemainSourceLocalAndDetached(t *testing.T) { t.Parallel() type config struct { Value string `yaml:"value"` } directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: - dir: absent - path: missing.yml - path: [] - dir: fragments exclude: '*.yml' - path: [partial-missing.yml, present.yml] - path: [] required: true `) writeImportFile(t, directory, "fragments/ignored.yml", "ignored: true\n") writeImportFile(t, directory, "present.yml", "value: imported\n") result, err := testLoad[config](File(mainPath), Options[config]{Imports: true}) if err != nil { t.Fatal(err) } if result.Config.Value != "imported" { t.Fatalf("value = %q", result.Config.Value) } sources := result.Sources() if len(sources) != 2 { t.Fatalf("sources = %#v", sources) } wantPaths := []string{"/configkit/imports/0", "/configkit/imports/1", "/configkit/imports/2", "/configkit/imports/3"} wantSelectors := []importSelector{importSelectorDirectory, importSelectorPath, importSelectorPath, importSelectorDirectory} evidence := sources[0].importSelections if len(evidence) != len(wantPaths) { t.Fatalf("empty-selection evidence = %#v", evidence) } for index, selection := range evidence { if selection.path.String() != wantPaths[index] || selection.location.SourceID != sources[0].ID || selection.selector != wantSelectors[index] || selection.outcome != importSelectionNone { t.Errorf("selection %d = %#v", index, selection) } } if len(sources[1].importSelections) != 0 { t.Fatalf("imported source unexpectedly owns declaration evidence: %#v", sources[1]) } sources[0].importSelections[0].path.segments[0].key = "changed" sources[0].importSelections[0].location.SourceID = "changed" if got := result.Sources()[0].importSelections[0]; got.path.String() != wantPaths[0] || got.location.SourceID != "source-1" { t.Fatalf("mutating a query snapshot changed the retained record: %#v", got) } other, err := testLoad[config](File(mainPath), Options[config]{Imports: true}) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(other.Sources()[0].importSelections, result.Inspection.Sources()[0].importSelections) { t.Fatal("one load's source snapshot affected another load") } } func TestFailedLoadRetainsOnlyCompletedEmptyImportSelections(t *testing.T) { t.Parallel() type config struct { Value string `yaml:"value"` } directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: - glob: absent/*.yml - path: required-missing.yml required: true - dir: never-visited value: main `) _, err := testLoad[config](File(mainPath), Options[config]{Imports: true}) var loadError *LoadError if !errors.As(err, &loadError) { t.Fatalf("error = %T %v", err, err) } sources := loadError.Sources() if len(sources) != 1 || len(sources[0].importSelections) != 1 { t.Fatalf("failed-load sources = %#v", sources) } selection := sources[0].importSelections[0] if selection.path.String() != "/configkit/imports/0" || selection.location.SourceID != sources[0].ID || selection.selector != importSelectorGlob || selection.outcome != importSelectionNone { t.Fatalf("retained selection = %#v", selection) } sources[0].importSelections[0].path.segments[0].key = "changed" if got := loadError.Sources()[0].importSelections[0].path.String(); got != "/configkit/imports/0" { t.Fatalf("mutating failed-load snapshot changed retained evidence: %q", got) } } func TestImportSelectionEvidenceSurvivesCompositionUnderlaysAndResolutionEntry(t *testing.T) { t.Parallel() type item struct { ID string `yaml:"id"` Metadata map[string]string `yaml:"metadata"` } type config struct { Value string `yaml:"value"` Helper string `yaml:"helper"` Items []item `yaml:"items"` Overlay string `yaml:"overlay"` } directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: - path: helper.yml - path: item.yml destination: [items] placement: sequence-item - dir: absent-main value: main `) helperPath := writeImportFile(t, directory, "helper.yml", `configkit: imports: - glob: absent-helper/*.yml helper: value `) itemPath := writeImportFile(t, directory, "item.yml", `configkit: imports: - path: [] - path: item-helper.yml destination: [metadata] id: selected `) itemHelperPath := writeImportFile(t, directory, "item-helper.yml", `configkit: imports: - dir: absent-item-helper key: value `) underlayPath := writeImportFile(t, directory, "underlay.yml", `configkit: imports: - path: [] value: underlay `) overlayPath := writeImportFile(t, directory, filepath.Join("overlays", "selected.yml"), "overlay: value\n") var resolutionSources map[string]SourceInfo loaded, err := LoadLayered(Input{Source: File(mainPath), Imports: true}, testSecretBinding(), LayeredOptions[config]{ Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error { resolutionSources = make(map[string]SourceInfo) for _, id := range []string{"source-1", "source-2", "source-3", "source-4", "source-5", "source-6"} { if source, ok := resolution.Source(id); ok { resolutionSources[id] = source } } return nil }}, Underlays: []Underlay{{Inputs: []Input{{Source: File(underlayPath), Imports: true}}}}, Overlay: "selected", OverlayFilesystem: &FilesystemContext{BaseDir: directory, Root: directory}, }) if err != nil { if loadError, ok := err.(*LoadError); ok { t.Fatalf("LoadLayered diagnostics: %#v", loadError.Diagnostics()) } t.Fatalf("LoadLayered: %v", err) } if loaded.Config.Value != "main" || loaded.Config.Overlay != "value" || len(loaded.Config.Items) != 1 || loaded.Config.Items[0].ID != "selected" { t.Fatalf("configuration = %#v", loaded.Config) } want := map[string]string{ "source-1": "/configkit/imports/2", "source-2": "/configkit/imports/0", "source-3": "/configkit/imports/0", "source-4": "/configkit/imports/0", "source-5": "/configkit/imports/0", } sources := loaded.Inspection.Sources() if len(sources) != 6 { t.Fatalf("sources = %#v", sources) } if sources[1].Path != canonicalTestPath(t, helperPath) || sources[2].Path != canonicalTestPath(t, itemPath) || sources[3].Path != canonicalTestPath(t, itemHelperPath) || sources[4].Path != canonicalTestPath(t, underlayPath) { t.Fatalf("acquired source order = %#v", sources) } if sources[3].destinationUnresolved || sources[3].Destination.String() != "/items/0/metadata" { t.Fatalf("nested item helper destination = %#v", sources[3]) } if sources[5].Path != canonicalTestPath(t, overlayPath) || sources[5].Layer != LayerOverlay { t.Fatalf("overlay source = %#v", sources[5]) } for id, path := range want { var found *SourceInfo for index := range sources { if sources[index].ID == id { found = &sources[index] break } } if found == nil || len(found.importSelections) != 1 || found.importSelections[0].path.String() != path || found.importSelections[0].location.SourceID != id { t.Errorf("source %s evidence = %#v, want %s", id, found, path) } if callbackSource, ok := resolutionSources[id]; !ok || len(callbackSource.importSelections) != 1 || callbackSource.importSelections[0].path.String() != path { t.Errorf("resolution source %s evidence = %#v, want %s", id, callbackSource, path) } } if len(sources[5].importSelections) != 0 { t.Fatalf("overlay unexpectedly has import-selection evidence: %#v", sources[5]) } } func TestFailedNestedItemLoadKeepsEvidenceWithUnresolvedDestination(t *testing.T) { t.Parallel() type item struct { ID string `yaml:"id"` } type config struct { Items []item `yaml:"items"` } directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: - path: item.yml destination: [items] placement: sequence-item `) writeImportFile(t, directory, "item.yml", `configkit: imports: - path: helper.yml destination: [metadata] `) writeImportFile(t, directory, "helper.yml", `configkit: imports: - dir: absent - path: missing.yml required: true key: value `) _, err := testLoad[config](File(mainPath), Options[config]{Imports: true}) var loadError *LoadError if !errors.As(err, &loadError) { t.Fatalf("error = %T %v", err, err) } for _, source := range loadError.Sources() { if source.Path != canonicalTestPath(t, filepath.Join(directory, "helper.yml")) { continue } if !source.destinationUnresolved || source.Destination.String() != "/items" || len(source.importSelections) != 1 || source.importSelections[0].path.String() != "/configkit/imports/0" || source.importSelections[0].location.SourceID != source.ID { t.Fatalf("failed nested item source = %#v", source) } return } t.Fatalf("helper source missing from failed-load snapshot: %#v", loadError.Sources()) }