English · 日本語
Driver abstraction, backends, and environment management¶
One
Driverinterface, behind which sit the backends —idb(iOS Simulator),adb(Android emulator),playwright(web browser), plus the in-memoryfakefor tests — with capability differences absorbed on the abstraction side. A platform-aware registry picks the actuator from thebackendlist; on iOS, launching the app (boot/launch) is handled by asimctlwrapper, and on Android by the twinadbwrapper.Implementation:
bajutsu/drivers/(base.py/idb.py/adb.py/playwright.py/fake.py) ·bajutsu/backends.py·bajutsu/simctl.py·bajutsu/adb.py.
Related: selectors (resolution) · the stability ladder · run-loop
Driver Protocol¶
The common interface every backend satisfies (base.py, a runtime_checkable Protocol).
Actions (tap/type/swipe/wait/query) are performed by the actuator only.
class Driver(Protocol):
def query(self) -> list[Element]: ... # the screen's element tree
def tap(self, sel: Selector) -> None: ...
def tap_point(self, p: Point) -> None: ... # raw coordinate tap (system alerts, etc.)
def long_press(self, sel: Selector, duration: float) -> None: ...
def swipe(self, frm: Point, to: Point) -> None: ... # a raw pointer drag (coordinate form)
def scroll(self, frm: Point, to: Point) -> None: ... # a directional scroll (BE-0227)
def type_text(self, text: str) -> None: ...
def wait_for(self, sel: Selector) -> bool: ... # single-shot: matches the current screen?
def screenshot(self, path: str) -> None: ...
def capabilities(self) -> set[str]: ... # provided capabilities (for actuator / fallback resolution)
About
wait_for: it is single-shot by contract (BE-0118) — it checks the current screen once and returns, never looping. The deadline poll lives in one shared helper,base.wait_until, so a caller'stimeoutmeans the same real seconds on every backend instead of each driver reimplementing its own loop. The run loop's own condition waits are done by the orchestrator pollingquery()directly (_wait, run-loop); sowait_untilis used only by callers outside that loop (e.g.golden_assert).
Capabilities (Capability)¶
The set of tokens returned by capabilities(), used for actuator selection, evidence fallback
resolution, and the preflight capability check (below).
| Capability | Meaning | idb | adb | playwright | fake |
|---|---|---|---|---|---|
query |
element-tree query | ✅ | ✅ | ✅ | ✅ |
elements |
element-dump evidence | ✅ | ✅ | ✅ | ✅ |
screenshot |
screenshot | ✅ | ✅ | ✅ | ✅ |
semanticTap |
tap directly by id/label (no coordinates) | — | — | ✅ | ✅ |
conditionWait |
native condition waiting | — | — | ✅ | ✅ |
network |
native network monitoring | — | — | ✅ | — |
multiTouch |
two-finger gestures (pinch / rotate) | — | ✅ | ✅ | ✅ |
textSelection |
select-all + clipboard copy on the focused field | — | ✅ | ✅ | ✅ |
deviceControl.setLocation |
set the simulated GPS location | ✅ | ✅ | — | — |
deviceControl.clipboard |
read / write / clear the clipboard | ✅ | ✅ | — | — |
deviceControl.push |
deliver a push notification | ✅ | — | — | — |
deviceControl.clearKeychain |
clear the keychain | ✅ | — | — | — |
deviceControl.appLifecycle |
background / foreground the app | ✅ | — | — | — |
deviceControl.statusBar |
override / clear the status bar | ✅ | — | — | — |
The
deviceControl.*tokens are theDeviceControlfamily split per operation (BE-0212, from the coarsedeviceControlof BE-0128), so a backend can advertise exactly the operations it can honor. idb backs the whole family; the Android emulator backssetLocation+clipboardonly (itspush/ keychain / status-bar / app-lifecycle operations have no faithful equivalent), which the split makes expressible without green-lighting the rest.idb and adb sit at the lean end, both actuating by frame-center coordinates — they expose no semantic tap, so the run loop resolves a unique element via
query()and taps its center. On idb,pinch/rotateraiseUnsupportedAction(single-touch); on iOS those go through codegen → XCUITest. adb advertisesquery/elements/screenshot,multiTouch(a rooted-devicesendeventtwo-finger sweep; BE-0232), plus the emulator-backed device-control subsetdeviceControl.setLocation+deviceControl.clipboard(BE-0211); the rest of the device-control family has no faithful emulator equivalent and stays unadvertised. Thefakedriver advertises a richer capability set (semanticTap / conditionWait / multiTouch) purely to exercise those code paths in tests. Theplaywright(web) driver advertisessemanticTap/conditionWait(Playwright has both natively),network— the first backend with native network, observing and stubbing traffic in-process with no app-side cooperation — andmultiTouch, synthesizing pinch / rotate via the Chromium DevTools protocol'sInput.dispatchTouchEvent(BE-0054).
Preflight capability check (BE-0082)¶
A backend's capability set is static, so a scenario that needs a capability the chosen actuator
lacks is knowable before any device work. At run start — after the actuator is selected, before
the first device is leased — the runner checks each scenario against the actuator's capabilities
(bajutsu/capability_preflight.py) and fails an unsupported scenario immediately, with one
aggregated UnsupportedAction-style reason, instead of booting a device and failing partway
through (prime directive #2: fail fast and clearly). It is a pure function of (scenario, capability
set) — no device, no clock — and per-scenario: only the offending scenarios fail, the rest run.
The check gates only the hard requirements the capability set cleanly decides: pinch /
rotate need multiTouch, select / copy need textSelection (select-all + clipboard copy;
idb is coordinate-only and refuses both — delete / clear stay ungated, as every backend backs
delete_text), a visual assertion needs screenshot, and each device-control step
needs the token for its own operation — setLocation needs deviceControl.setLocation, the
clipboard steps need deviceControl.clipboard, push needs deviceControl.push, and so on
(BE-0212 split the coarse deviceControl family of BE-0128 into these per-operation tokens). Every
run needs query + elements. It deliberately does not gate conditionWait (the run loop
polls for every wait, so no backend needs the token) or network (idb captures traffic through the
app-side collector despite not advertising network, so request / event / requestSequence /
responseSchema assertions and until: { request } waits run on idb). gestures.py's
_require_multi_touch stays as a defense-in-depth check at gesture time, and _need_control stays
as the equivalent for device-control steps — catching the case where the specific run has no
DeviceControl wired at all, e.g. a parallel run with no pinned device. Because the tokens are
per-operation, a backend that supports only part of the family (the Android emulator: setLocation
+ clipboard) passes preflight for what it advertises and fails fast for the rest, each unsupported
step named individually — rather than the family being all-or-nothing.
idb¶
Headless, coordinate-based. For CI (continuous integration). With no semantic tap, the abstraction resolves
id → frame center → coordinate tap. Implementation: drivers/idb.py.
query(): normalizesidb ui describe-all --udid <udid> --jsonviaparse_describe_all(handles both a JSON array and newline-delimited JSON, absorbingAXLabel/AXValue/AXUniqueId, etc.).tap(sel):_resolveto confirm uniqueness (retries not-found, fails ambiguity fast: a real-device tree can be transiently empty during transitions) →idb ui tap(integer coordinates) at the frame center.screenshot: idb's own frame capture is unreliable, so it usessimctl io screenshot.swipe: adds--duration 0.2to make it a real drag (an instantaneous swipe is not recognized as a pan by SwiftUI).
The describe-all JSON key names follow fb-idb's output and are validated on-device against fb-idb (iPhone 17 Pro, recent iOS) via
make -C demos/showcase run-swiftui+ theios-e2e.ymlCI workflow; re-check them only if the installed idb version changes the schema (the note atopidb.py). The idb client isuv sync --extra idb;idb_companionisbrew install facebook/fb/idb-companion.
Tracking the idb version (BE-0005)¶
idb is the only on-device backend, so a new Simulator runtime an older idb_companion cannot
drive — or a companion upgrade that reshapes the describe-all JSON — breaks a run without any
Bajutsu change. The version idb runs against is therefore a tracked, recorded input rather than
whatever happens to be installed:
- Pin a range in config.
defaults.idbVersionholds a constraint like">=1.1.8"or">=1.1.0,<2.0.0"(environment-level — the same pin regardless of which target a scenario drives).bajutsu doctorreports the installedidb_companionagainst it, e.g.✓ idb_companion version: 1.1.8 (expected >=1.1.8), so a mismatch surfaces in the pre-flight checklist instead of as a confusing downstream failure. A malformed pin is rejected at config load. With no pin declared,doctorshows no version line. - Recorded in the manifest. Every idb-backed run writes the
idb_companionand idb client versions intomanifest.json("idb": { "companion": …, "client": … }), so any artifact set states exactly which idb produced it. The manifest record is provenance only — it never affects pass/fail, so the run/CI verdict stays deterministic. - A scheduled compatibility monitor.
idb-monitor.ymlruns the smoke scenario through idb against the latestidb_companionon a weekly cadence (separate from the per-PR gate). Because the smoke run goes throughparse_describe_all→ Element normalization, a schema or behaviour drift fails it loudly there, on a cadence we control, rather than being discovered ad hoc.
adb (Android)¶
Headless, coordinate-based — the architectural twin of idb. With no semantic tap, the
abstraction resolves id → frame center → coordinate tap, exactly as on iOS. Implementation:
drivers/adb.py + bajutsu/adb.py (roadmap
BE-0007).
query(): reads the window's UI Automator XML and maps each<node>to anElementwith a pure parser (parse_hierarchy). The read runs over the resident UI Automator server when it is built (make -C BajutsuAndroidUIAutomatorServer build) — one warmUiAutomationsession answeringGET /sourceoveradb forward, so each read costs ≈ 0.1–0.3 s instead of the ≈ 2.4 s a freshadb -s <serial> exec-out uiautomator dump /dev/ttypays per invocation (roadmap BE-0245); the resident whole-screen dump is narrowed to the active window so it yields the same Elements. Without the built server — or on any channel failure — it falls back touiautomator dump, andBAJUTSU_ADB_RESIDENT(0/1) pins either path. The selector mapping isresource-id→identifier(the<package>:id/prefix stripped to the local name, so a ComposetestTagsurfaced viatestTagsAsResourceIdreproduces verbatim while a nativeandroid:iddrops its prefix),text→label(content-descfallback),content-desc→value(the app mirrors its state value there, SPEC §2.1), and the widgetclass(plus enabled / selected / checked state) →traits. The local name is matched exactly — the driver does no.↔_rewriting, which would conflate distinct ids and erode determinism. Where a platform's native id syntax cannot reproduce the SPEC id verbatim (Android Views:android:idallows neither.nor-, sostable.refreshsurfaces asstable_refresh), the scenario carries both id forms in one selector —id: [stable.refresh, stable_refresh]— and the match is an OR over the candidates (BE-0221); see scenarios.tap(sel):_resolveconfirms uniqueness (retries not-found, fails ambiguity fast — like idb, a mid-transition dump is a transient null-root that is retried, and a 2+ match fails immediately) →adb shell input tapat the frame center.swipeadds a finite duration so it is a real drag;long_pressis a same-point swipe held for the duration;type_textisinput text(spaces sent as its%sescape).- On-device actuation fidelity (roadmap
BE-0210):
the
backstep is the true system back (input keyevent 4/KEYCODE_BACK) — Android has no on-screen back element to tap, unlike iOS's OS back button;double_tapissues both taps in oneadb shellround-trip (input tap … ; input tap …) so the adb transport round-trip does not widen the gap past the platform's double-tap window; and a tap whose target is not in the current viewport scrolls toward it (a default up-swipe) and re-queries, bounded by a retry count — a condition wait, so a selector that never appears still fails deterministically.
[!NOTE] Scroll-into-view is an adb-only recovery today:
idb/ XCUITest / Playwright still fail atapfast when the target is not in the initial viewport. So atapon a below-the-fold element can pass on Android (after up to a few swipes) yet fail on iOS/web for the same scenario. The portable idiom stays an explicitswipestep (seedemos/showcase/scenarios/notices.yaml); the adb auto-scroll is a robustness net, not a substitute for it. Widening it to the other backends is a follow-up (BE-0210 scoped it to adb). - Multi-touch (BE-0232):pinch/rotatedrive a two-slot protocol-Bsendeventsweep (pinch_contacts/rotate_contactscompute the two contacts' geometry;rotatesweeps the straight chord between the endpoints, a linear approximation of the arc, like the web backend's rotate). This needs a rooted device with a discoverable touchscreen;_two_finger_gesturefails loudly withUnsupportedActionotherwise — there is no single-touch fallback, unlike the double-tap path below.MULTI_TOUCHis declared statically in the capability set regardless of root, so preflight admitsgestures_multitouchon adb; the root check is enforced at actuation time, not in the capability set. -screenshotwrites the PNG bytes fromadb exec-out screencap -p(binary-clean stdout). - Lifecycle (AndroidEnvironment, the twin of the iOSsimctlsequence): boot-readiness wait (pollinggetprop sys.boot_completedto a bounded deadline — a condition wait, no fixed sleep, and no unboundedadb wait-for-deviceblock) → optional APK install →pm clearfor a clean state (theeraseequivalent) →am force-stop→ runtime-permission pre-grant (pm grant, see below) →am start(the launcher activity resolved via the package manager; launch env forwarded as intent extras) → deeplink (am start -a android.intent.action.VIEW). The run manifest recordsbackend: "adb"so the selected actuator is disclosed. - Runtime permissions (BE-0210): the permissions listed in the target's configgrantPermissionsare granted up front withadb shell pm grant <package> <permission>at lease time — afterpm clear(which resets grants) and before launch — so a runtime permission prompt never blocks a scenario. Granting deterministically up front, rather than tapping the dialog when it appears, keeps timing off the run path; the list is app-specific, so it lives in config, not the driver. - Interval evidence (BE-0007 Unit 4):videorecords viaadb shell screenrecordanddeviceLogstreamsadb logcat, the twins of the simctl providers.screenrecordwrites device-side (it cannot stream to a host file), so the recording is finalized on SIGINT and pulled off withadb pullon stop;logcatstreams to the file and stops on SIGTERM. Both are supplied through the same driverdriver_intervalseam the web backend uses, so the backend-independentcapturepolicy drives them unchanged (see evidence). - Network is not observed natively (noNETWORKcapability) — the same mocked story as iOS: the app-side collector URL is forwarded through the launch env as an intent extra, somockswork with no new code path. Device control backs the emulator subsetsetLocation(emu geo fix, BE-0211) and the clipboard operations; the rest of the family stays unsupported. The clipboard runs through an in-app receiver (BajutsuAndroid, BE-0233), notcmd clipboard: on-device that command is a silent no-op, and since Android 10 only the foreground app / default IME may touch the clipboard — so bajutsu sends an orderedam broadcastthat a receiver inside the app handles from the app process (base64 both ways, so the argv needs no quoting; a missing receiver fails loudly rather than reading an empty clip). adb still advertisesclipboardbecause, like idb's over simctl, the backend can drive it given a cooperating app. SeeBajutsuAndroid.The XML attribute names follow UI Automator's
uiautomator dumpschema. The Viewsandroid:id.↔_case is resolved scenario-side: a selector lists both id forms and matches either (BE-0221), so the shared showcase scenarios run unchanged on both Android toolkits — checked on every push/PR byandroid-e2e.yml, which drivesshowcase-composeandshowcase-viewsover the same set. The fast gate exercises the parser, the frame-center taps, the transient-empty retry, and ambiguous-fails-fast over captured XML fixtures. adb isbrew install android-platform-tools.
Playwright (web)¶
Headless Chromium via Playwright (Python). Runs on Linux with no Mac and no Simulator, so it
fits the same toolchain as make check. Implementation: drivers/playwright.py (roadmap
BE-0041).
query(): onepage.evaluate()walks the visible / interactive / a11y-relevant DOM nodes and a pure parser (parse_dom) maps each to anElement. The id convention is the web equivalent of iOS accessibilityIdentifier:data-testid→Selector.id, ARIArole(or tag) →traits, accessible name /aria-label/ text →label, inputvalue→value.tap(sel): like idb, it resolves a unique element through the sharedresolve_unique/find_allagainst aquery()snapshot and clicks the frame center by coordinate (page.mouse.click). It deliberately does not use Playwright's ownget_by_test_id().click(), so selector semantics stay byte-identical to every other backend.type_texttypes viapage.keyboard(the orchestrator tapsintofirst, focusing the field);screenshotispage.screenshot;wait_foris single-shot viafind_all(like every backend — the sharedbase.wait_untilsupplies the deadline poll).- Lifecycle is owned by the driver: a fresh
BrowserContextis theeraseequivalent,navigate()(page.goto(baseUrl)) is thelaunch, andclose()tears the browser down. There is no simctl device, so the run uses a dummy lease and no device control. - Device mode (BE-0228): a web target's
deviceModeconfig selects how eachBrowserContextis created —desktop(the default, a plain desktop context, unchanged from before) or a Playwright device preset name (e.g.iPhone 13). A preset is resolved againstplaywright.devicesand its descriptor (viewport /device_scale_factor/is_mobile/has_touch/user_agent) is merged intonew_context(**kwargs)alongsidereduced_motion="reduce", so the target is driven as that mobile device. The descriptor is resolved lazily (config load never imports Playwright) and memoized, so areset_context(crawl clean start) and arelaunch(BE-0077) rebuild the identical context — the mode is stable across the browser's whole lifecycle, the same invariant the engine andreduced_motionalready hold. An unknown preset fails loudly with aValueErrorat driver start. Device mode is desktop-browser emulation — a mobile viewport and touch input in a desktop-class browser, exactly what Chrome DevTools' device toolbar does — not a real mobile browser on a real device or a device cloud; for a real mobile OS the Android backend is the path. - Directional
swipescrolls (BE-0227): the directional formswipe: { on, direction }means "scroll", and a mouse drag does not scroll a web page, so the web backend dispatches the input primitive that actually scrolls, keyed on the context's input mode (thedeviceModeabove). On a desktop (pointer) context it emits apage.mouse.wheel(...)over the gesture's start — the wheel is the reverse of the travel, so anupswipe scrolls the page down, exactly as a trackpad or wheel would. On a touch context (a mobiledeviceMode) it uses a real single-finger touch drag over CDP (the same pathpinch/rotatetake), so the page's touch and scroll listeners fire. The coordinate formswipe: { from, to }is unchanged — it stays a literalpage.mousedrag, the raw-drag last resort for a canvas / map pan / drag handle.codegenemits the desktop wheel scroll for the directional form, so a generated Playwright test scrolls in the physically correct direction instead of the old inert drag (a fixed default distance, as codegen has no viewport to scaleamountagainst). The separatedragaction (element-anchored pointer drag — a resize divider, a slider) routes to the driver'sswipe, so on web it is a realpage.mousedrag that moves the grabbed element, where a directionalswipewould only scroll. - Multi-touch (BE-0054):
pinch/rotateare synthesized as two-finger drags via the Chromium DevTools protocol (Input.dispatchTouchEvent) —mouseis single-pointer, so gestures go through CDP, the same path a real touch takes (so the page's touch listeners fire). The element center anchors the two fingers;scalespreads/closes their gap andradiansrotates them about it. - Native network (BE-0054): Playwright sees every request the page makes, so
--networkworks on web with no app-side cooperation.network_collector()hooks the page'srequestfinishedevent into the sameNetworkExchangethe iOS collector produces (sorequestassertions andnetwork.jsonevidence are unchanged), and a scenario'smocksare fulfilled in-process viapage.route— a matching request gets the canned response and is recorded withmocked: true. Mock matching reuses the deterministicrequestmatcher, and no model is consulted. - Console / page-error & video evidence (BE-0054): the
deviceLogcapture kind streams the browser console and uncaught page errors to<scenario>/device.log, andvideorecords the whole scenario — both Playwright-native (no simctl), the web analogues of the iOS os_log / simctl video. The pool enables recording only whenvideois in the scenario'scapture(theBrowserContextis created withrecord_video_dir), and thevideointerval finalizes it into<scenario>/scenario.mp4(webm content) on close. The pool injects the driver'sdriver_interval(the driver-supplied interval seam, shared with the adb backend) into theFileSink, so the same backend-agnosticcapturepolicy carries both.
playwrightis imported lazily (only when a browser is actually started), so it never loads on the default CLI path (locked bytests/serve/test_import_guard.py). Install withuv sync --extra web+uv run playwright install chromium; the demo atdemos/web(make -C demos/web e2e) drives a tiny static web app end to end.
FakeDriver¶
An in-memory implementation for testing the orchestrator / runner / record without a device.
Implementation: drivers/fake.py.
- Holds a
screen(a list ofElement) and returns it fromquery(). tap/long_pressgo throughresolve_uniquelike the real thing (ambiguous / not-found =SelectorError).- A
reactcallback lets you script "the screen changes in response to an action." actionsrecords the performed actions (for assertions).
def react(driver, kind, arg):
if kind == "tap":
driver.screen = [...] # swap in the post-tap screen
FakeDriver(screen=[...], react=react)
Backend selection and the actuator¶
Implementation: bajutsu/backends.py.
PLATFORMS = { # a platform token expands to its actuators (stability order)
"ios": ("xcuitest", "idb"), # most capable first (BE-0019)
"android": ("adb",), # planned
"web": ("playwright",), # implemented (BE-0041)
"fake": ("fake",), # the in-memory test/demo driver
}
COST_ORDER = {"ios": ("idb", "xcuitest")} # cheapest first (BE-0240); idb has no toolchain/runner cost
IMPLEMENTED = {"idb", "fake", "playwright", "xcuitest"} # actuators with a driver today
def default_available(actuator) -> bool: # implemented + backing tool present (playwright: package import; fake: always)
def resolve_actuators(backends) -> list: # expand each token (platform or actuator) to actuators
def select_actuator(backends, available) -> str: # first implemented + available, in stability order
def select_actuator_cost_first(backends, available) -> str: # cheapest available, no scenario in hand (BE-0267)
def select_actuator_for_scenario(backends, scenario, available, caps) -> str: # cheapest available + sufficient (BE-0240)
def make_driver(actuator, udid, *, base_url=None, runner_port=None) -> Driver: # "xcuitest"→XcuitestDriver, "idb"→IdbDriver, "playwright"→PlaywrightDriver, "fake"→FakeDriver
- A backend token is either a platform (
ios/android/web/fake) or a concrete actuator (e.g.idb). A platform with more than one actuator is resolved per scenario (BE-0240):--backend ios(orbackend: [ios]) runs each scenario on the cheapest actuator its own steps can use —idbby default, escalating to XCUITest only for a scenario whose constructs need a capability idb lacks (e.g.pinch/rotate→multiTouch). idb's capability set is a strict subset of XCUITest's, so no scenario needs idb specifically — idb is preferred only for cost. - Two orderings answer two questions. Stability order (
PLATFORMS, most-capable-first; concepts) drivesselect_actuator— the availability-only pick used where no scenario is in hand yet and cost doesn't matter (doctor, the pool's up-front setup, an explicit single-actuator pin). Cost order (COST_ORDER, cheapest-first) drives bothselect_actuator_for_scenarioandselect_actuator_cost_first, which share a candidate-resolution prefix (_cost_ordered_available).select_actuator_for_scenarioadditionally reusescapability_preflight.unsupported(BE-0082) against each candidate's capability set and returns the first that is both available and sufficient for that scenario's steps.select_actuator_cost_firstis the same cost-first pick with no scenario to check against — used where a live session needs the cheapest actuator it can bring up without capability escalation (serve's Author-tab Capture and Enrich; BE-0267 — the earlier stability-order pick there raised on an[ios]-config target since serve never starts an XCUITest runner). Both delegate toselect_actuator(keeping its diagnostics) whenever the resolved candidates collapse to one — an explicit single-actuator request never escalates (a hard pin, like--udid). If none is available,RuntimeError(the CLI exits with code 2). webresolves toplaywrightandandroidresolves toadb, both implemented (vision → reach). Truly unknown tokens are skipped (forward-compat: an older build can run a config that lists a future backend).- The availability check
availableis injectable (swappable in tests). The default isshutil.whichfor PATH-backed actuators;playwrightis gated on whether its Python package is importable, andfakeis always available. - The actuator is fixed per scenario and held for that scenario's whole execution (BE-0240), so two drivers never operate one device at once. Fixing the actuator per scenario narrows the earlier "fixed per invocation" unit without relaxing the single-actuator rule: at every instant exactly one actuator acts on the leased device, and there is never a mid-scenario driver swap.
Actuation stays with the single actuator. Non-actuator backends in the list can serve as read-only
evidence fallbacks (DESIGN §9, BE-0020):
a same-platform backend whose capabilities() advertises a kind the actuator lacks (e.g.
Capability.NETWORK) is resolved as the provider for that kind, accessed only through the narrow
EvidenceProvider Protocol (no tap/type/swipe — a type-level guarantee). When no backend can fill a
gap, the kind is skipped with a recorded reason (SkippedCapture) — graceful degradation, never a
run failure. See evidence — provider for provenance
details.
Environment management (simctl)¶
Implementation: bajutsu/simctl.py. Command builders are pure functions (unit-tested); execution goes
through an injectable RunFn.
| Method | Command | Notes |
|---|---|---|
erase() |
simctl erase <udid> |
clean environment |
boot() |
simctl boot <udid> |
idempotent if already booted (swallows the error) |
launch(bundle, args, env) |
simctl launch --terminate-running-process <udid> <bundle> <args> |
env injected via SIMCTL_CHILD_* |
terminate(bundle) |
simctl terminate <udid> <bundle> |
ignored if not running |
openurl(url) |
simctl openurl <udid> <url> |
deeplink |
screenshot(path) |
simctl io <udid> screenshot <path> |
— |
Injecting launch env: an env var to pass to the app is set on the parent process as
SIMCTL_CHILD_<NAME>, which reaches the child (the app) as<NAME>.child_env()does this conversion. The showcase's launch hooks likeSHOWCASE_UITESTuse this mechanism (showcase).
The video / deviceLog interval captures also use simctl io recordVideo / simctl spawn log
stream, but those live in the evidence subsystem (evidence/intervals.py)
(evidence).