268 lines
11 KiB
Go
268 lines
11 KiB
Go
package engine
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"runtime"
|
|
"testing"
|
|
)
|
|
|
|
type overlayTestConfig 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 TestLoadAppliesSelectedOverlayAfterBaseAndBeforeDefaults(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")
|
|
overlayPath := writeImportFile(t, directory, "overlays/testing.yml", `settings:
|
|
changed: overlay
|
|
values: [overlay]
|
|
nullable: null
|
|
value: scalar
|
|
`)
|
|
|
|
result, err := testLoad[overlayTestConfig](File(mainPath), Options[overlayTestConfig]{
|
|
Imports: true,
|
|
Overlay: "testing",
|
|
Definition: Definition[overlayTestConfig]{Defaults: func() overlayTestConfig {
|
|
defaultValue := "default"
|
|
return overlayTestConfig{
|
|
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 != "overlay" || result.Config.Settings.Added != "imported" {
|
|
t.Fatalf("settings = %#v", result.Config.Settings)
|
|
}
|
|
if !reflect.DeepEqual(result.Config.Values, []string{"overlay"}) || result.Config.Nullable != nil || result.Config.Value != "scalar" {
|
|
t.Fatalf("overlay 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, overlayPath)}
|
|
gotPaths := make([]string, len(sources))
|
|
for index, source := range sources {
|
|
gotPaths[index] = source.Path
|
|
}
|
|
if !reflect.DeepEqual(gotPaths, wantPaths) || sources[2].Layer != LayerOverlay {
|
|
t.Fatalf("sources = %#v", sources)
|
|
}
|
|
changed, _ := result.Metadata().Lookup(Keys("settings", "changed"))
|
|
kept, _ := result.Metadata().Lookup(Keys("settings", "kept"))
|
|
if changed.Origin.Layer != LayerOverlay || 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 != LayerOverlay {
|
|
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 TestLoadOverlaySelectionUsesOneFixedYAMLFile(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", "settings: {kept: base}\n")
|
|
writeImportFile(t, directory, "overlays/testing.yml", "settings: {changed: selected}\n")
|
|
writeImportFile(t, directory, "overlays/default.yml", "settings: {changed: forbidden}\n")
|
|
|
|
for _, name := range []string{"", "default"} {
|
|
result, err := testLoad[overlayTestConfig](File(mainPath), Options[overlayTestConfig]{Overlay: name})
|
|
if err != nil || result.Config.Settings.Changed != "" || len(result.Sources()) != 1 {
|
|
t.Fatalf("inactive overlay %q = %#v, %v", name, result, err)
|
|
}
|
|
}
|
|
selected, err := testLoad[overlayTestConfig](File(mainPath), Options[overlayTestConfig]{Overlay: "testing"})
|
|
if err != nil || selected.Config.Settings.Changed != "selected" || len(selected.Sources()) != 2 {
|
|
t.Fatalf("selected overlay = %#v, %v", selected, err)
|
|
}
|
|
|
|
for _, name := range []string{"../testing", "testing.yml", "one,two", "with space", "é", "bad\x00name"} {
|
|
t.Run(name, func(t *testing.T) {
|
|
requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Overlay: name}, CodeOptionsInvalid)
|
|
})
|
|
}
|
|
requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Overlay: "missing"}, CodeOverlayMissing)
|
|
writeImportFile(t, directory, "overlays/fallback.yaml", "settings: {changed: fallback}\n")
|
|
requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Overlay: "fallback"}, CodeOverlayMissing)
|
|
}
|
|
|
|
func TestLoadOverlayRejectsDirectivesVersionsAndBaseAliases(t *testing.T) {
|
|
versionRule := &VersionRule{Path: Keys("schema", "version"), Check: func(version Version, _ *Validation) error {
|
|
if !version.Present || version.Text != "4" {
|
|
return errors.New("unexpected version")
|
|
}
|
|
return nil
|
|
}}
|
|
for _, test := range []struct {
|
|
name string
|
|
overlay string
|
|
code Code
|
|
}{
|
|
{name: "root directive", overlay: "configkit: {}\n", code: CodeOverlayDirective},
|
|
{name: "nested directive", overlay: "settings: {configkit: {}}\n", code: CodeOverlayDirective},
|
|
{name: "equal integer version", overlay: "schema: {version: 4}\n", code: CodeOverlayVersion},
|
|
{name: "ancestor scalar", overlay: "schema: null\n", code: CodeOverlayVersion},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", "schema: {version: 4}\n")
|
|
writeImportFile(t, directory, "overlays/testing.yml", test.overlay)
|
|
requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Overlay: "testing", Definition: Definition[overlayTestConfig]{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: 4}\n")
|
|
writeImportFile(t, directory, "overlays/testing.yml", "schema: {other: allowed}\n")
|
|
result, err := testLoad[overlayTestConfig](File(mainPath), Options[overlayTestConfig]{Overlay: "testing", Definition: Definition[overlayTestConfig]{Version: versionRule}})
|
|
if err != nil || result.Config.Schema["other"] != "allowed" {
|
|
t.Fatalf("mapping sibling = %#v, %v", result, err)
|
|
}
|
|
})
|
|
|
|
t.Run("overlay aliases imported base", func(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: overlays/testing.yml}]}\n")
|
|
writeImportFile(t, directory, "overlays/testing.yml", "settings: {changed: value}\n")
|
|
requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Imports: true, Overlay: "testing"}, CodeOverlayRepeated)
|
|
})
|
|
}
|
|
|
|
func TestLoadOverlayKeepsBaseFailuresAndFilesystemContainment(t *testing.T) {
|
|
t.Run("base parse precedes overlay", func(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", "settings: [\n")
|
|
writeImportFile(t, directory, "overlays/testing.yml", "settings: {changed: overlay}\n")
|
|
requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Overlay: "testing"}, CodeYAMLInvalid)
|
|
})
|
|
|
|
t.Run("overlay parse", func(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", "{}\n")
|
|
writeImportFile(t, directory, "overlays/testing.yml", "settings: [\n")
|
|
loadError := requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Overlay: "testing"}, CodeYAMLInvalid)
|
|
if loadError.Diagnostics()[0].Primary.SourceID != "source-2" || len(loadError.Sources()) != 2 {
|
|
t.Fatalf("overlay 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()
|
|
outsideOverlay := writeImportFile(t, outside, "testing.yml", "settings: {changed: outside}\n")
|
|
if err := os.Mkdir(filepath.Join(root, "overlays"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(outsideOverlay, filepath.Join(root, "overlays/testing.yml")); err != nil {
|
|
t.Skipf("symlinks unavailable: %v", err)
|
|
}
|
|
mainPath := writeImportFile(t, root, "main.yml", "{}\n")
|
|
requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Overlay: "testing"}, CodeOverlayOutsideRoot)
|
|
})
|
|
}
|
|
|
|
t.Run("byte source context", func(t *testing.T) {
|
|
directory := t.TempDir()
|
|
writeImportFile(t, directory, "overlays/testing.yml", "settings: {changed: selected}\n")
|
|
result, err := testLoad[overlayTestConfig](Bytes("s3://bucket/main.yml", []byte("{}\n")), Options[overlayTestConfig]{
|
|
Overlay: "testing", Filesystem: &FilesystemContext{BaseDir: directory, Root: directory},
|
|
})
|
|
if err != nil || result.Config.Settings.Changed != "selected" {
|
|
t.Fatalf("byte overlay = %#v, %v", result, err)
|
|
}
|
|
requireOverlayLoadCode(t, Bytes("memory.yml", []byte("{}\n")), Options[overlayTestConfig]{Overlay: "testing"}, CodeOptionsInvalid)
|
|
})
|
|
}
|
|
|
|
func TestLoadOverlayCountsAgainstAggregateBudgets(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, overlayBytes int) importLimits {
|
|
return importLimits{sources: 2, bytes: main.bytes + int64(overlayBytes) - 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")
|
|
overlayData := "settings: {changed: selected}\n"
|
|
writeImportFile(t, directory, "overlays/testing.yml", overlayData)
|
|
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 = loadSelectedOverlayDocument(File(mainPath), nil, main, base, sourceInfos(main.source), "testing", nil, test.limits(main, len(overlayData)))
|
|
if len(diagnostics) == 0 || diagnostics[0].Code != CodeOverlayLimitExceeded {
|
|
t.Fatalf("diagnostics = %#v", diagnostics)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func requireOverlayLoadCode(t *testing.T, source Source, options Options[overlayTestConfig], code Code) *LoadError {
|
|
t.Helper()
|
|
result, err := testLoad[overlayTestConfig](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
|
|
}
|