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("Request", func(t *testing.T) { spec := FileNode("test", 0666, Request(func(_ context.Context, data []byte) ([]byte, error) { return nil, nil })) if spec.Request == nil { t.Error("Request 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.RequestRespMode() { 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{RequestResp_: true}).RequestRespMode() { t.Error("RequestRespMode 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 TestOpenLeafRequest(t *testing.T) { spec := DirNode("/", FileNode("request", 0666, Request(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.RequestRespMode() { t.Error("expected RequestRespMode") } 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 TestOpenLeafRequestError(t *testing.T) { spec := DirNode("/", FileNode("request", 0666, Request(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 TestOpenLeafRequestWithFallbackRead(t *testing.T) { spec := DirNode("/", FileNode("request", 0666, Read(func() ([]byte, error) { return []byte("fallback"), nil }), Request(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 TestOpenLeafRequestCancelOnClose(t *testing.T) { canceled := make(chan struct{}) started := make(chan struct{}) spec := DirNode("/", FileNode("request", 0666, Request(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 }