fix(runtime): release 2.7.1 with consistent ADB and failure diagnostics
This commit is contained in:
1 parent
b2316ca249
commit
8bf59897bb
24 files changed
+792
-45
No files matched your search
@@ -0,0 +1,144 @@
|
||||
"""Runtime snapshots are bounded, read-only evidence, never a task gate."""
|
||||
|
||||
import ctypes
|
||||
import hashlib
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
import psutil
|
||||
import pytest
|
||||
|
||||
from artemis.runtime import diagnostics as module
|
||||
from artemis.runtime.adb_endpoint import AdbEndpoint
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def isolated_identity_cache():
|
||||
identity = module.runtime_identity
|
||||
identity.cache_clear()
|
||||
yield
|
||||
identity.cache_clear()
|
||||
|
||||
|
||||
def test_tool_identity_records_exact_binary_hash_and_version(monkeypatch, tmp_path):
|
||||
adb = tmp_path / "platform tools" / "adb.exe"
|
||||
adb.parent.mkdir()
|
||||
adb.write_bytes(b"selected adb")
|
||||
monkeypatch.setattr(module.toolchain, "resolve", lambda _: str(adb))
|
||||
command = Mock(return_value="Android Debug Bridge version test")
|
||||
monkeypatch.setattr(module, "_command", command)
|
||||
record = module._tool_identity("adb")
|
||||
assert record["path"] == str(adb)
|
||||
assert record["sha256"] == hashlib.sha256(b"selected adb").hexdigest()
|
||||
assert record["version"] == command.return_value
|
||||
command.assert_called_once_with([str(adb), "version"])
|
||||
|
||||
|
||||
def test_unknown_source_and_archive_identity_are_not_invented(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(module, "ROOT_DIR", tmp_path)
|
||||
monkeypatch.setattr(module, "get_version", lambda: "test-version")
|
||||
command = Mock()
|
||||
monkeypatch.setattr(module, "_command", command)
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "private-test-key")
|
||||
record = module.runtime_identity(False)
|
||||
assert record["source_commit"] is None
|
||||
assert record["source_modified"] is None
|
||||
assert record["installation_archive_sha256"] is None
|
||||
assert record["artemis_version"] == "test-version"
|
||||
assert record["tools"] == {}
|
||||
assert "private-test-key" not in json.dumps(record)
|
||||
command.assert_not_called()
|
||||
|
||||
|
||||
def test_version_commands_are_bounded_and_never_repair_devices(monkeypatch):
|
||||
run = Mock(return_value=SimpleNamespace(stdout="version", returncode=0))
|
||||
monkeypatch.setattr(module.subprocess, "run", run)
|
||||
assert module._command(["adb", "version"]) == "version"
|
||||
assert run.call_args.kwargs["timeout"] == 3
|
||||
assert run.call_args.kwargs["check"] is False
|
||||
assert run.call_args.args == (["adb", "version"],)
|
||||
run.side_effect = OSError("Cannot create process")
|
||||
assert module._command(["adb", "version"]) is None
|
||||
|
||||
|
||||
@pytest.mark.skipif(module.sys.platform != "win32", reason="Windows PSAPI structure")
|
||||
def test_system_commit_converts_pages_to_bytes(monkeypatch):
|
||||
def query(pointer, size):
|
||||
values = pointer._obj
|
||||
assert size == ctypes.sizeof(module._PerformanceInformation)
|
||||
values.PageSize = 4096
|
||||
values.CommitTotal = 900
|
||||
values.CommitLimit = 1000
|
||||
return True
|
||||
|
||||
dll = SimpleNamespace(GetPerformanceInfo=Mock(side_effect=query))
|
||||
monkeypatch.setattr(module.ctypes, "WinDLL", lambda *args, **kwargs: dll)
|
||||
result = module._windows_commit()
|
||||
assert result["committed_bytes"] == 900 * 4096
|
||||
assert result["limit_bytes"] == 1000 * 4096
|
||||
assert result["available_bytes"] == 100 * 4096
|
||||
|
||||
|
||||
def test_remote_adb_process_is_explicitly_unknown(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
module, "current_adb_endpoint", lambda: AdbEndpoint.create("192.0.2.10", 5037)
|
||||
)
|
||||
connections = Mock()
|
||||
monkeypatch.setattr(module.psutil, "net_connections", connections)
|
||||
result = module._adb_server()
|
||||
assert result["local_listener"] is None
|
||||
assert result["endpoint"]["host"] == "192.0.2.10"
|
||||
connections.assert_not_called()
|
||||
|
||||
|
||||
def test_local_adb_listener_identity_excludes_unrelated_bindings(monkeypatch):
|
||||
monkeypatch.setattr(module, "current_adb_endpoint", lambda: AdbEndpoint.local())
|
||||
connections = [
|
||||
SimpleNamespace(status=psutil.CONN_LISTEN, laddr=SimpleNamespace(ip=ip, port=5037), pid=pid)
|
||||
for ip, pid in [("192.0.2.10", 5), ("127.0.0.1", 6)]
|
||||
]
|
||||
monkeypatch.setattr(module.psutil, "net_connections", lambda **_: connections)
|
||||
process = SimpleNamespace(pid=6, name=lambda: "adb.exe", exe=lambda: "selected/adb.exe")
|
||||
lookup = Mock(return_value=process)
|
||||
monkeypatch.setattr(module.psutil, "Process", lookup)
|
||||
assert module._adb_server()["local_listener"] == {
|
||||
"pid": 6,
|
||||
"name": "adb.exe",
|
||||
"path": "selected/adb.exe",
|
||||
}
|
||||
lookup.assert_called_once_with(6)
|
||||
|
||||
|
||||
def test_start_snapshot_survives_failure_and_repeated_start(monkeypatch, tmp_path):
|
||||
identity = Mock(return_value={"source_commit": "test-commit"})
|
||||
memory = Mock(side_effect=[{"available_bytes": 100}, {"available_bytes": 1}])
|
||||
monkeypatch.setattr(module, "runtime_identity", identity)
|
||||
monkeypatch.setattr(module, "_memory_snapshot", memory)
|
||||
monkeypatch.setattr(module, "_adb_server", lambda: {"local_listener": None})
|
||||
module.write_runtime_diagnostics(tmp_path, "start")
|
||||
module.write_runtime_diagnostics(tmp_path, "start")
|
||||
module.write_runtime_diagnostics(tmp_path, "failure")
|
||||
payload = json.loads((tmp_path / module.FILENAME).read_text(encoding="utf-8"))
|
||||
assert payload["start"]["memory"]["available_bytes"] == 100
|
||||
assert payload["failure"]["memory"]["available_bytes"] == 1
|
||||
assert payload["failure"]["runtime"]["source_commit"] == "test-commit"
|
||||
assert memory.call_count == 2
|
||||
assert not (tmp_path / "runtime_diagnostics.tmp").exists()
|
||||
|
||||
|
||||
def test_snapshot_failure_cannot_fail_a_task(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(module, "runtime_identity", lambda _: {})
|
||||
monkeypatch.setattr(module, "_memory_snapshot", Mock(side_effect=OSError("commit exhausted")))
|
||||
module.write_runtime_diagnostics(tmp_path, "failure")
|
||||
assert not (tmp_path / module.FILENAME).exists()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"error", [OSError("commit exhausted"), RuntimeError("cannot start thread")]
|
||||
)
|
||||
async def test_snapshot_thread_creation_failure_is_optional(monkeypatch, tmp_path, error):
|
||||
monkeypatch.setattr(module.asyncio, "to_thread", AsyncMock(side_effect=error))
|
||||
await module.capture_runtime_diagnostics(tmp_path, "start")
|
||||
assert not (tmp_path / module.FILENAME).exists()
|
||||
Reference in new issue
Block a user