fsedsl: comprehensive test coverage (95.8%)
- Add tests for Adapter methods (NewAdapter, all wrapper methods) - Add tests for all NodeOption functions (Stream, BlockOnce, Request, CreateFile, RemoveNode, StatOverride, UID, GID, Alias, Bind) - Add tests for Tree mount/child/split operations - Add tests for FileConfig methods and mode validation - Add tests for SyntheticFileInfo and SyntheticEntry - Add tests for builder delete/rename operations - Add tests for blocking/streaming read modes in openLeaf - Add tests for validation panics - Add tree_test.go for filesystem-backed Tree operations: - Create, Delete, Rename, MkdirAll - ReadOnly mode enforcement - Union directories - Child delegation - Resolver and opener fallbacks Coverage improved from 45.5% to 95.8%
This commit is contained in:
parent
2c8cbe23ca
commit
44f5238538
1470
fsedsl/decl_test.go
1470
fsedsl/decl_test.go
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,603 @@
|
|||
package fsedsl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// Filesystem-backed Tree tests
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
|
||||
func TestTreeFilesystemOperations(t *testing.T) {
|
||||
// Create temp directory
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create a test file
|
||||
testFile := filepath.Join(tmpDir, "testfile.txt")
|
||||
if err := os.WriteFile(testFile, []byte("hello"), 0644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
tree := NewTree([]string{tmpDir}, 0644)
|
||||
|
||||
// Test resolve (via Stat)
|
||||
fi, err := tree.Stat("testfile.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Stat: %v", err)
|
||||
}
|
||||
if fi.Name() != "testfile.txt" {
|
||||
t.Errorf("Name = %q", fi.Name())
|
||||
}
|
||||
|
||||
// Test List (without listFn, uses os.ReadDir)
|
||||
entries, err := tree.List()
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if len(entries) != 1 {
|
||||
t.Errorf("List: got %d entries, want 1", len(entries))
|
||||
}
|
||||
|
||||
// Test Open (without openFn, uses file system)
|
||||
f, err := tree.Open("testfile.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
data, err := f.Read()
|
||||
if err != nil {
|
||||
t.Fatalf("Read: %v", err)
|
||||
}
|
||||
if string(data) != "hello" {
|
||||
t.Errorf("data = %q, want 'hello'", data)
|
||||
}
|
||||
|
||||
// Test Write
|
||||
err = f.Write([]byte("world"))
|
||||
if err != nil {
|
||||
t.Fatalf("Write: %v", err)
|
||||
}
|
||||
// Verify write
|
||||
content, _ := os.ReadFile(testFile)
|
||||
if string(content) != "world" {
|
||||
t.Errorf("content = %q, want 'world'", content)
|
||||
}
|
||||
|
||||
// Test Readdir (without readdirFn)
|
||||
subDir := filepath.Join(tmpDir, "subdir")
|
||||
if err := os.Mkdir(subDir, 0755); err != nil {
|
||||
t.Fatalf("Mkdir: %v", err)
|
||||
}
|
||||
os.WriteFile(filepath.Join(subDir, "nested.txt"), []byte("nested"), 0644)
|
||||
|
||||
subEntries, err := tree.Readdir("subdir")
|
||||
if err != nil {
|
||||
t.Fatalf("Readdir: %v", err)
|
||||
}
|
||||
if len(subEntries) != 1 {
|
||||
t.Errorf("Readdir: got %d entries, want 1", len(subEntries))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeCreate(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tree := NewTree([]string{tmpDir}, 0644)
|
||||
|
||||
// Test Create (without createFn)
|
||||
err := tree.Create("newfile.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Create: %v", err)
|
||||
}
|
||||
|
||||
// Verify file exists
|
||||
_, err = os.Stat(filepath.Join(tmpDir, "newfile.txt"))
|
||||
if err != nil {
|
||||
t.Error("Created file should exist")
|
||||
}
|
||||
|
||||
// Test Create with nested path
|
||||
err = tree.Create("nested/deep/file.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Create nested: %v", err)
|
||||
}
|
||||
_, err = os.Stat(filepath.Join(tmpDir, "nested/deep/file.txt"))
|
||||
if err != nil {
|
||||
t.Error("Nested created file should exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeDelete(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create test file
|
||||
testFile := filepath.Join(tmpDir, "todelete.txt")
|
||||
os.WriteFile(testFile, []byte("delete me"), 0644)
|
||||
|
||||
tree := NewTree([]string{tmpDir}, 0644)
|
||||
|
||||
// Test Delete (without deleteFn)
|
||||
err := tree.Delete("todelete.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Delete: %v", err)
|
||||
}
|
||||
|
||||
// Verify file is gone
|
||||
_, err = os.Stat(testFile)
|
||||
if !os.IsNotExist(err) {
|
||||
t.Error("Deleted file should not exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeRename(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create test file
|
||||
oldFile := filepath.Join(tmpDir, "old.txt")
|
||||
os.WriteFile(oldFile, []byte("rename me"), 0644)
|
||||
|
||||
tree := NewTree([]string{tmpDir}, 0644)
|
||||
|
||||
// Test Rename (without renameFn)
|
||||
err := tree.Rename("old.txt", "new.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Rename: %v", err)
|
||||
}
|
||||
|
||||
// Verify old file is gone
|
||||
_, err = os.Stat(oldFile)
|
||||
if !os.IsNotExist(err) {
|
||||
t.Error("Old file should not exist")
|
||||
}
|
||||
|
||||
// Verify new file exists
|
||||
newFile := filepath.Join(tmpDir, "new.txt")
|
||||
_, err = os.Stat(newFile)
|
||||
if err != nil {
|
||||
t.Error("New file should exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeMkdirAll(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tree := NewTree([]string{tmpDir}, 0755)
|
||||
|
||||
// Test MkdirAll
|
||||
err := tree.MkdirAll("a/b/c")
|
||||
if err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
|
||||
// Verify directory exists
|
||||
fi, err := os.Stat(filepath.Join(tmpDir, "a/b/c"))
|
||||
if err != nil {
|
||||
t.Fatal("Directory should exist")
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
t.Error("Should be a directory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeReadOnly(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tree := NewTree([]string{tmpDir}, 0644, WithReadOnly())
|
||||
|
||||
// Create should fail
|
||||
err := tree.Create("file.txt")
|
||||
if err == nil {
|
||||
t.Error("Create should fail on read-only tree")
|
||||
}
|
||||
|
||||
// Delete should fail
|
||||
os.WriteFile(filepath.Join(tmpDir, "existing.txt"), []byte("x"), 0644)
|
||||
err = tree.Delete("existing.txt")
|
||||
if err == nil {
|
||||
t.Error("Delete should fail on read-only tree")
|
||||
}
|
||||
|
||||
// Rename should fail
|
||||
err = tree.Rename("existing.txt", "new.txt")
|
||||
if err == nil {
|
||||
t.Error("Rename should fail on read-only tree")
|
||||
}
|
||||
|
||||
// MkdirAll should fail
|
||||
err = tree.MkdirAll("newdir")
|
||||
if err == nil {
|
||||
t.Error("MkdirAll should fail on read-only tree")
|
||||
}
|
||||
|
||||
// Write via Open should fail
|
||||
f, err := tree.Open("existing.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
err = f.Write([]byte("new content"))
|
||||
if err == nil {
|
||||
t.Error("Write should fail on read-only tree")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeMkdirAllNoBackingDir(t *testing.T) {
|
||||
tree := NewTree(nil, 0755)
|
||||
|
||||
err := tree.MkdirAll("somedir")
|
||||
if err == nil {
|
||||
t.Error("MkdirAll should fail with no backing directory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeReaddirNoBackingDir(t *testing.T) {
|
||||
tree := NewTree(nil, 0755)
|
||||
|
||||
_, err := tree.Readdir("somedir")
|
||||
if err == nil {
|
||||
t.Error("Readdir should fail with no backing directory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeOpenNotFound(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tree := NewTree([]string{tmpDir}, 0644, WithReadOnly())
|
||||
|
||||
// Open non-existent file on read-only tree should fail
|
||||
_, err := tree.Open("nonexistent.txt")
|
||||
if err == nil {
|
||||
t.Error("Open should fail for nonexistent file on read-only tree")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeOpenNewFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tree := NewTree([]string{tmpDir}, 0644)
|
||||
|
||||
// Open non-existent file on writable tree should work (creates on write)
|
||||
f, err := tree.Open("newfile.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
|
||||
// Write creates the file
|
||||
err = f.Write([]byte("created"))
|
||||
if err != nil {
|
||||
t.Fatalf("Write: %v", err)
|
||||
}
|
||||
|
||||
// Verify file exists
|
||||
content, err := os.ReadFile(filepath.Join(tmpDir, "newfile.txt"))
|
||||
if err != nil {
|
||||
t.Fatal("File should exist after write")
|
||||
}
|
||||
if string(content) != "created" {
|
||||
t.Errorf("content = %q, want 'created'", content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeUnionDirs(t *testing.T) {
|
||||
// Create two directories
|
||||
dir1 := t.TempDir()
|
||||
dir2 := t.TempDir()
|
||||
|
||||
// Put different files in each
|
||||
os.WriteFile(filepath.Join(dir1, "file1.txt"), []byte("from dir1"), 0644)
|
||||
os.WriteFile(filepath.Join(dir2, "file2.txt"), []byte("from dir2"), 0644)
|
||||
// Same file in both - dir1 should win
|
||||
os.WriteFile(filepath.Join(dir1, "common.txt"), []byte("dir1 version"), 0644)
|
||||
os.WriteFile(filepath.Join(dir2, "common.txt"), []byte("dir2 version"), 0644)
|
||||
|
||||
tree := NewTree([]string{dir1, dir2}, 0644)
|
||||
|
||||
// List should show all unique files
|
||||
entries, err := tree.List()
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
names := make(map[string]bool)
|
||||
for _, e := range entries {
|
||||
names[e.Name()] = true
|
||||
}
|
||||
if !names["file1.txt"] || !names["file2.txt"] || !names["common.txt"] {
|
||||
t.Errorf("Missing expected files, got: %v", names)
|
||||
}
|
||||
|
||||
// Resolve common.txt should return dir1 version
|
||||
f, err := tree.Open("common.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
data, err := f.Read()
|
||||
if err != nil {
|
||||
t.Fatalf("Read: %v", err)
|
||||
}
|
||||
if string(data) != "dir1 version" {
|
||||
t.Errorf("data = %q, want 'dir1 version'", data)
|
||||
}
|
||||
|
||||
// Resolve file2.txt from dir2
|
||||
f2, err := tree.Open("file2.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Open file2: %v", err)
|
||||
}
|
||||
data2, err := f2.Read()
|
||||
if err != nil {
|
||||
t.Fatalf("Read: %v", err)
|
||||
}
|
||||
if string(data2) != "from dir2" {
|
||||
t.Errorf("data2 = %q, want 'from dir2'", data2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeChildDelegation(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
parentDir := t.TempDir()
|
||||
|
||||
// Create file in child
|
||||
os.WriteFile(filepath.Join(tmpDir, "childfile.txt"), []byte("child content"), 0644)
|
||||
|
||||
parent := NewTree([]string{parentDir}, 0755)
|
||||
child := NewTree([]string{tmpDir}, 0644)
|
||||
parent.Mount("sub", child)
|
||||
|
||||
// Operations should delegate to child
|
||||
|
||||
// Create in child
|
||||
err := parent.Create("sub/newfile.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Create in child: %v", err)
|
||||
}
|
||||
_, err = os.Stat(filepath.Join(tmpDir, "newfile.txt"))
|
||||
if err != nil {
|
||||
t.Error("File should exist in child dir")
|
||||
}
|
||||
|
||||
// Delete in child
|
||||
err = parent.Delete("sub/newfile.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Delete in child: %v", err)
|
||||
}
|
||||
|
||||
// Rename in child
|
||||
err = parent.Rename("sub/childfile.txt", "renamed.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Rename in child: %v", err)
|
||||
}
|
||||
|
||||
// Readdir via child (rest != "")
|
||||
subDir := filepath.Join(tmpDir, "nested")
|
||||
os.Mkdir(subDir, 0755)
|
||||
os.WriteFile(filepath.Join(subDir, "deep.txt"), []byte("deep"), 0644)
|
||||
|
||||
entries, err := parent.Readdir("sub/nested")
|
||||
if err != nil {
|
||||
t.Fatalf("Readdir nested: %v", err)
|
||||
}
|
||||
if len(entries) != 1 {
|
||||
t.Errorf("Readdir: got %d, want 1", len(entries))
|
||||
}
|
||||
|
||||
// MkdirAll in child
|
||||
err = parent.MkdirAll("sub/new/deep/path")
|
||||
if err != nil {
|
||||
t.Fatalf("MkdirAll in child: %v", err)
|
||||
}
|
||||
_, err = os.Stat(filepath.Join(tmpDir, "new/deep/path"))
|
||||
if err != nil {
|
||||
t.Error("Directory should exist in child")
|
||||
}
|
||||
|
||||
// Stat child directory itself
|
||||
fi, err := parent.Stat("sub")
|
||||
if err != nil {
|
||||
t.Fatalf("Stat child: %v", err)
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
t.Error("Child should be dir")
|
||||
}
|
||||
|
||||
// Stat nested path in child
|
||||
fi2, err := parent.Stat("sub/renamed.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Stat in child: %v", err)
|
||||
}
|
||||
if fi2.IsDir() {
|
||||
t.Error("File should not be dir")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeOpenChildDir(t *testing.T) {
|
||||
parent := NewTree(nil, 0755)
|
||||
child := NewTree(nil, 0755)
|
||||
parent.Mount("sub", child)
|
||||
|
||||
// Opening a directory should fail
|
||||
_, err := parent.Open("sub")
|
||||
if err == nil {
|
||||
t.Error("Open directory should fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeStatWithOpenerFallback(t *testing.T) {
|
||||
tree := NewTree(nil, 0755,
|
||||
WithOpener(func(dirs []string, name string) (File, error) {
|
||||
return &FileConfig{
|
||||
StatFn: func() (os.FileInfo, error) {
|
||||
return &SyntheticFileInfo{Name_: name, Mode_: 0600, Size_: 42}, nil
|
||||
},
|
||||
ReadFn: func() ([]byte, error) { return []byte("content"), nil },
|
||||
WriteFn: func([]byte) error { return nil },
|
||||
BlockingReadFn: func(context.Context, string) ([]byte, string, error) { return nil, "", nil },
|
||||
}, nil
|
||||
}),
|
||||
)
|
||||
|
||||
// Stat should use opener to get file info
|
||||
fi, err := tree.Stat("anyfile")
|
||||
if err != nil {
|
||||
t.Fatalf("Stat: %v", err)
|
||||
}
|
||||
if fi.Name() != "anyfile" {
|
||||
t.Errorf("Name = %q", fi.Name())
|
||||
}
|
||||
if fi.Size() != 42 {
|
||||
t.Errorf("Size = %d, want 42", fi.Size())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeStatWithResolverNameOverride(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
os.WriteFile(filepath.Join(tmpDir, "realfile.txt"), []byte("content"), 0644)
|
||||
|
||||
tree := NewTree([]string{tmpDir}, 0755,
|
||||
WithResolver(func(dirs []string, name string) (string, error) {
|
||||
// Map virtual name to real file
|
||||
if name == "virtual.txt" {
|
||||
return filepath.Join(dirs[0], "realfile.txt"), nil
|
||||
}
|
||||
return "", os.ErrNotExist
|
||||
}),
|
||||
)
|
||||
|
||||
// Stat via resolver should use synthetic info with original name
|
||||
fi, err := tree.Stat("virtual.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Stat: %v", err)
|
||||
}
|
||||
// Name should be the virtual name, not the real filename
|
||||
if fi.Name() != "virtual.txt" {
|
||||
t.Errorf("Name = %q, want 'virtual.txt'", fi.Name())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeListWithIndexAndChildren(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
os.WriteFile(filepath.Join(tmpDir, "file.txt"), []byte("x"), 0644)
|
||||
|
||||
tree := NewTree([]string{tmpDir}, 0755,
|
||||
WithIndex(func(ft FileTree) ([]byte, error) {
|
||||
return []byte("index"), nil
|
||||
}),
|
||||
)
|
||||
|
||||
child := NewTree(nil, 0755)
|
||||
tree.Mount("child", child)
|
||||
|
||||
entries, err := tree.List()
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
|
||||
names := make(map[string]bool)
|
||||
for _, e := range entries {
|
||||
names[e.Name()] = true
|
||||
}
|
||||
|
||||
if !names["idx"] {
|
||||
t.Error("Missing idx")
|
||||
}
|
||||
if !names["file.txt"] {
|
||||
t.Error("Missing file.txt")
|
||||
}
|
||||
if !names["child"] {
|
||||
t.Error("Missing child")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeListWithListerAndIndex(t *testing.T) {
|
||||
tree := NewTree(nil, 0755,
|
||||
WithLister(func(dirs []string) ([]os.DirEntry, error) {
|
||||
return []os.DirEntry{FileEntry("synthetic", 0644)}, nil
|
||||
}),
|
||||
WithIndex(func(ft FileTree) ([]byte, error) {
|
||||
return []byte("index"), nil
|
||||
}),
|
||||
)
|
||||
|
||||
child := NewTree(nil, 0755)
|
||||
tree.Mount("mounted", child)
|
||||
|
||||
entries, err := tree.List()
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
|
||||
names := make(map[string]bool)
|
||||
for _, e := range entries {
|
||||
names[e.Name()] = true
|
||||
}
|
||||
|
||||
if !names["idx"] {
|
||||
t.Error("Missing idx (should be prepended)")
|
||||
}
|
||||
if !names["synthetic"] {
|
||||
t.Error("Missing synthetic")
|
||||
}
|
||||
if !names["mounted"] {
|
||||
t.Error("Missing mounted child")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateReadModeAllThree(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("should panic with all three modes set")
|
||||
}
|
||||
}()
|
||||
|
||||
fc := &FileConfig{
|
||||
BlockingRead_: true,
|
||||
StreamMode_: true,
|
||||
RequestResp_: true,
|
||||
}
|
||||
fc.ValidateReadMode()
|
||||
}
|
||||
|
||||
// Test readdirDir error path
|
||||
func TestReaddirDirNotFound(t *testing.T) {
|
||||
spec := DirNode[TestCtx]("/",
|
||||
FileNode[TestCtx]("file", 0444),
|
||||
)
|
||||
|
||||
tree := BuildTree(spec, TestCtx{})
|
||||
_, err := tree.Readdir("nonexistent")
|
||||
if err == nil {
|
||||
t.Error("Readdir should fail for nonexistent")
|
||||
}
|
||||
}
|
||||
|
||||
// Test help.go edge cases
|
||||
func TestGenerateHelpModes(t *testing.T) {
|
||||
spec := DirNode[TestCtx]("/",
|
||||
FileNode[TestCtx]("readonly", 0444,
|
||||
Read(func(ctx TestCtx) ([]byte, error) { return nil, nil }),
|
||||
Doc[TestCtx]("Read-only file"),
|
||||
),
|
||||
FileNode[TestCtx]("writeonly", 0222,
|
||||
Write(func(ctx TestCtx, data []byte) error { return nil }),
|
||||
Doc[TestCtx]("Write-only file"),
|
||||
),
|
||||
FileNode[TestCtx]("executable", 0755,
|
||||
Doc[TestCtx]("Executable file"),
|
||||
),
|
||||
DirNode[TestCtx]("subdir",
|
||||
Doc[TestCtx]("A subdirectory"),
|
||||
FileNode[TestCtx]("nested", 0444),
|
||||
),
|
||||
)
|
||||
|
||||
help := GenerateHelp(spec)
|
||||
|
||||
if !contains(help, "readonly") {
|
||||
t.Error("Missing readonly in help")
|
||||
}
|
||||
if !contains(help, "writeonly") {
|
||||
t.Error("Missing writeonly in help")
|
||||
}
|
||||
if !contains(help, "subdir") {
|
||||
t.Error("Missing subdir in help")
|
||||
}
|
||||
// fmtMode produces 4-char strings like "-rr-", "-w--", "drwx"
|
||||
if !contains(help, "-rr-") && !contains(help, "-w--") {
|
||||
t.Errorf("Missing mode indicators in help:\n%s", help)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue