Files
ConfigKit/internal/engine/native_filesystem_test.go

311 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, 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)
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)
}