Unify host and native filesystem acquisition
This commit is contained in:
284
internal/engine/native_filesystem_test.go
Normal file
284
internal/engine/native_filesystem_test.go
Normal file
@@ -0,0 +1,284 @@
|
||||
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 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)
|
||||
}
|
||||
Reference in New Issue
Block a user