feat: improve task execution, verification and packaging

Add execution incidents, action bursts and shared Flash history.

Expose verification and Explorer settings with streamed check results.

Support graceful cancellation, recording recovery and bundled UI resources.

Add CI quality checks and clean up comments and documentation.
This commit is contained in:
somew1nd committed 2026-09-02 21:45:51 -07:00
1 parent 5ccf06d677
commit 67248c887e
289 files changed
+15883 -6919

No files matched your search

+92
View File
@@ -0,0 +1,92 @@
"""Fail CI when exception-handler or type-ignore counts exceed the baseline.
Lower the committed baseline when removing existing debt.
"""
from __future__ import annotations
import argparse
import ast
import json
from pathlib import Path
import re
import sys
ROOT = Path(__file__).resolve().parents[1]
SOURCE_ROOTS = (ROOT / "artemis", ROOT / "mcp_server", ROOT / "apps" / "admin_console")
BASELINE_PATH = ROOT / ".quality-baseline.json"
def _exception_names(node: ast.expr | None) -> set[str]:
if node is None:
return {"bare-except"}
if isinstance(node, ast.Name):
return {node.id}
if isinstance(node, ast.Attribute):
return {node.attr}
if isinstance(node, ast.Tuple):
names: set[str] = set()
for child in node.elts:
names.update(_exception_names(child))
return names
return set()
def collect_metrics() -> dict[str, int]:
metrics = {
"broad_exception_handlers": 0,
"silent_broad_exception_handlers": 0,
"type_ignore_comments": 0,
}
for source_root in SOURCE_ROOTS:
for path in source_root.rglob("*.py"):
text = path.read_text(encoding="utf-8-sig")
metrics["type_ignore_comments"] += len(re.findall(r"#\s*type:\s*ignore\b", text))
try:
tree = ast.parse(text, filename=str(path))
except SyntaxError as exc:
raise RuntimeError(f"Cannot inspect invalid Python file {path}: {exc}") from exc
for node in ast.walk(tree):
if not isinstance(node, ast.ExceptHandler):
continue
names = _exception_names(node.type)
if not names.intersection({"Exception", "BaseException", "bare-except"}):
continue
metrics["broad_exception_handlers"] += 1
if node.body and all(isinstance(statement, ast.Pass) for statement in node.body):
metrics["silent_broad_exception_handlers"] += 1
return metrics
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--print-current", action="store_true", help="print metrics as JSON")
args = parser.parse_args()
current = collect_metrics()
if args.print_current:
print(json.dumps(current, indent=2, sort_keys=True))
return 0
baseline = json.loads(BASELINE_PATH.read_text(encoding="utf-8"))
regressions = {
name: (baseline[name], value)
for name, value in current.items()
if value > int(baseline[name])
}
if regressions:
for name, (allowed, actual) in regressions.items():
print(f"quality regression: {name} increased from {allowed} to {actual}")
return 1
print("quality ratchet passed")
for name, value in current.items():
improvement = int(baseline[name]) - value
suffix = f" ({improvement} below baseline)" if improvement else ""
print(f" {name}: {value}{suffix}")
return 0
if __name__ == "__main__":
sys.exit(main())
+11 -7
View File
@@ -78,9 +78,13 @@ def _element_value(element: dict[str, Any] | None) -> str:
def _compact(value: str) -> str:
return re.sub(r"[\s`*_\[\](){}:;,.,。:;]", "", value.lower()).replace(
"×", "x"
).replace("*", "x").replace("=", "=").replace("=", "=")
return (
re.sub(r"[\s`*_\[\](){}:;,.,。:;]", "", value.lower())
.replace("×", "x")
.replace("*", "x")
.replace("=", "=")
.replace("=", "=")
)
def _contains_any(value: str, candidates: tuple[str, ...]) -> bool:
@@ -182,7 +186,9 @@ def _assert_calculation(
)
async def _run_ui_sequence(controller, recording_started: float) -> tuple[list[GroundTruthEvent], dict[str, str]]:
async def _run_ui_sequence(
controller, recording_started: float
) -> tuple[list[GroundTruthEvent], dict[str, str]]:
device = controller.driver.device
await asyncio.to_thread(
device.shell,
@@ -268,9 +274,7 @@ async def _run_ui_sequence(controller, recording_started: float) -> tuple[list[G
)
)
locator_methods["history_close"] = await _tap(
controller, "history_toggle_button", settle=0.7
)
locator_methods["history_close"] = await _tap(controller, "history_toggle_button", settle=0.7)
formula, result, history_state, _ = await _observe(controller)
if not _contains_any(_compact(history_state), ("已关闭", "close", "off")):
raise AssertionError(f"History panel did not close; state={history_state!r}")
+103 -31
View File
@@ -33,8 +33,12 @@ def _resolve_devices() -> tuple[str, str]:
all_devs = device_pool.list_devices()
real_dev = next((d for d in all_devs if not d.is_emulator and d.state == "device"), None)
emu_dev = next((d for d in all_devs if d.is_emulator and d.state == "device"), None)
phone = os.environ.get("ARTEMIS_PHONE_SERIAL") or (real_dev.serial if real_dev else "63191FDKX00062")
emu = os.environ.get("ARTEMIS_EMULATOR_SERIAL") or (emu_dev.serial if emu_dev else "emulator-5554")
phone = os.environ.get("ARTEMIS_PHONE_SERIAL") or (
real_dev.serial if real_dev else "63191FDKX00062"
)
emu = os.environ.get("ARTEMIS_EMULATOR_SERIAL") or (
emu_dev.serial if emu_dev else "emulator-5554"
)
return phone, emu
@@ -68,12 +72,20 @@ async def run_part_1():
print("=" * 80)
print("[Part 1: Single Device Serial Queueing & Automatic Relay]")
print("=" * 80)
print("🎯 Goal: Launch 2 tasks on the same physical device; verify FIFO queueing, Web Active Queue count, and relay.")
print(
"🎯 Goal: Launch 2 tasks on the same physical device; verify FIFO queueing, Web Active Queue count, and relay."
)
print("📱 Target Device: Physical Pixel 11 Pro (63191FDKX00062)")
print("👀 Please observe:")
print(" 1. Web Console (http://127.0.0.1:8000): Active Queue becomes 2 (1 Running blue badge, 1 Pending yellow badge)")
print(" 2. Device Screen: Task 1A opens Settings first; upon completion, Task 1B automatically takes over to open Clock")
print(" 3. Web Console: After 1A finishes, 1B switches to Running, and queue eventually clears to zero")
print(
" 1. Web Console (http://127.0.0.1:8000): Active Queue becomes 2 (1 Running blue badge, 1 Pending yellow badge)"
)
print(
" 2. Device Screen: Task 1A opens Settings first; upon completion, Task 1B automatically takes over to open Clock"
)
print(
" 3. Web Console: After 1A finishes, 1B switches to Running, and queue eventually clears to zero"
)
print("-" * 80)
DeviceExecutionLock.cleanup_stale_locks()
@@ -81,14 +93,21 @@ async def run_part_1():
async def task_1a():
print("🚀 [Task 1A] Launching: Open Settings on physical device...")
res = await client.run("Use manage_app to launch Settings app and report task status completed", profile="flash")
res = await client.run(
"Use manage_app to launch Settings app and report task status completed",
profile="flash",
)
print(f"✅ [Task 1A] Completed: status={res.status}")
return res
async def task_1b():
await asyncio.sleep(0.4)
print("🚀 [Task 1B] Launching: Open Clock on physical device -> should immediately enter queue...")
res = await client.run("Use manage_app to launch Clock app and report task status completed", profile="flash")
print(
"🚀 [Task 1B] Launching: Open Clock on physical device -> should immediately enter queue..."
)
res = await client.run(
"Use manage_app to launch Clock app and report task status completed", profile="flash"
)
print(f"✅ [Task 1B] Completed: status={res.status}")
return res
@@ -99,19 +118,27 @@ async def run_part_1():
print_status_snapshot("Part 1 Running")
await asyncio.gather(t1, t2)
print("🎉 Part 1 Success: Task 1B automatically relayed and completed after Task 1A finished!\n")
print(
"🎉 Part 1 Success: Task 1B automatically relayed and completed after Task 1A finished!\n"
)
async def run_part_2():
print("=" * 80)
print("[Part 2: Multi-Device True Parallel Execution]")
print("=" * 80)
print("🎯 Goal: Cross-device true concurrency; both devices run independently at the same time without blocking.")
print(
"🎯 Goal: Cross-device true concurrency; both devices run independently at the same time without blocking."
)
print(f"📱 Target Device 1: Physical Pixel 11 Pro ({PHONE_SERIAL})")
print(f"💻 Target Device 2: Android Emulator ({EMULATOR_SERIAL})")
print("👀 Please observe:")
print(" 1. Dual screens: Physical device and emulator operate simultaneously without sequential waiting!")
print(" 2. Web Console (http://127.0.0.1:8000): Active Tasks shows 2 (both with blue Running badges)")
print(
" 1. Dual screens: Physical device and emulator operate simultaneously without sequential waiting!"
)
print(
" 2. Web Console (http://127.0.0.1:8000): Active Tasks shows 2 (both with blue Running badges)"
)
print(" 3. Queue count: Queue is 0, each device displays its own Device Serial")
print("-" * 80)
@@ -123,13 +150,19 @@ async def run_part_2():
async def p_task():
print(f"🚀 [Phone Task] Launching: Open Settings on {PHONE_SERIAL}...")
res = await client_phone.run("Use manage_app to launch Settings app and report task status completed", profile="flash")
res = await client_phone.run(
"Use manage_app to launch Settings app and report task status completed",
profile="flash",
)
print(f"✅ [Phone Task] Finished in {time.time() - start_t:.2f}s")
return res
async def e_task():
print(f"🚀 [Emulator Task] Launching: Open Settings on {EMULATOR_SERIAL}...")
res = await client_emu.run("Use manage_app to launch Settings app and report task status completed", profile="flash")
res = await client_emu.run(
"Use manage_app to launch Settings app and report task status completed",
profile="flash",
)
print(f"✅ [Emulator Task] Finished in {time.time() - start_t:.2f}s")
return res
@@ -140,19 +173,27 @@ async def run_part_2():
print_status_snapshot("Part 2 Running (Multi-Device Concurrency)")
await asyncio.gather(tp, te)
print(f"🎉 Part 2 Success: Both devices completed tasks concurrently in {time.time() - start_t:.2f}s!\n")
print(
f"🎉 Part 2 Success: Both devices completed tasks concurrently in {time.time() - start_t:.2f}s!\n"
)
async def run_part_3():
print("=" * 80)
print("[Part 3: Global Concurrency Limit Across Devices]")
print("=" * 80)
print("🎯 Goal: Configure global concurrency mode (global, limit 1); even targeting different devices, tasks must queue globally.")
print(
"🎯 Goal: Configure global concurrency mode (global, limit 1); even targeting different devices, tasks must queue globally."
)
print(f"📱 Device 1: Physical Pixel 11 Pro ({PHONE_SERIAL})")
print(f"💻 Device 2: Android Emulator ({EMULATOR_SERIAL})")
print("👀 Please observe:")
print(" 1. Emulator screen: Even though emulator is completely idle, it waits until the phone task completes!")
print(" 2. Web Console: Phone task is Running (blue badge), emulator task is Pending (yellow badge)!")
print(
" 1. Emulator screen: Even though emulator is completely idle, it waits until the phone task completes!"
)
print(
" 2. Web Console: Phone task is Running (blue badge), emulator task is Pending (yellow badge)!"
)
print(" 3. Once phone finishes, emulator instantly activates and begins execution!")
print("-" * 80)
@@ -162,14 +203,22 @@ async def run_part_3():
async def task_3a():
print(f"🚀 [Task 3A - Phone] Global mode starting: Executing on {PHONE_SERIAL}...")
res = await client_phone_g.run("Use manage_app to launch Settings app and report task status completed", profile="flash")
res = await client_phone_g.run(
"Use manage_app to launch Settings app and report task status completed",
profile="flash",
)
print("✅ [Task 3A - Phone] Finished")
return res
async def task_3b():
await asyncio.sleep(0.4)
print(f"🚀 [Task 3B - Emulator] Global mode starting: Targeting {EMULATOR_SERIAL} (should queue due to global limit)...")
res = await client_emu_g.run("Use manage_app to launch Settings app and report task status completed", profile="flash")
print(
f"🚀 [Task 3B - Emulator] Global mode starting: Targeting {EMULATOR_SERIAL} (should queue due to global limit)..."
)
res = await client_emu_g.run(
"Use manage_app to launch Settings app and report task status completed",
profile="flash",
)
print("✅ [Task 3B - Emulator] Finished")
return res
@@ -180,7 +229,9 @@ async def run_part_3():
print_status_snapshot("Part 3 Running (Global Limit Cross-Device Queueing)")
await asyncio.gather(t3a, t3b)
print("🎉 Part 3 Success: Global mutex accurately queued and relayed tasks across physical phone and emulator!\n")
print(
"🎉 Part 3 Success: Global mutex accurately queued and relayed tasks across physical phone and emulator!\n"
)
async def run_part_4():
@@ -188,10 +239,16 @@ async def run_part_4():
print("[Part 4: Multi-Device Asymmetric Mixed Queueing]")
print("=" * 80)
print("🎯 Goal: Assign 2 tasks to physical phone, 1 task to emulator.")
print(" Expected: Phone 4A and Emulator 4C run concurrently; Phone 4B queues in dedicated phone queue; 4B relays immediately on phone after 4A.")
print(
" Expected: Phone 4A and Emulator 4C run concurrently; Phone 4B queues in dedicated phone queue; 4B relays immediately on phone after 4A."
)
print("👀 Please observe:")
print(" 1. Web Console: Active Tasks = 2 (Phone 4A + Emulator 4C), Queue = 1 (Phone 4B), Total = 3!")
print(" 2. Screens: Phone and emulator operate concurrently first; Phone 4B executes on phone immediately after 4A finishes!")
print(
" 1. Web Console: Active Tasks = 2 (Phone 4A + Emulator 4C), Queue = 1 (Phone 4B), Total = 3!"
)
print(
" 2. Screens: Phone and emulator operate concurrently first; Phone 4B executes on phone immediately after 4A finishes!"
)
print("-" * 80)
DeviceExecutionLock.cleanup_stale_locks()
@@ -200,16 +257,24 @@ async def run_part_4():
async def task_4a():
print("🚀 [Task 4A - Phone] Started...")
return await client_phone.run("Use manage_app to launch Settings app and report task status completed", profile="flash")
return await client_phone.run(
"Use manage_app to launch Settings app and report task status completed",
profile="flash",
)
async def task_4b():
await asyncio.sleep(0.4)
print("🚀 [Task 4B - Phone] Started (Queued behind 4A)...")
return await client_phone.run("Use manage_app to launch Clock app and report task status completed", profile="flash")
return await client_phone.run(
"Use manage_app to launch Clock app and report task status completed", profile="flash"
)
async def task_4c():
print("🚀 [Task 4C - Emulator] Started concurrently with 4A...")
return await client_emu.run("Use manage_app to launch Settings app and report task status completed", profile="flash")
return await client_emu.run(
"Use manage_app to launch Settings app and report task status completed",
profile="flash",
)
t4a = asyncio.create_task(task_4a())
t4b = asyncio.create_task(task_4b())
@@ -219,13 +284,20 @@ async def run_part_4():
print_status_snapshot("Part 4 Running (Mixed Queueing)")
await asyncio.gather(t4a, t4b, t4c)
print("🎉 Part 4 Success: Cross-device concurrency and single-device queueing work together seamlessly!\n")
print(
"🎉 Part 4 Success: Cross-device concurrency and single-device queueing work together seamlessly!\n"
)
async def main():
global PHONE_SERIAL, EMULATOR_SERIAL
parser = argparse.ArgumentParser(description="Artemis Concurrency & Queue Edge-Cases Runner")
parser.add_argument("--part", choices=["1", "2", "3", "4", "all"], default="1", help="Which part to run (1, 2, 3, 4, or all)")
parser.add_argument(
"--part",
choices=["1", "2", "3", "4", "all"],
default="1",
help="Which part to run (1, 2, 3, 4, or all)",
)
parser.add_argument("--phone", default=None, help="Target physical phone serial number")
parser.add_argument("--emulator", default=None, help="Target Android emulator serial number")
args = parser.parse_args()
+12 -1
View File
@@ -202,7 +202,18 @@ def main() -> None:
if has_uv and (ROOT_DIR / "pyproject.toml").exists():
# Launch via `python -m artemis` (not the `artemis` console-script shim) so the
# long-running server never locks .venv/Scripts/artemis.exe against reinstalls.
cmd = ["uv", "run", "python", "-m", "artemis", "ui", "--port", str(args.port), "--host", args.host]
cmd = [
"uv",
"run",
"python",
"-m",
"artemis",
"ui",
"--port",
str(args.port),
"--host",
args.host,
]
if args.no_open:
cmd.append("--no-open")
if args.reload:
+17 -6
View File
@@ -31,8 +31,12 @@ def _resolve_devices() -> tuple[str, str]:
all_devs = device_pool.list_devices()
real_dev = next((d for d in all_devs if not d.is_emulator and d.state == "device"), None)
emu_dev = next((d for d in all_devs if d.is_emulator and d.state == "device"), None)
phone = os.environ.get("ARTEMIS_PHONE_SERIAL") or (real_dev.serial if real_dev else "63191FDKX00062")
emu = os.environ.get("ARTEMIS_EMULATOR_SERIAL") or (emu_dev.serial if emu_dev else "emulator-5554")
phone = os.environ.get("ARTEMIS_PHONE_SERIAL") or (
real_dev.serial if real_dev else "63191FDKX00062"
)
emu = os.environ.get("ARTEMIS_EMULATOR_SERIAL") or (
emu_dev.serial if emu_dev else "emulator-5554"
)
return phone, emu
@@ -58,7 +62,7 @@ def print_status_snapshot(stage_name: str):
print(f" - Device: {a.get('device_id')} | Goal: {a.get('goal')} (PID: {a.get('pid')})")
print(f" ⏳ Queued Tasks ({len(queue)}):")
for q in queue:
dev = q.get('device_serial') or q.get('device_id')
dev = q.get("device_serial") or q.get("device_id")
print(f" - Device: {dev} | Goal: {q.get('goal')} [status={q.get('status')}]")
print(f" 📊 Total in Active Queue (Running + Pending) = {len(active) + len(queue)}\n")
@@ -83,7 +87,9 @@ async def stage_1_single_device_queueing():
async def task_1b():
# Small delay so 1A is definitely acquired first
await asyncio.sleep(0.4)
print(f"🚀 [Task 1B] Launching on {PHONE_SERIAL}: Press HOME and open Clock (Should queue!)...")
print(
f"🚀 [Task 1B] Launching on {PHONE_SERIAL}: Press HOME and open Clock (Should queue!)..."
)
res = await client_phone.run("Press HOME key and open Clock", profile="flash")
print(f"✅ [Task 1B] Finished with status: {res.status}")
return res
@@ -161,7 +167,9 @@ async def stage_3_global_concurrency_mode():
async def task_emu_g():
await asyncio.sleep(0.4)
print(f"💻 [Task 3B - Emulator] Requesting run on {EMULATOR_SERIAL} under global mode (Should queue!)...")
print(
f"💻 [Task 3B - Emulator] Requesting run on {EMULATOR_SERIAL} under global mode (Should queue!)..."
)
res = await client_emu_global.run("Press HOME key and open Settings", profile="flash")
print("✅ [Task 3B - Emulator] Finished.")
return res
@@ -175,7 +183,9 @@ async def stage_3_global_concurrency_mode():
print(f"Active count: {len(active)}, Queue count: {len(queue)}")
await asyncio.gather(p_task, e_task)
print("✓ Stage 3 Complete: Global lock correctly serialized execution across physical phone and emulator!")
print(
"✓ Stage 3 Complete: Global lock correctly serialized execution across physical phone and emulator!"
)
async def stage_4_mixed_queue_and_parallel():
@@ -214,6 +224,7 @@ async def stage_4_mixed_queue_and_parallel():
async def main():
import argparse
global PHONE_SERIAL, EMULATOR_SERIAL
parser = argparse.ArgumentParser(description="ARTEMIS All Edge-Cases Comprehensive Test Suite")
+108 -35
View File
@@ -37,8 +37,12 @@ async def run_experiment_1_single_device_relay(real_serial: str):
print_header("[Experiment 1] Single-Device Task Queueing & Relay (FIFO Relay on Single Device)")
print(f"Target Device: {real_serial} (Physical Pixel 11 Pro)")
client = ArtemisClient(device_serial=real_serial, default_profile="flash", concurrency_mode="per_device")
print(f"Client Init: device_serial='{client.device_serial}', concurrency_mode='{client.concurrency_mode}'")
client = ArtemisClient(
device_serial=real_serial, default_profile="flash", concurrency_mode="per_device"
)
print(
f"Client Init: device_serial='{client.device_serial}', concurrency_mode='{client.concurrency_mode}'"
)
task_1_launched = 0.0
task_1_start = 0.0
@@ -68,8 +72,10 @@ async def run_experiment_1_single_device_relay(real_serial: str):
async def execute_task_2():
nonlocal task_2_launched, task_2_start, task_2_end
task_2_launched = time.time()
print(f"[{now_str()}] ▶ [Task 2] Submitted concurrently (detects device occupied by Task 1, entering FIFO queue...)")
print(
f"[{now_str()}] ▶ [Task 2] Submitted concurrently (detects device occupied by Task 1, entering FIFO queue...)"
)
task_2_start = time.time()
res = await client.run(
"Return to home screen and open Clock",
@@ -90,14 +96,20 @@ async def run_experiment_1_single_device_relay(real_serial: str):
res1, res2 = await asyncio.gather(t_coro_1, t_coro_2)
print("\n[Timing & Relay Analysis]:")
print(f" Task 1 runtime interval: [{task_1_start:.3f}s ~ {task_1_end:.3f}s] (elapsed {task_1_end - task_1_start:.3f}s)")
print(
f" Task 1 runtime interval: [{task_1_start:.3f}s ~ {task_1_end:.3f}s] (elapsed {task_1_end - task_1_start:.3f}s)"
)
print(f" Task 2 submission time: {task_2_launched:.3f}s")
print(f" Task 2 actual start: {task_2_start:.3f}s (waited for Task 1 lock release)")
print(f" Task 2 end time: {task_2_end:.3f}s (elapsed {task_2_end - task_2_start:.3f}s)")
assert task_2_end > task_1_end, "Task 2 did not complete after Task 1!"
assert task_2_end - task_1_start >= (task_1_end - task_1_start) + 0.1, "Tasks failed to execute in sequential queue!"
print(" ✅ Verification successful: Task 2 queued properly and relayed immediately after Task 1 released the lock!")
assert task_2_end - task_1_start >= (task_1_end - task_1_start) + 0.1, (
"Tasks failed to execute in sequential queue!"
)
print(
" ✅ Verification successful: Task 2 queued properly and relayed immediately after Task 1 released the lock!"
)
async def run_experiment_2_multi_device_parallel(real_serial: str, emu_serial: str):
@@ -106,8 +118,12 @@ async def run_experiment_2_multi_device_parallel(real_serial: str, emu_serial: s
print(f"Device A (Physical): {real_serial}")
print(f"Device B (Emulator): {emu_serial}")
client_a = ArtemisClient(device_serial=real_serial, default_profile="flash", concurrency_mode="per_device")
client_b = ArtemisClient(device_serial=emu_serial, default_profile="flash", concurrency_mode="per_device")
client_a = ArtemisClient(
device_serial=real_serial, default_profile="flash", concurrency_mode="per_device"
)
client_b = ArtemisClient(
device_serial=emu_serial, default_profile="flash", concurrency_mode="per_device"
)
a_start, a_end = 0.0, 0.0
b_start, b_end = 0.0, 0.0
@@ -118,7 +134,9 @@ async def run_experiment_2_multi_device_parallel(real_serial: str, emu_serial: s
a_start = time.time()
res = await client_a.run("Test operation A on physical device", max_turns=3)
a_end = time.time()
print(f"[{now_str()}] ◀ [Device A Task] Completed! Elapsed: {a_end - a_start:.3f}s, Device: {res.device_id}")
print(
f"[{now_str()}] ◀ [Device A Task] Completed! Elapsed: {a_end - a_start:.3f}s, Device: {res.device_id}"
)
return res
async def run_on_device_b():
@@ -127,7 +145,9 @@ async def run_experiment_2_multi_device_parallel(real_serial: str, emu_serial: s
b_start = time.time()
res = await client_b.run("Test operation B on emulator", max_turns=3)
b_end = time.time()
print(f"[{now_str()}] ◀ [Device B Task] Completed! Elapsed: {b_end - b_start:.3f}s, Device: {res.device_id}")
print(
f"[{now_str()}] ◀ [Device B Task] Completed! Elapsed: {b_end - b_start:.3f}s, Device: {res.device_id}"
)
return res
t_start = time.time()
@@ -137,29 +157,44 @@ async def run_experiment_2_multi_device_parallel(real_serial: str, emu_serial: s
t_total = time.time() - t_start
print("\n[Dual-Device Concurrency Analysis]:")
print(f" Device A execution interval: {a_start:.3f}s ~ {a_end:.3f}s (elapsed {a_end - a_start:.3f}s)")
print(f" Device B execution interval: {b_start:.3f}s ~ {b_end:.3f}s (elapsed {b_end - b_start:.3f}s)")
print(f" Total concurrent duration: {t_total:.3f}s (sum of sequential durations: {(a_end - a_start) + (b_end - b_start):.3f}s)")
print(
f" Device A execution interval: {a_start:.3f}s ~ {a_end:.3f}s (elapsed {a_end - a_start:.3f}s)"
)
print(
f" Device B execution interval: {b_start:.3f}s ~ {b_end:.3f}s (elapsed {b_end - b_start:.3f}s)"
)
print(
f" Total concurrent duration: {t_total:.3f}s (sum of sequential durations: {(a_end - a_start) + (b_end - b_start):.3f}s)"
)
# Overlap verification
overlap = min(a_end, b_end) - max(a_start, b_start)
print(f" Parallel overlap duration: {overlap:.3f}s")
assert overlap > 0.0, "Dual devices failed to run concurrently with overlap!"
assert t_total < ((a_end - a_start) + (b_end - b_start) * 0.95), "Total duration should be less than sum of both tasks!"
assert t_total < ((a_end - a_start) + (b_end - b_start) * 0.95), (
"Total duration should be less than sum of both tasks!"
)
print(" ✅ Verification successful: Tasks on both devices ran concurrently without blocking!")
async def run_experiment_3_global_concurrency_serialization(real_serial: str, emu_serial: str):
"""Experiment 3: Global concurrency serialization across devices."""
print_header("[Experiment 3] Global Concurrency Mode Test")
print("Mode description: concurrency_mode='global', entire system allows only 1 task across all devices.")
print(
"Mode description: concurrency_mode='global', entire system allows only 1 task across all devices."
)
client_a = ArtemisClient(device_serial=real_serial, default_profile="flash", concurrency_mode="global")
client_b = ArtemisClient(device_serial=emu_serial, default_profile="flash", concurrency_mode="global")
client_a = ArtemisClient(
device_serial=real_serial, default_profile="flash", concurrency_mode="global"
)
client_b = ArtemisClient(
device_serial=emu_serial, default_profile="flash", concurrency_mode="global"
)
# 3.1 Test non-blocking mode rejection
print("\n[Subtest 3.1: Non-blocking rejection limit]")
from artemis.runtime import DeviceExecutionLock
lock_a = DeviceExecutionLock(real_serial, "Placeholder test lock", concurrency_mode="global")
await asyncio.to_thread(lock_a.acquire)
try:
@@ -192,19 +227,31 @@ async def run_experiment_3_global_concurrency_serialization(real_serial: str, em
# Submit task 2 shortly after task 1
await asyncio.sleep(0.04)
t2_submitted = time.time()
print(f"[{now_str()}] ▶ [Global Task 2] Submitted to emulator ({emu_serial}), entering queue due to global limit...")
print(
f"[{now_str()}] ▶ [Global Task 2] Submitted to emulator ({emu_serial}), entering queue due to global limit..."
)
res = await client_b.run("Global task 2", max_turns=3)
t2_end = time.time()
print(f"[{now_str()}] ◀ [Global Task 2] Relayed completion! Total time (including queue): {t2_end - t2_submitted:.3f}s")
print(
f"[{now_str()}] ◀ [Global Task 2] Relayed completion! Total time (including queue): {t2_end - t2_submitted:.3f}s"
)
return res
res1, res2 = await asyncio.gather(run_global_task_1(), run_global_task_2())
print(f" Global Task 1 interval: {t1_submitted:.3f}s ~ {t1_end:.3f}s (elapsed {t1_end - t1_submitted:.3f}s)")
print(
f" Global Task 1 interval: {t1_submitted:.3f}s ~ {t1_end:.3f}s (elapsed {t1_end - t1_submitted:.3f}s)"
)
print(f" Global Task 2 submit time: {t2_submitted:.3f}s")
print(f" Global Task 2 end time: {t2_end:.3f}s (waited for Task 1 completion before executing)")
print(
f" Global Task 2 end time: {t2_end:.3f}s (waited for Task 1 completion before executing)"
)
assert t2_end > t1_end, "Global Task 2 did not complete after Global Task 1!"
assert (t2_end - t1_end) >= 0.08, "Global Task 2 did not execute sequentially after Task 1 released global lock!"
print(" ✅ Verification successful: Under global mode, strictly 1 task executed sequentially across different devices!")
assert (t2_end - t1_end) >= 0.08, (
"Global Task 2 did not execute sequentially after Task 1 released global lock!"
)
print(
" ✅ Verification successful: Under global mode, strictly 1 task executed sequentially across different devices!"
)
async def run_experiment_4_parameters_and_utilities(real_serial: str, emu_serial: str):
@@ -216,17 +263,23 @@ async def run_experiment_4_parameters_and_utilities(real_serial: str, emu_serial
# Chain switch device
client.set_device(real_serial)
print(f"2. Chain switch to physical device: client.set_device('{real_serial}') -> '{client.device_serial}'")
print(
f"2. Chain switch to physical device: client.set_device('{real_serial}') -> '{client.device_serial}'"
)
assert client.device_serial == real_serial
# Attribute switch
client.device_serial = emu_serial
print(f"3. Property assignment to emulator: client.device_serial = '{emu_serial}' -> '{client.device_id}'")
print(
f"3. Property assignment to emulator: client.device_serial = '{emu_serial}' -> '{client.device_id}'"
)
assert client.device_id == emu_serial
# Dynamically change concurrency mode
client.set_concurrency_mode(ConcurrencyMode.GLOBAL)
print(f"4. Chain change concurrency mode: client.concurrency_mode = '{client.concurrency_mode}'")
print(
f"4. Chain change concurrency mode: client.concurrency_mode = '{client.concurrency_mode}'"
)
assert client.concurrency_mode == "global"
client.concurrency_mode = "per_device"
@@ -234,7 +287,9 @@ async def run_experiment_4_parameters_and_utilities(real_serial: str, emu_serial
print("5. client.list_devices() status:")
devices = client.list_devices()
for d in devices:
print(f" • [{d.serial}] State: {d.state}, Model: {d.model}, IsEmulator: {d.is_emulator}, Busy: {d.is_busy}")
print(
f" • [{d.serial}] State: {d.state}, Model: {d.model}, IsEmulator: {d.is_emulator}, Busy: {d.is_busy}"
)
# Query idle devices
idle = client.get_idle_devices()
@@ -250,7 +305,9 @@ async def run_experiment_4_parameters_and_utilities(real_serial: str, emu_serial
max_turns=3,
)
result = await client.run_task(task_obj)
print(f" Task result: Status={result.status}, Device={result.device_id}, Trace={result.trace_id[:8]}...")
print(
f" Task result: Status={result.status}, Device={result.device_id}, Trace={result.trace_id[:8]}..."
)
assert result.device_id == real_serial
# Use stream_run for live event stream
@@ -275,14 +332,26 @@ async def main():
print("[ARTEMIS Python SDK Concurrency & Device Targeting Experiment]")
print(f"Detected connected devices count: {len(devices)}")
for d in devices:
print(f" • Serial: {d.serial} | Model: {d.model} | State: {d.state} | IsEmulator: {d.is_emulator}")
print(
f" • Serial: {d.serial} | Model: {d.model} | State: {d.state} | IsEmulator: {d.is_emulator}"
)
print("=" * 76)
import argparse
parser = argparse.ArgumentParser(description="Live experiment demonstrating Artemis concurrency modes and device targeting.")
parser.add_argument("--phone", default=os.environ.get("ARTEMIS_PHONE_SERIAL"), help="Target physical phone serial number")
parser.add_argument("--emulator", default=os.environ.get("ARTEMIS_EMULATOR_SERIAL"), help="Target Android emulator serial number")
parser = argparse.ArgumentParser(
description="Live experiment demonstrating Artemis concurrency modes and device targeting."
)
parser.add_argument(
"--phone",
default=os.environ.get("ARTEMIS_PHONE_SERIAL"),
help="Target physical phone serial number",
)
parser.add_argument(
"--emulator",
default=os.environ.get("ARTEMIS_EMULATOR_SERIAL"),
help="Target Android emulator serial number",
)
args, _ = parser.parse_known_args()
# Auto-detect connected devices if not explicitly provided
@@ -295,7 +364,9 @@ async def main():
# Verify both devices are available
serials = [d.serial for d in devices]
if real_serial not in serials or emu_serial not in serials:
print(f"Error: Both physical device '{real_serial}' and emulator '{emu_serial}' must be connected for full experiment.")
print(
f"Error: Both physical device '{real_serial}' and emulator '{emu_serial}' must be connected for full experiment."
)
print(f"Currently online devices: {serials}")
return
@@ -305,7 +376,9 @@ async def main():
await run_experiment_3_global_concurrency_serialization(real_serial, emu_serial)
await run_experiment_4_parameters_and_utilities(real_serial, emu_serial)
print_header("🎉 All experiments completed successfully! All concurrency and device targeting verified!")
print_header(
"🎉 All experiments completed successfully! All concurrency and device targeting verified!"
)
if __name__ == "__main__":