Add selected profile overlays
This commit is contained in:
267
profile_test.go
Normal file
267
profile_test.go
Normal file
@@ -0,0 +1,267 @@
|
||||
package configkit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type profileTestConfig struct {
|
||||
Settings struct {
|
||||
Kept string `yaml:"kept"`
|
||||
Changed string `yaml:"changed"`
|
||||
Added string `yaml:"added"`
|
||||
} `yaml:"settings"`
|
||||
Values []string `yaml:"values"`
|
||||
Nullable *string `yaml:"nullable"`
|
||||
Value any `yaml:"value"`
|
||||
Schema map[string]string `yaml:"schema"`
|
||||
}
|
||||
|
||||
func TestLoadAppliesSelectedProfileAfterBaseAndBeforeDefaults(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", `_configkit:
|
||||
imports: [{path: base.yml}]
|
||||
settings:
|
||||
kept: main
|
||||
changed: base
|
||||
values: [base, stale]
|
||||
nullable: base
|
||||
value: {child: base}
|
||||
`)
|
||||
basePath := writeImportFile(t, directory, "base.yml", "settings: {added: imported}\n")
|
||||
profilePath := writeImportFile(t, directory, "profiles/testing.yml", `settings:
|
||||
changed: profile
|
||||
values: [profile]
|
||||
nullable: null
|
||||
value: scalar
|
||||
`)
|
||||
|
||||
result, err := Load[profileTestConfig](File(mainPath), Options[profileTestConfig]{
|
||||
Imports: true,
|
||||
Profile: "testing",
|
||||
Definition: Definition[profileTestConfig]{Defaults: func() profileTestConfig {
|
||||
defaultValue := "default"
|
||||
return profileTestConfig{
|
||||
Values: []string{"default"}, Nullable: &defaultValue,
|
||||
Settings: struct {
|
||||
Kept string `yaml:"kept"`
|
||||
Changed string `yaml:"changed"`
|
||||
Added string `yaml:"added"`
|
||||
}{Kept: "default", Changed: "default", Added: "default"},
|
||||
}
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Config.Settings.Kept != "main" || result.Config.Settings.Changed != "profile" || result.Config.Settings.Added != "imported" {
|
||||
t.Fatalf("settings = %#v", result.Config.Settings)
|
||||
}
|
||||
if !reflect.DeepEqual(result.Config.Values, []string{"profile"}) || result.Config.Nullable != nil || result.Config.Value != "scalar" {
|
||||
t.Fatalf("profile values = values %#v nullable %#v value %#v", result.Config.Values, result.Config.Nullable, result.Config.Value)
|
||||
}
|
||||
sources := result.Sources()
|
||||
wantPaths := []string{canonicalTestPath(t, mainPath), canonicalTestPath(t, basePath), canonicalTestPath(t, profilePath)}
|
||||
gotPaths := make([]string, len(sources))
|
||||
for index, source := range sources {
|
||||
gotPaths[index] = source.Path
|
||||
}
|
||||
if !reflect.DeepEqual(gotPaths, wantPaths) || sources[2].Layer != LayerProfile {
|
||||
t.Fatalf("sources = %#v", sources)
|
||||
}
|
||||
changed, _ := result.Metadata().Lookup(Keys("settings", "changed"))
|
||||
kept, _ := result.Metadata().Lookup(Keys("settings", "kept"))
|
||||
if changed.Origin.Layer != LayerProfile || changed.Origin.Locations[0].SourceID != "source-3" || kept.Origin.Layer != LayerBase {
|
||||
t.Fatalf("origins = changed %#v kept %#v", changed, kept)
|
||||
}
|
||||
if _, ok := result.Metadata().Lookup(Keys("value", "child")); ok {
|
||||
t.Fatal("removed child remains effective")
|
||||
}
|
||||
childHistory := result.Metadata().History(Keys("value", "child"))
|
||||
if len(childHistory) < 2 || childHistory[len(childHistory)-1].Path.String() != "/value" || childHistory[len(childHistory)-1].Origin.Layer != LayerProfile {
|
||||
t.Fatalf("removed child history = %#v", childHistory)
|
||||
}
|
||||
if _, ok := result.Metadata().Lookup(Keys("values").Index(1)); ok {
|
||||
t.Fatal("removed sequence index remains effective")
|
||||
}
|
||||
indexHistory := result.Metadata().History(Keys("values").Index(1))
|
||||
if len(indexHistory) < 2 || indexHistory[len(indexHistory)-1].Path.String() != "/values" {
|
||||
t.Fatalf("removed sequence history = %#v", indexHistory)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadProfileSelectionUsesOneFixedYAMLFile(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "settings: {kept: base}\n")
|
||||
writeImportFile(t, directory, "profiles/testing.yml", "settings: {changed: selected}\n")
|
||||
writeImportFile(t, directory, "profiles/default.yml", "settings: {changed: forbidden}\n")
|
||||
|
||||
for _, name := range []string{"", "default"} {
|
||||
result, err := Load[profileTestConfig](File(mainPath), Options[profileTestConfig]{Profile: name})
|
||||
if err != nil || result.Config.Settings.Changed != "" || len(result.Sources()) != 1 {
|
||||
t.Fatalf("inactive profile %q = %#v, %v", name, result, err)
|
||||
}
|
||||
}
|
||||
selected, err := Load[profileTestConfig](File(mainPath), Options[profileTestConfig]{Profile: "testing"})
|
||||
if err != nil || selected.Config.Settings.Changed != "selected" || len(selected.Sources()) != 2 {
|
||||
t.Fatalf("selected profile = %#v, %v", selected, err)
|
||||
}
|
||||
|
||||
for _, name := range []string{"../testing", "testing.yml", "one,two", "with space", "é", "bad\x00name"} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
requireProfileLoadCode(t, File(mainPath), Options[profileTestConfig]{Profile: name}, CodeOptionsInvalid)
|
||||
})
|
||||
}
|
||||
requireProfileLoadCode(t, File(mainPath), Options[profileTestConfig]{Profile: "missing"}, CodeProfileMissing)
|
||||
writeImportFile(t, directory, "profiles/fallback.yaml", "settings: {changed: fallback}\n")
|
||||
requireProfileLoadCode(t, File(mainPath), Options[profileTestConfig]{Profile: "fallback"}, CodeProfileMissing)
|
||||
}
|
||||
|
||||
func TestLoadProfileRejectsDirectivesVersionsAndBaseAliases(t *testing.T) {
|
||||
versionRule := &VersionRule{Path: Keys("schema", "version"), Check: func(version Version, _ *Validation) error {
|
||||
if !version.Present || version.Text != "v1" {
|
||||
return errors.New("unexpected version")
|
||||
}
|
||||
return nil
|
||||
}}
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
profile string
|
||||
code Code
|
||||
}{
|
||||
{name: "root directive", profile: "_configkit: {}\n", code: CodeProfileDirective},
|
||||
{name: "nested directive", profile: "settings: {_configkit: {}}\n", code: CodeProfileDirective},
|
||||
{name: "equal version", profile: "schema: {version: v1}\n", code: CodeProfileVersion},
|
||||
{name: "ancestor scalar", profile: "schema: null\n", code: CodeProfileVersion},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "schema: {version: v1}\n")
|
||||
writeImportFile(t, directory, "profiles/testing.yml", test.profile)
|
||||
requireProfileLoadCode(t, File(mainPath), Options[profileTestConfig]{Profile: "testing", Definition: Definition[profileTestConfig]{Version: versionRule}}, test.code)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("mapping sibling is allowed", func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "schema: {version: v1}\n")
|
||||
writeImportFile(t, directory, "profiles/testing.yml", "schema: {other: allowed}\n")
|
||||
result, err := Load[profileTestConfig](File(mainPath), Options[profileTestConfig]{Profile: "testing", Definition: Definition[profileTestConfig]{Version: versionRule}})
|
||||
if err != nil || result.Config.Schema["other"] != "allowed" {
|
||||
t.Fatalf("mapping sibling = %#v, %v", result, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("profile aliases imported base", func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: profiles/testing.yml}]}\n")
|
||||
writeImportFile(t, directory, "profiles/testing.yml", "settings: {changed: value}\n")
|
||||
requireProfileLoadCode(t, File(mainPath), Options[profileTestConfig]{Imports: true, Profile: "testing"}, CodeProfileRepeated)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadProfileKeepsBaseFailuresAndFilesystemContainment(t *testing.T) {
|
||||
t.Run("base parse precedes profile", func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "settings: [\n")
|
||||
writeImportFile(t, directory, "profiles/testing.yml", "settings: {changed: profile}\n")
|
||||
requireProfileLoadCode(t, File(mainPath), Options[profileTestConfig]{Profile: "testing"}, CodeYAMLInvalid)
|
||||
})
|
||||
|
||||
t.Run("profile parse", func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "{}\n")
|
||||
writeImportFile(t, directory, "profiles/testing.yml", "settings: [\n")
|
||||
loadError := requireProfileLoadCode(t, File(mainPath), Options[profileTestConfig]{Profile: "testing"}, CodeYAMLInvalid)
|
||||
if loadError.Diagnostics()[0].Primary.SourceID != "source-2" || len(loadError.Sources()) != 2 {
|
||||
t.Fatalf("profile parse attribution = %#v / %#v", loadError.Diagnostics(), loadError.Sources())
|
||||
}
|
||||
})
|
||||
|
||||
if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
|
||||
t.Run("symlink escape", func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
outside := t.TempDir()
|
||||
outsideProfile := writeImportFile(t, outside, "testing.yml", "settings: {changed: outside}\n")
|
||||
if err := os.Mkdir(filepath.Join(root, "profiles"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink(outsideProfile, filepath.Join(root, "profiles/testing.yml")); err != nil {
|
||||
t.Skipf("symlinks unavailable: %v", err)
|
||||
}
|
||||
mainPath := writeImportFile(t, root, "main.yml", "{}\n")
|
||||
requireProfileLoadCode(t, File(mainPath), Options[profileTestConfig]{Profile: "testing"}, CodeProfileOutsideRoot)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("byte source context", func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
writeImportFile(t, directory, "profiles/testing.yml", "settings: {changed: selected}\n")
|
||||
result, err := Load[profileTestConfig](Bytes("s3://bucket/main.yml", []byte("{}\n")), Options[profileTestConfig]{
|
||||
Profile: "testing", Filesystem: &FilesystemContext{BaseDir: directory, Root: directory},
|
||||
})
|
||||
if err != nil || result.Config.Settings.Changed != "selected" {
|
||||
t.Fatalf("byte profile = %#v, %v", result, err)
|
||||
}
|
||||
requireProfileLoadCode(t, Bytes("memory.yml", []byte("{}\n")), Options[profileTestConfig]{Profile: "testing"}, CodeOptionsInvalid)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadProfileCountsAgainstAggregateBudgets(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
limits func(parsedSource, int) importLimits
|
||||
}{
|
||||
{name: "source count", limits: func(_ parsedSource, _ int) importLimits {
|
||||
return importLimits{sources: 1, bytes: maxTotalBytes, nodes: maxDocumentNodes}
|
||||
}},
|
||||
{name: "aggregate bytes", limits: func(main parsedSource, profileBytes int) importLimits {
|
||||
return importLimits{sources: 2, bytes: main.bytes + int64(profileBytes) - 1, nodes: maxDocumentNodes}
|
||||
}},
|
||||
{name: "aggregate nodes", limits: func(main parsedSource, _ int) importLimits {
|
||||
return importLimits{sources: 2, bytes: maxTotalBytes, nodes: countInputNodes(main.document) + 1}
|
||||
}},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "{}\n")
|
||||
profileData := "settings: {changed: selected}\n"
|
||||
writeImportFile(t, directory, "profiles/testing.yml", profileData)
|
||||
main, diagnostics := parseSource(File(mainPath), "source-1", defaultParseLimits())
|
||||
if len(diagnostics) != 0 {
|
||||
t.Fatal(diagnostics)
|
||||
}
|
||||
base, diagnostics := composeDocuments(main.document)
|
||||
if len(diagnostics) != 0 {
|
||||
t.Fatal(diagnostics)
|
||||
}
|
||||
base.inputBytes = main.bytes
|
||||
base.inputNodes = countInputNodes(main.document)
|
||||
_, _, diagnostics = loadProfileDocument(File(mainPath), nil, main, base, sourceInfos(main.source), "testing", nil, test.limits(main, len(profileData)))
|
||||
if len(diagnostics) == 0 || diagnostics[0].Code != CodeProfileLimitExceeded {
|
||||
t.Fatalf("diagnostics = %#v", diagnostics)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func requireProfileLoadCode(t *testing.T, source Source, options Options[profileTestConfig], code Code) *LoadError {
|
||||
t.Helper()
|
||||
result, err := Load[profileTestConfig](source, options)
|
||||
if result != nil || err == nil {
|
||||
t.Fatalf("result, error = %#v, %v", result, err)
|
||||
}
|
||||
var loadError *LoadError
|
||||
if !errors.As(err, &loadError) {
|
||||
t.Fatalf("error type = %T", err)
|
||||
}
|
||||
diagnostics := loadError.Diagnostics()
|
||||
if len(diagnostics) == 0 || diagnostics[0].Code != code {
|
||||
t.Fatalf("diagnostics = %#v, want %s", diagnostics, code)
|
||||
}
|
||||
return loadError
|
||||
}
|
||||
Reference in New Issue
Block a user