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:
Levi Neely 2026-08-04 12:27:47 +02:00
parent 2c8cbe23ca
commit 44f5238538
2 changed files with 2065 additions and 8 deletions

File diff suppressed because it is too large Load Diff

603
fsedsl/tree_test.go Normal file
View File

@ -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)
}
}