English · 日本語
The run loop (Orchestrator) and the run pipeline¶
The Tier 2 deterministic runner. Each step is act → (wait) → verify, and pass/fail comes only from machine assertions. No AI is involved. It stops at the first failure.
Implementation:
bajutsu/orchestrator/(the loop body, package:loop/waits/substitution/evidence_rules/actions) ·bajutsu/runner/(real-device launch + report wiring, package:pipeline/pool/launch).
Related: scenarios · selectors · evidence · reporting
run_scenario (running one scenario)¶
def run_scenario(driver, scenario, clock=None, sink=None, on_blocked=None) -> RunResult
driver: abase.Driver(a real driver orFakeDriver). The loop depends only on this interface.clock: injected time / sleep (to make waits deterministic in tests). DefaultRealClock(time.monotonic/time.sleep).sink: the evidence output target (defaultNullSink= writes nothing) (evidence).on_blocked: a handler that, on step failure, "cleans up a blocker (a system alert, etc.) and returns True." If it does, the step is retried exactly once (the alert guard). For awaitstep (for/settled/screenChanged), the same handler is also armed mid-wait (BE-0269): it fires against the already-polled screen as soon as the tree looks collapsed — debounced, cooldown-limited, capped at two attempts per wait — so a blocked wait can recover before its own timeout elapses, independent of the end-of-step retry.
The flow of one step¶
For each step i (in orchestrator/loop.py):
kind = _action_of(step)— determine which action it is.step_id = step.name or f"step{i}"— the evidence output unit.- (If
capturePolicyhas ascreenChangedtrigger) record the pre-actionquery(). - Start interval captures (
video/deviceLogamong those that must begin before the action)._pre_intervalspicks only triggers determinable from the step itself (screenChanged/errorare too late). - Run the act (or wait / assert) via
_run_step_body→(ok, reason, assertion_results). - On failure, if
on_blockedcleared a blocker, retry once. - Stop interval captures (after the step has settled). Record the artifacts.
- Acquire the instant captures (
screenshot/elements) (from_collect_captures's firing result). - Push a
StepOutcome. On failure, setfailureand break.
_run_step_body (act / wait / assert dispatch)¶
wait→_wait(condition wait, below).assert_→ evaluateassertions.evaluate(driver.query(), ...)and AND.- otherwise (tap/longPress/type/swipe/relaunch) →
_do_action. - catches
SelectorError/NotImplementedErrorand converts to(False, reason, [])(does not propagate the exception).
_do_action (the action bodies)¶
| Action | Body |
|---|---|
tap |
driver.tap(sel) |
longPress |
driver.long_press(sel, duration) |
type |
if into, driver.tap(into) first → driver.type_text(text) |
swipe |
{from,to} → driver.swipe directly. {on,direction} → resolve_unique the target → from the frame center, a screen fraction in the direction (_SWIPE_FRACTION, default 0.125; amount overrides). A fraction, not a fixed count, keeps the scroll reach at parity across backends whose frames use different units (iOS points, Android pixels) |
relaunch |
terminate + relaunch the app (re-applying launch env/args + overrides) via the runner-injected relauncher, then wait until ready |
Waits (condition waits only)¶
_wait(driver, w, clock) -> (ok, reason). No fixed sleep. It polls query() at _POLL = 0.05s
intervals until the condition holds or timeout is reached.
| Form | Condition met | On timeout |
|---|---|---|
for: <sel> |
a matching element appears | fail |
until: { gone: <sel> } |
a matching element disappears | fail |
until: screenChanged |
query() changed from the initial value |
fail |
until: settled |
the screen is stable (two consecutive unchanged query()s, and there is an element with an id) |
proceed (does not fail) |
settledis a stabilization hint that "waits for a transition / animation to settle," not a correctness assertion. An empty / collapsed tree (mid-render, or covered by a system alert) is never treated as settled. On timeout it proceeds with the current screen.
Evidence rule firing¶
Decides whether each capturePolicy rule fires for this step (evidence).
_rule_fires: whether it matches one ofon.action(+ optionalidMatches) /on.event == screenChanged/on.result == error. The action name is mapped to the DSL name (long_press→longPress,assert_→assert)._collect_captures: gathers the inlinestep.capture+ the fired rules' captures and dedupes.- Instant kinds (screenshot/elements) are acquired by the sink's
capture(); interval kinds (video/deviceLog) are collected by stopping the ones started earlier viastart_intervals().
primary_id is "the id of the step's primary target selector" (tap → the tap target, type → into,
swipe → on). An idMatches trigger fnmatches against this id.
Run results (data structures)¶
@dataclass
class StepOutcome:
index: int
action: str # "tap" / "wait" / ...
ok: bool
reason: str # failure reason
duration_s: float # timing (the actionLog equivalent)
assertion_results: list[AssertionResult]
artifacts: list[Artifact] # evidence captured for this step
@dataclass
class RunResult:
scenario: str
ok: bool
steps: list[StepOutcome]
expect_results: list[AssertionResult] # evaluation of the final expect
failure: str | None # e.g. "step 3 (tap): no match: {...}"
expect is evaluated only after all steps pass. If on_blocked is present, expect is also
re-evaluated once. These become report/'s manifest.json / JUnit / HTML directly
(reporting).
runner (the run pipeline)¶
Implementation: bajutsu/runner/. Connects the orchestrator to a real device and wires through
to the report.
launch_driver (launch the app and return a ready driver)¶
Builds the environment with simctl per the preconditions:
erase (if pre.erase: shutdown → erase) → boot → terminate(bundle) (for a clean launch state)
→ launch(bundle, [launchArgs, *locale_args(locale)], {**config.launchEnv, **pre.launchEnv})
→ openurl(deeplink) (if any) → make_driver(actuator, udid)
→ _await_ready (poll until query() returns 2+ elements, up to 10s)
_await_readypolls until "the app has rendered a UI (more than the root element)."localeis applied at launch (the scenario'spreconditions.localeoverrides the config default, passed as launch args viaenv.locale_args). The simctl launch sequencing is validated on a real device (iPhone 17 Pro) viamake -C demos/showcase run-swiftui+ theios-e2e.ymlCI workflow.
device_factory / run_all / run_and_report¶
device_factory(udid, backends, ...): selects the actuator and returns a factory thatlaunch_drivers per scenario.run_all(eff, scenarios, factory, ...): runs each scenario with a freshly built driver (clean isolation).run_and_report(...): writes therun_allresults viawrite_report(runs_dir/run_id, ...)and returns(results, manifest_path).
The CLI's run calls this run_and_report (cli).