313 lines
12 KiB
Go
313 lines
12 KiB
Go
package engine
|
|
|
|
import (
|
|
"errors"
|
|
"io/fs"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
)
|
|
|
|
func TestNativeFilesystemImportsAndOverlay(t *testing.T) {
|
|
filesystem := fstest.MapFS{
|
|
"config/main.yml": &fstest.MapFile{Data: []byte(`configkit:
|
|
imports:
|
|
- path: ../shared.yml
|
|
- glob: fragments/*.yml
|
|
items: {main: main}
|
|
`)},
|
|
"shared.yml": &fstest.MapFile{Data: []byte("items: {shared: shared}\n")},
|
|
"config/fragments/b.yml": &fstest.MapFile{Data: []byte("items: {beta: beta}\n")},
|
|
"config/fragments/a.yml": &fstest.MapFile{Data: []byte("items: {alpha: alpha}\n")},
|
|
"overlays/testing.yml": &fstest.MapFile{Data: []byte("items: {main: overlaid}\n")},
|
|
}
|
|
namespace := newNativeNamespace("bundle", filesystem)
|
|
result, err := testLoad[importTestConfig](filesystemFile(namespace, "config/main.yml"), Options[importTestConfig]{Imports: true, Overlay: "testing"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := map[string]string{"main": "overlaid", "shared": "shared", "alpha": "alpha", "beta": "beta"}
|
|
if !reflect.DeepEqual(result.Config.Items, want) {
|
|
t.Fatalf("items = %#v, want %#v", result.Config.Items, want)
|
|
}
|
|
sources := result.Sources()
|
|
wantPaths := []string{"config/main.yml", "shared.yml", "config/fragments/a.yml", "config/fragments/b.yml", "overlays/testing.yml"}
|
|
gotPaths := make([]string, len(sources))
|
|
for index, source := range sources {
|
|
gotPaths[index] = source.Path
|
|
}
|
|
if !reflect.DeepEqual(gotPaths, wantPaths) {
|
|
t.Fatalf("source paths = %#v, want %#v", gotPaths, wantPaths)
|
|
}
|
|
}
|
|
|
|
func TestNativeFilesystemItemImportUsesSharedScopedAssembly(t *testing.T) {
|
|
t.Parallel()
|
|
type item struct {
|
|
ID string `yaml:"id"`
|
|
Name string `yaml:"name"`
|
|
}
|
|
type config struct {
|
|
Items []item `yaml:"items"`
|
|
}
|
|
filesystem := fstest.MapFS{
|
|
"main.yml": &fstest.MapFile{Data: []byte("configkit:\n imports:\n - path: records/item.yml\n destination: [items]\n placement: sequence-item\n")},
|
|
"records/item.yml": &fstest.MapFile{Data: []byte("configkit:\n imports:\n - path: helper.yml\nid: native\n")},
|
|
"records/helper.yml": &fstest.MapFile{Data: []byte("name: helper\n")},
|
|
}
|
|
namespace := newNativeNamespace("bundle", filesystem)
|
|
result, err := testLoad[config](filesystemFile(namespace, "main.yml"), Options[config]{Imports: true})
|
|
if err != nil {
|
|
t.Fatalf("native item import: %v", err)
|
|
}
|
|
if len(result.Config.Items) != 1 || result.Config.Items[0].ID != "native" || result.Config.Items[0].Name != "helper" {
|
|
t.Fatalf("native item = %#v", result.Config.Items)
|
|
}
|
|
sources := result.Sources()
|
|
if len(sources) != 3 || sources[1].Destination.String() != "/items" || sources[2].Destination.String() != "/items/0" || sources[1].Path != "records/item.yml" || sources[2].Path != "records/helper.yml" {
|
|
t.Fatalf("native item sources = %#v", sources)
|
|
}
|
|
}
|
|
|
|
func TestNativeFilesystemNamespaceAndPathFailures(t *testing.T) {
|
|
first := fstest.MapFS{"main.yml": &fstest.MapFile{Data: []byte("items: {name: first}\n")}}
|
|
second := fstest.MapFS{"main.yml": &fstest.MapFile{Data: []byte("items: {name: second}\n")}}
|
|
firstNamespace := newNativeNamespace("same", first)
|
|
secondNamespace := newNativeNamespace("same", second)
|
|
if reflect.TypeOf(first).Comparable() {
|
|
t.Fatal("test filesystem must have a non-comparable dynamic value")
|
|
}
|
|
if firstNamespace.token == secondNamespace.token {
|
|
t.Fatal("independent filesystem constructors shared identity")
|
|
}
|
|
if newNativeAccess(first, ".", firstNamespace.token).identity("main.yml") == newNativeAccess(second, ".", secondNamespace.token).identity("main.yml") {
|
|
t.Fatal("equal paths from separate filesystems have the same identity")
|
|
}
|
|
access := newNativeAccess(first, ".", firstNamespace.token)
|
|
if access.syntheticIdentity() == access.syntheticIdentity() || access.syntheticIdentity() == access.identity("<selected-source>") {
|
|
t.Fatal("independent synthetic sources shared a file identity")
|
|
}
|
|
for _, test := range []struct {
|
|
name string
|
|
namespace *nativeNamespace
|
|
want string
|
|
}{
|
|
{name: "first", namespace: firstNamespace, want: "first"},
|
|
{name: "second", namespace: secondNamespace, want: "second"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
result, err := testLoad[importTestConfig](filesystemFile(test.namespace, "main.yml"))
|
|
if err != nil || result.Config.Items["name"] != test.want {
|
|
t.Fatalf("result = %#v, %v", result, err)
|
|
}
|
|
})
|
|
}
|
|
for _, name := range []string{".", "../main.yml", "/main.yml", "missing.yml"} {
|
|
_, diagnostics := readSource(filesystemFile(firstNamespace, name), "source-1", defaultSourceLimits())
|
|
if len(diagnostics) != 1 {
|
|
t.Errorf("%q diagnostics = %#v", name, diagnostics)
|
|
}
|
|
}
|
|
missingImport := fstest.MapFS{"main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: host-only.yml}]}\n")}}
|
|
_, err := testLoad[importTestConfig](filesystemFile(newNativeNamespace("bundle", missingImport), "main.yml"), Options[importTestConfig]{Imports: true})
|
|
if err == nil {
|
|
t.Fatal("missing virtual import fell back to the host filesystem")
|
|
}
|
|
loadError, ok := err.(*LoadError)
|
|
if !ok || loadError.Diagnostics()[0].Code != CodeImportMissing {
|
|
t.Fatalf("missing import error = %#v", err)
|
|
}
|
|
}
|
|
|
|
func TestNativeFilesystemClosesFilesOnSuccessAndFailure(t *testing.T) {
|
|
for _, fallback := range []bool{false, true} {
|
|
t.Run(map[bool]string{false: "ReadDirFile", true: "ReadDir fallback"}[fallback], func(t *testing.T) {
|
|
filesystem := &observedFilesystem{files: fstest.MapFS{
|
|
"main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{glob: 'parts/*.yml'}]}\nitems: {main: main}\n")},
|
|
"parts/one.yml": &fstest.MapFile{Data: []byte("items: {one: one}\n")},
|
|
}, fallback: fallback}
|
|
result, err := testLoad[importTestConfig](filesystemFile(newNativeNamespace("bundle", filesystem), "main.yml"), Options[importTestConfig]{Imports: true})
|
|
if err != nil || result.Config.Items["one"] != "one" {
|
|
t.Fatalf("result = %#v, %v", result, err)
|
|
}
|
|
filesystem.assertClosed(t)
|
|
})
|
|
}
|
|
|
|
for _, failure := range []struct {
|
|
name string
|
|
file string
|
|
failRead string
|
|
failStat string
|
|
failDir string
|
|
}{
|
|
{name: "read", file: "items: {one: one}\n", failRead: "parts/one.yml"},
|
|
{name: "stat", file: "items: {one: one}\n", failStat: "parts/one.yml"},
|
|
{name: "parse", file: "items: [unterminated\n"},
|
|
{name: "directory", file: "items: {one: one}\n", failDir: "parts"},
|
|
} {
|
|
t.Run(failure.name, func(t *testing.T) {
|
|
filesystem := &observedFilesystem{files: fstest.MapFS{
|
|
"main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{glob: 'parts/*.yml'}]}\n")},
|
|
"parts/one.yml": &fstest.MapFile{Data: []byte(failure.file)},
|
|
}, failRead: failure.failRead, failStat: failure.failStat, failDir: failure.failDir}
|
|
_, err := testLoad[importTestConfig](filesystemFile(newNativeNamespace("bundle", filesystem), "main.yml"), Options[importTestConfig]{Imports: true})
|
|
if err == nil {
|
|
t.Fatal("expected a load failure")
|
|
}
|
|
if strings.Contains(err.Error(), "tracking failure canary") {
|
|
t.Fatal("unsafe filesystem error escaped")
|
|
}
|
|
filesystem.assertClosed(t)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNativeFilesystemRootAndOptionalImports(t *testing.T) {
|
|
filesystem := fstest.MapFS{
|
|
"config/sub/main.yml": &fstest.MapFile{Data: []byte(`configkit:
|
|
imports:
|
|
- path: ../shared.yml
|
|
- path: absent.yml
|
|
optional: true
|
|
- glob: absent/*.yml
|
|
optional: true
|
|
`)},
|
|
"config/shared.yml": &fstest.MapFile{Data: []byte("items: {shared: retained}\n")},
|
|
"outside.yml": &fstest.MapFile{Data: []byte("items: {outside: blocked}\n")},
|
|
}
|
|
namespace := newNativeNamespace("bundle", filesystem)
|
|
source := filesystemFile(namespace, "config/sub/main.yml")
|
|
parsed, diagnostics := parseSource(source, "source-1", defaultParseLimits())
|
|
if len(diagnostics) != 0 {
|
|
t.Fatal(diagnostics)
|
|
}
|
|
access := newNativeAccess(filesystem, "config", namespace.token)
|
|
composed, sources, diagnostics := loadImportedWithAccess(access, "config/sub", source.path, parsed, nil, defaultImportLimits(), &importAccounting{})
|
|
if len(diagnostics) != 0 || composed == nil || len(sources) != 2 || sources[1].Path != "config/shared.yml" {
|
|
t.Fatalf("contained imports = (%#v, %#v, %#v)", composed, sources, diagnostics)
|
|
}
|
|
|
|
outside := fstest.MapFS{
|
|
"config/sub/main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: ../../outside.yml}]}\n")},
|
|
"outside.yml": &fstest.MapFile{Data: []byte("items: {outside: blocked}\n")},
|
|
}
|
|
outsideNamespace := newNativeNamespace("bundle", outside)
|
|
parsed, diagnostics = parseSource(filesystemFile(outsideNamespace, "config/sub/main.yml"), "source-1", defaultParseLimits())
|
|
if len(diagnostics) != 0 {
|
|
t.Fatal(diagnostics)
|
|
}
|
|
_, _, diagnostics = loadImportedWithAccess(newNativeAccess(outside, "config", outsideNamespace.token), "config/sub", "config/sub/main.yml", parsed, nil, defaultImportLimits(), &importAccounting{})
|
|
if len(diagnostics) != 1 || diagnostics[0].Code != CodeImportOutsideRoot {
|
|
t.Fatalf("escaping import diagnostics = %#v", diagnostics)
|
|
}
|
|
}
|
|
|
|
func TestNativeFilesystemDetectsRepeatedAndCyclicImports(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
files fstest.MapFS
|
|
code Code
|
|
}{
|
|
{
|
|
name: "repeated",
|
|
files: fstest.MapFS{
|
|
"main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: child.yml}, {path: child.yml}]}\n")},
|
|
"child.yml": &fstest.MapFile{Data: []byte("items: {child: child}\n")},
|
|
},
|
|
code: CodeImportRepeated,
|
|
},
|
|
{
|
|
name: "cycle",
|
|
files: fstest.MapFS{
|
|
"main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: child.yml}]}\n")},
|
|
"child.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: main.yml}]}\n")},
|
|
},
|
|
code: CodeImportCycle,
|
|
},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
_, err := testLoad[importTestConfig](filesystemFile(newNativeNamespace("bundle", test.files), "main.yml"), Options[importTestConfig]{Imports: true})
|
|
loadError, ok := err.(*LoadError)
|
|
if !ok || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != test.code {
|
|
t.Fatalf("load error = %#v, want %q", err, test.code)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type observedFilesystem struct {
|
|
files fstest.MapFS
|
|
opened int
|
|
closed int
|
|
fallback bool
|
|
failRead string
|
|
failStat string
|
|
failDir string
|
|
}
|
|
|
|
func (f *observedFilesystem) Open(name string) (fs.File, error) {
|
|
inner, err := f.files.Open(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f.opened++
|
|
file := &observedFile{File: inner, owner: f, name: name}
|
|
if directory, ok := inner.(fs.ReadDirFile); ok && !f.fallback {
|
|
return &observedDirectoryFile{observedFile: file, directory: directory}, nil
|
|
}
|
|
return file, nil
|
|
}
|
|
|
|
func (f *observedFilesystem) ReadDir(name string) ([]fs.DirEntry, error) {
|
|
if name == f.failDir {
|
|
return nil, errors.New("tracking failure canary")
|
|
}
|
|
return fs.ReadDir(f.files, name)
|
|
}
|
|
|
|
func (f *observedFilesystem) assertClosed(t *testing.T) {
|
|
t.Helper()
|
|
if f.opened == 0 || f.opened != f.closed {
|
|
t.Fatalf("filesystem opens=%d closes=%d", f.opened, f.closed)
|
|
}
|
|
}
|
|
|
|
type observedFile struct {
|
|
fs.File
|
|
owner *observedFilesystem
|
|
name string
|
|
}
|
|
|
|
func (f *observedFile) Read(data []byte) (int, error) {
|
|
if f.name == f.owner.failRead {
|
|
return 0, errors.New("tracking failure canary")
|
|
}
|
|
return f.File.Read(data)
|
|
}
|
|
|
|
func (f *observedFile) Stat() (fs.FileInfo, error) {
|
|
if f.name == f.owner.failStat {
|
|
return nil, errors.New("tracking failure canary")
|
|
}
|
|
return f.File.Stat()
|
|
}
|
|
|
|
func (f *observedFile) Close() error {
|
|
f.owner.closed++
|
|
return f.File.Close()
|
|
}
|
|
|
|
type observedDirectoryFile struct {
|
|
*observedFile
|
|
directory fs.ReadDirFile
|
|
}
|
|
|
|
func (f *observedDirectoryFile) ReadDir(count int) ([]fs.DirEntry, error) {
|
|
if f.name == f.owner.failDir {
|
|
return nil, errors.New("tracking failure canary")
|
|
}
|
|
return f.directory.ReadDir(count)
|
|
}
|