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.
(cherry picked from commit 8cd43746a3)
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)
|
|
}
|