History

cmd/gitannex: Port unit tests to fstest

This enables the unit tests to run on any given backend, via the `-remote` flag, e.g. `go test -v ./cmd/gitannex/... -remote dropbox:`. We should also port the gitannex e2e tests at some point.

Dan McArdle committed 2 years ago

Commit be33e281 parent 00100903 files · +347 −190Browse files

Unified · Split

3 files
  1. cmd/gitannex/gitannex.go +26 −13
  2. cmd/gitannex/gitannex_test.go +311 −172
  3. cmd/gitannex/layout.go +10 −5
cmd/gitannex/gitannex.go+26−13
@@ -7,7 +7,7 @@ · 6 unmodified lines
77 // (Tracked in [issue #7625].)
88 //
99 // 1. ✅ Minimal support for the [external special remote protocol]. Tested on
10−// "local" and "drive" backends.
10+// "local", "drive", and "dropbox" backends.
1111 // 2. Add support for the ASYNC protocol extension. This may improve performance.
1212 // 3. Support the [simple export interface]. This will enable `git-annex
1313 // export` functionality.
@@ -33,6 +33,7 @@ import ( · 19 unmodified lines
3333 "github.com/rclone/rclone/cmd"
3434 "github.com/rclone/rclone/fs"
3535 "github.com/rclone/rclone/fs/cache"
36+ "github.com/rclone/rclone/fs/config"
3637 "github.com/rclone/rclone/fs/operations"
3738 "github.com/spf13/cobra"
3839 )
@@ -267,22 +268,34 @@ func (s *server) handleInitRemote() error { · 228 unmodified lines
267268 return fmt.Errorf("failed to get configs: %w", err)
268269 }
269270
270− remoteRootFs, err := cache.Get(context.TODO(), fmt.Sprintf("%s:", s.configRcloneRemoteName))
271− if err != nil {
272− s.sendMsg("INITREMOTE-FAILURE failed to open root directory of rclone remote")
273− return fmt.Errorf("failed to open root directory of rclone remote: %w", err)
274− }
271+ // Explicitly check that a remote with the given name exists. If we just
272+ // relied on `cache.Get()` to return `fs.ErrorNotFoundInConfigFile`, this
273+ // function would incorrectly succeed when the given remote name is actually
274+ // a file path.
275+ //
276+ // The :local: backend does not correspond to a remote named by the rclone
277+ // config, but is permitted to enable testing. Technically, a user might hit
278+ // this code path, but it would be a strange choice because git-annex
279+ // natively supports a "directory" special remote.
275280
276− if !remoteRootFs.Features().CanHaveEmptyDirectories {
277− s.sendMsg("INITREMOTE-FAILURE this rclone remote does not support empty directories")
278− return fmt.Errorf("rclone remote does not support empty directories")
279− }
281+ trimmedName := strings.TrimSuffix(s.configRcloneRemoteName, ":")
280282
281− if err := operations.Mkdir(context.TODO(), remoteRootFs, s.configPrefix); err != nil {
282− s.sendMsg("INITREMOTE-FAILURE failed to mkdir")
283− return fmt.Errorf("failed to mkdir: %w", err)
283+ if s.configRcloneRemoteName != ":local" {
284+ var remoteExists bool
285+ for _, remoteName := range config.FileSections() {
286+ if remoteName == trimmedName {
287+ remoteExists = true
288+ break
289+ }
290+ }
291+ if !remoteExists {
292+ s.sendMsg("INITREMOTE-FAILURE remote does not exist: " + s.configRcloneRemoteName)
293+ return fmt.Errorf("remote does not exist: %s", s.configRcloneRemoteName)
294+ }
284295 }
285296
297+ s.configRcloneRemoteName = trimmedName + ":"
298+
286299 s.sendMsg("INITREMOTE-SUCCESS")
287300 return nil
288301 }
cmd/gitannex/gitannex_test.go+311−172
@@ -2,25 +2,24 @@ package gitannex · 1 unmodified line
22
33 import (
44 "bufio"
5− "crypto/sha256"
5+ "context"
66 "fmt"
77 "io"
88 "os"
99 "path/filepath"
1010 "regexp"
11+ "runtime"
1112 "strings"
1213 "sync"
1314 "testing"
15+ "time"
1416
15− // Without this import, the local filesystem backend would be unavailable.
16− // It looks unused, but the act of importing it runs its `init()` function.
17− _ "github.com/rclone/rclone/backend/local"
17+ // Without this import, the various backends would be unavailable. It looks
18+ // unused, but the act of importing runs the package's `init()` function.
19+ _ "github.com/rclone/rclone/backend/all"
1820
19− "github.com/rclone/rclone/fs"
20− "github.com/rclone/rclone/fs/cache"
21− "github.com/rclone/rclone/fs/config"
22− "github.com/rclone/rclone/fs/config/configfile"
23− "github.com/rclone/rclone/fstest/mockfs"
21+ "github.com/rclone/rclone/fs/fspath"
22+ "github.com/rclone/rclone/fstest"
2423
2524 "github.com/stretchr/testify/assert"
2625 "github.com/stretchr/testify/require"
@@ -256,9 +255,9 @@ type testState struct { · 229 unmodified lines
256255 mockStdinW *io.PipeWriter
257256 mockStdoutReader *bufio.Reader
258257
259− localFsDir string
260− configPath string
261− remoteName string
258+ fstestRun *fstest.Run
259+ remoteName string
260+ remotePrefix string
262261 }
263262
264263 func makeTestState(t *testing.T) testState {
@@ -276,6 +275,10 @@ func makeTestState(t *testing.T) testState { · 11 unmodified lines
276275 }
277276 }
278277
278+func (h *testState) requireRemoteIsEmpty() {
279+ h.fstestRun.CheckRemoteItems(h.t)
280+}
281+
279282 func (h *testState) requireReadLineExact(line string) {
280283 receivedLine, err := h.mockStdoutReader.ReadString('\n')
281284 require.NoError(h.t, err)
@@ -296,21 +299,106 @@ func (h *testState) requireWriteLine(line string) { · 14 unmodified lines
296299 // Preconfigure the handle. This enables the calling test to skip the PREPARE
297300 // handshake.
298301 func (h *testState) preconfigureServer() {
299− h.server.configPrefix = h.localFsDir
300302 h.server.configRcloneRemoteName = h.remoteName
303+ h.server.configPrefix = h.remotePrefix
301304 h.server.configRcloneLayout = string(layoutModeNodir)
302305 h.server.configsDone = true
303306 }
304307
305−// getUniqueRemoteName returns a valid remote name derived from the given test's
306−// name. This is necessary because when a test registers a second remote with
307−// the same name, the original remote appears to take precedence. This function
308−// is injective, so each test gets a unique remote name. Returned strings
309−// contain no spaces.
310−func getUniqueRemoteName(t *testing.T) string {
311− // Using sha256 as a hack to ensure injectivity without adding a global
312− // variable.
313− return fmt.Sprintf("remote-%x", sha256.Sum256([]byte(t.Name())))
308+// Drop-in replacement for `filepath.Rel()` that works around a Windows-specific
309+// quirk when one of the paths begins with `\\?\` or `//?/`. It seems that
310+// fstest gives us paths with this prefix on Windows, which throws a wrench in
311+// the gitannex tests that need to construct relative paths from absolute paths.
312+// For a demonstration, see `TestWindowsFilepathRelQuirk` below.
313+//
314+// The `\\?\` prefix tells Windows APIs to pass strings unmodified to the
315+// filesystem without additional parsing [1]. Our workaround is roughly to add
316+// the prefix to whichever parameter doesn't have it (when the OS is Windows).
317+// I'm not sure this generalizes, but it works for the the kinds of inputs we're
318+// throwing at it.
319+//
320+// [1]: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#win32-file-namespaces
321+func relativeFilepathWorkaround(basepath, targpath string) (string, error) {
322+ if runtime.GOOS != "windows" {
323+ return filepath.Rel(basepath, targpath)
324+ }
325+ // Canonicalize paths to use backslashes.
326+ basepath = filepath.Clean(basepath)
327+ targpath = filepath.Clean(targpath)
328+
329+ const winFilePrefixDisableStringParsing = `\\?\`
330+ baseHasPrefix := strings.HasPrefix(basepath, winFilePrefixDisableStringParsing)
331+ targHasPrefix := strings.HasPrefix(targpath, winFilePrefixDisableStringParsing)
332+
333+ if baseHasPrefix && !targHasPrefix {
334+ targpath = winFilePrefixDisableStringParsing + targpath
335+ }
336+ if !baseHasPrefix && targHasPrefix {
337+ basepath = winFilePrefixDisableStringParsing + basepath
338+ }
339+ return filepath.Rel(basepath, targpath)
340+}
341+
342+func TestWindowsFilepathRelQuirk(t *testing.T) {
343+ if runtime.GOOS != "windows" {
344+ t.Skip()
345+ }
346+
347+ t.Run("filepathRelQuirk", func(t *testing.T) {
348+ var err error
349+
350+ _, err = filepath.Rel(`C:\foo`, `\\?\C:\foo\bar`)
351+ require.Error(t, err)
352+
353+ _, err = filepath.Rel(`C:/foo`, `//?/C:/foo/bar`)
354+ require.Error(t, err)
355+
356+ _, err = filepath.Rel(`\\?\C:\foo`, `C:\foo\bar`)
357+ require.Error(t, err)
358+
359+ _, err = filepath.Rel(`//?/C:/foo`, `C:/foo/bar`)
360+ require.Error(t, err)
361+
362+ path, err := filepath.Rel(`\\?\C:\foo`, `\\?\C:\foo\bar`)
363+ require.NoError(t, err)
364+ require.Equal(t, path, `bar`)
365+
366+ path, err = filepath.Rel(`//?/C:/foo`, `//?/C:/foo/bar`)
367+ require.NoError(t, err)
368+ require.Equal(t, path, `bar`)
369+ })
370+
371+ t.Run("fstestAndTempDirHaveDifferentPrefixes", func(t *testing.T) {
372+ r := fstest.NewRun(t)
373+ p := r.Flocal.Root()
374+ require.True(t, strings.HasPrefix(p, `//?/`))
375+
376+ tempDir := t.TempDir()
377+ require.False(t, strings.HasPrefix(tempDir, `//?/`))
378+ require.False(t, strings.HasPrefix(tempDir, `\\?\`))
379+ })
380+
381+ t.Run("workaroundWorks", func(t *testing.T) {
382+ path, err := relativeFilepathWorkaround(`C:\foo`, `\\?\C:\foo\bar`)
383+ require.NoError(t, err)
384+ require.Equal(t, path, "bar")
385+
386+ path, err = relativeFilepathWorkaround(`C:/foo`, `//?/C:/foo/bar`)
387+ require.NoError(t, err)
388+ require.Equal(t, path, "bar")
389+
390+ path, err = relativeFilepathWorkaround(`\\?\C:\foo`, `C:\foo\bar`)
391+ require.NoError(t, err)
392+ require.Equal(t, path, `bar`)
393+
394+ path, err = relativeFilepathWorkaround(`//?/C:/foo`, `C:/foo/bar`)
395+ require.NoError(t, err)
396+ require.Equal(t, path, `bar`)
397+
398+ path, err = relativeFilepathWorkaround(`\\?\C:\foo`, `\\?\C:\foo\bar`)
399+ require.NoError(t, err)
400+ require.Equal(t, path, `bar`)
401+ })
314402 }
315403
316404 type testCase struct {
@@ -319,8 +407,8 @@ type testCase struct { · 2 unmodified lines
319407 expectedError string
320408 }
321409
322−// These test cases run against the "local" backend.
323−var localBackendTestCases = []testCase{
410+// These test cases run against a backend selected by the `-remote` flag.
411+var fstestTestCases = []testCase{
324412 {
325413 label: "HandlesInit",
326414 testProtocolFunc: func(t *testing.T, h *testState) {
@@ -377,17 +465,85 @@ var localBackendTestCases = []testCase{ · 50 unmodified lines
377465 h.requireReadLineExact("GETCONFIG rcloneremotename")
378466 h.requireWriteLine("VALUE " + h.remoteName)
379467 h.requireReadLineExact("GETCONFIG rcloneprefix")
380− h.requireWriteLine("VALUE " + h.localFsDir)
468+ h.requireWriteLine("VALUE " + h.remotePrefix)
381469 h.requireReadLineExact("GETCONFIG rclonelayout")
382470 h.requireWriteLine("VALUE foo")
383471 h.requireReadLineExact("PREPARE-SUCCESS")
384472
385473 require.Equal(t, h.server.configRcloneRemoteName, h.remoteName)
386− require.Equal(t, h.server.configPrefix, h.localFsDir)
474+ require.Equal(t, h.server.configPrefix, h.remotePrefix)
475+ require.True(t, h.server.configsDone)
476+
477+ require.NoError(t, h.mockStdinW.Close())
478+ },
479+ },
480+ {
481+ label: "HandlesPrepareWithNonexistentRemote",
482+ testProtocolFunc: func(t *testing.T, h *testState) {
483+ h.requireReadLineExact("VERSION 1")
484+ h.requireWriteLine("EXTENSIONS INFO") // Advertise that we support the INFO extension
485+ h.requireReadLineExact("EXTENSIONS")
486+
487+ if !h.server.extensionInfo {
488+ t.Errorf("expected INFO extension to be enabled")
489+ return
490+ }
491+
492+ h.requireWriteLine("PREPARE")
493+ h.requireReadLineExact("GETCONFIG rcloneremotename")
494+ h.requireWriteLine("VALUE thisRemoteDoesNotExist")
495+ h.requireReadLineExact("GETCONFIG rcloneprefix")
496+ h.requireWriteLine("VALUE " + h.remotePrefix)
497+ h.requireReadLineExact("GETCONFIG rclonelayout")
498+ h.requireWriteLine("VALUE foo")
499+ h.requireReadLineExact("PREPARE-SUCCESS")
500+
501+ require.Equal(t, h.server.configRcloneRemoteName, "thisRemoteDoesNotExist")
502+ require.Equal(t, h.server.configPrefix, h.remotePrefix)
503+ require.True(t, h.server.configsDone)
504+
505+ h.requireWriteLine("INITREMOTE")
506+ h.requireReadLineExact("INITREMOTE-FAILURE remote does not exist: thisRemoteDoesNotExist")
507+
508+ require.NoError(t, h.mockStdinW.Close())
509+ },
510+ expectedError: "remote does not exist: thisRemoteDoesNotExist",
511+ },
512+ {
513+ label: "HandlesPrepareWithPathAsRemote",
514+ testProtocolFunc: func(t *testing.T, h *testState) {
515+ h.requireReadLineExact("VERSION 1")
516+ h.requireWriteLine("EXTENSIONS INFO") // Advertise that we support the INFO extension
517+ h.requireReadLineExact("EXTENSIONS")
518+
519+ if !h.server.extensionInfo {
520+ t.Errorf("expected INFO extension to be enabled")
521+ return
522+ }
523+
524+ h.requireWriteLine("PREPARE")
525+ h.requireReadLineExact("GETCONFIG rcloneremotename")
526+ h.requireWriteLine("VALUE " + h.remotePrefix)
527+ h.requireReadLineExact("GETCONFIG rcloneprefix")
528+ h.requireWriteLine("VALUE /foo")
529+ h.requireReadLineExact("GETCONFIG rclonelayout")
530+ h.requireWriteLine("VALUE foo")
531+ h.requireReadLineExact("PREPARE-SUCCESS")
532+
533+ require.Equal(t, h.server.configRcloneRemoteName, h.remotePrefix)
534+ require.Equal(t, h.server.configPrefix, "/foo")
387535 require.True(t, h.server.configsDone)
388536
537+ h.requireWriteLine("INITREMOTE")
538+
539+ require.Regexp(t,
540+ regexp.MustCompile("^INITREMOTE-FAILURE remote does not exist: "),
541+ h.requireReadLine(),
542+ )
543+
389544 require.NoError(t, h.mockStdinW.Close())
390545 },
546+ expectedError: "remote does not exist:",
391547 },
392548 {
393549 label: "HandlesPrepareWithSynonyms",
@@ -409,13 +565,13 @@ var localBackendTestCases = []testCase{ · 15 unmodified lines
409565 h.requireWriteLine("VALUE " + h.remoteName)
410566
411567 h.requireReadLineExact("GETCONFIG rcloneprefix")
412− h.requireWriteLine("VALUE " + h.localFsDir)
568+ h.requireWriteLine("VALUE " + h.remotePrefix)
413569 h.requireReadLineExact("GETCONFIG rclonelayout")
414570 h.requireWriteLine("VALUE foo")
415571 h.requireReadLineExact("PREPARE-SUCCESS")
416572
417573 require.Equal(t, h.server.configRcloneRemoteName, h.remoteName)
418− require.Equal(t, h.server.configPrefix, h.localFsDir)
574+ require.Equal(t, h.server.configPrefix, h.remotePrefix)
419575 require.True(t, h.server.configsDone)
420576
421577 require.NoError(t, h.mockStdinW.Close())
@@ -437,12 +593,12 @@ var localBackendTestCases = []testCase{ · 15 unmodified lines
437593 h.requireReadLineExact("GETCONFIG rcloneremotename")
438594
439595 remoteNameWithSpaces := fmt.Sprintf(" %s ", h.remoteName)
440− localFsDirWithSpaces := fmt.Sprintf(" %s\t", h.localFsDir)
596+ prefixWithWhitespace := fmt.Sprintf(" %s\t", h.remotePrefix)
441597
442598 h.requireWriteLine(fmt.Sprintf("VALUE %s", remoteNameWithSpaces))
443599
444600 h.requireReadLineExact("GETCONFIG rcloneprefix")
445− h.requireWriteLine(fmt.Sprintf("VALUE %s", localFsDirWithSpaces))
601+ h.requireWriteLine(fmt.Sprintf("VALUE %s", prefixWithWhitespace))
446602
447603 h.requireReadLineExact("GETCONFIG rclonelayout")
448604 h.requireWriteLine("VALUE")
@@ -452,7 +608,7 @@ var localBackendTestCases = []testCase{ · 3 unmodified lines
452608 h.requireReadLineExact("PREPARE-SUCCESS")
453609
454610 require.Equal(t, h.server.configRcloneRemoteName, remoteNameWithSpaces)
455− require.Equal(t, h.server.configPrefix, localFsDirWithSpaces)
611+ require.Equal(t, h.server.configPrefix, prefixWithWhitespace)
456612 require.True(t, h.server.configsDone)
457613
458614 require.NoError(t, h.mockStdinW.Close())
@@ -639,20 +795,25 @@ var localBackendTestCases = []testCase{ · 180 unmodified lines
639795 h.requireReadLineExact("INITREMOTE-SUCCESS")
640796
641797 // Create temp file for transfer with an absolute path.
642− fileToTransfer := filepath.Join(t.TempDir(), "file.txt")
643− require.NoError(t, os.WriteFile(fileToTransfer, []byte("HELLO"), 0600))
644− require.FileExists(t, fileToTransfer)
645− require.True(t, filepath.IsAbs(fileToTransfer))
798+ item := h.fstestRun.WriteFile("file.txt", "HELLO", time.Now())
799+ absPath := filepath.Join(h.fstestRun.Flocal.Root(), item.Path)
800+ require.True(t, filepath.IsAbs(absPath))
646801
647802 // Specify an absolute path to transfer.
648− h.requireWriteLine("TRANSFER STORE KeyAbsolute " + fileToTransfer)
803+ h.requireWriteLine("TRANSFER STORE KeyAbsolute " + absPath)
649804 h.requireReadLineExact("TRANSFER-SUCCESS STORE KeyAbsolute")
650− require.FileExists(t, filepath.Join(h.localFsDir, "KeyAbsolute"))
805+
806+ // Check that the file was transferred.
807+ remoteItem := fstest.NewItem("KeyAbsolute", "HELLO", item.ModTime)
808+ h.fstestRun.CheckRemoteItems(t, remoteItem)
651809
652810 // Transfer the same absolute path a second time, but with a different key.
653− h.requireWriteLine("TRANSFER STORE KeyAbsolute2 " + fileToTransfer)
811+ h.requireWriteLine("TRANSFER STORE KeyAbsolute2 " + absPath)
654812 h.requireReadLineExact("TRANSFER-SUCCESS STORE KeyAbsolute2")
655− require.FileExists(t, filepath.Join(h.localFsDir, "KeyAbsolute2"))
813+
814+ // Check that the same file was transferred to a new name.
815+ remoteItem2 := fstest.NewItem("KeyAbsolute2", "HELLO", item.ModTime)
816+ h.fstestRun.CheckRemoteItems(t, remoteItem, remoteItem2)
656817
657818 h.requireWriteLine("CHECKPRESENT KeyAbsolute2")
658819 h.requireReadLineExact("CHECKPRESENT-SUCCESS KeyAbsolute2")
@@ -668,30 +829,36 @@ var localBackendTestCases = []testCase{ · 9 unmodified lines
668829 {
669830 label: "TransferStoreRelative",
670831 testProtocolFunc: func(t *testing.T, h *testState) {
671− h.preconfigureServer()
672−
673832 // Save the current working directory so we can restore it when this
674833 // test ends.
675834 cwd, err := os.Getwd()
676835 require.NoError(t, err)
677836
678− require.NoError(t, os.Chdir(t.TempDir()))
837+ tempDir := t.TempDir()
838+
839+ require.NoError(t, os.Chdir(tempDir))
679840 t.Cleanup(func() { require.NoError(t, os.Chdir(cwd)) })
680841
842+ h.preconfigureServer()
843+
681844 h.requireReadLineExact("VERSION 1")
682845 h.requireWriteLine("INITREMOTE")
683846 h.requireReadLineExact("INITREMOTE-SUCCESS")
684847
685− // Create temp file for transfer with a relative path.
686− fileToTransfer := "file.txt"
687− require.NoError(t, os.WriteFile(fileToTransfer, []byte("HELLO"), 0600))
688− require.FileExists(t, fileToTransfer)
689− require.False(t, filepath.IsAbs(fileToTransfer))
848+ item := h.fstestRun.WriteFile("file.txt", "HELLO", time.Now())
849+ absPath := filepath.Join(h.fstestRun.Flocal.Root(), item.Path)
850+
851+ relativePath, err := relativeFilepathWorkaround(tempDir, absPath)
852+ require.NoError(t, err)
853+ require.False(t, filepath.IsAbs(relativePath))
854+ require.FileExists(t, relativePath)
690855
691856 // Specify a relative path to transfer.
692− h.requireWriteLine("TRANSFER STORE KeyRelative " + fileToTransfer)
857+ h.requireWriteLine("TRANSFER STORE KeyRelative " + relativePath)
693858 h.requireReadLineExact("TRANSFER-SUCCESS STORE KeyRelative")
694− require.FileExists(t, filepath.Join(h.localFsDir, "KeyRelative"))
859+
860+ remoteItem := fstest.NewItem("KeyRelative", "HELLO", item.ModTime)
861+ h.fstestRun.CheckRemoteItems(t, remoteItem)
695862
696863 h.requireWriteLine("CHECKPRESENT KeyRelative")
697864 h.requireReadLineExact("CHECKPRESENT-SUCCESS KeyRelative")
@@ -710,7 +877,8 @@ var localBackendTestCases = []testCase{ · 12 unmodified lines
710877 cwd, err := os.Getwd()
711878 require.NoError(t, err)
712879
713− require.NoError(t, os.Chdir(t.TempDir()))
880+ tempDir := t.TempDir()
881+ require.NoError(t, os.Chdir(tempDir))
714882 t.Cleanup(func() { require.NoError(t, os.Chdir(cwd)) })
715883
716884 h.preconfigureServer()
@@ -720,15 +888,19 @@ var localBackendTestCases = []testCase{ · 3 unmodified lines
720888 h.requireReadLineExact("INITREMOTE-SUCCESS")
721889
722890 // Create temp file for transfer.
723− fileToTransfer := "filename with spaces.txt"
724− require.NoError(t, os.WriteFile(fileToTransfer, []byte("HELLO"), 0600))
725− require.FileExists(t, fileToTransfer)
726− require.False(t, filepath.IsAbs(fileToTransfer))
891+ item := h.fstestRun.WriteFile("filename with spaces.txt", "HELLO", time.Now())
892+ absPath := filepath.Join(h.fstestRun.Flocal.Root(), item.Path)
893+ relativePath, err := relativeFilepathWorkaround(tempDir, absPath)
894+ require.NoError(t, err)
895+ require.False(t, filepath.IsAbs(relativePath))
896+ require.FileExists(t, relativePath)
727897
728898 // Specify a relative path to transfer.
729− h.requireWriteLine("TRANSFER STORE KeyRelative " + fileToTransfer)
899+ h.requireWriteLine("TRANSFER STORE KeyRelative " + relativePath)
730900 h.requireReadLineExact("TRANSFER-SUCCESS STORE KeyRelative")
731− require.FileExists(t, filepath.Join(h.localFsDir, "KeyRelative"))
901+
902+ remoteItem := fstest.NewItem("KeyRelative", "HELLO", item.ModTime)
903+ h.fstestRun.CheckRemoteItems(t, remoteItem)
732904
733905 h.requireWriteLine("CHECKPRESENT KeyRelative")
734906 h.requireReadLineExact("CHECKPRESENT-SUCCESS KeyRelative")
@@ -745,8 +917,9 @@ var localBackendTestCases = []testCase{ · 10 unmodified lines
745917 h.preconfigureServer()
746918
747919 // Create temp file for transfer.
748− fileToTransfer := filepath.Join(t.TempDir(), "file.txt")
749− require.NoError(t, os.WriteFile(fileToTransfer, []byte("HELLO"), 0600))
920+ item := h.fstestRun.WriteFile("file.txt", "HELLO", time.Now())
921+ absPath := filepath.Join(h.fstestRun.Flocal.Root(), item.Path)
922+ require.True(t, filepath.IsAbs(absPath))
750923
751924 h.requireReadLineExact("VERSION 1")
752925 h.requireWriteLine("INITREMOTE")
@@ -756,10 +929,11 @@ var localBackendTestCases = []testCase{ · 3 unmodified lines
756929 h.requireReadLineExact("CHECKPRESENT-FAILURE KeyThatDoesNotExist")
757930
758931 // Specify an absolute path to transfer.
759− require.True(t, filepath.IsAbs(fileToTransfer))
760− h.requireWriteLine("TRANSFER STORE KeyAbsolute " + fileToTransfer)
932+ h.requireWriteLine("TRANSFER STORE KeyAbsolute " + absPath)
761933 h.requireReadLineExact("TRANSFER-SUCCESS STORE KeyAbsolute")
762− require.FileExists(t, filepath.Join(h.localFsDir, "KeyAbsolute"))
934+
935+ remoteItem := fstest.NewItem("KeyAbsolute", "HELLO", item.ModTime)
936+ h.fstestRun.CheckRemoteItems(t, remoteItem)
763937
764938 require.NoError(t, h.mockStdinW.Close())
765939 },
@@ -781,8 +955,9 @@ var localBackendTestCases = []testCase{ · 15 unmodified lines
781955 h.preconfigureServer()
782956
783957 // Create temp file for transfer.
784− fileToTransfer := filepath.Join(t.TempDir(), "file.txt")
785− require.NoError(t, os.WriteFile(fileToTransfer, []byte("HELLO"), 0600))
958+ item := h.fstestRun.WriteFile("file.txt", "HELLO", time.Now())
959+ absPath := filepath.Join(h.fstestRun.Flocal.Root(), item.Path)
960+ require.True(t, filepath.IsAbs(absPath))
786961
787962 h.requireReadLineExact("VERSION 1")
788963 h.requireWriteLine("INITREMOTE")
@@ -791,9 +966,11 @@ var localBackendTestCases = []testCase{ · 2 unmodified lines
791966 h.requireWriteLine("CHECKPRESENT foo")
792967 h.requireReadLineExact("CHECKPRESENT-FAILURE foo")
793968
794− h.requireWriteLine("TRANSFER STORE foo " + fileToTransfer)
969+ h.requireWriteLine("TRANSFER STORE foo " + absPath)
795970 h.requireReadLineExact("TRANSFER-SUCCESS STORE foo")
796− require.FileExists(t, filepath.Join(h.localFsDir, "foo"))
971+
972+ remoteItem := fstest.NewItem("foo", "HELLO", item.ModTime)
973+ h.fstestRun.CheckRemoteItems(t, remoteItem)
797974
798975 h.requireWriteLine("CHECKPRESENT foo")
799976 h.requireReadLineExact("CHECKPRESENT-SUCCESS foo")
@@ -807,8 +984,9 @@ var localBackendTestCases = []testCase{ · 7 unmodified lines
807984 h.preconfigureServer()
808985
809986 // Create temp file for transfer.
810− fileToTransfer := filepath.Join(t.TempDir(), "file.txt")
811− require.NoError(t, os.WriteFile(fileToTransfer, []byte("HELLO"), 0600))
987+ item := h.fstestRun.WriteFile("file.txt", "HELLO", time.Now())
988+ absPath := filepath.Join(h.fstestRun.Flocal.Root(), item.Path)
989+ require.True(t, filepath.IsAbs(absPath))
812990
813991 h.requireReadLineExact("VERSION 1")
814992 h.requireWriteLine("INITREMOTE")
@@ -817,10 +995,11 @@ var localBackendTestCases = []testCase{ · 2 unmodified lines
817995 realisticKey := "SHA256E-s1048576--7ba87e06b9b7903cfbaf4a38736766c161e3e7b42f06fe57f040aa410a8f0701.this-is-a-test-key"
818996
819997 // Specify an absolute path to transfer.
820− require.True(t, filepath.IsAbs(fileToTransfer))
821− h.requireWriteLine(fmt.Sprintf("TRANSFER STORE %s %s", realisticKey, fileToTransfer))
998+ h.requireWriteLine(fmt.Sprintf("TRANSFER STORE %s %s", realisticKey, absPath))
822999 h.requireReadLineExact("TRANSFER-SUCCESS STORE " + realisticKey)
823− require.FileExists(t, filepath.Join(h.localFsDir, realisticKey))
1000+
1001+ remoteItem := fstest.NewItem(realisticKey, "HELLO", item.ModTime)
1002+ h.fstestRun.CheckRemoteItems(t, remoteItem)
8241003
8251004 h.requireWriteLine("CHECKPRESENT " + realisticKey)
8261005 h.requireReadLineExact("CHECKPRESENT-SUCCESS " + realisticKey)
@@ -849,27 +1028,36 @@ var localBackendTestCases = []testCase{ · 22 unmodified lines
8491028 h.preconfigureServer()
8501029
8511030 // Create temp file for transfer.
852− fileToTransfer := filepath.Join(t.TempDir(), "file.txt")
853− require.NoError(t, os.WriteFile(fileToTransfer, []byte("HELLO"), 0600))
1031+ item := h.fstestRun.WriteFile("file.txt", "HELLO", time.Now())
1032+ absPath := filepath.Join(h.fstestRun.Flocal.Root(), item.Path)
1033+ require.True(t, filepath.IsAbs(absPath))
8541034
8551035 h.requireReadLineExact("VERSION 1")
8561036 h.requireWriteLine("INITREMOTE")
8571037 h.requireReadLineExact("INITREMOTE-SUCCESS")
8581038
8591039 // Specify an absolute path to transfer.
860− require.True(t, filepath.IsAbs(fileToTransfer))
861− h.requireWriteLine("TRANSFER STORE SomeKey " + fileToTransfer)
1040+ h.requireWriteLine("TRANSFER STORE SomeKey " + absPath)
8621041 h.requireReadLineExact("TRANSFER-SUCCESS STORE SomeKey")
863− require.FileExists(t, filepath.Join(h.localFsDir, "SomeKey"))
1042+
1043+ remoteItem := fstest.NewItem("SomeKey", "HELLO", item.ModTime)
1044+ h.fstestRun.CheckRemoteItems(t, remoteItem)
8641045
8651046 h.requireWriteLine("CHECKPRESENT SomeKey")
8661047 h.requireReadLineExact("CHECKPRESENT-SUCCESS SomeKey")
8671048
868− retrievedFilePath := fileToTransfer + ".retrieved"
869− require.NoFileExists(t, retrievedFilePath)
1049+ h.fstestRun.CheckLocalItems(t,
1050+ fstest.NewItem("file.txt", "HELLO", item.ModTime),
1051+ )
1052+
1053+ retrievedFilePath := absPath + ".retrieved"
8701054 h.requireWriteLine("TRANSFER RETRIEVE SomeKey " + retrievedFilePath)
8711055 h.requireReadLineExact("TRANSFER-SUCCESS RETRIEVE SomeKey")
872− require.FileExists(t, retrievedFilePath)
1056+
1057+ h.fstestRun.CheckLocalItems(t,
1058+ fstest.NewItem("file.txt", "HELLO", item.ModTime),
1059+ fstest.NewItem("file.txt.retrieved", "HELLO", item.ModTime),
1060+ )
8731061
8741062 require.NoError(t, h.mockStdinW.Close())
8751063 },
@@ -879,11 +1067,13 @@ var localBackendTestCases = []testCase{ · 3 unmodified lines
8791067 testProtocolFunc: func(t *testing.T, h *testState) {
8801068 h.preconfigureServer()
8811069
1070+ ctx := context.WithoutCancel(context.Background())
1071+
8821072 // Write a file into the remote without using the git-annex
8831073 // protocol.
884− remoteFilePath := filepath.Join(h.localFsDir, "SomeKey")
885− require.NoError(t, os.WriteFile(remoteFilePath, []byte("HELLO"), 0600))
886− require.FileExists(t, remoteFilePath)
1074+ remoteItem := h.fstestRun.WriteObject(ctx, "SomeKey", "HELLO", time.Now())
1075+
1076+ h.fstestRun.CheckRemoteItems(t, remoteItem)
8871077
8881078 h.requireReadLineExact("VERSION 1")
8891079 h.requireWriteLine("INITREMOTE")
@@ -891,15 +1081,18 @@ var localBackendTestCases = []testCase{ · 1 unmodified line
8911081
8921082 h.requireWriteLine("CHECKPRESENT SomeKey")
8931083 h.requireReadLineExact("CHECKPRESENT-SUCCESS SomeKey")
894− require.FileExists(t, remoteFilePath)
1084+
1085+ h.fstestRun.CheckRemoteItems(t, remoteItem)
8951086
8961087 h.requireWriteLine("REMOVE SomeKey")
8971088 h.requireReadLineExact("REMOVE-SUCCESS SomeKey")
898− require.NoFileExists(t, remoteFilePath)
1089+
1090+ h.requireRemoteIsEmpty()
8991091
9001092 h.requireWriteLine("CHECKPRESENT SomeKey")
9011093 h.requireReadLineExact("CHECKPRESENT-FAILURE SomeKey")
902− require.NoFileExists(t, remoteFilePath)
1094+
1095+ h.requireRemoteIsEmpty()
9031096
9041097 require.NoError(t, h.mockStdinW.Close())
9051098 },
@@ -910,8 +1103,9 @@ var localBackendTestCases = []testCase{ · 4 unmodified lines
9101103 h.preconfigureServer()
9111104
9121105 // Create temp file for transfer.
913− fileToTransfer := filepath.Join(t.TempDir(), "file.txt")
914− require.NoError(t, os.WriteFile(fileToTransfer, []byte("HELLO"), 0600))
1106+ item := h.fstestRun.WriteFile("file.txt", "HELLO", time.Now())
1107+ absPath := filepath.Join(h.fstestRun.Flocal.Root(), item.Path)
1108+ require.True(t, filepath.IsAbs(absPath))
9151109
9161110 h.requireReadLineExact("VERSION 1")
9171111 h.requireWriteLine("INITREMOTE")
@@ -921,17 +1115,19 @@ var localBackendTestCases = []testCase{ · 3 unmodified lines
9211115 h.requireReadLineExact("CHECKPRESENT-FAILURE SomeKey")
9221116
9231117 // Specify an absolute path to transfer.
924− require.True(t, filepath.IsAbs(fileToTransfer))
925− h.requireWriteLine("TRANSFER STORE SomeKey " + fileToTransfer)
1118+ h.requireWriteLine("TRANSFER STORE SomeKey " + absPath)
9261119 h.requireReadLineExact("TRANSFER-SUCCESS STORE SomeKey")
927− require.FileExists(t, filepath.Join(h.localFsDir, "SomeKey"))
1120+
1121+ remoteItem := fstest.NewItem("SomeKey", "HELLO", item.ModTime)
1122+ h.fstestRun.CheckRemoteItems(t, remoteItem)
9281123
9291124 h.requireWriteLine("CHECKPRESENT SomeKey")
9301125 h.requireReadLineExact("CHECKPRESENT-SUCCESS SomeKey")
9311126
9321127 h.requireWriteLine("REMOVE SomeKey")
9331128 h.requireReadLineExact("REMOVE-SUCCESS SomeKey")
934− require.NoFileExists(t, filepath.Join(h.localFsDir, "SomeKey"))
1129+
1130+ h.requireRemoteIsEmpty()
9351131
9361132 h.requireWriteLine("CHECKPRESENT SomeKey")
9371133 h.requireReadLineExact("CHECKPRESENT-FAILURE SomeKey")
@@ -944,10 +1140,6 @@ var localBackendTestCases = []testCase{ · 6 unmodified lines
9441140 testProtocolFunc: func(t *testing.T, h *testState) {
9451141 h.preconfigureServer()
9461142
947− // Create temp file for transfer.
948− fileToTransfer := filepath.Join(t.TempDir(), "file.txt")
949− require.NoError(t, os.WriteFile(fileToTransfer, []byte("HELLO"), 0600))
950−
9511143 h.requireReadLineExact("VERSION 1")
9521144 h.requireWriteLine("INITREMOTE")
9531145 h.requireReadLineExact("INITREMOTE-SUCCESS")
@@ -955,10 +1147,12 @@ var localBackendTestCases = []testCase{ · 1 unmodified line
9551147 h.requireWriteLine("CHECKPRESENT SomeKey")
9561148 h.requireReadLineExact("CHECKPRESENT-FAILURE SomeKey")
9571149
958− require.NoFileExists(t, filepath.Join(h.localFsDir, "SomeKey"))
1150+ h.requireRemoteIsEmpty()
1151+
9591152 h.requireWriteLine("REMOVE SomeKey")
9601153 h.requireReadLineExact("REMOVE-SUCCESS SomeKey")
961− require.NoFileExists(t, filepath.Join(h.localFsDir, "SomeKey"))
1154+
1155+ h.requireRemoteIsEmpty()
9621156
9631157 h.requireWriteLine("CHECKPRESENT SomeKey")
9641158 h.requireReadLineExact("CHECKPRESENT-FAILURE SomeKey")
@@ -983,44 +1177,40 @@ var localBackendTestCases = []testCase{ · 18 unmodified lines
9831177 },
9841178 }
9851179
986−func TestGitAnnexLocalBackendCases(t *testing.T) {
987− for _, testCase := range localBackendTestCases {
988− // Clear global state left behind by tests that chdir to a temp directory.
989− cache.Clear()
1180+// TestMain drives the tests
1181+func TestMain(m *testing.M) {
1182+ fstest.TestMain(m)
1183+}
1184+
1185+// Run fstest-compatible test cases with backend selected by `-remote`.
1186+func TestGitAnnexFstestBackendCases(t *testing.T) {
9901187
1188+ for _, testCase := range fstestTestCases {
9911189 // TODO: Remove this when rclone requires a Go version >= 1.22. Future
9921190 // versions of Go fix the semantics of capturing a range variable.
9931191 // https://go.dev/blog/loopvar-preview
9941192 testCase := testCase
9951193
9961194 t.Run(testCase.label, func(t *testing.T) {
997− tempDir := t.TempDir()
1195+ r := fstest.NewRun(t)
1196+ t.Cleanup(func() { r.Finalise() })
9981197
999− // Create temp dir for an rclone remote pointing at local filesystem.
1000− localFsDir := filepath.Join(tempDir, "remoteTarget")
1001− require.NoError(t, os.Mkdir(localFsDir, 0700))
1198+ // Parse the fstest-provided remote string. It might have a path!
1199+ remoteName, remotePath, err := fspath.SplitFs(r.FremoteName)
1200+ require.NoError(t, err)
10021201
1003− // Create temp config
1004− remoteName := getUniqueRemoteName(t)
1005− configLines := []string{
1006− fmt.Sprintf("[%s]", remoteName),
1007− "type = local",
1008− fmt.Sprintf("remote = %s", localFsDir),
1202+ // The gitannex command requires the `rcloneremotename` is the name
1203+ // of a remote or exactly ":local", so the empty string will not
1204+ // suffice.
1205+ if remoteName == "" {
1206+ require.True(t, r.Fremote.Features().IsLocal)
1207+ remoteName = ":local"
10091208 }
1010− configContents := strings.Join(configLines, "\n")
1011−
1012− configPath := filepath.Join(tempDir, "rclone.conf")
1013− require.NoError(t, os.WriteFile(configPath, []byte(configContents), 0600))
1014− require.NoError(t, config.SetConfigPath(configPath))
1015−
1016− // The custom config file will be ignored unless we install the
1017− // global config file handler.
1018− configfile.Install()
10191209
10201210 handle := makeTestState(t)
1021− handle.localFsDir = localFsDir
1022− handle.configPath = configPath
1211+ handle.fstestRun = r
10231212 handle.remoteName = remoteName
1213+ handle.remotePrefix = remotePath
10241214
10251215 var wg sync.WaitGroup
10261216 wg.Add(1)
@@ -1042,54 +1232,3 @@ func TestGitAnnexLocalBackendCases(t *testing.T) { · 15 unmodified lines
10421232 })
10431233 }
10441234 }
1045−
1046−// Configure the git-annex client with a mockfs backend and send it the
1047−// "INITREMOTE" command over mocked stdin. This should fail because mockfs does
1048−// not support empty directories.
1049−func TestGitAnnexHandleInitRemoteBackendDoesNotSupportEmptyDirectories(t *testing.T) {
1050− tempDir := t.TempDir()
1051−
1052− // Temporarily override the filesystem registry.
1053− oldRegistry := fs.Registry
1054− mockfs.Register()
1055− defer func() { fs.Registry = oldRegistry }()
1056−
1057− // Create temp dir for an rclone remote pointing at local filesystem.
1058− localFsDir := filepath.Join(tempDir, "remoteTarget")
1059− require.NoError(t, os.Mkdir(localFsDir, 0700))
1060−
1061− // Create temp config
1062− remoteName := getUniqueRemoteName(t)
1063− configLines := []string{
1064− fmt.Sprintf("[%s]", remoteName),
1065− "type = mockfs",
1066− fmt.Sprintf("remote = %s", localFsDir),
1067− }
1068− configContents := strings.Join(configLines, "\n")
1069−
1070− configPath := filepath.Join(tempDir, "rclone.conf")
1071− require.NoError(t, os.WriteFile(configPath, []byte(configContents), 0600))
1072−
1073− // The custom config file will be ignored unless we install the global
1074− // config file handler.
1075− configfile.Install()
1076− require.NoError(t, config.SetConfigPath(configPath))
1077−
1078− handle := makeTestState(t)
1079− handle.server.configPrefix = localFsDir
1080− handle.server.configRcloneRemoteName = remoteName
1081− handle.server.configsDone = true
1082−
1083− var wg sync.WaitGroup
1084− wg.Add(1)
1085−
1086− go func() {
1087− require.NotNil(t, handle.server.run())
1088− wg.Done()
1089− }()
1090− defer wg.Wait()
1091−
1092− handle.requireReadLineExact("VERSION 1")
1093− handle.requireWriteLine("INITREMOTE")
1094− handle.requireReadLineExact("INITREMOTE-FAILURE this rclone remote does not support empty directories")
1095−}
cmd/gitannex/layout.go+10−5
@@ -3,6 +3,8 @@ package gitannex · 2 unmodified lines
33 import (
44 "fmt"
55 "strings"
6+
7+ "github.com/rclone/rclone/fs/fspath"
68 )
79
810 type layoutMode string
@@ -39,8 +41,11 @@ func parseLayoutMode(mode string) layoutMode { · 30 unmodified lines
3941 type queryDirhashFunc func(msg string) (string, error)
4042
4143 func buildFsString(queryDirhash queryDirhashFunc, mode layoutMode, key, remoteName, prefix string) (string, error) {
44+ remoteName = strings.TrimSuffix(remoteName, ":") + ":"
45+ remoteString := fspath.JoinRootPath(remoteName, prefix)
46+
4247 if mode == layoutModeNodir {
43− return fmt.Sprintf("%s:%s", remoteName, prefix), nil
48+ return remoteString, nil
4449 }
4550
4651 var dirhash string
@@ -59,13 +64,13 @@ func buildFsString(queryDirhash queryDirhashFunc, mode layoutMode, key, remoteNa · 12 unmodified lines
5964
6065 switch mode {
6166 case layoutModeLower:
62− return fmt.Sprintf("%s:%s/%s", remoteName, prefix, dirhash), nil
67+ return fmt.Sprintf("%s/%s", remoteString, dirhash), nil
6368 case layoutModeDirectory:
64− return fmt.Sprintf("%s:%s/%s%s", remoteName, prefix, dirhash, key), nil
69+ return fmt.Sprintf("%s/%s%s", remoteString, dirhash, key), nil
6570 case layoutModeMixed:
66− return fmt.Sprintf("%s:%s/%s", remoteName, prefix, dirhash), nil
71+ return fmt.Sprintf("%s/%s", remoteString, dirhash), nil
6772 case layoutModeFrankencase:
68− return fmt.Sprintf("%s:%s/%s", remoteName, prefix, strings.ToLower(dirhash)), nil
73+ return fmt.Sprintf("%s/%s", remoteString, strings.ToLower(dirhash)), nil
6974 default:
7075 panic("unreachable")
7176 }

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t