| 1 | //go:build !windows |
| 2 | |
| 3 | package main |
| 4 | |
| 5 | import ( |
| 6 | "os" |
| 7 | "path/filepath" |
| 8 | "runtime" |
| 9 | "strings" |
| 10 | "testing" |
| 11 | "time" |
| 12 | |
| 13 | "reasonix/internal/installlayout" |
| 14 | ) |
| 15 | |
| 16 | func writeShellBootstrapFixture(t *testing.T, goos string) (exe, shell string) { |
| 17 | t.Helper() |
| 18 | root := t.TempDir() |
| 19 | name := installlayout.ShellExecutableNameFor(goos) |
| 20 | if goos == "darwin" { |
| 21 | dir := filepath.Join(root, "Reasonix.app", "Contents", "MacOS") |
| 22 | exe, shell = filepath.Join(dir, "reasonix-desktop"), filepath.Join(dir, name) |
| 23 | } else { |
| 24 | exe, shell = filepath.Join(root, "reasonix-desktop"), filepath.Join(root, installlayout.AppShellDirName, name) |
| 25 | } |
| 26 | if err := os.MkdirAll(filepath.Dir(exe), 0o755); err != nil { |
| 27 | t.Fatal(err) |
| 28 | } |
| 29 | if err := os.WriteFile(exe, []byte("service"), 0o755); err != nil { |
| 30 | t.Fatal(err) |
| 31 | } |
| 32 | return exe, shell |
| 33 | } |
| 34 | |
| 35 | func TestBootstrapShellExecutableBesideResolvesPerPlatform(t *testing.T) { |
| 36 | for _, goos := range []string{"linux", "windows", "darwin"} { |
| 37 | exe, shell := writeShellBootstrapFixture(t, goos) |
| 38 | if got, ok := shellExecutableBeside(exe, goos); ok { |
| 39 | t.Fatalf("%s: resolved %s without an installed shell", goos, got) |
| 40 | } |
| 41 | if err := os.MkdirAll(filepath.Dir(shell), 0o755); err != nil { |
| 42 | t.Fatal(err) |
| 43 | } |
| 44 | if err := os.WriteFile(shell, []byte("shell"), 0o755); err != nil { |
| 45 | t.Fatal(err) |
| 46 | } |
| 47 | got, ok := shellExecutableBeside(exe, goos) |
| 48 | if !ok || got != shell { |
| 49 | t.Fatalf("%s: shell = %q ok=%v, want %q", goos, got, ok, shell) |
| 50 | } |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | func TestMacBundleContentsRecognizesSupportedServiceLayouts(t *testing.T) { |
| 55 | app := filepath.Join(t.TempDir(), "parent.app cache", "应用.app", "Contents") |
| 56 | for _, relative := range []string{"MacOS/reasonix-desktop", "Resources/service/reasonix-desktop"} { |
| 57 | exe := filepath.Join(app, filepath.FromSlash(relative)) |
| 58 | if got, ok := macAppContentsForExecutable(exe); !ok || got != app { |
| 59 | t.Fatalf("%s: contents = %q ok=%v, want %q", relative, got, ok, app) |
| 60 | } |
| 61 | } |
| 62 | for _, exe := range []string{ |
| 63 | filepath.Join(app, "Resources", "reasonix-desktop"), |
| 64 | filepath.Join(t.TempDir(), "reasonix-desktop"), |
| 65 | "relative/reasonix-desktop", |
| 66 | } { |
| 67 | if got, ok := macAppContentsForExecutable(exe); ok { |
| 68 | t.Fatalf("unexpected contents %q for %q", got, exe) |
| 69 | } |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | func TestMacResourcesServiceFindsBundleShell(t *testing.T) { |
| 74 | contents := filepath.Join(t.TempDir(), "Reasonix.app", "Contents") |
| 75 | exe := filepath.Join(contents, "Resources", "service", "reasonix-desktop") |
| 76 | want := filepath.Join(contents, "MacOS", "Reasonix") |
| 77 | if got := shellPathForExecutable(exe, "darwin"); got != want { |
| 78 | t.Fatalf("shell = %q, want %q", got, want) |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | func TestBootstrapShellExecutableBesideNeverReturnsItself(t *testing.T) { |
| 83 | dir := filepath.Join(t.TempDir(), "Reasonix.app", "Contents", "MacOS") |
| 84 | if err := os.MkdirAll(dir, 0o755); err != nil { |
| 85 | t.Fatal(err) |
| 86 | } |
| 87 | exe := filepath.Join(dir, installlayout.ShellExecutableNameFor("darwin")) |
| 88 | if err := os.WriteFile(exe, []byte("wails desktop"), 0o755); err != nil { |
| 89 | t.Fatal(err) |
| 90 | } |
| 91 | if got, ok := shellExecutableBeside(exe, "darwin"); ok { |
| 92 | t.Fatalf("resolved the running executable %s as the shell", got) |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | func TestBootstrapShellStartsDetachedShellWithServiceEnvAndArgs(t *testing.T) { |
| 97 | exe, shell := writeShellBootstrapFixture(t, runtime.GOOS) |
| 98 | out := filepath.Join(t.TempDir(), "shell.out") |
| 99 | script := "#!/bin/sh\nprintf '%s\\n' \"$REASONIX_DESKTOP_SERVICE\" \"$@\" > \"$REASONIX_TEST_SHELL_OUT\"\n" |
| 100 | if err := os.MkdirAll(filepath.Dir(shell), 0o755); err != nil { |
| 101 | t.Fatal(err) |
| 102 | } |
| 103 | if err := os.WriteFile(shell, []byte(script), 0o755); err != nil { |
| 104 | t.Fatal(err) |
| 105 | } |
| 106 | env := append(os.Environ(), "REASONIX_TEST_SHELL_OUT="+out, shellServiceEnv+"=stale") |
| 107 | handled, code := bootstrapShell(exe, runtime.GOOS, []string{"--flag", "two words"}, env) |
| 108 | if !handled || code != 0 { |
| 109 | t.Fatalf("bootstrap handled=%v code=%d", handled, code) |
| 110 | } |
| 111 | deadline := time.Now().Add(10 * time.Second) |
| 112 | want := []string{exe, "--flag", "two words"} |
| 113 | var raw []byte |
| 114 | for { |
| 115 | var err error |
| 116 | if raw, err = os.ReadFile(out); err == nil { |
| 117 | got := strings.Split(strings.TrimSuffix(string(raw), "\n"), "\n") |
| 118 | if strings.Join(got, "\x00") == strings.Join(want, "\x00") { |
| 119 | break |
| 120 | } |
| 121 | } |
| 122 | if time.Now().After(deadline) { |
| 123 | got := strings.Split(strings.TrimSuffix(string(raw), "\n"), "\n") |
| 124 | t.Fatalf("shell saw %q, want %q (read error: %v)", got, want, err) |
| 125 | } |
| 126 | time.Sleep(20 * time.Millisecond) |
| 127 | } |
| 128 | } |
| 129 | |
| 130 | func TestBootstrapShellSkipsWithoutShellBesideBinary(t *testing.T) { |
| 131 | if handled, _ := bootstrapShell(filepath.Join(t.TempDir(), "missing"), runtime.GOOS, nil, nil); handled { |
| 132 | t.Fatal("bootstrap handled a binary without a shell beside it") |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | func TestBootstrapShellIgnoresHostLaunchModes(t *testing.T) { |
| 137 | for _, args := range [][]string{{"--host-rpc"}, {"-emit-contract", t.TempDir()}, {"--emit-contract=" + t.TempDir()}} { |
| 138 | if handled, _ := maybeBootstrapShell(args); handled { |
| 139 | t.Fatalf("args %v must never bootstrap the shell", args) |
| 140 | } |
| 141 | } |
| 142 | } |
| 143 | |
| 144 | func TestLinuxBootstrapMatchesPackagedLayouts(t *testing.T) { |
| 145 | for exe, want := range map[string]string{ |
| 146 | "/opt/reasonix/reasonix-desktop": "/opt/reasonix/app/Reasonix", |
| 147 | "/opt/reasonix/versions/v1.39.0/reasonix-desktop": "/opt/reasonix/versions/v1.39.0/app/Reasonix", |
| 148 | "/usr/bin/reasonix-desktop": "/usr/lib/reasonix/app/Reasonix", |
| 149 | } { |
| 150 | if got := shellPathForExecutable(exe, "linux"); got != want { |
| 151 | t.Errorf("%s: got %s want %s", exe, got, want) |
| 152 | } |
| 153 | } |
| 154 | } |
| 155 |