Expose configuration search through the public facade
This commit is contained in:
230
facade_discovery_test.go
Normal file
230
facade_discovery_test.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package configkit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
)
|
||||
|
||||
type facadeDiscoveryConfig struct {
|
||||
Value string `yaml:"value"`
|
||||
Settings map[string]string `yaml:"settings"`
|
||||
}
|
||||
|
||||
func TestFacadeSearchPresetUsesCurrentWorkingDirectory(t *testing.T) {
|
||||
original, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root := t.TempDir()
|
||||
if err := os.Chdir(root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := os.Chdir(original); err != nil {
|
||||
t.Errorf("restore working directory: %v", err)
|
||||
}
|
||||
})
|
||||
writeFacadeImportFile(t, root, "myapp.yml", "value: working\n")
|
||||
|
||||
result, err := Load[facadeDiscoveryConfig](SearchConfig("myapp", WorkingDirectory()))
|
||||
if err != nil || result.Config.Value != "working" {
|
||||
t.Fatalf("working-directory search = %#v, %v", result, err)
|
||||
}
|
||||
if sources := result.Sources(); len(sources) != 1 || sources[0].Path != canonicalFacadePath(t, filepath.Join(root, "myapp.yml")) {
|
||||
t.Fatalf("working-directory sources = %#v", sources)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFacadeSearchCustomGenericNamesAndEmptyFilenameSelection(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
root := t.TempDir()
|
||||
filename := writeFacadeImportFile(t, root, "config.yml", "value: found\n")
|
||||
custom := SearchDirectory(root, DirectorySearchOptions{
|
||||
Filenames: []FilenameGroup{CustomFilenames("config.yml")},
|
||||
})
|
||||
result, err := Load[facadeDiscoveryConfig](SearchConfig("", custom))
|
||||
if err != nil || result.Config.Value != "found" {
|
||||
t.Fatalf("custom generic-only search with empty slug = %#v, %v", result, err)
|
||||
}
|
||||
if sources := result.Sources(); len(sources) != 1 || sources[0].Path != canonicalFacadePath(t, filename) {
|
||||
t.Fatalf("custom generic-only sources = %#v", sources)
|
||||
}
|
||||
|
||||
defaults := func() facadeDiscoveryConfig { return facadeDiscoveryConfig{Value: "default"} }
|
||||
empty := SearchDirectory(root, DirectorySearchOptions{Filenames: []FilenameGroup{}})
|
||||
absent, err := Load(SearchConfig("myapp", empty), Options[facadeDiscoveryConfig]{
|
||||
Definition: Definition[facadeDiscoveryConfig]{Defaults: defaults},
|
||||
})
|
||||
if err != nil || absent.Config.Value != "default" || len(absent.Sources()) != 0 {
|
||||
t.Fatalf("explicit empty filename selection = %#v, %v", absent, err)
|
||||
}
|
||||
omitted, err := Load[facadeDiscoveryConfig](SearchConfig("myapp", SearchDirectory(root)))
|
||||
if err != nil || omitted.Config.Value != "found" {
|
||||
t.Fatalf("omitted filename selection did not use defaults = %#v, %v", omitted, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFacadeSearchOrderingRequirednessAndSnapshots(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
root := t.TempDir()
|
||||
selectedPath := writeFacadeImportFile(t, root, "config.yml", "value: selected\n")
|
||||
laterPath := writeFacadeImportFile(t, root, "later.yml", "value: later\n")
|
||||
paths := []string{filepath.Join(root, "missing.yml")}
|
||||
names := []string{"config.yml"}
|
||||
groups := []FilenameGroup{CustomFilenames(names...)}
|
||||
directoryOptions := []DirectorySearchOptions{{Filenames: groups}}
|
||||
search := Searches(Paths(paths...), SearchDirectory(root, directoryOptions...), Paths(laterPath))
|
||||
source := SearchConfig("", search, SearchOptions{Required: true})
|
||||
|
||||
paths[0] = laterPath
|
||||
names[0] = "later.yml"
|
||||
groups[0] = CustomFilenames("later.yml")
|
||||
directoryOptions[0].Filenames[0] = CustomFilenames("later.yml")
|
||||
search = Paths(laterPath)
|
||||
|
||||
result, err := Load[facadeDiscoveryConfig](source)
|
||||
if err != nil || result.Config.Value != "selected" {
|
||||
t.Fatalf("ordered required search after caller mutation = %#v, %v", result, err)
|
||||
}
|
||||
if sources := result.Sources(); len(sources) != 1 || sources[0].Path != canonicalFacadePath(t, selectedPath) {
|
||||
t.Fatalf("ordered search sources = %#v", sources)
|
||||
}
|
||||
|
||||
_, err = Load[facadeDiscoveryConfig](SearchConfig("", Paths(filepath.Join(root, "missing.yml")), SearchOptions{Required: true}))
|
||||
var loadErr *LoadError
|
||||
if !errors.As(err, &loadErr) {
|
||||
t.Fatalf("required exhausted search error = %v, want *LoadError", err)
|
||||
}
|
||||
foundMissing := false
|
||||
for _, diagnostic := range loadErr.Diagnostics() {
|
||||
foundMissing = foundMissing || diagnostic.Code == CodeSourceMissing
|
||||
}
|
||||
if !foundMissing {
|
||||
t.Fatalf("required exhausted search diagnostics = %#v", loadErr.Diagnostics())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFacadeSearchWorksForLayeredMainAndUnderlay(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
root := t.TempDir()
|
||||
mainPath := writeFacadeImportFile(t, root, "main.yml", "settings: {main: value}\n")
|
||||
underlayPath := writeFacadeImportFile(t, root, "underlay/base.yml", "settings: {underlay: value}\n")
|
||||
result, err := LoadLayered(
|
||||
Input{Source: SearchConfig("", Paths(mainPath))},
|
||||
LayeredOptions[facadeDiscoveryConfig]{Underlays: []Underlay{{Inputs: []Input{{
|
||||
Source: SearchConfig("", SearchDirectory(filepath.Dir(underlayPath), DirectorySearchOptions{
|
||||
Filenames: []FilenameGroup{CustomFilenames("base.yml")},
|
||||
})),
|
||||
}}}}},
|
||||
)
|
||||
if err != nil || !reflect.DeepEqual(result.Config.Settings, map[string]string{"main": "value", "underlay": "value"}) {
|
||||
t.Fatalf("layered search = %#v, %v", result, err)
|
||||
}
|
||||
sources := result.Sources()
|
||||
if len(sources) != 2 || sources[0].Path != canonicalFacadePath(t, mainPath) || sources[0].Layer != LayerBase ||
|
||||
sources[1].Path != canonicalFacadePath(t, underlayPath) || sources[1].Layer != LayerUnderlay {
|
||||
t.Fatalf("layered search sources = %#v", sources)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFacadeSearchSelectedAndAbsentUseRootOnlyContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
root := t.TempDir()
|
||||
mainPath := writeFacadeImportFile(t, root, "main.yml", "value: main\n")
|
||||
writeFacadeImportFile(t, root, "overlays/testing.yml", "value: overlay\n")
|
||||
filesystem := &FilesystemContext{Root: root}
|
||||
options := Options[facadeDiscoveryConfig]{Overlay: "testing", Filesystem: filesystem}
|
||||
|
||||
selected, err := Load(SearchConfig("", Paths(mainPath)), options)
|
||||
if err != nil || selected.Config.Value != "overlay" {
|
||||
t.Fatalf("selected search with root-only context = %#v, %v", selected, err)
|
||||
}
|
||||
sources := selected.Sources()
|
||||
if len(sources) != 2 || sources[0].Path != canonicalFacadePath(t, mainPath) || sources[0].ID != "source-1" ||
|
||||
sources[1].ID != "source-2" || sources[1].Layer != LayerOverlay {
|
||||
t.Fatalf("selected search source evidence = %#v", sources)
|
||||
}
|
||||
mainContext, ok := selected.SourceContext("source-1")
|
||||
if !ok || mainContext.BaseDir != "." {
|
||||
t.Fatalf("selected file base context = %#v, %v", mainContext, ok)
|
||||
}
|
||||
|
||||
missing := SearchConfig("", Paths(filepath.Join(root, "missing.yml")))
|
||||
searchedAbsent, err := Load(missing, options)
|
||||
if err != nil {
|
||||
t.Fatalf("exhausted search with root-only overlay context: %v", err)
|
||||
}
|
||||
explicitAbsent, err := Load(NoSource(), options)
|
||||
if err != nil {
|
||||
t.Fatalf("NoSource with root-only overlay context: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(searchedAbsent.Config, explicitAbsent.Config) ||
|
||||
!reflect.DeepEqual(searchedAbsent.Sources(), explicitAbsent.Sources()) {
|
||||
t.Fatalf("searched absence differs from NoSource: %#v / %#v", searchedAbsent, explicitAbsent)
|
||||
}
|
||||
left, leftOK := searchedAbsent.Metadata().Lookup(Keys("value"))
|
||||
right, rightOK := explicitAbsent.Metadata().Lookup(Keys("value"))
|
||||
if leftOK != rightOK || !reflect.DeepEqual(left, right) || !leftOK {
|
||||
t.Fatalf("searched absence metadata = %#v, %v; NoSource metadata = %#v, %v", left, leftOK, right, rightOK)
|
||||
}
|
||||
if sources := searchedAbsent.Sources(); len(sources) != 1 || sources[0].ID != "source-1" || sources[0].Layer != LayerOverlay {
|
||||
t.Fatalf("absent search created unexpected source evidence = %#v", sources)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFacadeSearchRejectsSelectedContextWithoutFallback(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
root := t.TempDir()
|
||||
inside := writeFacadeImportFile(t, root, "inside.yml", "value: fallback\n")
|
||||
outsideRoot := t.TempDir()
|
||||
outside := writeFacadeImportFile(t, outsideRoot, "outside.yml", "value: selected\n")
|
||||
search := SearchConfig("", Paths(outside, inside))
|
||||
|
||||
_, err := Load(search, Options[facadeDiscoveryConfig]{Filesystem: &FilesystemContext{Root: root}})
|
||||
var loadErr *LoadError
|
||||
if !errors.As(err, &loadErr) || !hasFacadeDiagnostic(loadErr, CodeImportOutsideRoot) {
|
||||
t.Fatalf("selected outside-root file error = %v, want containment failure", err)
|
||||
}
|
||||
|
||||
_, err = Load(search, Options[facadeDiscoveryConfig]{Filesystem: &FilesystemContext{BaseDir: root, Root: root}})
|
||||
if !errors.As(err, &loadErr) || !hasFacadeDiagnostic(loadErr, CodeOptionsInvalid) {
|
||||
t.Fatalf("selected file with explicit base error = %v, want option failure", err)
|
||||
}
|
||||
|
||||
native := NewFilesystem("native", fstest.MapFS{})
|
||||
_, err = Load(search, Options[facadeDiscoveryConfig]{Filesystem: &FilesystemContext{FS: &native, BaseDir: ".", Root: "."}})
|
||||
if !errors.As(err, &loadErr) || !hasFacadeDiagnostic(loadErr, CodeOptionsInvalid) {
|
||||
t.Fatalf("selected file with native context error = %v, want option failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func hasFacadeDiagnostic(err *LoadError, code Code) bool {
|
||||
for _, diagnostic := range err.Diagnostics() {
|
||||
if diagnostic.Code == code {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func canonicalFacadePath(t *testing.T, path string) string {
|
||||
t.Helper()
|
||||
absolute, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
canonical, err := filepath.EvalSymlinks(absolute)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return canonical
|
||||
}
|
||||
Reference in New Issue
Block a user