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("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 TestTreeSplit(t *testing.T) { parent := &Tree{perm: 0755, children: make(map[string]*Tree)} child := &Tree{perm: 0755} parent.children["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) } } // ══════════════════════════════════════════════════════════════════ // Each with non-template name (like "peer/") // ══════════════════════════════════════════════════════════════════ func TestEachNonTemplateName(t *testing.T) { // This tests the pattern used by peer/ in olliesrv: // Each("peer", ...) creates a directory whose children come from Bindings writeData := "" spec := DirNode("/", Each("peer", func() ([]FsNodeDecl, error) { return []FsNodeDecl{ {Name: "alice", Mode: 0222, Write: func(data []byte) error { writeData = string(data); return nil }}, {Name: "bob", Mode: 0222, Write: func(data []byte) error { writeData = string(data); return nil }}, }, nil }), ) tree := BuildTree(spec) // List root should show "peer" as a directory entries, err := tree.List() if err != nil { t.Fatalf("List root: %v", err) } if len(entries) != 1 { t.Fatalf("got %d root entries, want 1", len(entries)) } if entries[0].Name() != "peer" || !entries[0].IsDir() { t.Errorf("root entry = %v (dir=%v), want peer dir", entries[0].Name(), entries[0].IsDir()) } // List peer/ should show alice and bob entries, err = tree.Readdir("peer") if err != nil { t.Fatalf("Readdir peer: %v", err) } if len(entries) != 2 { t.Fatalf("got %d peer entries, want 2", len(entries)) } names := map[string]bool{} for _, e := range entries { names[e.Name()] = true } if !names["alice"] || !names["bob"] { t.Errorf("peer entries = %v, want alice and bob", entries) } // Open and write to peer/alice f, err := tree.Open("peer/alice") if err != nil { t.Fatalf("Open peer/alice: %v", err) } if err := f.Write([]byte("hello")); err != nil { t.Fatalf("Write: %v", err) } if writeData != "hello" { t.Errorf("writeData = %q, want hello", writeData) } } // ══════════════════════════════════════════════════════════════════ // 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 }