feat: add multi-device concurrency, server lifecycle CLI, and unified execution stream
- Implement DevicePool and DeviceExecutionLock concurrency modes (per-device FIFO vs global serialization) - Extend ArtemisClient SDK and CLI with explicit device targeting and concurrency strategy configuration - Add server lifecycle management (artemis restart, stop, status) with cross-platform PID discovery - Update MCP server tools to support multi-device dispatch, trace tagging, and per-device execution - Enhance Showcase UI with step-by-step visual replay, playback speed controls, and device serial badges - Add comprehensive unit test coverage and edge-case verification scripts
This commit is contained in:
1 parent
a36a66ce6d
commit
c2c90a6378
84 files changed
+5612
-645
No files matched your search
@@ -291,17 +291,17 @@ async def _run_ui_sequence(controller, recording_started: float) -> tuple[list[G
|
||||
async def _analyze(analyzer: VideoAnalyzer, end_time: float, repeats: int) -> list[dict[str, Any]]:
|
||||
queries = {
|
||||
"timeline": (
|
||||
"按时间顺序列出录像中的四个事实并给出可见证据和录像相对时间:"
|
||||
"先完成 12+34=46;再完成 7×8=56;随后打开历史记录面板;最后关闭历史记录面板并回到显示 56 的主界面。"
|
||||
"不要根据提示猜测,只报告视频中实际可见的内容。"
|
||||
"List four facts from the recording in chronological order with visible evidence and relative timestamps: "
|
||||
"First complete 12+34=46; then complete 7×8=56; subsequently open the history panel; finally close the history panel and return to the main screen showing 56. "
|
||||
"Do not guess based on prompts, only report what is actually visible in the video."
|
||||
),
|
||||
"calculations": (
|
||||
"只核验录像中实际输入的两道计算及其最终可见结果。必须明确写出完整表达式 12+34=46 和 7×8=56,"
|
||||
"并按发生顺序报告;若任一项不可见就明确说缺失。"
|
||||
"Only verify the two calculations actually entered in the recording and their final visible results. Must explicitly write the complete expressions 12+34=46 and 7×8=56, "
|
||||
"and report them in order of occurrence; if any is not visible, clearly state that it is missing."
|
||||
),
|
||||
"history": (
|
||||
"只核验历史记录面板的状态变化:它是否在完成两次计算后被打开、是否显示两次计算记录、"
|
||||
"以及是否随后被关闭并返回计算器主界面。请明确使用“打开”和“关闭”描述两个转场。"
|
||||
"Only verify state changes of the history panel: whether it was opened after completing both calculations, whether it displayed both calculation records, "
|
||||
"and whether it was subsequently closed returning to the calculator main screen. Please explicitly use 'open' and 'close' to describe the two transitions."
|
||||
),
|
||||
}
|
||||
results: list[dict[str, Any]] = []
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
"""Interactive Edge-Case Test Suite with Clear Step-by-Step Guidance.
|
||||
|
||||
Supports running each edge-case part independently or all together.
|
||||
Usage:
|
||||
python scripts/run_edge_case_part.py --part 1
|
||||
python scripts/run_edge_case_part.py --part 2
|
||||
python scripts/run_edge_case_part.py --part 3
|
||||
python scripts/run_edge_case_part.py --part 4
|
||||
python scripts/run_edge_case_part.py --part all
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import requests
|
||||
|
||||
WORKSPACE_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if WORKSPACE_ROOT not in sys.path:
|
||||
sys.path.insert(0, WORKSPACE_ROOT)
|
||||
|
||||
from artemis.interfaces.sdk.client import ArtemisClient
|
||||
from artemis.runtime.device_lock import DeviceExecutionLock
|
||||
from artemis.toolchain import ensure_toolchain_in_path
|
||||
|
||||
ensure_toolchain_in_path()
|
||||
|
||||
from artemis.runtime import device_pool
|
||||
|
||||
|
||||
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")
|
||||
return phone, emu
|
||||
|
||||
|
||||
PHONE_SERIAL, EMULATOR_SERIAL = _resolve_devices()
|
||||
API_STATUS_URL = "http://127.0.0.1:8000/api/status"
|
||||
|
||||
|
||||
def query_api():
|
||||
try:
|
||||
r = requests.get(API_STATUS_URL, timeout=3).json()
|
||||
return r.get("active_tasks", []), r.get("queue", [])
|
||||
except Exception:
|
||||
return [], []
|
||||
|
||||
|
||||
def print_status_snapshot(label: str):
|
||||
active, queue = query_api()
|
||||
print(f"\n[{label}] Web Console Live Snapshot (/api/status):")
|
||||
print(f" ⚡ Running Tasks ({len(active)}):")
|
||||
for a in active:
|
||||
dev = a.get("device_id") or a.get("device_serial")
|
||||
print(f" - [Device: {dev}] {a.get('goal')} (PID: {a.get('pid')})")
|
||||
print(f" ⏳ Pending Tasks ({len(queue)}):")
|
||||
for q in queue:
|
||||
dev = q.get("device_serial") or q.get("device_id")
|
||||
print(f" - [Device: {dev}] {q.get('goal')} [status={q.get('status')}]")
|
||||
print(f" 📊 Total Active Queue (Running + Pending) = {len(active) + len(queue)}\n")
|
||||
|
||||
|
||||
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("📱 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("-" * 80)
|
||||
|
||||
DeviceExecutionLock.cleanup_stale_locks()
|
||||
client = ArtemisClient(device_serial=PHONE_SERIAL, concurrency_mode="per_device")
|
||||
|
||||
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")
|
||||
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(f"✅ [Task 1B] Completed: status={res.status}")
|
||||
return res
|
||||
|
||||
t1 = asyncio.create_task(task_1a())
|
||||
t2 = asyncio.create_task(task_1b())
|
||||
|
||||
await asyncio.sleep(1.2)
|
||||
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")
|
||||
|
||||
|
||||
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(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(" 3. Queue count: Queue is 0, each device displays its own Device Serial")
|
||||
print("-" * 80)
|
||||
|
||||
DeviceExecutionLock.cleanup_stale_locks()
|
||||
client_phone = ArtemisClient(device_serial=PHONE_SERIAL, concurrency_mode="per_device")
|
||||
client_emu = ArtemisClient(device_serial=EMULATOR_SERIAL, concurrency_mode="per_device")
|
||||
|
||||
start_t = time.time()
|
||||
|
||||
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")
|
||||
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")
|
||||
print(f"✅ [Emulator Task] Finished in {time.time() - start_t:.2f}s")
|
||||
return res
|
||||
|
||||
tp = asyncio.create_task(p_task())
|
||||
te = asyncio.create_task(e_task())
|
||||
|
||||
await asyncio.sleep(1.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")
|
||||
|
||||
|
||||
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(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(" 3. Once phone finishes, emulator instantly activates and begins execution!")
|
||||
print("-" * 80)
|
||||
|
||||
DeviceExecutionLock.cleanup_stale_locks()
|
||||
client_phone_g = ArtemisClient(device_serial=PHONE_SERIAL, concurrency_mode="global")
|
||||
client_emu_g = ArtemisClient(device_serial=EMULATOR_SERIAL, concurrency_mode="global")
|
||||
|
||||
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")
|
||||
print(f"✅ [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] Finished")
|
||||
return res
|
||||
|
||||
t3a = asyncio.create_task(task_3a())
|
||||
t3b = asyncio.create_task(task_3b())
|
||||
|
||||
await asyncio.sleep(1.2)
|
||||
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")
|
||||
|
||||
|
||||
async def run_part_4():
|
||||
print("=" * 80)
|
||||
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("👀 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("-" * 80)
|
||||
|
||||
DeviceExecutionLock.cleanup_stale_locks()
|
||||
client_phone = ArtemisClient(device_serial=PHONE_SERIAL, concurrency_mode="per_device")
|
||||
client_emu = ArtemisClient(device_serial=EMULATOR_SERIAL, concurrency_mode="per_device")
|
||||
|
||||
async def task_4a():
|
||||
print(f"🚀 [Task 4A - Phone] Started...")
|
||||
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(f"🚀 [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")
|
||||
|
||||
async def task_4c():
|
||||
print(f"🚀 [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")
|
||||
|
||||
t4a = asyncio.create_task(task_4a())
|
||||
t4b = asyncio.create_task(task_4b())
|
||||
t4c = asyncio.create_task(task_4c())
|
||||
|
||||
await asyncio.sleep(1.2)
|
||||
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")
|
||||
|
||||
|
||||
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("--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()
|
||||
|
||||
if args.phone:
|
||||
PHONE_SERIAL = args.phone
|
||||
if args.emulator:
|
||||
EMULATOR_SERIAL = args.emulator
|
||||
|
||||
if args.part == "1":
|
||||
await run_part_1()
|
||||
elif args.part == "2":
|
||||
await run_part_2()
|
||||
elif args.part == "3":
|
||||
await run_part_3()
|
||||
elif args.part == "4":
|
||||
await run_part_4()
|
||||
elif args.part == "all":
|
||||
await run_part_1()
|
||||
await asyncio.sleep(1)
|
||||
await run_part_2()
|
||||
await asyncio.sleep(1)
|
||||
await run_part_3()
|
||||
await asyncio.sleep(1)
|
||||
await run_part_4()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
+77
-7
@@ -73,12 +73,47 @@ def main() -> None:
|
||||
)
|
||||
parser.add_argument("-p", "--port", type=int, default=8000, help="Port to run UI on")
|
||||
parser.add_argument("-H", "--host", type=str, default="0.0.0.0", help="Host address to bind")
|
||||
parser.add_argument("--no-open", action="store_true", help="Do not automatically open browser")
|
||||
parser.add_argument(
|
||||
"--reload", action="store_true", help="Enable uvicorn auto-reload for development"
|
||||
"-r",
|
||||
"--restart",
|
||||
action="store_true",
|
||||
help="Restart the server by terminating any existing instance on the port first",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--stop",
|
||||
action="store_true",
|
||||
help="Stop running Artemis server on the specified port",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--status",
|
||||
action="store_true",
|
||||
help="Display status of running Artemis server on the specified port",
|
||||
)
|
||||
args, unknown = parser.parse_known_args()
|
||||
|
||||
# Ensure project root is on sys.path for lifecycle utility imports
|
||||
if str(ROOT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT_DIR))
|
||||
|
||||
from artemis.runtime.server_lifecycle import get_server_status, stop_server
|
||||
|
||||
if args.status:
|
||||
st = get_server_status(args.port)
|
||||
if st["running"]:
|
||||
pid_info = f" (PID: {', '.join(map(str, st['pids']))})" if st["pids"] else ""
|
||||
print(f"\033[1;32m● Artemis Server is RUNNING on port {args.port}{pid_info}\033[0m")
|
||||
print(f" URL: {st['url']}")
|
||||
else:
|
||||
print(f"\033[1;30m○ Artemis Server is STOPPED (Port {args.port} is free)\033[0m")
|
||||
return
|
||||
|
||||
if args.stop:
|
||||
print(f"\033[1;33m🛑 Stopping Artemis server on port {args.port}...\033[0m")
|
||||
ok, msg, pids = stop_server(args.port)
|
||||
print(f"\033[1;32m✓ {msg}\033[0m")
|
||||
return
|
||||
|
||||
ui_url = f"http://localhost:{args.port}"
|
||||
admin_url = f"http://localhost:{args.port}/admin"
|
||||
|
||||
@@ -86,15 +121,50 @@ def main() -> None:
|
||||
print("\033[1;36m ✨ Artemis Autonomous Mobile Agent UI \033[0m")
|
||||
print("\033[1;36m" + "=" * 56 + "\033[0m\n")
|
||||
|
||||
# Check if restart requested
|
||||
if args.restart:
|
||||
print(f"\033[1;33m🔄 Restart requested. Recycling port {args.port}...\033[0m")
|
||||
ok, msg, pids = stop_server(args.port)
|
||||
if pids:
|
||||
print(f"\033[1;32m✓ {msg}\033[0m\n")
|
||||
else:
|
||||
print(f"Port {args.port} is clear.\n")
|
||||
|
||||
# Check if server is already running
|
||||
if is_port_in_use(args.port):
|
||||
elif is_port_in_use(args.port):
|
||||
if is_artemis_ui_responsive(ui_url):
|
||||
print(f"\033[1;32m✓ Artemis UI is already running at:\033[0m \033[1;36m{ui_url}\033[0m")
|
||||
print(f"\033[1;35m🛠️ Admin Console:\033[0m \033[1;36m{admin_url}\033[0m\n")
|
||||
if not args.no_open:
|
||||
print("🌐 Opening browser...")
|
||||
webbrowser.open(ui_url)
|
||||
return
|
||||
|
||||
# In interactive terminal, allow user to restart or open browser
|
||||
if sys.stdin.isatty():
|
||||
print("Server is active in another session/terminal.")
|
||||
print(" [1] Open browser (default)")
|
||||
print(" [2] Restart server (stop existing and start here)")
|
||||
print(" [3] Stop server")
|
||||
try:
|
||||
choice = input("Select [1-3, default 1]: ").strip()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
choice = "1"
|
||||
|
||||
if choice == "2":
|
||||
print(f"\n\033[1;33m🔄 Restarting Artemis server on port {args.port}...\033[0m")
|
||||
stop_server(args.port)
|
||||
elif choice == "3":
|
||||
print(f"\n\033[1;33m🛑 Stopping Artemis server on port {args.port}...\033[0m")
|
||||
ok, msg, _ = stop_server(args.port)
|
||||
print(f"\033[1;32m✓ {msg}\033[0m")
|
||||
return
|
||||
else:
|
||||
if not args.no_open:
|
||||
print("🌐 Opening browser...")
|
||||
webbrowser.open(ui_url)
|
||||
return
|
||||
else:
|
||||
if not args.no_open:
|
||||
print("🌐 Opening browser...")
|
||||
webbrowser.open(ui_url)
|
||||
return
|
||||
else:
|
||||
print(f"\033[1;33m⚠️ Port {args.port} is in use by another process.\033[0m")
|
||||
print("Attempting to connect or start on specified port...")
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
"""Comprehensive Edge-Case Test Suite for Artemis Multi-Device Concurrency & Unified Queue.
|
||||
|
||||
Covers:
|
||||
1. Single Device FIFO Serial Queueing & Relay
|
||||
2. Multi-Device True Parallel Execution
|
||||
3. Global Concurrency Serialization across Distinct Devices
|
||||
4. Mixed Multi-Device Queueing
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import requests
|
||||
|
||||
# Ensure workspace in path
|
||||
WORKSPACE_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if WORKSPACE_ROOT not in sys.path:
|
||||
sys.path.insert(0, WORKSPACE_ROOT)
|
||||
|
||||
from artemis.interfaces.sdk.client import ArtemisClient
|
||||
from artemis.runtime.device_lock import DeviceExecutionLock
|
||||
from artemis.toolchain import ensure_toolchain_in_path
|
||||
|
||||
ensure_toolchain_in_path()
|
||||
|
||||
from artemis.runtime import device_pool
|
||||
|
||||
|
||||
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")
|
||||
return phone, emu
|
||||
|
||||
|
||||
PHONE_SERIAL, EMULATOR_SERIAL = _resolve_devices()
|
||||
API_STATUS_URL = "http://127.0.0.1:8000/api/status"
|
||||
|
||||
|
||||
def query_api_status():
|
||||
try:
|
||||
res = requests.get(API_STATUS_URL, timeout=3).json()
|
||||
active = res.get("active_tasks", [])
|
||||
queue = res.get("queue", [])
|
||||
return active, queue
|
||||
except Exception as e:
|
||||
return [], []
|
||||
|
||||
|
||||
def print_status_snapshot(stage_name: str):
|
||||
active, queue = query_api_status()
|
||||
print(f"\n[{stage_name}] Live Snapshot from Web Console (/api/status):")
|
||||
print(f" ⚡ Running Tasks ({len(active)}):")
|
||||
for a in active:
|
||||
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')
|
||||
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")
|
||||
|
||||
|
||||
async def stage_1_single_device_queueing():
|
||||
print("\n" + "=" * 80)
|
||||
print("STAGE 1: Single Device FIFO Queueing & Relay")
|
||||
print(f"Target Device: {PHONE_SERIAL} (Pixel 11 Pro)")
|
||||
print("=" * 80)
|
||||
|
||||
client_phone = ArtemisClient(device_serial=PHONE_SERIAL, concurrency_mode="per_device")
|
||||
|
||||
t1_started = False
|
||||
t2_queued = False
|
||||
|
||||
async def task_1a():
|
||||
print(f"🚀 [Task 1A] Launching on {PHONE_SERIAL}: Press HOME and open Settings...")
|
||||
res = await client_phone.run("Press HOME key and open Settings", profile="flash")
|
||||
print(f"✅ [Task 1A] Finished with status: {res.status}")
|
||||
return res
|
||||
|
||||
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!)...")
|
||||
res = await client_phone.run("Press HOME key and open Clock", profile="flash")
|
||||
print(f"✅ [Task 1B] Finished with status: {res.status}")
|
||||
return res
|
||||
|
||||
# Run both simultaneously
|
||||
task_a_coro = asyncio.create_task(task_1a())
|
||||
task_b_coro = asyncio.create_task(task_1b())
|
||||
|
||||
# Wait 1.5s and inspect status while 1A is running and 1B is queued
|
||||
await asyncio.sleep(1.2)
|
||||
print_status_snapshot("STAGE 1 In-Progress")
|
||||
active, queue = query_api_status()
|
||||
assert len(active) >= 1, "Expected Task 1A to be actively running!"
|
||||
# 1B should be in the queue or 1A just finished
|
||||
print(f"✓ Verified: Task 1A is running on {PHONE_SERIAL}")
|
||||
|
||||
res_a, res_b = await asyncio.gather(task_a_coro, task_b_coro)
|
||||
print("✓ Stage 1 Complete: Both tasks executed in order and completed successfully!")
|
||||
|
||||
|
||||
async def stage_2_multi_device_parallel():
|
||||
print("\n" + "=" * 80)
|
||||
print("STAGE 2: Multi-Device True Parallel Execution")
|
||||
print(f"Devices: {PHONE_SERIAL} (Pixel 11 Pro) AND {EMULATOR_SERIAL} (Emulator)")
|
||||
print("Concurrency Mode: per_device (Should run simultaneously!)")
|
||||
print("=" * 80)
|
||||
|
||||
client_phone = ArtemisClient(device_serial=PHONE_SERIAL, concurrency_mode="per_device")
|
||||
client_emu = ArtemisClient(device_serial=EMULATOR_SERIAL, concurrency_mode="per_device")
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
async def task_phone():
|
||||
print(f"📱 [Phone Task] Running on {PHONE_SERIAL}: Open Settings...")
|
||||
res = await client_phone.run("Press HOME key and open Settings", profile="flash")
|
||||
print(f"✅ [Phone Task] Completed in {time.time() - start_time:.2f}s")
|
||||
return res
|
||||
|
||||
async def task_emu():
|
||||
print(f"💻 [Emulator Task] Running on {EMULATOR_SERIAL}: Open Settings...")
|
||||
res = await client_emu.run("Press HOME key and open Settings", profile="flash")
|
||||
print(f"✅ [Emulator Task] Completed in {time.time() - start_time:.2f}s")
|
||||
return res
|
||||
|
||||
p_task = asyncio.create_task(task_phone())
|
||||
e_task = asyncio.create_task(task_emu())
|
||||
|
||||
# Check during run
|
||||
await asyncio.sleep(1.2)
|
||||
print_status_snapshot("STAGE 2 Parallel In-Progress")
|
||||
active, queue = query_api_status()
|
||||
print(f"Active devices count: {len(active)}")
|
||||
active_devices = {a.get("device_id") for a in active}
|
||||
print(f"Active devices: {active_devices}")
|
||||
|
||||
await asyncio.gather(p_task, e_task)
|
||||
total_duration = time.time() - start_time
|
||||
print(f"✓ Stage 2 Complete: Both devices completed parallel tasks in {total_duration:.2f}s!")
|
||||
|
||||
|
||||
async def stage_3_global_concurrency_mode():
|
||||
print("\n" + "=" * 80)
|
||||
print("STAGE 3: Global Concurrency Mode Serialization")
|
||||
print("Concurrency Mode: global (max 1 task system-wide even across different devices)")
|
||||
print("=" * 80)
|
||||
|
||||
client_phone_global = ArtemisClient(device_serial=PHONE_SERIAL, concurrency_mode="global")
|
||||
client_emu_global = ArtemisClient(device_serial=EMULATOR_SERIAL, concurrency_mode="global")
|
||||
|
||||
async def task_phone_g():
|
||||
print(f"📱 [Task 3A - Phone] Acquiring global lock on {PHONE_SERIAL}...")
|
||||
res = await client_phone_global.run("Press HOME key and open Settings", profile="flash")
|
||||
print(f"✅ [Task 3A - Phone] Finished.")
|
||||
return res
|
||||
|
||||
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!)...")
|
||||
res = await client_emu_global.run("Press HOME key and open Settings", profile="flash")
|
||||
print(f"✅ [Task 3B - Emulator] Finished.")
|
||||
return res
|
||||
|
||||
p_task = asyncio.create_task(task_phone_g())
|
||||
e_task = asyncio.create_task(task_emu_g())
|
||||
|
||||
await asyncio.sleep(1.2)
|
||||
print_status_snapshot("STAGE 3 Global Mode In-Progress")
|
||||
active, queue = query_api_status()
|
||||
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!")
|
||||
|
||||
|
||||
async def stage_4_mixed_queue_and_parallel():
|
||||
print("\n" + "=" * 80)
|
||||
print("STAGE 4: Mixed Multi-Device Queueing")
|
||||
print("Scenario: Phone gets 2 tasks (4A, 4B), Emulator gets 1 task (4C).")
|
||||
print("Expectation: 4A (Phone) and 4C (Emulator) run concurrently; 4B waits for Phone.")
|
||||
print("=" * 80)
|
||||
|
||||
client_phone = ArtemisClient(device_serial=PHONE_SERIAL, concurrency_mode="per_device")
|
||||
client_emu = ArtemisClient(device_serial=EMULATOR_SERIAL, concurrency_mode="per_device")
|
||||
|
||||
async def task_4a():
|
||||
print(f"📱 [Task 4A - Phone] Running...")
|
||||
return await client_phone.run("Press HOME key and open Settings", profile="flash")
|
||||
|
||||
async def task_4b():
|
||||
await asyncio.sleep(0.4)
|
||||
print(f"📱 [Task 4B - Phone] Queued behind 4A...")
|
||||
return await client_phone.run("Press HOME key and open Clock", profile="flash")
|
||||
|
||||
async def task_4c():
|
||||
print(f"💻 [Task 4C - Emulator] Running in parallel with 4A...")
|
||||
return await client_emu.run("Press HOME key and open Settings", profile="flash")
|
||||
|
||||
t4a = asyncio.create_task(task_4a())
|
||||
t4b = asyncio.create_task(task_4b())
|
||||
t4c = asyncio.create_task(task_4c())
|
||||
|
||||
await asyncio.sleep(1.2)
|
||||
print_status_snapshot("STAGE 4 Mixed Multi-Device In-Progress")
|
||||
|
||||
await asyncio.gather(t4a, t4b, t4c)
|
||||
print("✓ Stage 4 Complete: 4A and 4C ran in parallel, 4B queued and picked up phone relay!")
|
||||
|
||||
|
||||
async def main():
|
||||
import argparse
|
||||
global PHONE_SERIAL, EMULATOR_SERIAL
|
||||
|
||||
parser = argparse.ArgumentParser(description="ARTEMIS All Edge-Cases Comprehensive Test Suite")
|
||||
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()
|
||||
|
||||
if args.phone:
|
||||
PHONE_SERIAL = args.phone
|
||||
if args.emulator:
|
||||
EMULATOR_SERIAL = args.emulator
|
||||
|
||||
print("\n" + "#" * 80)
|
||||
print("# ARTEMIS ALL EDGE-CASES COMPREHENSIVE CONCURRENCY & QUEUE TEST SUITE")
|
||||
print(f"# Devices: Real Phone ({PHONE_SERIAL}) + Android Emulator ({EMULATOR_SERIAL})")
|
||||
print("# Web Console API: http://127.0.0.1:8000/api/status")
|
||||
print("#" * 80)
|
||||
|
||||
# Initial cleanup
|
||||
DeviceExecutionLock.cleanup_stale_locks()
|
||||
print_status_snapshot("Initial System State")
|
||||
|
||||
# Run all 4 stages
|
||||
await stage_1_single_device_queueing()
|
||||
await asyncio.sleep(1)
|
||||
|
||||
await stage_2_multi_device_parallel()
|
||||
await asyncio.sleep(1)
|
||||
|
||||
await stage_3_global_concurrency_mode()
|
||||
await asyncio.sleep(1)
|
||||
|
||||
await stage_4_mixed_queue_and_parallel()
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("🎉 ALL EDGE-CASE STAGES COMPLETED SUCCESSFULLY!")
|
||||
print("=" * 80)
|
||||
print_status_snapshot("Final System State")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,312 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright 2026 Google LLC
|
||||
"""Live experiment demonstrating Artemis concurrency modes and device targeting.
|
||||
|
||||
Covers:
|
||||
1. Single-device queueing and relay (FIFO mutual exclusion on the same device).
|
||||
2. Multi-device parallel execution (per_device concurrency mode across real device & emulator).
|
||||
3. Cross-device global serialization (global concurrency mode restricting all devices to 1 task).
|
||||
4. Full parameter coverage (device_serial, set_device, Task objects, list_devices, get_idle_devices).
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
# Add project root to sys.path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from artemis import ArtemisClient, ConcurrencyMode, Task
|
||||
from artemis.runtime import DeviceBusyError, device_pool
|
||||
|
||||
|
||||
def print_header(title: str):
|
||||
print("\n" + "=" * 76)
|
||||
print(f" {title}")
|
||||
print("=" * 76)
|
||||
|
||||
|
||||
def now_str() -> str:
|
||||
return datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
||||
|
||||
|
||||
async def run_experiment_1_single_device_relay(real_serial: str):
|
||||
"""Experiment 1: Single device FIFO queueing & relay."""
|
||||
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}'")
|
||||
|
||||
task_1_launched = 0.0
|
||||
task_1_start = 0.0
|
||||
task_1_end = 0.0
|
||||
|
||||
task_2_launched = 0.0
|
||||
task_2_start = 0.0
|
||||
task_2_end = 0.0
|
||||
|
||||
async def execute_task_1():
|
||||
nonlocal task_1_launched, task_1_start, task_1_end
|
||||
task_1_launched = time.time()
|
||||
print(f"[{now_str()}] ▶ [Task 1] Submitted for scheduling (target device: {real_serial})")
|
||||
task_1_start = time.time()
|
||||
res = await client.run(
|
||||
"Open Settings and check battery level",
|
||||
device_serial=real_serial,
|
||||
max_turns=3,
|
||||
)
|
||||
task_1_end = time.time()
|
||||
print(
|
||||
f"[{now_str()}] ◀ [Task 1] Execution completed! Status={res.status}, "
|
||||
f"Trace={res.trace_id[:8]}..., Elapsed={task_1_end - task_1_start:.3f}s"
|
||||
)
|
||||
return res
|
||||
|
||||
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...)")
|
||||
|
||||
task_2_start = time.time()
|
||||
res = await client.run(
|
||||
"Return to home screen and open Clock",
|
||||
device_serial=real_serial,
|
||||
max_turns=3,
|
||||
)
|
||||
task_2_end = time.time()
|
||||
print(
|
||||
f"[{now_str()}] ◀ [Task 2] Relayed execution completed! Status={res.status}, "
|
||||
f"Trace={res.trace_id[:8]}..., Elapsed={task_2_end - task_2_start:.3f}s"
|
||||
)
|
||||
return res
|
||||
|
||||
# Launch Task 1 first, then Task 2 40ms later while Task 1 is holding the lock
|
||||
t_coro_1 = asyncio.create_task(execute_task_1())
|
||||
await asyncio.sleep(0.04)
|
||||
t_coro_2 = asyncio.create_task(execute_task_2())
|
||||
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 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!")
|
||||
|
||||
|
||||
async def run_experiment_2_multi_device_parallel(real_serial: str, emu_serial: str):
|
||||
"""Experiment 2: Multi-device parallel execution under per_device concurrency."""
|
||||
print_header("[Experiment 2] Multi-Device Parallel Execution Test")
|
||||
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")
|
||||
|
||||
a_start, a_end = 0.0, 0.0
|
||||
b_start, b_end = 0.0, 0.0
|
||||
|
||||
async def run_on_device_a():
|
||||
nonlocal a_start, a_end
|
||||
print(f"[{now_str()}] ▶ [Device A Task] Starting on {real_serial}")
|
||||
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}")
|
||||
return res
|
||||
|
||||
async def run_on_device_b():
|
||||
nonlocal b_start, b_end
|
||||
print(f"[{now_str()}] ▶ [Device B Task] Starting on {emu_serial}")
|
||||
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}")
|
||||
return res
|
||||
|
||||
t_start = time.time()
|
||||
task_a = asyncio.create_task(run_on_device_a())
|
||||
task_b = asyncio.create_task(run_on_device_b())
|
||||
res_a, res_b = await asyncio.gather(task_a, task_b)
|
||||
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)")
|
||||
|
||||
# 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!"
|
||||
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(f"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")
|
||||
|
||||
# 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:
|
||||
print(f"[{now_str()}] Device A ({real_serial}) holds the global lock...")
|
||||
try:
|
||||
print(f"[{now_str()}] Device B ({emu_serial}) attempts run with blocking=False...")
|
||||
await client_b.run("Emulator non-blocking task", blocking=False)
|
||||
print(" ❌ Exception: Was not intercepted by global lock!")
|
||||
except DeviceBusyError as exc:
|
||||
print(f" ✅ Correctly intercepted with DeviceBusyError: {exc}")
|
||||
finally:
|
||||
await asyncio.to_thread(lock_a.release)
|
||||
|
||||
# 3.2 Test cross-device queueing and relay
|
||||
print("\n[Subtest 3.2: Cross-device global queueing & relay]")
|
||||
t1_submitted, t1_end = 0.0, 0.0
|
||||
t2_submitted, t2_end = 0.0, 0.0
|
||||
|
||||
async def run_global_task_1():
|
||||
nonlocal t1_submitted, t1_end
|
||||
t1_submitted = time.time()
|
||||
print(f"[{now_str()}] ▶ [Global Task 1] Started on physical device ({real_serial})...")
|
||||
res = await client_a.run("Global task 1", max_turns=3)
|
||||
t1_end = time.time()
|
||||
print(f"[{now_str()}] ◀ [Global Task 1] Completed! Elapsed: {t1_end - t1_submitted:.3f}s")
|
||||
return res
|
||||
|
||||
async def run_global_task_2():
|
||||
nonlocal t2_submitted, t2_end
|
||||
# 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...")
|
||||
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")
|
||||
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 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)")
|
||||
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!")
|
||||
|
||||
|
||||
async def run_experiment_4_parameters_and_utilities(real_serial: str, emu_serial: str):
|
||||
"""Experiment 4: Comprehensive SDK parameter and utility testing."""
|
||||
print_header("[Experiment 4] SDK Parameter & Utility Comprehensive Test")
|
||||
|
||||
client = ArtemisClient()
|
||||
print(f"1. Default idle device selection: client.device_serial = '{client.device_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}'")
|
||||
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}'")
|
||||
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}'")
|
||||
assert client.concurrency_mode == "global"
|
||||
client.concurrency_mode = "per_device"
|
||||
|
||||
# Query all available devices
|
||||
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}")
|
||||
|
||||
# Query idle devices
|
||||
idle = client.get_idle_devices()
|
||||
print(f"6. client.get_idle_devices(): {[d.serial for d in idle]}")
|
||||
|
||||
# Use Task structured object with run_task
|
||||
print("7. Calling client.run_task() with structured Task entity:")
|
||||
task_obj = Task(
|
||||
goal="Verify physical device call via Task object",
|
||||
profile="flash",
|
||||
device_serial=real_serial,
|
||||
concurrency_mode="per_device",
|
||||
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]}...")
|
||||
assert result.device_id == real_serial
|
||||
|
||||
# Use stream_run for live event stream
|
||||
print("8. Using client.stream_run() for live event stream:")
|
||||
events_received = 0
|
||||
async for ev in client.stream_run(
|
||||
"Stream operation test",
|
||||
device_serial=emu_serial,
|
||||
profile="flash",
|
||||
max_turns=3,
|
||||
):
|
||||
events_received += 1
|
||||
print(f" • [{ev.event_type}] payload={ev.payload}")
|
||||
assert events_received >= 2
|
||||
print(f" ✅ Stream events captured successfully! Captured {events_received} events.")
|
||||
|
||||
|
||||
async def main():
|
||||
# Detect devices
|
||||
devices = device_pool.list_devices()
|
||||
print("=" * 76)
|
||||
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("=" * 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")
|
||||
args, _ = parser.parse_known_args()
|
||||
|
||||
# Auto-detect connected devices if not explicitly provided
|
||||
real_dev = next((d for d in devices if not d.is_emulator and d.state == "device"), None)
|
||||
emu_dev = next((d for d in devices if d.is_emulator and d.state == "device"), None)
|
||||
|
||||
real_serial = args.phone or (real_dev.serial if real_dev else "63191FDKX00062")
|
||||
emu_serial = args.emulator or (emu_dev.serial if emu_dev else "emulator-5554")
|
||||
|
||||
# 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"Currently online devices: {serials}")
|
||||
return
|
||||
|
||||
# Run experiments in sequence
|
||||
await run_experiment_1_single_device_relay(real_serial)
|
||||
await run_experiment_2_multi_device_parallel(real_serial, emu_serial)
|
||||
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!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in new issue
Block a user