mobster-cli 0.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mobile_agent/__init__.py +4 -0
- mobile_agent/__main__.py +598 -0
- mobile_agent/accounts/__init__.py +1 -0
- mobile_agent/accounts/context.py +80 -0
- mobile_agent/accounts/mcp.py +55 -0
- mobile_agent/accounts/pacing.py +318 -0
- mobile_agent/accounts/register.py +78 -0
- mobile_agent/accounts/routes.py +212 -0
- mobile_agent/accounts/routing.py +69 -0
- mobile_agent/accounts/screen.py +273 -0
- mobile_agent/accounts/stats.py +54 -0
- mobile_agent/accounts/store.py +394 -0
- mobile_agent/agent_hooks.py +123 -0
- mobile_agent/ai_plans/__init__.py +7 -0
- mobile_agent/ai_plans/agents.py +96 -0
- mobile_agent/ai_plans/claude_code.py +208 -0
- mobile_agent/ai_plans/cli.py +400 -0
- mobile_agent/ai_plans/codex.py +258 -0
- mobile_agent/ai_plans/compat_chat.py +307 -0
- mobile_agent/ai_plans/device_login.py +164 -0
- mobile_agent/ai_plans/draft_client.py +68 -0
- mobile_agent/ai_plans/gemini.py +137 -0
- mobile_agent/ai_plans/instructions.py +65 -0
- mobile_agent/ai_plans/mcp_http.py +160 -0
- mobile_agent/ai_plans/phone_tools.py +892 -0
- mobile_agent/ai_plans/providers.py +235 -0
- mobile_agent/ai_plans/routes.py +160 -0
- mobile_agent/ai_plans/runner.py +400 -0
- mobile_agent/ai_plans/vault.py +114 -0
- mobile_agent/alerts.py +337 -0
- mobile_agent/android/__init__.py +18 -0
- mobile_agent/android/adb.py +268 -0
- mobile_agent/android/conditions.py +128 -0
- mobile_agent/android/doctor.py +54 -0
- mobile_agent/android/driver.py +137 -0
- mobile_agent/android/emulator.py +216 -0
- mobile_agent/android/logs.py +85 -0
- mobile_agent/android/manager.py +225 -0
- mobile_agent/android/records.py +31 -0
- mobile_agent/android/tree.py +96 -0
- mobile_agent/answer_shapes.py +230 -0
- mobile_agent/api_devices.py +333 -0
- mobile_agent/api_errors.py +17 -0
- mobile_agent/api_get.py +249 -0
- mobile_agent/api_http.py +467 -0
- mobile_agent/api_post.py +453 -0
- mobile_agent/api_routes.py +173 -0
- mobile_agent/app_api/__init__.py +4 -0
- mobile_agent/app_api/cli.py +151 -0
- mobile_agent/app_api/endpoints.py +193 -0
- mobile_agent/app_api/mcp.py +54 -0
- mobile_agent/app_api/openapi.py +146 -0
- mobile_agent/app_api/reading.py +172 -0
- mobile_agent/app_api/register.py +10 -0
- mobile_agent/app_api/screen.py +216 -0
- mobile_agent/app_api/service.py +382 -0
- mobile_agent/app_api/sessions.py +404 -0
- mobile_agent/app_api/tokens.py +167 -0
- mobile_agent/app_choice.py +131 -0
- mobile_agent/app_icons.py +183 -0
- mobile_agent/app_killer.py +69 -0
- mobile_agent/artifacts.py +606 -0
- mobile_agent/attach/__init__.py +13 -0
- mobile_agent/attach/cli.py +46 -0
- mobile_agent/attach/flutter.py +28 -0
- mobile_agent/attach/score.py +119 -0
- mobile_agent/attach/web.py +13 -0
- mobile_agent/attachments/__init__.py +1 -0
- mobile_agent/attachments/agent.py +418 -0
- mobile_agent/attachments/extract.py +217 -0
- mobile_agent/attachments/mcp.py +104 -0
- mobile_agent/attachments/register.py +73 -0
- mobile_agent/attachments/routes.py +326 -0
- mobile_agent/attachments/service.py +170 -0
- mobile_agent/attachments/sniff.py +152 -0
- mobile_agent/attachments/store.py +441 -0
- mobile_agent/audit_verify.py +436 -0
- mobile_agent/bench/README.md +410 -0
- mobile_agent/bench/__init__.py +0 -0
- mobile_agent/bench/__main__.py +394 -0
- mobile_agent/bench/ab.py +168 -0
- mobile_agent/bench/actuation.py +368 -0
- mobile_agent/bench/agents/__init__.py +9 -0
- mobile_agent/bench/agents/base.py +105 -0
- mobile_agent/bench/agents/plan_clients.py +245 -0
- mobile_agent/bench/agents/smart.py +533 -0
- mobile_agent/bench/checks.py +358 -0
- mobile_agent/bench/decide_gate.py +214 -0
- mobile_agent/bench/diagnose.py +181 -0
- mobile_agent/bench/evals/__init__.py +14 -0
- mobile_agent/bench/evals/build.py +565 -0
- mobile_agent/bench/evals/checkpoint.py +214 -0
- mobile_agent/bench/evals/irt.py +219 -0
- mobile_agent/bench/evals/l0.py +647 -0
- mobile_agent/bench/evals/l1.py +866 -0
- mobile_agent/bench/evals/models.py +124 -0
- mobile_agent/bench/evals/pairwise.py +189 -0
- mobile_agent/bench/evals/proxy.py +90 -0
- mobile_agent/bench/evals/sets.py +149 -0
- mobile_agent/bench/evals/traces.py +446 -0
- mobile_agent/bench/fixtures.py +176 -0
- mobile_agent/bench/guard_replay.py +129 -0
- mobile_agent/bench/h2-manifest.json +20 -0
- mobile_agent/bench/h3-manifest.json +20 -0
- mobile_agent/bench/heldout-manifest.json +32 -0
- mobile_agent/bench/heldout.py +375 -0
- mobile_agent/bench/iosworld.py +901 -0
- mobile_agent/bench/iosworld_stats.py +510 -0
- mobile_agent/bench/llm_units.py +116 -0
- mobile_agent/bench/manifest.json +93 -0
- mobile_agent/bench/metrics.py +66 -0
- mobile_agent/bench/phone.py +44 -0
- mobile_agent/bench/replay.py +172 -0
- mobile_agent/bench/report.py +207 -0
- mobile_agent/bench/runner.py +456 -0
- mobile_agent/bench/safety.py +310 -0
- mobile_agent/bench/stream_timing.py +213 -0
- mobile_agent/bench/suite.py +444 -0
- mobile_agent/bench/sweep.py +176 -0
- mobile_agent/bench/tape.py +798 -0
- mobile_agent/bench/tape_replay.py +88 -0
- mobile_agent/bench/trace_evals.py +128 -0
- mobile_agent/bench/trace_view.py +81 -0
- mobile_agent/bench/truth/iphone15pro.json +138 -0
- mobile_agent/bench/truth.py +340 -0
- mobile_agent/bench/tuning.py +543 -0
- mobile_agent/bench/tuning_analysis.py +656 -0
- mobile_agent/bench/vague-heldout-manifest.json +12 -0
- mobile_agent/bench/vague_rubric.py +135 -0
- mobile_agent/bench/vague_run.py +243 -0
- mobile_agent/brand.py +454 -0
- mobile_agent/brand_fonts.py +756 -0
- mobile_agent/build_identity.py +21 -0
- mobile_agent/cable_bt.py +623 -0
- mobile_agent/cable_bt_runtime.py +128 -0
- mobile_agent/catalog.py +69 -0
- mobile_agent/chart_svg.py +415 -0
- mobile_agent/chat_here.py +189 -0
- mobile_agent/cli_constants.py +5 -0
- mobile_agent/cli_parser.py +611 -0
- mobile_agent/cli_run.py +115 -0
- mobile_agent/compiled_read/__init__.py +21 -0
- mobile_agent/compiled_read/delta.py +89 -0
- mobile_agent/compiled_read/extractors.py +202 -0
- mobile_agent/compiled_read/routes.py +56 -0
- mobile_agent/completion.py +267 -0
- mobile_agent/compose.py +48 -0
- mobile_agent/config.py +114 -0
- mobile_agent/connectors/__init__.py +1 -0
- mobile_agent/connectors/api_session.py +76 -0
- mobile_agent/connectors/callback_page.py +146 -0
- mobile_agent/connectors/client.py +582 -0
- mobile_agent/connectors/github_app.py +55 -0
- mobile_agent/connectors/graph_api.py +396 -0
- mobile_agent/connectors/imports.py +170 -0
- mobile_agent/connectors/issue.py +66 -0
- mobile_agent/connectors/keystore.py +85 -0
- mobile_agent/connectors/marks.py +222 -0
- mobile_agent/connectors/oauth.py +476 -0
- mobile_agent/connectors/picker.py +228 -0
- mobile_agent/connectors/presets.py +591 -0
- mobile_agent/connectors/register.py +48 -0
- mobile_agent/connectors/routes.py +153 -0
- mobile_agent/connectors/service.py +966 -0
- mobile_agent/connectors/shared.py +136 -0
- mobile_agent/connectors/skill.py +446 -0
- mobile_agent/connectors/slack_api.py +269 -0
- mobile_agent/connectors/steps.py +92 -0
- mobile_agent/connectors/store.py +144 -0
- mobile_agent/connectors/vault.py +72 -0
- mobile_agent/console.py +178 -0
- mobile_agent/content_queue/__init__.py +1 -0
- mobile_agent/content_queue/client.py +263 -0
- mobile_agent/content_queue/importing.py +153 -0
- mobile_agent/content_queue/media_check.py +224 -0
- mobile_agent/content_queue/prompt.py +64 -0
- mobile_agent/content_queue/register.py +91 -0
- mobile_agent/content_queue/routes.py +370 -0
- mobile_agent/content_queue/runner.py +475 -0
- mobile_agent/content_queue/store.py +521 -0
- mobile_agent/contract.py +996 -0
- mobile_agent/contract_asks.py +248 -0
- mobile_agent/contract_prompt.py +45 -0
- mobile_agent/contract_screen.py +981 -0
- mobile_agent/contract_view.py +84 -0
- mobile_agent/corpus/__init__.py +26 -0
- mobile_agent/corpus/cli.py +63 -0
- mobile_agent/corpus/consent.py +55 -0
- mobile_agent/corpus/listener.py +76 -0
- mobile_agent/corpus/register.py +9 -0
- mobile_agent/corpus/skeleton.py +72 -0
- mobile_agent/corpus/store.py +143 -0
- mobile_agent/costs.py +161 -0
- mobile_agent/decider_scope.py +40 -0
- mobile_agent/decision_provider.py +189 -0
- mobile_agent/deeplinks.py +352 -0
- mobile_agent/device_info.py +116 -0
- mobile_agent/device_manager.py +978 -0
- mobile_agent/device_status.py +195 -0
- mobile_agent/device_targets.py +217 -0
- mobile_agent/devices.py +676 -0
- mobile_agent/devkit/__init__.py +64 -0
- mobile_agent/devkit/account_cli.py +90 -0
- mobile_agent/devkit/asc.py +562 -0
- mobile_agent/devkit/asc_cli.py +214 -0
- mobile_agent/devkit/ci.py +737 -0
- mobile_agent/devkit/ci_cli.py +196 -0
- mobile_agent/devkit/ci_hosts.py +194 -0
- mobile_agent/devkit/es256.py +197 -0
- mobile_agent/devkit/expo.py +440 -0
- mobile_agent/devkit/expo_cli.py +405 -0
- mobile_agent/devkit/expo_dev.py +203 -0
- mobile_agent/devkit/expo_routes.py +90 -0
- mobile_agent/devkit/fixtures_cli.py +133 -0
- mobile_agent/devkit/fixtures_routes.py +82 -0
- mobile_agent/devkit/links.py +375 -0
- mobile_agent/devkit/links_cli.py +123 -0
- mobile_agent/devkit/links_routes.py +83 -0
- mobile_agent/devkit/nightly.py +152 -0
- mobile_agent/devkit/shots.py +203 -0
- mobile_agent/devkit/testflight.py +155 -0
- mobile_agent/devkit/testflight_cli.py +127 -0
- mobile_agent/devkit/testflight_routes.py +111 -0
- mobile_agent/devkit/xcode.py +236 -0
- mobile_agent/devkit/xcode_cli.py +101 -0
- mobile_agent/devtools.py +419 -0
- mobile_agent/doctor.py +509 -0
- mobile_agent/doctor_fix.py +467 -0
- mobile_agent/downloads.py +55 -0
- mobile_agent/drivers.py +793 -0
- mobile_agent/dry_run.py +102 -0
- mobile_agent/effect_ledger.py +83 -0
- mobile_agent/engine_health.py +378 -0
- mobile_agent/engine_hooks.py +96 -0
- mobile_agent/engines.py +408 -0
- mobile_agent/env_file.py +63 -0
- mobile_agent/env_secrets.py +149 -0
- mobile_agent/errors.py +15 -0
- mobile_agent/evals/__init__.py +3 -0
- mobile_agent/evals/app_survey.py +149 -0
- mobile_agent/evals/development/__init__.py +1 -0
- mobile_agent/evals/development/score.py +276 -0
- mobile_agent/evals/latency_report.py +312 -0
- mobile_agent/evals/offline_report.py +374 -0
- mobile_agent/evals/oracles.py +298 -0
- mobile_agent/evals/redteam/__init__.py +15 -0
- mobile_agent/evals/redteam/__main__.py +72 -0
- mobile_agent/evals/redteam/cases.py +730 -0
- mobile_agent/evals/redteam/harness.py +604 -0
- mobile_agent/evals/redteam/live.py +139 -0
- mobile_agent/evals/redteam/plain.py +85 -0
- mobile_agent/evals/vision/__init__.py +0 -0
- mobile_agent/evals/vision/images/dog_001.jpg +0 -0
- mobile_agent/evals/vision/images/dog_002.jpg +0 -0
- mobile_agent/evals/vision/images/dog_003.jpg +0 -0
- mobile_agent/evals/vision/images/dog_004.jpg +0 -0
- mobile_agent/evals/vision/images/dog_005.jpg +0 -0
- mobile_agent/evals/vision/images/dog_006.jpg +0 -0
- mobile_agent/evals/vision/images/dog_007.jpg +0 -0
- mobile_agent/evals/vision/images/dog_010.jpg +0 -0
- mobile_agent/evals/vision/images/dog_011.jpg +0 -0
- mobile_agent/evals/vision/images/dog_013.jpg +0 -0
- mobile_agent/evals/vision/images/dog_014.jpg +0 -0
- mobile_agent/evals/vision/images/dog_015.jpg +0 -0
- mobile_agent/evals/vision/images/dog_016.jpg +0 -0
- mobile_agent/evals/vision/images/dog_017.jpg +0 -0
- mobile_agent/evals/vision/images/dog_019.jpg +0 -0
- mobile_agent/evals/vision/images/dog_020.jpg +0 -0
- mobile_agent/evals/vision/images/dog_021.jpg +0 -0
- mobile_agent/evals/vision/images/dog_022.jpg +0 -0
- mobile_agent/evals/vision/images/dog_023.jpg +0 -0
- mobile_agent/evals/vision/images/dog_024.jpg +0 -0
- mobile_agent/evals/vision/images/dog_026.jpg +0 -0
- mobile_agent/evals/vision/images/dog_027.jpg +0 -0
- mobile_agent/evals/vision/images/dog_034.jpg +0 -0
- mobile_agent/evals/vision/images/dog_035.jpg +0 -0
- mobile_agent/evals/vision/images/dog_038.jpg +0 -0
- mobile_agent/evals/vision/images/dog_042.jpg +0 -0
- mobile_agent/evals/vision/images/dog_047.jpg +0 -0
- mobile_agent/evals/vision/images/dog_052.jpg +0 -0
- mobile_agent/evals/vision/images/dog_058.jpg +0 -0
- mobile_agent/evals/vision/images/dog_062.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
- mobile_agent/evals/vision/manifest.json +2355 -0
- mobile_agent/eventbus.py +110 -0
- mobile_agent/explain.py +34 -0
- mobile_agent/failure_line.py +197 -0
- mobile_agent/fanout.py +663 -0
- mobile_agent/feedback/__init__.py +2 -0
- mobile_agent/feedback/bundle.py +204 -0
- mobile_agent/feedback/outbox.py +196 -0
- mobile_agent/feedback/register.py +9 -0
- mobile_agent/feedback/routes.py +177 -0
- mobile_agent/fleet.py +841 -0
- mobile_agent/fleet_health.py +729 -0
- mobile_agent/fleet_phone.py +186 -0
- mobile_agent/frontier/__init__.py +79 -0
- mobile_agent/frontier/act.py +380 -0
- mobile_agent/frontier/actions.py +70 -0
- mobile_agent/frontier/agent.py +256 -0
- mobile_agent/frontier/approvals.py +366 -0
- mobile_agent/frontier/hooks.py +199 -0
- mobile_agent/frontier/interrupts.py +125 -0
- mobile_agent/frontier/observe.py +331 -0
- mobile_agent/frontier/phases.py +381 -0
- mobile_agent/frontier/pipeline.py +61 -0
- mobile_agent/frontier/prompt.py +384 -0
- mobile_agent/frontier/protocol.py +91 -0
- mobile_agent/frontier/safety.py +467 -0
- mobile_agent/frontier/saved.py +375 -0
- mobile_agent/frontier/screen.py +419 -0
- mobile_agent/frontier/skills.py +147 -0
- mobile_agent/frontier/stages/__init__.py +1 -0
- mobile_agent/frontier/stages/after.py +192 -0
- mobile_agent/frontier/stages/approve.py +138 -0
- mobile_agent/frontier/stages/controls.py +115 -0
- mobile_agent/frontier/stages/ends.py +102 -0
- mobile_agent/frontier/stages/gate.py +92 -0
- mobile_agent/frontier/stages/guards.py +132 -0
- mobile_agent/frontier/stages/points.py +155 -0
- mobile_agent/frontier/stages/send.py +177 -0
- mobile_agent/frontier/turn.py +151 -0
- mobile_agent/full_disk_access.py +83 -0
- mobile_agent/gemini.py +69 -0
- mobile_agent/genuine.py +120 -0
- mobile_agent/graph/__init__.py +4 -0
- mobile_agent/graph/catalog.py +789 -0
- mobile_agent/graph/chain.py +306 -0
- mobile_agent/graph/dispatch.py +98 -0
- mobile_agent/graph/draft.py +373 -0
- mobile_agent/graph/edit.py +76 -0
- mobile_agent/graph/estimate.py +189 -0
- mobile_agent/graph/expr.py +562 -0
- mobile_agent/graph/functions.py +271 -0
- mobile_agent/graph/infer.py +144 -0
- mobile_agent/graph/nodes.py +936 -0
- mobile_agent/graph/phone_steps.py +178 -0
- mobile_agent/graph/rerun.py +327 -0
- mobile_agent/graph/run.py +899 -0
- mobile_agent/graph/schema.py +842 -0
- mobile_agent/graph/state.py +161 -0
- mobile_agent/graph/values.py +198 -0
- mobile_agent/harness/__init__.py +2 -0
- mobile_agent/harness/checkpoints.py +468 -0
- mobile_agent/harness/control.py +101 -0
- mobile_agent/harness/prewarm.py +213 -0
- mobile_agent/harness/register.py +43 -0
- mobile_agent/harness/routes.py +114 -0
- mobile_agent/harness/settings.py +66 -0
- mobile_agent/harness/tools/__init__.py +67 -0
- mobile_agent/harness/tools/ask_user.py +102 -0
- mobile_agent/harness/tools/watch.py +120 -0
- mobile_agent/harness_api.py +815 -0
- mobile_agent/hedge.py +203 -0
- mobile_agent/help_topics.py +305 -0
- mobile_agent/hints.py +248 -0
- mobile_agent/history.py +69 -0
- mobile_agent/home.py +214 -0
- mobile_agent/http_pool.py +175 -0
- mobile_agent/image_policy.py +139 -0
- mobile_agent/inbox.py +148 -0
- mobile_agent/installed_apps.py +344 -0
- mobile_agent/integrations/__init__.py +1 -0
- mobile_agent/integrations/cli.py +472 -0
- mobile_agent/integrations/client_files.py +65 -0
- mobile_agent/integrations/clients.py +998 -0
- mobile_agent/integrations/doctor.py +185 -0
- mobile_agent/integrations/jsonc.py +383 -0
- mobile_agent/integrations/routes.py +135 -0
- mobile_agent/integrations/rules.py +112 -0
- mobile_agent/integrations/server_options.py +46 -0
- mobile_agent/integrations/skill.py +152 -0
- mobile_agent/integrations/skill_files.py +10 -0
- mobile_agent/integrations/textedit.py +309 -0
- mobile_agent/intents/__init__.py +17 -0
- mobile_agent/intents/atlas.py +216 -0
- mobile_agent/intents/callback.py +44 -0
- mobile_agent/intents/cli.py +55 -0
- mobile_agent/intents/harvest.py +91 -0
- mobile_agent/intents/plan.py +93 -0
- mobile_agent/ios/__init__.py +1 -0
- mobile_agent/ios/guide.py +16 -0
- mobile_agent/ios/kb.py +212 -0
- mobile_agent/ios/kb_data.py +1588 -0
- mobile_agent/ios/mcp.py +44 -0
- mobile_agent/ios/register.py +8 -0
- mobile_agent/ios/system.py +369 -0
- mobile_agent/journal.py +531 -0
- mobile_agent/keychain_acl.py +374 -0
- mobile_agent/keys.py +395 -0
- mobile_agent/latency_overlap.py +36 -0
- mobile_agent/latency_trace.py +170 -0
- mobile_agent/learned/__init__.py +15 -0
- mobile_agent/learned/app_path.py +160 -0
- mobile_agent/learned/code_read.py +38 -0
- mobile_agent/learned/parts.py +546 -0
- mobile_agent/learned/programs.py +691 -0
- mobile_agent/learned/register.py +171 -0
- mobile_agent/learned/routes.py +61 -0
- mobile_agent/learned/taught.py +329 -0
- mobile_agent/learned/teach.py +538 -0
- mobile_agent/learned/workflows.py +647 -0
- mobile_agent/learned/zero.py +41 -0
- mobile_agent/links.py +37 -0
- mobile_agent/listen.py +607 -0
- mobile_agent/lock_spike.py +278 -0
- mobile_agent/lockscreen.py +526 -0
- mobile_agent/login.py +354 -0
- mobile_agent/longform/__init__.py +15 -0
- mobile_agent/longform/ads.py +191 -0
- mobile_agent/longform/blocks.py +117 -0
- mobile_agent/longform/budget.py +265 -0
- mobile_agent/longform/context.py +285 -0
- mobile_agent/longform/costs.py +93 -0
- mobile_agent/longform/digest.py +145 -0
- mobile_agent/longform/extract.py +508 -0
- mobile_agent/longform/feed.py +505 -0
- mobile_agent/longform/followup.py +168 -0
- mobile_agent/longform/frames.py +82 -0
- mobile_agent/longform/identity.py +87 -0
- mobile_agent/longform/inbox.py +221 -0
- mobile_agent/longform/items.py +425 -0
- mobile_agent/longform/models.py +115 -0
- mobile_agent/longform/notebook.py +329 -0
- mobile_agent/longform/plan.py +614 -0
- mobile_agent/longform/recall.py +195 -0
- mobile_agent/longform/receipts.py +81 -0
- mobile_agent/longform/register.py +124 -0
- mobile_agent/longform/report.py +343 -0
- mobile_agent/longform/routes.py +102 -0
- mobile_agent/longform/session.py +644 -0
- mobile_agent/longform/store.py +146 -0
- mobile_agent/longform/synthesis.py +347 -0
- mobile_agent/mac_tools.py +223 -0
- mobile_agent/manual_control.py +143 -0
- mobile_agent/maps.py +149 -0
- mobile_agent/marks.py +92 -0
- mobile_agent/mcp_server/__init__.py +3 -0
- mobile_agent/mcp_server/__main__.py +29 -0
- mobile_agent/mcp_server/action_journal.py +24 -0
- mobile_agent/mcp_server/action_tools.py +638 -0
- mobile_agent/mcp_server/approvals.py +219 -0
- mobile_agent/mcp_server/catalog.py +92 -0
- mobile_agent/mcp_server/cli.py +194 -0
- mobile_agent/mcp_server/codes.py +147 -0
- mobile_agent/mcp_server/device_tools.py +306 -0
- mobile_agent/mcp_server/direct.py +272 -0
- mobile_agent/mcp_server/expo_tools.py +105 -0
- mobile_agent/mcp_server/phone_tools.py +510 -0
- mobile_agent/mcp_server/protocol.py +364 -0
- mobile_agent/mcp_server/registry.py +55 -0
- mobile_agent/mcp_server/run_tools.py +531 -0
- mobile_agent/mcp_server/schemas.py +425 -0
- mobile_agent/mcp_server/screen_tools.py +168 -0
- mobile_agent/mcp_server/session.py +469 -0
- mobile_agent/mcp_server/support.py +145 -0
- mobile_agent/mcp_server/tools.py +493 -0
- mobile_agent/memory/__init__.py +9 -0
- mobile_agent/memory/cli.py +207 -0
- mobile_agent/memory/extract.py +191 -0
- mobile_agent/memory/listener.py +100 -0
- mobile_agent/memory/model.py +144 -0
- mobile_agent/memory/project.py +96 -0
- mobile_agent/memory/provider.py +127 -0
- mobile_agent/memory/register.py +21 -0
- mobile_agent/memory/retrieval.py +172 -0
- mobile_agent/memory/routes.py +216 -0
- mobile_agent/memory/runs.py +124 -0
- mobile_agent/memory/store.py +806 -0
- mobile_agent/memory/writer.py +278 -0
- mobile_agent/mirroring.py +585 -0
- mobile_agent/mirroring_runtime.py +221 -0
- mobile_agent/model_clients.py +604 -0
- mobile_agent/narrate.py +813 -0
- mobile_agent/native_helpers.py +84 -0
- mobile_agent/netxray/__init__.py +1 -0
- mobile_agent/netxray/analyze.py +322 -0
- mobile_agent/netxray/attribution.py +62 -0
- mobile_agent/netxray/capture.py +88 -0
- mobile_agent/netxray/cli.py +142 -0
- mobile_agent/netxray/job.py +219 -0
- mobile_agent/netxray/mobster-pcap.c +560 -0
- mobile_agent/netxray/packets.py +410 -0
- mobile_agent/netxray/pcapng.py +223 -0
- mobile_agent/netxray/register.py +93 -0
- mobile_agent/netxray/report.py +214 -0
- mobile_agent/netxray/store.py +48 -0
- mobile_agent/netxray/summary.py +157 -0
- mobile_agent/netxray/target.py +67 -0
- mobile_agent/netxray/tool.py +66 -0
- mobile_agent/netxray/trackers.py +194 -0
- mobile_agent/notify_triggers/__init__.py +1 -0
- mobile_agent/notify_triggers/rules.py +39 -0
- mobile_agent/notify_triggers/sources.py +315 -0
- mobile_agent/notify_triggers/watch.py +135 -0
- mobile_agent/offers.py +86 -0
- mobile_agent/orchestrate.py +215 -0
- mobile_agent/outlets/__init__.py +1 -0
- mobile_agent/outlets/connect.py +249 -0
- mobile_agent/outlets/folder.py +180 -0
- mobile_agent/outlets/hooks.py +453 -0
- mobile_agent/outlets/inbound.py +472 -0
- mobile_agent/outlets/inbound_reply.py +57 -0
- mobile_agent/outlets/inbound_routes.py +81 -0
- mobile_agent/outlets/kinds.py +308 -0
- mobile_agent/outlets/mail.py +110 -0
- mobile_agent/outlets/net.py +86 -0
- mobile_agent/outlets/notion.py +244 -0
- mobile_agent/outlets/register.py +56 -0
- mobile_agent/outlets/routes.py +135 -0
- mobile_agent/outlets/service.py +580 -0
- mobile_agent/outlets/sheets.py +169 -0
- mobile_agent/outlets/slack.py +177 -0
- mobile_agent/outlets/store.py +194 -0
- mobile_agent/outlets/vault.py +80 -0
- mobile_agent/outlets/webhook.py +316 -0
- mobile_agent/output_contract.py +141 -0
- mobile_agent/owner_presence.py +211 -0
- mobile_agent/ownership.py +219 -0
- mobile_agent/packs/__init__.py +1 -0
- mobile_agent/packs/calendar.py +243 -0
- mobile_agent/packs/context.py +68 -0
- mobile_agent/packs/core.py +757 -0
- mobile_agent/packs/front_door.py +316 -0
- mobile_agent/packs/heal.py +214 -0
- mobile_agent/packs/health.py +122 -0
- mobile_agent/packs/insights.py +76 -0
- mobile_agent/packs/instagram.py +640 -0
- mobile_agent/packs/linkedin.py +106 -0
- mobile_agent/packs/maps.py +196 -0
- mobile_agent/packs/mcp.py +149 -0
- mobile_agent/packs/messages.py +270 -0
- mobile_agent/packs/notes.py +226 -0
- mobile_agent/packs/photos.py +166 -0
- mobile_agent/packs/pins.py +47 -0
- mobile_agent/packs/reddit.py +110 -0
- mobile_agent/packs/register.py +276 -0
- mobile_agent/packs/rows.py +108 -0
- mobile_agent/packs/safari.py +143 -0
- mobile_agent/packs/settings.py +253 -0
- mobile_agent/packs/social.py +300 -0
- mobile_agent/packs/threads.py +97 -0
- mobile_agent/packs/tiktok.py +455 -0
- mobile_agent/packs/tools.py +290 -0
- mobile_agent/packs/x.py +127 -0
- mobile_agent/packs/youtube.py +199 -0
- mobile_agent/parity.py +270 -0
- mobile_agent/paths.py +99 -0
- mobile_agent/perception.py +470 -0
- mobile_agent/permissions/__init__.py +32 -0
- mobile_agent/permissions/consequence.py +535 -0
- mobile_agent/permissions/exceptions.py +99 -0
- mobile_agent/permissions/gate.py +316 -0
- mobile_agent/permissions/outside.py +316 -0
- mobile_agent/permissions/policy.py +288 -0
- mobile_agent/permissions/project_policy.py +411 -0
- mobile_agent/permissions/register.py +148 -0
- mobile_agent/permissions/rules.py +291 -0
- mobile_agent/phone_io/__init__.py +146 -0
- mobile_agent/phone_io/cli.py +427 -0
- mobile_agent/phone_io/clipboard.py +78 -0
- mobile_agent/phone_io/controls.py +78 -0
- mobile_agent/phone_io/files.py +289 -0
- mobile_agent/phone_io/handoff.py +123 -0
- mobile_agent/phone_io/icloud.py +118 -0
- mobile_agent/phone_io/install.py +188 -0
- mobile_agent/phone_io/network_capture.py +188 -0
- mobile_agent/phone_io/network_cli.py +138 -0
- mobile_agent/phone_io/network_profiles.py +138 -0
- mobile_agent/phone_io/photos.py +512 -0
- mobile_agent/phone_io/recording.py +115 -0
- mobile_agent/phone_io/secrets.py +146 -0
- mobile_agent/phone_io/transfer.py +895 -0
- mobile_agent/phone_io/tunnel.py +129 -0
- mobile_agent/phone_ready.py +91 -0
- mobile_agent/pinning.py +158 -0
- mobile_agent/plan_gate.py +807 -0
- mobile_agent/plugins/__init__.py +3 -0
- mobile_agent/plugins/install.py +163 -0
- mobile_agent/plugins/manifest.py +242 -0
- mobile_agent/plugins/register.py +15 -0
- mobile_agent/plugins/routes.py +82 -0
- mobile_agent/plugins/service.py +322 -0
- mobile_agent/plugins/skills.py +76 -0
- mobile_agent/plugins/store.py +79 -0
- mobile_agent/pool.py +285 -0
- mobile_agent/power_watch.py +275 -0
- mobile_agent/premium_model.py +22 -0
- mobile_agent/project_env.py +291 -0
- mobile_agent/project_env_cli.py +74 -0
- mobile_agent/project_env_routes.py +55 -0
- mobile_agent/projects/__init__.py +27 -0
- mobile_agent/projects/ci.py +332 -0
- mobile_agent/projects/clone.py +339 -0
- mobile_agent/projects/drafts.py +158 -0
- mobile_agent/projects/folder.py +436 -0
- mobile_agent/projects/github.py +131 -0
- mobile_agent/projects/register.py +31 -0
- mobile_agent/projects/revisions.py +253 -0
- mobile_agent/projects/routes.py +234 -0
- mobile_agent/projects/runs.py +297 -0
- mobile_agent/projects/service.py +695 -0
- mobile_agent/projects/setup.py +84 -0
- mobile_agent/projects/store.py +379 -0
- mobile_agent/projects/sync.py +380 -0
- mobile_agent/projects/workflow_files.py +270 -0
- mobile_agent/replay_core/__init__.py +26 -0
- mobile_agent/replay_core/checks.py +594 -0
- mobile_agent/replay_core/engine.py +596 -0
- mobile_agent/replay_core/heal.py +364 -0
- mobile_agent/replay_core/path.py +240 -0
- mobile_agent/replay_core/recorder.py +206 -0
- mobile_agent/replay_core/store.py +171 -0
- mobile_agent/result_blocks.py +941 -0
- mobile_agent/run.py +642 -0
- mobile_agent/run_delivery.py +123 -0
- mobile_agent/run_executor.py +200 -0
- mobile_agent/run_export.py +284 -0
- mobile_agent/run_gif.py +891 -0
- mobile_agent/runner_hints.py +86 -0
- mobile_agent/runner_renewal.py +205 -0
- mobile_agent/runtime_create.py +549 -0
- mobile_agent/runtime_driver.py +458 -0
- mobile_agent/runtime_limits.py +319 -0
- mobile_agent/runtime_queue.py +394 -0
- mobile_agent/runtime_runs.py +383 -0
- mobile_agent/runtime_status.py +181 -0
- mobile_agent/runtime_teach.py +295 -0
- mobile_agent/runtime_work.py +599 -0
- mobile_agent/screen_text.py +101 -0
- mobile_agent/secret_filter.py +113 -0
- mobile_agent/serve.py +160 -0
- mobile_agent/server.py +363 -0
- mobile_agent/service_support.py +42 -0
- mobile_agent/settle/__init__.py +1 -0
- mobile_agent/settle/predict.py +102 -0
- mobile_agent/settle/replay.py +92 -0
- mobile_agent/settle/store.py +93 -0
- mobile_agent/settle/stream.py +43 -0
- mobile_agent/setup_cli.py +225 -0
- mobile_agent/setup_errors.py +87 -0
- mobile_agent/setup_service.py +527 -0
- mobile_agent/share.py +104 -0
- mobile_agent/signing.py +184 -0
- mobile_agent/sim/__init__.py +6 -0
- mobile_agent/sim/__main__.py +26 -0
- mobile_agent/sim/api.py +952 -0
- mobile_agent/sim/apps.py +172 -0
- mobile_agent/sim/biometrics.py +84 -0
- mobile_agent/sim/cli.py +271 -0
- mobile_agent/sim/doctor.py +293 -0
- mobile_agent/sim/governor.py +90 -0
- mobile_agent/sim/holders.py +88 -0
- mobile_agent/sim/models.py +62 -0
- mobile_agent/sim/registry.py +161 -0
- mobile_agent/sim/simctl.py +269 -0
- mobile_agent/sim/wda.py +618 -0
- mobile_agent/sim/xcode.py +74 -0
- mobile_agent/skills/__init__.py +12 -0
- mobile_agent/skills/codes.py +597 -0
- mobile_agent/skills/notifications.py +281 -0
- mobile_agent/skills/open_url.py +127 -0
- mobile_agent/smart_events.py +588 -0
- mobile_agent/smart_result.py +490 -0
- mobile_agent/smart_run.py +413 -0
- mobile_agent/state.py +662 -0
- mobile_agent/states.py +217 -0
- mobile_agent/steering.py +70 -0
- mobile_agent/style.py +178 -0
- mobile_agent/supervised.py +234 -0
- mobile_agent/swift/helper_bt_hid.swift +534 -0
- mobile_agent/swift/helper_doc_text.swift +184 -0
- mobile_agent/swift/helper_draft_reply.swift +170 -0
- mobile_agent/swift/helper_map.swift +110 -0
- mobile_agent/swift/helper_mirroring.swift +1099 -0
- mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
- mobile_agent/swift/helper_pdf.swift +100 -0
- mobile_agent/swift/helper_phone_audio.swift +993 -0
- mobile_agent/swift/helper_phone_screen.swift +416 -0
- mobile_agent/swift/helper_screen_text.swift +24 -0
- mobile_agent/swift/helper_transfer.swift +246 -0
- mobile_agent/swift/ocr.swift +22 -0
- mobile_agent/task_policy.py +337 -0
- mobile_agent/team_signing/__init__.py +34 -0
- mobile_agent/team_signing/apple_ca.py +37 -0
- mobile_agent/team_signing/bundle.py +136 -0
- mobile_agent/team_signing/cli.py +208 -0
- mobile_agent/team_signing/identity.py +248 -0
- mobile_agent/team_signing/ledger.py +105 -0
- mobile_agent/team_signing/prebuilt.py +61 -0
- mobile_agent/team_signing/resign.py +256 -0
- mobile_agent/team_signing/service.py +382 -0
- mobile_agent/team_signing/tooling.py +243 -0
- mobile_agent/team_signing/writes.py +268 -0
- mobile_agent/terminal_image.py +81 -0
- mobile_agent/testkit/__init__.py +1 -0
- mobile_agent/testkit/a11y_baseline.py +132 -0
- mobile_agent/testkit/api.py +403 -0
- mobile_agent/testkit/capsule.py +400 -0
- mobile_agent/testkit/capsule_cli.py +57 -0
- mobile_agent/testkit/cli.py +948 -0
- mobile_agent/testkit/cli_arguments.py +193 -0
- mobile_agent/testkit/comparison.py +288 -0
- mobile_agent/testkit/comparison_cli.py +41 -0
- mobile_agent/testkit/discover.py +257 -0
- mobile_agent/testkit/editor.py +243 -0
- mobile_agent/testkit/execute.py +515 -0
- mobile_agent/testkit/github_checks.py +248 -0
- mobile_agent/testkit/gitref.py +66 -0
- mobile_agent/testkit/heal_review.py +176 -0
- mobile_agent/testkit/health.py +480 -0
- mobile_agent/testkit/history.py +255 -0
- mobile_agent/testkit/hosted_results.py +321 -0
- mobile_agent/testkit/input_files.py +173 -0
- mobile_agent/testkit/jobs.py +285 -0
- mobile_agent/testkit/junit_verdict.py +33 -0
- mobile_agent/testkit/maestro.py +802 -0
- mobile_agent/testkit/mcp.py +562 -0
- mobile_agent/testkit/mcp_comparison.py +88 -0
- mobile_agent/testkit/minimize.py +471 -0
- mobile_agent/testkit/minimizer_cli.py +48 -0
- mobile_agent/testkit/notify.py +212 -0
- mobile_agent/testkit/phone_video.py +191 -0
- mobile_agent/testkit/pr.py +491 -0
- mobile_agent/testkit/pr_report.py +562 -0
- mobile_agent/testkit/prbuild.py +148 -0
- mobile_agent/testkit/proof_report.py +55 -0
- mobile_agent/testkit/provenance.py +119 -0
- mobile_agent/testkit/prrun.py +244 -0
- mobile_agent/testkit/recent.py +166 -0
- mobile_agent/testkit/recipes.py +146 -0
- mobile_agent/testkit/record.py +204 -0
- mobile_agent/testkit/register.py +288 -0
- mobile_agent/testkit/reports.py +998 -0
- mobile_agent/testkit/repro.py +432 -0
- mobile_agent/testkit/scenario.py +440 -0
- mobile_agent/testkit/scenario_cli.py +37 -0
- mobile_agent/testkit/snapshots_cli.py +60 -0
- mobile_agent/testkit/suite.py +670 -0
- mobile_agent/testkit/targets.py +497 -0
- mobile_agent/testkit/xctest.py +378 -0
- mobile_agent/threads/__init__.py +1 -0
- mobile_agent/threads/chat_once.py +264 -0
- mobile_agent/threads/cli.py +740 -0
- mobile_agent/threads/client.py +326 -0
- mobile_agent/threads/context.py +246 -0
- mobile_agent/threads/mcp.py +739 -0
- mobile_agent/threads/recent.py +34 -0
- mobile_agent/threads/register.py +54 -0
- mobile_agent/threads/routes.py +188 -0
- mobile_agent/threads/service.py +975 -0
- mobile_agent/threads/store.py +361 -0
- mobile_agent/threads/summary.py +117 -0
- mobile_agent/threads/term.py +101 -0
- mobile_agent/threads/workflow_cli.py +104 -0
- mobile_agent/tls.py +36 -0
- mobile_agent/traces.py +271 -0
- mobile_agent/tracks.py +235 -0
- mobile_agent/transport.py +271 -0
- mobile_agent/triggers/__init__.py +1 -0
- mobile_agent/triggers/actions.py +204 -0
- mobile_agent/triggers/model.py +146 -0
- mobile_agent/triggers/register.py +29 -0
- mobile_agent/triggers/routes.py +101 -0
- mobile_agent/triggers/service.py +387 -0
- mobile_agent/triggers/store.py +142 -0
- mobile_agent/tui/__init__.py +89 -0
- mobile_agent/tui/app.py +582 -0
- mobile_agent/tui/chrome.py +321 -0
- mobile_agent/tui/commands.py +375 -0
- mobile_agent/tui/demo_phone.py +369 -0
- mobile_agent/tui/images.py +29 -0
- mobile_agent/tui/keys.py +64 -0
- mobile_agent/tui/prompt.py +195 -0
- mobile_agent/tui/render.py +395 -0
- mobile_agent/tui/screens.py +180 -0
- mobile_agent/tui/session.py +308 -0
- mobile_agent/tui/tasks.py +287 -0
- mobile_agent/tui/theme.py +118 -0
- mobile_agent/tui/widgets.py +272 -0
- mobile_agent/update_check.py +213 -0
- mobile_agent/usage.py +184 -0
- mobile_agent/verify/__init__.py +0 -0
- mobile_agent/verify/__main__.py +41 -0
- mobile_agent/verify/a11y.py +189 -0
- mobile_agent/verify/actions.py +290 -0
- mobile_agent/verify/assertions.py +570 -0
- mobile_agent/verify/audit.py +312 -0
- mobile_agent/verify/business.py +221 -0
- mobile_agent/verify/business_run.py +131 -0
- mobile_agent/verify/business_state.py +168 -0
- mobile_agent/verify/check_schema.py +398 -0
- mobile_agent/verify/checks.py +614 -0
- mobile_agent/verify/cli.py +423 -0
- mobile_agent/verify/conditions.py +483 -0
- mobile_agent/verify/crashes.py +321 -0
- mobile_agent/verify/device_audit.py +246 -0
- mobile_agent/verify/dialogs.py +287 -0
- mobile_agent/verify/evidence.py +705 -0
- mobile_agent/verify/fixtures.py +305 -0
- mobile_agent/verify/flow.py +213 -0
- mobile_agent/verify/l10n.py +141 -0
- mobile_agent/verify/netcerts.py +67 -0
- mobile_agent/verify/netproxy.py +356 -0
- mobile_agent/verify/netrun.py +81 -0
- mobile_agent/verify/netshim.m +28 -0
- mobile_agent/verify/netshim.py +48 -0
- mobile_agent/verify/netstubs.py +132 -0
- mobile_agent/verify/overlay.py +174 -0
- mobile_agent/verify/perf.py +239 -0
- mobile_agent/verify/proof.py +192 -0
- mobile_agent/verify/replay_admission.py +48 -0
- mobile_agent/verify/report.py +952 -0
- mobile_agent/verify/run_cleanup.py +67 -0
- mobile_agent/verify/run_links.py +48 -0
- mobile_agent/verify/run_perf.py +44 -0
- mobile_agent/verify/run_secrets.py +39 -0
- mobile_agent/verify/runner.py +976 -0
- mobile_agent/verify/runs.py +57 -0
- mobile_agent/verify/screen_coverage.py +181 -0
- mobile_agent/verify/smart.py +106 -0
- mobile_agent/verify/snapshot_run.py +114 -0
- mobile_agent/verify/snapshots.py +306 -0
- mobile_agent/verify/sound.py +335 -0
- mobile_agent/verify/source_observation.py +130 -0
- mobile_agent/verify/system_surfaces/__init__.py +7 -0
- mobile_agent/verify/system_surfaces/play.py +106 -0
- mobile_agent/verify/system_surfaces/spec.py +139 -0
- mobile_agent/verify/system_surfaces/stage.py +138 -0
- mobile_agent/verify/system_surfaces/surfaces.py +121 -0
- mobile_agent/verify/tree.py +223 -0
- mobile_agent/verify/verdict.py +377 -0
- mobile_agent/verify/wire.py +392 -0
- mobile_agent/verify/wirestore.py +176 -0
- mobile_agent/waiting/__init__.py +1 -0
- mobile_agent/waiting/contacts.py +73 -0
- mobile_agent/waiting/drafts.py +136 -0
- mobile_agent/waiting/job.py +145 -0
- mobile_agent/waiting/messages.py +174 -0
- mobile_agent/waiting/rank.py +115 -0
- mobile_agent/waiting/register.py +26 -0
- mobile_agent/watch.py +502 -0
- mobile_agent/wda/__init__.py +2 -0
- mobile_agent/wda/frame_clock.py +646 -0
- mobile_agent/wda/frame_source.py +325 -0
- mobile_agent/wda/frame_store.py +115 -0
- mobile_agent/wda/gate.py +163 -0
- mobile_agent/wda/gestures.py +291 -0
- mobile_agent/wda/guards.py +94 -0
- mobile_agent/wda/identity.py +55 -0
- mobile_agent/wda/observe.py +884 -0
- mobile_agent/wda/patches.py +856 -0
- mobile_agent/wda/point_gestures.py +150 -0
- mobile_agent/wda/source.py +32 -0
- mobile_agent/wda/system_dialogs.py +571 -0
- mobile_agent/wda/text.py +694 -0
- mobile_agent/wda/timeouts.py +77 -0
- mobile_agent/wda/video.py +505 -0
- mobile_agent/wda/watchdog.py +243 -0
- mobile_agent/wireless/__init__.py +15 -0
- mobile_agent/wireless/api.py +354 -0
- mobile_agent/wireless/cli.py +287 -0
- mobile_agent/wireless/devicectl.py +229 -0
- mobile_agent/wireless/exposure.py +33 -0
- mobile_agent/wireless/mobster-wifi-pair.c +122 -0
- mobile_agent/wireless/monitor.py +332 -0
- mobile_agent/wireless/network.py +334 -0
- mobile_agent/wireless/onboarding.py +293 -0
- mobile_agent/wireless/pair.py +124 -0
- mobile_agent/wireless/probe.py +322 -0
- mobile_agent/wireless/register.py +8 -0
- mobile_agent/wireless/relay.py +179 -0
- mobile_agent/wireless/result.py +87 -0
- mobile_agent/wireless/store.py +85 -0
- mobile_agent/wireless/switch.py +77 -0
- mobile_agent/wireless/transport.py +118 -0
- mobile_agent/wireless/words.py +81 -0
- mobile_agent/wireless/xctestrun.py +83 -0
- mobile_agent/workflow_tools.py +59 -0
- mobile_agent/workflows/__init__.py +2 -0
- mobile_agent/workflows/brief.py +201 -0
- mobile_agent/workflows/cron.py +110 -0
- mobile_agent/workflows/health.py +257 -0
- mobile_agent/workflows/history.py +126 -0
- mobile_agent/workflows/records.py +442 -0
- mobile_agent/workflows/runs.py +1000 -0
- mobile_agent/workflows/scheduler.py +134 -0
- mobile_agent/workflows/service.py +973 -0
- mobile_agent/workflows/starters.py +285 -0
- mobile_agent/workflows/triggers.py +248 -0
- mobile_agent/xray/__init__.py +1 -0
- mobile_agent/xray/app_cli.py +116 -0
- mobile_agent/xray/cli.py +21 -0
- mobile_agent/xray/crawl.py +754 -0
- mobile_agent/xray/diff.py +171 -0
- mobile_agent/xray/diff_cli.py +43 -0
- mobile_agent/xray/job.py +184 -0
- mobile_agent/xray/paywall.py +188 -0
- mobile_agent/xray/policy.py +305 -0
- mobile_agent/xray/register.py +43 -0
- mobile_agent/xray/report.py +283 -0
- mobile_agent/xray/screens.py +117 -0
- mobster_cli-0.4.3.dist-info/METADATA +93 -0
- mobster_cli-0.4.3.dist-info/RECORD +1009 -0
- mobster_cli-0.4.3.dist-info/WHEEL +5 -0
- mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
- mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
- mobster_cli-0.4.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from collections import deque
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from ..verify.report import dots, has_node
|
|
11
|
+
from .targets import Refused, Target, TargetError, for_target
|
|
12
|
+
|
|
13
|
+
EXIT_CODES = {"passed": 0, "failed": 1, "needs_review": 2, "couldnt_run": 3}
|
|
14
|
+
# A cell's status when its repetitions disagree, and the suite's exit code: the first of these present wins.
|
|
15
|
+
WORST_FIRST = ("failed", "couldnt_run", "needs_review", "passed")
|
|
16
|
+
# couldnt_run classes a second attempt can fix.
|
|
17
|
+
RETRY_CLASSES = frozenset({"wda", "simulator", "launch", "busy", "internal", "replay"})
|
|
18
|
+
SAFETY_STOPS = ("unknown_outcome", "journal")
|
|
19
|
+
SAFETY_EXIT_STOPS = (*SAFETY_STOPS, "refused")
|
|
20
|
+
STOP_RANK = {"stopped": 0, "refused": 1, "journal": 2, "unknown_outcome": 3}
|
|
21
|
+
MAX_RETRIES, MAX_REPEAT, MAX_PARALLEL = 5, 20, 8
|
|
22
|
+
NOT_RUN = "The suite was stopped before this ran."
|
|
23
|
+
MARKS = {"passed": "✓", "failed": "✗", "needs_review": "!", "couldnt_run": "✗"}
|
|
24
|
+
# A real iPhone is asked this long whether it still answers after a check couldn't run on it.
|
|
25
|
+
ALIVE_TIMEOUT = 3.0
|
|
26
|
+
LOST = ("{name} stopped answering while this check was running. Mobster didn't run it again, because it can't tell "
|
|
27
|
+
"how far it got.")
|
|
28
|
+
LOST_FIX = "Reconnect and unlock the iPhone, then run the check again."
|
|
29
|
+
LOST_BEFORE = "{name} stopped answering earlier in this run, so this check didn't run on it."
|
|
30
|
+
BUSY_BEFORE = "{name} is in use by another Mobster app or command, so this check didn't run on it."
|
|
31
|
+
NO_DEVICE_LEFT = "Every device in this run stopped answering or is in use elsewhere, so this check didn't run."
|
|
32
|
+
# Where a spread check that never started is reported: on no device.
|
|
33
|
+
NOT_RUN_TARGET = Target("none", "device", record={"id": "none", "name": "Not run", "kind": "none"})
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Unit:
|
|
38
|
+
"""One repetition of one check on one target, and its attempts."""
|
|
39
|
+
index: int
|
|
40
|
+
entry: object
|
|
41
|
+
target: object
|
|
42
|
+
repetition: int
|
|
43
|
+
check: object = None
|
|
44
|
+
record: Optional[dict] = None
|
|
45
|
+
refused: Optional[dict] = None
|
|
46
|
+
notes: list = field(default_factory=list)
|
|
47
|
+
attempts: list = field(default_factory=list)
|
|
48
|
+
started: bool = False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def worst(verdicts):
|
|
52
|
+
for verdict in WORST_FIRST:
|
|
53
|
+
if verdict in verdicts:
|
|
54
|
+
return verdict
|
|
55
|
+
return "couldnt_run"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def attempt_record(result, attempt):
|
|
59
|
+
"""What a suite keeps of one verify result."""
|
|
60
|
+
reason = result.get("reason") or {}
|
|
61
|
+
frames = result.get("frames") or []
|
|
62
|
+
run_dir = result.get("run_dir")
|
|
63
|
+
frame = next((f for f in reversed(frames) if f.endswith("-verdict-ax.jpg")), None) or \
|
|
64
|
+
next((f for f in reversed(frames) if f.endswith("-verdict.jpg")), None)
|
|
65
|
+
return {"attempt": attempt, "runId": result.get("run_id"), "verdict": result.get("verdict") or "couldnt_run",
|
|
66
|
+
"class": reason.get("class"), "message": reason.get("message") or result.get("summary"),
|
|
67
|
+
"fix": reason.get("fix"), "summary": result.get("summary"), "seconds": result.get("seconds") or 0,
|
|
68
|
+
"runDir": run_dir, "report": result.get("report"),
|
|
69
|
+
"frame": f"{run_dir}/{frame}" if run_dir and frame else None, "video": result.get("video"),
|
|
70
|
+
"videoOffsetMs": result.get("video_offset_ms"), "device": dict(result.get("device") or {}),
|
|
71
|
+
"costUsd": result.get("cost_usd") or 0,
|
|
72
|
+
"replay": dict(result.get("replay") or {}) or None,
|
|
73
|
+
"linkFirst": dict((result.get("agent") or {}).get("linkFirst") or {}) or None,
|
|
74
|
+
"conditions": result.get("conditions"),
|
|
75
|
+
"business": _business_proof(result), "visual": dict(result.get("visual") or {}) or None,
|
|
76
|
+
"findings": [dict(item) for item in result.get("findings") or () if isinstance(item, dict)],
|
|
77
|
+
"screen": dict(result.get("screen") or {}) or None,
|
|
78
|
+
"snapshot": snapshot_of(result),
|
|
79
|
+
"evidence": dict(result.get("evidence") or {}) or None, "stepProof": result.get("step_proof"),
|
|
80
|
+
"perf": result.get("perf") or None, "screenCoverage": dict(result.get("screenCoverage") or {}) or None,
|
|
81
|
+
"healed": [dict(item) for item in result.get("healed") or () if isinstance(item, dict)],
|
|
82
|
+
"assertions": [{"index": item.get("index"), "text": item.get("text"), "ok": bool(item.get("ok")),
|
|
83
|
+
"observed": item.get("observed"), "outlined": has_node(item)}
|
|
84
|
+
for item in result.get("assertions") or () if isinstance(item, dict)]}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _business_proof(result):
|
|
88
|
+
"""Keep lifecycle proof and registered field IDs, without app-produced values."""
|
|
89
|
+
business = result.get("business") or {}
|
|
90
|
+
if not isinstance(business, dict) or not business:
|
|
91
|
+
return None
|
|
92
|
+
declared = ((result.get("check") or {}).get("business") or {}).get("expect")
|
|
93
|
+
observed = business.get("assertions")
|
|
94
|
+
fields = ([item.get("field") for item in declared] if isinstance(declared, list) and
|
|
95
|
+
all(isinstance(item, dict) and isinstance(item.get("field"), str) for item in declared) else None)
|
|
96
|
+
complete = (fields is not None and len(fields) == len(set(fields)) and isinstance(observed, list)
|
|
97
|
+
and len(observed) == len(fields) and
|
|
98
|
+
all(isinstance(item, dict) and type(item.get("ok")) is bool for item in observed) and
|
|
99
|
+
[item.get("field") for item in observed] == fields)
|
|
100
|
+
generation = business.get("generation")
|
|
101
|
+
source_digest = business.get("sourceSha256")
|
|
102
|
+
return {"bindingSha256": business.get("bindingSha256"), "setup": business.get("setup"),
|
|
103
|
+
"status": business.get("status"), "cleanup": business.get("cleanup"),
|
|
104
|
+
"generation": generation if type(generation) is int and generation >= 1 else None,
|
|
105
|
+
"sourceSha256": source_digest if isinstance(source_digest, str) and
|
|
106
|
+
re.fullmatch(r"[0-9a-f]{64}", source_digest) else None,
|
|
107
|
+
"assertionFields": fields if complete else None,
|
|
108
|
+
"passedFields": [item["field"] for item in observed if item["ok"]] if complete else None,
|
|
109
|
+
"failedFields": [item["field"] for item in observed if not item["ok"]] if complete else None}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def snapshot_of(result):
|
|
113
|
+
"""The run's snapshot (verify/snapshot_run.py) with its image paths made absolute, or None."""
|
|
114
|
+
snapshot = result.get("snapshot")
|
|
115
|
+
run_dir = result.get("run_dir")
|
|
116
|
+
if not isinstance(snapshot, dict):
|
|
117
|
+
return None
|
|
118
|
+
out = dict(snapshot)
|
|
119
|
+
for key in ("baseline", "current", "diff"):
|
|
120
|
+
if out.get(key) and run_dir:
|
|
121
|
+
out[key] = f"{run_dir}/{out[key]}"
|
|
122
|
+
return out
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def retryable(record):
|
|
126
|
+
if record["class"] in SAFETY_STOPS:
|
|
127
|
+
return False
|
|
128
|
+
if record["verdict"] == "failed":
|
|
129
|
+
return True
|
|
130
|
+
return record["verdict"] == "couldnt_run" and record["class"] in RETRY_CLASSES
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def device_label(target, attempts, check=None, resolved=None):
|
|
134
|
+
# Any idle emulator's record names no device, so a default-target Android run is named by what it reported.
|
|
135
|
+
named = resolved if resolved is not None and resolved.get("kind") != "android" else target.record
|
|
136
|
+
if named is not None:
|
|
137
|
+
device = (attempts[-1].get("device") or {}) if attempts else {}
|
|
138
|
+
runtime = device.get("runtime")
|
|
139
|
+
label = (named.get("name") or named.get("id") or "device") + (
|
|
140
|
+
f" · {runtime}" if runtime and named.get("kind") != "simulator" else "")
|
|
141
|
+
return label, target.key if named is target.record else (
|
|
142
|
+
f"device:{named.get('id') or named.get('udid') or named.get('name') or 'unknown'}")
|
|
143
|
+
for record in reversed(attempts):
|
|
144
|
+
device = record.get("device") or {}
|
|
145
|
+
if device.get("type") and device.get("runtime") and device.get("udid"):
|
|
146
|
+
return f"{device['type']} · {device['runtime']}", f"sim:{device['type']}@{device['runtime']}"
|
|
147
|
+
if target.kind == "default":
|
|
148
|
+
named = getattr(check, "device", None)
|
|
149
|
+
label = " · ".join(filter(None, [named or "Default simulator", getattr(check, "runtime", None)]))
|
|
150
|
+
# Until a run reports its simulator, the label says each check picks its own; the key stays stable.
|
|
151
|
+
return (label if named else target.label), f"default:{label}"
|
|
152
|
+
return target.label, target.key
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
GB_PER_SIMULATOR = 4 # the memory budget for a simulator, WebDriverAgent and a run
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def machine_parallel_cap(cpus=None, memory=None, environ=None):
|
|
159
|
+
"""How many simulators this Mac runs at once: one per two cores and per 4 GB of memory, 1 to MAX_PARALLEL."""
|
|
160
|
+
import os
|
|
161
|
+
environ = os.environ if environ is None else environ
|
|
162
|
+
try:
|
|
163
|
+
fixed = int(environ.get("MOBSTER_TEST_MAX_PARALLEL", ""))
|
|
164
|
+
except ValueError:
|
|
165
|
+
fixed = 0
|
|
166
|
+
if fixed > 0:
|
|
167
|
+
return min(fixed, MAX_PARALLEL)
|
|
168
|
+
cpus = cpus if cpus is not None else (os.cpu_count() or 2)
|
|
169
|
+
if memory is None:
|
|
170
|
+
try:
|
|
171
|
+
memory = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES")
|
|
172
|
+
except (ValueError, OSError, AttributeError):
|
|
173
|
+
memory = 16 * 1024 ** 3
|
|
174
|
+
return max(1, min(MAX_PARALLEL, cpus // 2, int(memory // (GB_PER_SIMULATOR * 1024 ** 3))))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class Suite:
|
|
178
|
+
"""``attempt(check, record, *, label, video_name)`` runs one attempt and returns verify's result (execute.Attempts,
|
|
179
|
+
or a fake)."""
|
|
180
|
+
|
|
181
|
+
def __init__(self, entries, targets, *, attempt, retries=1, repeat=1, parallel=1, strict=False, gate=None,
|
|
182
|
+
named_device=None, progress=None, stop=None, clock=time.time, on_cancel=None, spread=False,
|
|
183
|
+
alive=None, cap=None, before_attempt=None):
|
|
184
|
+
self.entries, self.targets = list(entries), list(targets)
|
|
185
|
+
self.attempt = attempt
|
|
186
|
+
self.retries = max(0, min(int(retries), MAX_RETRIES))
|
|
187
|
+
self.repeat = max(1, min(int(repeat), MAX_REPEAT))
|
|
188
|
+
self.parallel = max(1, min(int(parallel), MAX_PARALLEL))
|
|
189
|
+
self.progress = progress or (lambda line: None)
|
|
190
|
+
cap = machine_parallel_cap() if cap is None else cap
|
|
191
|
+
if self.parallel > cap:
|
|
192
|
+
self.progress(f"This Mac’s configured simulator limit is {cap}, so --parallel {self.parallel} runs {cap}. "
|
|
193
|
+
"MOBSTER_TEST_MAX_PARALLEL=N changes the limit.")
|
|
194
|
+
self.parallel = cap
|
|
195
|
+
self.strict = bool(strict)
|
|
196
|
+
self.gate, self.named_device = gate, named_device
|
|
197
|
+
self.stop = stop or threading.Event()
|
|
198
|
+
self.clock = clock
|
|
199
|
+
self.on_cancel = on_cancel
|
|
200
|
+
self.before_attempt = before_attempt
|
|
201
|
+
self.spread = bool(spread)
|
|
202
|
+
self.alive = alive or phone_answers # alive(target) -> whether a real iPhone still answers
|
|
203
|
+
self.out = {} # target key -> "lost" | "busy": devices that take no more checks in this run
|
|
204
|
+
self._inflight = 0
|
|
205
|
+
self.interrupted = None # the KeyboardInterrupt (ctrl+c, or devtools.Terminated) that stopped the run
|
|
206
|
+
self.units = []
|
|
207
|
+
self.started_at = self.finished_at = None
|
|
208
|
+
self._lock = threading.Lock()
|
|
209
|
+
self._abort_called = False
|
|
210
|
+
self._sealed = False
|
|
211
|
+
self.stop_reason = None
|
|
212
|
+
bind_gate = getattr(attempt, "bind_gate", None)
|
|
213
|
+
self._leased_real = callable(bind_gate)
|
|
214
|
+
if self._leased_real:
|
|
215
|
+
bind_gate(gate)
|
|
216
|
+
self._plan()
|
|
217
|
+
|
|
218
|
+
def _plan(self):
|
|
219
|
+
index = 0
|
|
220
|
+
for entry in self.entries:
|
|
221
|
+
if self.spread:
|
|
222
|
+
for repetition in range(1, self.repeat + 1):
|
|
223
|
+
index += 1
|
|
224
|
+
refused = None if entry.check is not None else self._resolve(entry, None)[2]
|
|
225
|
+
self.units.append(Unit(index, entry, NOT_RUN_TARGET if refused else None, repetition,
|
|
226
|
+
refused=refused))
|
|
227
|
+
continue
|
|
228
|
+
for target in self.targets:
|
|
229
|
+
check, record, refused, notes = self._resolve(entry, target)
|
|
230
|
+
if self.repeat > 1 and check is not None:
|
|
231
|
+
bind_repeat = getattr(self.attempt, "bind_repeat", None)
|
|
232
|
+
if callable(bind_repeat):
|
|
233
|
+
bind_repeat(check, record)
|
|
234
|
+
for repetition in range(1, self.repeat + 1):
|
|
235
|
+
index += 1
|
|
236
|
+
self.units.append(Unit(index, entry, target, repetition, check=check, record=record,
|
|
237
|
+
refused=refused, notes=list(notes)))
|
|
238
|
+
|
|
239
|
+
def _resolve(self, entry, target):
|
|
240
|
+
if entry.check is None:
|
|
241
|
+
return None, None, {"class": "usage", "message": entry.error or "The check can't be read.", "fix": None}, []
|
|
242
|
+
if target is not None and target.real and not self._leased_real:
|
|
243
|
+
return None, None, {"class": "device", "message": "Real-device checks need a runner that holds the "
|
|
244
|
+
"device lease during app admission.", "fix": None}, []
|
|
245
|
+
try:
|
|
246
|
+
check, notes, record = for_target(entry.check, target, named_device=self.named_device, gate=self.gate)
|
|
247
|
+
except Refused as error:
|
|
248
|
+
return None, None, {"class": error.klass, "message": error.message, "fix": error.fix or None}, []
|
|
249
|
+
except TargetError as error:
|
|
250
|
+
return None, None, {"class": "usage", "message": str(error), "fix": None}, []
|
|
251
|
+
return check, record, None, notes
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def total(self):
|
|
255
|
+
return len(self.units)
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def done(self):
|
|
259
|
+
with self._lock:
|
|
260
|
+
return sum(1 for unit in self.units if unit.refused is not None or (
|
|
261
|
+
unit.attempts and (self.stop.is_set() or not self._needs_more(unit))))
|
|
262
|
+
|
|
263
|
+
def cancel(self, reason="stopped"):
|
|
264
|
+
"""Seal dispatch and abort in-flight work once, including an already-set external stop."""
|
|
265
|
+
with self._lock:
|
|
266
|
+
if self._sealed:
|
|
267
|
+
return False
|
|
268
|
+
self.stop.set()
|
|
269
|
+
if self.stop_reason is None or STOP_RANK[reason] > STOP_RANK[self.stop_reason]:
|
|
270
|
+
self.stop_reason = reason
|
|
271
|
+
abort = not self._abort_called
|
|
272
|
+
self._abort_called = True
|
|
273
|
+
if abort and self.on_cancel is not None:
|
|
274
|
+
self.on_cancel()
|
|
275
|
+
return True
|
|
276
|
+
|
|
277
|
+
def seal(self):
|
|
278
|
+
"""Commit the terminal stop decision before reports are serialized."""
|
|
279
|
+
with self._lock:
|
|
280
|
+
if self.stop.is_set() and self.stop_reason is None:
|
|
281
|
+
self.stop_reason = "stopped"
|
|
282
|
+
self._sealed = True
|
|
283
|
+
|
|
284
|
+
def run(self):
|
|
285
|
+
"""Run every unit; returns ``results()``. Stops early, between attempts, once ``stop`` is set."""
|
|
286
|
+
self.started_at = self.clock()
|
|
287
|
+
if self.stop.is_set():
|
|
288
|
+
self.cancel()
|
|
289
|
+
workers = []
|
|
290
|
+
if self.spread:
|
|
291
|
+
queue = deque(unit for unit in self.units if unit.refused is None)
|
|
292
|
+
sims = [target for target in self.targets if target.record is None]
|
|
293
|
+
lanes = [(queue, target) for target in self.targets if target.record is not None]
|
|
294
|
+
lanes += [(queue,
|
|
295
|
+
sims[number % len(sims)]) for number in range(max(self.parallel, len(sims)))] if sims else []
|
|
296
|
+
else:
|
|
297
|
+
queues = {}
|
|
298
|
+
for unit in self.units:
|
|
299
|
+
if unit.refused is None:
|
|
300
|
+
record = unit.record
|
|
301
|
+
lane = (f"device:{record.get('udid') or record.get('id') or 'unknown'}"
|
|
302
|
+
if record is not None else "sims")
|
|
303
|
+
queues.setdefault(lane, deque()).append(unit)
|
|
304
|
+
lanes = [(queue, None) for lane, queue in queues.items()
|
|
305
|
+
for _ in range(self.parallel if lane == "sims" else 1)]
|
|
306
|
+
try:
|
|
307
|
+
for queue, target in lanes:
|
|
308
|
+
worker = threading.Thread(target=self._work, args=(queue, target), daemon=True,
|
|
309
|
+
name=f"mobster-test-{len(workers) + 1}")
|
|
310
|
+
workers.append(worker)
|
|
311
|
+
worker.start()
|
|
312
|
+
for worker in workers:
|
|
313
|
+
while worker.is_alive():
|
|
314
|
+
if self.stop.is_set():
|
|
315
|
+
self.cancel()
|
|
316
|
+
worker.join(.2)
|
|
317
|
+
except BaseException as error:
|
|
318
|
+
if isinstance(error, KeyboardInterrupt):
|
|
319
|
+
self.interrupted = error
|
|
320
|
+
self.cancel()
|
|
321
|
+
self.progress("Stopping: the runs in flight end and write their reports.")
|
|
322
|
+
# A terminal report cannot outrun a worker that still owns a device: its final action may be uncertain.
|
|
323
|
+
while any(worker.is_alive() for worker in workers):
|
|
324
|
+
for worker in workers:
|
|
325
|
+
if worker.is_alive():
|
|
326
|
+
try:
|
|
327
|
+
worker.join(.2)
|
|
328
|
+
except KeyboardInterrupt as second:
|
|
329
|
+
self.interrupted = self.interrupted or second
|
|
330
|
+
if not isinstance(error, KeyboardInterrupt):
|
|
331
|
+
raise
|
|
332
|
+
if self.stop.is_set():
|
|
333
|
+
self.cancel()
|
|
334
|
+
self.finished_at = self.clock()
|
|
335
|
+
return self.results()
|
|
336
|
+
|
|
337
|
+
def _take(self, queue, target):
|
|
338
|
+
"""The next unit for a worker. Spread: it goes to ``target``; a unit that target refuses is finished here."""
|
|
339
|
+
while True:
|
|
340
|
+
with self._lock:
|
|
341
|
+
if self.stop.is_set():
|
|
342
|
+
return None
|
|
343
|
+
unit = queue.popleft() if queue else None
|
|
344
|
+
if unit is not None:
|
|
345
|
+
unit.started = True
|
|
346
|
+
self._inflight += 1
|
|
347
|
+
elif not self._inflight:
|
|
348
|
+
return None
|
|
349
|
+
if unit is None:
|
|
350
|
+
time.sleep(.02)
|
|
351
|
+
continue
|
|
352
|
+
if self.spread:
|
|
353
|
+
unit.target = target
|
|
354
|
+
unit.check, unit.record, unit.refused, unit.notes = self._resolve(unit.entry, target)
|
|
355
|
+
if self.repeat > 1 and unit.check is not None:
|
|
356
|
+
bind_repeat = getattr(self.attempt, "bind_repeat", None)
|
|
357
|
+
if callable(bind_repeat):
|
|
358
|
+
bind_repeat(unit.check, unit.record)
|
|
359
|
+
if unit.refused is not None:
|
|
360
|
+
with self._lock:
|
|
361
|
+
self._inflight -= 1
|
|
362
|
+
continue
|
|
363
|
+
return unit
|
|
364
|
+
|
|
365
|
+
def _work(self, queue, target):
|
|
366
|
+
while True:
|
|
367
|
+
if target is not None and target.key in self.out:
|
|
368
|
+
return
|
|
369
|
+
unit = self._take(queue, target)
|
|
370
|
+
if unit is None:
|
|
371
|
+
return
|
|
372
|
+
try:
|
|
373
|
+
self._run_unit(unit, queue)
|
|
374
|
+
finally:
|
|
375
|
+
with self._lock:
|
|
376
|
+
self._inflight -= 1
|
|
377
|
+
|
|
378
|
+
def _needs_more(self, unit):
|
|
379
|
+
last = unit.attempts[-1] if unit.attempts else None
|
|
380
|
+
if last is None:
|
|
381
|
+
return True
|
|
382
|
+
return last["verdict"] != "passed" and retryable(last) and len(unit.attempts) <= self.retries
|
|
383
|
+
|
|
384
|
+
def _run_unit(self, unit, queue):
|
|
385
|
+
if unit.target.key in self.out and not self.spread:
|
|
386
|
+
return # its device is gone for this run; the cell says so
|
|
387
|
+
while not self.stop.is_set():
|
|
388
|
+
number = len(unit.attempts) + 1
|
|
389
|
+
label = self._label(unit, number)
|
|
390
|
+
self.progress(f"▸ {label}")
|
|
391
|
+
validation_refused = False
|
|
392
|
+
try:
|
|
393
|
+
if self.before_attempt is not None:
|
|
394
|
+
self.before_attempt(unit.entry, unit.check, unit.record)
|
|
395
|
+
except Refused as error:
|
|
396
|
+
validation_refused = True
|
|
397
|
+
self.cancel("refused")
|
|
398
|
+
result = {"verdict": "couldnt_run", "reason": {"class": error.klass, "message": error.message,
|
|
399
|
+
"fix": error.fix or None}}
|
|
400
|
+
except Exception as error:
|
|
401
|
+
validation_refused = True
|
|
402
|
+
self.cancel("refused")
|
|
403
|
+
result = {"verdict": "couldnt_run", "reason": {"class": "internal", "message":
|
|
404
|
+
f"Mobster couldn't check whether this run is still allowed ({type(error).__name__})."}}
|
|
405
|
+
else:
|
|
406
|
+
if self.stop.is_set():
|
|
407
|
+
self.cancel()
|
|
408
|
+
result = {"verdict": "couldnt_run", "reason": {"class": "stopped", "message": NOT_RUN}}
|
|
409
|
+
else:
|
|
410
|
+
try:
|
|
411
|
+
result = self.attempt(unit.check, unit.record, label=label,
|
|
412
|
+
video_name=f"{unit.index:03d}-{_slug(unit.entry.name)}-{number}")
|
|
413
|
+
except Exception as error: # an attempt runner that raises: couldn't run, class internal
|
|
414
|
+
result = {"verdict": "couldnt_run", "reason": {"class": "internal", "message":
|
|
415
|
+
f"Mobster hit an unexpected error ({type(error).__name__})."}}
|
|
416
|
+
record = self._lost(unit.target, attempt_record(result, number))
|
|
417
|
+
if validation_refused:
|
|
418
|
+
record["_validation_refused"] = True
|
|
419
|
+
if self.spread and not unit.attempts and record["class"] == "busy" and unit.target.real and \
|
|
420
|
+
result.get("not_dispatched"):
|
|
421
|
+
# Held by another Mobster process before anything was tapped: another device takes the check.
|
|
422
|
+
with self._lock:
|
|
423
|
+
self.out[unit.target.key] = "busy"
|
|
424
|
+
unit.started, unit.target = False, None
|
|
425
|
+
queue.appendleft(unit)
|
|
426
|
+
self.progress(f"! {unit.entry.name}: {record['message']} Another device takes it.")
|
|
427
|
+
return
|
|
428
|
+
with self._lock:
|
|
429
|
+
unit.notes.extend(result.get("notes") or ())
|
|
430
|
+
unit.attempts.append(record)
|
|
431
|
+
safety_stop = record["class"] in SAFETY_STOPS
|
|
432
|
+
if safety_stop:
|
|
433
|
+
self.stop.set()
|
|
434
|
+
more = self._needs_more(unit) and not self.stop.is_set()
|
|
435
|
+
if safety_stop:
|
|
436
|
+
self.cancel(record["class"])
|
|
437
|
+
self.progress(self._ended(unit, record, more))
|
|
438
|
+
if not more:
|
|
439
|
+
return
|
|
440
|
+
|
|
441
|
+
def _lost(self, target, record):
|
|
442
|
+
"""``record``, or device_lost when a real iPhone's check couldn't run because the phone stopped answering."""
|
|
443
|
+
# Never run again: Mobster can't tell how far it got, and a step may have sent something.
|
|
444
|
+
if record["verdict"] != "couldnt_run" or record["class"] not in ("wda", "device") or not target.real \
|
|
445
|
+
or self.alive(target):
|
|
446
|
+
return record
|
|
447
|
+
with self._lock:
|
|
448
|
+
self.out[target.key] = "lost"
|
|
449
|
+
return {**record, "class": "device_lost", "message": LOST.format(name=f"“{dots(target.label)}”"),
|
|
450
|
+
"fix": LOST_FIX}
|
|
451
|
+
|
|
452
|
+
def _label(self, unit, number, attempts=()):
|
|
453
|
+
"""The line's name: the simulator a default-target run reported once it has, else the target's own label."""
|
|
454
|
+
where = device_label(unit.target, attempts, unit.entry.check)[0] if unit.target.kind == "default" \
|
|
455
|
+
else unit.target.label
|
|
456
|
+
parts = [f"{unit.entry.name} on {dots(where)}"]
|
|
457
|
+
if self.repeat > 1:
|
|
458
|
+
parts.append(f"run {unit.repetition} of {self.repeat}")
|
|
459
|
+
if number > 1:
|
|
460
|
+
parts.append(f"attempt {number} of {self.retries + 1}")
|
|
461
|
+
return parts[0] + (f" ({', '.join(parts[1:])})" if len(parts) > 1 else "")
|
|
462
|
+
|
|
463
|
+
def _ended(self, unit, record, more):
|
|
464
|
+
mark = MARKS.get(record["verdict"], "?")
|
|
465
|
+
word = {"passed": "passed", "failed": "failed", "needs_review": "needs review",
|
|
466
|
+
"couldnt_run": "couldn't run"}.get(record["verdict"], record["verdict"])
|
|
467
|
+
line = f"{mark} {word} {self._label(unit, record['attempt'], [record])} ({record['seconds']} s)"
|
|
468
|
+
if record["verdict"] != "passed" and record.get("message"):
|
|
469
|
+
line += f"\n {record['message']}"
|
|
470
|
+
if more:
|
|
471
|
+
line += "\n Trying again."
|
|
472
|
+
return line
|
|
473
|
+
|
|
474
|
+
def _units(self, entry, target):
|
|
475
|
+
return [unit for unit in self.units if unit.entry is entry and (unit.target or NOT_RUN_TARGET) is target]
|
|
476
|
+
|
|
477
|
+
def _targets_of(self, entry):
|
|
478
|
+
"""The targets ``entry`` has a result on: every target, or, spread, the ones its units ran on (or none)."""
|
|
479
|
+
if not self.spread:
|
|
480
|
+
return self.targets
|
|
481
|
+
found = {}
|
|
482
|
+
for unit in self.units:
|
|
483
|
+
if unit.entry is entry:
|
|
484
|
+
target = unit.target or NOT_RUN_TARGET
|
|
485
|
+
found[target.key] = target
|
|
486
|
+
return list(found.values())
|
|
487
|
+
|
|
488
|
+
def _unstarted(self, target):
|
|
489
|
+
"""Why a unit that never ran has no attempt: its device was lost or busy, none was left, or the suite ended."""
|
|
490
|
+
state = self.out.get(target.key)
|
|
491
|
+
if state == "lost":
|
|
492
|
+
return {"class": "device_lost", "message": LOST_BEFORE.format(name=f"“{dots(target.label)}”"),
|
|
493
|
+
"fix": LOST_FIX}
|
|
494
|
+
if state == "busy":
|
|
495
|
+
return {"class": "busy", "message": BUSY_BEFORE.format(name=f"“{dots(target.label)}”"), "fix": None}
|
|
496
|
+
if self.spread and self.out and not self.stop.is_set():
|
|
497
|
+
return {"class": "device_lost", "message": NO_DEVICE_LEFT, "fix": LOST_FIX}
|
|
498
|
+
return {"class": "stopped", "message": NOT_RUN, "fix": None}
|
|
499
|
+
|
|
500
|
+
def cell(self, entry, target):
|
|
501
|
+
"""One check on one target, across its repetitions: results.json's per-device result."""
|
|
502
|
+
units = self._units(entry, target)
|
|
503
|
+
finals, attempts = [], []
|
|
504
|
+
for unit in units:
|
|
505
|
+
if unit.refused is not None:
|
|
506
|
+
finals.append(("couldnt_run", {}, dict(unit.refused)))
|
|
507
|
+
continue
|
|
508
|
+
if not unit.attempts:
|
|
509
|
+
finals.append(("couldnt_run", {}, self._unstarted(unit.target or target)))
|
|
510
|
+
continue
|
|
511
|
+
last = unit.attempts[-1]
|
|
512
|
+
finals.append((last["verdict"], last, {"class": last["class"], "message": last["message"],
|
|
513
|
+
"fix": last["fix"]}))
|
|
514
|
+
attempts.extend({**record, "repetition": unit.repetition} for record in unit.attempts)
|
|
515
|
+
passed = sum(verdict == "passed" for verdict, _, _ in finals)
|
|
516
|
+
status = worst({verdict for verdict, _, _ in finals})
|
|
517
|
+
selected, reason = next(((record, why) for verdict, record, why in reversed(finals)
|
|
518
|
+
if verdict == status), ({}, None))
|
|
519
|
+
safety_record = next((record for klass in SAFETY_STOPS for record in reversed(attempts)
|
|
520
|
+
if record["class"] == klass), None)
|
|
521
|
+
safety_record = safety_record or next((record for record in reversed(attempts)
|
|
522
|
+
if record.get("_validation_refused")), None)
|
|
523
|
+
safety_stop = None
|
|
524
|
+
if safety_record is not None:
|
|
525
|
+
status, selected = "couldnt_run", safety_record
|
|
526
|
+
reason = {"class": safety_record["class"], "message": safety_record["message"],
|
|
527
|
+
"fix": safety_record["fix"]}
|
|
528
|
+
safety_stop = safety_record["class"] if safety_record["class"] in SAFETY_STOPS else "refused"
|
|
529
|
+
verdicts = {record["verdict"] for record in attempts}
|
|
530
|
+
flaky = "passed" in verdicts and len(verdicts) > 1
|
|
531
|
+
label, key = device_label(target, attempts, entry.check, units[0].record if units else None)
|
|
532
|
+
notes = list(dict.fromkeys(note for unit in units for note in unit.notes))
|
|
533
|
+
skipped = (selected.get("conditions") or {}).get("skipped") or ()
|
|
534
|
+
if skipped:
|
|
535
|
+
notes.append("Skipped on this device: " + " ".join(f"{item['key']}: {item['why']}" for item in skipped))
|
|
536
|
+
out = {"device": label, "deviceKey": key, "status": status, "attempts": len(attempts), "flaky": flaky,
|
|
537
|
+
"runId": selected.get("runId"), "runDir": selected.get("runDir"),
|
|
538
|
+
"report": selected.get("report"), "visual": selected.get("visual"),
|
|
539
|
+
"business": selected.get("business"),
|
|
540
|
+
"screenCoverage": selected.get("screenCoverage"),
|
|
541
|
+
"durationMs": round(sum(r["seconds"] for r in attempts) * 1000),
|
|
542
|
+
"healed": list(selected.get("healed") or ()), "reason": None if status == "passed" else reason,
|
|
543
|
+
"quarantined": entry.quarantined is not None,
|
|
544
|
+
"frame": selected.get("frame"), "video": selected.get("video"),
|
|
545
|
+
"videoOffsetMs": selected.get("videoOffsetMs"),
|
|
546
|
+
"assertions": list(selected.get("assertions") or ()), "stepProof": selected.get("stepProof"),
|
|
547
|
+
"costUsd": round(sum(r.get("costUsd") or 0 for r in attempts), 5),
|
|
548
|
+
"replayed": (selected.get("replay") or {}).get("mode") in ("replayed", "healed"),
|
|
549
|
+
"runs": [{key_: record.get(key_) for key_ in ("repetition", "attempt", "runId", "verdict", "class",
|
|
550
|
+
"message", "seconds", "report", "runDir", "frame",
|
|
551
|
+
"video", "business", "visual", "screenCoverage")}
|
|
552
|
+
for record in attempts]}
|
|
553
|
+
device = selected.get("device") or {}
|
|
554
|
+
out["environment"] = {"kind": "iphone" if target.real else "simulator",
|
|
555
|
+
"model": device.get("type"), "runtime": device.get("runtime")}
|
|
556
|
+
if self.repeat > 1:
|
|
557
|
+
out["passRate"] = round(passed / len(finals), 4) if finals else 0.0
|
|
558
|
+
if flaky:
|
|
559
|
+
out["flakyReason"] = next(({"class": r["class"], "message": r["message"],
|
|
560
|
+
"assertions": [item for item in r.get("assertions") or () if not item["ok"]]}
|
|
561
|
+
for r in attempts if r["verdict"] != "passed"), None)
|
|
562
|
+
if selected.get("conditions"):
|
|
563
|
+
out["conditions"] = selected["conditions"]
|
|
564
|
+
if selected.get("findings"):
|
|
565
|
+
out["findings"] = selected["findings"]
|
|
566
|
+
if selected.get("screen"):
|
|
567
|
+
out["screen"] = selected["screen"]
|
|
568
|
+
if selected.get("snapshot"):
|
|
569
|
+
out["snapshot"] = selected["snapshot"]
|
|
570
|
+
if selected.get("perf"):
|
|
571
|
+
out["perf"] = selected["perf"]
|
|
572
|
+
if notes:
|
|
573
|
+
out["notes"] = notes
|
|
574
|
+
if safety_stop:
|
|
575
|
+
out["safetyStop"] = safety_stop
|
|
576
|
+
# A pass after retry still retains the earlier crash/log evidence for diagnosing flakiness.
|
|
577
|
+
evidence = (_evidence([selected]) if selected else None) or _evidence(attempts)
|
|
578
|
+
if evidence is not None:
|
|
579
|
+
out["evidence"] = evidence
|
|
580
|
+
if selected.get("linkFirst"):
|
|
581
|
+
out["linkFirst"] = selected["linkFirst"]
|
|
582
|
+
if selected.get("replay"):
|
|
583
|
+
replay = selected["replay"]
|
|
584
|
+
out["replay"] = {key_: replay.get(key_) for key_ in ("mode", "reason", "steps", "replayed", "healed",
|
|
585
|
+
"live", "seconds", "costUsd", "pendingHeals",
|
|
586
|
+
"status", "approvedRevision", "fixed", "saved")}
|
|
587
|
+
return out
|
|
588
|
+
|
|
589
|
+
def results(self):
|
|
590
|
+
checks, devices = [], {}
|
|
591
|
+
for entry in self.entries:
|
|
592
|
+
results = []
|
|
593
|
+
for target in self._targets_of(entry):
|
|
594
|
+
result = self.cell(entry, target)
|
|
595
|
+
results.append(result)
|
|
596
|
+
known = devices.setdefault(result["deviceKey"], {**target.public(), "key": result["deviceKey"],
|
|
597
|
+
"name": result["device"]})
|
|
598
|
+
if len(result["device"]) > len(known["name"]):
|
|
599
|
+
known["name"] = result["device"] # "Sam's iPhone · iOS 26.4" once a run there reported its iOS
|
|
600
|
+
item = {"name": entry.name, "file": entry.file, "classname": entry.classname, "tags": list(entry.tags),
|
|
601
|
+
"quarantined": entry.quarantined is not None, "results": results}
|
|
602
|
+
if entry.locale:
|
|
603
|
+
item["locale"] = entry.locale
|
|
604
|
+
identity = json.dumps((entry.file, entry.locale), ensure_ascii=False, separators=(",", ":"))
|
|
605
|
+
item["caseId"] = "locale-" + hashlib.sha256(identity.encode("utf-8")).hexdigest()
|
|
606
|
+
if entry.quarantined:
|
|
607
|
+
item["quarantineReason"] = entry.quarantined
|
|
608
|
+
if entry.error:
|
|
609
|
+
item["error"] = entry.error
|
|
610
|
+
checks.append(item)
|
|
611
|
+
cells = [result for check in checks for result in check["results"]]
|
|
612
|
+
counted = [result for result in cells if not result["quarantined"]]
|
|
613
|
+
summary = {"passed": sum(r["status"] == "passed" for r in counted),
|
|
614
|
+
"failed": sum(r["status"] == "failed" for r in counted),
|
|
615
|
+
"needsReview": sum(r["status"] == "needs_review" for r in counted),
|
|
616
|
+
"couldntRun": sum(r["status"] == "couldnt_run" for r in counted),
|
|
617
|
+
"flaky": sum(bool(r["flaky"]) for r in counted),
|
|
618
|
+
"quarantined": len(cells) - len(counted), "total": len(cells),
|
|
619
|
+
"costUsd": round(sum(r["costUsd"] for r in cells), 5)}
|
|
620
|
+
smart = [r for r in cells if r.get("replay")]
|
|
621
|
+
if smart:
|
|
622
|
+
summary["replayed"] = sum(bool(r.get("replayed")) for r in smart)
|
|
623
|
+
summary["healed"] = sum(len(r.get("healed") or ()) for r in smart)
|
|
624
|
+
summary["cacheHitRate"] = round(summary["replayed"] / len(smart), 4)
|
|
625
|
+
linked = [r["linkFirst"] for r in cells if r.get("linkFirst")]
|
|
626
|
+
if linked:
|
|
627
|
+
summary["linkFirst"] = {"steps": sum(r["taken"] for r in linked),
|
|
628
|
+
"modelCallsSaved": sum(r["modelCallsSaved"] for r in linked)}
|
|
629
|
+
return {"checks": checks, "devices": list(devices.values()), "summary": summary,
|
|
630
|
+
"stopReason": self.stop_reason,
|
|
631
|
+
"exitCode": self.exit_code(counted),
|
|
632
|
+
"startedAt": _ms(self.started_at), "finishedAt": _ms(self.finished_at),
|
|
633
|
+
"durationMs": round(((self.finished_at or self.clock()) - (self.started_at or self.clock())) * 1000)}
|
|
634
|
+
|
|
635
|
+
def exit_code(self, counted):
|
|
636
|
+
if self.stop_reason in SAFETY_EXIT_STOPS:
|
|
637
|
+
return 3
|
|
638
|
+
if self.stop_reason == "stopped":
|
|
639
|
+
signum = getattr(self.interrupted, "signum", None)
|
|
640
|
+
return 130 if signum is None else 128 + int(signum)
|
|
641
|
+
statuses = {result["status"] for result in counted}
|
|
642
|
+
if "failed" in statuses or (self.strict and any(result["flaky"] for result in counted)):
|
|
643
|
+
return 1
|
|
644
|
+
if "couldnt_run" in statuses:
|
|
645
|
+
return 3
|
|
646
|
+
if "needs_review" in statuses:
|
|
647
|
+
return 2
|
|
648
|
+
return 0
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _evidence(attempts):
|
|
652
|
+
for record in reversed(attempts):
|
|
653
|
+
if record["verdict"] != "passed" and record.get("evidence"):
|
|
654
|
+
return {**record["evidence"], "runId": record.get("runId"), "runDir": record.get("runDir")}
|
|
655
|
+
return None
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def phone_answers(target):
|
|
659
|
+
"""Whether a real iPhone's WebDriverAgent still answers. An iPhone with no address doesn't."""
|
|
660
|
+
from ..devices import wda_answers
|
|
661
|
+
url = (target.record or {}).get("wdaUrl") or ""
|
|
662
|
+
return bool(url) and wda_answers(url, timeout=ALIVE_TIMEOUT)
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def _ms(seconds):
|
|
666
|
+
return round(seconds * 1000) if seconds is not None else None
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _slug(text):
|
|
670
|
+
return re.sub(r"[^a-z0-9]+", "-", str(text).lower()).strip("-")[:40] or "check"
|