Files
ConfigKit/internal/engine/native_filesystem_test.go

370 lines
14 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, required: true}]}\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
- glob: absent/*.yml
`)},
"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)
request := testImportRequest(source, nil, parsed, Path{}, nil, nil)
policy := &acquisitionPolicy{importLimits: defaultImportLimits()}
state := &acquisitionState{accounting: &importAccounting{}}
tier := &tierAcquisitionState{}
resolved := resolvedFilesystemContext{native: namespace, rootPath: "config", basePath: "config/sub", mainPath: source.path}
composed, sources, diagnostics := acquireImportedWithAccess(request, policy, state, tier, access, resolved)
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)
}
request = testImportRequest(filesystemFile(outsideNamespace, "config/sub/main.yml"), nil, parsed, Path{}, nil, nil)
policy = &acquisitionPolicy{importLimits: defaultImportLimits()}
state = &acquisitionState{accounting: &importAccounting{}}
tier = &tierAcquisitionState{}
resolved = resolvedFilesystemContext{native: outsideNamespace, rootPath: "config", basePath: "config/sub", mainPath: "config/sub/main.yml"}
_, _, diagnostics = acquireImportedWithAccess(request, policy, state, tier, newNativeAccess(outside, "config", outsideNamespace.token), resolved)
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
failOpen string
failRead string
failStat string
failDir string
failReadDir string
readDirErr error
failNoProgress string
failClose string
closeErr error
failFallbackDir string
fallbackErr error
fallbackBeforeClose bool
handles []*observedFile
}
func (f *observedFilesystem) Open(name string) (fs.File, error) {
if name == f.failOpen {
return nil, fs.ErrNotExist
}
inner, err := f.files.Open(name)
if err != nil {
return nil, err
}
f.opened++
file := &observedFile{File: inner, owner: f, name: name}
f.handles = append(f.handles, file)
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")
}
entries, err := fs.ReadDir(f.files, name)
if name == f.failFallbackDir {
for _, handle := range f.handles {
if handle.name == name && !handle.closed {
f.fallbackBeforeClose = true
}
}
return entries, f.fallbackErr
}
return entries, err
}
func (f *observedFilesystem) assertClosed(t *testing.T) {
t.Helper()
if f.opened != f.closed || len(f.handles) != f.opened {
t.Fatalf("filesystem opens=%d closes=%d handles=%d", f.opened, f.closed, len(f.handles))
}
for _, handle := range f.handles {
if !handle.closed || handle.closeCalls != 1 {
t.Errorf("filesystem handle %q closed=%t close calls=%d", handle.name, handle.closed, handle.closeCalls)
}
}
}
type observedFile struct {
fs.File
owner *observedFilesystem
name string
closed bool
closeCalls int
}
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++
f.closeCalls++
f.closed = true
err := f.File.Close()
if f.name == f.owner.failClose {
if f.owner.closeErr != nil {
return f.owner.closeErr
}
return fs.ErrNotExist
}
return err
}
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")
}
if f.name == f.owner.failNoProgress {
return nil, nil
}
entries, err := f.directory.ReadDir(count)
if f.name == f.owner.failReadDir {
if f.owner.readDirErr != nil {
return entries, f.owner.readDirErr
}
return entries, fs.ErrNotExist
}
return entries, err
}