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