mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
Merge branch 'clear-state-must-happen' into correctness-and-spec-skills
This commit is contained in:
commit
d977265b30
6 files changed
+283
-4
No files matched your search
@@ -123,14 +123,17 @@ func antiFreezeCommands() [][]string {
|
|||||||
// reinstalling it. This replaces `pm clear` for clear-state: ColorOS and other
|
// reinstalling it. This replaces `pm clear` for clear-state: ColorOS and other
|
||||||
// hardened OEM builds deny CLEAR_APP_USER_DATA even to the adb shell user, so a
|
// hardened OEM builds deny CLEAR_APP_USER_DATA even to the adb shell user, so a
|
||||||
// clear aborts the launch, whereas uninstall+install is always permitted.
|
// clear aborts the launch, whereas uninstall+install is always permitted.
|
||||||
// The uninstall is best effort so a not-installed app is not an error.
|
// A failed uninstall is not passed over: `install -r` keeps the app's data, so
|
||||||
|
// the reinstall would report a clear-state that never happened.
|
||||||
func ReinstallApp(ctx context.Context, serial, bundleID, apkPath string, stdout io.Writer) error {
|
func ReinstallApp(ctx context.Context, serial, bundleID, apkPath string, stdout io.Writer) error {
|
||||||
adb, err := AdbBinary()
|
adb, err := AdbBinary()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if output, err := exec.CommandContext(ctx, adb, adbArgs(serial, "uninstall", bundleID)...).CombinedOutput(); err != nil {
|
if output, err := exec.CommandContext(ctx, adb, adbArgs(serial, "uninstall", bundleID)...).CombinedOutput(); err != nil {
|
||||||
fmt.Fprintf(stdout, "clear-state: uninstall %s skipped (%v: %s)\n", bundleID, err, strings.TrimSpace(string(output)))
|
if err := clearDataUninstallLeftBehind(ctx, adb, serial, bundleID, strings.TrimSpace(string(output)), stdout); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if output, err := exec.CommandContext(ctx, adb, adbArgs(serial, "install", "-r", apkPath)...).CombinedOutput(); err != nil {
|
if output, err := exec.CommandContext(ctx, adb, adbArgs(serial, "install", "-r", apkPath)...).CombinedOutput(); err != nil {
|
||||||
return fmt.Errorf("install %s: %w: %s", apkPath, err, strings.TrimSpace(string(output)))
|
return fmt.Errorf("install %s: %w: %s", apkPath, err, strings.TrimSpace(string(output)))
|
||||||
@@ -138,6 +141,37 @@ func ReinstallApp(ctx context.Context, serial, bundleID, apkPath string, stdout
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clearDataUninstallLeftBehind reaches first-launch state after `adb uninstall`
|
||||||
|
// failed. The failure text cannot say why: an API 34 emulator answers
|
||||||
|
// "Failure [DELETE_FAILED_INTERNAL_ERROR]" both for a package that was never
|
||||||
|
// installed and for one it refuses to remove. So ask the package manager which
|
||||||
|
// happened. Nothing installed means nothing to clear. Still installed
|
||||||
|
// means the data survives the reinstall, and `pm clear` is the one remaining
|
||||||
|
// way to reach first-launch state; when that fails too, so does clear-state.
|
||||||
|
func clearDataUninstallLeftBehind(ctx context.Context, adb, serial, bundleID, uninstallOutput string, stdout io.Writer) error {
|
||||||
|
if !packageInstalled(ctx, adb, serial, bundleID) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
output, err := exec.CommandContext(ctx, adb, adbArgs(serial, "shell", "pm", "clear", bundleID)...).CombinedOutput()
|
||||||
|
cleared := strings.TrimSpace(string(output))
|
||||||
|
if err != nil || !strings.Contains(cleared, "Success") {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"clear-state: %s is still installed after `adb uninstall` said %q, and `pm clear` said %q: its data was not cleared",
|
||||||
|
bundleID, uninstallOutput, cleared,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(stdout, "clear-state: uninstall %s said %q and left it installed; cleared its data with `pm clear` instead\n", bundleID, uninstallOutput)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// packageInstalled reports whether the package manager resolves an APK path for
|
||||||
|
// bundleID. The printed path is the signal rather than the exit status, which
|
||||||
|
// `adb shell` does not forward from devices below API 24.
|
||||||
|
func packageInstalled(ctx context.Context, adb, serial, bundleID string) bool {
|
||||||
|
output, _ := exec.CommandContext(ctx, adb, adbArgs(serial, "shell", "pm", "path", bundleID)...).Output()
|
||||||
|
return strings.HasPrefix(strings.TrimSpace(string(output)), "package:")
|
||||||
|
}
|
||||||
|
|
||||||
const threeButtonNavOverlay = "com.android.internal.systemui.navbar.threebutton"
|
const threeButtonNavOverlay = "com.android.internal.systemui.navbar.threebutton"
|
||||||
|
|
||||||
// navModeOverlays are the system navigation-mode overlays. Only one is active at
|
// navModeOverlays are the system navigation-mode overlays. Only one is active at
|
||||||
|
|||||||
@@ -441,3 +441,134 @@ func TestNavModeToRestore(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// scriptedAdb puts an adb under a fake SDK root that logs each invocation's
|
||||||
|
// arguments and answers from replies, a `case "$*" in` body. SDK lookup is
|
||||||
|
// isolated onto that root, so ReinstallApp runs its real command sequence
|
||||||
|
// against the script and the log holds what reached adb.
|
||||||
|
func scriptedAdb(t *testing.T, replies string) string {
|
||||||
|
t.Helper()
|
||||||
|
root := t.TempDir()
|
||||||
|
log := filepath.Join(root, "adb.log")
|
||||||
|
adb := filepath.Join(root, "platform-tools", "adb")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(adb), 0o755); err != nil {
|
||||||
|
t.Fatalf("mkdir %s: %v", filepath.Dir(adb), err)
|
||||||
|
}
|
||||||
|
script := "#!/bin/sh\necho \"$*\" >> " + log + "\ncase \"$*\" in\n" + replies + "\nesac\n"
|
||||||
|
if err := os.WriteFile(adb, []byte(script), 0o755); err != nil {
|
||||||
|
t.Fatalf("write %s: %v", adb, err)
|
||||||
|
}
|
||||||
|
isolateSDKLookup(t, root)
|
||||||
|
return log
|
||||||
|
}
|
||||||
|
|
||||||
|
func adbCalls(t *testing.T, log string) []string {
|
||||||
|
t.Helper()
|
||||||
|
contents, err := os.ReadFile(log)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
t.Fatalf("read %s: %v", log, err)
|
||||||
|
}
|
||||||
|
return strings.Split(strings.TrimSpace(string(contents)), "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
uninstallFails = `"uninstall "*) echo "Failure [DELETE_FAILED_INTERNAL_ERROR]"; exit 1;;`
|
||||||
|
stillInstalled = `"shell pm path "*) echo "package:/data/app/app.example-1/base.apk";;`
|
||||||
|
notInstalled = `"shell pm path "*) exit 1;;`
|
||||||
|
installSucceeds = `"install "*) echo "Success";;`
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReinstallApp_RefusedUninstallClearsTheDataItLeftBehind(t *testing.T) {
|
||||||
|
log := scriptedAdb(t, strings.Join([]string{
|
||||||
|
uninstallFails,
|
||||||
|
stillInstalled,
|
||||||
|
`"shell pm clear "*) echo "Success";;`,
|
||||||
|
installSucceeds,
|
||||||
|
}, "\n"))
|
||||||
|
output := &strings.Builder{}
|
||||||
|
|
||||||
|
if err := ReinstallApp(t.Context(), "", "app.example", "/tmp/app.apk", output); err != nil {
|
||||||
|
t.Fatalf("ReinstallApp: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []string{
|
||||||
|
"uninstall app.example",
|
||||||
|
"shell pm path app.example",
|
||||||
|
"shell pm clear app.example",
|
||||||
|
"install -r /tmp/app.apk",
|
||||||
|
}
|
||||||
|
if got := adbCalls(t, log); !slices.Equal(got, want) {
|
||||||
|
t.Fatalf("adb calls = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output.String(), "pm clear") {
|
||||||
|
t.Errorf("output %q does not say the data was cleared some other way", output.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReinstallApp_RefusedUninstallThatCannotBeClearedIsFatal(t *testing.T) {
|
||||||
|
log := scriptedAdb(t, strings.Join([]string{
|
||||||
|
uninstallFails,
|
||||||
|
stillInstalled,
|
||||||
|
`"shell pm clear "*) echo "Failed"; exit 1;;`,
|
||||||
|
installSucceeds,
|
||||||
|
}, "\n"))
|
||||||
|
|
||||||
|
err := ReinstallApp(t.Context(), "", "app.example", "/tmp/app.apk", &strings.Builder{})
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("ReinstallApp reported success while app.example kept the data clear-state was asked to remove")
|
||||||
|
}
|
||||||
|
for _, want := range []string{"app.example", "DELETE_FAILED_INTERNAL_ERROR", "Failed"} {
|
||||||
|
if !strings.Contains(err.Error(), want) {
|
||||||
|
t.Errorf("error %q does not quote %q", err, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if slices.Contains(adbCalls(t, log), "install -r /tmp/app.apk") {
|
||||||
|
t.Error("installed over an app whose data survived, which is the reinstall reporting a clear it did not perform")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReinstallApp_UninstallFailureWithNothingInstalledIsQuiet(t *testing.T) {
|
||||||
|
log := scriptedAdb(t, strings.Join([]string{
|
||||||
|
uninstallFails,
|
||||||
|
notInstalled,
|
||||||
|
installSucceeds,
|
||||||
|
}, "\n"))
|
||||||
|
output := &strings.Builder{}
|
||||||
|
|
||||||
|
if err := ReinstallApp(t.Context(), "", "app.example", "/tmp/app.apk", output); err != nil {
|
||||||
|
t.Fatalf("ReinstallApp: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
calls := adbCalls(t, log)
|
||||||
|
if !slices.Contains(calls, "install -r /tmp/app.apk") {
|
||||||
|
t.Fatalf("adb calls = %v, want the install to go ahead: a first run has no app to uninstall", calls)
|
||||||
|
}
|
||||||
|
if slices.Contains(calls, "shell pm clear app.example") {
|
||||||
|
t.Errorf("adb calls = %v, want no data clear: there was no app holding data", calls)
|
||||||
|
}
|
||||||
|
if output.String() != "" {
|
||||||
|
t.Errorf("output = %q, want nothing: a first run has no app to uninstall", output.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReinstallApp_SuccessfulUninstallNeedsNoFallback(t *testing.T) {
|
||||||
|
log := scriptedAdb(t, strings.Join([]string{
|
||||||
|
`"uninstall "*) echo "Success";;`,
|
||||||
|
stillInstalled,
|
||||||
|
`"shell pm clear "*) echo "Success";;`,
|
||||||
|
installSucceeds,
|
||||||
|
}, "\n"))
|
||||||
|
|
||||||
|
if err := ReinstallApp(t.Context(), "", "app.example", "/tmp/app.apk", &strings.Builder{}); err != nil {
|
||||||
|
t.Fatalf("ReinstallApp: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []string{"uninstall app.example", "install -r /tmp/app.apk"}
|
||||||
|
if got := adbCalls(t, log); !slices.Equal(got, want) {
|
||||||
|
t.Fatalf("adb calls = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -224,8 +224,14 @@ func (d *Driver) respawnDevice(ctx context.Context) error {
|
|||||||
// devicectlReinstall uninstalls then installs the app bundle via devicectl,
|
// devicectlReinstall uninstalls then installs the app bundle via devicectl,
|
||||||
// keyed on the CoreDevice id. App lifecycle stays with devicectl: the runner's
|
// keyed on the CoreDevice id. App lifecycle stays with devicectl: the runner's
|
||||||
// own install path is simulator-specific.
|
// own install path is simulator-specific.
|
||||||
|
// A failed uninstall ends the reinstall: installing over an app keeps its data,
|
||||||
|
// so clear-state would be reported without happening. Uninstalling an app that
|
||||||
|
// is not installed exits 0 ("App uninstalled." on a paired iPhone running iOS
|
||||||
|
// 26.5), so there is no benign failure here to sort out from a real one.
|
||||||
func (d *Driver) devicectlReinstall(ctx context.Context) error {
|
func (d *Driver) devicectlReinstall(ctx context.Context) error {
|
||||||
_ = exec.CommandContext(ctx, "xcrun", "devicectl", "device", "uninstall", "app", "--device", d.coreDeviceID, d.bundleID).Run()
|
if output, err := exec.CommandContext(ctx, "xcrun", "devicectl", "device", "uninstall", "app", "--device", d.coreDeviceID, d.bundleID).CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("devicectl uninstall %s: %w: %s", d.bundleID, err, strings.TrimSpace(string(output)))
|
||||||
|
}
|
||||||
output, err := exec.CommandContext(ctx, "xcrun", "devicectl", "device", "install", "app", "--device", d.coreDeviceID, d.appPath).CombinedOutput()
|
output, err := exec.CommandContext(ctx, "xcrun", "devicectl", "device", "install", "app", "--device", d.coreDeviceID, d.appPath).CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("devicectl install: %w: %s", err, strings.TrimSpace(string(output)))
|
return fmt.Errorf("devicectl install: %w: %s", err, strings.TrimSpace(string(output)))
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/priyanshujain/sanderling/internal/driver/ioscompanion/transport"
|
"github.com/priyanshujain/sanderling/internal/driver/ioscompanion/transport"
|
||||||
@@ -182,6 +183,45 @@ func TestNewDeviceReinstallsOnceBeforeTheRunnerSession(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDevicectlReinstallStopsWhenTheUninstallFails(t *testing.T) {
|
||||||
|
log := scriptedXcrun(t, `"devicectl device uninstall "*) echo "ERROR: Internal logic error: Connection was invalidated"; exit 1;;
|
||||||
|
"devicectl device install "*) :;;`)
|
||||||
|
d := &Driver{coreDeviceID: "CORE-DEVICE", bundleID: "app.example", appPath: "/tmp/Sample.app"}
|
||||||
|
|
||||||
|
err := d.devicectlReinstall(context.Background())
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("devicectlReinstall reported success while app.example kept the data clear-state was asked to remove")
|
||||||
|
}
|
||||||
|
for _, want := range []string{"app.example", "Connection was invalidated"} {
|
||||||
|
if !strings.Contains(err.Error(), want) {
|
||||||
|
t.Errorf("error %q does not quote %q", err, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
calls := xcrunCalls(t, log)
|
||||||
|
if slices.ContainsFunc(calls, func(call string) bool { return strings.HasPrefix(call, "devicectl device install") }) {
|
||||||
|
t.Errorf("xcrun calls = %v: installing over the app carries its data into the run", calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDevicectlReinstallProceedsWhenNothingIsInstalled(t *testing.T) {
|
||||||
|
log := scriptedXcrun(t, `"devicectl device uninstall "*) echo "App uninstalled.";;
|
||||||
|
"devicectl device install "*) :;;`)
|
||||||
|
d := &Driver{coreDeviceID: "CORE-DEVICE", bundleID: "app.example", appPath: "/tmp/Sample.app"}
|
||||||
|
|
||||||
|
if err := d.devicectlReinstall(context.Background()); err != nil {
|
||||||
|
t.Fatalf("devicectlReinstall: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []string{
|
||||||
|
"devicectl device uninstall app --device CORE-DEVICE app.example",
|
||||||
|
"devicectl device install app --device CORE-DEVICE /tmp/Sample.app",
|
||||||
|
}
|
||||||
|
if got := xcrunCalls(t, log); !slices.Equal(got, want) {
|
||||||
|
t.Fatalf("xcrun calls = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDeviceClearStateWithoutAppPathWarnsOnce(t *testing.T) {
|
func TestDeviceClearStateWithoutAppPathWarnsOnce(t *testing.T) {
|
||||||
output := &bytes.Buffer{}
|
output := &bytes.Buffer{}
|
||||||
d := &Driver{output: output, deviceMode: true}
|
d := &Driver{output: output, deviceMode: true}
|
||||||
|
|||||||
@@ -615,8 +615,14 @@ func (d *Driver) clearAppState(ctx context.Context) error {
|
|||||||
// simctlReinstall uninstalls and reinstalls the app bundle via simctl. App
|
// simctlReinstall uninstalls and reinstalls the app bundle via simctl. App
|
||||||
// lifecycle stays with simctl: the companion's install RPC misreads current
|
// lifecycle stays with simctl: the companion's install RPC misreads current
|
||||||
// simulator targets' architectures and rejects valid bundles.
|
// simulator targets' architectures and rejects valid bundles.
|
||||||
|
// A failed uninstall ends the reinstall: `simctl install` over an installed app
|
||||||
|
// carries its data container across, so clear-state would be reported without
|
||||||
|
// happening. Uninstalling an app that is not installed exits 0, so there is no
|
||||||
|
// benign failure here to sort out from a real one.
|
||||||
func (d *Driver) simctlReinstall(ctx context.Context) error {
|
func (d *Driver) simctlReinstall(ctx context.Context) error {
|
||||||
_ = exec.CommandContext(ctx, "xcrun", "simctl", "uninstall", d.udid, d.bundleID).Run()
|
if output, err := exec.CommandContext(ctx, "xcrun", "simctl", "uninstall", d.udid, d.bundleID).CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("simctl uninstall %s: %w: %s", d.bundleID, err, strings.TrimSpace(string(output)))
|
||||||
|
}
|
||||||
output, err := exec.CommandContext(ctx, "xcrun", "simctl", "install", d.udid, d.appPath).CombinedOutput()
|
output, err := exec.CommandContext(ctx, "xcrun", "simctl", "install", d.udid, d.appPath).CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("simctl install: %w: %s", err, strings.TrimSpace(string(output)))
|
return fmt.Errorf("simctl install: %w: %s", err, strings.TrimSpace(string(output)))
|
||||||
|
|||||||
@@ -329,6 +329,68 @@ func TestNewRejectsClearStateWithoutBundleID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// scriptedXcrun puts an xcrun on PATH that logs each invocation's arguments and
|
||||||
|
// answers from replies, a `case "$*" in` body, so a reinstall runs its real
|
||||||
|
// command sequence and the log holds what reached the tool.
|
||||||
|
func scriptedXcrun(t *testing.T, replies string) string {
|
||||||
|
t.Helper()
|
||||||
|
directory := t.TempDir()
|
||||||
|
log := filepath.Join(directory, "xcrun.log")
|
||||||
|
script := "#!/bin/sh\necho \"$*\" >> " + log + "\ncase \"$*\" in\n" + replies + "\nesac\n"
|
||||||
|
if err := os.WriteFile(filepath.Join(directory, "xcrun"), []byte(script), 0o755); err != nil {
|
||||||
|
t.Fatalf("write xcrun: %v", err)
|
||||||
|
}
|
||||||
|
t.Setenv("PATH", directory)
|
||||||
|
return log
|
||||||
|
}
|
||||||
|
|
||||||
|
func xcrunCalls(t *testing.T, log string) []string {
|
||||||
|
t.Helper()
|
||||||
|
contents, err := os.ReadFile(log)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
t.Fatalf("read %s: %v", log, err)
|
||||||
|
}
|
||||||
|
return strings.Split(strings.TrimSpace(string(contents)), "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimctlReinstallStopsWhenTheUninstallFails(t *testing.T) {
|
||||||
|
log := scriptedXcrun(t, `"simctl uninstall "*) echo "Simulator device failed to uninstall app.example."; echo "Uninstall prohibited."; exit 22;;
|
||||||
|
"simctl install "*) :;;`)
|
||||||
|
d := &Driver{udid: "SIM-UDID", bundleID: "app.example", appPath: "/tmp/Sample.app"}
|
||||||
|
|
||||||
|
err := d.simctlReinstall(context.Background())
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("simctlReinstall reported success while app.example kept the data clear-state was asked to remove")
|
||||||
|
}
|
||||||
|
for _, want := range []string{"app.example", "Uninstall prohibited."} {
|
||||||
|
if !strings.Contains(err.Error(), want) {
|
||||||
|
t.Errorf("error %q does not quote %q", err, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if calls := xcrunCalls(t, log); slices.Contains(calls, "simctl install SIM-UDID /tmp/Sample.app") {
|
||||||
|
t.Errorf("xcrun calls = %v: installing over the app carries its data into the run", calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimctlReinstallProceedsWhenNothingIsInstalled(t *testing.T) {
|
||||||
|
log := scriptedXcrun(t, `"simctl uninstall "*) :;;
|
||||||
|
"simctl install "*) :;;`)
|
||||||
|
d := &Driver{udid: "SIM-UDID", bundleID: "app.example", appPath: "/tmp/Sample.app"}
|
||||||
|
|
||||||
|
if err := d.simctlReinstall(context.Background()); err != nil {
|
||||||
|
t.Fatalf("simctlReinstall: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []string{"simctl uninstall SIM-UDID app.example", "simctl install SIM-UDID /tmp/Sample.app"}
|
||||||
|
if got := xcrunCalls(t, log); !slices.Equal(got, want) {
|
||||||
|
t.Fatalf("xcrun calls = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLaunchRejectsEnvironment(t *testing.T) {
|
func TestLaunchRejectsEnvironment(t *testing.T) {
|
||||||
d := newTestDriver(&fakeCompanion{accessibilityJSON: "[]"})
|
d := newTestDriver(&fakeCompanion{accessibilityJSON: "[]"})
|
||||||
err := d.Launch(context.Background(), "", false, map[string]string{"K": "V"})
|
err := d.Launch(context.Background(), "", false, map[string]string{"K": "V"})
|
||||||
|
|||||||
Reference in new issue
Block a user