fix(ios): the device path clears before its runner session too

This commit is contained in:
pj committed 2026-08-15 21:31:53 +05:30
1 parent aed65976e3
commit 67da9313dd
2 files changed
+50 -1

No files matched your search

+20 -1
View File
@@ -31,17 +31,22 @@ type DeviceOptions struct {
BundleID string BundleID string
// AppPath is the .app bundle installed via devicectl for clear-state. // AppPath is the .app bundle installed via devicectl for clear-state.
AppPath string AppPath string
// ClearState reinstalls the app while NewDevice runs, before the runner's
// test session exists. Clear state is a property of the driver rather than
// of a launch: see Launch.
ClearState bool
// Output receives the runner session log path and driver warnings. // Output receives the runner session log path and driver warnings.
Output io.Writer Output io.Writer
// DoubleTapGapMilliseconds overrides the synthesized double-tap gap. // DoubleTapGapMilliseconds overrides the synthesized double-tap gap.
DoubleTapGapMilliseconds float64 DoubleTapGapMilliseconds float64
// Test seams. Production leaves them nil and NewDevice wires the real // Test seams. Production leaves them nil and NewDevice wires the real
// build/spawn/tunnel/dial. // build/spawn/tunnel/dial/devicectl.
spawnRunner func(ctx context.Context, address string) (*exec.Cmd, error) spawnRunner func(ctx context.Context, address string) (*exec.Cmd, error)
startTunnel func(ctx context.Context, hardwareUDID, localAddress, devicePort string) (io.Closer, error) startTunnel func(ctx context.Context, hardwareUDID, localAddress, devicePort string) (io.Closer, error)
dialRunner func(address string) (transport.Companion, error) dialRunner func(address string) (transport.Companion, error)
pickAddress func() (string, error) pickAddress func() (string, error)
reinstallApp func(ctx context.Context) error
} }
// deviceStartupTimeout bounds the runner's startup once its hosting test // deviceStartupTimeout bounds the runner's startup once its hosting test
@@ -61,6 +66,9 @@ func NewDevice(ctx context.Context, options DeviceOptions) (*Driver, error) {
if options.CoreDeviceID == "" { if options.CoreDeviceID == "" {
return nil, errors.New("ios device: CoreDeviceID is required") return nil, errors.New("ios device: CoreDeviceID is required")
} }
if options.ClearState && options.BundleID == "" {
return nil, errors.New("ios device: clear-state needs BundleID: there is nothing to uninstall without it")
}
output := options.Output output := options.Output
if output == nil { if output == nil {
output = io.Discard output = io.Discard
@@ -75,6 +83,7 @@ func NewDevice(ctx context.Context, options DeviceOptions) (*Driver, error) {
coreDeviceID: options.CoreDeviceID, coreDeviceID: options.CoreDeviceID,
bundleID: options.BundleID, bundleID: options.BundleID,
appPath: options.AppPath, appPath: options.AppPath,
clearStateAtStartup: options.ClearState,
output: output, output: output,
doubleTapGapMilliseconds: gap, doubleTapGapMilliseconds: gap,
deviceMode: true, deviceMode: true,
@@ -103,7 +112,10 @@ func NewDevice(ctx context.Context, options DeviceOptions) (*Driver, error) {
// Device seams: clear-state reinstalls via devicectl; the container reset and // Device seams: clear-state reinstalls via devicectl; the container reset and
// paste grant are simulator-only and become no-ops. The runner types // paste grant are simulator-only and become no-ops. The runner types
// natively, so no paste prompt is ever hit. // natively, so no paste prompt is ever hit.
d.reinstallApp = options.reinstallApp
if d.reinstallApp == nil {
d.reinstallApp = d.devicectlReinstall d.reinstallApp = d.devicectlReinstall
}
d.resetContainer = d.deviceResetContainerUnsupported d.resetContainer = d.deviceResetContainerUnsupported
d.grantPaste = func(context.Context) error { return nil } d.grantPaste = func(context.Context) error { return nil }
d.restart = d.respawnDevice d.restart = d.respawnDevice
@@ -116,6 +128,13 @@ func NewDevice(ctx context.Context, options DeviceOptions) (*Driver, error) {
} }
d.deviceLock = lock d.deviceLock = lock
if options.ClearState {
if err := d.clearAppState(ctx); err != nil {
d.Close()
return nil, err
}
}
if err := d.bringUpDevice(ctx); err != nil { if err := d.bringUpDevice(ctx); err != nil {
d.Close() d.Close()
return nil, err return nil, err
@@ -6,6 +6,7 @@ import (
"io" "io"
"net" "net"
"os/exec" "os/exec"
"slices"
"testing" "testing"
"github.com/priyanshujain/sanderling/internal/driver/ioscompanion/transport" "github.com/priyanshujain/sanderling/internal/driver/ioscompanion/transport"
@@ -152,6 +153,35 @@ func TestDeviceEraseAndPressKeyRouteThroughEditor(t *testing.T) {
} }
} }
func TestNewDeviceReinstallsOnceBeforeTheRunnerSession(t *testing.T) {
address := startLoopbackListener(t)
probe := &clearStateProbe{}
options := testDeviceOptions(address, newDeviceCompanion())
options.HardwareUDID = "00008140-CLEAR"
options.AppPath = "/tmp/Sample.app"
options.ClearState = true
options.reinstallApp = func(context.Context) error { probe.record("reinstall"); return nil }
spawn := options.spawnRunner
options.spawnRunner = func(ctx context.Context, runnerAddress string) (*exec.Cmd, error) {
probe.record("runner session")
return spawn(ctx, runnerAddress)
}
d, err := NewDevice(context.Background(), options)
if err != nil {
t.Fatalf("NewDevice: %v", err)
}
defer d.Close()
if err := d.Launch(context.Background(), "", true, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
want := []string{"reinstall", "runner session"}
if got := probe.recorded(); !slices.Equal(got, want) {
t.Fatalf("calls = %v, want %v: devicectl must reinstall once, before the runner's test session attaches", 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}