198 lines
8.9 KiB
Go
198 lines
8.9 KiB
Go
package configkit_test
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
configkit "go.maximumdirect.net/configkit"
|
|
)
|
|
|
|
func TestMainRelativeDiscoveryAndEvidence(t *testing.T) {
|
|
root := t.TempDir()
|
|
first, second := filepath.Join(root, "first"), filepath.Join(root, "second")
|
|
for _, dir := range []string{first, second} {
|
|
if err := os.MkdirAll(dir, 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "main.yml"), []byte("count: 1\n"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "fragment.yml"), []byte("settings: {selected: "+filepath.Base(dir)+"}\n"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
binding := configkit.MainRelative()
|
|
underlay := configkit.Input{Source: configkit.Bytes("declaration", []byte("config: {imports: [{path: fragment.yml}]}\n")), Imports: true, MainRelative: binding}
|
|
for _, test := range []struct {
|
|
name string
|
|
paths []string
|
|
want string
|
|
}{
|
|
{"first", []string{filepath.Join(first, "main.yml"), filepath.Join(second, "main.yml")}, "first"},
|
|
{"later", []string{filepath.Join(root, "missing.yml"), filepath.Join(second, "main.yml")}, "second"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
var callbacks int
|
|
result, err := configkit.LoadLayered(configkit.Input{Source: configkit.SearchConfig("", configkit.SearchPaths(test.paths...))}, configkit.LayeredOptions[layeredFacadeConfig]{Underlays: []configkit.Underlay{{Inputs: []configkit.Input{underlay}}}, Definition: configkit.Definition[layeredFacadeConfig]{Validate: func(*layeredFacadeConfig, *configkit.Validation) { callbacks++ }}})
|
|
if err != nil {
|
|
t.Fatalf("%v: %#v", err, err.(*configkit.LoadError).Diagnostics())
|
|
}
|
|
if result.Config.Settings["selected"] != test.want || callbacks != 1 {
|
|
t.Fatalf("config = %#v, callbacks = %d", result.Config, callbacks)
|
|
}
|
|
sources := result.Sources()
|
|
if len(sources) != 3 || sources[0].Layer != configkit.LayerMain || sources[1].Layer != configkit.LayerUnderlay || sources[2].Layer != configkit.LayerUnderlay || sources[2].UnderlayIndex != 1 || filepath.Base(sources[2].Path) != "fragment.yml" || sources[1].Path != "" {
|
|
t.Fatalf("sources = %#v", sources)
|
|
}
|
|
if _, ok := result.SourceContext(sources[1].ID); !ok {
|
|
t.Fatal("bound synthetic source lacks context")
|
|
}
|
|
if _, ok := result.SourceContext(sources[2].ID); !ok {
|
|
t.Fatal("import source lacks context")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMainRelativeSyntheticMainContexts(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(root, "fragment.yml"), []byte("settings: {selected: synthetic}\n"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx := &configkit.FilesystemContext{Root: root, BaseDir: root}
|
|
mainSources := []configkit.Source{configkit.Bytes("main bytes", []byte("count: 1\n")), configkit.Reader("main reader", strings.NewReader("count: 1\n")), configkit.Mapping("main mapping", map[string]any{"count": 1})}
|
|
for _, source := range mainSources {
|
|
input := configkit.Input{Source: configkit.Bytes("underlay", []byte("config: {imports: [{path: fragment.yml}]}\n")), Imports: true, MainRelative: configkit.MainRelative()}
|
|
result, err := configkit.LoadLayered(configkit.Input{Source: source, Filesystem: ctx}, configkit.LayeredOptions[layeredFacadeConfig]{Underlays: []configkit.Underlay{{Inputs: []configkit.Input{input}}}})
|
|
if err != nil {
|
|
t.Fatalf("%v: %#v", err, err.(*configkit.LoadError).Diagnostics())
|
|
}
|
|
if result.Config.Settings["selected"] != "synthetic" {
|
|
t.Fatalf("config = %#v", result.Config)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMainRelativeSelectedMainFailureIsTerminal(t *testing.T) {
|
|
root := t.TempDir()
|
|
bad, good := filepath.Join(root, "bad.yml"), filepath.Join(root, "good.yml")
|
|
if err := os.WriteFile(bad, []byte("count: [\n"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(good, []byte("count: 1\n"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
called := false
|
|
input := configkit.Input{Source: configkit.Bytes("underlay", []byte("count: 3\n")), MainRelative: &configkit.MainRelativeContext{Fallback: &configkit.FilesystemContext{Root: root, BaseDir: root}}}
|
|
_, err := configkit.LoadLayered(configkit.Input{Source: configkit.SearchConfig("", configkit.SearchPaths(bad, good))}, configkit.LayeredOptions[layeredFacadeConfig]{Underlays: []configkit.Underlay{{Inputs: []configkit.Input{input}}}, Definition: configkit.Definition[layeredFacadeConfig]{Validate: func(*layeredFacadeConfig, *configkit.Validation) { called = true }}})
|
|
if err == nil || called {
|
|
t.Fatalf("error = %v, callback = %v", err, called)
|
|
}
|
|
}
|
|
|
|
func TestMainRelativeSourceVersionEvidence(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(root, "main.yml"), []byte("version: 1\ncount: 1\n"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(root, "fragment.yml"), []byte("version: 2\nsettings: {loaded: fragment}\n"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
input := configkit.Input{Source: configkit.Bytes("companion", []byte("version: 3\nconfig: {imports: [{path: fragment.yml}]}\n")), Imports: true, MainRelative: configkit.MainRelative()}
|
|
type versioned struct {
|
|
Version int `yaml:"version"`
|
|
Count int `yaml:"count"`
|
|
Settings map[string]string `yaml:"settings"`
|
|
}
|
|
definition := configkit.Definition[versioned]{Version: &configkit.VersionRule{Path: configkit.Keys("version"), Check: func(configkit.Version, *configkit.Validation) error { return nil }}}
|
|
result, err := configkit.LoadLayered(configkit.Input{Source: configkit.File(filepath.Join(root, "main.yml"))}, configkit.LayeredOptions[versioned]{Definition: definition, SourceVersionMode: configkit.SourceVersionModeRequired, Underlays: []configkit.Underlay{{Inputs: []configkit.Input{input}}}})
|
|
if err != nil {
|
|
t.Fatalf("%v: %#v", err, err.(*configkit.LoadError).Diagnostics())
|
|
}
|
|
sources := result.Sources()
|
|
if len(sources) != 3 {
|
|
t.Fatalf("sources = %#v", sources)
|
|
}
|
|
for i, want := range []string{"1", "3", "2"} {
|
|
evidence := sources[i].VersionEvidence
|
|
if evidence == nil || evidence.Version.Text != want || evidence.Location == nil || evidence.Location.SourceID != sources[i].ID {
|
|
t.Fatalf("version evidence = %#v", sources)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMainRelativeOptionalSearchExhaustion(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := os.Mkdir(filepath.Join(root, "overlays"), 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for name, content := range map[string]string{
|
|
"fragment.yml": "settings: {companion: loaded}\n",
|
|
"overlays/live.yml": "count: 9\n",
|
|
} {
|
|
if err := os.WriteFile(filepath.Join(root, name), []byte(content), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
search := configkit.SearchConfig("", configkit.SearchPaths(filepath.Join(root, "absent.yml")))
|
|
missing := filepath.Join(root, "missing-fallback")
|
|
fallback := &configkit.FilesystemContext{Root: missing, BaseDir: missing}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
mainContext *configkit.FilesystemContext
|
|
bindingFallback *configkit.FilesystemContext
|
|
overlayFallback *configkit.FilesystemContext
|
|
overlay string
|
|
wantCount int
|
|
wantSources int
|
|
}{
|
|
{name: "skip without context", wantCount: 3, wantSources: 1},
|
|
{
|
|
name: "root-only main context wins", mainContext: &configkit.FilesystemContext{Root: root},
|
|
bindingFallback: fallback, overlayFallback: fallback, overlay: "live", wantCount: 9, wantSources: 3,
|
|
},
|
|
{name: "selected missing companion fallback", bindingFallback: fallback},
|
|
{name: "selected missing overlay fallback", overlay: "live", overlayFallback: fallback},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
underlay := configkit.Input{
|
|
Source: configkit.Bytes("companion", []byte("count: 3\nconfig: {imports: [{path: fragment.yml}]}\n")),
|
|
Imports: true, MainRelative: &configkit.MainRelativeContext{Fallback: tc.bindingFallback},
|
|
}
|
|
result, err := configkit.LoadLayered(
|
|
configkit.Input{Source: search, Filesystem: tc.mainContext},
|
|
configkit.LayeredOptions[layeredFacadeConfig]{
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{underlay}}},
|
|
Overlay: tc.overlay, OverlayFallback: tc.overlayFallback,
|
|
},
|
|
)
|
|
if tc.wantSources == 0 {
|
|
if err == nil {
|
|
t.Fatal("missing selected fallback succeeded")
|
|
}
|
|
diagnostics := err.(*configkit.LoadError).Diagnostics()
|
|
if len(diagnostics) != 1 || diagnostics[0].Code != configkit.CodeImportInvalid {
|
|
t.Fatalf("diagnostics = %#v", diagnostics)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sources := result.Sources()
|
|
if result.Config.Count != tc.wantCount || len(sources) != tc.wantSources {
|
|
t.Fatalf("config = %#v, sources = %#v", result.Config, sources)
|
|
}
|
|
if sources[0].Layer != configkit.LayerUnderlay || len(sources[0].EmptyImportSelections) != 0 {
|
|
t.Fatalf("declaring source = %#v", sources[0])
|
|
}
|
|
if tc.mainContext != nil && (result.Config.Settings["companion"] != "loaded" || sources[2].Layer != configkit.LayerOverlay) {
|
|
t.Fatalf("config = %#v, sources = %#v", result.Config, sources)
|
|
}
|
|
})
|
|
}
|
|
}
|