| // Copyright 2014 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package webdav |
| |
| import ( |
| "context" |
| "encoding/xml" |
| "fmt" |
| "io" |
| "os" |
| "path" |
| "path/filepath" |
| "reflect" |
| "runtime" |
| "sort" |
| "strconv" |
| "strings" |
| "testing" |
| ) |
| |
| func TestSlashClean(t *testing.T) { |
| testCases := []string{ |
| "", |
| ".", |
| "/", |
| "/./", |
| "//", |
| "//.", |
| "//a", |
| "/a", |
| "/a/b/c", |
| "/a//b/./../c/d/", |
| "a", |
| "a/b/c", |
| } |
| for _, tc := range testCases { |
| got := slashClean(tc) |
| want := path.Clean("/" + tc) |
| if got != want { |
| t.Errorf("tc=%q: got %q, want %q", tc, got, want) |
| } |
| } |
| } |
| |
| func TestDirResolve(t *testing.T) { |
| testCases := []struct { |
| dir, name, want string |
| }{ |
| {"/", "", "/"}, |
| {"/", "/", "/"}, |
| {"/", ".", "/"}, |
| {"/", "./a", "/a"}, |
| {"/", "..", "/"}, |
| {"/", "..", "/"}, |
| {"/", "../", "/"}, |
| {"/", "../.", "/"}, |
| {"/", "../a", "/a"}, |
| {"/", "../..", "/"}, |
| {"/", "../bar/a", "/bar/a"}, |
| {"/", "../baz/a", "/baz/a"}, |
| {"/", "...", "/..."}, |
| {"/", ".../a", "/.../a"}, |
| {"/", ".../..", "/"}, |
| {"/", "a", "/a"}, |
| {"/", "a/./b", "/a/b"}, |
| {"/", "a/../../b", "/b"}, |
| {"/", "a/../b", "/b"}, |
| {"/", "a/b", "/a/b"}, |
| {"/", "a/b/c/../../d", "/a/d"}, |
| {"/", "a/b/c/../../../d", "/d"}, |
| {"/", "a/b/c/../../../../d", "/d"}, |
| {"/", "a/b/c/d", "/a/b/c/d"}, |
| |
| {"/foo/bar", "", "/foo/bar"}, |
| {"/foo/bar", "/", "/foo/bar"}, |
| {"/foo/bar", ".", "/foo/bar"}, |
| {"/foo/bar", "./a", "/foo/bar/a"}, |
| {"/foo/bar", "..", "/foo/bar"}, |
| {"/foo/bar", "../", "/foo/bar"}, |
| {"/foo/bar", "../.", "/foo/bar"}, |
| {"/foo/bar", "../a", "/foo/bar/a"}, |
| {"/foo/bar", "../..", "/foo/bar"}, |
| {"/foo/bar", "../bar/a", "/foo/bar/bar/a"}, |
| {"/foo/bar", "../baz/a", "/foo/bar/baz/a"}, |
| {"/foo/bar", "...", "/foo/bar/..."}, |
| {"/foo/bar", ".../a", "/foo/bar/.../a"}, |
| {"/foo/bar", ".../..", "/foo/bar"}, |
| {"/foo/bar", "a", "/foo/bar/a"}, |
| {"/foo/bar", "a/./b", "/foo/bar/a/b"}, |
| {"/foo/bar", "a/../../b", "/foo/bar/b"}, |
| {"/foo/bar", "a/../b", "/foo/bar/b"}, |
| {"/foo/bar", "a/b", "/foo/bar/a/b"}, |
| {"/foo/bar", "a/b/c/../../d", "/foo/bar/a/d"}, |
| {"/foo/bar", "a/b/c/../../../d", "/foo/bar/d"}, |
| {"/foo/bar", "a/b/c/../../../../d", "/foo/bar/d"}, |
| {"/foo/bar", "a/b/c/d", "/foo/bar/a/b/c/d"}, |
| |
| {"/foo/bar/", "", "/foo/bar"}, |
| {"/foo/bar/", "/", "/foo/bar"}, |
| {"/foo/bar/", ".", "/foo/bar"}, |
| {"/foo/bar/", "./a", "/foo/bar/a"}, |
| {"/foo/bar/", "..", "/foo/bar"}, |
| |
| {"/foo//bar///", "", "/foo/bar"}, |
| {"/foo//bar///", "/", "/foo/bar"}, |
| {"/foo//bar///", ".", "/foo/bar"}, |
| {"/foo//bar///", "./a", "/foo/bar/a"}, |
| {"/foo//bar///", "..", "/foo/bar"}, |
| |
| {"/x/y/z", "ab/c\x00d/ef", ""}, |
| |
| {".", "", "."}, |
| {".", "/", "."}, |
| {".", ".", "."}, |
| {".", "./a", "a"}, |
| {".", "..", "."}, |
| {".", "..", "."}, |
| {".", "../", "."}, |
| {".", "../.", "."}, |
| {".", "../a", "a"}, |
| {".", "../..", "."}, |
| {".", "../bar/a", "bar/a"}, |
| {".", "../baz/a", "baz/a"}, |
| {".", "...", "..."}, |
| {".", ".../a", ".../a"}, |
| {".", ".../..", "."}, |
| {".", "a", "a"}, |
| {".", "a/./b", "a/b"}, |
| {".", "a/../../b", "b"}, |
| {".", "a/../b", "b"}, |
| {".", "a/b", "a/b"}, |
| {".", "a/b/c/../../d", "a/d"}, |
| {".", "a/b/c/../../../d", "d"}, |
| {".", "a/b/c/../../../../d", "d"}, |
| {".", "a/b/c/d", "a/b/c/d"}, |
| |
| {"", "", "."}, |
| {"", "/", "."}, |
| {"", ".", "."}, |
| {"", "./a", "a"}, |
| {"", "..", "."}, |
| } |
| |
| for _, tc := range testCases { |
| d := Dir(filepath.FromSlash(tc.dir)) |
| if got := filepath.ToSlash(d.resolve(tc.name)); got != tc.want { |
| t.Errorf("dir=%q, name=%q: got %q, want %q", tc.dir, tc.name, got, tc.want) |
| } |
| } |
| } |
| |
| func TestWalk(t *testing.T) { |
| type walkStep struct { |
| name, frag string |
| final bool |
| } |
| |
| testCases := []struct { |
| dir string |
| want []walkStep |
| }{ |
| {"", []walkStep{ |
| {"", "", true}, |
| }}, |
| {"/", []walkStep{ |
| {"", "", true}, |
| }}, |
| {"/a", []walkStep{ |
| {"", "a", true}, |
| }}, |
| {"/a/", []walkStep{ |
| {"", "a", true}, |
| }}, |
| {"/a/b", []walkStep{ |
| {"", "a", false}, |
| {"a", "b", true}, |
| }}, |
| {"/a/b/", []walkStep{ |
| {"", "a", false}, |
| {"a", "b", true}, |
| }}, |
| {"/a/b/c", []walkStep{ |
| {"", "a", false}, |
| {"a", "b", false}, |
| {"b", "c", true}, |
| }}, |
| // The following test case is the one mentioned explicitly |
| // in the method description. |
| {"/foo/bar/x", []walkStep{ |
| {"", "foo", false}, |
| {"foo", "bar", false}, |
| {"bar", "x", true}, |
| }}, |
| } |
| |
| ctx := context.Background() |
| |
| for _, tc := range testCases { |
| fs := NewMemFS().(*memFS) |
| |
| parts := strings.Split(tc.dir, "/") |
| for p := 2; p < len(parts); p++ { |
| d := strings.Join(parts[:p], "/") |
| if err := fs.Mkdir(ctx, d, 0666); err != nil { |
| t.Errorf("tc.dir=%q: mkdir: %q: %v", tc.dir, d, err) |
| } |
| } |
| |
| i, prevFrag := 0, "" |
| err := fs.walk("test", tc.dir, func(dir *memFSNode, frag string, final bool) error { |
| got := walkStep{ |
| name: prevFrag, |
| frag: frag, |
| final: final, |
| } |
| want := tc.want[i] |
| |
| if got != want { |
| return fmt.Errorf("got %+v, want %+v", got, want) |
| } |
| i, prevFrag = i+1, frag |
| return nil |
| }) |
| if err != nil { |
| t.Errorf("tc.dir=%q: %v", tc.dir, err) |
| } |
| } |
| } |
| |
| // find appends to ss the names of the named file and its children. It is |
| // analogous to the Unix find command. |
| // |
| // The returned strings are not guaranteed to be in any particular order. |
| func find(ctx context.Context, ss []string, fs FileSystem, name string) ([]string, error) { |
| stat, err := fs.Stat(ctx, name) |
| if err != nil { |
| return nil, err |
| } |
| ss = append(ss, name) |
| if stat.IsDir() { |
| f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0) |
| if err != nil { |
| return nil, err |
| } |
| defer f.Close() |
| children, err := f.Readdir(-1) |
| if err != nil { |
| return nil, err |
| } |
| for _, c := range children { |
| ss, err = find(ctx, ss, fs, path.Join(name, c.Name())) |
| if err != nil { |
| return nil, err |
| } |
| } |
| } |
| return ss, nil |
| } |
| |
| func testFS(t *testing.T, fs FileSystem) { |
| errStr := func(err error) string { |
| switch { |
| case os.IsExist(err): |
| return "errExist" |
| case os.IsNotExist(err): |
| return "errNotExist" |
| case err != nil: |
| return "err" |
| } |
| return "ok" |
| } |
| |
| // The non-"find" non-"stat" test cases should change the file system state. The |
| // indentation of the "find"s and "stat"s helps distinguish such test cases. |
| testCases := []string{ |
| " stat / want dir", |
| " stat /a want errNotExist", |
| " stat /d want errNotExist", |
| " stat /d/e want errNotExist", |
| "create /a A want ok", |
| " stat /a want 1", |
| "create /d/e EEE want errNotExist", |
| "mk-dir /a want errExist", |
| "mk-dir /d/m want errNotExist", |
| "mk-dir /d want ok", |
| " stat /d want dir", |
| "create /d/e EEE want ok", |
| " stat /d/e want 3", |
| " find / /a /d /d/e", |
| "create /d/f FFFF want ok", |
| "create /d/g GGGGGGG want ok", |
| "mk-dir /d/m want ok", |
| "mk-dir /d/m want errExist", |
| "create /d/m/p PPPPP want ok", |
| " stat /d/e want 3", |
| " stat /d/f want 4", |
| " stat /d/g want 7", |
| " stat /d/h want errNotExist", |
| " stat /d/m want dir", |
| " stat /d/m/p want 5", |
| " find / /a /d /d/e /d/f /d/g /d/m /d/m/p", |
| "rm-all /d want ok", |
| " stat /a want 1", |
| " stat /d want errNotExist", |
| " stat /d/e want errNotExist", |
| " stat /d/f want errNotExist", |
| " stat /d/g want errNotExist", |
| " stat /d/m want errNotExist", |
| " stat /d/m/p want errNotExist", |
| " find / /a", |
| "mk-dir /d/m want errNotExist", |
| "mk-dir /d want ok", |
| "create /d/f FFFF want ok", |
| "rm-all /d/f want ok", |
| "mk-dir /d/m want ok", |
| "rm-all /z want ok", |
| "rm-all / want err", |
| "create /b BB want ok", |
| " stat / want dir", |
| " stat /a want 1", |
| " stat /b want 2", |
| " stat /c want errNotExist", |
| " stat /d want dir", |
| " stat /d/m want dir", |
| " find / /a /b /d /d/m", |
| "move__ o=F /b /c want ok", |
| " stat /b want errNotExist", |
| " stat /c want 2", |
| " stat /d/m want dir", |
| " stat /d/n want errNotExist", |
| " find / /a /c /d /d/m", |
| "move__ o=F /d/m /d/n want ok", |
| "create /d/n/q QQQQ want ok", |
| " stat /d/m want errNotExist", |
| " stat /d/n want dir", |
| " stat /d/n/q want 4", |
| "move__ o=F /d /d/n/z want err", |
| "move__ o=T /c /d/n/q want ok", |
| " stat /c want errNotExist", |
| " stat /d/n/q want 2", |
| " find / /a /d /d/n /d/n/q", |
| "create /d/n/r RRRRR want ok", |
| "mk-dir /u want ok", |
| "mk-dir /u/v want ok", |
| "move__ o=F /d/n /u want errExist", |
| "create /t TTTTTT want ok", |
| "move__ o=F /d/n /t want errExist", |
| "rm-all /t want ok", |
| "move__ o=F /d/n /t want ok", |
| " stat /d want dir", |
| " stat /d/n want errNotExist", |
| " stat /d/n/r want errNotExist", |
| " stat /t want dir", |
| " stat /t/q want 2", |
| " stat /t/r want 5", |
| " find / /a /d /t /t/q /t/r /u /u/v", |
| "move__ o=F /t / want errExist", |
| "move__ o=T /t /u/v want ok", |
| " stat /u/v/r want 5", |
| "move__ o=F / /z want err", |
| " find / /a /d /u /u/v /u/v/q /u/v/r", |
| " stat /a want 1", |
| " stat /b want errNotExist", |
| " stat /c want errNotExist", |
| " stat /u/v/r want 5", |
| "copy__ o=F d=0 /a /b want ok", |
| "copy__ o=T d=0 /a /c want ok", |
| " stat /a want 1", |
| " stat /b want 1", |
| " stat /c want 1", |
| " stat /u/v/r want 5", |
| "copy__ o=F d=0 /u/v/r /b want errExist", |
| " stat /b want 1", |
| "copy__ o=T d=0 /u/v/r /b want ok", |
| " stat /a want 1", |
| " stat /b want 5", |
| " stat /u/v/r want 5", |
| "rm-all /a want ok", |
| "rm-all /b want ok", |
| "mk-dir /u/v/w want ok", |
| "create /u/v/w/s SSSSSSSS want ok", |
| " stat /d want dir", |
| " stat /d/x want errNotExist", |
| " stat /d/y want errNotExist", |
| " stat /u/v/r want 5", |
| " stat /u/v/w/s want 8", |
| " find / /c /d /u /u/v /u/v/q /u/v/r /u/v/w /u/v/w/s", |
| "copy__ o=T d=0 /u/v /d/x want ok", |
| "copy__ o=T d=∞ /u/v /d/y want ok", |
| "rm-all /u want ok", |
| " stat /d/x want dir", |
| " stat /d/x/q want errNotExist", |
| " stat /d/x/r want errNotExist", |
| " stat /d/x/w want errNotExist", |
| " stat /d/x/w/s want errNotExist", |
| " stat /d/y want dir", |
| " stat /d/y/q want 2", |
| " stat /d/y/r want 5", |
| " stat /d/y/w want dir", |
| " stat /d/y/w/s want 8", |
| " stat /u want errNotExist", |
| " find / /c /d /d/x /d/y /d/y/q /d/y/r /d/y/w /d/y/w/s", |
| "copy__ o=F d=∞ /d/y /d/x want errExist", |
| } |
| |
| ctx := context.Background() |
| |
| for i, tc := range testCases { |
| tc = strings.TrimSpace(tc) |
| j := strings.IndexByte(tc, ' ') |
| if j < 0 { |
| t.Fatalf("test case #%d %q: invalid command", i, tc) |
| } |
| op, arg := tc[:j], tc[j+1:] |
| |
| switch op { |
| default: |
| t.Fatalf("test case #%d %q: invalid operation %q", i, tc, op) |
| |
| case "create": |
| parts := strings.Split(arg, " ") |
| if len(parts) != 4 || parts[2] != "want" { |
| t.Fatalf("test case #%d %q: invalid write", i, tc) |
| } |
| f, opErr := fs.OpenFile(ctx, parts[0], os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) |
| if got := errStr(opErr); got != parts[3] { |
| t.Fatalf("test case #%d %q: OpenFile: got %q (%v), want %q", i, tc, got, opErr, parts[3]) |
| } |
| if f != nil { |
| if _, err := f.Write([]byte(parts[1])); err != nil { |
| t.Fatalf("test case #%d %q: Write: %v", i, tc, err) |
| } |
| if err := f.Close(); err != nil { |
| t.Fatalf("test case #%d %q: Close: %v", i, tc, err) |
| } |
| } |
| |
| case "find": |
| got, err := find(ctx, nil, fs, "/") |
| if err != nil { |
| t.Fatalf("test case #%d %q: find: %v", i, tc, err) |
| } |
| sort.Strings(got) |
| want := strings.Split(arg, " ") |
| if !reflect.DeepEqual(got, want) { |
| t.Fatalf("test case #%d %q:\ngot %s\nwant %s", i, tc, got, want) |
| } |
| |
| case "copy__", "mk-dir", "move__", "rm-all", "stat": |
| nParts := 3 |
| switch op { |
| case "copy__": |
| nParts = 6 |
| case "move__": |
| nParts = 5 |
| } |
| parts := strings.Split(arg, " ") |
| if len(parts) != nParts { |
| t.Fatalf("test case #%d %q: invalid %s", i, tc, op) |
| } |
| |
| got, opErr := "", error(nil) |
| switch op { |
| case "copy__": |
| depth := 0 |
| if parts[1] == "d=∞" { |
| depth = infiniteDepth |
| } |
| _, opErr = copyFiles(ctx, fs, parts[2], parts[3], parts[0] == "o=T", depth, 0) |
| case "mk-dir": |
| opErr = fs.Mkdir(ctx, parts[0], 0777) |
| case "move__": |
| _, opErr = moveFiles(ctx, fs, parts[1], parts[2], parts[0] == "o=T") |
| case "rm-all": |
| opErr = fs.RemoveAll(ctx, parts[0]) |
| case "stat": |
| var stat os.FileInfo |
| fileName := parts[0] |
| if stat, opErr = fs.Stat(ctx, fileName); opErr == nil { |
| if stat.IsDir() { |
| got = "dir" |
| } else { |
| got = strconv.Itoa(int(stat.Size())) |
| } |
| |
| if fileName == "/" { |
| // For a Dir FileSystem, the virtual file system root maps to a |
| // real file system name like "/tmp/webdav-test012345", which does |
| // not end with "/". We skip such cases. |
| } else if statName := stat.Name(); path.Base(fileName) != statName { |
| t.Fatalf("test case #%d %q: file name %q inconsistent with stat name %q", |
| i, tc, fileName, statName) |
| } |
| } |
| } |
| if got == "" { |
| got = errStr(opErr) |
| } |
| |
| if parts[len(parts)-2] != "want" { |
| t.Fatalf("test case #%d %q: invalid %s", i, tc, op) |
| } |
| if want := parts[len(parts)-1]; got != want { |
| t.Fatalf("test case #%d %q: got %q (%v), want %q", i, tc, got, opErr, want) |
| } |
| } |
| } |
| } |
| |
| func TestDir(t *testing.T) { |
| switch runtime.GOOS { |
| case "nacl": |
| t.Skip("see golang.org/issue/12004") |
| case "plan9": |
| t.Skip("see golang.org/issue/11453") |
| } |
| |
| td, err := os.MkdirTemp("", "webdav-test") |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer os.RemoveAll(td) |
| testFS(t, Dir(td)) |
| } |
| |
| func TestMemFS(t *testing.T) { |
| testFS(t, NewMemFS()) |
| } |
| |
| func TestMemFSRoot(t *testing.T) { |
| ctx := context.Background() |
| fs := NewMemFS() |
| for i := 0; i < 5; i++ { |
| stat, err := fs.Stat(ctx, "/") |
| if err != nil { |
| t.Fatalf("i=%d: Stat: %v", i, err) |
| } |
| if !stat.IsDir() { |
| t.Fatalf("i=%d: Stat.IsDir is false, want true", i) |
| } |
| |
| f, err := fs.OpenFile(ctx, "/", os.O_RDONLY, 0) |
| if err != nil { |
| t.Fatalf("i=%d: OpenFile: %v", i, err) |
| } |
| defer f.Close() |
| children, err := f.Readdir(-1) |
| if err != nil { |
| t.Fatalf("i=%d: Readdir: %v", i, err) |
| } |
| if len(children) != i { |
| t.Fatalf("i=%d: got %d children, want %d", i, len(children), i) |
| } |
| |
| if _, err := f.Write(make([]byte, 1)); err == nil { |
| t.Fatalf("i=%d: Write: got nil error, want non-nil", i) |
| } |
| |
| if err := fs.Mkdir(ctx, fmt.Sprintf("/dir%d", i), 0777); err != nil { |
| t.Fatalf("i=%d: Mkdir: %v", i, err) |
| } |
| } |
| } |
| |
| func TestMemFileReaddir(t *testing.T) { |
| ctx := context.Background() |
| fs := NewMemFS() |
| if err := fs.Mkdir(ctx, "/foo", 0777); err != nil { |
| t.Fatalf("Mkdir: %v", err) |
| } |
| readdir := func(count int) ([]os.FileInfo, error) { |
| f, err := fs.OpenFile(ctx, "/foo", os.O_RDONLY, 0) |
| if err != nil { |
| t.Fatalf("OpenFile: %v", err) |
| } |
| defer f.Close() |
| return f.Readdir(count) |
| } |
| if got, err := readdir(-1); len(got) != 0 || err != nil { |
| t.Fatalf("readdir(-1): got %d fileInfos with err=%v, want 0, <nil>", len(got), err) |
| } |
| if got, err := readdir(+1); len(got) != 0 || err != io.EOF { |
| t.Fatalf("readdir(+1): got %d fileInfos with err=%v, want 0, EOF", len(got), err) |
| } |
| } |
| |
| func TestMemFile(t *testing.T) { |
| testCases := []string{ |
| "wantData ", |
| "wantSize 0", |
| "write abc", |
| "wantData abc", |
| "write de", |
| "wantData abcde", |
| "wantSize 5", |
| "write 5*x", |
| "write 4*y+2*z", |
| "write 3*st", |
| "wantData abcdexxxxxyyyyzzststst", |
| "wantSize 22", |
| "seek set 4 want 4", |
| "write EFG", |
| "wantData abcdEFGxxxyyyyzzststst", |
| "wantSize 22", |
| "seek set 2 want 2", |
| "read cdEF", |
| "read Gx", |
| "seek cur 0 want 8", |
| "seek cur 2 want 10", |
| "seek cur -1 want 9", |
| "write J", |
| "wantData abcdEFGxxJyyyyzzststst", |
| "wantSize 22", |
| "seek cur -4 want 6", |
| "write ghijk", |
| "wantData abcdEFghijkyyyzzststst", |
| "wantSize 22", |
| "read yyyz", |
| "seek cur 0 want 15", |
| "write ", |
| "seek cur 0 want 15", |
| "read ", |
| "seek cur 0 want 15", |
| "seek end -3 want 19", |
| "write ZZ", |
| "wantData abcdEFghijkyyyzzstsZZt", |
| "wantSize 22", |
| "write 4*A", |
| "wantData abcdEFghijkyyyzzstsZZAAAA", |
| "wantSize 25", |
| "seek end 0 want 25", |
| "seek end -5 want 20", |
| "read Z+4*A", |
| "write 5*B", |
| "wantData abcdEFghijkyyyzzstsZZAAAABBBBB", |
| "wantSize 30", |
| "seek end 10 want 40", |
| "write C", |
| "wantData abcdEFghijkyyyzzstsZZAAAABBBBB..........C", |
| "wantSize 41", |
| "write D", |
| "wantData abcdEFghijkyyyzzstsZZAAAABBBBB..........CD", |
| "wantSize 42", |
| "seek set 43 want 43", |
| "write E", |
| "wantData abcdEFghijkyyyzzstsZZAAAABBBBB..........CD.E", |
| "wantSize 44", |
| "seek set 0 want 0", |
| "write 5*123456789_", |
| "wantData 123456789_123456789_123456789_123456789_123456789_", |
| "wantSize 50", |
| "seek cur 0 want 50", |
| "seek cur -99 want err", |
| } |
| |
| ctx := context.Background() |
| |
| const filename = "/foo" |
| fs := NewMemFS() |
| f, err := fs.OpenFile(ctx, filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) |
| if err != nil { |
| t.Fatalf("OpenFile: %v", err) |
| } |
| defer f.Close() |
| |
| for i, tc := range testCases { |
| j := strings.IndexByte(tc, ' ') |
| if j < 0 { |
| t.Fatalf("test case #%d %q: invalid command", i, tc) |
| } |
| op, arg := tc[:j], tc[j+1:] |
| |
| // Expand an arg like "3*a+2*b" to "aaabb". |
| parts := strings.Split(arg, "+") |
| for j, part := range parts { |
| if k := strings.IndexByte(part, '*'); k >= 0 { |
| repeatCount, repeatStr := part[:k], part[k+1:] |
| n, err := strconv.Atoi(repeatCount) |
| if err != nil { |
| t.Fatalf("test case #%d %q: invalid repeat count %q", i, tc, repeatCount) |
| } |
| parts[j] = strings.Repeat(repeatStr, n) |
| } |
| } |
| arg = strings.Join(parts, "") |
| |
| switch op { |
| default: |
| t.Fatalf("test case #%d %q: invalid operation %q", i, tc, op) |
| |
| case "read": |
| buf := make([]byte, len(arg)) |
| if _, err := io.ReadFull(f, buf); err != nil { |
| t.Fatalf("test case #%d %q: ReadFull: %v", i, tc, err) |
| } |
| if got := string(buf); got != arg { |
| t.Fatalf("test case #%d %q:\ngot %q\nwant %q", i, tc, got, arg) |
| } |
| |
| case "seek": |
| parts := strings.Split(arg, " ") |
| if len(parts) != 4 { |
| t.Fatalf("test case #%d %q: invalid seek", i, tc) |
| } |
| |
| whence := 0 |
| switch parts[0] { |
| default: |
| t.Fatalf("test case #%d %q: invalid seek whence", i, tc) |
| case "set": |
| whence = io.SeekStart |
| case "cur": |
| whence = io.SeekCurrent |
| case "end": |
| whence = io.SeekEnd |
| } |
| offset, err := strconv.Atoi(parts[1]) |
| if err != nil { |
| t.Fatalf("test case #%d %q: invalid offset %q", i, tc, parts[1]) |
| } |
| |
| if parts[2] != "want" { |
| t.Fatalf("test case #%d %q: invalid seek", i, tc) |
| } |
| if parts[3] == "err" { |
| _, err := f.Seek(int64(offset), whence) |
| if err == nil { |
| t.Fatalf("test case #%d %q: Seek returned nil error, want non-nil", i, tc) |
| } |
| } else { |
| got, err := f.Seek(int64(offset), whence) |
| if err != nil { |
| t.Fatalf("test case #%d %q: Seek: %v", i, tc, err) |
| } |
| want, err := strconv.Atoi(parts[3]) |
| if err != nil { |
| t.Fatalf("test case #%d %q: invalid want %q", i, tc, parts[3]) |
| } |
| if got != int64(want) { |
| t.Fatalf("test case #%d %q: got %d, want %d", i, tc, got, want) |
| } |
| } |
| |
| case "write": |
| n, err := f.Write([]byte(arg)) |
| if err != nil { |
| t.Fatalf("test case #%d %q: write: %v", i, tc, err) |
| } |
| if n != len(arg) { |
| t.Fatalf("test case #%d %q: write returned %d bytes, want %d", i, tc, n, len(arg)) |
| } |
| |
| case "wantData": |
| g, err := fs.OpenFile(ctx, filename, os.O_RDONLY, 0666) |
| if err != nil { |
| t.Fatalf("test case #%d %q: OpenFile: %v", i, tc, err) |
| } |
| gotBytes, err := io.ReadAll(g) |
| if err != nil { |
| t.Fatalf("test case #%d %q: ReadAll: %v", i, tc, err) |
| } |
| for i, c := range gotBytes { |
| if c == '\x00' { |
| gotBytes[i] = '.' |
| } |
| } |
| got := string(gotBytes) |
| if got != arg { |
| t.Fatalf("test case #%d %q:\ngot %q\nwant %q", i, tc, got, arg) |
| } |
| if err := g.Close(); err != nil { |
| t.Fatalf("test case #%d %q: Close: %v", i, tc, err) |
| } |
| |
| case "wantSize": |
| n, err := strconv.Atoi(arg) |
| if err != nil { |
| t.Fatalf("test case #%d %q: invalid size %q", i, tc, arg) |
| } |
| fi, err := fs.Stat(ctx, filename) |
| if err != nil { |
| t.Fatalf("test case #%d %q: Stat: %v", i, tc, err) |
| } |
| if got, want := fi.Size(), int64(n); got != want { |
| t.Fatalf("test case #%d %q: got %d, want %d", i, tc, got, want) |
| } |
| } |
| } |
| } |
| |
| // TestMemFileWriteAllocs tests that writing N consecutive 1KiB chunks to a |
| // memFile doesn't allocate a new buffer for each of those N times. Otherwise, |
| // calling io.Copy(aMemFile, src) is likely to have quadratic complexity. |
| func TestMemFileWriteAllocs(t *testing.T) { |
| if runtime.Compiler == "gccgo" { |
| t.Skip("gccgo allocates here") |
| } |
| ctx := context.Background() |
| fs := NewMemFS() |
| f, err := fs.OpenFile(ctx, "/xxx", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) |
| if err != nil { |
| t.Fatalf("OpenFile: %v", err) |
| } |
| defer f.Close() |
| |
| xxx := make([]byte, 1024) |
| for i := range xxx { |
| xxx[i] = 'x' |
| } |
| |
| a := testing.AllocsPerRun(100, func() { |
| f.Write(xxx) |
| }) |
| // AllocsPerRun returns an integral value, so we compare the rounded-down |
| // number to zero. |
| if a > 0 { |
| t.Fatalf("%v allocs per run, want 0", a) |
| } |
| } |
| |
| func BenchmarkMemFileWrite(b *testing.B) { |
| ctx := context.Background() |
| fs := NewMemFS() |
| xxx := make([]byte, 1024) |
| for i := range xxx { |
| xxx[i] = 'x' |
| } |
| |
| b.ResetTimer() |
| for i := 0; i < b.N; i++ { |
| f, err := fs.OpenFile(ctx, "/xxx", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) |
| if err != nil { |
| b.Fatalf("OpenFile: %v", err) |
| } |
| for j := 0; j < 100; j++ { |
| f.Write(xxx) |
| } |
| if err := f.Close(); err != nil { |
| b.Fatalf("Close: %v", err) |
| } |
| if err := fs.RemoveAll(ctx, "/xxx"); err != nil { |
| b.Fatalf("RemoveAll: %v", err) |
| } |
| } |
| } |
| |
| func TestCopyMoveProps(t *testing.T) { |
| ctx := context.Background() |
| fs := NewMemFS() |
| create := func(name string) error { |
| f, err := fs.OpenFile(ctx, name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) |
| if err != nil { |
| return err |
| } |
| _, wErr := f.Write([]byte("contents")) |
| cErr := f.Close() |
| if wErr != nil { |
| return wErr |
| } |
| return cErr |
| } |
| patch := func(name string, patches ...Proppatch) error { |
| f, err := fs.OpenFile(ctx, name, os.O_RDWR, 0666) |
| if err != nil { |
| return err |
| } |
| _, pErr := f.(DeadPropsHolder).Patch(patches) |
| cErr := f.Close() |
| if pErr != nil { |
| return pErr |
| } |
| return cErr |
| } |
| props := func(name string) (map[xml.Name]Property, error) { |
| f, err := fs.OpenFile(ctx, name, os.O_RDWR, 0666) |
| if err != nil { |
| return nil, err |
| } |
| m, pErr := f.(DeadPropsHolder).DeadProps() |
| cErr := f.Close() |
| if pErr != nil { |
| return nil, pErr |
| } |
| if cErr != nil { |
| return nil, cErr |
| } |
| return m, nil |
| } |
| |
| p0 := Property{ |
| XMLName: xml.Name{Space: "x:", Local: "boat"}, |
| InnerXML: []byte("pea-green"), |
| } |
| p1 := Property{ |
| XMLName: xml.Name{Space: "x:", Local: "ring"}, |
| InnerXML: []byte("1 shilling"), |
| } |
| p2 := Property{ |
| XMLName: xml.Name{Space: "x:", Local: "spoon"}, |
| InnerXML: []byte("runcible"), |
| } |
| p3 := Property{ |
| XMLName: xml.Name{Space: "x:", Local: "moon"}, |
| InnerXML: []byte("light"), |
| } |
| |
| if err := create("/src"); err != nil { |
| t.Fatalf("create /src: %v", err) |
| } |
| if err := patch("/src", Proppatch{Props: []Property{p0, p1}}); err != nil { |
| t.Fatalf("patch /src +p0 +p1: %v", err) |
| } |
| if _, err := copyFiles(ctx, fs, "/src", "/tmp", true, infiniteDepth, 0); err != nil { |
| t.Fatalf("copyFiles /src /tmp: %v", err) |
| } |
| if _, err := moveFiles(ctx, fs, "/tmp", "/dst", true); err != nil { |
| t.Fatalf("moveFiles /tmp /dst: %v", err) |
| } |
| if err := patch("/src", Proppatch{Props: []Property{p0}, Remove: true}); err != nil { |
| t.Fatalf("patch /src -p0: %v", err) |
| } |
| if err := patch("/src", Proppatch{Props: []Property{p2}}); err != nil { |
| t.Fatalf("patch /src +p2: %v", err) |
| } |
| if err := patch("/dst", Proppatch{Props: []Property{p1}, Remove: true}); err != nil { |
| t.Fatalf("patch /dst -p1: %v", err) |
| } |
| if err := patch("/dst", Proppatch{Props: []Property{p3}}); err != nil { |
| t.Fatalf("patch /dst +p3: %v", err) |
| } |
| |
| gotSrc, err := props("/src") |
| if err != nil { |
| t.Fatalf("props /src: %v", err) |
| } |
| wantSrc := map[xml.Name]Property{ |
| p1.XMLName: p1, |
| p2.XMLName: p2, |
| } |
| if !reflect.DeepEqual(gotSrc, wantSrc) { |
| t.Fatalf("props /src:\ngot %v\nwant %v", gotSrc, wantSrc) |
| } |
| |
| gotDst, err := props("/dst") |
| if err != nil { |
| t.Fatalf("props /dst: %v", err) |
| } |
| wantDst := map[xml.Name]Property{ |
| p0.XMLName: p0, |
| p3.XMLName: p3, |
| } |
| if !reflect.DeepEqual(gotDst, wantDst) { |
| t.Fatalf("props /dst:\ngot %v\nwant %v", gotDst, wantDst) |
| } |
| } |
| |
| func TestWalkFS(t *testing.T) { |
| testCases := []struct { |
| desc string |
| buildfs []string |
| startAt string |
| depth int |
| walkFn filepath.WalkFunc |
| want []string |
| }{{ |
| "just root", |
| []string{}, |
| "/", |
| infiniteDepth, |
| nil, |
| []string{ |
| "/", |
| }, |
| }, { |
| "infinite walk from root", |
| []string{ |
| "mkdir /a", |
| "mkdir /a/b", |
| "touch /a/b/c", |
| "mkdir /a/d", |
| "mkdir /e", |
| "touch /f", |
| }, |
| "/", |
| infiniteDepth, |
| nil, |
| []string{ |
| "/", |
| "/a", |
| "/a/b", |
| "/a/b/c", |
| "/a/d", |
| "/e", |
| "/f", |
| }, |
| }, { |
| "infinite walk from subdir", |
| []string{ |
| "mkdir /a", |
| "mkdir /a/b", |
| "touch /a/b/c", |
| "mkdir /a/d", |
| "mkdir /e", |
| "touch /f", |
| }, |
| "/a", |
| infiniteDepth, |
| nil, |
| []string{ |
| "/a", |
| "/a/b", |
| "/a/b/c", |
| "/a/d", |
| }, |
| }, { |
| "depth 1 walk from root", |
| []string{ |
| "mkdir /a", |
| "mkdir /a/b", |
| "touch /a/b/c", |
| "mkdir /a/d", |
| "mkdir /e", |
| "touch /f", |
| }, |
| "/", |
| 1, |
| nil, |
| []string{ |
| "/", |
| "/a", |
| "/e", |
| "/f", |
| }, |
| }, { |
| "depth 1 walk from subdir", |
| []string{ |
| "mkdir /a", |
| "mkdir /a/b", |
| "touch /a/b/c", |
| "mkdir /a/b/g", |
| "mkdir /a/b/g/h", |
| "touch /a/b/g/i", |
| "touch /a/b/g/h/j", |
| }, |
| "/a/b", |
| 1, |
| nil, |
| []string{ |
| "/a/b", |
| "/a/b/c", |
| "/a/b/g", |
| }, |
| }, { |
| "depth 0 walk from subdir", |
| []string{ |
| "mkdir /a", |
| "mkdir /a/b", |
| "touch /a/b/c", |
| "mkdir /a/b/g", |
| "mkdir /a/b/g/h", |
| "touch /a/b/g/i", |
| "touch /a/b/g/h/j", |
| }, |
| "/a/b", |
| 0, |
| nil, |
| []string{ |
| "/a/b", |
| }, |
| }, { |
| "infinite walk from file", |
| []string{ |
| "mkdir /a", |
| "touch /a/b", |
| "touch /a/c", |
| }, |
| "/a/b", |
| 0, |
| nil, |
| []string{ |
| "/a/b", |
| }, |
| }, { |
| "infinite walk with skipped subdir", |
| []string{ |
| "mkdir /a", |
| "mkdir /a/b", |
| "touch /a/b/c", |
| "mkdir /a/b/g", |
| "mkdir /a/b/g/h", |
| "touch /a/b/g/i", |
| "touch /a/b/g/h/j", |
| "touch /a/b/z", |
| }, |
| "/", |
| infiniteDepth, |
| func(path string, info os.FileInfo, err error) error { |
| if path == "/a/b/g" { |
| return filepath.SkipDir |
| } |
| return nil |
| }, |
| []string{ |
| "/", |
| "/a", |
| "/a/b", |
| "/a/b/c", |
| "/a/b/z", |
| }, |
| }} |
| ctx := context.Background() |
| for _, tc := range testCases { |
| fs, err := buildTestFS(tc.buildfs) |
| if err != nil { |
| t.Fatalf("%s: cannot create test filesystem: %v", tc.desc, err) |
| } |
| var got []string |
| traceFn := func(path string, info os.FileInfo, err error) error { |
| if tc.walkFn != nil { |
| err = tc.walkFn(path, info, err) |
| if err != nil { |
| return err |
| } |
| } |
| got = append(got, path) |
| return nil |
| } |
| fi, err := fs.Stat(ctx, tc.startAt) |
| if err != nil { |
| t.Fatalf("%s: cannot stat: %v", tc.desc, err) |
| } |
| err = walkFS(ctx, fs, tc.depth, tc.startAt, fi, traceFn) |
| if err != nil { |
| t.Errorf("%s:\ngot error %v, want nil", tc.desc, err) |
| continue |
| } |
| sort.Strings(got) |
| sort.Strings(tc.want) |
| if !reflect.DeepEqual(got, tc.want) { |
| t.Errorf("%s:\ngot %q\nwant %q", tc.desc, got, tc.want) |
| continue |
| } |
| } |
| } |
| |
| func buildTestFS(buildfs []string) (FileSystem, error) { |
| // TODO: Could this be merged with the build logic in TestFS? |
| |
| ctx := context.Background() |
| fs := NewMemFS() |
| for _, b := range buildfs { |
| op := strings.Split(b, " ") |
| switch op[0] { |
| case "mkdir": |
| err := fs.Mkdir(ctx, op[1], os.ModeDir|0777) |
| if err != nil { |
| return nil, err |
| } |
| case "touch": |
| f, err := fs.OpenFile(ctx, op[1], os.O_RDWR|os.O_CREATE, 0666) |
| if err != nil { |
| return nil, err |
| } |
| f.Close() |
| case "write": |
| f, err := fs.OpenFile(ctx, op[1], os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) |
| if err != nil { |
| return nil, err |
| } |
| _, err = f.Write([]byte(op[2])) |
| f.Close() |
| if err != nil { |
| return nil, err |
| } |
| default: |
| return nil, fmt.Errorf("unknown file operation %q", op[0]) |
| } |
| } |
| return fs, nil |
| } |