// Package winfsp exercises a live SeaweedFS mount on Windows. // // It runs against whatever is mounted at -mountpoint, so it needs a real // WinFsp mount and is skipped otherwise. The CI job drives it; run it by hand // with: go test ./test/winfsp -mountpoint=S:\ package winfsp import ( "bytes" "crypto/rand" "flag" "fmt" "os" "path/filepath" "sort" "strings" "sync" "testing" ) var mountPoint = flag.String("mountpoint", "", "a mounted SeaweedFS drive, e.g. S:\\") // entryCount is the directory width the test builds. The reported case that // motivated Windows support is 200k files in one folder; CI uses less so the // job stays inside its budget, but the read path is the same. var entryCount = flag.Int("entries", 5000, "how many files to put in one directory") func testRoot(t *testing.T) string { t.Helper() if *mountPoint == "" { t.Skip("no -mountpoint given; this test needs a live WinFsp mount") } dir := filepath.Join(*mountPoint, "winfsp-test-"+t.Name()) if err := os.RemoveAll(dir); err != nil && !os.IsNotExist(err) { t.Fatalf("clean %s: %v", dir, err) } if err := os.MkdirAll(dir, 0755); err != nil { t.Fatalf("mkdir %s: %v", dir, err) } t.Cleanup(func() { os.RemoveAll(dir) }) return dir } func TestWriteReadRoundTrip(t *testing.T) { dir := testRoot(t) for _, size := range []int{0, 1, 4095, 4096, 1 << 20, 5 << 20} { t.Run(fmt.Sprintf("%dbytes", size), func(t *testing.T) { want := make([]byte, size) if _, err := rand.Read(want); err != nil { t.Fatalf("rand: %v", err) } path := filepath.Join(dir, fmt.Sprintf("file-%d", size)) if err := os.WriteFile(path, want, 0644); err != nil { t.Fatalf("write: %v", err) } got, err := os.ReadFile(path) if err != nil { t.Fatalf("read: %v", err) } if !bytes.Equal(got, want) { t.Fatalf("content differs: got %d bytes, want %d", len(got), len(want)) } info, err := os.Stat(path) if err != nil { t.Fatalf("stat: %v", err) } if info.Size() != int64(size) { t.Fatalf("stat size = %d, want %d", info.Size(), size) } }) } } func TestSeekWriteAndOverwrite(t *testing.T) { dir := testRoot(t) path := filepath.Join(dir, "sparse") f, err := os.Create(path) if err != nil { t.Fatalf("create: %v", err) } if _, err := f.WriteAt([]byte("tail"), 4096); err != nil { t.Fatalf("write at offset: %v", err) } if _, err := f.WriteAt([]byte("head"), 0); err != nil { t.Fatalf("write at start: %v", err) } if err := f.Close(); err != nil { t.Fatalf("close: %v", err) } got, err := os.ReadFile(path) if err != nil { t.Fatalf("read: %v", err) } if len(got) != 4100 { t.Fatalf("size = %d, want 4100", len(got)) } if string(got[:4]) != "head" || string(got[4096:]) != "tail" { t.Fatalf("content at the edges is wrong: %q ... %q", got[:4], got[4096:]) } if !bytes.Equal(got[4:4096], make([]byte, 4092)) { t.Fatal("the hole between the two writes is not zero-filled") } } func TestRenameAndDelete(t *testing.T) { dir := testRoot(t) src := filepath.Join(dir, "before") dst := filepath.Join(dir, "after") if err := os.WriteFile(src, []byte("payload"), 0644); err != nil { t.Fatalf("write: %v", err) } if err := os.Rename(src, dst); err != nil { t.Fatalf("rename: %v", err) } if _, err := os.Stat(src); !os.IsNotExist(err) { t.Fatalf("old name still resolves: %v", err) } got, err := os.ReadFile(dst) if err != nil { t.Fatalf("read renamed: %v", err) } if string(got) != "payload" { t.Fatalf("content after rename = %q", got) } if err := os.Remove(dst); err != nil { t.Fatalf("remove: %v", err) } if _, err := os.Stat(dst); !os.IsNotExist(err) { t.Fatalf("deleted file still resolves: %v", err) } } func TestDirectoryTree(t *testing.T) { dir := testRoot(t) nested := filepath.Join(dir, "a", "b", "c") if err := os.MkdirAll(nested, 0755); err != nil { t.Fatalf("mkdirall: %v", err) } if err := os.WriteFile(filepath.Join(nested, "leaf"), []byte("x"), 0644); err != nil { t.Fatalf("write leaf: %v", err) } info, err := os.Stat(nested) if err != nil { t.Fatalf("stat dir: %v", err) } if !info.IsDir() { t.Fatal("nested path is not reported as a directory") } // A non-empty directory must not be removable. if err := os.Remove(filepath.Join(dir, "a", "b")); err == nil { t.Fatal("removed a non-empty directory") } if err := os.RemoveAll(filepath.Join(dir, "a")); err != nil { t.Fatalf("removeall: %v", err) } } // TestWideDirectory is the case from the report: one folder holding far more // entries than Explorer or the Windows WebDAV client cope with. func TestWideDirectory(t *testing.T) { dir := testRoot(t) want := make([]string, 0, *entryCount) for i := 0; i < *entryCount; i++ { name := fmt.Sprintf("img-%06d.dat", i) if err := os.WriteFile(filepath.Join(dir, name), []byte{byte(i)}, 0644); err != nil { t.Fatalf("write %s: %v", name, err) } want = append(want, name) } entries, err := os.ReadDir(dir) if err != nil { t.Fatalf("readdir: %v", err) } got := make([]string, 0, len(entries)) for _, e := range entries { got = append(got, e.Name()) } sort.Strings(got) sort.Strings(want) if len(got) != len(want) { t.Fatalf("readdir returned %d entries, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Fatalf("entry %d = %q, want %q", i, got[i], want[i]) } } // Reading the directory twice must be stable; a paging bug shows up here. again, err := os.ReadDir(dir) if err != nil { t.Fatalf("second readdir: %v", err) } if len(again) != len(entries) { t.Fatalf("second readdir returned %d entries, first returned %d", len(again), len(entries)) } } func TestConcurrentWriters(t *testing.T) { dir := testRoot(t) const writers = 8 const perWriter = 32 var wg sync.WaitGroup errs := make(chan error, writers) for w := 0; w < writers; w++ { wg.Add(1) go func(w int) { defer wg.Done() for i := 0; i < perWriter; i++ { name := filepath.Join(dir, fmt.Sprintf("w%d-%d", w, i)) body := []byte(strings.Repeat(fmt.Sprintf("%d", w), 128)) if err := os.WriteFile(name, body, 0644); err != nil { errs <- fmt.Errorf("writer %d: %w", w, err) return } } }(w) } wg.Wait() close(errs) for err := range errs { t.Fatalf("%v", err) } entries, err := os.ReadDir(dir) if err != nil { t.Fatalf("readdir: %v", err) } if len(entries) != writers*perWriter { t.Fatalf("got %d files, want %d", len(entries), writers*perWriter) } } func TestStatfsReportsCapacity(t *testing.T) { if *mountPoint == "" { t.Skip("no -mountpoint given; this test needs a live WinFsp mount") } // The drive has to report a size, or Explorer refuses to show it. var free, total, totalFree uint64 if err := getDiskFreeSpace(*mountPoint, &free, &total, &totalFree); err != nil { t.Fatalf("GetDiskFreeSpaceEx: %v", err) } if total == 0 { t.Fatal("drive reports zero total bytes") } }