| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382 |
- package clusterfs
- import (
- "bytes"
- "errors"
- "io"
- "os"
- "path"
- "path/filepath"
- "sort"
- "strings"
- "sync"
- "testing"
- "imuslab.com/arozos/mod/filesystem/arozfs"
- )
- // fakeBackend is an in-memory namespace.
- type fakeBackend struct {
- mu sync.Mutex
- files map[string][]byte // path -> content, nil value = directory
- ready bool
- }
- func newFake() *fakeBackend {
- return &fakeBackend{files: map[string][]byte{"/": nil}, ready: true}
- }
- func (f *fakeBackend) Ready() bool { return f.ready }
- func (f *fakeBackend) Stat(p string) (Info, error) {
- f.mu.Lock()
- defer f.mu.Unlock()
- c, ok := f.files[p]
- if !ok {
- return Info{}, os.ErrNotExist
- }
- return Info{Path: p, IsDir: c == nil, Size: int64(len(c)), ModTime: 1}, nil
- }
- func (f *fakeBackend) List(p string) ([]Info, error) {
- f.mu.Lock()
- defer f.mu.Unlock()
- if c, ok := f.files[p]; !ok || c != nil {
- return nil, os.ErrNotExist
- }
- out := []Info{}
- for k, c := range f.files {
- if k != "/" && path.Dir(k) == p {
- out = append(out, Info{Path: k, IsDir: c == nil, Size: int64(len(c)), ModTime: 1})
- }
- }
- sort.Slice(out, func(i, j int) bool { return out[i].Path < out[j].Path })
- return out, nil
- }
- func (f *fakeBackend) Mkdir(p string, owner string) error {
- f.mu.Lock()
- defer f.mu.Unlock()
- for cur := p; cur != "/"; cur = path.Dir(cur) {
- if _, ok := f.files[cur]; !ok {
- f.files[cur] = nil
- }
- }
- return nil
- }
- func (f *fakeBackend) Remove(p string, recursive bool) error {
- f.mu.Lock()
- defer f.mu.Unlock()
- if _, ok := f.files[p]; !ok {
- return os.ErrNotExist
- }
- for k := range f.files {
- if k == p || (recursive && strings.HasPrefix(k, p+"/")) {
- delete(f.files, k)
- }
- }
- return nil
- }
- func (f *fakeBackend) Rename(o, n string) error {
- f.mu.Lock()
- defer f.mu.Unlock()
- moved := map[string][]byte{}
- for k, c := range f.files {
- if k == o || strings.HasPrefix(k, o+"/") {
- moved[n+k[len(o):]] = c
- delete(f.files, k)
- }
- }
- for k, c := range moved {
- f.files[k] = c
- }
- return nil
- }
- func (f *fakeBackend) OpenRead(p string) (io.ReadCloser, error) {
- f.mu.Lock()
- defer f.mu.Unlock()
- c, ok := f.files[p]
- if !ok || c == nil {
- return nil, os.ErrNotExist
- }
- return io.NopCloser(bytes.NewReader(c)), nil
- }
- func (f *fakeBackend) Write(p string, r io.Reader, owner string) error {
- data, err := io.ReadAll(r)
- if err != nil {
- return err
- }
- f.Mkdir(path.Dir(p), owner)
- f.mu.Lock()
- defer f.mu.Unlock()
- f.files[p] = data
- return nil
- }
- func TestNormalize(t *testing.T) {
- tests := map[string]string{
- "cluster:/a/../b": "/b",
- "cluster:a\\b": "/a/b",
- "": "/",
- "/x/": "/x",
- "y/z": "/y/z",
- }
- for in, want := range tests {
- if got := normalize(in); got != want {
- t.Errorf("normalize(%q)=%q want %q", in, got, want)
- }
- }
- }
- func TestClusterFileSystemRoundTrip(t *testing.T) {
- fb := newFake()
- c := New("cluster", fb)
- if c.Name() != "cluster" || c.Heartbeat() != nil {
- t.Errorf("name/heartbeat wrong")
- }
- if err := c.WriteStream("cluster:/docs/a.txt", strings.NewReader("hello"), 0644); err != nil {
- t.Fatalf("WriteStream: %v", err)
- }
- if err := c.WriteFile("/docs/sub/b.txt", []byte("bee"), 0644); err != nil {
- t.Fatalf("WriteFile: %v", err)
- }
- if !c.FileExists("/docs/a.txt") || c.FileExists("/docs/none") || !c.IsDir("/docs") || c.IsDir("/docs/a.txt") {
- t.Errorf("exists/isdir wrong")
- }
- if c.GetFileSize("/docs/a.txt") != 5 {
- t.Errorf("size wrong")
- }
- if mt, err := c.GetModTime("/docs/a.txt"); err != nil || mt != 1 {
- t.Errorf("modtime wrong: %d %v", mt, err)
- }
- data, err := c.ReadFile("/docs/a.txt")
- if err != nil || string(data) != "hello" {
- t.Errorf("ReadFile: %q %v", data, err)
- }
- r, err := c.ReadStream("/docs/sub/b.txt")
- if err != nil {
- t.Fatalf("ReadStream: %v", err)
- }
- got, _ := io.ReadAll(r)
- r.Close()
- if string(got) != "bee" {
- t.Errorf("ReadStream content wrong")
- }
- fi, err := c.Stat("/docs")
- if err != nil || !fi.IsDir() || fi.Name() != "docs" || fi.Mode()&os.ModeDir == 0 {
- t.Errorf("Stat dir: %+v %v", fi, err)
- }
- entries, err := c.ReadDir("/docs")
- if err != nil || len(entries) != 2 || entries[0].Name() != "a.txt" || !entries[1].IsDir() {
- t.Errorf("ReadDir: %+v %v", entries, err)
- }
- info, _ := entries[0].Info()
- if info.Size() != 5 {
- t.Errorf("DirEntry.Info wrong")
- }
- //Glob
- matches, err := c.Glob("/docs/*.txt")
- if err != nil || len(matches) != 1 || matches[0] != "/docs/a.txt" {
- t.Errorf("Glob: %v %v", matches, err)
- }
- matches, _ = c.Glob("/docs/*/*.txt")
- if len(matches) != 1 || matches[0] != "/docs/sub/b.txt" {
- t.Errorf("nested Glob: %v", matches)
- }
- //Walk
- seen := []string{}
- c.Walk("/docs", func(p string, fi os.FileInfo, err error) error {
- seen = append(seen, p)
- return nil
- })
- if len(seen) != 4 {
- t.Errorf("Walk visited %v", seen)
- }
- //Rename / Mkdir / Remove
- if err := c.Rename("/docs/a.txt", "/docs/c.txt"); err != nil || c.FileExists("/docs/a.txt") || !c.FileExists("/docs/c.txt") {
- t.Errorf("Rename failed: %v", err)
- }
- if err := c.MkdirAll("/deep/er/dir", 0755); err != nil || !c.IsDir("/deep/er") {
- t.Errorf("MkdirAll failed: %v", err)
- }
- if err := c.Remove("/docs/c.txt"); err != nil || c.FileExists("/docs/c.txt") {
- t.Errorf("Remove failed: %v", err)
- }
- if err := c.RemoveAll("/docs"); err != nil || c.FileExists("/docs/sub/b.txt") {
- t.Errorf("RemoveAll failed: %v", err)
- }
- //Virtual path translation
- if rp, _ := c.VirtualPathToRealPath("cluster:/photos/x.jpg", "toby"); rp != "/photos/x.jpg" {
- t.Errorf("VirtualPathToRealPath = %q", rp)
- }
- if vp, _ := c.RealPathToVirtualPath("/photos/x.jpg", "toby"); vp != "cluster:/photos/x.jpg" {
- t.Errorf("RealPathToVirtualPath = %q", vp)
- }
- if vp, _ := c.RealPathToVirtualPath(filepath.FromSlash("photos/y.jpg"), ""); vp != "cluster:/photos/y.jpg" {
- t.Errorf("RealPathToVirtualPath os-form = %q", vp)
- }
- //Unsupported handle operations
- if _, err := c.Open("/x"); err == nil {
- t.Errorf("Open should be unsupported")
- }
- if _, err := c.Create("/x"); err == nil {
- t.Errorf("Create should be unsupported")
- }
- fb.ready = false
- if c.Heartbeat() == nil {
- t.Errorf("Heartbeat should fail when backend not ready")
- }
- }
- /*
- Storage info
- A backend that can explain where a file is kept answers through the
- abstraction, so the File Manager properties dialog does not need to know
- that the cluster is involved. A backend that cannot say anything must
- fail cleanly instead of pretending the file is nowhere.
- */
- // infoBackend is a fakeBackend that also reports where a file is kept.
- type infoBackend struct {
- *fakeBackend
- info arozfs.StorageInfo
- err error
- last string
- }
- func (i *infoBackend) StorageInfo(logical string) (arozfs.StorageInfo, error) {
- i.last = logical
- if i.err != nil {
- return arozfs.StorageInfo{}, i.err
- }
- return i.info, nil
- }
- func TestStorageInfoFromBackend(t *testing.T) {
- backend := &infoBackend{fakeBackend: newFake(), info: arozfs.StorageInfo{
- Type: "cluster",
- Title: "Cluster",
- Items: []arozfs.StorageInfoItem{{Title: "NodeA", State: arozfs.StorageStateOK}},
- }}
- c := New("cluster", backend)
- //Any path form the file system accepts has to reach the backend as a
- //plain namespace path
- testcases := []struct{ given, wanted string }{
- {"cluster:/photos/a.jpg", "/photos/a.jpg"},
- {"/photos/a.jpg", "/photos/a.jpg"},
- {"photos/a.jpg", "/photos/a.jpg"},
- {"cluster:/", "/"},
- }
- for _, tc := range testcases {
- info, err := c.StorageInfo(tc.given)
- if err != nil {
- t.Fatalf("StorageInfo(%q) returned an error: %v", tc.given, err)
- }
- if backend.last != tc.wanted {
- t.Errorf("StorageInfo(%q) asked the backend for %q, wanted %q", tc.given, backend.last, tc.wanted)
- }
- if info.Type != "cluster" || len(info.Items) != 1 {
- t.Errorf("StorageInfo(%q) did not pass the backend answer through: %+v", tc.given, info)
- }
- }
- }
- func TestStorageInfoErrors(t *testing.T) {
- //A backend that cannot resolve the file passes its error on
- failing := &infoBackend{fakeBackend: newFake(), err: errors.New("no such record")}
- if _, err := New("cluster", failing).StorageInfo("/photos/a.jpg"); err == nil {
- t.Error("Expected the backend error to be returned")
- }
- //A backend with no idea where files are kept is not an error case worth
- //guessing at: it simply has nothing to report
- if _, err := New("cluster", newFake()).StorageInfo("/photos/a.jpg"); err == nil {
- t.Error("Expected an error from a backend that does not report storage info")
- }
- }
- // thumbBackend is a fakeBackend whose files have thumbnails.
- type thumbBackend struct {
- *fakeBackend
- keys map[string]string
- last string
- }
- func (b *thumbBackend) ThumbnailKey(logical string) (string, error) {
- b.last = logical
- key, ok := b.keys[logical]
- if !ok {
- return "", errors.New("no such record")
- }
- return key, nil
- }
- func (b *thumbBackend) RenderThumbnail(logical string) ([]byte, error) {
- b.last = logical
- if _, ok := b.keys[logical]; !ok {
- return nil, arozfs.ErrNoThumbnail
- }
- return []byte("image of " + logical), nil
- }
- func TestThumbnailFromBackend(t *testing.T) {
- backend := &thumbBackend{fakeBackend: newFake(), keys: map[string]string{"/v/a.mp4": "sha-a"}}
- var c interface{} = New("cluster", backend)
- r, ok := c.(arozfs.ThumbnailRenderer)
- if !ok {
- t.Fatal("ClusterFileSystem should be an arozfs.ThumbnailRenderer")
- }
- testcases := []struct {
- given string
- wantKey string
- wantErr bool
- }{
- {"cluster:/v/a.mp4", "sha-a", false},
- {"/v/a.mp4", "sha-a", false},
- {"v/a.mp4", "sha-a", false},
- {"/v/missing.mp4", "", true},
- }
- for _, tc := range testcases {
- key, err := r.ThumbnailKey(tc.given)
- if (err != nil) != tc.wantErr || key != tc.wantKey {
- t.Errorf("ThumbnailKey(%q) = %q, %v; wanted %q (error %v)", tc.given, key, err, tc.wantKey, tc.wantErr)
- }
- data, err := r.RenderThumbnail(tc.given)
- if tc.wantErr {
- if !errors.Is(err, arozfs.ErrNoThumbnail) {
- t.Errorf("RenderThumbnail(%q) should report ErrNoThumbnail, got %v", tc.given, err)
- }
- continue
- }
- if err != nil || string(data) != "image of /v/a.mp4" {
- t.Errorf("RenderThumbnail(%q) = %q, %v", tc.given, data, err)
- }
- if backend.last != "/v/a.mp4" {
- t.Errorf("RenderThumbnail(%q) asked the backend for %q", tc.given, backend.last)
- }
- }
- }
- func TestThumbnailWithoutBackendSupport(t *testing.T) {
- c := New("cluster", newFake())
- if _, err := c.ThumbnailKey("/v/a.mp4"); !errors.Is(err, arozfs.ErrNoThumbnail) {
- t.Errorf("ThumbnailKey on a plain backend should report ErrNoThumbnail, got %v", err)
- }
- if _, err := c.RenderThumbnail("/v/a.mp4"); !errors.Is(err, arozfs.ErrNoThumbnail) {
- t.Errorf("RenderThumbnail on a plain backend should report ErrNoThumbnail, got %v", err)
- }
- }
|