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,705 @@
|
|
|
1
|
+
"""Failure evidence: what a run keeps to say why a check went red, not just that it did."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import plistlib
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import signal
|
|
9
|
+
import subprocess
|
|
10
|
+
import tempfile
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from collections import deque
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from .. import secret_filter
|
|
19
|
+
from . import crashes
|
|
20
|
+
|
|
21
|
+
LOG_FILE = "logs/app.log"
|
|
22
|
+
CRASH_DIR = "crashes"
|
|
23
|
+
LOG_CAP_BYTES = 1_000_000
|
|
24
|
+
TAIL_LINES = 2_000
|
|
25
|
+
LINE_LIMIT = 2_000
|
|
26
|
+
# The log process is reaped on a thread of its own: waiting for `simctl spawn` cost a check 0.35 s (16 Oct).
|
|
27
|
+
DRAIN_WAIT = .3
|
|
28
|
+
WINDOW_MS = 10_000
|
|
29
|
+
WINDOW_LINES = 60
|
|
30
|
+
STOP_WAIT = 3.0
|
|
31
|
+
# ReportCrash wrote a simulator's report 1-4 min after the crash under load (16 Oct); `mobster test` looks again.
|
|
32
|
+
EXIT_WAIT = 2.0
|
|
33
|
+
REPORT_WAIT = {"simulator": 8.0, "iphone": 15.0}
|
|
34
|
+
CRASH_POLL = .5
|
|
35
|
+
# A phone's clock and the Mac's can disagree by a little.
|
|
36
|
+
CLOCK_SLACK = 5.0
|
|
37
|
+
# SpringBoard's line when an app's process ends: "[app<id>:pid] Process exited: ... domain:signal(2) code:SIGTRAP(5)".
|
|
38
|
+
# A crash is a signal other than a kill, a watchdog or jetsam; Mobster's own relaunch shows as a force-quit.
|
|
39
|
+
EXITED = re.compile(r"\[app<(?P<bundle>[^>(]+)(?:\([^)]*\))?>:(?P<pid>\d+)\] Process exited: .*?"
|
|
40
|
+
r"domain:(?P<domain>[\w-]+)\(\d+\) code:(?P<code>[\w-]+)(?:\((?P<number>[^)]*)\))?")
|
|
41
|
+
NOT_CRASHES = frozenset({"SIGKILL", "SIGTERM", "SIGINT", "SIGHUP", "SIGQUIT"})
|
|
42
|
+
LEVELS = {"default": "notice", "notice": "notice", "info": "info", "debug": "debug", "error": "error",
|
|
43
|
+
"fault": "fault", "warning": "error", "critical": "fault"}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class Line:
|
|
48
|
+
at_ms: int
|
|
49
|
+
level: str
|
|
50
|
+
process: str
|
|
51
|
+
text: str
|
|
52
|
+
own: bool = False # from the app's own code (a subsystem not Apple's), which the window keeps first
|
|
53
|
+
driver: bool = False # XCTest's own lines in the app's process (com.apple.dt.*): Mobster's driver, not the app
|
|
54
|
+
apple: bool = False # an Apple framework's subsystem (com.apple.*), even inside the app's process: accessibility
|
|
55
|
+
|
|
56
|
+
def render(self):
|
|
57
|
+
return f"{self.at_ms / 1000:9.3f} {self.level:<6} {self.process}: {self.text}"
|
|
58
|
+
|
|
59
|
+
def to_dict(self):
|
|
60
|
+
return {"atMs": self.at_ms, "level": self.level, "process": self.process, "text": self.text}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class Exit:
|
|
65
|
+
"""An app process ending, from SpringBoard's log."""
|
|
66
|
+
at_ms: int
|
|
67
|
+
pid: int
|
|
68
|
+
domain: str
|
|
69
|
+
code: str
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def crashed(self):
|
|
73
|
+
if self.domain == "signal":
|
|
74
|
+
return self.code not in NOT_CRASHES
|
|
75
|
+
if self.domain == "jetsam":
|
|
76
|
+
return True
|
|
77
|
+
return self.domain == "frontboard" and "8badf00d" in self.code.lower()
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def why(self):
|
|
81
|
+
"""'SIGTRAP', 'killed for using too much memory (jetsam)', 'the watchdog (0x8badf00d)'."""
|
|
82
|
+
if self.domain == "jetsam":
|
|
83
|
+
return "killed for using too much memory (jetsam)"
|
|
84
|
+
if "8badf00d" in self.code.lower():
|
|
85
|
+
return "killed by the watchdog (0x8badf00d)"
|
|
86
|
+
return self.code
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def exit_event(text, bundle_id):
|
|
90
|
+
"""The Exit in a SpringBoard line about ``bundle_id``'s process (at_ms 0), or None."""
|
|
91
|
+
match = EXITED.search(text or "")
|
|
92
|
+
if not match or match.group("bundle") != bundle_id:
|
|
93
|
+
return None
|
|
94
|
+
code = match.group("code")
|
|
95
|
+
if match.group("domain") == "frontboard" and match.group("number"):
|
|
96
|
+
code = match.group("number")
|
|
97
|
+
return Exit(0, int(match.group("pid")), match.group("domain"), code)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _predicate_text(value):
|
|
101
|
+
return '"' + str(value).replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class SimulatorTools:
|
|
105
|
+
"""A simulator's evidence: its unified log and the Mac's crash reports for its processes."""
|
|
106
|
+
|
|
107
|
+
kind = "simulator"
|
|
108
|
+
|
|
109
|
+
def __init__(self, udid, *, popen=subprocess.Popen, run=subprocess.run, reports=None):
|
|
110
|
+
self.udid = udid
|
|
111
|
+
self.popen, self.run = popen, run
|
|
112
|
+
self.reports = Path(reports) if reports else crashes.DIAGNOSTIC_REPORTS
|
|
113
|
+
|
|
114
|
+
def executable(self, app):
|
|
115
|
+
return getattr(app, "executable", None)
|
|
116
|
+
|
|
117
|
+
def log_argv(self, executable, bundle_id):
|
|
118
|
+
"""The app's own log, and SpringBoard's line when its process exits."""
|
|
119
|
+
predicate = (f"process == {_predicate_text(executable)} OR (process == \"SpringBoard\" AND "
|
|
120
|
+
f"eventMessage CONTAINS \"Process exited\" AND "
|
|
121
|
+
f"eventMessage CONTAINS {_predicate_text(bundle_id)})")
|
|
122
|
+
return ["/usr/bin/xcrun", "simctl", "spawn", self.udid, "log", "stream", "--style", "ndjson", "--level",
|
|
123
|
+
"info", "--predicate", predicate]
|
|
124
|
+
|
|
125
|
+
def parse(self, raw, arrival, t0_wall):
|
|
126
|
+
try:
|
|
127
|
+
event = json.loads(raw)
|
|
128
|
+
except ValueError:
|
|
129
|
+
return None
|
|
130
|
+
if not isinstance(event, dict) or event.get("eventType") not in (None, "logEvent", "activityCreateEvent"):
|
|
131
|
+
return None
|
|
132
|
+
stamp = _log_time(event.get("timestamp")) or arrival
|
|
133
|
+
process = Path(str(event.get("processImagePath") or "")).name or "app"
|
|
134
|
+
where = "/".join(filter(None, [event.get("subsystem"), event.get("category")]))
|
|
135
|
+
text = str(event.get("eventMessage") or "")
|
|
136
|
+
if where:
|
|
137
|
+
text = f"[{where}] {text}"
|
|
138
|
+
level = LEVELS.get(str(event.get("messageType") or "default").lower(), "notice")
|
|
139
|
+
subsystem = str(event.get("subsystem") or "")
|
|
140
|
+
own = bool(subsystem) and not subsystem.startswith("com.apple.")
|
|
141
|
+
return Line(round((stamp - t0_wall) * 1000), level, process, text, own, subsystem.startswith("com.apple.dt."),
|
|
142
|
+
subsystem.startswith("com.apple."))
|
|
143
|
+
|
|
144
|
+
def crash_reports(self, executable, since):
|
|
145
|
+
"""The crash reports that may be this run's: the Mac's own files, read in place (no copy)."""
|
|
146
|
+
return crashes.candidates(self.reports, executable, since - CLOCK_SLACK), None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class PhoneTools:
|
|
150
|
+
# idevicecrashreport --keep copies the phone's crash reports and never removes them from the phone.
|
|
151
|
+
kind = "iphone"
|
|
152
|
+
|
|
153
|
+
def __init__(self, udid, *, network=False, popen=subprocess.Popen, run=subprocess.run, tool=None):
|
|
154
|
+
self.udid, self.network = udid, network
|
|
155
|
+
self.popen, self.run = popen, run
|
|
156
|
+
if tool is None:
|
|
157
|
+
from ..mac_tools import tool
|
|
158
|
+
self._tool = tool
|
|
159
|
+
|
|
160
|
+
def _base(self, name):
|
|
161
|
+
path = self._tool(name)
|
|
162
|
+
if not path:
|
|
163
|
+
raise LookupError(f"{name} isn't installed")
|
|
164
|
+
return [path, "-u", self.udid] + (["-n"] if self.network else [])
|
|
165
|
+
|
|
166
|
+
def executable(self, app):
|
|
167
|
+
"""The app's CFBundleExecutable, read from the phone (``ideviceinstaller list -b``); None when it can't."""
|
|
168
|
+
known = getattr(app, "executable", None)
|
|
169
|
+
if known:
|
|
170
|
+
return known
|
|
171
|
+
try:
|
|
172
|
+
done = self.run(self._base("ideviceinstaller") + ["list", "--all", "--xml", "-b", app.bundle_id,
|
|
173
|
+
"-a", "CFBundleIdentifier", "-a", "CFBundleExecutable"],
|
|
174
|
+
capture_output=True, timeout=20, stdin=subprocess.DEVNULL)
|
|
175
|
+
items = plistlib.loads(done.stdout) if done.returncode == 0 and done.stdout else []
|
|
176
|
+
except (OSError, subprocess.SubprocessError, LookupError, ValueError, plistlib.InvalidFileException):
|
|
177
|
+
return None
|
|
178
|
+
for item in items if isinstance(items, list) else ():
|
|
179
|
+
if isinstance(item, dict) and item.get("CFBundleIdentifier") == app.bundle_id:
|
|
180
|
+
name = item.get("CFBundleExecutable")
|
|
181
|
+
return name if isinstance(name, str) and name else None
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
def log_argv(self, executable, bundle_id):
|
|
185
|
+
"""The app's process and SpringBoard's (for the line when the app exits; the rest of SpringBoard's is dropped as
|
|
186
|
+
it is read)."""
|
|
187
|
+
return self._base("idevicesyslog") + ["--no-colors", "-p", f"{executable}|SpringBoard"]
|
|
188
|
+
|
|
189
|
+
SYSLOG = re.compile(r"^\w{3} +\d+ [\d:.]+ \S+ (?P<process>[^\[(\s]+)(?:\([^)]*\))?\[(?P<pid>\d+)\] "
|
|
190
|
+
r"<(?P<level>\w+)>: ?(?P<text>.*)$")
|
|
191
|
+
|
|
192
|
+
def parse(self, raw, arrival, t0_wall):
|
|
193
|
+
# Stamped when the Mac read it: the phone's clock isn't the run's.
|
|
194
|
+
text = raw.rstrip("\n")
|
|
195
|
+
if not text.strip() or text.startswith("[connected") or text.startswith("[disconnected"):
|
|
196
|
+
return None
|
|
197
|
+
match = self.SYSLOG.match(text)
|
|
198
|
+
at = round((arrival - t0_wall) * 1000)
|
|
199
|
+
if not match:
|
|
200
|
+
return Line(at, "", "", text.strip())
|
|
201
|
+
return Line(at, LEVELS.get(match.group("level").lower(), "notice"), match.group("process"),
|
|
202
|
+
match.group("text"))
|
|
203
|
+
|
|
204
|
+
def crash_reports(self, executable, since):
|
|
205
|
+
"""Copies of the phone's crash reports for ``executable`` (``idevicecrashreport --keep --filter``) in a
|
|
206
|
+
temporary folder the caller removes."""
|
|
207
|
+
folder = tempfile.mkdtemp(prefix="mobster-crashes-")
|
|
208
|
+
try:
|
|
209
|
+
self.run(self._base("idevicecrashreport") + ["--keep", "--filter", executable, folder],
|
|
210
|
+
capture_output=True, timeout=60, stdin=subprocess.DEVNULL)
|
|
211
|
+
except (OSError, subprocess.SubprocessError, LookupError):
|
|
212
|
+
return [], folder
|
|
213
|
+
found = []
|
|
214
|
+
for path in Path(folder).rglob("*.ips"):
|
|
215
|
+
if path.name.startswith(f"{executable}-"):
|
|
216
|
+
found.append(path)
|
|
217
|
+
return sorted(found, reverse=True), folder
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _log_time(text):
|
|
221
|
+
if not isinstance(text, str):
|
|
222
|
+
return None
|
|
223
|
+
try:
|
|
224
|
+
return datetime.strptime(text.strip(), "%Y-%m-%d %H:%M:%S.%f%z").timestamp()
|
|
225
|
+
except ValueError:
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class LogCapture:
|
|
230
|
+
"""One log process, read on its own thread into ``logs/app.log`` (capped) and kept in memory for the window."""
|
|
231
|
+
|
|
232
|
+
def __init__(self, tools, executable, bundle_id, path, *, t0_wall, clock=time.time, on_exit=None):
|
|
233
|
+
self.tools, self.executable, self.bundle_id, self.path = tools, executable, bundle_id, Path(path)
|
|
234
|
+
self.t0_wall, self.clock = t0_wall, clock
|
|
235
|
+
self.on_exit = on_exit or (lambda event: None)
|
|
236
|
+
self.lines = [] # what the file holds, in order
|
|
237
|
+
self.tail = deque(maxlen=TAIL_LINES)
|
|
238
|
+
self.dropped = 0
|
|
239
|
+
self.bytes = 0
|
|
240
|
+
self.error = None
|
|
241
|
+
self._process = self._thread = None
|
|
242
|
+
self._handle = None
|
|
243
|
+
self._lock = threading.Lock()
|
|
244
|
+
self._stopped = False
|
|
245
|
+
self._last = None # the entry a continuation line belongs to
|
|
246
|
+
|
|
247
|
+
def start(self):
|
|
248
|
+
argv = self.tools.log_argv(self.executable, self.bundle_id)
|
|
249
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
250
|
+
self._handle = open(self.path, "w", encoding="utf-8")
|
|
251
|
+
try:
|
|
252
|
+
self._process = self.tools.popen(argv, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
|
253
|
+
stderr=subprocess.DEVNULL, start_new_session=True, text=True,
|
|
254
|
+
encoding="utf-8", errors="replace", bufsize=1)
|
|
255
|
+
except BaseException:
|
|
256
|
+
self._handle.close()
|
|
257
|
+
self._handle = None
|
|
258
|
+
raise
|
|
259
|
+
self._thread = threading.Thread(target=self._read, name="mobster-evidence-log", daemon=True)
|
|
260
|
+
self._thread.start()
|
|
261
|
+
|
|
262
|
+
def _read(self):
|
|
263
|
+
try:
|
|
264
|
+
for raw in self._process.stdout:
|
|
265
|
+
line = self.tools.parse(raw, self.clock(), self.t0_wall)
|
|
266
|
+
if line is not None:
|
|
267
|
+
self._take(line)
|
|
268
|
+
except (OSError, ValueError, AttributeError) as error:
|
|
269
|
+
self.error = type(error).__name__
|
|
270
|
+
|
|
271
|
+
def _take(self, line):
|
|
272
|
+
if not line.level and not line.process: # the next line of the entry before
|
|
273
|
+
if self._last is None:
|
|
274
|
+
return
|
|
275
|
+
line = Line(self._last.at_ms, self._last.level, self._last.process, line.text)
|
|
276
|
+
elif line.process == "SpringBoard":
|
|
277
|
+
event = exit_event(line.text, self.bundle_id)
|
|
278
|
+
self._last = None
|
|
279
|
+
if event is None:
|
|
280
|
+
return
|
|
281
|
+
event.at_ms = line.at_ms
|
|
282
|
+
self.on_exit(event)
|
|
283
|
+
# SpringBoard's own words hold "code:SIGTRAP(5)", which the secret filter would take for a code.
|
|
284
|
+
line = Line(line.at_ms, line.level, "SpringBoard", f"{self.bundle_id} process {event.pid} exited: "
|
|
285
|
+
f"{event.domain} {event.code}" + (" (a crash)" if event.crashed else ""))
|
|
286
|
+
self._last = line if line.process != "SpringBoard" else None
|
|
287
|
+
line.text = secret_filter.redact(line.text[:LINE_LIMIT])
|
|
288
|
+
with self._lock:
|
|
289
|
+
if self._stopped:
|
|
290
|
+
return
|
|
291
|
+
size = len(line.render().encode("utf-8")) + 1
|
|
292
|
+
if self.bytes + size <= LOG_CAP_BYTES and not self.tail:
|
|
293
|
+
self.lines.append(line)
|
|
294
|
+
self.bytes += size
|
|
295
|
+
self._handle.write(line.render() + "\n")
|
|
296
|
+
else:
|
|
297
|
+
if len(self.tail) == self.tail.maxlen:
|
|
298
|
+
self.dropped += 1
|
|
299
|
+
self.tail.append(line)
|
|
300
|
+
|
|
301
|
+
def stop(self):
|
|
302
|
+
"""End the log process and finish the file (idempotent): the head, a note of what was dropped, the tail."""
|
|
303
|
+
process, self._process = self._process, None
|
|
304
|
+
if process is not None:
|
|
305
|
+
_signal(process)
|
|
306
|
+
threading.Thread(target=_end, args=(process,), name="mobster-evidence-reap", daemon=True).start()
|
|
307
|
+
if self._thread is not None:
|
|
308
|
+
self._thread.join(DRAIN_WAIT)
|
|
309
|
+
with self._lock:
|
|
310
|
+
if self._stopped:
|
|
311
|
+
return
|
|
312
|
+
self._stopped = True
|
|
313
|
+
handle, self._handle = self._handle, None
|
|
314
|
+
if handle is None:
|
|
315
|
+
return
|
|
316
|
+
try:
|
|
317
|
+
if self.dropped:
|
|
318
|
+
handle.write(f"[{self.dropped} lines dropped: the log passed {LOG_CAP_BYTES // 1000} kB; the last "
|
|
319
|
+
f"{len(self.tail)} follow]\n")
|
|
320
|
+
for line in self.tail:
|
|
321
|
+
text = line.render()
|
|
322
|
+
handle.write(text + "\n")
|
|
323
|
+
self.bytes += len(text.encode("utf-8")) + 1
|
|
324
|
+
finally:
|
|
325
|
+
handle.close()
|
|
326
|
+
|
|
327
|
+
@property
|
|
328
|
+
def count(self):
|
|
329
|
+
with self._lock:
|
|
330
|
+
return len(self.lines) + len(self.tail) + self.dropped
|
|
331
|
+
|
|
332
|
+
def all(self):
|
|
333
|
+
with self._lock:
|
|
334
|
+
return list(self.lines) + list(self.tail)
|
|
335
|
+
|
|
336
|
+
def window(self, center_ms, half=WINDOW_MS, limit=WINDOW_LINES):
|
|
337
|
+
near = [line for line in self.all() if abs(line.at_ms - center_ms) <= half and not line.driver]
|
|
338
|
+
if len(near) > limit:
|
|
339
|
+
def rank(i):
|
|
340
|
+
line = near[i]
|
|
341
|
+
return (0 if line.own or line.level in ("error", "fault") or line.process == "SpringBoard" else 1,
|
|
342
|
+
abs(line.at_ms - center_ms))
|
|
343
|
+
keep = sorted(range(len(near)), key=rank)[:limit]
|
|
344
|
+
near = [near[i] for i in sorted(keep)]
|
|
345
|
+
return near
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _signal(process):
|
|
349
|
+
"""SIGTERM to the log process's group (it was started in a session of its own)."""
|
|
350
|
+
try:
|
|
351
|
+
if process.poll() is None:
|
|
352
|
+
try:
|
|
353
|
+
if not isinstance(process, subprocess.Popen):
|
|
354
|
+
raise TypeError("not a process this module started")
|
|
355
|
+
os.killpg(process.pid, signal.SIGTERM)
|
|
356
|
+
except (OSError, TypeError):
|
|
357
|
+
process.terminate()
|
|
358
|
+
except Exception:
|
|
359
|
+
pass
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _end(process):
|
|
363
|
+
"""Reap the log process, killing it when it hasn't ended STOP_WAIT seconds after SIGTERM."""
|
|
364
|
+
try:
|
|
365
|
+
try:
|
|
366
|
+
process.wait(timeout=STOP_WAIT)
|
|
367
|
+
except subprocess.TimeoutExpired:
|
|
368
|
+
process.kill()
|
|
369
|
+
process.wait(timeout=STOP_WAIT)
|
|
370
|
+
except Exception:
|
|
371
|
+
pass
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
class Evidence:
|
|
375
|
+
"""The evidence of one run."""
|
|
376
|
+
|
|
377
|
+
def __init__(self, tools, app, run_dir, *, t0_wall, project_root=None, log=None, clock=time.time,
|
|
378
|
+
sleep=time.sleep):
|
|
379
|
+
self.tools, self.app, self.run_dir = tools, app, Path(run_dir)
|
|
380
|
+
self.t0_wall, self.root = t0_wall, project_root
|
|
381
|
+
self._log = log or (lambda line: None)
|
|
382
|
+
self.clock, self.sleep = clock, sleep
|
|
383
|
+
self.executable = None
|
|
384
|
+
self.capture = None
|
|
385
|
+
self.note = None # why there's no log, when there isn't one
|
|
386
|
+
self.pids = set() # the app's processes the run saw
|
|
387
|
+
self.exits = [] # Exit events from SpringBoard's log
|
|
388
|
+
self.crash_summary = None
|
|
389
|
+
self._looked = False
|
|
390
|
+
self._starter = None
|
|
391
|
+
self._closed = False # stop() came: a log the starter hasn't started yet never starts
|
|
392
|
+
self._lock = threading.Lock()
|
|
393
|
+
|
|
394
|
+
@property
|
|
395
|
+
def kind(self):
|
|
396
|
+
return self.tools.kind
|
|
397
|
+
|
|
398
|
+
def start(self):
|
|
399
|
+
"""Start the log on a thread of its own, so the launch never waits for it (an iPhone's executable name is one
|
|
400
|
+
ideviceinstaller call)."""
|
|
401
|
+
self._starter = threading.Thread(target=self._start, name="mobster-evidence-start", daemon=True)
|
|
402
|
+
self._starter.start()
|
|
403
|
+
|
|
404
|
+
def _start(self):
|
|
405
|
+
try:
|
|
406
|
+
self.executable = self.tools.executable(self.app)
|
|
407
|
+
except Exception as error:
|
|
408
|
+
self._log(f"evidence: no executable name ({type(error).__name__})")
|
|
409
|
+
if not self.executable:
|
|
410
|
+
self.note = "No log: Mobster couldn't read the app's executable name."
|
|
411
|
+
return
|
|
412
|
+
capture = LogCapture(self.tools, self.executable, self.app.bundle_id, self.run_dir / LOG_FILE,
|
|
413
|
+
t0_wall=self.t0_wall, clock=self.clock, on_exit=self._exited)
|
|
414
|
+
with self._lock:
|
|
415
|
+
if self._closed:
|
|
416
|
+
return
|
|
417
|
+
try:
|
|
418
|
+
capture.start()
|
|
419
|
+
except Exception as error:
|
|
420
|
+
self.note = "No log: " + ("the log tool isn't installed." if isinstance(error, LookupError)
|
|
421
|
+
else f"the log didn't start ({type(error).__name__}).")
|
|
422
|
+
self._log(f"evidence: no log ({type(error).__name__}: {error})")
|
|
423
|
+
return
|
|
424
|
+
self.capture = capture
|
|
425
|
+
|
|
426
|
+
def _exited(self, event):
|
|
427
|
+
with self._lock:
|
|
428
|
+
self.exits.append(event)
|
|
429
|
+
if event.crashed:
|
|
430
|
+
self.saw(event.pid)
|
|
431
|
+
self._log(f"evidence: {self.app.bundle_id} process {event.pid} exited: {event.why}")
|
|
432
|
+
|
|
433
|
+
def saw(self, pid):
|
|
434
|
+
"""A process of the app the run saw (WebDriverAgent's foreground app): its crash is this run's."""
|
|
435
|
+
if isinstance(pid, int) and not isinstance(pid, bool) and pid > 0:
|
|
436
|
+
with self._lock:
|
|
437
|
+
self.pids.add(pid)
|
|
438
|
+
|
|
439
|
+
def _ready(self, wait=STOP_WAIT * 4):
|
|
440
|
+
if self._starter is not None:
|
|
441
|
+
self._starter.join(wait)
|
|
442
|
+
|
|
443
|
+
def crashed(self):
|
|
444
|
+
"""The first crashed Exit the log showed, or None."""
|
|
445
|
+
with self._lock:
|
|
446
|
+
return next((event for event in self.exits if event.crashed), None)
|
|
447
|
+
|
|
448
|
+
def crash(self, *, suspect=False):
|
|
449
|
+
"""This run's crash summary, or None. Looks once; with ``suspect`` it waits EXIT_WAIT, then REPORT_WAIT."""
|
|
450
|
+
with self._lock:
|
|
451
|
+
if self._looked:
|
|
452
|
+
return self.crash_summary
|
|
453
|
+
self._ready()
|
|
454
|
+
exited = self.crashed()
|
|
455
|
+
if exited is None and suspect and self.capture is not None:
|
|
456
|
+
deadline = self.clock() + EXIT_WAIT
|
|
457
|
+
while exited is None and self.clock() < deadline:
|
|
458
|
+
self.sleep(CRASH_POLL)
|
|
459
|
+
exited = self.crashed()
|
|
460
|
+
summary = None
|
|
461
|
+
executable = self.executable or getattr(self.app, "executable", None)
|
|
462
|
+
if exited is not None or (suspect and self.capture is None):
|
|
463
|
+
wait = REPORT_WAIT.get(self.kind, 8.0) if exited is not None else 0
|
|
464
|
+
summary = self._report(executable, self.clock() + wait) if executable else None
|
|
465
|
+
if summary is None and exited is not None:
|
|
466
|
+
summary = {"type": None, "signal": exited.why, "subtype": None, "message": None, "thread": None,
|
|
467
|
+
"process": executable, "pid": exited.pid, "frames": [], "symbolicated": False,
|
|
468
|
+
"atMs": exited.at_ms, "report": None}
|
|
469
|
+
elif summary is not None and exited is not None and not summary.get("signal"):
|
|
470
|
+
summary["signal"] = exited.why
|
|
471
|
+
with self._lock:
|
|
472
|
+
self._looked = True
|
|
473
|
+
self.crash_summary = summary
|
|
474
|
+
return summary
|
|
475
|
+
|
|
476
|
+
def _report(self, executable, deadline):
|
|
477
|
+
"""The crash report's summary, looking until ``deadline``; None when it hasn't appeared."""
|
|
478
|
+
while True:
|
|
479
|
+
report = self._find(executable)
|
|
480
|
+
if report is not None:
|
|
481
|
+
return self._summarize(report, executable)
|
|
482
|
+
if self.clock() >= deadline:
|
|
483
|
+
return None
|
|
484
|
+
self.sleep(CRASH_POLL)
|
|
485
|
+
|
|
486
|
+
def _summarize(self, report, executable):
|
|
487
|
+
try:
|
|
488
|
+
dsyms = crashes.find_dsyms(getattr(self.app, "path", None), self.root)
|
|
489
|
+
summary = crashes.summary(report, executable=executable, dsyms=dsyms, run=self.tools.run)
|
|
490
|
+
copy = crashes.write_copy(report, self.run_dir / CRASH_DIR)
|
|
491
|
+
except Exception as error:
|
|
492
|
+
self._log(f"evidence: the crash report couldn't be read ({type(error).__name__}: {error})")
|
|
493
|
+
return None
|
|
494
|
+
summary["report"] = copy.relative_to(self.run_dir).as_posix()
|
|
495
|
+
summary["atMs"] = round((report.wall - self.t0_wall) * 1000)
|
|
496
|
+
self._log(f"evidence: crash report {report.path.name} ({summary.get('type')})")
|
|
497
|
+
return summary
|
|
498
|
+
|
|
499
|
+
def _find(self, executable):
|
|
500
|
+
try:
|
|
501
|
+
paths, scratch = self.tools.crash_reports(executable, self.t0_wall)
|
|
502
|
+
except Exception as error:
|
|
503
|
+
self._log(f"evidence: no crash reports ({type(error).__name__})")
|
|
504
|
+
return None
|
|
505
|
+
try:
|
|
506
|
+
with self._lock:
|
|
507
|
+
pids = set(self.pids)
|
|
508
|
+
udid = getattr(self.tools, "udid", None) if self.kind == "simulator" else None
|
|
509
|
+
for path in paths:
|
|
510
|
+
report = crashes.read(path)
|
|
511
|
+
if report is not None and crashes.belongs(
|
|
512
|
+
report, bundle_id=self.app.bundle_id, executable=executable,
|
|
513
|
+
since=self.t0_wall - (CLOCK_SLACK if self.kind == "iphone" else 0),
|
|
514
|
+
until=self.clock() + CLOCK_SLACK, pids=pids, udid=udid):
|
|
515
|
+
return report
|
|
516
|
+
return None
|
|
517
|
+
finally:
|
|
518
|
+
if scratch:
|
|
519
|
+
shutil.rmtree(scratch, ignore_errors=True)
|
|
520
|
+
|
|
521
|
+
def stop(self):
|
|
522
|
+
"""Stop the log (idempotent). A log still starting never starts."""
|
|
523
|
+
self._ready(STOP_WAIT)
|
|
524
|
+
with self._lock:
|
|
525
|
+
self._closed = True
|
|
526
|
+
capture = self.capture
|
|
527
|
+
if capture is not None:
|
|
528
|
+
capture.stop()
|
|
529
|
+
|
|
530
|
+
def lines(self):
|
|
531
|
+
"""The captured log as dicts for the per-step proof (verify/proof.py)."""
|
|
532
|
+
with self._lock:
|
|
533
|
+
capture = self.capture
|
|
534
|
+
return [{"atMs": line.at_ms, "level": line.level, "process": line.process, "text": line.text, "own": line.own,
|
|
535
|
+
"driver": line.driver} for line in capture.all()] if capture is not None else []
|
|
536
|
+
|
|
537
|
+
def summary(self, *, center_ms=None, steps=(), failed=False):
|
|
538
|
+
"""results.json's ``evidence``: the log file, and for a run that didn't pass, the window around the failure, the
|
|
539
|
+
step it came in and the crash."""
|
|
540
|
+
with self._lock:
|
|
541
|
+
capture, crash = self.capture, self.crash_summary
|
|
542
|
+
out = {"source": self.kind, "startedAt": round(self.t0_wall * 1000)}
|
|
543
|
+
if capture is not None:
|
|
544
|
+
out["log"] = {"path": LOG_FILE, "lines": capture.count, "bytes": capture.bytes, "dropped": capture.dropped}
|
|
545
|
+
elif self.note:
|
|
546
|
+
out["note"] = self.note
|
|
547
|
+
if crash is not None:
|
|
548
|
+
out["crash"] = dict(crash)
|
|
549
|
+
if not failed and crash is None:
|
|
550
|
+
return out
|
|
551
|
+
center = crash.get("atMs") if crash else center_ms
|
|
552
|
+
if center is None:
|
|
553
|
+
return out
|
|
554
|
+
out["atMs"] = center
|
|
555
|
+
step = failing_step(steps, center)
|
|
556
|
+
if step is not None:
|
|
557
|
+
out["step"] = step
|
|
558
|
+
if capture is not None:
|
|
559
|
+
# "app": the app under test's own line (its process, not an Apple framework's subsystem). The Mac app shows
|
|
560
|
+
# those first and folds the rest (accessibility, system daemons) behind Show system log (ICP-3).
|
|
561
|
+
executable = getattr(capture, "executable", None)
|
|
562
|
+
out["window"] = {"fromMs": center - WINDOW_MS, "toMs": center + WINDOW_MS,
|
|
563
|
+
"lines": [{**line.to_dict(), "app": bool(executable) and line.process == executable
|
|
564
|
+
and not line.apple} for line in capture.window(center)]}
|
|
565
|
+
return out
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def failing_step(steps, at_ms):
|
|
569
|
+
"""The step the failure came in: the first one recorded at or after ``at_ms``, else the last."""
|
|
570
|
+
# A step is recorded once its action and settle end, so a crash its tap causes comes before the step's own time.
|
|
571
|
+
timed = [(step.get("at_ms"), step.get("index")) for step in steps or ()
|
|
572
|
+
if isinstance(step.get("at_ms"), (int, float))]
|
|
573
|
+
for at, index in timed:
|
|
574
|
+
if at >= at_ms:
|
|
575
|
+
return index
|
|
576
|
+
return timed[-1][1] if timed else None
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def for_run(manager, target, app, run_dir, *, t0_wall, project_root=None, log=None):
|
|
580
|
+
"""The run's Evidence, or None when its manager offers none (a WebDriverAgent address, a fake)."""
|
|
581
|
+
offer = getattr(manager, "evidence_tools", None)
|
|
582
|
+
if not callable(offer):
|
|
583
|
+
return None
|
|
584
|
+
try:
|
|
585
|
+
tools = offer(target)
|
|
586
|
+
except Exception:
|
|
587
|
+
return None
|
|
588
|
+
if tools is None:
|
|
589
|
+
return None
|
|
590
|
+
return Evidence(tools, app, run_dir, t0_wall=t0_wall, project_root=project_root, log=log)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
# `mobster test` waits this long, once, at the end of a suite, for crash reports macOS hadn't written when their run
|
|
594
|
+
# ended. A Mac under load wrote them 1-4 min after the crash (16 Oct); an idle one, within seconds.
|
|
595
|
+
LATE_WAIT = 60.0
|
|
596
|
+
LATE_POLL = 1.0
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def late_reports(results, *, progress=None, budget=LATE_WAIT, clock=time.time, sleep=time.sleep, tools_for=None):
|
|
600
|
+
pending = [cell for check in results.get("checks") or () for cell in check.get("results") or ()
|
|
601
|
+
if _waiting(cell.get("evidence"))]
|
|
602
|
+
if not pending:
|
|
603
|
+
return 0
|
|
604
|
+
progress = progress or (lambda line: None)
|
|
605
|
+
count = len(pending)
|
|
606
|
+
progress(f"Waiting up to {budget:.0f} s for {count} crash report{'' if count == 1 else 's'}: macOS writes "
|
|
607
|
+
"them after the crash")
|
|
608
|
+
deadline, found = clock() + budget, 0
|
|
609
|
+
while pending:
|
|
610
|
+
for cell in list(pending):
|
|
611
|
+
try:
|
|
612
|
+
attached = _attach(cell, tools_for=tools_for or _tools_for)
|
|
613
|
+
except Exception:
|
|
614
|
+
attached = None
|
|
615
|
+
if attached is not False and attached is not None:
|
|
616
|
+
pending.remove(cell)
|
|
617
|
+
found += 1
|
|
618
|
+
elif attached is False:
|
|
619
|
+
pending.remove(cell) # its run folder is gone or unreadable: nothing to wait for
|
|
620
|
+
if not pending or clock() >= deadline:
|
|
621
|
+
break
|
|
622
|
+
sleep(LATE_POLL)
|
|
623
|
+
if pending:
|
|
624
|
+
progress(f"{len(pending)} crash report{'' if len(pending) == 1 else 's'} didn't arrive in time; macOS keeps "
|
|
625
|
+
"them in ~/Library/Logs/DiagnosticReports")
|
|
626
|
+
return found
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _waiting(evidence):
|
|
630
|
+
crash = evidence.get("crash") if isinstance(evidence, dict) else None
|
|
631
|
+
return bool(crash) and not crash.get("report") and bool(evidence.get("runDir"))
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _tools_for(source, udid):
|
|
635
|
+
if source == "simulator":
|
|
636
|
+
return SimulatorTools(udid)
|
|
637
|
+
if source == "iphone":
|
|
638
|
+
return PhoneTools(udid)
|
|
639
|
+
return None
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _attach(cell, *, tools_for):
|
|
643
|
+
"""One crash report, looked for once: True when found and written, None when not there yet, False when the run can't
|
|
644
|
+
take one."""
|
|
645
|
+
from types import SimpleNamespace
|
|
646
|
+
|
|
647
|
+
from .checks import project_root
|
|
648
|
+
from .crashes import headline
|
|
649
|
+
from .report import write_report
|
|
650
|
+
evidence = cell["evidence"]
|
|
651
|
+
run_dir = Path(evidence["runDir"])
|
|
652
|
+
try:
|
|
653
|
+
result = json.loads((run_dir / "result.json").read_text(encoding="utf-8"))
|
|
654
|
+
except (OSError, ValueError):
|
|
655
|
+
return False
|
|
656
|
+
stored = result.get("evidence") or {}
|
|
657
|
+
crash, app, device = stored.get("crash") or {}, result.get("app") or {}, result.get("device") or {}
|
|
658
|
+
tools = tools_for(stored.get("source"), device.get("udid"))
|
|
659
|
+
if tools is None or not stored.get("startedAt") or not crash.get("process"):
|
|
660
|
+
return False
|
|
661
|
+
lookup = Evidence(tools, SimpleNamespace(bundle_id=app.get("bundle_id"), executable=crash["process"],
|
|
662
|
+
path=app.get("path")),
|
|
663
|
+
run_dir, t0_wall=stored["startedAt"] / 1000, project_root=project_root(run_dir))
|
|
664
|
+
lookup.executable = crash["process"]
|
|
665
|
+
if isinstance(crash.get("pid"), int):
|
|
666
|
+
lookup.saw(crash["pid"])
|
|
667
|
+
summary = lookup._report(crash["process"], deadline=0)
|
|
668
|
+
if summary is None:
|
|
669
|
+
return None
|
|
670
|
+
if not summary.get("signal"):
|
|
671
|
+
summary["signal"] = crash.get("signal")
|
|
672
|
+
stored["crash"] = summary
|
|
673
|
+
# The report's own time is the crash's: SpringBoard logs the exit seconds later.
|
|
674
|
+
stored["atMs"] = summary["atMs"]
|
|
675
|
+
step = failing_step(result.get("steps"), summary["atMs"])
|
|
676
|
+
if step is not None:
|
|
677
|
+
stored["step"] = step
|
|
678
|
+
message = headline(summary, app.get("name") or app.get("bundle_id") or "The app")
|
|
679
|
+
if (result.get("reason") or {}).get("class") == "app_crashed":
|
|
680
|
+
result["reason"]["message"] = message
|
|
681
|
+
result["summary"] = message
|
|
682
|
+
result["evidence"] = stored
|
|
683
|
+
from ..paths import write_text_atomic
|
|
684
|
+
write_text_atomic(run_dir / "result.json", json.dumps(result, indent=1, ensure_ascii=False, allow_nan=False) + "\n")
|
|
685
|
+
write_report(run_dir, result)
|
|
686
|
+
evidence["crash"] = summary
|
|
687
|
+
evidence["atMs"] = stored["atMs"]
|
|
688
|
+
if "step" in stored:
|
|
689
|
+
evidence["step"] = stored["step"]
|
|
690
|
+
if (cell.get("reason") or {}).get("class") == "app_crashed":
|
|
691
|
+
cell["reason"]["message"] = message
|
|
692
|
+
for run in cell.get("runs") or ():
|
|
693
|
+
if run.get("runDir") == str(run_dir) and run.get("class") == "app_crashed":
|
|
694
|
+
run["message"] = message
|
|
695
|
+
return True
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def log_text(lines):
|
|
699
|
+
"""Window lines as plain text, for JUnit's <system-out>."""
|
|
700
|
+
return "\n".join(Line(item.get("atMs") or 0, item.get("level") or "", item.get("process") or "",
|
|
701
|
+
item.get("text") or "").render() for item in lines or ())
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
__all__ = ["Evidence", "LogCapture", "Line", "Exit", "SimulatorTools", "PhoneTools", "for_run", "failing_step",
|
|
705
|
+
"exit_event", "late_reports", "log_text", "WINDOW_MS", "LOG_FILE"]
|