From 9ede9dc3e2a3971c920897f06beaa23ba539de6d Mon Sep 17 00:00:00 2001 From: PJ Date: Sun, 16 Aug 2026 01:03:02 +0530 Subject: [PATCH] test(ios): pin the stop that has to precede a clear The ordering probe now records the stop, and a scripted xcrun holds what reaches the tool: terminate before get_app_container, with the previous run's files gone after. A simctl terminate that finds nothing to stop still leaves the clear a success. --- internal/driver/ioscompanion/driver_test.go | 58 +++++++++++++++++++-- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/internal/driver/ioscompanion/driver_test.go b/internal/driver/ioscompanion/driver_test.go index bb68213..b4f66ad 100644 --- a/internal/driver/ioscompanion/driver_test.go +++ b/internal/driver/ioscompanion/driver_test.go @@ -232,6 +232,7 @@ func clearStateOptions(t *testing.T, probe *clearStateProbe, udid string, clearS }, reinstallApp: func(context.Context) error { probe.record("reinstall"); return nil }, resetContainer: func(context.Context) error { probe.record("reset container"); return nil }, + terminateApp: func(context.Context) error { probe.record("stop app"); return nil }, } } @@ -249,9 +250,9 @@ func TestClearStateReinstallsOnceBeforeTheRunnerSession(t *testing.T) { t.Fatalf("Launch: %v", err) } - want := []string{"reinstall", "runner session"} + want := []string{"stop app", "reinstall", "runner session"} if got := probe.recorded(); !slices.Equal(got, want) { - t.Fatalf("calls = %v, want %v: the reinstall must run once, before the automation session attaches", got, want) + t.Fatalf("calls = %v, want %v: the reinstall must run once, on a stopped app, before the automation session attaches", got, want) } } @@ -270,9 +271,9 @@ func TestClearStateWithoutAppPathWipesContainerBeforeTheRunnerSession(t *testing t.Fatalf("Launch: %v", err) } - want := []string{"reset container", "runner session"} + want := []string{"stop app", "reset container", "runner session"} if got := probe.recorded(); !slices.Equal(got, want) { - t.Fatalf("calls = %v, want %v: the fallback must wipe the container once, before the session, and never reinstall", got, want) + t.Fatalf("calls = %v, want %v: the fallback must wipe a stopped app's container once, before the session, and never reinstall", got, want) } if warnings := strings.Count(output.String(), "resetting the data container only"); warnings != 1 { t.Fatalf("warning emitted %d times, want once", warnings) @@ -393,6 +394,55 @@ func TestSimctlReinstallProceedsWhenNothingIsInstalled(t *testing.T) { } } +// TestClearStateStopsTheAppBeforeWipingItsContainer covers the ordering Launch +// used to hold. simctl uninstall copes with a running app; deleting the data +// container out from under one does not, and the CI iOS leg passes no app path +// so it is the wipe that runs. A run whose previous run was interrupted finds +// the app still up. +func TestClearStateStopsTheAppBeforeWipingItsContainer(t *testing.T) { + container := t.TempDir() + stale := filepath.Join(container, "Documents") + if err := os.Mkdir(stale, 0o755); err != nil { + t.Fatal(err) + } + log := scriptedXcrun(t, `"simctl terminate "*) :;; +"simctl get_app_container "*) echo `+container+`;;`) + d := &Driver{udid: "SIM-UDID", bundleID: "app.example", output: &bytes.Buffer{}} + d.terminateApp = d.simctlTerminate + d.resetContainer = d.resetDataContainer + + if err := d.clearAppState(context.Background()); err != nil { + t.Fatalf("clearAppState: %v", err) + } + + want := []string{ + "simctl terminate SIM-UDID app.example", + "simctl get_app_container SIM-UDID app.example data", + } + if got := xcrunCalls(t, log); !slices.Equal(got, want) { + t.Fatalf("xcrun calls = %v, want %v: the app was still writing to the container being deleted", got, want) + } + if _, err := os.Stat(stale); !os.IsNotExist(err) { + t.Fatalf("stat %s = %v, want the previous run's state gone", stale, err) + } +} + +// TestClearStateSurvivesAnAppThatIsNotRunning holds the terminate to best +// effort. simctl exits non-zero when there is nothing to stop, and a first run +// on a fresh simulator must not fail on it. +func TestClearStateSurvivesAnAppThatIsNotRunning(t *testing.T) { + container := t.TempDir() + scriptedXcrun(t, `"simctl terminate "*) echo "No matching processes belonging to bundle identifier app.example"; exit 3;; +"simctl get_app_container "*) echo `+container+`;;`) + d := &Driver{udid: "SIM-UDID", bundleID: "app.example", output: &bytes.Buffer{}} + d.terminateApp = d.simctlTerminate + d.resetContainer = d.resetDataContainer + + if err := d.clearAppState(context.Background()); err != nil { + t.Fatalf("clearAppState: %v: an app that is not running is not a failure to clear", err) + } +} + func TestLaunchRejectsEnvironment(t *testing.T) { d := newTestDriver(&fakeCompanion{accessibilityJSON: "[]"}) err := d.Launch(context.Background(), "", false, map[string]string{"K": "V"})