rclone/cmd/gitannex/e2e_test.go
Dan McArdle ef42c32cc6 cmd/gitannex: Replace e2e test script with Go test
This commit implements milestone 2.1 for the gitannex subcommand:
https://github.com/rclone/rclone/issues/7625#issuecomment-1951403856

This rewrite makes a few improvements over the old shell script:

(1) It no longer uses the system's rclone.conf. Now, it writes the
    rclone.conf file in an ephemeral directory.

(2) It no longer makes any assumptions about the contents of /tmp.

However, it now assumes that an rclone built from the HEAD commit is on
the PATH. It makes a best-effort attempt to verify this assumption, but
I'm not sure it's bulletproof.

I'm hoping that writing this in Go will enable more cross-platform
support in the future, but for now we're still restricted to Unixy
systems due to reliance on the HOME environment variable.

Issue #7625
2024-04-05 18:01:39 +01:00

159 lines
4.8 KiB
Go

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())
}
}