//go:build !noselfupdate // +build !noselfupdate package selfupdate import ( "context" "io/ioutil" "os" "os/exec" "path/filepath" "regexp" "runtime" "testing" "time" "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fstest/testy" "github.com/rclone/rclone/lib/file" "github.com/rclone/rclone/lib/random" "github.com/stretchr/testify/assert" ) func TestGetVersion(t *testing.T) { testy.SkipUnreliable(t) ctx := context.Background() // a beta version can only have "v" prepended resultVer, _, err := GetVersion(ctx, true, "1.2.3.4") assert.NoError(t, err) assert.Equal(t, "v1.2.3.4", resultVer) // but a stable version syntax should be checked _, _, err = GetVersion(ctx, false, "1") assert.Error(t, err) _, _, err = GetVersion(ctx, false, "1.") assert.Error(t, err) _, _, err = GetVersion(ctx, false, "1.2.") assert.Error(t, err) _, _, err = GetVersion(ctx, false, "1.2.3.4") assert.Error(t, err) // incomplete stable version should have micro release added resultVer, _, err = GetVersion(ctx, false, "1.52") assert.NoError(t, err) assert.Equal(t, "v1.52.3", resultVer) } func makeTestDir() (testDir string, err error) { const maxAttempts = 5 testDirBase := filepath.Join(os.TempDir(), "rclone-test-selfupdate.") for attempt := 0; attempt < maxAttempts; attempt++ { testDir = testDirBase + random.String(4) err = file.MkdirAll(testDir, os.ModePerm) if err == nil { break } } return } func TestInstallOnLinux(t *testing.T) { testy.SkipUnreliable(t) if runtime.GOOS != "linux" { t.Skip("this is a Linux only test") } // Prepare for test ctx := context.Background() testDir, err := makeTestDir() assert.NoError(t, err) path := filepath.Join(testDir, "rclone") defer func() { _ = os.Chmod(path, 0644) _ = os.RemoveAll(testDir) }() regexVer := regexp.MustCompile(`v[0-9]\S+`) betaVer, _, err := GetVersion(ctx, true, "") assert.NoError(t, err) // Must do nothing if version isn't changing assert.NoError(t, InstallUpdate(ctx, &Options{Beta: true, Output: path, Version: fs.Version})) // Must fail on non-writable file assert.NoError(t, ioutil.WriteFile(path, []byte("test"), 0644)) assert.NoError(t, os.Chmod(path, 0000)) err = (InstallUpdate(ctx, &Options{Beta: true, Output: path})) assert.Error(t, err) assert.Contains(t, err.Error(), "run self-update as root") // Must keep non-standard permissions assert.NoError(t, os.Chmod(path, 0644)) assert.NoError(t, InstallUpdate(ctx, &Options{Beta: true, Output: path})) info, err := os.Stat(path) assert.NoError(t, err) assert.Equal(t, os.FileMode(0644), info.Mode().Perm()) // Must remove temporary files files, err := ioutil.ReadDir(testDir) assert.NoError(t, err) assert.Equal(t, 1, len(files)) // Must contain valid executable assert.NoError(t, os.Chmod(path, 0755)) cmd := exec.Command(path, "version") output, err := cmd.CombinedOutput() assert.NoError(t, err) assert.True(t, cmd.ProcessState.Success()) assert.Equal(t, betaVer, regexVer.FindString(string(output))) } func TestRenameOnWindows(t *testing.T) { testy.SkipUnreliable(t) if runtime.GOOS != "windows" { t.Skip("this is a Windows only test") } // Prepare for test ctx := context.Background() testDir, err := makeTestDir() assert.NoError(t, err) defer func() { _ = os.RemoveAll(testDir) }() path := filepath.Join(testDir, "rclone.exe") regexVer := regexp.MustCompile(`v[0-9]\S+`) stableVer, _, err := GetVersion(ctx, false, "") assert.NoError(t, err) betaVer, _, err := GetVersion(ctx, true, "") assert.NoError(t, err) // Must not create temporary files when target doesn't exist assert.NoError(t, InstallUpdate(ctx, &Options{Beta: true, Output: path})) files, err := ioutil.ReadDir(testDir) assert.NoError(t, err) assert.Equal(t, 1, len(files)) // Must save running executable as the "old" file cmdWait := exec.Command(path, "config") stdinWait, err := cmdWait.StdinPipe() // Make it run waiting for input assert.NoError(t, err) assert.NoError(t, cmdWait.Start()) assert.NoError(t, InstallUpdate(ctx, &Options{Beta: false, Output: path})) files, err = ioutil.ReadDir(testDir) assert.NoError(t, err) assert.Equal(t, 2, len(files)) pathOld := filepath.Join(testDir, "rclone.old.exe") _, err = os.Stat(pathOld) assert.NoError(t, err) cmd := exec.Command(path, "version") output, err := cmd.CombinedOutput() assert.NoError(t, err) assert.True(t, cmd.ProcessState.Success()) assert.Equal(t, stableVer, regexVer.FindString(string(output))) cmdOld := exec.Command(pathOld, "version") output, err = cmdOld.CombinedOutput() assert.NoError(t, err) assert.True(t, cmdOld.ProcessState.Success()) assert.Equal(t, betaVer, regexVer.FindString(string(output))) // Stop previous waiting executable, run new and saved executables _ = stdinWait.Close() _ = cmdWait.Wait() time.Sleep(100 * time.Millisecond) cmdWait = exec.Command(path, "config") stdinWait, err = cmdWait.StdinPipe() assert.NoError(t, err) assert.NoError(t, cmdWait.Start()) cmdWaitOld := exec.Command(pathOld, "config") stdinWaitOld, err := cmdWaitOld.StdinPipe() assert.NoError(t, err) assert.NoError(t, cmdWaitOld.Start()) // Updating when the "old" executable is running must produce a random "old" file assert.NoError(t, InstallUpdate(ctx, &Options{Beta: true, Output: path})) files, err = ioutil.ReadDir(testDir) assert.NoError(t, err) assert.Equal(t, 3, len(files)) // Stop all waiting executables _ = stdinWait.Close() _ = cmdWait.Wait() _ = stdinWaitOld.Close() _ = cmdWaitOld.Wait() time.Sleep(100 * time.Millisecond) }