feat: enable remote SDK and resilient device orchestration

This commit is contained in:
somew1nd committed 2026-08-30 00:41:07 -07:00
1 parent 3cc711df51
commit ddfa420945
110 files changed
+14450 -12954

No files matched your search

+191
View File
@@ -12,11 +12,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import importlib
from types import SimpleNamespace
import pytest
from artemis.runtime.device_lock import DeviceExecutionLock
from artemis.runtime.device_pool import DevicePool, DeviceStatus
device_pool_module = importlib.import_module("artemis.runtime.device_pool")
@pytest.fixture(autouse=True)
def isolated_lock_directory(tmp_path, monkeypatch):
@@ -83,3 +89,188 @@ def test_device_pool_list_devices_and_busy_status(monkeypatch):
assert pool.select_device("emulator-5554") == "emulator-5554"
finally:
lock.release()
RAW_DEVICE = ("emulator-5554", "device", "Emulator", "sdk")
def _install_fake_clock(monkeypatch):
clock = {"now": 1000.0}
monkeypatch.setattr(
device_pool_module,
"time",
SimpleNamespace(monotonic=lambda: clock["now"]),
)
return clock
def test_enumeration_snapshot_collapses_repeat_queries(monkeypatch):
pool = DevicePool()
calls = {"count": 0}
def fake_query(timeout=None):
calls["count"] += 1
return [RAW_DEVICE]
monkeypatch.setattr(pool, "_query_adb_devices_sync", fake_query)
first = pool.list_devices()
second = pool.list_devices()
assert calls["count"] == 1
assert [d.serial for d in first] == [d.serial for d in second] == ["emulator-5554"]
def test_enumeration_failure_serves_last_known_snapshot(monkeypatch):
clock = _install_fake_clock(monkeypatch)
pool = DevicePool()
responses = iter([[RAW_DEVICE], None, None])
monkeypatch.setattr(pool, "_query_adb_devices_sync", lambda timeout=None: next(responses))
assert [d.serial for d in pool.list_devices()] == ["emulator-5554"]
# Past the fresh TTL but inside the stale-on-error window, a failed query
# keeps answering with the last successful snapshot.
clock["now"] += pool.CACHE_TTL + 1.0
assert [d.serial for d in pool.list_devices()] == ["emulator-5554"]
# Once the stale window closes, persistent failure is reported honestly.
clock["now"] += pool.STALE_ON_ERROR_TTL + 1.0
assert pool.list_devices() == []
def test_query_timeout_widens_until_first_success(monkeypatch):
pool = DevicePool()
assert pool._current_query_timeout() == pool.COLD_QUERY_TIMEOUT
monkeypatch.setattr(pool, "_query_adb_devices_sync", lambda timeout=None: [RAW_DEVICE])
pool.list_devices()
assert pool._current_query_timeout() == pool.HOT_QUERY_TIMEOUT
def test_async_enumeration_single_flight(monkeypatch):
pool = DevicePool()
calls = {"count": 0}
async def fake_query(timeout=None):
calls["count"] += 1
await asyncio.sleep(0.05)
return [RAW_DEVICE]
monkeypatch.setattr(pool, "_query_adb_devices_async", fake_query)
async def run():
results = await asyncio.gather(
pool.list_devices_async(),
pool.list_devices_async(),
pool.list_devices_async(),
)
return results
results = asyncio.run(run())
assert calls["count"] == 1
for devices in results:
assert [d.serial for d in devices] == ["emulator-5554"]
def test_warm_up_waits_for_device_handshake(monkeypatch):
pool = DevicePool()
monkeypatch.setattr(pool, "_resolve_adb", lambda: "adb")
started = {"count": 0}
async def fake_start_server(timeout):
started["count"] += 1
responses = iter([[], [("emulator-5554", "offline", None, None)], [RAW_DEVICE]])
async def fake_query(timeout=None):
return next(responses)
monkeypatch.setattr(pool, "_start_adb_server", fake_start_server)
monkeypatch.setattr(pool, "_query_adb_devices_async", fake_query)
warmed = asyncio.run(pool.warm_up_async(settle_timeout=2.0, poll_interval=0.01))
assert warmed is True
assert started["count"] == 1
# The settled enumeration is cached for the first post-startup listings.
assert pool._cached_raw == [RAW_DEVICE]
def test_warm_up_accepts_empty_enumeration_at_deadline(monkeypatch):
pool = DevicePool()
monkeypatch.setattr(pool, "_resolve_adb", lambda: "adb")
async def fake_start_server(timeout):
pass
async def fake_query(timeout=None):
return []
monkeypatch.setattr(pool, "_start_adb_server", fake_start_server)
monkeypatch.setattr(pool, "_query_adb_devices_async", fake_query)
warmed = asyncio.run(pool.warm_up_async(settle_timeout=0.0))
assert warmed is True
assert pool._current_query_timeout() == pool.HOT_QUERY_TIMEOUT
def test_warm_up_without_adb_returns_false(monkeypatch):
pool = DevicePool()
monkeypatch.setattr(pool, "_resolve_adb", lambda: None)
assert asyncio.run(pool.warm_up_async(settle_timeout=0.0)) is False
def test_try_list_devices_distinguishes_failure_from_empty(monkeypatch):
pool = DevicePool()
monkeypatch.setattr(pool, "_query_adb_devices_sync", lambda timeout=None: None)
assert pool.try_list_devices() is None
assert pool.list_devices() == []
fresh_pool = DevicePool()
monkeypatch.setattr(fresh_pool, "_query_adb_devices_sync", lambda timeout=None: [])
assert fresh_pool.try_list_devices() == []
def test_validate_explicit_serial_fails_open_on_indeterminate_enumeration(monkeypatch):
pool = DevicePool()
monkeypatch.setattr(pool, "_query_adb_devices_sync", lambda timeout=None: None)
assert pool.validate_explicit_serial("pixel-10") is None
empty_pool = DevicePool()
monkeypatch.setattr(empty_pool, "_query_adb_devices_sync", lambda timeout=None: [])
assert empty_pool.validate_explicit_serial("pixel-10") is None
def test_validate_explicit_serial_rejects_only_on_authoritative_enumeration(monkeypatch):
pool = DevicePool()
monkeypatch.setattr(
pool,
"_query_adb_devices_sync",
lambda timeout=None: [
("pixel-10", "device", None, None),
("pixel-11", "unauthorized", None, None),
],
)
assert pool.validate_explicit_serial("pixel-10") is None
assert "not ready" in pool.validate_explicit_serial("pixel-11")
missing = pool.validate_explicit_serial("pixel-12")
assert "not connected" in missing
assert "pixel-10" in missing
def test_validate_explicit_serial_async_matches_sync(monkeypatch):
pool = DevicePool()
async def fake_query(timeout=None):
return [("pixel-10", "device", None, None)]
monkeypatch.setattr(pool, "_query_adb_devices_async", fake_query)
async def run():
return (
await pool.validate_explicit_serial_async("pixel-10"),
await pool.validate_explicit_serial_async("pixel-12"),
)
ok, missing = asyncio.run(run())
assert ok is None
assert "not connected" in missing