Refresh MTP peer identity before validation
This commit is contained in:
parent
4f0a5fee31
commit
4573fe9a42
2 changed files with 88 additions and 5 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
|
@ -14,6 +15,54 @@ import sync_peer_worker # noqa: E402
|
|||
|
||||
|
||||
class SyncErrorClassificationTest(unittest.TestCase):
|
||||
def test_mtp_refreshes_a_retained_peer_advertisement_before_validation(self):
|
||||
expected_peer = "peer_00000000-0000-4000-8000-000000000001"
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
peer_path = root / "android-peer-v1.json"
|
||||
peer_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"format": "trainlog-sync-peer",
|
||||
"version": 1,
|
||||
"peer_id": expected_peer,
|
||||
"capabilities": sorted(
|
||||
sync_peer_worker.CAPS - {"session-preparations-v2"}
|
||||
),
|
||||
}
|
||||
)
|
||||
)
|
||||
pulls = []
|
||||
|
||||
def refresh(_adapter, operation, peer, transport, _deadline, allow_missing):
|
||||
pulls.append((operation, peer, transport, allow_missing))
|
||||
peer_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"format": "trainlog-sync-peer",
|
||||
"version": 1,
|
||||
"peer_id": expected_peer,
|
||||
"capabilities": sorted(sync_peer_worker.CAPS),
|
||||
}
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
with mock.patch.object(sync_peer_worker, "run_adapter", side_effect=refresh):
|
||||
sync_peer_worker.refresh_mtp_peer(
|
||||
Path("/adapter"),
|
||||
expected_peer,
|
||||
root,
|
||||
sync_peer_worker.time.monotonic() + 60,
|
||||
)
|
||||
|
||||
value = sync_peer_worker.load_peer(root, expected_peer)
|
||||
self.assertEqual(value["peer_id"], expected_peer)
|
||||
self.assertEqual(
|
||||
pulls,
|
||||
[("pull", expected_peer, root, True)],
|
||||
)
|
||||
|
||||
def test_transport_timeout_has_a_stable_code_and_bounded_diagnostic(self):
|
||||
timeout = subprocess.TimeoutExpired(["adapter", "push"], 30)
|
||||
with mock.patch.object(sync_peer_worker.subprocess, "run", side_effect=timeout):
|
||||
|
|
|
|||
|
|
@ -194,6 +194,39 @@ def wait_json(path: Path, deadline: float, predicate, pump=None) -> dict:
|
|||
time.sleep(0.05)
|
||||
|
||||
|
||||
def refresh_mtp_peer(
|
||||
adapter: Path,
|
||||
peer: str,
|
||||
root: Path,
|
||||
deadline: float,
|
||||
) -> None:
|
||||
"""Require a successful device pull before trusting a retained peer file.
|
||||
|
||||
WHY: the durable transport root legitimately retains coordination objects
|
||||
from earlier runs. A local peer advertisement can therefore predate an
|
||||
Android upgrade that adds a required capability.
|
||||
CONTRACT: MTP mode validates only an advertisement obtained after at least
|
||||
one successful pull of the expected physical peer during this run.
|
||||
INVARIANT: an unavailable peer never makes a stale local advertisement
|
||||
authoritative and remains a bounded wait state.
|
||||
"""
|
||||
peer_path = root / "android-peer-v1.json"
|
||||
while True:
|
||||
pulled = run_adapter(
|
||||
adapter,
|
||||
"pull",
|
||||
peer,
|
||||
root,
|
||||
deadline,
|
||||
True,
|
||||
)
|
||||
if pulled and peer_path.is_file():
|
||||
return
|
||||
if time.monotonic() >= deadline:
|
||||
raise RuntimeError("timeout waiting for android-peer-v1.json")
|
||||
time.sleep(0.05)
|
||||
|
||||
|
||||
def correlated_peer_error(root: Path, run_id: str) -> None:
|
||||
path = root / "android-generation-error-v1.json"
|
||||
if not path.is_file():
|
||||
|
|
@ -254,11 +287,12 @@ def main() -> int:
|
|||
deadline,
|
||||
True,
|
||||
)
|
||||
# Android publishes its durable identity only while participating. A
|
||||
# missing expected advertisement is therefore a bounded wait state;
|
||||
# device, transport, ambiguity and malformed-object failures remain
|
||||
# immediate and distinct adapter errors.
|
||||
wait_file(args.transport_root / "android-peer-v1.json", deadline, pump)
|
||||
refresh_mtp_peer(
|
||||
args.mtp_adapter,
|
||||
args.expected_peer,
|
||||
args.transport_root,
|
||||
deadline,
|
||||
)
|
||||
peer = load_peer(args.transport_root, args.expected_peer)
|
||||
with closing(generation.connect_database(args.database)) as db:
|
||||
generation.require_schema(db)
|
||||
|
|
|
|||
Loading…
Reference in a new issue