mirror of
https://github.com/rclone/rclone.git
synced 2026-10-09 14:39:28 -04:00
To find the latest patch release of a minor version, for example `rclone selfupdate --version 1.75`, selfupdate searched the listing of downloads.rclone.org for href="./vX.Y.Z/". The leading ./ is a detail of how Caddy's file server writes its links. A listing which linked to the same directories as "vX.Y.Z/", which is an equally valid relative URL, made selfupdate fail with "could not find the minor release". This makes the ./ optional in the pattern, so selfupdate no longer depends on which program wrote the listing, and takes the version from a capture group rather than from fixed offsets into the match. The listings written by rclone index now include the ./ for the benefit of rclone versions without this fix, so this is to remove the dependency for the future.
231 lines
6.2 KiB
Go
231 lines
6.2 KiB
Go
//go:build !noselfupdate
|
|
|
|
package selfupdate
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rclone/rclone/fs"
|
|
_ "github.com/rclone/rclone/fstest" // needed to run under integration tests
|
|
"github.com/rclone/rclone/fstest/testy"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
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 TestLatestMicroRelease(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
html string
|
|
want string
|
|
}{
|
|
{
|
|
name: "caddy",
|
|
html: `<a href="./v1.51.0/">v1.51.0/</a>
|
|
<a href="./v1.52.0/">v1.52.0/</a>
|
|
<a href="./v1.52.3/">v1.52.3/</a>
|
|
<a href="./v1.53.0/">v1.53.0/</a>
|
|
<a href="./v1.52-DEV/">v1.52-DEV/</a>
|
|
<a href="./v1.52.3/rclone-v1.52.3-linux-amd64.zip">rclone</a>`,
|
|
want: "v1.52.3",
|
|
},
|
|
{
|
|
name: "no prefix",
|
|
html: `<a href="v1.51.0/">v1.51.0/</a>
|
|
<a href="v1.52.0/">v1.52.0/</a>
|
|
<a href="v1.52.3/">v1.52.3/</a>
|
|
<a href="v1.53.0/">v1.53.0/</a>`,
|
|
want: "v1.52.3",
|
|
},
|
|
{
|
|
name: "other prefix",
|
|
html: `<a href="../v1.52.0/">v1.52.0/</a>
|
|
<a href="https://example.com/v1.52.1/">v1.52.1/</a>
|
|
<a href="v1.52.2/index.html">v1.52.2/</a>
|
|
<a href="v1x52.3/">v1x52.3/</a>`,
|
|
},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
got, err := latestMicroRelease([]byte(test.html), "v1.52")
|
|
if test.want == "" {
|
|
assert.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
assert.Equal(t, test.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
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 := t.TempDir()
|
|
path := filepath.Join(testDir, "rclone")
|
|
|
|
// Pin the running version so the checks below do not depend on
|
|
// whether this build happens to be the latest beta.
|
|
oldVersion := fs.Version
|
|
fs.Version = "v1.0.0"
|
|
t.Cleanup(func() { fs.Version = oldVersion })
|
|
|
|
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, os.WriteFile(path, []byte("test"), 0644))
|
|
assert.NoError(t, os.Chmod(path, 0000))
|
|
defer func() {
|
|
_ = os.Chmod(path, 0644)
|
|
}()
|
|
err = (InstallUpdate(ctx, &Options{Beta: true, Output: path}))
|
|
require.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))
|
|
require.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 := os.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 := t.TempDir()
|
|
|
|
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 := os.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 = os.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 = os.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)
|
|
}
|