Files
ConfigKit/internal/engine/import_evidence_test.go

304 lines
10 KiB
Go

package engine
import (
"errors"
"fmt"
"path/filepath"
"reflect"
"testing"
)
func TestBroadEmptyImportSelectionsKeepDeclaringSourceOwnership(t *testing.T) {
t.Parallel()
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
imports:
- glob: fragments/*.yml
- path: []
`)
const fragments = 32
for index := 0; index < fragments; index++ {
writeImportFile(t, directory, fmt.Sprintf("fragments/fragment-%02d.yml", index), `configkit:
imports:
- path: missing.yml
- path: []
`)
}
loaded, err := testLoad[map[string]any](File(mainPath), Options[map[string]any]{Imports: true})
if err != nil {
t.Fatal(err)
}
sources := loaded.Sources()
if len(sources) != fragments+1 {
t.Fatalf("source count = %d, want %d", len(sources), fragments+1)
}
for index, source := range sources {
wantPaths := []string{"/configkit/imports/0", "/configkit/imports/1"}
if index == 0 {
wantPaths = wantPaths[1:]
}
if len(source.importSelections) != len(wantPaths) {
t.Fatalf("source %s selections = %#v", source.ID, source.importSelections)
}
for selectionIndex, selection := range source.importSelections {
if selection.path.String() != wantPaths[selectionIndex] || selection.location.SourceID != source.ID ||
selection.selector != importSelectorPath || selection.outcome != importSelectionNone {
t.Errorf("source %s selection %d = %#v", source.ID, selectionIndex, selection)
}
}
}
}
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())
}