mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-23 06:20:48 +02:00
Setup creates the mount point before weed mount starts, so statting and listing it succeeded on the plain directory and tests ran against local disk whenever the mount was slow or died. waitForMount now waits until the mount point's device id differs from its parent's. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
467 lines
13 KiB
Go
467 lines
13 KiB
Go
package fuse
|
|
|
|
import (
|
|
"fmt"
|
|
"io/fs"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// FuseTestFramework provides utilities for FUSE integration testing
|
|
type FuseTestFramework struct {
|
|
t *testing.T
|
|
tempDir string
|
|
mountPoint string
|
|
dataDir string
|
|
masterProcess *os.Process
|
|
volumeProcess *os.Process
|
|
filerProcess *os.Process
|
|
mountProcess *os.Process
|
|
masterAddr string
|
|
volumeAddr string
|
|
filerAddr string
|
|
weedBinary string
|
|
isSetup bool
|
|
config *TestConfig
|
|
externalMount bool
|
|
}
|
|
|
|
// TestConfig holds configuration for FUSE tests
|
|
type TestConfig struct {
|
|
Collection string
|
|
Replication string
|
|
ChunkSizeMB int
|
|
CacheSizeMB int
|
|
NumVolumes int
|
|
EnableDebug bool
|
|
MountOptions []string
|
|
SkipCleanup bool // for debugging failed tests
|
|
}
|
|
|
|
// DefaultTestConfig returns a default configuration for FUSE tests
|
|
func DefaultTestConfig() *TestConfig {
|
|
return &TestConfig{
|
|
Collection: "",
|
|
Replication: "000",
|
|
ChunkSizeMB: 4,
|
|
CacheSizeMB: 100,
|
|
NumVolumes: 3,
|
|
EnableDebug: false,
|
|
MountOptions: []string{},
|
|
SkipCleanup: false,
|
|
}
|
|
}
|
|
|
|
// NewFuseTestFramework creates a new FUSE testing framework
|
|
func NewFuseTestFramework(t *testing.T, config *TestConfig) *FuseTestFramework {
|
|
if config == nil {
|
|
config = DefaultTestConfig()
|
|
}
|
|
|
|
tempDir, err := os.MkdirTemp("", "seaweedfs_fuse_test_")
|
|
require.NoError(t, err)
|
|
|
|
return &FuseTestFramework{
|
|
t: t,
|
|
tempDir: tempDir,
|
|
mountPoint: filepath.Join(tempDir, "mount"),
|
|
dataDir: filepath.Join(tempDir, "data"),
|
|
masterAddr: "127.0.0.1:19333",
|
|
volumeAddr: "127.0.0.1:18080",
|
|
filerAddr: "127.0.0.1:18888",
|
|
weedBinary: findWeedBinary(),
|
|
isSetup: false,
|
|
config: config,
|
|
}
|
|
}
|
|
|
|
// Setup starts SeaweedFS cluster and mounts FUSE filesystem
|
|
func (f *FuseTestFramework) Setup(config *TestConfig) error {
|
|
if f.isSetup {
|
|
return fmt.Errorf("framework already setup")
|
|
}
|
|
if config != nil {
|
|
f.config = config
|
|
}
|
|
|
|
// Check if we should skip cluster setup and use existing mount
|
|
if os.Getenv("TEST_SKIP_CLUSTER_SETUP") == "true" {
|
|
// Use existing mount point from environment
|
|
if existingMount := os.Getenv("TEST_MOUNT_POINT"); existingMount != "" {
|
|
f.externalMount = true
|
|
|
|
// Verify mount point is accessible
|
|
if _, err := os.Stat(existingMount); err != nil {
|
|
return fmt.Errorf("existing mount point not accessible: %v", err)
|
|
}
|
|
|
|
// Run inside a private work dir so test files do not pile up in
|
|
// the shared mount root and cleanup can remove only what it made
|
|
workDir, err := os.MkdirTemp(existingMount, ".fuse_test_")
|
|
if err != nil {
|
|
return fmt.Errorf("creating work dir under existing mount: %v", err)
|
|
}
|
|
f.mountPoint = workDir
|
|
f.t.Logf("Using existing mount point: %s (work dir %s)", existingMount, f.mountPoint)
|
|
|
|
// Test basic functionality
|
|
testFile := filepath.Join(f.mountPoint, ".framework_test")
|
|
if err := os.WriteFile(testFile, []byte("test"), 0644); err != nil {
|
|
return fmt.Errorf("mount point not writable: %v", err)
|
|
}
|
|
if err := os.Remove(testFile); err != nil {
|
|
f.t.Logf("Warning: failed to cleanup test file: %v", err)
|
|
}
|
|
|
|
// Verify this is actually a SeaweedFS mount by checking for SeaweedFS-specific behavior
|
|
// Create a test file and verify it appears in the filer
|
|
verifyFile := filepath.Join(f.mountPoint, ".seaweedfs_mount_verification")
|
|
if err := os.WriteFile(verifyFile, []byte("SeaweedFS mount verification"), 0644); err != nil {
|
|
return fmt.Errorf("mount point verification failed - cannot write: %v", err)
|
|
}
|
|
|
|
// Read it back to ensure it's working
|
|
if data, err := os.ReadFile(verifyFile); err != nil {
|
|
return fmt.Errorf("mount point verification failed - cannot read: %v", err)
|
|
} else if string(data) != "SeaweedFS mount verification" {
|
|
return fmt.Errorf("mount point verification failed - data mismatch")
|
|
}
|
|
|
|
// Clean up verification file
|
|
if err := os.Remove(verifyFile); err != nil {
|
|
f.t.Logf("Warning: failed to cleanup verification file: %v", err)
|
|
}
|
|
|
|
f.t.Logf("✅ SeaweedFS mount point verified and working: %s", f.mountPoint)
|
|
|
|
f.isSetup = true
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Create directories for full cluster setup
|
|
dirs := []string{f.mountPoint, f.dataDir}
|
|
for _, dir := range dirs {
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create directory %s: %v", dir, err)
|
|
}
|
|
}
|
|
|
|
// Start master
|
|
if err := f.startMaster(config); err != nil {
|
|
return fmt.Errorf("failed to start master: %v", err)
|
|
}
|
|
|
|
// Wait for master to be ready
|
|
if err := f.waitForService(f.masterAddr, 30*time.Second); err != nil {
|
|
return fmt.Errorf("master not ready: %v", err)
|
|
}
|
|
|
|
// Start volume servers
|
|
if err := f.startVolumeServers(config); err != nil {
|
|
return fmt.Errorf("failed to start volume servers: %v", err)
|
|
}
|
|
|
|
// Wait for volume server to be ready
|
|
if err := f.waitForService(f.volumeAddr, 30*time.Second); err != nil {
|
|
return fmt.Errorf("volume server not ready: %v", err)
|
|
}
|
|
|
|
// Start filer
|
|
if err := f.startFiler(config); err != nil {
|
|
return fmt.Errorf("failed to start filer: %v", err)
|
|
}
|
|
|
|
// Wait for filer to be ready
|
|
if err := f.waitForService(f.filerAddr, 30*time.Second); err != nil {
|
|
return fmt.Errorf("filer not ready: %v", err)
|
|
}
|
|
|
|
// Mount FUSE filesystem
|
|
if err := f.mountFuse(config); err != nil {
|
|
return fmt.Errorf("failed to mount FUSE: %v", err)
|
|
}
|
|
|
|
// Wait for mount to be ready
|
|
if err := f.waitForMount(30 * time.Second); err != nil {
|
|
return fmt.Errorf("FUSE mount not ready: %v", err)
|
|
}
|
|
|
|
f.isSetup = true
|
|
return nil
|
|
}
|
|
|
|
// Cleanup stops all processes and removes temporary files
|
|
func (f *FuseTestFramework) Cleanup() {
|
|
// An external mount needs no teardown; only this run's work dir is ours.
|
|
// A TEST_SKIP_CLUSTER_SETUP that fell through to a self-started cluster
|
|
// still owns its processes and must not skip stopping them.
|
|
if f.externalMount {
|
|
if !f.config.SkipCleanup {
|
|
os.RemoveAll(f.mountPoint)
|
|
}
|
|
return
|
|
}
|
|
|
|
if f.mountProcess != nil {
|
|
f.unmountFuse()
|
|
}
|
|
|
|
// Stop processes in reverse order
|
|
processes := []*os.Process{f.mountProcess, f.filerProcess, f.volumeProcess, f.masterProcess}
|
|
for _, proc := range processes {
|
|
if proc != nil {
|
|
proc.Signal(syscall.SIGTERM)
|
|
proc.Wait()
|
|
}
|
|
}
|
|
|
|
// Remove temp directory
|
|
if !f.config.SkipCleanup {
|
|
os.RemoveAll(f.tempDir)
|
|
}
|
|
}
|
|
|
|
// GetMountPoint returns the FUSE mount point path
|
|
func (f *FuseTestFramework) GetMountPoint() string {
|
|
return f.mountPoint
|
|
}
|
|
|
|
// GetFilerAddr returns the filer address
|
|
func (f *FuseTestFramework) GetFilerAddr() string {
|
|
return f.filerAddr
|
|
}
|
|
|
|
// startMaster starts the SeaweedFS master server
|
|
func (f *FuseTestFramework) startMaster(config *TestConfig) error {
|
|
var args []string
|
|
if config.EnableDebug {
|
|
// -v is a global flag; before the subcommand or flag parsing dies
|
|
args = append(args, "-v=4")
|
|
}
|
|
args = append(args,
|
|
"master",
|
|
"-ip=127.0.0.1",
|
|
"-port=19333",
|
|
"-mdir=" + filepath.Join(f.dataDir, "master"),
|
|
"-raftBootstrap",
|
|
)
|
|
|
|
cmd := exec.Command(f.weedBinary, args...)
|
|
cmd.Dir = f.tempDir
|
|
if err := cmd.Start(); err != nil {
|
|
return err
|
|
}
|
|
f.masterProcess = cmd.Process
|
|
return nil
|
|
}
|
|
|
|
// startVolumeServers starts SeaweedFS volume servers
|
|
func (f *FuseTestFramework) startVolumeServers(config *TestConfig) error {
|
|
var args []string
|
|
if config.EnableDebug {
|
|
args = append(args, "-v=4")
|
|
}
|
|
args = append(args,
|
|
"volume",
|
|
"-mserver=" + f.masterAddr,
|
|
"-ip=127.0.0.1",
|
|
"-port=18080",
|
|
"-dir=" + filepath.Join(f.dataDir, "volume"),
|
|
fmt.Sprintf("-max=%d", config.NumVolumes),
|
|
)
|
|
|
|
cmd := exec.Command(f.weedBinary, args...)
|
|
cmd.Dir = f.tempDir
|
|
if err := cmd.Start(); err != nil {
|
|
return err
|
|
}
|
|
f.volumeProcess = cmd.Process
|
|
return nil
|
|
}
|
|
|
|
// startFiler starts the SeaweedFS filer server
|
|
func (f *FuseTestFramework) startFiler(config *TestConfig) error {
|
|
var args []string
|
|
if config.EnableDebug {
|
|
args = append(args, "-v=4")
|
|
}
|
|
args = append(args,
|
|
"filer",
|
|
"-master=" + f.masterAddr,
|
|
"-ip=127.0.0.1",
|
|
"-port=18888",
|
|
)
|
|
|
|
cmd := exec.Command(f.weedBinary, args...)
|
|
cmd.Dir = f.tempDir
|
|
if err := cmd.Start(); err != nil {
|
|
return err
|
|
}
|
|
f.filerProcess = cmd.Process
|
|
return nil
|
|
}
|
|
|
|
// mountFuse mounts the SeaweedFS FUSE filesystem
|
|
func (f *FuseTestFramework) mountFuse(config *TestConfig) error {
|
|
var args []string
|
|
if config.EnableDebug {
|
|
args = append(args, "-v=4")
|
|
}
|
|
args = append(args,
|
|
"mount",
|
|
"-filer=" + f.filerAddr,
|
|
"-dir=" + f.mountPoint,
|
|
"-filer.path=/",
|
|
"-dirAutoCreate",
|
|
)
|
|
|
|
if config.Collection != "" {
|
|
args = append(args, "-collection="+config.Collection)
|
|
}
|
|
if config.Replication != "" {
|
|
args = append(args, "-replication="+config.Replication)
|
|
}
|
|
if config.ChunkSizeMB > 0 {
|
|
args = append(args, fmt.Sprintf("-chunkSizeLimitMB=%d", config.ChunkSizeMB))
|
|
}
|
|
if config.CacheSizeMB > 0 {
|
|
args = append(args, fmt.Sprintf("-cacheSizeMB=%d", config.CacheSizeMB))
|
|
}
|
|
|
|
args = append(args, config.MountOptions...)
|
|
|
|
cmd := exec.Command(f.weedBinary, args...)
|
|
cmd.Dir = f.tempDir
|
|
if err := cmd.Start(); err != nil {
|
|
return err
|
|
}
|
|
f.mountProcess = cmd.Process
|
|
return nil
|
|
}
|
|
|
|
// unmountFuse unmounts the FUSE filesystem
|
|
func (f *FuseTestFramework) unmountFuse() error {
|
|
if f.mountProcess != nil {
|
|
f.mountProcess.Signal(syscall.SIGTERM)
|
|
f.mountProcess.Wait()
|
|
f.mountProcess = nil
|
|
}
|
|
|
|
// Also try system unmount as backup
|
|
exec.Command("umount", f.mountPoint).Run()
|
|
return nil
|
|
}
|
|
|
|
// waitForService waits for a service to be available
|
|
func (f *FuseTestFramework) waitForService(addr string, timeout time.Duration) error {
|
|
deadline := time.Now().Add(timeout)
|
|
for time.Now().Before(deadline) {
|
|
conn, err := net.DialTimeout("tcp", addr, 1*time.Second)
|
|
if err == nil {
|
|
conn.Close()
|
|
return nil
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
return fmt.Errorf("service at %s not ready within timeout", addr)
|
|
}
|
|
|
|
// waitForMount waits for the FUSE mount to be ready
|
|
func (f *FuseTestFramework) waitForMount(timeout time.Duration) error {
|
|
// The mount point is created before weed mount starts, so Stat/ReadDir
|
|
// alone succeed on the plain directory. The device id only differs from
|
|
// its parent's once a filesystem is actually mounted on it.
|
|
parentDev, err := deviceID(filepath.Dir(f.mountPoint))
|
|
if err != nil {
|
|
return fmt.Errorf("stat mount parent: %w", err)
|
|
}
|
|
deadline := time.Now().Add(timeout)
|
|
for time.Now().Before(deadline) {
|
|
if dev, err := deviceID(f.mountPoint); err == nil && dev != parentDev {
|
|
if _, err := os.ReadDir(f.mountPoint); err == nil {
|
|
return nil
|
|
}
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
return fmt.Errorf("mount point not ready within timeout")
|
|
}
|
|
|
|
// findWeedBinary locates the weed binary
|
|
func findWeedBinary() string {
|
|
// Try different possible locations
|
|
candidates := []string{
|
|
"./weed",
|
|
"../weed",
|
|
"../../weed",
|
|
"weed", // in PATH
|
|
}
|
|
|
|
for _, candidate := range candidates {
|
|
if _, err := exec.LookPath(candidate); err == nil {
|
|
return candidate
|
|
}
|
|
if info, err := os.Stat(candidate); err == nil && !info.IsDir() && info.Mode()&0111 != 0 {
|
|
abs, _ := filepath.Abs(candidate)
|
|
return abs
|
|
}
|
|
}
|
|
|
|
// Default fallback
|
|
return "weed"
|
|
}
|
|
|
|
// Helper functions for test assertions
|
|
|
|
// AssertFileExists checks if a file exists in the mount point
|
|
func (f *FuseTestFramework) AssertFileExists(relativePath string) {
|
|
fullPath := filepath.Join(f.mountPoint, relativePath)
|
|
_, err := os.Stat(fullPath)
|
|
require.NoError(f.t, err, "file should exist: %s", relativePath)
|
|
}
|
|
|
|
// AssertFileNotExists checks if a file does not exist in the mount point
|
|
func (f *FuseTestFramework) AssertFileNotExists(relativePath string) {
|
|
fullPath := filepath.Join(f.mountPoint, relativePath)
|
|
_, err := os.Stat(fullPath)
|
|
require.True(f.t, os.IsNotExist(err), "file should not exist: %s", relativePath)
|
|
}
|
|
|
|
// AssertFileContent checks if a file has expected content
|
|
func (f *FuseTestFramework) AssertFileContent(relativePath string, expectedContent []byte) {
|
|
fullPath := filepath.Join(f.mountPoint, relativePath)
|
|
actualContent, err := os.ReadFile(fullPath)
|
|
require.NoError(f.t, err, "failed to read file: %s", relativePath)
|
|
require.Equal(f.t, expectedContent, actualContent, "file content mismatch: %s", relativePath)
|
|
}
|
|
|
|
// AssertFileMode checks if a file has expected permissions
|
|
func (f *FuseTestFramework) AssertFileMode(relativePath string, expectedMode fs.FileMode) {
|
|
fullPath := filepath.Join(f.mountPoint, relativePath)
|
|
info, err := os.Stat(fullPath)
|
|
require.NoError(f.t, err, "failed to stat file: %s", relativePath)
|
|
require.Equal(f.t, expectedMode, info.Mode(), "file mode mismatch: %s", relativePath)
|
|
}
|
|
|
|
// CreateTestFile creates a test file with specified content
|
|
func (f *FuseTestFramework) CreateTestFile(relativePath string, content []byte) {
|
|
fullPath := filepath.Join(f.mountPoint, relativePath)
|
|
dir := filepath.Dir(fullPath)
|
|
require.NoError(f.t, os.MkdirAll(dir, 0755), "failed to create directory: %s", dir)
|
|
require.NoError(f.t, os.WriteFile(fullPath, content, 0644), "failed to create file: %s", relativePath)
|
|
}
|
|
|
|
// CreateTestDir creates a test directory
|
|
func (f *FuseTestFramework) CreateTestDir(relativePath string) {
|
|
fullPath := filepath.Join(f.mountPoint, relativePath)
|
|
require.NoError(f.t, os.MkdirAll(fullPath, 0755), "failed to create directory: %s", relativePath)
|
|
}
|