package gitannex import ( "encoding/json" "errors" "fmt" "os" "os/exec" "path/filepath" "runtime" "testing" "github.com/stretchr/testify/require" "github.com/rclone/rclone/fs" "github.com/rclone/rclone/lib/buildinfo" ) // checkRcloneBinaryVersion runs whichever rclone is on the PATH and checks // whether it reports a version that matches the test's expectations. Returns // nil when the version is the expected version, otherwise returns an error. func checkRcloneBinaryVersion() error { // versionInfo is a subset of information produced by "core/version". type versionInfo struct { Version string IsGit bool GoTags string } cmd := exec.Command("rclone", "rc", "--loopback", "core/version") stdout, err := cmd.Output() if err != nil { return fmt.Errorf("failed to get rclone version: %w", err) } var parsed versionInfo if err := json.Unmarshal(stdout, &parsed); err != nil { return fmt.Errorf("failed to parse rclone version: %w", err) } if parsed.Version != fs.Version { return fmt.Errorf("expected version %q, but got %q", fs.Version, parsed.Version) } if !parsed.IsGit { return errors.New("expected rclone to be a dev build") } _, tagString := buildinfo.GetLinkingAndTags() if parsed.GoTags != tagString { return fmt.Errorf("expected tag string %q, but got %q", tagString, parsed.GoTags) } return nil } // This end-to-end test runs `git annex testremote` in a temporary git repo. // This test will be skipped unless the `rclone` binary on PATH reports the // expected version. // // When run on CI, an rclone binary built from HEAD will be on the PATH. When // running locally, you will likely need to ensure the current binary is on the // PATH like so: // // go build && PATH="$(realpath .):$PATH" go test -v ./cmd/gitannex/... // // In the future, this test will probably be extended to test a number of // parameters like repo layouts, and runtime may suffer from a combinatorial // explosion. func TestEndToEnd(t *testing.T) { if testing.Short() { t.Skip("Skipping due to short mode.") } // TODO: Support this test on Windows. Need to evaluate the semantics of the // HOME and PATH environment variables. switch runtime.GOOS { case "darwin", "freebsd", "linux", "netbsd", "openbsd", "plan9", "solaris": default: t.Skipf("GOOS %q is not supported.", runtime.GOOS) } if err := checkRcloneBinaryVersion(); err != nil { t.Skipf("Skipping due to rclone version: %s", err) } if _, err := exec.LookPath("git-annex"); err != nil { t.Skipf("Skipping because git-annex was not found: %s", err) } // Create a temp directory and chdir there, just in case. originalWd, err := os.Getwd() require.NoError(t, err) tempDir := t.TempDir() require.NoError(t, os.Chdir(tempDir)) defer func() { require.NoError(t, os.Chdir(originalWd)) }() // Flesh out subdirectories of the temp directory: // // . // |-- bin // | `-- git-annex-remote-rclone-builtin -> ${PATH_TO_RCLONE_BINARY} // |-- ephemeralRepo // `-- user // `-- .config // `-- rclone // `-- rclone.conf binDir := filepath.Join(tempDir, "bin") homeDir := filepath.Join(tempDir, "user") configDir := filepath.Join(homeDir, ".config") rcloneConfigDir := filepath.Join(configDir, "rclone") ephemeralRepoDir := filepath.Join(tempDir, "ephemeralRepo") for _, dir := range []string{binDir, homeDir, configDir, rcloneConfigDir, ephemeralRepoDir} { require.NoError(t, os.Mkdir(dir, 0700)) } // Install the symlink that enables git-annex to invoke "rclone gitannex" // without explicitly specifying the subcommand. rcloneBinaryPath, err := exec.LookPath("rclone") require.NoError(t, err) require.NoError(t, os.Symlink( rcloneBinaryPath, filepath.Join(binDir, "git-annex-remote-rclone-builtin"))) // Install the rclone.conf file that defines the remote. rcloneConfigPath := filepath.Join(rcloneConfigDir, "rclone.conf") rcloneConfigContents := "[MyRcloneRemote]\ntype = local" require.NoError(t, os.WriteFile(rcloneConfigPath, []byte(rcloneConfigContents), 0600)) // NOTE: These commands must be run with HOME pointing at an ephemeral // directory, rather than the real home directory. cmds := [][]string{ {"git", "annex", "version"}, {"git", "config", "--global", "user.name", "User Name"}, {"git", "config", "--global", "user.email", "user@example.com"}, {"git", "init"}, {"git", "annex", "init"}, {"git", "annex", "initremote", "MyTestRemote", "type=external", "externaltype=rclone-builtin", "encryption=none", "rcloneremotename=MyRcloneRemote", "rcloneprefix=" + ephemeralRepoDir}, {"git", "annex", "testremote", "MyTestRemote"}, } for _, args := range cmds { fmt.Printf("+ %v\n", args) cmd := exec.Command(args[0], args[1:]...) cmd.Dir = ephemeralRepoDir cmd.Env = []string{ "HOME=" + homeDir, "PATH=" + os.Getenv("PATH") + ":" + binDir, } cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr require.NoError(t, cmd.Run()) } }