Merge branch 'ios-clear-before-session' into trial-merge

This commit is contained in:
pj committed 2026-08-15 21:43:31 +05:30
commit dba9da8575
6 files changed
+290 -59

No files matched your search

+25 -6
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 = d.devicectlReinstall d.reinstallApp = options.reinstallApp
if d.reinstallApp == nil {
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}
+58 -21
View File
@@ -73,16 +73,24 @@ type Options struct {
// AppPath is the .app bundle directory. Required for clear-state reinstall; // AppPath is the .app bundle directory. Required for clear-state reinstall;
// when empty, clear state falls back to resetting the data container. // when empty, clear state falls back to resetting the data container.
AppPath string AppPath string
// ClearState resets the app to first-launch state while New runs, before
// any automation session attaches. Clear state is a property of the driver
// rather than of a launch: see Launch.
ClearState bool
// Output receives companion stdout and stderr plus driver warnings. // Output receives companion stdout and stderr plus 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
// spawnChild, dialCompanion, and pickAddress are test seams. Production // These are test seams. Production leaves them nil and New wires the real
// leaves them nil and New wires the real extraction, spawn, and dial. // extraction, spawn, dial and simctl calls.
spawnChild func(ctx context.Context, address string) (*exec.Cmd, error) spawnChild func(ctx context.Context, address string) (*exec.Cmd, error)
dialCompanion func(address string) (transport.Companion, error) dialCompanion func(address string) (transport.Companion, error)
pickAddress func() (string, error) pickAddress func() (string, error)
spawnRunner func(ctx context.Context, address string) (*exec.Cmd, error)
dialRunner func(address string) (transport.Companion, error)
reinstallApp func(ctx context.Context) error
resetContainer func(ctx context.Context) error
} }
// Driver implements driver.DeviceDriver against an iOS simulator companion. // Driver implements driver.DeviceDriver against an iOS simulator companion.
@@ -93,6 +101,12 @@ type Driver struct {
appPath string appPath string
output io.Writer output io.Writer
// clearStateAtStartup records that New (or NewDevice) reset the app to
// first-launch state before attaching, which is the only point in a run
// where clearing is safe. Launch refuses a clear-state request the driver
// was not built for rather than reinstalling under a live session.
clearStateAtStartup bool
screenWidth int screenWidth int
screenHeight int screenHeight int
@@ -137,12 +151,13 @@ type Driver struct {
// lifecycle, screenshot) with an in-simulator runner that serves // lifecycle, screenshot) with an in-simulator runner that serves
// collapse-free accessibility snapshots and native unicode typing. // collapse-free accessibility snapshots and native unicode typing.
// runnerClient is nil on the legacy-only path. // runnerClient is nil on the legacy-only path.
runnerClient transport.Companion runnerClient transport.Companion
runnerChild *exec.Cmd runnerChild *exec.Cmd
runnerAddress string runnerAddress string
spawnRunner func(ctx context.Context, address string) (*exec.Cmd, error) spawnRunner func(ctx context.Context, address string) (*exec.Cmd, error)
dialRunner func(address string) (transport.Companion, error) dialRunner func(address string) (transport.Companion, error)
hybrid bool pickRunnerAddress func() (string, error)
hybrid bool
// Device-mode fields. On the physical-device path d.companion is the runner // Device-mode fields. On the physical-device path d.companion is the runner
// dialed over a usbmux tunnel, hybrid is false, and runnerClient is nil. // dialed over a usbmux tunnel, hybrid is false, and runnerClient is nil.
@@ -197,6 +212,9 @@ func New(ctx context.Context, options Options) (*Driver, error) {
if options.UniqueDeviceIdentifier == "" { if options.UniqueDeviceIdentifier == "" {
return nil, errors.New("ios companion: UniqueDeviceIdentifier is required") return nil, errors.New("ios companion: UniqueDeviceIdentifier is required")
} }
if options.ClearState && options.BundleID == "" {
return nil, errors.New("ios companion: clear-state needs BundleID: there is nothing to uninstall or wipe without it")
}
output := options.Output output := options.Output
if output == nil { if output == nil {
output = io.Discard output = io.Discard
@@ -210,10 +228,15 @@ func New(ctx context.Context, options Options) (*Driver, error) {
udid: options.UniqueDeviceIdentifier, udid: options.UniqueDeviceIdentifier,
bundleID: options.BundleID, bundleID: options.BundleID,
appPath: options.AppPath, appPath: options.AppPath,
clearStateAtStartup: options.ClearState,
output: output, output: output,
doubleTapGapMilliseconds: gap, doubleTapGapMilliseconds: gap,
spawnChild: options.spawnChild, spawnChild: options.spawnChild,
dial: options.dialCompanion, dial: options.dialCompanion,
spawnRunner: options.spawnRunner,
dialRunner: options.dialRunner,
reinstallApp: options.reinstallApp,
resetContainer: options.resetContainer,
hybrid: hybridCompanionEnabled(), hybrid: hybridCompanionEnabled(),
} }
if driverInstance.spawnChild == nil { if driverInstance.spawnChild == nil {
@@ -240,9 +263,14 @@ func New(ctx context.Context, options Options) (*Driver, error) {
return nil, err return nil, err
} }
driverInstance.address = address driverInstance.address = address
driverInstance.pickRunnerAddress = pickAddress
driverInstance.restart = driverInstance.respawnAndRedial driverInstance.restart = driverInstance.respawnAndRedial
driverInstance.resetContainer = driverInstance.resetDataContainer if driverInstance.resetContainer == nil {
driverInstance.reinstallApp = driverInstance.simctlReinstall driverInstance.resetContainer = driverInstance.resetDataContainer
}
if driverInstance.reinstallApp == nil {
driverInstance.reinstallApp = driverInstance.simctlReinstall
}
driverInstance.grantPaste = driverInstance.grantPasteboardAccess driverInstance.grantPaste = driverInstance.grantPasteboardAccess
driverInstance.processContext, driverInstance.processCancel = context.WithCancel(ctx) driverInstance.processContext, driverInstance.processCancel = context.WithCancel(ctx)
@@ -253,6 +281,13 @@ func New(ctx context.Context, options Options) (*Driver, error) {
} }
driverInstance.deviceLock = lock driverInstance.deviceLock = lock
if options.ClearState {
if err := driverInstance.clearAppState(ctx); err != nil {
driverInstance.Close()
return nil, err
}
}
if err := driverInstance.bringUp(ctx); err != nil { if err := driverInstance.bringUp(ctx); err != nil {
driverInstance.Close() driverInstance.Close()
return nil, err return nil, err
@@ -366,7 +401,7 @@ func (d *Driver) bringUpRunner(ctx context.Context) error {
// A fresh port every bring-up: after a restart the dying session's // A fresh port every bring-up: after a restart the dying session's
// listener may still answer on the old port and would satisfy the wait // listener may still answer on the old port and would satisfy the wait
// below with a dead server. // below with a dead server.
address, err := pickLoopbackAddress() address, err := d.pickRunnerAddress()
if err != nil { if err != nil {
return err return err
} }
@@ -460,6 +495,13 @@ func (d *Driver) Launch(ctx context.Context, bundleID string, clearState bool, e
// loudly rather than silently dropping the request. // loudly rather than silently dropping the request.
return errors.New("ios companion: launch with environment variables is unsupported on this backend") return errors.New("ios companion: launch with environment variables is unsupported on this backend")
} }
if clearState && !d.clearStateAtStartup {
// Clearing here would uninstall and reinstall the app underneath a live
// automation session, which is what races FrontBoard's registration and
// leaves the session launching a bundle FrontBoard has not registered.
return errors.New("ios companion: clear-state must be requested when the driver is created (Options.ClearState); " +
"this backend clears the app before its automation session exists")
}
// Terminate first so the launch is a clean cold start regardless of the // Terminate first so the launch is a clean cold start regardless of the
// app's prior state. A not-running app is not an error here. // app's prior state. A not-running app is not an error here.
@@ -467,12 +509,6 @@ func (d *Driver) Launch(ctx context.Context, bundleID string, clearState bool, e
return companion.Terminate(callCtx, d.bundleID) return companion.Terminate(callCtx, d.bundleID)
}) })
if clearState {
if err := d.clearAppState(ctx); err != nil {
return err
}
}
// Grant the app pasteboard access before it runs so unicode input (which // Grant the app pasteboard access before it runs so unicode input (which
// must go through the pasteboard, since HID cannot express it) never trips // must go through the pasteboard, since HID cannot express it) never trips
// the iOS paste-permission prompt. clearState reinstall resets the grant, // the iOS paste-permission prompt. clearState reinstall resets the grant,
@@ -560,7 +596,8 @@ func (d *Driver) lifecycleCompanion() transport.Companion {
// clearAppState resets the app to a first-launch state. With an app path it // clearAppState resets the app to a first-launch state. With an app path it
// uninstalls and reinstalls; without one it falls back to wiping the app's data // uninstalls and reinstalls; without one it falls back to wiping the app's data
// container and warns once that a full reinstall needs the app path. // container and warns once that a full reinstall needs the app path. Called
// only from construction, before any automation session is attached to the app.
func (d *Driver) clearAppState(ctx context.Context) error { func (d *Driver) clearAppState(ctx context.Context) error {
if d.appPath != "" { if d.appPath != "" {
if err := d.reinstallApp(ctx); err != nil { if err := d.reinstallApp(ctx); err != nil {
+132 -29
View File
@@ -12,6 +12,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
"sync" "sync"
"testing" "testing"
@@ -182,47 +183,149 @@ func TestLaunchContinuesWhenGrantFails(t *testing.T) {
} }
} }
func TestLaunchClearStateReinstallsWithAppPath(t *testing.T) { // clearStateProbe records, in order, the calls a run makes to reset the app and
// to bring the runner's automation session up. A reinstall recorded after the
// session is the ordering that races FrontBoard.
type clearStateProbe struct {
mutex sync.Mutex
events []string
}
func (p *clearStateProbe) record(event string) {
p.mutex.Lock()
defer p.mutex.Unlock()
p.events = append(p.events, event)
}
func (p *clearStateProbe) recorded() []string {
p.mutex.Lock()
defer p.mutex.Unlock()
out := make([]string, len(p.events))
copy(out, p.events)
return out
}
// clearStateOptions wires every seam a hybrid bring-up needs, so New runs its
// real sequence against fakes: no simulator, no simctl, no XCTest session.
func clearStateOptions(t *testing.T, probe *clearStateProbe, udid string, clearState bool) Options {
t.Helper()
t.Setenv("SANDERLING_SIMULATOR_COMPANION", "")
address := startLoopbackListener(t)
return Options{
UniqueDeviceIdentifier: udid,
BundleID: "com.example.app",
ClearState: clearState,
Output: &bytes.Buffer{},
pickAddress: func() (string, error) { return address, nil },
spawnChild: func(context.Context, string) (*exec.Cmd, error) { return &exec.Cmd{}, nil },
dialCompanion: func(string) (transport.Companion, error) {
return &fakeCompanion{accessibilityJSON: "[]"}, nil
},
spawnRunner: func(context.Context, string) (*exec.Cmd, error) {
probe.record("runner session")
return &exec.Cmd{}, nil
},
dialRunner: func(string) (transport.Companion, error) {
return &fakeCompanion{accessibilityJSON: "[]"}, nil
},
reinstallApp: func(context.Context) error { probe.record("reinstall"); return nil },
resetContainer: func(context.Context) error { probe.record("reset container"); return nil },
}
}
func TestClearStateReinstallsOnceBeforeTheRunnerSession(t *testing.T) {
probe := &clearStateProbe{}
options := clearStateOptions(t, probe, "CLEAR-REINSTALL-UDID", true)
options.AppPath = "/tmp/Sample.app"
d, err := New(context.Background(), options)
if err != nil {
t.Fatalf("New: %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: the reinstall must run once, before the automation session attaches", got, want)
}
}
func TestClearStateWithoutAppPathWipesContainerBeforeTheRunnerSession(t *testing.T) {
probe := &clearStateProbe{}
output := &bytes.Buffer{}
options := clearStateOptions(t, probe, "CLEAR-CONTAINER-UDID", true)
options.Output = output
d, err := New(context.Background(), options)
if err != nil {
t.Fatalf("New: %v", err)
}
defer d.Close()
if err := d.Launch(context.Background(), "", true, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
want := []string{"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)
}
if warnings := strings.Count(output.String(), "resetting the data container only"); warnings != 1 {
t.Fatalf("warning emitted %d times, want once", warnings)
}
}
func TestWithoutClearStateTheAppIsLeftAlone(t *testing.T) {
probe := &clearStateProbe{}
options := clearStateOptions(t, probe, "NO-CLEAR-UDID", false)
options.AppPath = "/tmp/Sample.app"
d, err := New(context.Background(), options)
if err != nil {
t.Fatalf("New: %v", err)
}
defer d.Close()
if err := d.Launch(context.Background(), "", false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
want := []string{"runner session"}
if got := probe.recorded(); !slices.Equal(got, want) {
t.Fatalf("calls = %v, want %v: a run that did not ask for clear state must not touch the install", got, want)
}
}
func TestLaunchRefusesClearStateTheDriverWasNotBuiltFor(t *testing.T) {
companion := &fakeCompanion{accessibilityJSON: "[]"} companion := &fakeCompanion{accessibilityJSON: "[]"}
d := newTestDriver(companion) d := newTestDriver(companion)
d.appPath = "/tmp/Sample.app" d.appPath = "/tmp/Sample.app"
reinstalls := 0 reinstalls := 0
d.reinstallApp = func(context.Context) error { reinstalls++; return nil } d.reinstallApp = func(context.Context) error { reinstalls++; return nil }
if err := d.Launch(context.Background(), "", true, nil); err != nil {
t.Fatalf("Launch: %v", err) err := d.Launch(context.Background(), "", true, nil)
if err == nil || !strings.Contains(err.Error(), "clear-state") {
t.Fatalf("Launch err = %v, want a refusal naming clear-state", err)
} }
if reinstalls != 1 { if reinstalls != 0 {
t.Fatalf("clear-state with app path must reinstall exactly once; got %d", reinstalls) t.Fatalf("reinstalls = %d, want 0: a live session must never have the app reinstalled under it", reinstalls)
} }
if indexOf(companion.calls, "launch") < indexOf(companion.calls, "terminate") { if indexOf(companion.recorded(), "launch") >= 0 {
t.Fatalf("launch must still follow terminate; got %v", companion.calls) t.Fatalf("a refused launch must not reach the companion; got %v", companion.recorded())
} }
} }
func TestLaunchClearStateFallbackWarnsOnce(t *testing.T) { func TestNewRejectsClearStateWithoutBundleID(t *testing.T) {
companion := &fakeCompanion{accessibilityJSON: "[]"} probe := &clearStateProbe{}
output := &bytes.Buffer{} options := clearStateOptions(t, probe, "NO-BUNDLE-UDID", true)
d := newTestDriver(companion) options.BundleID = ""
d.output = output
resets := 0
d.resetContainer = func(context.Context) error { resets++; return nil }
for i := 0; i < 2; i++ { if _, err := New(context.Background(), options); err == nil || !strings.Contains(err.Error(), "BundleID") {
if err := d.Launch(context.Background(), "", true, nil); err != nil { t.Fatalf("New err = %v, want a refusal naming BundleID", err)
t.Fatalf("Launch %d: %v", i, err)
}
} }
if resets != 2 { if got := probe.recorded(); len(got) != 0 {
t.Fatalf("resetContainer called %d times, want 2", resets) t.Fatalf("calls = %v, want none: clearing an unnamed bundle would reinstall without resetting anything", got)
}
warnings := strings.Count(output.String(), "resetting the data container only")
if warnings != 1 {
t.Fatalf("warning emitted %d times, want once", warnings)
}
for _, call := range companion.calls {
if call == "install" || call == "uninstall" {
t.Fatalf("fallback path must not install/uninstall; got %v", companion.calls)
}
} }
} }
+15 -2
View File
@@ -84,6 +84,17 @@ var newDeviceDriver = func(ctx context.Context, options ioscompanion.DeviceOptio
return d, d.Close, nil return d, d.Close, nil
} }
// newSimulatorDriver constructs the iOS simulator driver and its cleanup. A
// seam so routing tests assert the run's options reach ioscompanion.Options
// without spawning a companion.
var newSimulatorDriver = func(ctx context.Context, options ioscompanion.Options) (driver.DeviceDriver, func(), error) {
d, err := ioscompanion.New(ctx, options)
if err != nil {
return nil, nil, err
}
return d, d.Close, nil
}
// buildDriver creates the appropriate DeviceDriver for the platform and returns // buildDriver creates the appropriate DeviceDriver for the platform and returns
// a cleanup function. For web, ChromeDriver is used directly. An iOS simulator // a cleanup function. For web, ChromeDriver is used directly. An iOS simulator
// is driven by the native simulator companion (no JVM). A physical iOS device // is driven by the native simulator companion (no JVM). A physical iOS device
@@ -99,16 +110,17 @@ func buildDriver(ctx context.Context, options Options, stdout io.Writer) (driver
} }
if options.Platform == "ios" && options.iosIsSimulator { if options.Platform == "ios" && options.iosIsSimulator {
d, err := ioscompanion.New(ctx, ioscompanion.Options{ d, cleanup, err := newSimulatorDriver(ctx, ioscompanion.Options{
UniqueDeviceIdentifier: options.iosUDID, UniqueDeviceIdentifier: options.iosUDID,
BundleID: options.BundleID, BundleID: options.BundleID,
AppPath: options.IosAppPath, AppPath: options.IosAppPath,
ClearState: options.ClearData,
Output: stdout, Output: stdout,
}) })
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("ios simulator driver: %w", err) return nil, nil, fmt.Errorf("ios simulator driver: %w", err)
} }
return d, d.Close, nil return d, cleanup, nil
} }
if options.Platform == "ios" { if options.Platform == "ios" {
@@ -117,6 +129,7 @@ func buildDriver(ctx context.Context, options Options, stdout io.Writer) (driver
CoreDeviceID: options.iosCoreDeviceID, CoreDeviceID: options.iosCoreDeviceID,
BundleID: options.BundleID, BundleID: options.BundleID,
AppPath: options.IosAppPath, AppPath: options.IosAppPath,
ClearState: options.ClearData,
Output: stdout, Output: stdout,
}) })
if err != nil { if err != nil {
+30 -1
View File
@@ -27,7 +27,7 @@ func TestBuildDriverRoutesPhysicalIOSToDeviceDriver(t *testing.T) {
return stubDeviceDriver{}, func() { closed = true }, nil return stubDeviceDriver{}, func() { closed = true }, nil
} }
options := Options{Platform: "ios", BundleID: "app.folio", IosAppPath: "/tmp/iosApp.app"} options := Options{Platform: "ios", BundleID: "app.folio", IosAppPath: "/tmp/iosApp.app", ClearData: true}
options.iosIsSimulator = false options.iosIsSimulator = false
options.iosUDID = "00008140-HW" options.iosUDID = "00008140-HW"
options.iosCoreDeviceID = "CORE-1" options.iosCoreDeviceID = "CORE-1"
@@ -45,12 +45,41 @@ func TestBuildDriverRoutesPhysicalIOSToDeviceDriver(t *testing.T) {
if got.BundleID != "app.folio" || got.AppPath != "/tmp/iosApp.app" { if got.BundleID != "app.folio" || got.AppPath != "/tmp/iosApp.app" {
t.Fatalf("DeviceOptions = %+v, want bundle and app path threaded through", got) t.Fatalf("DeviceOptions = %+v, want bundle and app path threaded through", got)
} }
if !got.ClearState {
t.Fatalf("DeviceOptions = %+v, want clear-data threaded through: the driver clears before its session, so a launch cannot", got)
}
cleanup() cleanup()
if !closed { if !closed {
t.Fatal("cleanup must close the device driver") t.Fatal("cleanup must close the device driver")
} }
} }
func TestBuildDriverThreadsClearStateToTheSimulatorDriver(t *testing.T) {
stubPreflight(t)
original := newSimulatorDriver
t.Cleanup(func() { newSimulatorDriver = original })
var got ioscompanion.Options
newSimulatorDriver = func(_ context.Context, options ioscompanion.Options) (driver.DeviceDriver, func(), error) {
got = options
return stubDeviceDriver{}, func() {}, nil
}
options := Options{Platform: "ios", BundleID: "app.folio", IosAppPath: "/tmp/iosApp.app", ClearData: true}
options.iosIsSimulator = true
options.iosUDID = "SIM-UDID"
if _, _, err := buildDriver(context.Background(), options, io.Discard); err != nil {
t.Fatalf("buildDriver: %v", err)
}
if got.UniqueDeviceIdentifier != "SIM-UDID" || got.BundleID != "app.folio" || got.AppPath != "/tmp/iosApp.app" {
t.Fatalf("Options = %+v, want the resolved target, bundle and app path", got)
}
if !got.ClearState {
t.Fatalf("Options = %+v, want clear-data threaded through: the driver clears before its session, so a launch cannot", got)
}
}
func TestBuildDriverSurfacesDeviceConstructionError(t *testing.T) { func TestBuildDriverSurfacesDeviceConstructionError(t *testing.T) {
stubPreflight(t) stubPreflight(t)
original := newDeviceDriver original := newDeviceDriver