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,948 @@
|
|
|
1
|
+
"""`mobster test`: run a project's checks on simulators and iPhones, with retries, flake detection and reports."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import shlex
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from ..paths import write_text_atomic
|
|
10
|
+
from ..verify.checks import LIMITS
|
|
11
|
+
from ..verify.report import dots
|
|
12
|
+
from . import repro, snapshots_cli, xctest
|
|
13
|
+
from .cli_arguments import USAGE_EXIT, add_arguments
|
|
14
|
+
|
|
15
|
+
RECORD = "record"
|
|
16
|
+
IMPORT = "import"
|
|
17
|
+
IMPORT_SHOWN = 8
|
|
18
|
+
ADD = "add"
|
|
19
|
+
PR = "pr"
|
|
20
|
+
PR_FLAGS = (("base", "--base"), ("no_build", "--no-build"), ("scheme", "--scheme"), ("project", "--project"),
|
|
21
|
+
("drafts", "--drafts"), ("no_gifs", "--no-gifs"), ("tested_with", "--tested-with"),
|
|
22
|
+
("comment", "--comment"), ("github", "--github"))
|
|
23
|
+
HEAL = "heal"
|
|
24
|
+
NIGHTLY = "nightly"
|
|
25
|
+
SCHEDULE = "schedule"
|
|
26
|
+
CAPSULE = "capsule"
|
|
27
|
+
COMPARE = "compare"
|
|
28
|
+
SCENARIO = "scenario"
|
|
29
|
+
MINIMIZE = "minimize"
|
|
30
|
+
LOCAL_RESULTS = {
|
|
31
|
+
"sim": None, "device": None, "testflight": None, "matrix": None, "locale": None,
|
|
32
|
+
"spread": False, "parallel": 1, "retries": None, "repeat": 1, "strict": False,
|
|
33
|
+
"quarantine": None, "auto_quarantine": False, "tag": None, "audit": None,
|
|
34
|
+
"a11y": None, "update_a11y_baseline": False, "shard": None, "keyless": False,
|
|
35
|
+
"replay": False, "fresh": False, "no_heal": False, "junit": None, "html": None,
|
|
36
|
+
"video": False, "history": False, "share": False, "share_days": 30,
|
|
37
|
+
"assert_timeout": 5, "record_run": None, "record_name": None,
|
|
38
|
+
"bundle": None, "app": None, "skip_unsupported": False, "dry_run": False,
|
|
39
|
+
"force": False, "add_list": False, "add_set": None, "base": None,
|
|
40
|
+
"no_build": False, "scheme": None, "project": None, "drafts": "run",
|
|
41
|
+
"no_gifs": False, "tested_with": False, "comment": None, "github": "check",
|
|
42
|
+
"at": None, "accept": False, "reject": False,
|
|
43
|
+
}
|
|
44
|
+
REPRO = "repro"
|
|
45
|
+
TONES = {"passed": "green", "failed": "coral", "needs_review": "accent", "couldnt_run": "tertiary", "flaky": "amber",
|
|
46
|
+
"draft": "accent"}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _paint(color):
|
|
50
|
+
from ..style import Palette
|
|
51
|
+
return Palette(color)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _pr_flags(args):
|
|
55
|
+
"""The first `mobster test pr` flag given without pr, or None."""
|
|
56
|
+
for name, flag in PR_FLAGS:
|
|
57
|
+
if name in ("scheme", "project") and getattr(args, "xctest", False):
|
|
58
|
+
continue
|
|
59
|
+
value = getattr(args, name, None)
|
|
60
|
+
if value and not (name == "drafts" and value == "run") and not (name == "github" and value == "check"):
|
|
61
|
+
return flag
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _isatty(stream):
|
|
66
|
+
try:
|
|
67
|
+
return stream.isatty()
|
|
68
|
+
except (AttributeError, ValueError):
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _only_testing(args, options):
|
|
73
|
+
"""Set options.xctest_only from --only-testing; True (after saying why) when one isn't a test identifier."""
|
|
74
|
+
try:
|
|
75
|
+
options.xctest_only = xctest.only_testing_ok(getattr(args, "only_testing", None) or ())
|
|
76
|
+
except xctest.XctestError as error:
|
|
77
|
+
_usage(f"{error.message} {error.fix}")
|
|
78
|
+
return True
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _usage(message):
|
|
83
|
+
print(f"mobster test: {message}", file=sys.stderr, flush=True)
|
|
84
|
+
return USAGE_EXIT
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def run(args):
|
|
88
|
+
"""`mobster test` for parsed ``args``; returns the exit code. Never raises into __main__."""
|
|
89
|
+
from ..devtools import signals_as_interrupt
|
|
90
|
+
paths = list(getattr(args, "paths", None) or ())
|
|
91
|
+
if (getattr(args, "minimize_bad_app", None) or getattr(args, "minimize_control_app", None) or
|
|
92
|
+
getattr(args, "minimize_isolated", False) or getattr(args, "minimize_max_attempts", None) is not None) \
|
|
93
|
+
and (not paths or paths[0] != MINIMIZE):
|
|
94
|
+
return _usage("--bad-app, --control-app, --isolated and --max-attempts go with mobster test minimize")
|
|
95
|
+
if getattr(args, "capsule_include", None) and (not paths or paths[0] != CAPSULE):
|
|
96
|
+
return _usage("--include goes with capsule export: mobster test capsule export SUITE_ID --include KIND")
|
|
97
|
+
if paths and paths[0] in (CAPSULE, COMPARE, SCENARIO, MINIMIZE):
|
|
98
|
+
command = paths[0]
|
|
99
|
+
for name, default in LOCAL_RESULTS.items():
|
|
100
|
+
if getattr(args, name, default) != default:
|
|
101
|
+
return _usage(f"--{name.replace('_', '-')} doesn't go with mobster test {command}")
|
|
102
|
+
if command != CAPSULE and getattr(args, "record_output", None):
|
|
103
|
+
return _usage(f"--output doesn't go with mobster test {command}")
|
|
104
|
+
if command == SCENARIO:
|
|
105
|
+
from . import scenario_cli
|
|
106
|
+
with signals_as_interrupt():
|
|
107
|
+
try:
|
|
108
|
+
return scenario_cli.run(args, paths[1:])
|
|
109
|
+
except KeyboardInterrupt:
|
|
110
|
+
print("mobster test scenario: stopped", file=sys.stderr, flush=True)
|
|
111
|
+
return 130
|
|
112
|
+
except Exception as error:
|
|
113
|
+
print(f"mobster test scenario: unexpected error ({type(error).__name__}: {error}). "
|
|
114
|
+
"Report the command you ran.", file=sys.stderr, flush=True)
|
|
115
|
+
return USAGE_EXIT
|
|
116
|
+
if command == MINIMIZE:
|
|
117
|
+
from . import minimizer_cli
|
|
118
|
+
with signals_as_interrupt():
|
|
119
|
+
try:
|
|
120
|
+
return minimizer_cli.run(args, paths[1:])
|
|
121
|
+
except KeyboardInterrupt:
|
|
122
|
+
print("mobster test minimize: stopped; review the saved trial before another attempt",
|
|
123
|
+
file=sys.stderr, flush=True)
|
|
124
|
+
return 130
|
|
125
|
+
except Exception as error:
|
|
126
|
+
print(f"mobster test minimize: unexpected error ({type(error).__name__}). "
|
|
127
|
+
"Review the saved trial before another attempt.", file=sys.stderr, flush=True)
|
|
128
|
+
return USAGE_EXIT
|
|
129
|
+
from . import capsule_cli, comparison_cli
|
|
130
|
+
return (capsule_cli if command == CAPSULE else comparison_cli).run(args, paths[1:])
|
|
131
|
+
simple = {RECORD: record, IMPORT: import_flows, ADD: add, snapshots_cli.SNAPSHOTS: snapshots_cli.snapshots,
|
|
132
|
+
REPRO: repro.main, NIGHTLY: nightly, SCHEDULE: schedule} # each takes (args, the rest of the paths)
|
|
133
|
+
if paths and paths[0] in simple:
|
|
134
|
+
return simple[paths[0]](args, paths[1:])
|
|
135
|
+
if paths and paths[0] == PR:
|
|
136
|
+
with signals_as_interrupt():
|
|
137
|
+
try:
|
|
138
|
+
return pr(args, paths[1:])
|
|
139
|
+
except KeyboardInterrupt:
|
|
140
|
+
print("mobster test: stopped", file=sys.stderr, flush=True)
|
|
141
|
+
return 130
|
|
142
|
+
except Exception as error: # a last resort, as for the suite
|
|
143
|
+
print(f"mobster test pr: unexpected error ({type(error).__name__}: {error}). Report it at "
|
|
144
|
+
"github.com/RadishSoftware/mobster/issues with the command you ran.", file=sys.stderr, flush=True)
|
|
145
|
+
return USAGE_EXIT
|
|
146
|
+
if getattr(args, "history", False):
|
|
147
|
+
return history(args, paths)
|
|
148
|
+
if (getattr(args, "test_plan", None) or getattr(args, "only_testing", None)) and not getattr(args, "xctest", False):
|
|
149
|
+
return _usage("--test-plan and --only-testing go with --xctest: mobster test --xctest --test-plan NAME")
|
|
150
|
+
if getattr(args, "at", None):
|
|
151
|
+
return _usage("--at goes with nightly: mobster test nightly on --at 02:00")
|
|
152
|
+
stray = _pr_flags(args)
|
|
153
|
+
if stray:
|
|
154
|
+
return _usage(f"{stray} goes with pr: mobster test pr {stray} …")
|
|
155
|
+
if paths and paths[0] == HEAL:
|
|
156
|
+
return heal(args, paths[1:])
|
|
157
|
+
if getattr(args, "accept", False) or getattr(args, "reject", False):
|
|
158
|
+
return _usage("--accept and --reject go with heal: mobster test heal --accept CHECK")
|
|
159
|
+
if getattr(args, "fresh", False) and (getattr(args, "no_heal", False) or getattr(args, "replay", False)):
|
|
160
|
+
return _usage("--fresh records the path again, so it doesn't go with --replay or --no-heal")
|
|
161
|
+
if getattr(args, "keyless", False) and (getattr(args, "fresh", False) or getattr(args, "no_heal", False)):
|
|
162
|
+
return _usage("--keyless never runs Smart, so --fresh and --no-heal don't apply")
|
|
163
|
+
if getattr(args, "record_run", None) or getattr(args, "record_name", None) or getattr(args, "record_output", None):
|
|
164
|
+
return _usage("--run, --name and -o go with record: mobster test record --run RUN_ID")
|
|
165
|
+
if getattr(args, "repro_arg", None) or getattr(args, "link", None):
|
|
166
|
+
return _usage("--arg and --link go with repro: mobster test repro CRASH_FILE --link myapp://cart")
|
|
167
|
+
stray = next((flag for flag, name in (("--bundle", "bundle"), ("--app", "app"),
|
|
168
|
+
("--skip-unsupported", "skip_unsupported"), ("--dry-run", "dry_run"),
|
|
169
|
+
("--force", "force"))
|
|
170
|
+
if getattr(args, name, None)), None)
|
|
171
|
+
if stray:
|
|
172
|
+
return _usage(f"{stray} goes with import: mobster test import maestro PATH {stray}")
|
|
173
|
+
if getattr(args, "add_list", None) or getattr(args, "add_set", None):
|
|
174
|
+
return _usage("--list and --set go with add: mobster test add RECIPE")
|
|
175
|
+
with signals_as_interrupt():
|
|
176
|
+
try:
|
|
177
|
+
return suite(args, paths)
|
|
178
|
+
except KeyboardInterrupt:
|
|
179
|
+
print("mobster test: stopped", file=sys.stderr, flush=True)
|
|
180
|
+
return 130
|
|
181
|
+
except Exception as error: # a last resort: never a traceback, never "failed" (exit 1)
|
|
182
|
+
print(f"mobster test: unexpected error ({type(error).__name__}: {error}). Report it at "
|
|
183
|
+
"github.com/RadishSoftware/mobster/issues with the command you ran.", file=sys.stderr, flush=True)
|
|
184
|
+
return USAGE_EXIT
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _command(args):
|
|
188
|
+
return "mobster test " + " ".join(shlex.quote(item) for item in sys.argv[2:]) if len(sys.argv) > 2 and \
|
|
189
|
+
sys.argv[1] == "test" else "mobster test"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def suite(args, paths, *, execute=None, plan=None, out=None, err=None):
|
|
193
|
+
from .api import Options, UsageError, parse_locales
|
|
194
|
+
out, err = out or sys.stdout, err or sys.stderr
|
|
195
|
+
options = Options(paths=tuple(paths), sims=tuple(args.sim or ()), devices=tuple(args.device or ()),
|
|
196
|
+
matrix=args.matrix, parallel=args.parallel, retries=args.retries, repeat=args.repeat,
|
|
197
|
+
quarantine=args.quarantine, tags=tuple(args.tag or ()), shard=args.shard, junit=args.junit,
|
|
198
|
+
html=args.html, strict=args.strict, keyless=args.keyless, video=args.video,
|
|
199
|
+
assert_timeout=args.assert_timeout, command=_command(args),
|
|
200
|
+
testflight=tuple(getattr(args, "testflight", None) or ()),
|
|
201
|
+
audit=tuple(getattr(args, "audit", None) or ()), a11y=getattr(args, "a11y", None),
|
|
202
|
+
android=getattr(args, "android", None), **snapshots_cli.options(args),
|
|
203
|
+
**{flag: bool(getattr(args, flag, False)) for flag in (
|
|
204
|
+
"fresh", "no_heal", "update_a11y_baseline", "spread", "auto_quarantine", "xctest")},
|
|
205
|
+
xctest_scheme=getattr(args, "scheme", None), xctest_project=getattr(args, "project", None),
|
|
206
|
+
xctest_plan=getattr(args, "test_plan", None))
|
|
207
|
+
if _only_testing(args, options):
|
|
208
|
+
return USAGE_EXIT
|
|
209
|
+
try:
|
|
210
|
+
options.locales = parse_locales(getattr(args, "locale", None))
|
|
211
|
+
except UsageError as error:
|
|
212
|
+
return _usage(str(error))
|
|
213
|
+
if options.testflight and not options.devices:
|
|
214
|
+
return _usage("--testflight goes with --device: a TestFlight build is on an iPhone, not a simulator")
|
|
215
|
+
if plan is None:
|
|
216
|
+
from .api import plan
|
|
217
|
+
if execute is None:
|
|
218
|
+
from .api import execute
|
|
219
|
+
try:
|
|
220
|
+
planned = plan(options)
|
|
221
|
+
except UsageError as error:
|
|
222
|
+
return _usage(str(error))
|
|
223
|
+
try:
|
|
224
|
+
from ..devtools import ProgressLines
|
|
225
|
+
progress = ProgressLines(err)
|
|
226
|
+
except ImportError:
|
|
227
|
+
def progress(line):
|
|
228
|
+
print(line, file=err, flush=True)
|
|
229
|
+
for note in planned.notes:
|
|
230
|
+
progress(note)
|
|
231
|
+
runs = len(planned.entries) * len(planned.targets) * options.repeat
|
|
232
|
+
if not runs and not options.xctest:
|
|
233
|
+
print("No checks to run.", file=out, flush=True)
|
|
234
|
+
return 0
|
|
235
|
+
progress(f"mobster test: {len(planned.entries)} check{'' if len(planned.entries) == 1 else 's'} on "
|
|
236
|
+
f"{_targets(planned.targets)}" + (f", {options.repeat} times each" if options.repeat > 1 else "")
|
|
237
|
+
+ (", each on whichever is free" if options.spread else "")
|
|
238
|
+
+ (f", {options.parallel} at a time" if options.parallel > 1 else "")
|
|
239
|
+
+ (", then your XCTest targets" if options.xctest else ""))
|
|
240
|
+
try:
|
|
241
|
+
doc, report_paths = execute(planned, progress=progress)
|
|
242
|
+
finally:
|
|
243
|
+
getattr(progress, "end", lambda: None)()
|
|
244
|
+
shared = None
|
|
245
|
+
if getattr(args, "share", False) and report_paths.get("folder"):
|
|
246
|
+
shared = _share(report_paths["folder"], args.share_days, err)
|
|
247
|
+
_tell_slack(planned.root, report_paths, doc, shared, err)
|
|
248
|
+
if args.json:
|
|
249
|
+
try:
|
|
250
|
+
print(json.dumps(doc, ensure_ascii=False, allow_nan=False), file=out, flush=True)
|
|
251
|
+
except BrokenPipeError:
|
|
252
|
+
os.dup2(os.open(os.devnull, os.O_WRONLY), out.fileno())
|
|
253
|
+
else:
|
|
254
|
+
from ..style import color_enabled
|
|
255
|
+
print(summary_text(doc, report_paths, color=color_enabled(out)), file=out, flush=True)
|
|
256
|
+
return int(doc.get("exitCode", USAGE_EXIT))
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _share(folder, days, err):
|
|
260
|
+
"""Upload the suite's report, sealed (teams/report_share.py), and say its link; or say why not."""
|
|
261
|
+
try:
|
|
262
|
+
from ..teams import report_share
|
|
263
|
+
except ModuleNotFoundError as error:
|
|
264
|
+
if error.name != "mobile_agent.teams":
|
|
265
|
+
raise
|
|
266
|
+
print(f"mobster test: sharing reports needs the Mobster app's team extension. Your report is in {folder}.",
|
|
267
|
+
file=err, flush=True)
|
|
268
|
+
return None
|
|
269
|
+
try:
|
|
270
|
+
shared = report_share.share_folder(folder, days=days)
|
|
271
|
+
except report_share.ShareRefused as refused:
|
|
272
|
+
print(f"mobster test: {refused}", file=err, flush=True)
|
|
273
|
+
return None
|
|
274
|
+
except (OSError, ValueError):
|
|
275
|
+
# The error could name a file, and a file's path names a check or a task: say what happened, not the path.
|
|
276
|
+
print(f"mobster test: the report wasn't shared. Your report is in {folder}.", file=err, flush=True)
|
|
277
|
+
return None
|
|
278
|
+
print(f"Shared with your team for {days} days: {shared.link}", file=err, flush=True)
|
|
279
|
+
return shared
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _targets(targets):
|
|
283
|
+
if len(targets) == 1:
|
|
284
|
+
return targets[0].label if targets[0].kind != "default" else "each check's simulator"
|
|
285
|
+
return f"{len(targets)} devices"
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _relative(path, root=None):
|
|
289
|
+
if not path:
|
|
290
|
+
return path
|
|
291
|
+
try:
|
|
292
|
+
return str(Path(path).resolve().relative_to(Path(root or Path.cwd()).resolve()))
|
|
293
|
+
except ValueError:
|
|
294
|
+
return str(path)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def schedule(args, rest, *, cwd=None, out=None, err=None, schedules=None):
|
|
298
|
+
"""`mobster test schedule [list | run-now NAME | device NAME DEVICE|none]`: the scheduled health checks."""
|
|
299
|
+
import time
|
|
300
|
+
|
|
301
|
+
from ..verify.checks import project_root
|
|
302
|
+
from . import health
|
|
303
|
+
out, err = out or sys.stdout, err or sys.stderr
|
|
304
|
+
cwd = Path(cwd or Path.cwd())
|
|
305
|
+
root = project_root(cwd)
|
|
306
|
+
verb = rest[0] if rest else "list"
|
|
307
|
+
if verb == "list":
|
|
308
|
+
found = health.listing(root, round(time.time() * 1000))
|
|
309
|
+
if args.json:
|
|
310
|
+
print(json.dumps(found, ensure_ascii=False, allow_nan=False), file=out, flush=True)
|
|
311
|
+
return 0
|
|
312
|
+
for problem in found["problems"]:
|
|
313
|
+
print(f"mobster test: {problem}", file=err, flush=True)
|
|
314
|
+
if not found["schedules"]:
|
|
315
|
+
print("No scheduled checks. Add `schedule: every 1h` to a check, or list schedules in "
|
|
316
|
+
".mobster/schedule.yaml.", file=out, flush=True)
|
|
317
|
+
return 0
|
|
318
|
+
for row in found["schedules"]:
|
|
319
|
+
last = row["last"]
|
|
320
|
+
ended = "never ran" if last is None else f"last {last['outcome']}" + (
|
|
321
|
+
f" {last['verdict']}" if last["verdict"] else "") + (f" ({last['reason']})" if last["reason"] else "")
|
|
322
|
+
target = f"phone {row['device']}" if row["device"] else row["sim"] or "each check's simulator"
|
|
323
|
+
nxt = time.strftime("%a %H:%M", time.localtime(row["nextAt"] / 1000)) if row["nextAt"] else "-"
|
|
324
|
+
nxt = f"next {nxt}" if row["on"] else "off on this Mac"
|
|
325
|
+
print(f"{row['name']:<24} {row['when']:<14} {nxt} on {target} {ended}", file=out)
|
|
326
|
+
print(file=out, flush=True)
|
|
327
|
+
return 0
|
|
328
|
+
if verb in ("run-now", "device") and len(rest) < 2:
|
|
329
|
+
return _usage(f"{verb} needs the schedule's name: mobster test schedule {verb} NAME")
|
|
330
|
+
specs, problems = health.load(root)
|
|
331
|
+
spec = next((item for item in specs if len(rest) > 1 and item.name == rest[1]), None)
|
|
332
|
+
if verb not in ("run-now", "device"):
|
|
333
|
+
return _usage(f"schedule takes list, run-now NAME or device NAME DEVICE, not {verb}")
|
|
334
|
+
if spec is None:
|
|
335
|
+
known = ", ".join(item.name for item in specs) or "none"
|
|
336
|
+
return _usage(f"no schedule named {rest[1]!r} (known: {known})")
|
|
337
|
+
if verb == "device":
|
|
338
|
+
if len(rest) < 3:
|
|
339
|
+
return _usage("device needs a device, or none: mobster test schedule device NAME \"Sam's iPhone\"")
|
|
340
|
+
health.set_device(root, spec.name, None if rest[2] == "none" else rest[2])
|
|
341
|
+
print(f"{spec.name} " + ("runs on its simulator again." if rest[2] == "none" else
|
|
342
|
+
f"runs on {rest[2]} while Mobster is running."), file=out, flush=True)
|
|
343
|
+
return 0
|
|
344
|
+
# a phone only by --device: its plan gate runs first
|
|
345
|
+
runner = schedules or health.TestSchedules(devices=lambda: {})
|
|
346
|
+
device = (args.device or [None])[0]
|
|
347
|
+
sim = (args.sim or [None])[0]
|
|
348
|
+
answer = runner.start(spec, trigger="run-now", device=device, sim=sim, wait=True,
|
|
349
|
+
progress=lambda line: print(line, file=err, flush=True))
|
|
350
|
+
if answer["outcome"] == "skipped":
|
|
351
|
+
print(f"mobster test: {spec.name} was skipped. {answer['reason']}", file=err, flush=True)
|
|
352
|
+
return USAGE_EXIT
|
|
353
|
+
print(f"{spec.name}: {answer['verdict'].replace('_', ' ')} (suite {answer['suite']})", file=out, flush=True)
|
|
354
|
+
return answer["exitCode"] or 0
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def history(args, paths, *, out=None):
|
|
358
|
+
"""`mobster test --history [PATH]`: the project's per-check history, flaky first. Runs nothing."""
|
|
359
|
+
from ..verify import perf
|
|
360
|
+
from ..verify.checks import project_root
|
|
361
|
+
from .history import report
|
|
362
|
+
out = out or sys.stdout
|
|
363
|
+
start = Path(paths[0]) if paths else Path.cwd()
|
|
364
|
+
root = project_root(start if start.exists() else Path.cwd())
|
|
365
|
+
found = report(root)
|
|
366
|
+
if args.json:
|
|
367
|
+
print(json.dumps(found, ensure_ascii=False, allow_nan=False), file=out, flush=True)
|
|
368
|
+
return 0
|
|
369
|
+
if not found["ok"]:
|
|
370
|
+
print("mobster test: the history file couldn't be read, so it starts again with the next run. Delete "
|
|
371
|
+
".mobster/test-results/history.json.corrupt when you no longer need it.", file=sys.stderr, flush=True)
|
|
372
|
+
if not found["checks"]:
|
|
373
|
+
print("No test history yet. Run `mobster test` and each check's results are kept here.", file=out, flush=True)
|
|
374
|
+
return 0
|
|
375
|
+
marks = {"passed": "✓", "failed": "✗", "needs_review": "!"}
|
|
376
|
+
for row in found["checks"]:
|
|
377
|
+
trend = f", {row['trend']}" if row["trend"] and row["trend"] != "steady" else ""
|
|
378
|
+
line = (f"{row['state']:<7} {row['name']} on {dots(row['device'])} "
|
|
379
|
+
f"{''.join(marks[o] for o in row['outcomes'])} {round((row['passRate'] or 0) * 100)}% of "
|
|
380
|
+
f"{row['runs']} runs, flake score {row['flakeScore']:.2f}{trend}{perf.last_run(row)}")
|
|
381
|
+
print(line, file=out)
|
|
382
|
+
if row["suggestQuarantine"]:
|
|
383
|
+
print(f" Flaky: add {row['file']} to .mobster/quarantine.yaml, or run with --auto-quarantine.", file=out)
|
|
384
|
+
print(file=out, flush=True)
|
|
385
|
+
return 0
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def summary_text(doc, paths, color=False):
|
|
389
|
+
"""The human summary: one line per check and device that didn't pass cleanly, the counts, the reports."""
|
|
390
|
+
from .reports import counts_text, duration_text
|
|
391
|
+
words = {"passed": "✓ passed", "failed": "✗ failed", "needs_review": "! needs review",
|
|
392
|
+
"couldnt_run": "✗ couldn't run", "flaky": "~ flaky"}
|
|
393
|
+
|
|
394
|
+
def paint(text, tone):
|
|
395
|
+
return _paint(color)(text, TONES[tone], "bold")
|
|
396
|
+
|
|
397
|
+
lines = []
|
|
398
|
+
for check in doc.get("checks") or ():
|
|
399
|
+
for result in check.get("results") or ():
|
|
400
|
+
status = result.get("status")
|
|
401
|
+
tone = "flaky" if status == "passed" and result.get("flaky") else status
|
|
402
|
+
line = f"{paint(words.get(tone, status), tone)} {check.get('name')} on {dots(result.get('device') or '')}"
|
|
403
|
+
if result.get("passRate") is not None:
|
|
404
|
+
line += f" ({round(result['passRate'] * 100)}% of {doc.get('options', {}).get('repeat')} runs)"
|
|
405
|
+
elif (result.get("attempts") or 0) > 1:
|
|
406
|
+
line += f" ({result['attempts']} attempts)"
|
|
407
|
+
if check.get("quarantined"):
|
|
408
|
+
line += " [quarantined]"
|
|
409
|
+
lines.append(line)
|
|
410
|
+
reason = result.get("reason") or (result.get("flakyReason") if result.get("flaky") else None) or {}
|
|
411
|
+
if status != "passed" or result.get("flaky"):
|
|
412
|
+
for text in (reason.get("message"), reason.get("fix")):
|
|
413
|
+
if text:
|
|
414
|
+
lines.append(f" {text}")
|
|
415
|
+
for item in result.get("healed") or ():
|
|
416
|
+
if isinstance(item, dict):
|
|
417
|
+
lines.append(f" healed step {item.get('step')}: {item.get('from')} → {item.get('to')}")
|
|
418
|
+
lines += xctest.text_lines(doc, paint)
|
|
419
|
+
for item in (doc.get("history") or {}).get("suggestQuarantine") or ():
|
|
420
|
+
lines.append(f"~ {item['check']} on {dots(item['device'] or '')} is flaky over its last 20 runs. Quarantine "
|
|
421
|
+
f"it: add {item['file']} to .mobster/quarantine.yaml.")
|
|
422
|
+
summary = doc.get("summary") or {}
|
|
423
|
+
head = f"{counts_text(summary)} in {duration_text(doc.get('durationMs'))}"
|
|
424
|
+
if summary.get("costUsd"):
|
|
425
|
+
head += f", ${summary['costUsd']:.2f}"
|
|
426
|
+
if summary.get("replayed"):
|
|
427
|
+
head += f", {summary['replayed']} replayed with no model"
|
|
428
|
+
if summary.get("linkFirst"):
|
|
429
|
+
head += (f", {summary['linkFirst']['steps']} link-first, "
|
|
430
|
+
f"{summary['linkFirst']['modelCallsSaved']} model calls saved")
|
|
431
|
+
lines += ["", head]
|
|
432
|
+
if summary.get("healed"):
|
|
433
|
+
lines.append(f"{summary['healed']} healed step{'' if summary['healed'] == 1 else 's'} wait for review: "
|
|
434
|
+
"mobster test heal")
|
|
435
|
+
from .a11y_baseline import counts as a11y_counts
|
|
436
|
+
found = a11y_counts(doc)
|
|
437
|
+
if any(found.values()):
|
|
438
|
+
lines.append(f"Accessibility: {found['new']} new, {found['warnings']} warning"
|
|
439
|
+
f"{'' if found['warnings'] == 1 else 's'}, {found['known']} in the baseline")
|
|
440
|
+
if doc.get("a11yBaseline") is not None:
|
|
441
|
+
lines.append(f"Wrote .mobster/a11y-baseline.json with {doc['a11yBaseline']} finding"
|
|
442
|
+
f"{'' if doc['a11yBaseline'] == 1 else 's'}.")
|
|
443
|
+
if doc.get("stopped"):
|
|
444
|
+
lines.append("Stopped before every check ran.")
|
|
445
|
+
if paths.get("html"):
|
|
446
|
+
lines.append(f"Report {_relative(paths.get('htmlCopy') or paths['html'])}")
|
|
447
|
+
if paths.get("junitCopy") or paths.get("junit"):
|
|
448
|
+
lines.append(f"JUnit {_relative(paths.get('junitCopy') or paths['junit'])}")
|
|
449
|
+
return "\n".join(lines)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _tell_slack(root, paths, doc, shared, err):
|
|
453
|
+
"""Post the suite to Slack when the project's rules ask (notify.py), and say what happened in one line."""
|
|
454
|
+
if not paths.get("folder"):
|
|
455
|
+
return
|
|
456
|
+
from .notify import notify
|
|
457
|
+
line = notify(root, paths["folder"], doc, link=getattr(shared, "link", None))
|
|
458
|
+
if line:
|
|
459
|
+
print(line, file=err, flush=True)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def pr(args, rest, *, run=None, out=None, err=None, cwd=None):
|
|
463
|
+
"""`mobster test pr [--base REF] [options]`: the checks the branch's changes need, built, run and reported."""
|
|
464
|
+
from .api import Options, UsageError
|
|
465
|
+
from .pr import PrError
|
|
466
|
+
from .prrun import PrOptions
|
|
467
|
+
out, err = out or sys.stdout, err or sys.stderr
|
|
468
|
+
if rest:
|
|
469
|
+
return _usage(f"pr takes no paths ({' '.join(rest)}): it picks the checks from the diff")
|
|
470
|
+
if getattr(args, "record_run", None) or getattr(args, "record_name", None) or \
|
|
471
|
+
getattr(args, "record_output", None):
|
|
472
|
+
return _usage("--run, --name and -o go with record: mobster test record --run RUN_ID")
|
|
473
|
+
for flag, name in (("--matrix", "matrix"), ("--shard", "shard"), ("--tag", "tag")):
|
|
474
|
+
if getattr(args, name, None):
|
|
475
|
+
return _usage(f"{flag} doesn't go with pr: pr picks the checks from the diff")
|
|
476
|
+
options = Options(sims=tuple(args.sim or ()), devices=tuple(args.device or ()), parallel=args.parallel,
|
|
477
|
+
retries=args.retries, repeat=args.repeat, quarantine=args.quarantine, junit=args.junit,
|
|
478
|
+
html=args.html, strict=args.strict, keyless=args.keyless, video=args.video,
|
|
479
|
+
assert_timeout=args.assert_timeout, command=_command(args),
|
|
480
|
+
testflight=tuple(getattr(args, "testflight", None) or ()),
|
|
481
|
+
fresh=bool(getattr(args, "fresh", False)), no_heal=bool(getattr(args, "no_heal", False)),
|
|
482
|
+
auto_quarantine=bool(getattr(args, "auto_quarantine", False)),
|
|
483
|
+
spread=bool(getattr(args, "spread", False)),
|
|
484
|
+
xctest=bool(getattr(args, "xctest", False)), xctest_scheme=args.scheme,
|
|
485
|
+
xctest_project=args.project, xctest_plan=getattr(args, "test_plan", None))
|
|
486
|
+
if _only_testing(args, options):
|
|
487
|
+
return USAGE_EXIT
|
|
488
|
+
pr_options = PrOptions(base=args.base, build=not args.no_build, scheme=args.scheme, project=args.project,
|
|
489
|
+
drafts=args.drafts or "run", gifs=not args.no_gifs, tested_with=args.tested_with,
|
|
490
|
+
github=getattr(args, "github", None) or "check")
|
|
491
|
+
if run is None:
|
|
492
|
+
from .prrun import test_pr as run
|
|
493
|
+
from ..verify.checks import project_root
|
|
494
|
+
root = project_root(Path(cwd or Path.cwd()))
|
|
495
|
+
try:
|
|
496
|
+
from ..devtools import ProgressLines
|
|
497
|
+
progress = ProgressLines(err)
|
|
498
|
+
except ImportError:
|
|
499
|
+
def progress(line):
|
|
500
|
+
print(line, file=err, flush=True)
|
|
501
|
+
try:
|
|
502
|
+
result = run(pr_options, options, root=root, progress=progress)
|
|
503
|
+
except (PrError, UsageError) as error:
|
|
504
|
+
return _usage(str(error))
|
|
505
|
+
finally:
|
|
506
|
+
getattr(progress, "end", lambda: None)()
|
|
507
|
+
shared = None
|
|
508
|
+
if getattr(args, "share", False) and result.paths.get("folder"):
|
|
509
|
+
shared = _share(result.paths["folder"], args.share_days, err)
|
|
510
|
+
if shared is not None:
|
|
511
|
+
from ..paths import write_text_atomic
|
|
512
|
+
from .pr_report import comment, hosted_pages, write
|
|
513
|
+
try:
|
|
514
|
+
# A write that fails leaves the comment linking the local files, and the exit code as it was.
|
|
515
|
+
write({**result.pr, "hostedReport": shared.link}, result.paths["folder"])
|
|
516
|
+
write_text_atomic(Path(result.paths["comment"]), comment(result.pr, artifact_url=shared.link,
|
|
517
|
+
file_url=hosted_pages(shared.link)))
|
|
518
|
+
result.pr["hostedReport"] = shared.link
|
|
519
|
+
except (OSError, ValueError):
|
|
520
|
+
print("mobster test: the report is shared, but its link couldn't be written into the comment.",
|
|
521
|
+
file=err, flush=True)
|
|
522
|
+
_tell_slack(root, result.paths, result.doc, shared, err)
|
|
523
|
+
if args.comment:
|
|
524
|
+
target = Path(args.comment).expanduser()
|
|
525
|
+
try:
|
|
526
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
527
|
+
target.write_text(Path(result.paths["comment"]).read_text(encoding="utf-8"), encoding="utf-8")
|
|
528
|
+
except OSError as error:
|
|
529
|
+
return _usage(f"--comment {args.comment}: can't write it ({error.strerror or type(error).__name__})")
|
|
530
|
+
if args.json:
|
|
531
|
+
print(json.dumps(result.pr, ensure_ascii=False), file=out, flush=True)
|
|
532
|
+
else:
|
|
533
|
+
from ..style import color_enabled
|
|
534
|
+
print(pr_text(result, color=color_enabled(out)), file=out, flush=True)
|
|
535
|
+
return result.exit_code
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def pr_text(result, color=False):
|
|
539
|
+
"""The human summary of `mobster test pr`: the changed screens, a line per check, the drafts, the verdict and where
|
|
540
|
+
the comment and GIFs are."""
|
|
541
|
+
pr = result.pr
|
|
542
|
+
tones = TONES
|
|
543
|
+
words = {"passed": "✓ passed", "failed": "✗ failed", "needs_review": "! needs review",
|
|
544
|
+
"couldnt_run": "✗ couldn't run", "draft": "~ draft"}
|
|
545
|
+
|
|
546
|
+
def paint(text, tone):
|
|
547
|
+
return _paint(color)(text, tones[tone], "bold")
|
|
548
|
+
|
|
549
|
+
lines = []
|
|
550
|
+
screens = [screen.get("name") for screen in (pr.get("plan") or {}).get("screens") or ()]
|
|
551
|
+
if screens:
|
|
552
|
+
lines.append("Changed screens: " + ", ".join(screens[:8]) + (" …" if len(screens) > 8 else ""))
|
|
553
|
+
for row in pr.get("rows") or ():
|
|
554
|
+
status = row.get("status")
|
|
555
|
+
if row.get("draft"):
|
|
556
|
+
lines.append(f"{paint(words['draft'], 'draft')} {row.get('file')} on {dots(row.get('device') or '')}, "
|
|
557
|
+
f"{(status or '').replace('_', ' ')}")
|
|
558
|
+
continue
|
|
559
|
+
lines.append(f"{paint(words.get(status, status), status if status in tones else 'couldnt_run')} "
|
|
560
|
+
f"{row.get('check')} on {dots(row.get('device') or '')}")
|
|
561
|
+
if status != "passed":
|
|
562
|
+
lines.append(f" {row.get('expected')}: {row.get('found')}")
|
|
563
|
+
lines += xctest.text_lines(result.doc, paint)
|
|
564
|
+
lines += ["", pr.get("headline") or ""]
|
|
565
|
+
lines += list(pr.get("notes") or ())
|
|
566
|
+
folder = result.paths.get("folder")
|
|
567
|
+
if result.paths.get("comment"):
|
|
568
|
+
lines.append(f"Comment {_relative(result.paths['comment'])}")
|
|
569
|
+
gifs = [row.get("gif") for row in pr.get("rows") or () if row.get("gif")]
|
|
570
|
+
if gifs and folder:
|
|
571
|
+
lines.append(f"GIFs {_relative(Path(folder) / 'pr' / 'gifs')} ({len(gifs)})")
|
|
572
|
+
return "\n".join(lines).strip("\n")
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def add(args, rest, *, cwd=None, out=None, err=None):
|
|
576
|
+
from . import recipes
|
|
577
|
+
out, err = out or sys.stdout, err or sys.stderr
|
|
578
|
+
if getattr(args, "add_list", False):
|
|
579
|
+
if rest:
|
|
580
|
+
return _usage("--list takes no recipe")
|
|
581
|
+
width = max(len(name) for name in recipes.names())
|
|
582
|
+
for recipe in recipes.BUILTIN:
|
|
583
|
+
print(f"{recipe.name.ljust(width)} {recipe.summary}", file=out)
|
|
584
|
+
return 0
|
|
585
|
+
if len(rest) != 1:
|
|
586
|
+
return _usage("add takes one recipe: mobster test add RECIPE (mobster test add --list shows them)")
|
|
587
|
+
given = {}
|
|
588
|
+
for item in getattr(args, "add_set", None) or ():
|
|
589
|
+
key, equals, value = item.partition("=")
|
|
590
|
+
if not equals or not key.strip():
|
|
591
|
+
return _usage(f"--set {item}: write it as KEY=VALUE")
|
|
592
|
+
given[key.strip().replace("-", "_")] = value
|
|
593
|
+
if getattr(args, "bundle", None):
|
|
594
|
+
given["bundle"] = args.bundle
|
|
595
|
+
try:
|
|
596
|
+
recipe, data = recipes.build(rest[0], given)
|
|
597
|
+
except recipes.RecipeError as error:
|
|
598
|
+
return _usage(str(error))
|
|
599
|
+
from ..verify.checks import project_root
|
|
600
|
+
base = Path(cwd or os.getcwd())
|
|
601
|
+
target = project_root(base) / ".mobster" / "checks" / f"{recipe.name}.yaml"
|
|
602
|
+
if target.exists() and not getattr(args, "force", False):
|
|
603
|
+
return _usage(f"{_relative(target)} exists, so Mobster left it as it is: pass --force to replace it")
|
|
604
|
+
import yaml
|
|
605
|
+
text = (f"# A Mobster check from the {recipe.name} recipe. Run it with: mobster test {_relative(target)}\n"
|
|
606
|
+
+ yaml.safe_dump(data, sort_keys=False, allow_unicode=True, default_flow_style=False, width=100))
|
|
607
|
+
try:
|
|
608
|
+
write_text_atomic(target, text)
|
|
609
|
+
except OSError as error:
|
|
610
|
+
return _usage(f"{_relative(target)}: can't write it ({error.strerror or type(error).__name__})")
|
|
611
|
+
print(f"Saved {_relative(target)} from the {recipe.name} recipe. Run it with: mobster test "
|
|
612
|
+
f"{shlex.quote(_relative(target))}" + (f". {recipe.notes}" if recipe.notes else ""), file=err, flush=True)
|
|
613
|
+
return 0
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def record(args, rest, *, find=None, out=None, err=None):
|
|
617
|
+
"""`mobster test record --run RUN_ID [--name NAME] [-o PATH]`."""
|
|
618
|
+
from .record import RecordError, check_data, check_yaml, find_run
|
|
619
|
+
out, err = out or sys.stdout, err or sys.stderr
|
|
620
|
+
if rest:
|
|
621
|
+
return _usage(f"record takes no paths ({' '.join(rest)}); write the check where you like with -o PATH")
|
|
622
|
+
run_id = getattr(args, "record_run", None)
|
|
623
|
+
if not run_id:
|
|
624
|
+
return _usage("record needs the task: mobster test record --run RUN_ID (the Mobster app's task id)")
|
|
625
|
+
output = getattr(args, "record_output", None)
|
|
626
|
+
name = getattr(args, "record_name", None)
|
|
627
|
+
if name is not None and (not name.strip() or len(name) > LIMITS["name"]):
|
|
628
|
+
return _usage(f"--name is 1 to {LIMITS['name']} characters")
|
|
629
|
+
target = Path(output).expanduser() if output else None
|
|
630
|
+
shown = _relative(target) if target else ".mobster/checks/<name>.yaml"
|
|
631
|
+
try:
|
|
632
|
+
found = (find or find_run)(run_id)
|
|
633
|
+
data = check_data(found, name=name)
|
|
634
|
+
text = check_yaml(found, name=name, path=shown)
|
|
635
|
+
except RecordError as error:
|
|
636
|
+
return _usage(error.message)
|
|
637
|
+
if target is None:
|
|
638
|
+
print(text, end="", file=out, flush=True)
|
|
639
|
+
return 0
|
|
640
|
+
if target.suffix.lower() not in (".yaml", ".yml"):
|
|
641
|
+
return _usage(f"-o {output}: a check file ends in .yaml")
|
|
642
|
+
existed = target.exists()
|
|
643
|
+
try:
|
|
644
|
+
write_text_atomic(target, text)
|
|
645
|
+
except OSError as error:
|
|
646
|
+
return _usage(f"-o {output}: can't write it ({error.strerror or type(error).__name__})")
|
|
647
|
+
steps = len(data.get("steps") or ())
|
|
648
|
+
print(f"{'Replaced' if existed else 'Saved'} {_relative(target)}: {steps} step{'' if steps == 1 else 's'}, "
|
|
649
|
+
f"{len(data['expect'])} expectation{'' if len(data['expect']) == 1 else 's'}. Run it with: mobster test "
|
|
650
|
+
f"{shlex.quote(_relative(target))}", file=err, flush=True)
|
|
651
|
+
return 0
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def import_flows(args, rest, *, out=None, err=None, cwd=None):
|
|
655
|
+
"""`mobster test import maestro PATH… [-o DIR]`: Maestro flows as checks."""
|
|
656
|
+
from . import maestro
|
|
657
|
+
out, err = out or sys.stdout, err or sys.stderr
|
|
658
|
+
if not rest or rest[0] != "maestro":
|
|
659
|
+
return _usage("import needs the format: mobster test import maestro .maestro")
|
|
660
|
+
sources = rest[1:]
|
|
661
|
+
if not sources:
|
|
662
|
+
return _usage("import maestro needs the flows: mobster test import maestro .maestro")
|
|
663
|
+
cwd = Path(cwd or os.getcwd())
|
|
664
|
+
for flag, name in (("--run", "record_run"), ("--name", "record_name")):
|
|
665
|
+
if getattr(args, name, None):
|
|
666
|
+
return _usage(f"{flag} goes with record, not import")
|
|
667
|
+
bundle = getattr(args, "bundle", None)
|
|
668
|
+
if bundle:
|
|
669
|
+
from ..state import validate_bundle_id
|
|
670
|
+
try:
|
|
671
|
+
validate_bundle_id(bundle)
|
|
672
|
+
except ValueError:
|
|
673
|
+
return _usage(f"--bundle {bundle!r} is not a bundle ID such as com.example.app")
|
|
674
|
+
folder = Path(getattr(args, "record_output", None) or ".mobster/checks").expanduser()
|
|
675
|
+
if not folder.is_absolute():
|
|
676
|
+
folder = cwd / folder
|
|
677
|
+
root = cwd
|
|
678
|
+
app = getattr(args, "app", None)
|
|
679
|
+
try:
|
|
680
|
+
results = maestro.convert_tree([Path(item) if Path(item).is_absolute() else cwd / item for item in sources],
|
|
681
|
+
bundle=bundle, app_path=app,
|
|
682
|
+
skip_unsupported=getattr(args, "skip_unsupported", False))
|
|
683
|
+
except maestro.MaestroError as error:
|
|
684
|
+
return _usage(str(error))
|
|
685
|
+
dry, force = getattr(args, "dry_run", False), getattr(args, "force", False)
|
|
686
|
+
written, refused = _write_imports(results, folder, root, dry=dry, force=force)
|
|
687
|
+
if getattr(args, "json", False):
|
|
688
|
+
print(json.dumps({"flows": [item.to_dict() for item in results], "folder": _relative(folder, root)},
|
|
689
|
+
indent=2, ensure_ascii=False), file=out, flush=True)
|
|
690
|
+
else:
|
|
691
|
+
print(import_text(results, folder, root, dry=dry), file=out, flush=True)
|
|
692
|
+
return 1 if refused else 0
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _write_imports(results, folder, root, *, dry, force):
|
|
696
|
+
"""Write every imported check; a check that exists is kept unless ``force``. (checks written, flows refused)."""
|
|
697
|
+
from . import maestro
|
|
698
|
+
refused = sum(1 for item in results if item.outcome == "refused")
|
|
699
|
+
written = 0
|
|
700
|
+
for item in results:
|
|
701
|
+
if item.outcome != "imported":
|
|
702
|
+
continue
|
|
703
|
+
for stem, data in item.checks:
|
|
704
|
+
target = folder / f"{stem}.yaml"
|
|
705
|
+
text = maestro.render(data, item.source)
|
|
706
|
+
if target.exists() and not force:
|
|
707
|
+
if target.read_text(encoding="utf-8", errors="replace") == text:
|
|
708
|
+
item.same.append(stem) # importing twice gives the same file: nothing to replace or report
|
|
709
|
+
continue
|
|
710
|
+
item.notes.append(f"{_relative(target, root)} exists, so it was not replaced; use --force")
|
|
711
|
+
item.outcome = "kept"
|
|
712
|
+
continue
|
|
713
|
+
if dry:
|
|
714
|
+
continue
|
|
715
|
+
try:
|
|
716
|
+
write_text_atomic(target, text)
|
|
717
|
+
written += 1
|
|
718
|
+
except OSError as error:
|
|
719
|
+
item.outcome = "refused"
|
|
720
|
+
item.reason = f"can't write {_relative(target, root)} ({error.strerror or type(error).__name__})"
|
|
721
|
+
refused += 1
|
|
722
|
+
break
|
|
723
|
+
return written, refused
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def _import_table(results):
|
|
727
|
+
rows = [(item.source, item.converted, len(item.placeholders),
|
|
728
|
+
len(item.problems) if item.outcome == "refused" else 0) for item in results if item.outcome != "skipped"]
|
|
729
|
+
if not rows:
|
|
730
|
+
return []
|
|
731
|
+
rows.append(("Total", *(sum(row[n] for row in rows) for n in (1, 2, 3))))
|
|
732
|
+
width = max(len(row[0]) for row in rows + [("Flow",)])
|
|
733
|
+
head = ("Flow", "Converted", "Needs a first run", "Unsupported")
|
|
734
|
+
out = [f"{head[0]:<{width}} {head[1]:>9} {head[2]:>17} {head[3]:>11}"]
|
|
735
|
+
out += [f"{name:<{width}} {a:>9} {b:>17} {c:>11}" for name, a, b, c in rows]
|
|
736
|
+
return out
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def import_text(results, folder, root, *, dry=False):
|
|
740
|
+
"""What the import did, flow by flow: the checks written, what was refused and why, and what to run."""
|
|
741
|
+
lines = []
|
|
742
|
+
imported = [item for item in results if item.outcome in ("imported", "kept")]
|
|
743
|
+
refused = [item for item in results if item.outcome == "refused"]
|
|
744
|
+
checks = sum(len(item.checks) for item in imported)
|
|
745
|
+
where = _relative(folder, root)
|
|
746
|
+
verb = "Would import" if dry else "Imported"
|
|
747
|
+
lines.append(f"{verb} {len(imported)} of {len(results)} Maestro flow{'' if len(results) == 1 else 's'} "
|
|
748
|
+
f"({checks} check{'' if checks == 1 else 's'}) into {where}")
|
|
749
|
+
for item in results:
|
|
750
|
+
lines.append("")
|
|
751
|
+
lines.append(item.source)
|
|
752
|
+
if item.outcome == "skipped":
|
|
753
|
+
lines.append(f" skipped: {item.reason}")
|
|
754
|
+
elif item.outcome == "refused":
|
|
755
|
+
lines.append(f" refused: {item.reason}" if item.reason
|
|
756
|
+
else " refused: it has commands Mobster can't run, so no check was written")
|
|
757
|
+
for problem in item.problems[:IMPORT_SHOWN]:
|
|
758
|
+
lines.append(f" {problem.where}: {problem.why}")
|
|
759
|
+
if problem.instead:
|
|
760
|
+
lines.append(f" instead: {problem.instead}")
|
|
761
|
+
if len(item.problems) > IMPORT_SHOWN:
|
|
762
|
+
lines.append(f" and {len(item.problems) - IMPORT_SHOWN} more; fix these, then import again")
|
|
763
|
+
else:
|
|
764
|
+
for stem, data in item.checks:
|
|
765
|
+
steps, expects = len(data.get("steps") or ()), len(data["expect"])
|
|
766
|
+
verb = "unchanged" if stem in item.same else "would write" if dry else "wrote"
|
|
767
|
+
lines.append(f" {verb} {_relative(folder / (stem + '.yaml'), root)}: "
|
|
768
|
+
f"{steps} step{'' if steps == 1 else 's'}, "
|
|
769
|
+
f"{expects} expectation{'' if expects == 1 else 's'}")
|
|
770
|
+
for problem in item.placeholders:
|
|
771
|
+
lines.append(f" needs a first run, {problem.where}: {problem.why}")
|
|
772
|
+
for problem in item.dropped:
|
|
773
|
+
lines.append(f" dropped {problem.where}: {problem.why}")
|
|
774
|
+
if problem.instead:
|
|
775
|
+
lines.append(f" instead: {problem.instead}")
|
|
776
|
+
for note in item.notes:
|
|
777
|
+
lines.append(f" note: {note}")
|
|
778
|
+
lines.append("")
|
|
779
|
+
lines.extend(_import_table(results))
|
|
780
|
+
lines.append("")
|
|
781
|
+
if refused:
|
|
782
|
+
lines.append(f"{len(refused)} flow{'' if len(refused) == 1 else 's'} refused: Mobster won't write a check "
|
|
783
|
+
"that covers less than the flow does. Fix the commands named above, or add --skip-unsupported "
|
|
784
|
+
"to drop the ones that aren't assertions and list each.")
|
|
785
|
+
if checks and not dry:
|
|
786
|
+
lines.append(f"Run them with: mobster test {shlex.quote(where)}")
|
|
787
|
+
return "\n".join(lines)
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def heal(args, rest, *, cwd=None, out=None, err=None):
|
|
791
|
+
"""`mobster test heal [CHECK...] [--accept | --reject] [--json]`: list or settle the heals waiting for review."""
|
|
792
|
+
from ..replay_core import heal as heals
|
|
793
|
+
from ..replay_core.checks import cache_dir_for
|
|
794
|
+
from ..verify.checks import project_root
|
|
795
|
+
from .discover import DiscoveryError, discover
|
|
796
|
+
out, err = out or sys.stdout, err or sys.stderr
|
|
797
|
+
accept, reject = bool(getattr(args, "accept", False)), bool(getattr(args, "reject", False))
|
|
798
|
+
if accept and reject:
|
|
799
|
+
return _usage("heal takes --accept or --reject, not both")
|
|
800
|
+
cwd = Path(cwd or Path.cwd())
|
|
801
|
+
root = project_root(cwd / rest[0] if rest and (cwd / rest[0]).exists() else cwd)
|
|
802
|
+
try:
|
|
803
|
+
entries = discover(_heal_paths(rest, root, cwd), root, cwd=cwd)
|
|
804
|
+
except DiscoveryError as error:
|
|
805
|
+
return _usage(str(error))
|
|
806
|
+
cache = cache_dir_for(root / ".mobster" / "runs")
|
|
807
|
+
found = []
|
|
808
|
+
for entry in entries:
|
|
809
|
+
check_file = Path(entry.path)
|
|
810
|
+
try:
|
|
811
|
+
document = heals.read(check_file)
|
|
812
|
+
except heals.HealError as error:
|
|
813
|
+
print(f"mobster test heal: {error}", file=err, flush=True)
|
|
814
|
+
continue
|
|
815
|
+
if document is not None:
|
|
816
|
+
found.append((check_file, document))
|
|
817
|
+
if getattr(args, "json", False) and not (accept or reject):
|
|
818
|
+
print(json.dumps({"heals": [{"check": _relative(str(file)), "diff": _relative(str(heals.heal_file(file))),
|
|
819
|
+
**document} for file, document in found]}, ensure_ascii=False, default=str),
|
|
820
|
+
file=out, flush=True)
|
|
821
|
+
return 0
|
|
822
|
+
if not found:
|
|
823
|
+
print("No heals wait for review.", file=out, flush=True)
|
|
824
|
+
return 0
|
|
825
|
+
if accept or reject:
|
|
826
|
+
total = 0
|
|
827
|
+
for check_file, _ in found:
|
|
828
|
+
try:
|
|
829
|
+
count = (heals.accept if accept else heals.reject)(check_file, cache)
|
|
830
|
+
except heals.HealError as error:
|
|
831
|
+
print(f"mobster test heal: {_relative(str(check_file))}: {error}", file=err, flush=True)
|
|
832
|
+
continue
|
|
833
|
+
total += count
|
|
834
|
+
word = "Accepted" if accept else "Kept failing"
|
|
835
|
+
print(f"{word}: {_relative(str(check_file))} ({count} step{'' if count == 1 else 's'})", file=out,
|
|
836
|
+
flush=True)
|
|
837
|
+
if reject and total:
|
|
838
|
+
print("Those steps now fail the check when their screen changes again.", file=out, flush=True)
|
|
839
|
+
return 0
|
|
840
|
+
lines = []
|
|
841
|
+
for check_file, document in found:
|
|
842
|
+
app = document.get("app") or {}
|
|
843
|
+
where = ", ".join(filter(None, [f"run {document.get('run')}" if document.get("run") else None,
|
|
844
|
+
f"app {app.get('version')}" if isinstance(app, dict) and app.get("version")
|
|
845
|
+
else None, f"iOS {document.get('ios')}" if document.get("ios") else None]))
|
|
846
|
+
lines.append(f"{_relative(str(check_file))}" + (f" ({where})" if where else ""))
|
|
847
|
+
for item in document["heals"]:
|
|
848
|
+
if not isinstance(item, dict):
|
|
849
|
+
continue
|
|
850
|
+
lines.append(f" step {item.get('step')} {item.get('from')} → {item.get('to')}")
|
|
851
|
+
if item.get("why"):
|
|
852
|
+
lines.append(f" {item['why']}")
|
|
853
|
+
for name in ("before", "after"):
|
|
854
|
+
frame = (item.get("frames") or {}).get(name)
|
|
855
|
+
if frame:
|
|
856
|
+
lines.append(f" {name:<6} {frame}")
|
|
857
|
+
count = sum(len(document["heals"]) for _, document in found)
|
|
858
|
+
lines += ["", f"{count} heal{'' if count == 1 else 's'} wait{'s' if count == 1 else ''} for review. Accept: "
|
|
859
|
+
"mobster test heal --accept Keep failing: mobster test heal --reject"]
|
|
860
|
+
print("\n".join(lines), file=out, flush=True)
|
|
861
|
+
return 0
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def _heal_paths(rest, root, cwd):
|
|
865
|
+
"""Check paths for `heal`: as given (a name like "paywall" is .mobster/checks/paywall.yaml), else none (all)."""
|
|
866
|
+
out = []
|
|
867
|
+
for item in rest:
|
|
868
|
+
path = Path(item)
|
|
869
|
+
if not path.suffix and not (cwd / path).exists():
|
|
870
|
+
for suffix in (".yaml", ".yml", ".json"):
|
|
871
|
+
candidate = root / ".mobster" / "checks" / (item + suffix)
|
|
872
|
+
if candidate.exists():
|
|
873
|
+
path = candidate
|
|
874
|
+
break
|
|
875
|
+
out.append(str(path))
|
|
876
|
+
return out
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
NIGHTLY_FLAGS = (("sim", "--sim"), ("device", "--device"), ("tag", "--tag"), ("testflight", "--testflight"))
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
def _night_flags(args):
|
|
883
|
+
"""The `mobster test` flags `nightly on` keeps for each night's run."""
|
|
884
|
+
flags = []
|
|
885
|
+
for name, flag in NIGHTLY_FLAGS:
|
|
886
|
+
for value in getattr(args, name, None) or ():
|
|
887
|
+
flags += [flag, value]
|
|
888
|
+
if getattr(args, "parallel", 1) and args.parallel > 1:
|
|
889
|
+
flags += ["--parallel", str(args.parallel)]
|
|
890
|
+
if getattr(args, "retries", None) is not None:
|
|
891
|
+
flags += ["--retries", str(args.retries)]
|
|
892
|
+
for name, flag in (("keyless", "--keyless"), ("spread", "--spread")):
|
|
893
|
+
if getattr(args, name, False):
|
|
894
|
+
flags.append(flag)
|
|
895
|
+
for value in getattr(args, "audit", None) or ():
|
|
896
|
+
flags += ["--audit", value]
|
|
897
|
+
if getattr(args, "a11y", None):
|
|
898
|
+
flags.append(f"--a11y={args.a11y}")
|
|
899
|
+
return flags
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def nightly(args, rest, *, cwd=None, out=None, err=None):
|
|
903
|
+
"""`mobster test nightly on --at HH:MM [flags] | off | status | run [flags]`."""
|
|
904
|
+
from ..devkit import DevError
|
|
905
|
+
from ..devkit import nightly as night
|
|
906
|
+
from ..verify.checks import project_root
|
|
907
|
+
out, err = out or sys.stdout, err or sys.stderr
|
|
908
|
+
verb = rest[0] if rest else "status"
|
|
909
|
+
root = project_root(Path(cwd or Path.cwd()))
|
|
910
|
+
try:
|
|
911
|
+
if verb == "on":
|
|
912
|
+
if not getattr(args, "at", None):
|
|
913
|
+
return _usage("nightly on needs --at HH:MM, such as --at 02:00")
|
|
914
|
+
path = night.turn_on(root, args.at, _night_flags(args))
|
|
915
|
+
print(f"Every night at {args.at}, Mobster runs the checks of {root.name}"
|
|
916
|
+
+ (f" ({' '.join(_night_flags(args))})" if _night_flags(args) else "")
|
|
917
|
+
+ f". The morning report goes to {night.FOLDER}/<date>/morning.md, with a notification.\n"
|
|
918
|
+
f"Keep this Mac on power and signed in. Stop it with: mobster test nightly off\n({path})",
|
|
919
|
+
file=out, flush=True)
|
|
920
|
+
return 0
|
|
921
|
+
if verb == "off":
|
|
922
|
+
print("Turned the nightly run off." if night.turn_off(root) else "No nightly run is set up here.",
|
|
923
|
+
file=out, flush=True)
|
|
924
|
+
return 0
|
|
925
|
+
if verb == "run":
|
|
926
|
+
code, text = night.run_night(root, _night_flags(args))
|
|
927
|
+
print(text, file=out, flush=True)
|
|
928
|
+
return code
|
|
929
|
+
if verb == "status":
|
|
930
|
+
state = night.status(root)
|
|
931
|
+
if getattr(args, "json", False):
|
|
932
|
+
print(json.dumps(state), file=out, flush=True)
|
|
933
|
+
return 0
|
|
934
|
+
lines = [f"Every night at {state['at']}" + (f" ({' '.join(state['flags'])})" if state["flags"] else "")
|
|
935
|
+
if state["on"]
|
|
936
|
+
else "No nightly run is set up here. Turn it on: mobster test nightly on --at 02:00"]
|
|
937
|
+
if state["last"]:
|
|
938
|
+
lines.append(f"Last run {state['last'].get('date')}: {state['last'].get('headline')}. "
|
|
939
|
+
f"{state['last'].get('morning')}")
|
|
940
|
+
print("\n".join(lines), file=out, flush=True)
|
|
941
|
+
return 0
|
|
942
|
+
except DevError as error:
|
|
943
|
+
return _usage(error.line())
|
|
944
|
+
return _usage(f"nightly takes on, off, status or run, not {verb}")
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
__all__ = ["add_arguments", "run", "summary_text", "suite", "pr", "pr_text", "add", "record", "import_flows", "heal",
|
|
948
|
+
"nightly"]
|