Files
magpie/internal/appdir/appdir_test.go
T
yetone e520849b74 appdir: a folder named data in /Applications isn't taken for magpie's portable data while ~/.config/magpie has its files (#989, Treyang: after updating 0.1.1077 → 0.1.1078 magpie opened with no providers, sign-ins or settings; ~/.config/magpie was intact and /Applications/data had appeared) — portable mode (#508) took any folder named data beside the folder holding Magpie.app, and on a Mac that folder is /Applications, which every app shares. The reporter's /Applications/data began with nodes/, providers/, tools/, skills/ and an initialized marker, none of which magpie makes (magpie keeps providers.json, not a providers folder); magpie's cli-identity.json, install-id and last-version came after, written once a magpie had taken the folder for its own. The updater didn't make it: Stage only unpacks and codesign-checks the new app in .magpie-update, Install renames, Relaunch opens the bundle, and nothing runs the staged copy; appdir never creates a data folder without a .portable marker either. So a folder another app left there turned an installed magpie portable and empty at its next start. Now, in /Applications or ~/Applications, a data folder is magpie's portable data only while the installed folder (~/.config/magpie, or $XDG_CONFIG_HOME/magpie) holds no providers.json, logins.json or settings.json — one who keeps magpie portable there never ran it installed, and keeps it — or with a .portable marker beside the app, which makes it portable as before. A data folder beside magpie anywhere else (a USB stick, a Windows or Linux folder of its own, an AppImage's) is taken as before. A user already hit is back on ~/.config/magpie at the next start, and the data folder is left as it is, never deleted or moved; the log says it was passed over. TestResolveSharedFolder (/Applications stood in by a temp folder: a data folder with nodes/ and install-id, installed state present/absent, .portable, a data folder on a folder of its own) fails without the change (Resolve and Config give the shared data folder) and passes; TestSharedFolderIsApplications pins which folders count. go test -race ./internal/appdir/ (-count=3), go test ./internal/settings/ ./internal/shortcut/ ./internal/update/, go vet (darwin, windows), darwin/linux(nogui)/windows builds pass. Not tried with the reporter's /Applications or the updater on a real install.
2026-10-06 14:48:39 +08:00

238 lines
7.6 KiB
Go

package appdir
import (
"os"
"path/filepath"
"runtime"
"testing"
)
// root is a temp folder with its links followed (/var is /private/var on a
// Mac), as Resolve gives its answers.
func root(t *testing.T) string {
d, err := filepath.EvalSymlinks(t.TempDir())
if err != nil {
t.Fatal(err)
}
return d
}
func touch(t *testing.T, p string) string {
t.Helper()
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte("x"), 0o755); err != nil {
t.Fatal(err)
}
return p
}
func mkdir(t *testing.T, p string) {
t.Helper()
if err := os.MkdirAll(p, 0o755); err != nil {
t.Fatal(err)
}
}
func TestResolve(t *testing.T) {
t.Setenv("APPIMAGE", "")
r := root(t)
// no data folder: installed
exe := touch(t, filepath.Join(r, "plain", "magpie"))
if got := Resolve(exe); got != "" {
t.Errorf("no data folder: %q, want installed", got)
}
// a file named data is not the folder
touch(t, filepath.Join(r, "plain", "data"))
if got := Resolve(exe); got != "" {
t.Errorf("data is a file: %q, want installed", got)
}
// a data folder beside it
exe = touch(t, filepath.Join(r, "port", "magpie.exe"))
mkdir(t, filepath.Join(r, "port", "data"))
if got, want := Resolve(exe), filepath.Join(r, "port", "data"); got != want {
t.Errorf("data folder: %q, want %q", got, want)
}
// a .portable marker: the data folder is made beside it when written
exe = touch(t, filepath.Join(r, "marker", "magpie"))
touch(t, filepath.Join(r, "marker", ".portable"))
if got, want := Resolve(exe), filepath.Join(r, "marker", "data"); got != want {
t.Errorf(".portable: %q, want %q", got, want)
}
// a Mac app: beside the bundle, never inside it
exe = touch(t, filepath.Join(r, "mac", "Magpie.app", "Contents", "MacOS", "magpie"))
mkdir(t, filepath.Join(r, "mac", "Magpie.app", "Contents", "MacOS", "data"))
if got := Resolve(exe); got != "" {
t.Errorf("data inside the bundle: %q, want installed", got)
}
mkdir(t, filepath.Join(r, "mac", "data"))
if got, want := Resolve(exe), filepath.Join(r, "mac", "data"); got != want {
t.Errorf("Mac app: %q, want %q", got, want)
}
}
func TestResolveSymlinkedExe(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("symlinks need privileges on Windows")
}
t.Setenv("APPIMAGE", "")
r := root(t)
real := touch(t, filepath.Join(r, "app", "magpie"))
mkdir(t, filepath.Join(r, "bin"))
link := filepath.Join(r, "bin", "magpie")
if err := os.Symlink(real, link); err != nil {
t.Fatal(err)
}
// a data folder beside the link only: the link isn't where magpie is
mkdir(t, filepath.Join(r, "bin", "data"))
if got := Resolve(link); got != "" {
t.Errorf("data beside the link: %q, want installed", got)
}
mkdir(t, filepath.Join(r, "app", "data"))
if got, want := Resolve(link), filepath.Join(r, "app", "data"); got != want {
t.Errorf("symlinked exe: %q, want %q", got, want)
}
}
func TestResolveAppImage(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("AppImage is Linux's")
}
r := root(t)
exe := touch(t, filepath.Join(r, "mount", "usr", "bin", "magpie"))
img := touch(t, filepath.Join(r, "apps", "Magpie.AppImage"))
mkdir(t, filepath.Join(r, "apps", "data"))
t.Setenv("APPIMAGE", img)
t.Setenv("APPDIR", filepath.Join(r, "mount"))
if got, want := Resolve(exe), filepath.Join(r, "apps", "data"); got != want {
t.Errorf("AppImage: %q, want %q", got, want)
}
// inherited from another AppImage (a terminal): magpie isn't in its mount
other := touch(t, filepath.Join(r, "bin", "magpie"))
if got := Resolve(other); got != "" {
t.Errorf("inherited APPIMAGE: %q, want installed", got)
}
}
func TestFolders(t *testing.T) {
t.Setenv("APPIMAGE", "")
r := root(t)
t.Setenv("XDG_CONFIG_HOME", filepath.Join(r, "cfg"))
t.Setenv("XDG_CACHE_HOME", filepath.Join(r, "cache"))
t.Cleanup(func() { UseExecutable("") })
UseExecutable("")
if Portable() != "" || Config() != filepath.Join(r, "cfg", "magpie") || Cache() != filepath.Join(r, "cache", "magpie") {
t.Errorf("installed: portable %q, config %q, cache %q", Portable(), Config(), Cache())
}
if d, _ := SystemCache(); filepath.Base(d) != "magpie" {
t.Errorf("installed system cache %q", d)
}
exe := touch(t, filepath.Join(r, "port", "magpie"))
data := filepath.Join(r, "port", "data")
mkdir(t, data)
UseExecutable(exe)
if Portable() != data || Config() != data || Cache() != filepath.Join(data, "cache") {
t.Errorf("portable: portable %q, config %q, cache %q", Portable(), Config(), Cache())
}
if d, _ := SystemCache(); d != filepath.Join(data, "cache") {
t.Errorf("portable system cache %q", d)
}
}
// #508: portable, WebView2's profile goes into the data folder, not
// %APPDATA%\magpie.exe; installed, it is left at WebView2's default.
func TestWebView(t *testing.T) {
t.Setenv("APPIMAGE", "")
r := root(t)
t.Cleanup(func() { UseExecutable("") })
UseExecutable("")
if got := WebView(); got != "" {
t.Errorf("installed: WebView() = %q, want the default", got)
}
exe := touch(t, filepath.Join(r, "port", "magpie.exe"))
touch(t, filepath.Join(r, "port", ".portable"))
UseExecutable(exe)
if got, want := WebView(), filepath.Join(r, "port", "data", "webview2"); got != want {
t.Errorf("portable: WebView() = %q, want %q", got, want)
}
}
// #989: a folder named data in /Applications, where every app keeps its
// own, isn't magpie's portable data while the installed folder holds
// magpie's files; it is when nothing is installed, or with .portable.
func TestResolveSharedFolder(t *testing.T) {
t.Setenv("APPIMAGE", "")
r := root(t)
apps := filepath.Join(r, "Applications")
was := sharedFolder
sharedFolder = func(dir string) bool { return dir == apps }
t.Cleanup(func() { sharedFolder = was })
t.Setenv("XDG_CONFIG_HOME", filepath.Join(r, "cfg"))
exe := touch(t, filepath.Join(apps, "Magpie.app", "Contents", "MacOS", "magpie"))
data := filepath.Join(apps, "data")
mkdir(t, filepath.Join(data, "nodes"))
touch(t, filepath.Join(data, "install-id"))
// nothing installed: one who keeps magpie portable there
if got := Resolve(exe); got != data {
t.Errorf("nothing installed: %q, want %q", got, data)
}
if got := Passed(exe); got != "" {
t.Errorf("nothing installed: Passed %q", got)
}
// installed magpie has its files: the data folder isn't taken
touch(t, filepath.Join(r, "cfg", "magpie", "providers.json"))
if got := Resolve(exe); got != "" {
t.Errorf("installed has files: %q, want installed", got)
}
if got := Passed(exe); got != data {
t.Errorf("Passed %q, want %q", got, data)
}
t.Cleanup(func() { UseExecutable("") })
UseExecutable(exe)
if Config() != filepath.Join(r, "cfg", "magpie") {
t.Errorf("Config %q, want the installed folder", Config())
}
// asked for with .portable: portable
touch(t, filepath.Join(apps, ".portable"))
if got := Resolve(exe); got != data {
t.Errorf(".portable: %q, want %q", got, data)
}
// any other folder: a data folder beside magpie is its own, as before
other := touch(t, filepath.Join(r, "usb", "Magpie.app", "Contents", "MacOS", "magpie"))
mkdir(t, filepath.Join(r, "usb", "data"))
if got, want := Resolve(other), filepath.Join(r, "usb", "data"); got != want {
t.Errorf("own folder: %q, want %q", got, want)
}
}
func TestSharedFolderIsApplications(t *testing.T) {
if runtime.GOOS != "darwin" {
if sharedFolder("/Applications") {
t.Error("shared off the Mac")
}
return
}
h, _ := Home()
for _, d := range []string{"/Applications", "/Applications/", filepath.Join(h, "Applications")} {
if !sharedFolder(d) {
t.Errorf("%s not shared", d)
}
}
for _, d := range []string{"/Volumes/USB", filepath.Join(h, "Downloads"), "/Applications/Utilities"} {
if sharedFolder(d) {
t.Errorf("%s shared", d)
}
}
}