framework.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. package fuse_test
  2. import (
  3. "fmt"
  4. "io/fs"
  5. "os"
  6. "os/exec"
  7. "path/filepath"
  8. "syscall"
  9. "testing"
  10. "time"
  11. "github.com/stretchr/testify/require"
  12. )
  13. // FuseTestFramework provides utilities for FUSE integration testing
  14. type FuseTestFramework struct {
  15. t *testing.T
  16. tempDir string
  17. mountPoint string
  18. dataDir string
  19. masterProcess *os.Process
  20. volumeProcess *os.Process
  21. filerProcess *os.Process
  22. mountProcess *os.Process
  23. masterAddr string
  24. volumeAddr string
  25. filerAddr string
  26. weedBinary string
  27. isSetup bool
  28. }
  29. // TestConfig holds configuration for FUSE tests
  30. type TestConfig struct {
  31. Collection string
  32. Replication string
  33. ChunkSizeMB int
  34. CacheSizeMB int
  35. NumVolumes int
  36. EnableDebug bool
  37. MountOptions []string
  38. SkipCleanup bool // for debugging failed tests
  39. }
  40. // DefaultTestConfig returns a default configuration for FUSE tests
  41. func DefaultTestConfig() *TestConfig {
  42. return &TestConfig{
  43. Collection: "",
  44. Replication: "000",
  45. ChunkSizeMB: 4,
  46. CacheSizeMB: 100,
  47. NumVolumes: 3,
  48. EnableDebug: false,
  49. MountOptions: []string{},
  50. SkipCleanup: false,
  51. }
  52. }
  53. // NewFuseTestFramework creates a new FUSE testing framework
  54. func NewFuseTestFramework(t *testing.T, config *TestConfig) *FuseTestFramework {
  55. if config == nil {
  56. config = DefaultTestConfig()
  57. }
  58. tempDir, err := os.MkdirTemp("", "seaweedfs_fuse_test_")
  59. require.NoError(t, err)
  60. return &FuseTestFramework{
  61. t: t,
  62. tempDir: tempDir,
  63. mountPoint: filepath.Join(tempDir, "mount"),
  64. dataDir: filepath.Join(tempDir, "data"),
  65. masterAddr: "127.0.0.1:19333",
  66. volumeAddr: "127.0.0.1:18080",
  67. filerAddr: "127.0.0.1:18888",
  68. weedBinary: findWeedBinary(),
  69. isSetup: false,
  70. }
  71. }
  72. // Setup starts SeaweedFS cluster and mounts FUSE filesystem
  73. func (f *FuseTestFramework) Setup(config *TestConfig) error {
  74. if f.isSetup {
  75. return fmt.Errorf("framework already setup")
  76. }
  77. // Create directories
  78. dirs := []string{f.mountPoint, f.dataDir}
  79. for _, dir := range dirs {
  80. if err := os.MkdirAll(dir, 0755); err != nil {
  81. return fmt.Errorf("failed to create directory %s: %v", dir, err)
  82. }
  83. }
  84. // Start master
  85. if err := f.startMaster(config); err != nil {
  86. return fmt.Errorf("failed to start master: %v", err)
  87. }
  88. // Wait for master to be ready
  89. if err := f.waitForService(f.masterAddr, 30*time.Second); err != nil {
  90. return fmt.Errorf("master not ready: %v", err)
  91. }
  92. // Start volume servers
  93. if err := f.startVolumeServers(config); err != nil {
  94. return fmt.Errorf("failed to start volume servers: %v", err)
  95. }
  96. // Wait for volume server to be ready
  97. if err := f.waitForService(f.volumeAddr, 30*time.Second); err != nil {
  98. return fmt.Errorf("volume server not ready: %v", err)
  99. }
  100. // Start filer
  101. if err := f.startFiler(config); err != nil {
  102. return fmt.Errorf("failed to start filer: %v", err)
  103. }
  104. // Wait for filer to be ready
  105. if err := f.waitForService(f.filerAddr, 30*time.Second); err != nil {
  106. return fmt.Errorf("filer not ready: %v", err)
  107. }
  108. // Mount FUSE filesystem
  109. if err := f.mountFuse(config); err != nil {
  110. return fmt.Errorf("failed to mount FUSE: %v", err)
  111. }
  112. // Wait for mount to be ready
  113. if err := f.waitForMount(30 * time.Second); err != nil {
  114. return fmt.Errorf("FUSE mount not ready: %v", err)
  115. }
  116. f.isSetup = true
  117. return nil
  118. }
  119. // Cleanup stops all processes and removes temporary files
  120. func (f *FuseTestFramework) Cleanup() {
  121. if f.mountProcess != nil {
  122. f.unmountFuse()
  123. }
  124. // Stop processes in reverse order
  125. processes := []*os.Process{f.mountProcess, f.filerProcess, f.volumeProcess, f.masterProcess}
  126. for _, proc := range processes {
  127. if proc != nil {
  128. proc.Signal(syscall.SIGTERM)
  129. proc.Wait()
  130. }
  131. }
  132. // Remove temp directory
  133. if !DefaultTestConfig().SkipCleanup {
  134. os.RemoveAll(f.tempDir)
  135. }
  136. }
  137. // GetMountPoint returns the FUSE mount point path
  138. func (f *FuseTestFramework) GetMountPoint() string {
  139. return f.mountPoint
  140. }
  141. // GetFilerAddr returns the filer address
  142. func (f *FuseTestFramework) GetFilerAddr() string {
  143. return f.filerAddr
  144. }
  145. // startMaster starts the SeaweedFS master server
  146. func (f *FuseTestFramework) startMaster(config *TestConfig) error {
  147. args := []string{
  148. "master",
  149. "-ip=127.0.0.1",
  150. "-port=19333",
  151. "-mdir=" + filepath.Join(f.dataDir, "master"),
  152. "-raftBootstrap",
  153. }
  154. if config.EnableDebug {
  155. args = append(args, "-v=4")
  156. }
  157. cmd := exec.Command(f.weedBinary, args...)
  158. cmd.Dir = f.tempDir
  159. if err := cmd.Start(); err != nil {
  160. return err
  161. }
  162. f.masterProcess = cmd.Process
  163. return nil
  164. }
  165. // startVolumeServers starts SeaweedFS volume servers
  166. func (f *FuseTestFramework) startVolumeServers(config *TestConfig) error {
  167. args := []string{
  168. "volume",
  169. "-mserver=" + f.masterAddr,
  170. "-ip=127.0.0.1",
  171. "-port=18080",
  172. "-dir=" + filepath.Join(f.dataDir, "volume"),
  173. fmt.Sprintf("-max=%d", config.NumVolumes),
  174. }
  175. if config.EnableDebug {
  176. args = append(args, "-v=4")
  177. }
  178. cmd := exec.Command(f.weedBinary, args...)
  179. cmd.Dir = f.tempDir
  180. if err := cmd.Start(); err != nil {
  181. return err
  182. }
  183. f.volumeProcess = cmd.Process
  184. return nil
  185. }
  186. // startFiler starts the SeaweedFS filer server
  187. func (f *FuseTestFramework) startFiler(config *TestConfig) error {
  188. args := []string{
  189. "filer",
  190. "-master=" + f.masterAddr,
  191. "-ip=127.0.0.1",
  192. "-port=18888",
  193. }
  194. if config.EnableDebug {
  195. args = append(args, "-v=4")
  196. }
  197. cmd := exec.Command(f.weedBinary, args...)
  198. cmd.Dir = f.tempDir
  199. if err := cmd.Start(); err != nil {
  200. return err
  201. }
  202. f.filerProcess = cmd.Process
  203. return nil
  204. }
  205. // mountFuse mounts the SeaweedFS FUSE filesystem
  206. func (f *FuseTestFramework) mountFuse(config *TestConfig) error {
  207. args := []string{
  208. "mount",
  209. "-filer=" + f.filerAddr,
  210. "-dir=" + f.mountPoint,
  211. "-filer.path=/",
  212. "-dirAutoCreate",
  213. }
  214. if config.Collection != "" {
  215. args = append(args, "-collection="+config.Collection)
  216. }
  217. if config.Replication != "" {
  218. args = append(args, "-replication="+config.Replication)
  219. }
  220. if config.ChunkSizeMB > 0 {
  221. args = append(args, fmt.Sprintf("-chunkSizeLimitMB=%d", config.ChunkSizeMB))
  222. }
  223. if config.CacheSizeMB > 0 {
  224. args = append(args, fmt.Sprintf("-cacheSizeMB=%d", config.CacheSizeMB))
  225. }
  226. if config.EnableDebug {
  227. args = append(args, "-v=4")
  228. }
  229. args = append(args, config.MountOptions...)
  230. cmd := exec.Command(f.weedBinary, args...)
  231. cmd.Dir = f.tempDir
  232. if err := cmd.Start(); err != nil {
  233. return err
  234. }
  235. f.mountProcess = cmd.Process
  236. return nil
  237. }
  238. // unmountFuse unmounts the FUSE filesystem
  239. func (f *FuseTestFramework) unmountFuse() error {
  240. if f.mountProcess != nil {
  241. f.mountProcess.Signal(syscall.SIGTERM)
  242. f.mountProcess.Wait()
  243. f.mountProcess = nil
  244. }
  245. // Also try system unmount as backup
  246. exec.Command("umount", f.mountPoint).Run()
  247. return nil
  248. }
  249. // waitForService waits for a service to be available
  250. func (f *FuseTestFramework) waitForService(addr string, timeout time.Duration) error {
  251. deadline := time.Now().Add(timeout)
  252. for time.Now().Before(deadline) {
  253. conn, err := net.DialTimeout("tcp", addr, 1*time.Second)
  254. if err == nil {
  255. conn.Close()
  256. return nil
  257. }
  258. time.Sleep(100 * time.Millisecond)
  259. }
  260. return fmt.Errorf("service at %s not ready within timeout", addr)
  261. }
  262. // waitForMount waits for the FUSE mount to be ready
  263. func (f *FuseTestFramework) waitForMount(timeout time.Duration) error {
  264. deadline := time.Now().Add(timeout)
  265. for time.Now().Before(deadline) {
  266. // Check if mount point is accessible
  267. if _, err := os.Stat(f.mountPoint); err == nil {
  268. // Try to list directory
  269. if _, err := os.ReadDir(f.mountPoint); err == nil {
  270. return nil
  271. }
  272. }
  273. time.Sleep(100 * time.Millisecond)
  274. }
  275. return fmt.Errorf("mount point not ready within timeout")
  276. }
  277. // findWeedBinary locates the weed binary
  278. func findWeedBinary() string {
  279. // Try different possible locations
  280. candidates := []string{
  281. "./weed",
  282. "../weed",
  283. "../../weed",
  284. "weed", // in PATH
  285. }
  286. for _, candidate := range candidates {
  287. if _, err := exec.LookPath(candidate); err == nil {
  288. return candidate
  289. }
  290. if _, err := os.Stat(candidate); err == nil {
  291. abs, _ := filepath.Abs(candidate)
  292. return abs
  293. }
  294. }
  295. // Default fallback
  296. return "weed"
  297. }
  298. // Helper functions for test assertions
  299. // AssertFileExists checks if a file exists in the mount point
  300. func (f *FuseTestFramework) AssertFileExists(relativePath string) {
  301. fullPath := filepath.Join(f.mountPoint, relativePath)
  302. _, err := os.Stat(fullPath)
  303. require.NoError(f.t, err, "file should exist: %s", relativePath)
  304. }
  305. // AssertFileNotExists checks if a file does not exist in the mount point
  306. func (f *FuseTestFramework) AssertFileNotExists(relativePath string) {
  307. fullPath := filepath.Join(f.mountPoint, relativePath)
  308. _, err := os.Stat(fullPath)
  309. require.True(f.t, os.IsNotExist(err), "file should not exist: %s", relativePath)
  310. }
  311. // AssertFileContent checks if a file has expected content
  312. func (f *FuseTestFramework) AssertFileContent(relativePath string, expectedContent []byte) {
  313. fullPath := filepath.Join(f.mountPoint, relativePath)
  314. actualContent, err := os.ReadFile(fullPath)
  315. require.NoError(f.t, err, "failed to read file: %s", relativePath)
  316. require.Equal(f.t, expectedContent, actualContent, "file content mismatch: %s", relativePath)
  317. }
  318. // AssertFileMode checks if a file has expected permissions
  319. func (f *FuseTestFramework) AssertFileMode(relativePath string, expectedMode fs.FileMode) {
  320. fullPath := filepath.Join(f.mountPoint, relativePath)
  321. info, err := os.Stat(fullPath)
  322. require.NoError(f.t, err, "failed to stat file: %s", relativePath)
  323. require.Equal(f.t, expectedMode, info.Mode(), "file mode mismatch: %s", relativePath)
  324. }
  325. // CreateTestFile creates a test file with specified content
  326. func (f *FuseTestFramework) CreateTestFile(relativePath string, content []byte) {
  327. fullPath := filepath.Join(f.mountPoint, relativePath)
  328. dir := filepath.Dir(fullPath)
  329. require.NoError(f.t, os.MkdirAll(dir, 0755), "failed to create directory: %s", dir)
  330. require.NoError(f.t, os.WriteFile(fullPath, content, 0644), "failed to create file: %s", relativePath)
  331. }
  332. // CreateTestDir creates a test directory
  333. func (f *FuseTestFramework) CreateTestDir(relativePath string) {
  334. fullPath := filepath.Join(f.mountPoint, relativePath)
  335. require.NoError(f.t, os.MkdirAll(fullPath, 0755), "failed to create directory: %s", relativePath)
  336. }