ollie/virtfs/decl_test.go

701 lines
19 KiB
Go

package virtfs
import (
"context"
"errors"
"os"
"testing"
"time"
)
// ══════════════════════════════════════════════════════════════════
// NodeOption tests
// ══════════════════════════════════════════════════════════════════
func TestNodeOptions(t *testing.T) {
t.Run("Stream", func(t *testing.T) {
spec := FileNode("test", 0444, StreamRaw(func(ctx context.Context, base string) ([]byte, string, error) {
return nil, "", nil
}))
if spec.Stream == nil {
t.Error("Stream not set")
}
})
t.Run("BlockOnce", func(t *testing.T) {
spec := FileNode("test", 0444, BlockOnceRaw(func(ctx context.Context, base string) ([]byte, string, error) {
return nil, "", nil
}))
if spec.BlockOnce == nil {
t.Error("BlockOnce not set")
}
})
t.Run("Rdwr", func(t *testing.T) {
spec := FileNode("test", 0666, Rdwr(func(_ context.Context, data []byte) ([]byte, error) {
return nil, nil
}))
if spec.Rdwr == nil {
t.Error("Rdwr not set")
}
})
t.Run("RemoveNode", func(t *testing.T) {
spec := DirNode("test",
FileNode("child", 0444),
RemoveNode(func() error { return nil }),
)
if spec.Remove == nil {
t.Error("Remove not set")
}
})
t.Run("StatOverride", func(t *testing.T) {
spec := FileNode("test", 0444, StatOverride(func() os.FileInfo {
return &SyntheticFileInfo{Name_: "test"}
}))
if spec.Stat == nil {
t.Error("Stat not set")
}
})
t.Run("UID", func(t *testing.T) {
spec := FileNode("test", 0444, UID("owner"))
if spec.UID != "owner" {
t.Errorf("UID = %q, want 'owner'", spec.UID)
}
})
t.Run("GID", func(t *testing.T) {
spec := FileNode("test", 0444, GID("group"))
if spec.GID != "group" {
t.Errorf("GID = %q, want 'group'", spec.GID)
}
})
t.Run("Doc", func(t *testing.T) {
spec := FileNode("test", 0444, Doc("description"))
if spec.Desc != "description" {
t.Errorf("Desc = %q", spec.Desc)
}
})
}
// ══════════════════════════════════════════════════════════════════
// Tree tests
// ══════════════════════════════════════════════════════════════════
func TestTreeMount(t *testing.T) {
parent := &Tree{perm: 0755}
child := &Tree{perm: 0755}
child.listFn = func(_ []string) ([]os.DirEntry, error) {
return []os.DirEntry{fileEntry("childfile", 0444)}, nil
}
parent.Mount("subdir", child)
got := parent.Child("subdir")
if got != child {
t.Error("Child did not return mounted child")
}
children := parent.Children()
if len(children) != 1 {
t.Errorf("Children = %d, want 1", len(children))
}
nilChild := parent.Child("nonexistent")
if nilChild != nil {
t.Error("Child should return nil for nonexistent")
}
}
func TestTreeSplit(t *testing.T) {
parent := &Tree{perm: 0755}
child := &Tree{perm: 0755}
parent.Mount("sub", child)
c, rest, ok := parent.split("sub")
if !ok || c != child || rest != "" {
t.Error("split exact match failed")
}
c2, rest2, ok2 := parent.split("sub/file")
if !ok2 || c2 != child || rest2 != "file" {
t.Error("split prefix match failed")
}
_, _, ok3 := parent.split("other")
if ok3 {
t.Error("split should not find 'other'")
}
}
// ══════════════════════════════════════════════════════════════════
// fileConfig tests
// ══════════════════════════════════════════════════════════════════
func TestFileConfigMethods(t *testing.T) {
fc := &fileConfig{
StatFn: func() (os.FileInfo, error) { return &SyntheticFileInfo{Name_: "test"}, nil },
ReadFn: func() ([]byte, error) { return []byte("content"), nil },
WriteFn: func(data []byte) error { return nil },
BlockingReadFn: func(ctx context.Context, base string) ([]byte, string, error) { return nil, "", nil },
}
fi, err := fc.Stat()
if err != nil || fi.Name() != "test" {
t.Error("Stat failed")
}
data, err := fc.Read()
if err != nil || string(data) != "content" {
t.Error("Read failed")
}
if err := fc.Write([]byte("x")); err != nil {
t.Error("Write failed")
}
if _, _, err := fc.BlockingRead(context.Background(), ""); err != nil {
t.Error("BlockingRead failed")
}
}
func TestFileConfigModes(t *testing.T) {
fc := &fileConfig{}
if fc.BlockingReadMode() || fc.StreamMode() || fc.RdwrMode() {
t.Error("default modes should be false")
}
if !(&fileConfig{BlockingRead_: true}).BlockingReadMode() {
t.Error("BlockingReadMode should be true")
}
if !(&fileConfig{StreamMode_: true}).StreamMode() {
t.Error("StreamMode should be true")
}
if !(&fileConfig{Rdwr_: true}).RdwrMode() {
t.Error("RdwrMode should be true")
}
}
// ══════════════════════════════════════════════════════════════════
// Builder tests - delete/rename operations
// ══════════════════════════════════════════════════════════════════
func TestDeleteDir(t *testing.T) {
var deleted bool
spec := DirNode("/",
Each("{id}", func() ([]FsNodeDecl, error) {
return []FsNodeDecl{{
Name: "item",
Remove: func() error { deleted = true; return nil },
Children: []FsNodeDecl{
FileNode("data", 0444),
},
}}, nil
}),
)
tree := BuildTree(spec)
err := tree.Delete("item")
if err != nil {
t.Fatalf("Delete: %v", err)
}
if !deleted {
t.Error("remove handler not called")
}
}
func TestDeleteDirNotFound(t *testing.T) {
spec := DirNode("/", FileNode("file", 0444))
tree := BuildTree(spec)
if err := tree.Delete("nonexistent"); err == nil {
t.Error("Delete should fail for nonexistent")
}
}
func TestRenameDir(t *testing.T) {
var newName string
spec := DirNode("/",
Each("{id}", func() ([]FsNodeDecl, error) {
return []FsNodeDecl{{
Name: "item",
Rename: func(n string) error { newName = n; return nil },
Children: []FsNodeDecl{
FileNode("data", 0444),
},
}}, nil
}),
)
tree := BuildTree(spec)
if err := tree.Rename("item", "newitem"); err != nil {
t.Fatalf("Rename: %v", err)
}
if newName != "newitem" {
t.Errorf("newName = %q, want 'newitem'", newName)
}
}
func TestRenameDirNotFound(t *testing.T) {
spec := DirNode("/", FileNode("file", 0444))
tree := BuildTree(spec)
if err := tree.Rename("nonexistent", "new"); err == nil {
t.Error("Rename should fail for nonexistent")
}
}
// ══════════════════════════════════════════════════════════════════
// Builder tests - blocking modes
// ══════════════════════════════════════════════════════════════════
func TestOpenLeafBlockOnce(t *testing.T) {
spec := DirNode("/",
FileNode("blocking", 0444, BlockOnceRaw(func(ctx context.Context, base string) ([]byte, string, error) {
return []byte("data"), "next", nil
})),
)
tree := BuildTree(spec)
f, err := tree.Open("blocking")
if err != nil {
t.Fatalf("Open: %v", err)
}
if !f.BlockingReadMode() {
t.Error("expected BlockingReadMode")
}
}
func TestOpenLeafStream(t *testing.T) {
spec := DirNode("/",
FileNode("streaming", 0444, StreamRaw(func(ctx context.Context, base string) ([]byte, string, error) {
return []byte("stream"), "cursor", nil
})),
)
tree := BuildTree(spec)
f, err := tree.Open("streaming")
if err != nil {
t.Fatalf("Open: %v", err)
}
if !f.StreamMode() {
t.Error("expected StreamMode")
}
}
func TestOpenLeafRdwr(t *testing.T) {
spec := DirNode("/",
FileNode("request", 0666, Rdwr(func(_ context.Context, data []byte) ([]byte, error) {
return append([]byte("echo:"), data...), nil
})),
)
tree := BuildTree(spec)
f, err := tree.Open("request")
if err != nil {
t.Fatalf("Open: %v", err)
}
if !f.RdwrMode() {
t.Error("expected RdwrMode")
}
if err := f.Write([]byte("test")); err != nil {
t.Fatalf("Write: %v", err)
}
data, err := f.Read()
if err != nil {
t.Fatalf("Read: %v", err)
}
if string(data) != "echo:test" {
t.Errorf("data = %q, want 'echo:test'", data)
}
}
func TestOpenLeafRdwrError(t *testing.T) {
spec := DirNode("/",
FileNode("request", 0666, Rdwr(func(_ context.Context, data []byte) ([]byte, error) {
return nil, errors.New("request error")
})),
)
tree := BuildTree(spec)
f, err := tree.Open("request")
if err != nil {
t.Fatalf("Open: %v", err)
}
if err := f.Write([]byte("test")); err == nil {
t.Error("Write should fail when request returns error")
}
}
func TestOpenLeafRdwrWithFallbackRead(t *testing.T) {
spec := DirNode("/",
FileNode("request", 0666,
Read(func() ([]byte, error) { return []byte("fallback"), nil }),
Rdwr(func(_ context.Context, data []byte) ([]byte, error) { return []byte("response"), nil }),
),
)
tree := BuildTree(spec)
f, err := tree.Open("request")
if err != nil {
t.Fatalf("Open: %v", err)
}
data, err := f.Read()
if err != nil {
t.Fatalf("Read: %v", err)
}
if string(data) != "fallback" {
t.Errorf("data = %q, want 'fallback'", data)
}
}
func TestOpenLeafRdwrCancelOnClose(t *testing.T) {
canceled := make(chan struct{})
started := make(chan struct{})
spec := DirNode("/",
FileNode("request", 0666, Rdwr(func(ctx context.Context, data []byte) ([]byte, error) {
close(started)
<-ctx.Done()
close(canceled)
return nil, ctx.Err()
})),
)
tree := BuildTree(spec)
f, err := tree.Open("request")
if err != nil {
t.Fatalf("Open: %v", err)
}
// Start the request in a goroutine
done := make(chan error, 1)
go func() {
done <- f.Write([]byte("test"))
}()
// Wait for request to start
<-started
// Close should cancel the context
f.Close()
// Verify context was canceled
select {
case <-canceled:
// Success
case <-time.After(time.Second):
t.Fatal("context was not canceled on close")
}
// Write should return with context canceled error
select {
case err := <-done:
if err == nil || err != context.Canceled {
t.Errorf("Write error = %v, want context.Canceled", err)
}
case <-time.After(time.Second):
t.Fatal("Write did not return after close")
}
}
func TestOpenLeafNoWriteHandler(t *testing.T) {
spec := DirNode("/",
FileNode("readonly", 0444, Read(func() ([]byte, error) { return []byte("data"), nil })),
)
tree := BuildTree(spec)
f, err := tree.Open("readonly")
if err != nil {
t.Fatalf("Open: %v", err)
}
if err := f.Write([]byte("test")); err == nil {
t.Error("Write should fail on read-only file")
}
}
func TestOpenLeafNoReadHandler(t *testing.T) {
spec := DirNode("/",
FileNode("noread", 0222, Write(func(data []byte) error { return nil })),
)
tree := BuildTree(spec)
f, err := tree.Open("noread")
if err != nil {
t.Fatalf("Open: %v", err)
}
data, err := f.Read()
if err != nil {
t.Fatalf("Read: %v", err)
}
if len(data) != 0 {
t.Errorf("data = %q, want empty", data)
}
}
func TestOpenLeafStatWithCustomStat(t *testing.T) {
spec := DirNode("/",
FileNode("custom", 0444, StatOverride(func() os.FileInfo {
return &SyntheticFileInfo{Name_: "customname", Size_: 999, Mode_: 0777}
})),
)
tree := BuildTree(spec)
f, err := tree.Open("custom")
if err != nil {
t.Fatalf("Open: %v", err)
}
fi, err := f.Stat()
if err != nil {
t.Fatalf("Stat: %v", err)
}
if fi.Name() != "customname" || fi.Size() != 999 {
t.Errorf("got Name=%q Size=%d", fi.Name(), fi.Size())
}
}
func TestOpenLeafDefaultBlockingRead(t *testing.T) {
spec := DirNode("/",
FileNode("normal", 0444, Read(func() ([]byte, error) { return []byte("data"), nil })),
)
tree := BuildTree(spec)
f, err := tree.Open("normal")
if err != nil {
t.Fatalf("Open: %v", err)
}
if _, _, err := f.BlockingRead(context.Background(), ""); err == nil {
t.Error("BlockingRead should fail for non-blocking file")
}
}
// ══════════════════════════════════════════════════════════════════
// Builder tests - validation
// ══════════════════════════════════════════════════════════════════
func TestValidateDirWithFileHandlers(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Error("should panic for dir with file handlers")
}
}()
spec := DirNode("/",
FileNode("child", 0444),
Read(func() ([]byte, error) { return nil, nil }),
)
BuildTree(spec)
}
func TestValidateMultipleBlockingModes(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Error("should panic for multiple blocking modes")
}
}()
spec := DirNode("/",
FileNode("bad", 0444,
BlockOnceRaw(func(context.Context, string) ([]byte, string, error) { return nil, "", nil }),
StreamRaw(func(context.Context, string) ([]byte, string, error) { return nil, "", nil }),
),
)
BuildTree(spec)
}
func TestValidateTemplateWithoutBindings(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Error("should panic for template without bindings")
}
}()
spec := DirNode("/", FileNode("{id}", 0444))
BuildTree(spec)
}
// ══════════════════════════════════════════════════════════════════
// Builder tests - Each with closure capture
// ══════════════════════════════════════════════════════════════════
func TestEachClosureCapture(t *testing.T) {
type Item struct{ Name, Value string }
items := []Item{{"foo", "hello"}, {"bar", "world"}}
spec := DirNode("/",
Each("{item}", func() ([]FsNodeDecl, error) {
var out []FsNodeDecl
for _, item := range items {
it := item
out = append(out, FsNodeDecl{
Name: it.Name,
Children: []FsNodeDecl{
FileNode("value", 0444, Read(func() ([]byte, error) {
return []byte(it.Value), nil
})),
},
})
}
return out, nil
}),
)
tree := BuildTree(spec)
// Read foo/value
f, err := tree.Open("foo/value")
if err != nil {
t.Fatalf("Open foo/value: %v", err)
}
data, err := f.Read()
if err != nil {
t.Fatalf("Read: %v", err)
}
if string(data) != "hello" {
t.Errorf("foo/value = %q, want 'hello'", data)
}
// Read bar/value
f2, err := tree.Open("bar/value")
if err != nil {
t.Fatalf("Open bar/value: %v", err)
}
data2, err := f2.Read()
if err != nil {
t.Fatalf("Read: %v", err)
}
if string(data2) != "world" {
t.Errorf("bar/value = %q, want 'world'", data2)
}
}
func TestEachDirListing(t *testing.T) {
spec := DirNode("/",
Each("{item}", func() ([]FsNodeDecl, error) {
return []FsNodeDecl{
{Name: "alpha", Children: []FsNodeDecl{FileNode("x", 0444)}},
{Name: "beta", Children: []FsNodeDecl{FileNode("x", 0444)}},
}, nil
}),
)
tree := BuildTree(spec)
entries, err := tree.List()
if err != nil {
t.Fatalf("List: %v", err)
}
if len(entries) != 2 {
t.Fatalf("got %d entries, want 2", len(entries))
}
if entries[0].Name() != "alpha" || entries[1].Name() != "beta" {
t.Errorf("entries = %v, %v", entries[0].Name(), entries[1].Name())
}
}
func TestEachWithAliases(t *testing.T) {
spec := DirNode("/",
Each("{item}", func() ([]FsNodeDecl, error) {
return []FsNodeDecl{{
Name: "friendly",
Aliases: []string{"uuid-123"},
Children: []FsNodeDecl{
FileNode("name", 0444, Read(func() ([]byte, error) { return []byte("found"), nil })),
},
}}, nil
}),
)
tree := BuildTree(spec)
// Access by alias
f, err := tree.Open("uuid-123/name")
if err != nil {
t.Fatalf("Open by alias: %v", err)
}
data, err := f.Read()
if err != nil {
t.Fatalf("Read: %v", err)
}
if string(data) != "found" {
t.Errorf("data = %q", data)
}
}
func TestStatDir(t *testing.T) {
spec := DirNode("/",
Each("{item}", func() ([]FsNodeDecl, error) {
return []FsNodeDecl{{
Name: "sub",
Children: []FsNodeDecl{FileNode("file", 0444)},
}}, nil
}),
)
tree := BuildTree(spec)
fi, err := tree.Stat("sub")
if err != nil {
t.Fatalf("Stat: %v", err)
}
if !fi.IsDir() {
t.Error("sub should be a directory")
}
}
func TestStatFile(t *testing.T) {
spec := DirNode("/",
FileNode("readme", 0644, Read(func() ([]byte, error) { return []byte("hi"), nil })),
)
tree := BuildTree(spec)
fi, err := tree.Stat("readme")
if err != nil {
t.Fatalf("Stat: %v", err)
}
if fi.IsDir() {
t.Error("readme should not be a directory")
}
if fi.Mode() != 0644 {
t.Errorf("mode = %o, want 0644", fi.Mode())
}
}
func TestNestedPath(t *testing.T) {
spec := DirNode("/",
DirNode("a",
DirNode("b",
FileNode("c", 0444, Read(func() ([]byte, error) { return []byte("deep"), nil })),
),
),
)
tree := BuildTree(spec)
f, err := tree.Open("a/b/c")
if err != nil {
t.Fatalf("Open a/b/c: %v", err)
}
data, err := f.Read()
if err != nil {
t.Fatalf("Read: %v", err)
}
if string(data) != "deep" {
t.Errorf("data = %q, want 'deep'", data)
}
}
// ══════════════════════════════════════════════════════════════════
// GenerateHelp test
// ══════════════════════════════════════════════════════════════════
func TestGenerateHelp(t *testing.T) {
spec := DirNode("/",
FileNode("ctl", 0222, Doc("Control file")),
FileNode("info", 0444, Doc("Server info")),
)
help := GenerateHelp(spec)
if help == "" {
t.Error("help is empty")
}
if !contains(help, "ctl") || !contains(help, "Control file") {
t.Errorf("help missing ctl: %q", help)
}
}
func contains(s, sub string) bool {
return len(s) >= len(sub) && (s == sub || len(s) > 0 && containsStr(s, sub))
}
func containsStr(s, sub string) bool {
for i := 0; i <= len(s)-len(sub); i++ {
if s[i:i+len(sub)] == sub {
return true
}
}
return false
}