clusterfs_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. package clusterfs
  2. import (
  3. "bytes"
  4. "errors"
  5. "io"
  6. "os"
  7. "path"
  8. "path/filepath"
  9. "sort"
  10. "strings"
  11. "sync"
  12. "testing"
  13. "imuslab.com/arozos/mod/filesystem/arozfs"
  14. )
  15. // fakeBackend is an in-memory namespace.
  16. type fakeBackend struct {
  17. mu sync.Mutex
  18. files map[string][]byte // path -> content, nil value = directory
  19. ready bool
  20. }
  21. func newFake() *fakeBackend {
  22. return &fakeBackend{files: map[string][]byte{"/": nil}, ready: true}
  23. }
  24. func (f *fakeBackend) Ready() bool { return f.ready }
  25. func (f *fakeBackend) Stat(p string) (Info, error) {
  26. f.mu.Lock()
  27. defer f.mu.Unlock()
  28. c, ok := f.files[p]
  29. if !ok {
  30. return Info{}, os.ErrNotExist
  31. }
  32. return Info{Path: p, IsDir: c == nil, Size: int64(len(c)), ModTime: 1}, nil
  33. }
  34. func (f *fakeBackend) List(p string) ([]Info, error) {
  35. f.mu.Lock()
  36. defer f.mu.Unlock()
  37. if c, ok := f.files[p]; !ok || c != nil {
  38. return nil, os.ErrNotExist
  39. }
  40. out := []Info{}
  41. for k, c := range f.files {
  42. if k != "/" && path.Dir(k) == p {
  43. out = append(out, Info{Path: k, IsDir: c == nil, Size: int64(len(c)), ModTime: 1})
  44. }
  45. }
  46. sort.Slice(out, func(i, j int) bool { return out[i].Path < out[j].Path })
  47. return out, nil
  48. }
  49. func (f *fakeBackend) Mkdir(p string, owner string) error {
  50. f.mu.Lock()
  51. defer f.mu.Unlock()
  52. for cur := p; cur != "/"; cur = path.Dir(cur) {
  53. if _, ok := f.files[cur]; !ok {
  54. f.files[cur] = nil
  55. }
  56. }
  57. return nil
  58. }
  59. func (f *fakeBackend) Remove(p string, recursive bool) error {
  60. f.mu.Lock()
  61. defer f.mu.Unlock()
  62. if _, ok := f.files[p]; !ok {
  63. return os.ErrNotExist
  64. }
  65. for k := range f.files {
  66. if k == p || (recursive && strings.HasPrefix(k, p+"/")) {
  67. delete(f.files, k)
  68. }
  69. }
  70. return nil
  71. }
  72. func (f *fakeBackend) Rename(o, n string) error {
  73. f.mu.Lock()
  74. defer f.mu.Unlock()
  75. moved := map[string][]byte{}
  76. for k, c := range f.files {
  77. if k == o || strings.HasPrefix(k, o+"/") {
  78. moved[n+k[len(o):]] = c
  79. delete(f.files, k)
  80. }
  81. }
  82. for k, c := range moved {
  83. f.files[k] = c
  84. }
  85. return nil
  86. }
  87. func (f *fakeBackend) OpenRead(p string) (io.ReadCloser, error) {
  88. f.mu.Lock()
  89. defer f.mu.Unlock()
  90. c, ok := f.files[p]
  91. if !ok || c == nil {
  92. return nil, os.ErrNotExist
  93. }
  94. return io.NopCloser(bytes.NewReader(c)), nil
  95. }
  96. func (f *fakeBackend) Write(p string, r io.Reader, owner string) error {
  97. data, err := io.ReadAll(r)
  98. if err != nil {
  99. return err
  100. }
  101. f.Mkdir(path.Dir(p), owner)
  102. f.mu.Lock()
  103. defer f.mu.Unlock()
  104. f.files[p] = data
  105. return nil
  106. }
  107. func TestNormalize(t *testing.T) {
  108. tests := map[string]string{
  109. "cluster:/a/../b": "/b",
  110. "cluster:a\\b": "/a/b",
  111. "": "/",
  112. "/x/": "/x",
  113. "y/z": "/y/z",
  114. }
  115. for in, want := range tests {
  116. if got := normalize(in); got != want {
  117. t.Errorf("normalize(%q)=%q want %q", in, got, want)
  118. }
  119. }
  120. }
  121. func TestClusterFileSystemRoundTrip(t *testing.T) {
  122. fb := newFake()
  123. c := New("cluster", fb)
  124. if c.Name() != "cluster" || c.Heartbeat() != nil {
  125. t.Errorf("name/heartbeat wrong")
  126. }
  127. if err := c.WriteStream("cluster:/docs/a.txt", strings.NewReader("hello"), 0644); err != nil {
  128. t.Fatalf("WriteStream: %v", err)
  129. }
  130. if err := c.WriteFile("/docs/sub/b.txt", []byte("bee"), 0644); err != nil {
  131. t.Fatalf("WriteFile: %v", err)
  132. }
  133. if !c.FileExists("/docs/a.txt") || c.FileExists("/docs/none") || !c.IsDir("/docs") || c.IsDir("/docs/a.txt") {
  134. t.Errorf("exists/isdir wrong")
  135. }
  136. if c.GetFileSize("/docs/a.txt") != 5 {
  137. t.Errorf("size wrong")
  138. }
  139. if mt, err := c.GetModTime("/docs/a.txt"); err != nil || mt != 1 {
  140. t.Errorf("modtime wrong: %d %v", mt, err)
  141. }
  142. data, err := c.ReadFile("/docs/a.txt")
  143. if err != nil || string(data) != "hello" {
  144. t.Errorf("ReadFile: %q %v", data, err)
  145. }
  146. r, err := c.ReadStream("/docs/sub/b.txt")
  147. if err != nil {
  148. t.Fatalf("ReadStream: %v", err)
  149. }
  150. got, _ := io.ReadAll(r)
  151. r.Close()
  152. if string(got) != "bee" {
  153. t.Errorf("ReadStream content wrong")
  154. }
  155. fi, err := c.Stat("/docs")
  156. if err != nil || !fi.IsDir() || fi.Name() != "docs" || fi.Mode()&os.ModeDir == 0 {
  157. t.Errorf("Stat dir: %+v %v", fi, err)
  158. }
  159. entries, err := c.ReadDir("/docs")
  160. if err != nil || len(entries) != 2 || entries[0].Name() != "a.txt" || !entries[1].IsDir() {
  161. t.Errorf("ReadDir: %+v %v", entries, err)
  162. }
  163. info, _ := entries[0].Info()
  164. if info.Size() != 5 {
  165. t.Errorf("DirEntry.Info wrong")
  166. }
  167. //Glob
  168. matches, err := c.Glob("/docs/*.txt")
  169. if err != nil || len(matches) != 1 || matches[0] != "/docs/a.txt" {
  170. t.Errorf("Glob: %v %v", matches, err)
  171. }
  172. matches, _ = c.Glob("/docs/*/*.txt")
  173. if len(matches) != 1 || matches[0] != "/docs/sub/b.txt" {
  174. t.Errorf("nested Glob: %v", matches)
  175. }
  176. //Walk
  177. seen := []string{}
  178. c.Walk("/docs", func(p string, fi os.FileInfo, err error) error {
  179. seen = append(seen, p)
  180. return nil
  181. })
  182. if len(seen) != 4 {
  183. t.Errorf("Walk visited %v", seen)
  184. }
  185. //Rename / Mkdir / Remove
  186. if err := c.Rename("/docs/a.txt", "/docs/c.txt"); err != nil || c.FileExists("/docs/a.txt") || !c.FileExists("/docs/c.txt") {
  187. t.Errorf("Rename failed: %v", err)
  188. }
  189. if err := c.MkdirAll("/deep/er/dir", 0755); err != nil || !c.IsDir("/deep/er") {
  190. t.Errorf("MkdirAll failed: %v", err)
  191. }
  192. if err := c.Remove("/docs/c.txt"); err != nil || c.FileExists("/docs/c.txt") {
  193. t.Errorf("Remove failed: %v", err)
  194. }
  195. if err := c.RemoveAll("/docs"); err != nil || c.FileExists("/docs/sub/b.txt") {
  196. t.Errorf("RemoveAll failed: %v", err)
  197. }
  198. //Virtual path translation
  199. if rp, _ := c.VirtualPathToRealPath("cluster:/photos/x.jpg", "toby"); rp != "/photos/x.jpg" {
  200. t.Errorf("VirtualPathToRealPath = %q", rp)
  201. }
  202. if vp, _ := c.RealPathToVirtualPath("/photos/x.jpg", "toby"); vp != "cluster:/photos/x.jpg" {
  203. t.Errorf("RealPathToVirtualPath = %q", vp)
  204. }
  205. if vp, _ := c.RealPathToVirtualPath(filepath.FromSlash("photos/y.jpg"), ""); vp != "cluster:/photos/y.jpg" {
  206. t.Errorf("RealPathToVirtualPath os-form = %q", vp)
  207. }
  208. //Unsupported handle operations
  209. if _, err := c.Open("/x"); err == nil {
  210. t.Errorf("Open should be unsupported")
  211. }
  212. if _, err := c.Create("/x"); err == nil {
  213. t.Errorf("Create should be unsupported")
  214. }
  215. fb.ready = false
  216. if c.Heartbeat() == nil {
  217. t.Errorf("Heartbeat should fail when backend not ready")
  218. }
  219. }
  220. /*
  221. Storage info
  222. A backend that can explain where a file is kept answers through the
  223. abstraction, so the File Manager properties dialog does not need to know
  224. that the cluster is involved. A backend that cannot say anything must
  225. fail cleanly instead of pretending the file is nowhere.
  226. */
  227. // infoBackend is a fakeBackend that also reports where a file is kept.
  228. type infoBackend struct {
  229. *fakeBackend
  230. info arozfs.StorageInfo
  231. err error
  232. last string
  233. }
  234. func (i *infoBackend) StorageInfo(logical string) (arozfs.StorageInfo, error) {
  235. i.last = logical
  236. if i.err != nil {
  237. return arozfs.StorageInfo{}, i.err
  238. }
  239. return i.info, nil
  240. }
  241. func TestStorageInfoFromBackend(t *testing.T) {
  242. backend := &infoBackend{fakeBackend: newFake(), info: arozfs.StorageInfo{
  243. Type: "cluster",
  244. Title: "Cluster",
  245. Items: []arozfs.StorageInfoItem{{Title: "NodeA", State: arozfs.StorageStateOK}},
  246. }}
  247. c := New("cluster", backend)
  248. //Any path form the file system accepts has to reach the backend as a
  249. //plain namespace path
  250. testcases := []struct{ given, wanted string }{
  251. {"cluster:/photos/a.jpg", "/photos/a.jpg"},
  252. {"/photos/a.jpg", "/photos/a.jpg"},
  253. {"photos/a.jpg", "/photos/a.jpg"},
  254. {"cluster:/", "/"},
  255. }
  256. for _, tc := range testcases {
  257. info, err := c.StorageInfo(tc.given)
  258. if err != nil {
  259. t.Fatalf("StorageInfo(%q) returned an error: %v", tc.given, err)
  260. }
  261. if backend.last != tc.wanted {
  262. t.Errorf("StorageInfo(%q) asked the backend for %q, wanted %q", tc.given, backend.last, tc.wanted)
  263. }
  264. if info.Type != "cluster" || len(info.Items) != 1 {
  265. t.Errorf("StorageInfo(%q) did not pass the backend answer through: %+v", tc.given, info)
  266. }
  267. }
  268. }
  269. func TestStorageInfoErrors(t *testing.T) {
  270. //A backend that cannot resolve the file passes its error on
  271. failing := &infoBackend{fakeBackend: newFake(), err: errors.New("no such record")}
  272. if _, err := New("cluster", failing).StorageInfo("/photos/a.jpg"); err == nil {
  273. t.Error("Expected the backend error to be returned")
  274. }
  275. //A backend with no idea where files are kept is not an error case worth
  276. //guessing at: it simply has nothing to report
  277. if _, err := New("cluster", newFake()).StorageInfo("/photos/a.jpg"); err == nil {
  278. t.Error("Expected an error from a backend that does not report storage info")
  279. }
  280. }
  281. // thumbBackend is a fakeBackend whose files have thumbnails.
  282. type thumbBackend struct {
  283. *fakeBackend
  284. keys map[string]string
  285. last string
  286. }
  287. func (b *thumbBackend) ThumbnailKey(logical string) (string, error) {
  288. b.last = logical
  289. key, ok := b.keys[logical]
  290. if !ok {
  291. return "", errors.New("no such record")
  292. }
  293. return key, nil
  294. }
  295. func (b *thumbBackend) RenderThumbnail(logical string) ([]byte, error) {
  296. b.last = logical
  297. if _, ok := b.keys[logical]; !ok {
  298. return nil, arozfs.ErrNoThumbnail
  299. }
  300. return []byte("image of " + logical), nil
  301. }
  302. func TestThumbnailFromBackend(t *testing.T) {
  303. backend := &thumbBackend{fakeBackend: newFake(), keys: map[string]string{"/v/a.mp4": "sha-a"}}
  304. var c interface{} = New("cluster", backend)
  305. r, ok := c.(arozfs.ThumbnailRenderer)
  306. if !ok {
  307. t.Fatal("ClusterFileSystem should be an arozfs.ThumbnailRenderer")
  308. }
  309. testcases := []struct {
  310. given string
  311. wantKey string
  312. wantErr bool
  313. }{
  314. {"cluster:/v/a.mp4", "sha-a", false},
  315. {"/v/a.mp4", "sha-a", false},
  316. {"v/a.mp4", "sha-a", false},
  317. {"/v/missing.mp4", "", true},
  318. }
  319. for _, tc := range testcases {
  320. key, err := r.ThumbnailKey(tc.given)
  321. if (err != nil) != tc.wantErr || key != tc.wantKey {
  322. t.Errorf("ThumbnailKey(%q) = %q, %v; wanted %q (error %v)", tc.given, key, err, tc.wantKey, tc.wantErr)
  323. }
  324. data, err := r.RenderThumbnail(tc.given)
  325. if tc.wantErr {
  326. if !errors.Is(err, arozfs.ErrNoThumbnail) {
  327. t.Errorf("RenderThumbnail(%q) should report ErrNoThumbnail, got %v", tc.given, err)
  328. }
  329. continue
  330. }
  331. if err != nil || string(data) != "image of /v/a.mp4" {
  332. t.Errorf("RenderThumbnail(%q) = %q, %v", tc.given, data, err)
  333. }
  334. if backend.last != "/v/a.mp4" {
  335. t.Errorf("RenderThumbnail(%q) asked the backend for %q", tc.given, backend.last)
  336. }
  337. }
  338. }
  339. func TestThumbnailWithoutBackendSupport(t *testing.T) {
  340. c := New("cluster", newFake())
  341. if _, err := c.ThumbnailKey("/v/a.mp4"); !errors.Is(err, arozfs.ErrNoThumbnail) {
  342. t.Errorf("ThumbnailKey on a plain backend should report ErrNoThumbnail, got %v", err)
  343. }
  344. if _, err := c.RenderThumbnail("/v/a.mp4"); !errors.Is(err, arozfs.ErrNoThumbnail) {
  345. t.Errorf("RenderThumbnail on a plain backend should report ErrNoThumbnail, got %v", err)
  346. }
  347. }