Nine tests fail on an ordinary Windows machine, eight in backend/local and
TestEnvironmentVariables in cmdtest, all with
symlink file.txt \?\C:\Users\...\symlink.txt: A required privilege is not
held by the client.
Windows grants SeCreateSymbolicLinkPrivilege only to an elevated process or one
running with Developer Mode enabled, and a default install gives an ordinary
user neither. CI does not see this because the windows-latest runner is
elevated, so the failures only show up on a contributor's own machine, where
AGENTS.md asks for make quicktest to pass before opening a pull request.
cmdtest already recognised the situation and attached a note to the failure
saying the test could safely be ignored. If it is safe to ignore then the test
knows it cannot run, so skip it and say why instead.
backend/local gains a helper that tries a symlink in t.TempDir() and skips if it
cannot make one, called from the six tests that need the privilege. Where a
platform can create symlinks the probe succeeds and nothing is skipped, so other
platforms are unchanged.
TestMetadata is skipped whole because it creates its symlink before anything
else and the object built from it is used throughout.
TestSymlinkEscapeConcurrent is left alone: it goes through putLink and ignores
the error, so it never needed the privilege.
387 lines
13 KiB
Go
387 lines
13 KiB
Go
// environment_test tests the use and precedence of environment variables
|
|
//
|
|
// The tests rely on functions defined in cmdtest_test.go
|
|
|
|
package cmdtest
|
|
|
|
import (
|
|
"os"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestEnvironmentVariables demonstrates and verifies the test functions for end-to-end testing of rclone
|
|
func TestEnvironmentVariables(t *testing.T) {
|
|
|
|
createTestEnvironment(t)
|
|
|
|
testdataPath := createSimpleTestData(t)
|
|
|
|
// Non backend flags
|
|
// =================
|
|
|
|
// First verify default behaviour of the implicit max_depth=-1
|
|
env := ""
|
|
out, err := rcloneEnv(env, "lsl", testFolder)
|
|
//t.Logf("\n" + out)
|
|
if assert.NoError(t, err) {
|
|
assert.Contains(t, out, "rclone.config") // depth 1
|
|
assert.Contains(t, out, "file1.txt") // depth 2
|
|
assert.Contains(t, out, "fileA1.txt") // depth 3
|
|
assert.Contains(t, out, "fileAA1.txt") // depth 4
|
|
}
|
|
|
|
// Test of flag.Value
|
|
env = "RCLONE_MAX_DEPTH=2"
|
|
out, err = rcloneEnv(env, "lsl", testFolder)
|
|
if assert.NoError(t, err) {
|
|
assert.Contains(t, out, "file1.txt") // depth 2
|
|
assert.NotContains(t, out, "fileA1.txt") // depth 3
|
|
}
|
|
|
|
// Test of flag.Changed (tests #5341 Issue1)
|
|
env = "RCLONE_LOG_LEVEL=DEBUG"
|
|
out, err = rcloneEnv(env, "version", "--quiet")
|
|
if assert.Error(t, err) {
|
|
assert.Contains(t, out, " DEBUG ")
|
|
assert.Contains(t, out, "Can't set -q and --log-level")
|
|
assert.Contains(t, "exit status 1", err.Error())
|
|
}
|
|
|
|
// Test of flag.DefValue
|
|
env = "RCLONE_STATS=173ms"
|
|
out, err = rcloneEnv(env, "help", "flags")
|
|
if assert.NoError(t, err) {
|
|
assert.Contains(t, out, "(default 173ms)")
|
|
}
|
|
|
|
// Test of command line flags overriding environment flags
|
|
env = "RCLONE_MAX_DEPTH=2"
|
|
out, err = rcloneEnv(env, "lsl", testFolder, "--max-depth", "3")
|
|
if assert.NoError(t, err) {
|
|
assert.Contains(t, out, "fileA1.txt") // depth 3
|
|
assert.NotContains(t, out, "fileAA1.txt") // depth 4
|
|
}
|
|
|
|
// Test of debug logging while initialising flags from environment (tests #5341 Enhance1)
|
|
env = "RCLONE_STATS=173ms"
|
|
out, err = rcloneEnv(env, "version", "-vv")
|
|
if assert.NoError(t, err) {
|
|
assert.Contains(t, out, " DEBUG : ")
|
|
assert.Contains(t, out, "--stats")
|
|
assert.Contains(t, out, "173ms")
|
|
assert.Contains(t, out, "RCLONE_STATS=")
|
|
}
|
|
|
|
// Backend flags and remote name
|
|
// - The listremotes command includes names from environment variables,
|
|
// the part between "RCLONE_CONFIG_" and "_TYPE", converted to lowercase.
|
|
// - When using a remote created from env, e.g. with lsd command,
|
|
// the name is case insensitive in contrast to remotes in config file
|
|
// (fs.ConfigToEnv converts to uppercase before checking environment).
|
|
// - Previously using a remote created from env, e.g. with lsd command,
|
|
// would not be possible for remotes with '-' in names, and remote names
|
|
// with '_' could be referred to with both '-' and '_', because any '-'
|
|
// were replaced with '_' before lookup.
|
|
// ===================================
|
|
|
|
env = "RCLONE_CONFIG_MY-LOCAL_TYPE=local"
|
|
out, err = rcloneEnv(env, "listremotes")
|
|
if assert.NoError(t, err) {
|
|
assert.Contains(t, out, "my-local:")
|
|
}
|
|
out, err = rcloneEnv(env, "lsl", "my-local:"+testFolder)
|
|
if assert.NoError(t, err) {
|
|
assert.Contains(t, out, "rclone.config")
|
|
assert.Contains(t, out, "file1.txt")
|
|
assert.Contains(t, out, "fileA1.txt")
|
|
assert.Contains(t, out, "fileAA1.txt")
|
|
}
|
|
out, err = rcloneEnv(env, "lsl", "mY-LoCaL:"+testFolder)
|
|
if assert.NoError(t, err) {
|
|
assert.Contains(t, out, "rclone.config")
|
|
assert.Contains(t, out, "file1.txt")
|
|
assert.Contains(t, out, "fileA1.txt")
|
|
assert.Contains(t, out, "fileAA1.txt")
|
|
}
|
|
out, err = rcloneEnv(env, "lsl", "my_local:"+testFolder)
|
|
if assert.Error(t, err) {
|
|
assert.Contains(t, out, "Failed to create file system")
|
|
}
|
|
|
|
env = "RCLONE_CONFIG_MY_LOCAL_TYPE=local"
|
|
out, err = rcloneEnv(env, "listremotes")
|
|
if assert.NoError(t, err) {
|
|
assert.Contains(t, out, "my_local:")
|
|
}
|
|
out, err = rcloneEnv(env, "lsl", "my_local:"+testFolder)
|
|
if assert.NoError(t, err) {
|
|
assert.Contains(t, out, "rclone.config")
|
|
assert.Contains(t, out, "file1.txt")
|
|
assert.Contains(t, out, "fileA1.txt")
|
|
assert.Contains(t, out, "fileAA1.txt")
|
|
}
|
|
out, err = rcloneEnv(env, "lsl", "my-local:"+testFolder)
|
|
if assert.Error(t, err) {
|
|
assert.Contains(t, out, "Failed to create file system")
|
|
}
|
|
|
|
// Backend flags and option precedence
|
|
// ===================================
|
|
|
|
// Test approach:
|
|
// Verify no symlink warning when skip_links=true one the level with highest precedence
|
|
// and skip_links=false on all levels with lower precedence
|
|
//
|
|
// Reference: https://rclone.org/docs/#precedence
|
|
// Create a symlink in test data
|
|
err = os.Symlink(testdataPath+"/folderA", testdataPath+"/symlinkA")
|
|
if err != nil && runtime.GOOS == "windows" {
|
|
// Windows grants the privilege only to an elevated process or one
|
|
// running with Developer Mode enabled.
|
|
t.Skipf("Skipping as symlinks are unavailable: %v", err)
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
// Create a local remote with explicit skip_links=false
|
|
out, err = rclone("config", "create", "myLocal", "local", "skip_links", "false")
|
|
if assert.NoError(t, err) {
|
|
assert.Contains(t, out, "[myLocal]")
|
|
assert.Contains(t, out, "type = local")
|
|
assert.Contains(t, out, "skip_links = false")
|
|
}
|
|
|
|
// Verify symlink warning when skip_links=false on all levels
|
|
env = "RCLONE_SKIP_LINKS=false;RCLONE_LOCAL_SKIP_LINKS=false;RCLONE_CONFIG_MYLOCAL_SKIP_LINKS=false"
|
|
out, err = rcloneEnv(env, "lsd", "myLocal,skip_links=false:"+testdataPath, "--skip-links=false")
|
|
//t.Logf("\n" + out)
|
|
if assert.NoError(t, err) {
|
|
assert.Contains(t, out, "NOTICE: symlinkA:")
|
|
assert.Contains(t, out, "folderA")
|
|
}
|
|
|
|
// Test precedence of connection strings
|
|
env = "RCLONE_SKIP_LINKS=false;RCLONE_LOCAL_SKIP_LINKS=false;RCLONE_CONFIG_MYLOCAL_SKIP_LINKS=false"
|
|
out, err = rcloneEnv(env, "lsd", "myLocal,skip_links:"+testdataPath, "--skip-links=false")
|
|
if assert.NoError(t, err) {
|
|
assert.NotContains(t, out, "symlinkA")
|
|
assert.Contains(t, out, "folderA")
|
|
}
|
|
|
|
// Test precedence of command line flags
|
|
env = "RCLONE_SKIP_LINKS=false;RCLONE_LOCAL_SKIP_LINKS=false;RCLONE_CONFIG_MYLOCAL_SKIP_LINKS=false"
|
|
out, err = rcloneEnv(env, "lsd", "myLocal:"+testdataPath, "--skip-links")
|
|
if assert.NoError(t, err) {
|
|
assert.NotContains(t, out, "symlinkA")
|
|
assert.Contains(t, out, "folderA")
|
|
}
|
|
|
|
// Test precedence of remote specific environment variables (tests #5341 Issue2)
|
|
env = "RCLONE_SKIP_LINKS=false;RCLONE_LOCAL_SKIP_LINKS=false;RCLONE_CONFIG_MYLOCAL_SKIP_LINKS=true"
|
|
out, err = rcloneEnv(env, "lsd", "myLocal:"+testdataPath)
|
|
if assert.NoError(t, err) {
|
|
assert.NotContains(t, out, "symlinkA")
|
|
assert.Contains(t, out, "folderA")
|
|
}
|
|
|
|
// Test precedence of backend specific environment variables (tests #5341 Issue3)
|
|
env = "RCLONE_SKIP_LINKS=false;RCLONE_LOCAL_SKIP_LINKS=true"
|
|
out, err = rcloneEnv(env, "lsd", "myLocal:"+testdataPath)
|
|
if assert.NoError(t, err) {
|
|
assert.NotContains(t, out, "symlinkA")
|
|
assert.Contains(t, out, "folderA")
|
|
}
|
|
|
|
// Test precedence of backend generic environment variables
|
|
env = "RCLONE_SKIP_LINKS=true"
|
|
out, err = rcloneEnv(env, "lsd", "myLocal:"+testdataPath)
|
|
if assert.NoError(t, err) {
|
|
assert.NotContains(t, out, "symlinkA")
|
|
assert.Contains(t, out, "folderA")
|
|
}
|
|
|
|
// Recreate the test remote with explicit skip_links=true
|
|
out, err = rclone("config", "create", "myLocal", "local", "skip_links", "true")
|
|
if assert.NoError(t, err) {
|
|
assert.Contains(t, out, "[myLocal]")
|
|
assert.Contains(t, out, "type = local")
|
|
assert.Contains(t, out, "skip_links = true")
|
|
}
|
|
|
|
// Test precedence of config file options
|
|
env = ""
|
|
out, err = rcloneEnv(env, "lsd", "myLocal:"+testdataPath)
|
|
if assert.NoError(t, err) {
|
|
assert.NotContains(t, out, "symlinkA")
|
|
assert.Contains(t, out, "folderA")
|
|
}
|
|
|
|
// Recreate the test remote with rclone defaults, that is implicit skip_links=false
|
|
out, err = rclone("config", "create", "myLocal", "local")
|
|
if assert.NoError(t, err) {
|
|
assert.Contains(t, out, "[myLocal]")
|
|
assert.Contains(t, out, "type = local")
|
|
assert.NotContains(t, out, "skip_links")
|
|
}
|
|
|
|
// Verify the rclone default value (implicit skip_links=false)
|
|
env = ""
|
|
out, err = rcloneEnv(env, "lsd", "myLocal:"+testdataPath)
|
|
if assert.NoError(t, err) {
|
|
assert.Contains(t, out, "NOTICE: symlinkA:")
|
|
assert.Contains(t, out, "folderA")
|
|
}
|
|
|
|
// Display of backend defaults (tests #4659)
|
|
//------------------------------------------
|
|
|
|
env = "RCLONE_DRIVE_CHUNK_SIZE=111M"
|
|
out, err = rcloneEnv(env, "help", "flags")
|
|
if assert.NoError(t, err) {
|
|
assert.Regexp(t, "--drive-chunk-size[^\\(]+\\(default 111M\\)", out)
|
|
}
|
|
|
|
// Options on referencing remotes (alias, crypt, etc.)
|
|
//----------------------------------------------------
|
|
|
|
// Create alias remote on myLocal having implicit skip_links=false
|
|
out, err = rclone("config", "create", "myAlias", "alias", "remote", "myLocal:"+testdataPath)
|
|
if assert.NoError(t, err) {
|
|
assert.Contains(t, out, "[myAlias]")
|
|
assert.Contains(t, out, "type = alias")
|
|
assert.Contains(t, out, "remote = myLocal:")
|
|
}
|
|
|
|
// Verify symlink warnings on the alias
|
|
env = ""
|
|
out, err = rcloneEnv(env, "lsd", "myAlias:")
|
|
if assert.NoError(t, err) {
|
|
assert.Contains(t, out, "NOTICE: symlinkA:")
|
|
assert.Contains(t, out, "folderA")
|
|
}
|
|
|
|
// Test backend generic flags
|
|
// having effect on the underlying local remote
|
|
env = "RCLONE_SKIP_LINKS=true"
|
|
out, err = rcloneEnv(env, "lsd", "myAlias:")
|
|
if assert.NoError(t, err) {
|
|
assert.NotContains(t, out, "symlinkA")
|
|
assert.Contains(t, out, "folderA")
|
|
}
|
|
|
|
// Test backend specific flags
|
|
// having effect on the underlying local remote
|
|
env = "RCLONE_LOCAL_SKIP_LINKS=true"
|
|
out, err = rcloneEnv(env, "lsd", "myAlias:")
|
|
if assert.NoError(t, err) {
|
|
assert.NotContains(t, out, "symlinkA")
|
|
assert.Contains(t, out, "folderA")
|
|
}
|
|
|
|
// Test remote specific flags
|
|
// having no effect unless supported by the immediate remote (alias)
|
|
env = "RCLONE_CONFIG_MYALIAS_SKIP_LINKS=true"
|
|
out, err = rcloneEnv(env, "lsd", "myAlias:")
|
|
if assert.NoError(t, err) {
|
|
assert.Contains(t, out, "NOTICE: symlinkA:")
|
|
assert.Contains(t, out, "folderA")
|
|
}
|
|
|
|
env = "RCLONE_CONFIG_MYALIAS_REMOTE=" + "myLocal:" + testdataPath + "/folderA"
|
|
out, err = rcloneEnv(env, "lsl", "myAlias:")
|
|
if assert.NoError(t, err) {
|
|
assert.Contains(t, out, "fileA1.txt")
|
|
assert.NotContains(t, out, "fileB1.txt")
|
|
}
|
|
|
|
// Test command line flags
|
|
// having effect on the underlying local remote
|
|
env = ""
|
|
out, err = rcloneEnv(env, "lsd", "myAlias:", "--skip-links")
|
|
if assert.NoError(t, err) {
|
|
assert.NotContains(t, out, "symlinkA")
|
|
assert.Contains(t, out, "folderA")
|
|
}
|
|
|
|
// Test connection specific flags
|
|
// having no effect unless supported by the immediate remote (alias)
|
|
env = ""
|
|
out, err = rcloneEnv(env, "lsd", "myAlias,skip_links:")
|
|
if assert.NoError(t, err) {
|
|
assert.Contains(t, out, "NOTICE: symlinkA:")
|
|
assert.Contains(t, out, "folderA")
|
|
}
|
|
|
|
env = ""
|
|
out, err = rcloneEnv(env, "lsl", "myAlias,remote='myLocal:"+testdataPath+"/folderA':", "-vv")
|
|
if assert.NoError(t, err) {
|
|
assert.Contains(t, out, "fileA1.txt")
|
|
assert.NotContains(t, out, "fileB1.txt")
|
|
}
|
|
|
|
// Test --use-json-log and -vv combinations
|
|
jsonLogOK := func() {
|
|
t.Helper()
|
|
if assert.NoError(t, err) {
|
|
assert.Contains(t, out, `"level":"debug"`)
|
|
assert.Contains(t, out, `"msg":"Version `)
|
|
assert.Contains(t, out, `"}`)
|
|
}
|
|
}
|
|
env = "RCLONE_USE_JSON_LOG=1;RCLONE_LOG_LEVEL=DEBUG"
|
|
out, err = rcloneEnv(env, "version")
|
|
jsonLogOK()
|
|
env = "RCLONE_USE_JSON_LOG=1"
|
|
out, err = rcloneEnv(env, "version", "-vv")
|
|
jsonLogOK()
|
|
env = "RCLONE_LOG_LEVEL=DEBUG"
|
|
out, err = rcloneEnv(env, "version", "--use-json-log")
|
|
jsonLogOK()
|
|
env = ""
|
|
out, err = rcloneEnv(env, "version", "-vv", "--use-json-log")
|
|
jsonLogOK()
|
|
|
|
// Find all the File filter lines in out and return them
|
|
parseFileFilters := func(out string) (extensions []string) {
|
|
// Match: - (^|/)[^/]*\.jpg$
|
|
find := regexp.MustCompile(`^- \(\^\|\/\)\[\^\/\]\*\\\.(.*?)\$$`)
|
|
for line := range strings.SplitSeq(out, "\n") {
|
|
if m := find.FindStringSubmatch(line); m != nil {
|
|
extensions = append(extensions, m[1])
|
|
}
|
|
}
|
|
return extensions
|
|
}
|
|
|
|
// Make sure that multiple valued (stringArray) environment variables are handled properly
|
|
env = ``
|
|
out, err = rcloneEnv(env, "version", "-vv", "--dump", "filters", "--exclude", "*.gif", "--exclude", "*.tif")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"gif", "tif"}, parseFileFilters(out))
|
|
|
|
env = `RCLONE_EXCLUDE=*.jpg`
|
|
out, err = rcloneEnv(env, "version", "-vv", "--dump", "filters", "--exclude", "*.gif")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"jpg", "gif"}, parseFileFilters(out))
|
|
|
|
env = `RCLONE_EXCLUDE=*.jpg,*.png`
|
|
out, err = rcloneEnv(env, "version", "-vv", "--dump", "filters", "--exclude", "*.gif", "--exclude", "*.tif")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"jpg", "png", "gif", "tif"}, parseFileFilters(out))
|
|
|
|
env = `RCLONE_EXCLUDE="*.jpg","*.png"`
|
|
out, err = rcloneEnv(env, "version", "-vv", "--dump", "filters")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"jpg", "png"}, parseFileFilters(out))
|
|
|
|
env = `RCLONE_EXCLUDE="*.,,,","*.png"`
|
|
out, err = rcloneEnv(env, "version", "-vv", "--dump", "filters")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{",,,", "png"}, parseFileFilters(out))
|
|
}
|