// cmdtest_test creates a testable interface to rclone main // // The interface is used to perform end-to-end test of // commands, flags, environment variables etc. package cmdtest import ( "os" "os/exec" "path/filepath" "strings" "testing" "github.com/rclone/rclone/fs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TestMain is initially called by go test to initiate the testing. // TestMain is also called during the tests to start rclone main in a fresh context (using exec.Command). // The context is determined by setting/finding the environment variable RCLONE_TEST_MAIN func TestMain(m *testing.M) { _, found := os.LookupEnv(rcloneTestMain) if !found { // started by Go test => execute tests err := os.Setenv(rcloneTestMain, "true") if err != nil { fs.Fatalf(nil, "Unable to set %s: %s", rcloneTestMain, err.Error()) } os.Exit(m.Run()) } else { // started by func rcloneExecMain => call rclone main in cmdtest.go err := os.Unsetenv(rcloneTestMain) if err != nil { fs.Fatalf(nil, "Unable to unset %s: %s", rcloneTestMain, err.Error()) } main() } } const rcloneTestMain = "RCLONE_TEST_MAIN" // rcloneExecMain calls rclone with the given environment and arguments. // The environment variables are in a single string separated by ; // The terminal output is returned as a string. func rcloneExecMain(env string, args ...string) (string, error) { _, found := os.LookupEnv(rcloneTestMain) if !found { fs.Fatalf(nil, "Unexpected execution path: %s is missing.", rcloneTestMain) } // make a call to self to execute rclone main in a predefined environment (enters TestMain above) command := exec.Command(os.Args[0], args...) command.Env = getEnvInitial() if env != "" { command.Env = append(command.Env, strings.Split(env, ";")...) } out, err := command.CombinedOutput() return string(out), err } // rcloneEnv calls rclone with the given environment and arguments. // The environment variables are in a single string separated by ; // The test config file is automatically configured in RCLONE_CONFIG. // The terminal output is returned as a string. func rcloneEnv(env string, args ...string) (string, error) { envConfig := env if testConfig != "" { if envConfig != "" { envConfig += ";" } envConfig += "RCLONE_CONFIG=" + testConfig } return rcloneExecMain(envConfig, args...) } // rclone calls rclone with the given arguments, E.g. "version","--help". // The test config file is automatically configured in RCLONE_CONFIG. // The terminal output is returned as a string. func rclone(args ...string) (string, error) { return rcloneEnv("", args...) } // getEnvInitial returns the os environment variables cleaned for RCLONE_ vars (except RCLONE_TEST_MAIN). func getEnvInitial() []string { if envInitial == nil { // Set initial environment variables osEnv := os.Environ() for i := range osEnv { if !strings.HasPrefix(osEnv[i], "RCLONE_") || strings.HasPrefix(osEnv[i], rcloneTestMain) { envInitial = append(envInitial, osEnv[i]) } } } return envInitial } var envInitial []string // createTestEnvironment creates a temporary testFolder and // sets testConfig to testFolder/rclone.config. func createTestEnvironment(t *testing.T) { //Set temporary folder for config and test data tempFolder := t.TempDir() testFolder = filepath.ToSlash(tempFolder) // Set path to temporary config file testConfig = testFolder + "/rclone.config" } var testFolder string var testConfig string // createTestFile creates the file testFolder/name func createTestFile(name string, t *testing.T) string { err := os.WriteFile(testFolder+"/"+name, []byte("content_of_"+name), 0666) require.NoError(t, err) return testFolder + "/" + name } // createTestFolder creates the folder testFolder/name func createTestFolder(name string, t *testing.T) string { err := os.Mkdir(testFolder+"/"+name, 0777) require.NoError(t, err) return testFolder + "/" + name } // createSimpleTestData creates simple test data in testFolder/subFolder func createSimpleTestData(t *testing.T) string { createTestFolder("testdata", t) createTestFile("testdata/file1.txt", t) createTestFile("testdata/file2.txt", t) createTestFolder("testdata/folderA", t) createTestFile("testdata/folderA/fileA1.txt", t) createTestFile("testdata/folderA/fileA2.txt", t) createTestFolder("testdata/folderA/folderAA", t) createTestFile("testdata/folderA/folderAA/fileAA1.txt", t) createTestFile("testdata/folderA/folderAA/fileAA2.txt", t) createTestFolder("testdata/folderB", t) createTestFile("testdata/folderB/fileB1.txt", t) createTestFile("testdata/folderB/fileB2.txt", t) t.Cleanup(func() { err := os.RemoveAll(testFolder + "/testdata") require.NoError(t, err) }) return testFolder + "/testdata" } // TestCmdTest demonstrates and verifies the test functions for end-to-end testing of rclone func TestCmdTest(t *testing.T) { createTestEnvironment(t) // Test simple call and output from rclone out, err := rclone("version") t.Log("rclone version\n" + out) if assert.NoError(t, err) { assert.Contains(t, out, "rclone v") assert.Contains(t, out, "version: ") assert.NotContains(t, out, "Error:") assert.NotContains(t, out, "--help") assert.NotContains(t, out, " DEBUG : ") assert.Regexp(t, "rclone\\s+v\\d+\\.\\d+", out) // rclone v_.__ } // Test multiple arguments and DEBUG output out, err = rclone("version", "-vv") if assert.NoError(t, err) { assert.Contains(t, out, "rclone v") assert.Contains(t, out, " DEBUG : ") } // Test error and error output out, err = rclone("version", "--provoke-an-error") if assert.Error(t, err) { assert.Contains(t, err.Error(), "exit status 2") assert.Contains(t, out, "Error: unknown flag") } // Test effect of environment variable env := "RCLONE_LOG_LEVEL=DEBUG" out, err = rcloneEnv(env, "version") if assert.NoError(t, err) { assert.Contains(t, out, "rclone v") assert.Contains(t, out, " DEBUG : ") } // Test effect of multiple environment variables, including one with , env = "RCLONE_LOG_LEVEL=DEBUG;RCLONE_LOG_FORMAT=date,shortfile;RCLONE_STATS=173ms" out, err = rcloneEnv(env, "version") if assert.NoError(t, err) { assert.Contains(t, out, "rclone v") assert.Contains(t, out, " DEBUG : ") assert.Regexp(t, "[^\\s]+\\.go:\\d+:", out) // ___.go:__: assert.Contains(t, out, "173ms") } // Test setup of config file out, err = rclone("config", "create", "myLocal", "local") if assert.NoError(t, err) { assert.Contains(t, out, "[myLocal]") assert.Contains(t, out, "type = local") } // Test creation of simple test data createSimpleTestData(t) // Test access to config file and simple test data out, err = rclone("lsl", "myLocal:"+testFolder) t.Log("rclone lsl myLocal:testFolder\n" + out) if assert.NoError(t, err) { assert.Contains(t, out, "rclone.config") assert.Contains(t, out, "testdata/folderA/fileA1.txt") } }