Files
ddidderr 4a1b08db98 test(peer-cli): verify protocol 8 transfer lifecycle
Replace legacy metadata and relay expectations with current protocol-8 JSONL
assertions. Scenarios now bind every source to authenticated PeerId and exact
ContentId, prove typed attempt lifecycle order, and fence cancellation,
quarantine, rollback, republishing, and peer-departure outcomes against vacuous
success.

Isolated topologies distinguish direct author pulls from relay and ambient mDNS
substitution. The run log records focused diagnostics and the final fresh-image
S1-S49 acceptance result without presenting Docker-host throughput as a
representative external-LAN measurement.

Test Plan:
- `LANSPREAD_S37_MIN_MIB_PER_S=100 just peer-cli-tests` -- passed S1-S49
- S37 -- passed 2,147,483,656 bytes in 17 chunks at 551.10 MiB/s
- `python3 -m py_compile crates/lanspread-peer-cli/scripts/run_extended_scenarios.py` -- passed
- `ruff check --select F,E9 crates/lanspread-peer-cli/scripts/run_extended_scenarios.py` -- passed
- `git diff --cached --check` -- passed
2026-08-10 14:00:03 +02:00

4214 lines
171 KiB
Python

#!/usr/bin/env python3
"""Run the peer-cli scenarios S1-S49 through Docker."""
from __future__ import annotations
import argparse
import base64
import binascii
import hashlib
import json
import os
import queue
import shlex
import shutil
import subprocess
import sys
import threading
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable
REPO = Path(__file__).resolve().parents[3]
RUN_ROOT = REPO / ".lanspread-peer-cli" / "extended-scenarios"
IMAGE = "lanspread-peer-cli:dev"
NETWORK = "lanspread"
CONTAINER_PREFIX = "lanspread-peer-cli-ext"
CATALOG_DB = "/app/game.db"
CATALOG_MANIFESTS = "/app/manifests"
FIXTURES = REPO / "crates" / "lanspread-peer-cli" / "fixtures"
CATALOG_PROFILES = REPO / "crates" / "lanspread-peer-cli" / "catalogs"
CHUNK_SIZE = 128 * 1024 * 1024
# Runtime liveness needs the 90-second stale threshold plus a 20-second ping
# tick before a departed peer and its remote state are removed. Keep a
# conservative harness margin; graceful shutdown intentionally sends no
# protocol Goodbye.
PEER_DEPARTURE_TIMEOUT_SECONDS = 125
CATALOG_VERSIONS = {
"alienswarm": "20190317",
"bf1942": "20160130",
"bfbc2": "20210416",
"cnc4": "20170204",
"cnctw": "20160128",
"cod5": "20160920",
"cod6": "20200315",
"coh": "20200907",
"css": "20240623",
"ggoo": "20200721",
}
PERF_GAME_ID = "bf1942"
PERF_GAME_VERSION = CATALOG_VERSIONS[PERF_GAME_ID]
PERF_GAME_SIZE = 2 * 1024 * 1024 * 1024
IGNORED_DIFF_NAMES = {".lanspread", ".lanspread.json", "local"}
class ScenarioError(RuntimeError):
pass
@dataclass
class LineWaiter:
seen: int = 0
@dataclass
class Peer:
runner: "Runner"
name: str
network: str = NETWORK
games_dir: Path | None = None
readonly_games: bool = False
tmpfs_size: str | None = None
fixtures: list[str] = field(default_factory=list)
extra_args: list[str] = field(default_factory=list)
catalog_dir: Path | None = None
process: subprocess.Popen[str] | None = None
output: list[dict[str, Any]] = field(default_factory=list)
raw_output: list[str] = field(default_factory=list)
events: queue.Queue[dict[str, Any]] = field(default_factory=queue.Queue)
condition: threading.Condition = field(default_factory=threading.Condition)
request_index: int = 0
ready_addr: str | None = None
peer_id: str | None = None
@property
def container_name(self) -> str:
return f"{CONTAINER_PREFIX}-{self.runner.run_id}-{self.name}"
@property
def host_games_dir(self) -> Path:
if self.games_dir is not None:
return self.games_dir
return self.runner.games_root / self.name
@property
def host_state_dir(self) -> Path:
return self.runner.state_root / self.name
def start(self) -> "Peer":
self.host_state_dir.mkdir(parents=True, exist_ok=True)
if self.tmpfs_size is None:
self.host_games_dir.mkdir(parents=True, exist_ok=True)
command = [
"docker",
"run",
"--rm",
"--init",
"--network",
self.network,
"--name",
self.container_name,
"-i",
"-v",
f"{self.host_state_dir}:/state",
]
if self.tmpfs_size is None:
mode = "ro" if self.readonly_games else "rw"
command.extend(["-v", f"{self.host_games_dir}:/games:{mode}"])
else:
command.extend(["--tmpfs", f"/games:size={self.tmpfs_size}"])
catalog_db = CATALOG_DB
catalog_manifests = CATALOG_MANIFESTS
if self.catalog_dir is not None:
if not (self.catalog_dir / "game.db").is_file():
raise ScenarioError(f"catalog profile has no game.db: {self.catalog_dir}")
if not (self.catalog_dir / "manifests").is_dir():
raise ScenarioError(f"catalog profile has no manifests/: {self.catalog_dir}")
command.extend(["-v", f"{self.catalog_dir}:/scenario-catalog:ro"])
catalog_db = "/scenario-catalog/game.db"
catalog_manifests = "/scenario-catalog/manifests"
command.extend(
[
IMAGE,
"--name",
self.name,
"--games-dir",
"/games",
"--state-dir",
"/state",
"--catalog-db",
catalog_db,
"--manifests-dir",
catalog_manifests,
]
)
for fixture in self.fixtures:
command.extend(["--fixture", fixture])
command.extend(self.extra_args)
self.runner.log(f"start {self.name}: {' '.join(command)}")
self.process = subprocess.Popen(
command,
cwd=REPO,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
threading.Thread(target=self._read_output, daemon=True).start()
self.wait_ready()
return self
def _read_output(self) -> None:
assert self.process is not None
assert self.process.stdout is not None
for line in self.process.stdout:
stripped = line.rstrip("\r\n")
with self.condition:
self.raw_output.append(stripped)
try:
parsed = json.loads(stripped)
except json.JSONDecodeError:
parsed = {"type": "raw", "line": stripped}
self.output.append(parsed)
self.condition.notify_all()
self.events.put(parsed)
def wait_ready(self) -> None:
line = self.wait_for(
lambda item: item.get("type") == "event"
and item.get("event") == "local-peer-ready",
timeout=20,
description=f"{self.name} local-peer-ready",
)
data = line["data"]
self.ready_addr = data["addr"]
self.peer_id = data["peer_id"]
def send(
self,
payload: dict[str, Any],
*,
expect_error: bool = False,
timeout: float = 20,
) -> dict[str, Any]:
assert self.process is not None
assert self.process.stdin is not None
self.request_index += 1
request_id = f"{self.name}-{self.request_index}"
payload = dict(payload)
payload["id"] = request_id
line = json.dumps(payload, separators=(",", ":"))
self.process.stdin.write(line + "\n")
self.process.stdin.flush()
response = self.wait_for(
lambda item: item.get("id") == request_id
and item.get("type") in {"result", "error"},
timeout=timeout,
description=f"{self.name} response to {payload.get('cmd')}",
)
if response["type"] == "error":
if expect_error:
return response
raise ScenarioError(f"{self.name} command failed: {response}")
if expect_error:
raise ScenarioError(f"{self.name} command unexpectedly succeeded: {response}")
return response
def wait_for(
self,
predicate: Callable[[dict[str, Any]], bool],
*,
timeout: float,
description: str,
waiter: LineWaiter | None = None,
) -> dict[str, Any]:
deadline = time.monotonic() + timeout
with self.condition:
if waiter is None:
start = 0
else:
start = waiter.seen
while True:
for index in range(start, len(self.output)):
item = self.output[index]
if predicate(item):
if waiter is not None:
waiter.seen = index + 1
return item
start = len(self.output)
if waiter is not None:
waiter.seen = start
remaining = deadline - time.monotonic()
if remaining <= 0:
tail = "\n".join(self.raw_output[-20:])
raise ScenarioError(
f"timed out waiting for {description} on {self.name}\n{tail}"
)
self.condition.wait(remaining)
def list_games(self) -> dict[str, Any]:
return self.send({"cmd": "list-games"})["data"]
def list_peers(self) -> list[dict[str, Any]]:
return self.send({"cmd": "list-peers"})["data"]["peers"]
def status(self) -> dict[str, Any]:
return self.send({"cmd": "status"})["data"]
def call_to_play_events(self) -> list[dict[str, Any]]:
return self.send({"cmd": "list-call-to-play"})["data"]["view"]["events"]
def publish_call_to_play(self, intent: dict[str, Any]) -> dict[str, Any]:
return self.send(
{"cmd": "publish-call-to-play", "intent": intent}
)["data"]["receipt"]
def set_call_to_play_display_name(self, display_name: str) -> None:
self.send(
{
"cmd": "set-call-to-play-display-name",
"display_name": display_name,
}
)
def ready_endpoint(self) -> dict[str, str]:
if self.ready_addr is None or self.peer_id is None:
raise ScenarioError(f"{self.name} is not ready")
return {"peer_id": self.peer_id, "addr": self.ready_addr}
def connect_to(self, other: "Peer") -> None:
self.send({"cmd": "connect", **other.ready_endpoint()})
self.send({"cmd": "wait-peers", "count": 1, "timeout_ms": 10000})
def connect_to_at(self, other: "Peer", addr: str, peer_count: int) -> None:
if other.peer_id is None:
raise ScenarioError(f"{other.name} has no ready peer identity")
self.send({"cmd": "connect", "peer_id": other.peer_id, "addr": addr})
self.send({"cmd": "wait-peers", "count": peer_count, "timeout_ms": 10000})
def shutdown(self) -> None:
if self.process is None:
return
if self.process.poll() is None:
try:
self.send({"cmd": "shutdown"}, timeout=8)
except Exception:
self.kill()
try:
self.process.wait(timeout=8)
except subprocess.TimeoutExpired:
self.kill()
def shutdown_gracefully_for_proof(self) -> None:
if self.process is None or self.process.poll() is not None:
raise ScenarioError(
f"{self.name} was not running at the strict graceful-shutdown fence"
)
result = self.send({"cmd": "shutdown"}, timeout=15)
if (
result.get("command") != "shutdown"
or result.get("data", {}).get("stopped") is not True
):
raise ScenarioError(
f"{self.name} returned an invalid graceful-shutdown proof: {result}"
)
try:
exit_code = self.process.wait(timeout=8)
except subprocess.TimeoutExpired as error:
raise ScenarioError(
f"{self.name} did not exit after its graceful-shutdown proof"
) from error
if exit_code != 0:
raise ScenarioError(
f"{self.name} exited {exit_code} after its graceful-shutdown proof"
)
def kill(self) -> None:
subprocess.run(
["docker", "rm", "-f", self.container_name],
cwd=REPO,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False,
)
if self.process is not None:
try:
self.process.wait(timeout=5)
except subprocess.TimeoutExpired:
self.process.kill()
def docker_exec(self, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["docker", "exec", self.container_name, *args],
cwd=REPO,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
check=check,
)
class Runner:
def __init__(self, selected: set[str] | None = None, build_image: bool = False) -> None:
self.selected = selected
self.build_image = build_image
self.run_id = str(int(time.time()))
self.state_root = RUN_ROOT / "state"
self.games_root = RUN_ROOT / "games"
self.fixture_root = RUN_ROOT / "fixtures"
self.current_peers: list[Peer] = []
self.current_networks: list[str] = []
self.results: list[tuple[str, str]] = []
def log(self, message: str) -> None:
print(message, flush=True)
def run(self) -> None:
self.prepare()
scenarios: list[tuple[str, Callable[[], str]]] = [
("S1", self.s1_startup_scan),
("S2", self.s2_direct_connect_handshake),
("S3", self.s3_remote_aggregation),
("S4", self.s4_single_source_download),
("S5", self.s5_auto_install_download),
("S6", self.s6_manual_install_uninstall),
("S7", self.s7_duplicate_source_download),
("S8", self.s8_catalog_file_shape_failover),
("S9", self.s9_missing_game),
("S10", self.s10_shutdown_cleanup),
("S11", self.s11_same_identity_reconnect),
("S12", self.s12_transfer_serving_gates),
("S13", self.s13_exact_transfer_equality),
("S14", self.s14_large_multi_peer_chunking),
("S15", self.s15_three_way_version_skew),
("S16", self.s16_catalog_fanout_with_stale),
("S17", self.s17_catalog_byte_quarantine),
("S18", self.s18_redundant_source_drop),
("S19", self.s19_sole_source_drop),
("S20", self.s20_receiver_write_failure),
("S21", self.s21_add_game_propagation),
("S22", self.s22_remove_game_propagation),
("S23", self.s23_version_bump_propagation),
("S24", self.s24_two_clients_one_source),
("S25", self.s25_two_downloads_one_client),
("S26", self.s26_duplicate_download_rejection),
("S27", self.s27_self_connect_rejection),
("S28", self.s28_reconnect_generation_unit),
("S29", self.s29_empty_peer_participates),
("S30", self.s30_mesh_aggregation),
("S31", self.s31_bootstrapped_peer_source),
("S32", self.s32_reinstall_after_uninstall),
("S33", self.s33_install_after_mutation),
("S34", self.s34_many_small_files),
("S35", self.s35_unknown_game_filtered),
("S36", self.s36_catalog_singleton),
("S37", self.s37_single_source_download_throughput),
("S38", self.s38_first_play_launch_settings),
("S39", self.s39_streamed_install_local_only),
("S40", self.s40_streamed_receiver_not_source),
("S41", self.s41_solid_archive_streamed_install),
("S42", self.s42_streamed_install_retries_next_source),
("S43", self.s43_streamed_install_rejects_installed_game),
("S44", self.s44_corrupt_stream_rolls_back),
("S45", self.s45_sender_disconnect_mid_stream),
("S46", self.s46_receiver_cancel_mid_stream),
("S47", self.s47_multi_archive_streams_in_sorted_order),
("S48", self.s48_call_to_play_replication_and_late_join),
("S49", self.s49_terminal_call_to_play_late_join),
]
for scenario_id, scenario in scenarios:
if self.selected and scenario_id.lower() not in self.selected:
continue
self.cleanup_containers()
self.cleanup_scenario_networks()
self.current_peers = []
try:
self.log(f"\n== {scenario_id} ==")
evidence = scenario()
self.results.append((scenario_id, evidence))
self.log(f"{scenario_id} PASS: {evidence}")
finally:
self.stop_peers()
if not self.results:
raise ScenarioError("no scenarios selected")
self.log("\nSummary:")
for scenario_id, evidence in self.results:
self.log(f"- {scenario_id}: {evidence}")
def prepare(self) -> None:
reset_run_root()
self.state_root.mkdir(parents=True, exist_ok=True)
self.games_root.mkdir(parents=True, exist_ok=True)
self.fixture_root.mkdir(parents=True, exist_ok=True)
self.cleanup_containers()
self.cleanup_scenario_networks()
if self.build_image:
run(["just", "peer-cli-image"], "build peer-cli image")
run(["just", "peer-cli-net"], "prepare peer-cli docker network")
def cleanup_containers(self) -> None:
result = subprocess.run(
["docker", "ps", "-a", "--format", "{{.Names}}"],
cwd=REPO,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=True,
)
names = [
name
for name in result.stdout.splitlines()
if name.startswith(f"{CONTAINER_PREFIX}-")
]
if names:
subprocess.run(
["docker", "rm", "-f", *names],
cwd=REPO,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False,
)
def stop_peers(self) -> None:
for peer in reversed(self.current_peers):
peer.shutdown()
self.cleanup_containers()
self.cleanup_scenario_networks()
def create_scenario_network(self, name: str, *, internal: bool = False) -> str:
network = f"{CONTAINER_PREFIX}-{self.run_id}-{name}"
command = ["docker", "network", "create"]
if internal:
command.append("--internal")
command.append(network)
run(command, f"create {name} network")
self.current_networks.append(network)
return network
def cleanup_scenario_networks(self) -> None:
for network in reversed(self.current_networks):
subprocess.run(
["docker", "network", "rm", network],
cwd=REPO,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False,
)
self.current_networks = []
def connect_peer_to_network(self, peer: Peer, network: str) -> str:
run(
["docker", "network", "connect", network, peer.container_name],
f"attach {peer.name} to {network}",
)
inspect = run(
[
"docker",
"inspect",
"--format",
f"{{{{(index .NetworkSettings.Networks {json.dumps(network)}).IPAddress}}}}",
peer.container_name,
],
f"inspect {peer.name} address on {network}",
)
address = inspect.stdout.strip()
if not address or peer.ready_addr is None:
raise ScenarioError(f"{peer.name} has no address on {network}")
_, port = peer.ready_addr.rsplit(":", 1)
return f"{address}:{port}"
def peer(
self,
name: str,
*,
network: str = NETWORK,
games_dir: Path | None = None,
readonly_games: bool = False,
tmpfs_size: str | None = None,
fixtures: list[str] | None = None,
extra_args: list[str] | None = None,
catalog_dir: Path | None = None,
) -> Peer:
peer = Peer(
runner=self,
name=name,
network=network,
games_dir=games_dir,
readonly_games=readonly_games,
tmpfs_size=tmpfs_size,
fixtures=fixtures or [],
extra_args=extra_args or [],
catalog_dir=catalog_dir,
).start()
self.current_peers.append(peer)
return peer
def fixture_catalog(
self,
name: str,
game_root: Path,
*,
streamed_install: bool,
) -> Path:
output = self.fixture_root / f"{name}-catalog"
command = ["just", "fixture-catalog", str(output), str(game_root)]
if not streamed_install:
command = [
"just",
"fixture-download-only-catalog",
str(output),
game_root.name,
str(game_root),
]
run(command, f"publish {name} fixture catalog")
return output
def s1_startup_scan(self) -> str:
alpha = self.peer("s1-alpha", games_dir=FIXTURES / "fixture-alpha", readonly_games=True)
games = alpha.list_games()["local"]
expected = {"alienswarm", "bf1942", "ggoo"}
seen = {game["id"] for game in games}
if not expected.issubset(seen):
raise ScenarioError(f"fixture-alpha games missing: expected {expected}, saw {seen}")
for game in games:
if game["id"] in expected:
assert_game_state(game, downloaded=True, installed=False, availability="Ready")
return "fixture-alpha emitted ready local games alienswarm, bf1942, and ggoo"
def s2_direct_connect_handshake(self) -> str:
alpha = self.peer("s2-alpha", games_dir=FIXTURES / "fixture-alpha", readonly_games=True)
bravo = self.peer("s2-bravo", games_dir=FIXTURES / "fixture-bravo", readonly_games=True)
alpha.connect_to(bravo)
bravo.connect_to(alpha)
# Poll for library convergence rather than reading once: wait-peers only
# guarantees peer presence, and the mDNS path can upsert a peer with an
# empty library before its snapshot arrives.
bravo_content_id = catalog_content_id(bravo, "bfbc2")
wait_peer_has_game(alpha, bravo.peer_id, "bfbc2", bravo_content_id)
wait_remote_library_content(
alpha, "bfbc2", bravo_content_id, peer_count=1
)
peers = alpha.list_peers()
if len(peers) != 1 or peers[0]["peer_id"] == alpha.peer_id:
raise ScenarioError(f"bad alpha peers after connect: {peers}")
if peers[0]["game_count"] != 4:
raise ScenarioError(f"expected bravo game_count=4, got {peers}")
# Peer state is pulled by the dialing side; prove each direction with an
# explicit authenticated endpoint.
alpha_content_id = catalog_content_id(alpha, "alienswarm")
bravo_view = wait_peer_has_game(
bravo, alpha.peer_id, "alienswarm", alpha_content_id
)
wait_remote_library_content(
bravo, "alienswarm", alpha_content_id, peer_count=1
)
if bravo_view["game_count"] != 3:
raise ScenarioError(f"expected alpha game_count=3 on bravo, got {bravo_view}")
return "alpha<->bravo exchanged libraries: bravo had 4 games on alpha, alpha had 3 on bravo"
def s3_remote_aggregation(self) -> str:
alpha = self.peer("s3-alpha", games_dir=FIXTURES / "fixture-alpha", readonly_games=True)
bravo = self.peer("s3-bravo", games_dir=FIXTURES / "fixture-bravo", readonly_games=True)
client = self.peer("s3-client")
connect_many(client, [alpha, bravo])
expected_counts = {
"ggoo": 2,
"alienswarm": 1,
"bf1942": 1,
"bfbc2": 1,
"cnc4": 1,
"cnctw": 1,
}
for game_id, peer_count in expected_counts.items():
wait_remote_game(client, game_id, peer_count=peer_count)
return "empty client aggregated alpha/bravo with ggoo peer_count=2 and unique games peer_count=1"
def s4_single_source_download(self) -> str:
bravo = self.peer("s4-bravo", games_dir=FIXTURES / "fixture-bravo", readonly_games=True)
client = self.peer("s4-client")
connect_many(client, [bravo])
content_id = catalog_content_id(bravo, "bfbc2")
wait_peer_has_game(client, bravo.peer_id, "bfbc2", content_id)
wait_remote_library_content(client, "bfbc2", content_id, peer_count=1)
waiter = LineWaiter(len(client.output))
client.send({"cmd": "download", "game_id": "bfbc2", "install": False})
client.wait_for(event_is("download-begin", "bfbc2"), timeout=20, description="begin bfbc2", waiter=waiter)
client.wait_for(event_is("download-finished", "bfbc2"), timeout=60, description="finish bfbc2", waiter=waiter)
assert_chunk_authority(client, "bfbc2", content_id, {bravo.peer_id})
game = wait_local_game(client, "bfbc2", downloaded=True, installed=False)
diff_game_dirs(FIXTURES / "fixture-bravo" / "bfbc2", client.host_games_dir / "bfbc2")
if (client.host_games_dir / "bfbc2" / "local").exists():
raise ScenarioError("bfbc2 local/ exists after install=false")
return (
"bfbc2 exact ContentId matched manifest, raw peer snapshot, remote view, "
f"and every authenticated chunk; install=false state installed={game['installed']}"
)
def s5_auto_install_download(self) -> str:
bravo = self.peer("s5-bravo", games_dir=FIXTURES / "fixture-bravo", readonly_games=True)
client = self.peer("s5-client")
connect_many(client, [bravo])
start = len(client.output)
waiter = LineWaiter(start)
client.send({"cmd": "download", "game_id": "cnctw"})
client.wait_for(event_is("download-finished", "cnctw"), timeout=60, description="finish cnctw", waiter=waiter)
client.wait_for(event_is("install-finished", "cnctw"), timeout=30, description="install cnctw", waiter=waiter)
wait_local_game(client, "cnctw", downloaded=True, installed=True)
if not (client.host_games_dir / "cnctw" / "local" / "fixture-payload.txt").is_file():
raise ScenarioError("cnctw install payload missing")
diff_game_dirs(FIXTURES / "fixture-bravo" / "cnctw", client.host_games_dir / "cnctw")
return "cnctw auto-installed, local fixture payload existed, root diff matched excluding local metadata"
def s6_manual_install_uninstall(self) -> str:
bravo = self.peer("s6-bravo", games_dir=FIXTURES / "fixture-bravo", readonly_games=True)
client = self.peer("s6-client")
connect_many(client, [bravo])
waiter = LineWaiter(len(client.output))
client.send({"cmd": "download", "game_id": "bfbc2", "install": False})
client.wait_for(event_is("download-finished", "bfbc2"), timeout=60, description="finish bfbc2", waiter=waiter)
client.send({"cmd": "install", "game_id": "bfbc2"})
client.wait_for(event_is("install-finished", "bfbc2"), timeout=30, description="install bfbc2", waiter=waiter)
wait_local_game(client, "bfbc2", downloaded=True, installed=True)
client.send({"cmd": "uninstall", "game_id": "bfbc2"})
client.wait_for(event_is("uninstall-finished", "bfbc2"), timeout=30, description="uninstall bfbc2", waiter=waiter)
wait_local_game(client, "bfbc2", downloaded=True, installed=False)
if (client.host_games_dir / "bfbc2" / "local").exists():
raise ScenarioError("bfbc2 local/ remained after uninstall")
diff_game_dirs(FIXTURES / "fixture-bravo" / "bfbc2", client.host_games_dir / "bfbc2")
return "manual install/uninstall toggled installed state, removed local/, preserved downloaded root"
def s7_duplicate_source_download(self) -> str:
alpha = self.peer("s7-alpha", games_dir=FIXTURES / "fixture-alpha", readonly_games=True)
bravo = self.peer("s7-bravo", games_dir=FIXTURES / "fixture-bravo", readonly_games=True)
client = self.peer("s7-client")
connect_many(client, [alpha, bravo])
content_id = catalog_content_id(client, "ggoo")
wait_peer_has_game(client, alpha.peer_id, "ggoo", content_id)
wait_peer_has_game(client, bravo.peer_id, "ggoo", content_id)
wait_remote_library_content(client, "ggoo", content_id, peer_count=2)
wait_remote_game(client, "ggoo", peer_count=2)
waiter = LineWaiter(len(client.output))
client.send({"cmd": "download", "game_id": "ggoo", "install": False})
client.wait_for(event_is("download-finished", "ggoo"), timeout=60, description="finish ggoo", waiter=waiter)
diff_game_dirs(FIXTURES / "fixture-alpha" / "ggoo", client.host_games_dir / "ggoo")
# The fixtures are byte-identical, so the diff alone is source-agnostic.
# Prove the duplicate-source path: download committed exactly once, every
# chunk came from the validated 2-peer set, both peers actually served,
# and nothing was fetched twice.
if count_events(client, "download-finished", "ggoo") != 1:
raise ScenarioError("ggoo did not finish exactly once")
assert_chunk_authority(client, "ggoo", content_id, {alpha.peer_id, bravo.peer_id})
if chunk_sources(client, "ggoo") != {alpha.peer_id, bravo.peer_id}:
raise ScenarioError(
f"expected both validated sources to serve ggoo, got {chunk_sources(client, 'ggoo')}"
)
assert_no_duplicate_chunks(client, "ggoo")
return "ggoo downloaded once, served from both validated sources with no duplicate chunks"
def s8_catalog_file_shape_failover(self) -> str:
game_id = "ggoo"
source_a_dir = self.fixture_root / "s8-source-a"
source_b_dir = self.fixture_root / "s8-source-b"
copy_game(game_id, source_a_dir)
copy_game(game_id, source_b_dir)
source_a = self.peer("s8-source-a", games_dir=source_a_dir)
source_b = self.peer("s8-source-b", games_dir=source_b_dir)
bad, good = sorted(
[source_a, source_b], key=lambda peer: peer_id_sort_key(peer.peer_id)
)
# Production distributes the first catalog entry to the first source in
# authenticated (PeerId, address) order. Corrupt that source only after
# startup so this scenario deterministically exercises archive admission
# failure and retry regardless of Docker address or connection order.
with (bad.host_games_dir / game_id / f"{game_id}.eti").open("ab") as handle:
handle.write(b"conflict")
client = self.peer("s8-client")
connect_many(client, [bad, good])
content_id = catalog_content_id(client, game_id)
wait_peer_has_game(client, bad.peer_id, game_id, content_id)
wait_peer_has_game(client, good.peer_id, game_id, content_id)
wait_remote_library_content(client, game_id, content_id, peer_count=2)
wait_remote_game(client, game_id, peer_count=2, version=CATALOG_VERSIONS[game_id])
start = len(client.output)
waiter = LineWaiter(start)
client.send({"cmd": "download", "game_id": game_id, "install": False})
client.wait_for(
event_is("download-finished", game_id),
timeout=60,
description="ggoo oversized-source failover",
waiter=waiter,
)
assert_download_status_trace(
client,
game_id,
start=start,
terminal="download-finished",
)
# The bad source's version.ini is still exact catalog content and is a
# valid successful chunk. Only its oversized archive must be rejected.
assert_chunk_authority(client, game_id, content_id, {bad.peer_id, good.peer_id})
archive_totals = chunk_totals(client, game_id, f"{game_id}.eti")
if set(archive_totals) != {good.peer_id}:
raise ScenarioError(
"oversized source supplied a successful archive chunk instead of failing over: "
f"{archive_totals}"
)
assert_no_duplicate_chunks(client, game_id)
diff_game_dirs(good.host_games_dir / game_id, client.host_games_dir / game_id)
bad_only = self.peer("s8-oversized-only-client")
connect_many(bad_only, [bad])
wait_peer_has_game(bad_only, bad.peer_id, game_id, content_id)
wait_remote_library_content(bad_only, game_id, content_id, peer_count=1)
wait_remote_game(bad_only, game_id, peer_count=1)
failed_start = len(bad_only.output)
failed_waiter = LineWaiter(failed_start)
bad_only.send({"cmd": "download", "game_id": game_id, "install": False})
bad_only.wait_for(
event_is("download-failed", game_id),
timeout=60,
description="oversized-only ggoo failure",
waiter=failed_waiter,
)
assert_download_status_trace(
bad_only,
game_id,
start=failed_start,
terminal="download-failed",
reason="verified-catalog-sources-exhausted",
)
assert_no_event_since(bad_only, failed_start, "download-finished", game_id)
assert_local_absent(bad_only, game_id)
assert_not_exists(bad_only.host_games_dir / game_id / "version.ini")
assert_not_exists(bad_only.host_games_dir / game_id / "local")
return (
"oversized source failed exact catalog-file admission and the honest source "
"completed; an oversized-only receiver published no version.ini or local/"
)
def s9_missing_game(self) -> str:
game_id = "cod6"
client = self.peer("s9-client")
start = len(client.output)
waiter = LineWaiter(start)
queued = client.send(
{"cmd": "download", "game_id": game_id, "install": False}
)
if queued.get("data", {}).get("queued") is not True:
raise ScenarioError(f"missing-source download was not queued: {queued}")
client.wait_for(
event_is("download-failed", game_id),
timeout=20,
description="asynchronous no-source download failure",
waiter=waiter,
)
assert_download_status_trace(
client,
game_id,
start=start,
terminal="download-failed",
reason="verified-catalog-sources-exhausted",
expect_begin=False,
expect_verification=False,
)
assert_no_event_since(client, start, "download-begin", game_id)
assert_no_event_since(client, start, "download-finished", game_id)
active_events = [
item
for item in client.output[start:]
if item.get("type") == "event"
and item.get("event") == "active-operations-changed"
and any(
operation.get("game_id") == game_id
for operation in item.get("data", {}).get("active_operations", [])
)
]
if active_events:
raise ScenarioError(
f"missing-source download entered active operations: {active_events}"
)
assert_no_active(client, game_id)
assert_local_absent(client, game_id)
game_root = client.host_games_dir / game_id
assert_not_exists(game_root / "version.ini")
assert_not_exists(game_root / "local")
assert_not_exists(game_root)
return (
"missing-source command queued, then emitted download-failed without begin, "
"active-operation, version.ini, local/, or game-root mutation"
)
def s10_shutdown_cleanup(self) -> str:
alpha = self.peer("s10-alpha", games_dir=FIXTURES / "fixture-alpha", readonly_games=True)
bravo = self.peer("s10-bravo", games_dir=FIXTURES / "fixture-bravo", readonly_games=True)
connect_bidirectional(alpha, bravo)
wait_remote_game(alpha, "bfbc2", peer_count=1)
bravo.shutdown()
wait_departure_topology(
alpha,
expected_peer_ids=set(),
expected_remote_game_ids=set(),
phase="S10 bravo shutdown",
)
return (
"bravo shutdown converged through pinned liveness to an exact empty peer "
"set and remote-library view"
)
def s11_same_identity_reconnect(self) -> str:
alpha = self.peer("s11-alpha", games_dir=FIXTURES / "fixture-alpha", readonly_games=True)
bravo_dir = FIXTURES / "fixture-bravo"
bravo = self.peer("s11-bravo", games_dir=bravo_dir, readonly_games=True)
connect_bidirectional(alpha, bravo)
wait_remote_game(alpha, "bfbc2", peer_count=1)
first_peer = alpha.list_peers()[0]
first_addr = first_peer["addr"]
first_id = first_peer["peer_id"]
bravo.shutdown()
wait_departure_topology(
alpha,
expected_peer_ids=set(),
expected_remote_game_ids=set(),
phase="S11 old bravo generation shutdown",
)
bravo = self.peer("s11-bravo", games_dir=bravo_dir, readonly_games=True)
connect_bidirectional(alpha, bravo)
wait_remote_game(alpha, "bfbc2", peer_count=1)
peers = alpha.list_peers()
# The real invariant is a single peer entry reusing the same identity (no
# duplicate). The listener address is an OS-assigned ephemeral port: it
# almost always differs across restarts, but asserting it MUST change
# tests the kernel's port allocator, not the peer, and can fail spuriously
# if the same port is reused. So we only require the identity to be stable.
if len(peers) != 1:
raise ScenarioError(f"expected one bravo peer after reconnect, got {peers}")
if peers[0]["peer_id"] != first_id:
raise ScenarioError(f"bravo peer id changed: {first_id} -> {peers[0]['peer_id']}")
changed = "new" if peers[0]["addr"] != first_addr else "same"
return f"bravo reused peer id {first_id} as a single entry at {changed} address {peers[0]['addr']}"
def s12_transfer_serving_gates(self) -> str:
output = run_just_test()
required = [
"wrong_identity_path_or_range_never_authorizes_an_entry",
"chunk_boundaries_are_exact_including_empty_files",
"admission_rejects_every_ineligible_v8_case_before_transfer_registration",
"stream_install_admission_separates_identity_capability_and_valid_payload",
]
missing = [name for name in required if f"{name} ... ok" not in output]
if missing:
raise ScenarioError(f"S12 unit proof tests did not run-and-pass: {missing}")
return "just test passed including catalog/sentinel/active/local-path serve gate tests"
def s13_exact_transfer_equality(self) -> str:
bravo = self.peer("s13-bravo", games_dir=FIXTURES / "fixture-bravo", readonly_games=True)
alpha = self.peer("s13-alpha", games_dir=FIXTURES / "fixture-alpha", readonly_games=True)
client = self.peer("s13-client")
connect_many(client, [bravo, alpha])
waiter = LineWaiter(len(client.output))
client.send({"cmd": "download", "game_id": "bfbc2", "install": False})
client.wait_for(event_is("download-finished", "bfbc2"), timeout=60, description="bfbc2 finish", waiter=waiter)
client.send({"cmd": "download", "game_id": "alienswarm", "install": False})
client.wait_for(event_is("download-finished", "alienswarm"), timeout=90, description="alienswarm finish", waiter=waiter)
diff_game_dirs(FIXTURES / "fixture-bravo" / "bfbc2", client.host_games_dir / "bfbc2")
diff_game_dirs(FIXTURES / "fixture-alpha" / "alienswarm", client.host_games_dir / "alienswarm")
return "small bfbc2 and large alienswarm transfers both diffed cleanly against sources"
def s14_large_multi_peer_chunking(self) -> str:
game_id = PERF_GAME_ID
source_dir = self.fixture_root / "s14-alpha"
# Four 128 MiB chunks so the balance assertion is meaningful: with two
# peers a fair split is 2+2 chunks (diff 0) and a 3+1 imbalance would
# exceed one CHUNK_SIZE. A 2-chunk file could never trip the check.
file_size = CHUNK_SIZE * 4
create_large_sparse_game(source_dir / game_id, size=file_size)
catalog_dir = self.fixture_catalog(
"s14", source_dir / game_id, streamed_install=False
)
alpha = self.peer("s14-alpha", games_dir=source_dir, catalog_dir=catalog_dir)
stage = self.peer("s14-stage", catalog_dir=catalog_dir)
connect_many(stage, [alpha])
waiter = LineWaiter(len(stage.output))
stage.send({"cmd": "download", "game_id": game_id, "install": False})
stage.wait_for(event_is("download-finished", game_id), timeout=180, description="stage finish", waiter=waiter)
diff_game_dirs(source_dir / game_id, stage.host_games_dir / game_id)
client = self.peer("s14-client", catalog_dir=catalog_dir)
connect_many(client, [alpha, stage])
wait_remote_game(client, game_id, peer_count=2, version=PERF_GAME_VERSION)
start = len(client.output)
waiter = LineWaiter(start)
client.send({"cmd": "download", "game_id": game_id, "install": False})
client.wait_for(event_is("download-finished", game_id), timeout=180, description="client finish", waiter=waiter)
diff_game_dirs(source_dir / game_id, client.host_games_dir / game_id)
totals = chunk_totals(client, game_id, f"{game_id}.eti")
if len(totals) != 2:
raise ScenarioError(f"expected .eti chunks from exactly two peers, got {totals}")
if sum(totals.values()) != file_size:
raise ScenarioError(f"chunk bytes {sum(totals.values())} != file size {file_size}: {totals}")
values = list(totals.values())
if max(values) - min(values) > CHUNK_SIZE:
raise ScenarioError(f"chunk totals not balanced within one chunk: {totals}")
return f"{game_id} ({file_size // (1024 * 1024)} MiB) split across two sources, balanced, diff matched: {totals}"
def s15_three_way_version_skew(self) -> str:
stale_dir = self.fixture_root / "s15-a"
wrong_content_dir = self.fixture_root / "s15-b"
exact_dir = self.fixture_root / "s15-c"
copy_game("cnc4", stale_dir, version="20150101")
copy_game("cnc4", wrong_content_dir, version=CATALOG_VERSIONS["cnc4"])
with (wrong_content_dir / "cnc4" / "cnc4.eti").open("ab") as archive:
archive.write(b"wrong-content-id")
wrong_catalog = self.fixture_catalog(
"s15-wrong-content",
wrong_content_dir / "cnc4",
streamed_install=False,
)
copy_game("cnc4", exact_dir, version=CATALOG_VERSIONS["cnc4"])
peers = [
self.peer("s15-a", games_dir=stale_dir),
self.peer(
"s15-b", games_dir=wrong_content_dir, catalog_dir=wrong_catalog
),
self.peer("s15-c", games_dir=exact_dir),
]
client = self.peer("s15-client")
connect_many(client, peers)
expected_content_id = catalog_content_id(client, "cnc4")
wrong_content_id = catalog_content_id(peers[1], "cnc4")
if wrong_content_id == expected_content_id:
raise ScenarioError("modified s15 fixture did not change ContentId")
wait_peer_without_game(client, peers[0].peer_id, "cnc4")
wait_peer_has_game(client, peers[1].peer_id, "cnc4", wrong_content_id)
wait_peer_has_game(client, peers[2].peer_id, "cnc4", expected_content_id)
wait_remote_library_content(
client, "cnc4", wrong_content_id, peer_count=1
)
wait_remote_library_content(
client, "cnc4", expected_content_id, peer_count=1
)
wait_remote_game(client, "cnc4", peer_count=1, version=CATALOG_VERSIONS["cnc4"])
waiter = LineWaiter(len(client.output))
client.send({"cmd": "download", "game_id": "cnc4", "install": False})
client.wait_for(event_is("download-finished", "cnc4"), timeout=60, description="cnc4 finish", waiter=waiter)
assert_chunk_authority(
client, "cnc4", expected_content_id, {peers[2].peer_id}
)
diff_game_dirs(peers[2].host_games_dir / "cnc4", client.host_games_dir / "cnc4")
return (
"stale-version peer advertised nothing; wrong-ContentId peer stayed in the raw "
"view but was excluded; every verified chunk came from the exact catalog peer"
)
def s16_catalog_fanout_with_stale(self) -> str:
specs = [
("s16-a", "20180101"),
("s16-b", CATALOG_VERSIONS["alienswarm"]),
("s16-c", CATALOG_VERSIONS["alienswarm"]),
]
peer_dirs = []
for name, version in specs:
game_dir = self.fixture_root / name
copy_game("alienswarm", game_dir, version=version)
# Two 128 MiB chunks so the .eti can actually fan out across the two
# catalog-version peers; the stock 120 MiB fixture is a single chunk
# that can only ever come from one source.
inflate_archive_sparse(game_dir / "alienswarm", "alienswarm", CHUNK_SIZE * 2)
peer_dirs.append((name, game_dir))
catalog_dir = self.fixture_catalog(
"s16", peer_dirs[1][1] / "alienswarm", streamed_install=False
)
peers = [
self.peer(name, games_dir=game_dir, catalog_dir=catalog_dir)
for name, game_dir in peer_dirs
]
client = self.peer("s16-client", catalog_dir=catalog_dir)
connect_many(client, peers)
wait_remote_game(client, "alienswarm", peer_count=2, version=CATALOG_VERSIONS["alienswarm"])
waiter = LineWaiter(len(client.output))
client.send({"cmd": "download", "game_id": "alienswarm", "install": False})
client.wait_for(event_is("download-finished", "alienswarm"), timeout=180, description="alienswarm finish", waiter=waiter)
assert_only_chunk_sources(client, "alienswarm", {peers[1].peer_id, peers[2].peer_id})
totals = chunk_totals(client, "alienswarm", "alienswarm.eti")
if peers[0].peer_id in totals:
raise ScenarioError(f"stale peer contributed chunks: {totals}")
if set(totals) != {peers[1].peer_id, peers[2].peer_id}:
raise ScenarioError(f"expected .eti to fan out across both B and C, got {totals}")
diff_game_dirs(peers[1].host_games_dir / "alienswarm", client.host_games_dir / "alienswarm")
return f"catalog-version B/C peers split alienswarm.eti while stale A contributed zero; totals={totals}"
def s17_catalog_byte_quarantine(self) -> str:
game_id = "cnc4"
source_a_dir = self.fixture_root / "s17-source-a"
source_b_dir = self.fixture_root / "s17-source-b"
copy_game(game_id, source_a_dir)
copy_game(game_id, source_b_dir)
source_a = self.peer("s17-source-a", games_dir=source_a_dir)
source_b = self.peer("s17-source-b", games_dir=source_b_dir)
bad, good = sorted(
[source_a, source_b], key=lambda peer: peer_id_sort_key(peer.peer_id)
)
corrupt_file_same_length(bad.host_games_dir / game_id / f"{game_id}.eti")
client = self.peer("s17-client")
connect_many(client, [bad, good])
content_id = catalog_content_id(client, game_id)
wait_peer_has_game(client, bad.peer_id, game_id, content_id)
wait_peer_has_game(client, good.peer_id, game_id, content_id)
wait_remote_library_content(client, game_id, content_id, peer_count=2)
wait_remote_game(client, "cnc4", peer_count=2, version=CATALOG_VERSIONS["cnc4"])
start = len(client.output)
waiter = LineWaiter(start)
client.send({"cmd": "download", "game_id": game_id, "install": False})
client.wait_for(
event_is("download-finished", game_id),
timeout=60,
description="cnc4 retry after corrupt source",
waiter=waiter,
)
assert_download_status_trace(
client,
game_id,
start=start,
terminal="download-finished",
expect_invalid_source_retry=True,
)
assert_chunk_authority(client, game_id, content_id, {bad.peer_id, good.peer_id})
archive_totals = chunk_totals(client, game_id, f"{game_id}.eti")
if set(archive_totals) != {good.peer_id}:
raise ScenarioError(
"corrupt source supplied a successful archive chunk instead of being quarantined: "
f"{archive_totals}"
)
assert_no_duplicate_chunks(client, game_id)
diff_game_dirs(good.host_games_dir / game_id, client.host_games_dir / game_id)
# A fresh receiver has no runtime quarantine history. With only the
# same-length corrupt source available, catalog verification must fail
# and must not publish either the sentinel or local install state.
bad_only = self.peer("s17-bad-only-client")
connect_many(bad_only, [bad])
wait_peer_has_game(bad_only, bad.peer_id, game_id, content_id)
wait_remote_library_content(bad_only, game_id, content_id, peer_count=1)
wait_remote_game(bad_only, game_id, peer_count=1)
failed_start = len(bad_only.output)
failed_waiter = LineWaiter(failed_start)
bad_only.send({"cmd": "download", "game_id": game_id, "install": False})
bad_only.wait_for(
event_is("download-failed", game_id),
timeout=60,
description="all-bad cnc4 catalog failure",
waiter=failed_waiter,
)
assert_download_status_trace(
bad_only,
game_id,
start=failed_start,
terminal="download-failed",
reason="verified-catalog-sources-exhausted",
)
assert_no_event_since(bad_only, failed_start, "download-finished", game_id)
assert_local_absent(bad_only, game_id)
assert_not_exists(bad_only.host_games_dir / game_id / "version.ini")
assert_not_exists(bad_only.host_games_dir / game_id / "local")
return (
"same-length corrupt source was quarantined and retried via the honest source; "
"a fresh all-bad receiver failed without version.ini or local/"
)
def s18_redundant_source_drop(self) -> str:
game_id = "bf1942"
source_a_dir = self.fixture_root / "s18-a"
source_b_dir = self.fixture_root / "s18-b"
# Multi-chunk sparse archive so BOTH peers have more assigned .eti chunks
# than the per-peer stream window; a single-window file could finish before
# the force-kill lands. We verify the download SURVIVES a mid-download
# source kill (every byte still arrives, no download-failed, diff matches).
# Waiting for the lower-ID source to fill the stream window makes the
# interruption non-vacuous; the final lower-ID-planned offset must then be
# completed by the survivor. Sparse zero bytes are identical, so both
# sources match the one publisher-derived catalog authority.
file_size = CHUNK_SIZE * 16
create_large_sparse_game(source_a_dir / game_id, size=file_size)
create_large_sparse_game(source_b_dir / game_id, size=file_size)
catalog_dir = self.fixture_catalog(
"s18", source_a_dir / game_id, streamed_install=False
)
source_a = self.peer("s18-a", games_dir=source_a_dir, catalog_dir=catalog_dir)
source_b = self.peer("s18-b", games_dir=source_b_dir, catalog_dir=catalog_dir)
client = self.peer("s18-client", catalog_dir=catalog_dir)
connect_many(client, [source_a, source_b])
content_id = catalog_content_id(client, game_id)
wait_peer_has_game(client, source_a.peer_id, game_id, content_id)
wait_peer_has_game(client, source_b.peer_id, game_id, content_id)
wait_remote_library_content(client, game_id, content_id, peer_count=2)
wait_remote_game(client, game_id, peer_count=2)
killed, survivor = sorted(
[source_a, source_b], key=lambda peer: peer_id_sort_key(peer.peer_id)
)
start = len(client.output)
waiter = LineWaiter(start)
client.send({"cmd": "download", "game_id": game_id, "install": False})
client.wait_for(event_is("download-begin", game_id), timeout=20, description="download begin", waiter=waiter)
active_streams = wait_outbound_transfer(
killed, game_id, minimum=4, timeout=30
)
assert_no_event_since(client, start, "download-finished", game_id)
assert_no_event_since(client, start, "download-failed", game_id)
killed.kill()
client.wait_for(event_is("download-finished", game_id), timeout=180, description="download finish", waiter=waiter)
assert_download_status_trace(
client,
game_id,
start=start,
terminal="download-finished",
)
# Scan the WHOLE download window (start..) for download-failed. The old
# assert_no_event reused a waiter already advanced past download-finished,
# so it only saw the empty tail and could never fire.
assert_no_event_since(client, start, "download-failed", game_id)
diff_game_dirs(survivor.host_games_dir / game_id, client.host_games_dir / game_id)
assert_chunk_authority(
client, game_id, content_id, {source_a.peer_id, source_b.peer_id}
)
totals = chunk_totals(client, game_id, f"{game_id}.eti")
survivor_id = survivor.peer_id
if survivor_id is None:
raise ScenarioError("surviving source has no authenticated PeerId")
if survivor_id not in totals:
raise ScenarioError(f"surviving source served no .eti chunks: {totals}")
if sum(totals.values()) != file_size:
raise ScenarioError(f"download did not deliver the whole archive ({sum(totals.values())} != {file_size}): {totals}")
# Exact sources are ordered by PeerId. The lower-ID peer receives every
# even archive chunk, so this last even offset was originally assigned to
# `killed`; seeing the survivor complete it proves retry, not just its own
# initial half of the plan.
retried_offset = file_size - (2 * CHUNK_SIZE)
if not any(
item.get("type") == "event"
and item.get("event") == "download-chunk-finished"
and item.get("data", {}).get("game_id") == game_id
and item["data"].get("relative_path") == f"{game_id}.eti"
and item["data"].get("offset") == retried_offset
and item["data"].get("peer_id") == survivor_id
for item in client.output[start:]
):
raise ScenarioError(
"survivor did not retry the killed source's final planned archive chunk: "
f"offset={retried_offset} totals={totals}"
)
# We deliberately do NOT assert the exact per-source split. The killed
# source can serve a chunk or two before `docker rm -f` lands (a fast-LAN
# race), so requiring totals == {survivor: file_size} would be flaky. The
# robust proof of redundancy is that a source died mid-download yet every
# byte still arrived (sum == file_size plus the diff), the survivor served
# part of it, and no download-failed was emitted.
survivor_bytes = totals[survivor_id]
return (
f"lower-PeerId source killed with {active_streams} outbound streams; "
f"all {file_size} bytes delivered "
f"({survivor_bytes} from the survivor), no download-failed; diff matched; bytes={totals}"
)
def s19_sole_source_drop(self) -> str:
game_id = "bf1942"
source_dir = self.fixture_root / "s19-source"
# More chunks than the per-peer stream window ensure pending work remains
# when the force-kill interrupts four positively observed active streams.
create_large_sparse_game(source_dir / game_id, size=CHUNK_SIZE * 8)
catalog_dir = self.fixture_catalog(
"s19", source_dir / game_id, streamed_install=False
)
source = self.peer("s19-source", games_dir=source_dir, catalog_dir=catalog_dir)
client = self.peer("s19-client", catalog_dir=catalog_dir)
connect_many(client, [source])
wait_remote_game(client, game_id, peer_count=1)
start = len(client.output)
waiter = LineWaiter(start)
client.send({"cmd": "download", "game_id": game_id, "install": False})
client.wait_for(event_is("download-begin", game_id), timeout=20, description="download begin", waiter=waiter)
active_streams = wait_outbound_transfer(
source, game_id, minimum=4, timeout=30
)
assert_no_event_since(client, start, "download-finished", game_id)
assert_no_event_since(client, start, "download-failed", game_id)
# Forceful kill drops positively active streams without a departure signal.
source.kill()
terminal = client.wait_for(
event_is("download-failed", game_id),
timeout=120,
description="sole-source drop terminal failure",
waiter=waiter,
)
assert_download_status_trace(
client,
game_id,
start=start,
terminal="download-failed",
reason="verified-catalog-sources-exhausted",
)
assert_no_event_since(client, start, "download-finished", game_id)
assert_not_exists(client.host_games_dir / game_id / "version.ini")
wait_no_active(client, game_id)
assert_local_absent(client, game_id)
return (
f"sole-source forceful drop with {active_streams} outbound streams -> "
f"{terminal['event']}; version.ini absent; no ready row; no active op"
)
def s20_receiver_write_failure(self) -> str:
source_dir = self.fixture_root / "s20-source"
copy_game("alienswarm", source_dir)
source = self.peer("s20-source", games_dir=source_dir)
client = self.peer("s20-client", tmpfs_size="32m")
connect_many(client, [source])
wait_remote_game(client, "alienswarm", peer_count=1)
start = len(client.output)
waiter = LineWaiter(start)
client.send({"cmd": "download", "game_id": "alienswarm", "install": False})
client.wait_for(event_is("download-failed", "alienswarm"), timeout=90, description="download failed", waiter=waiter)
assert_download_status_trace(
client,
"alienswarm",
start=start,
terminal="download-failed",
reason="operation-failed",
)
client.docker_exec("test", "!", "-e", "/games/alienswarm/version.ini")
assert_no_active(client, "alienswarm")
return "32m tmpfs receiver emitted download-failed; /games/alienswarm/version.ini absent; active operations empty"
def s21_add_game_propagation(self) -> str:
alpha = self.peer("s21-alpha")
bravo_dir = self.fixture_root / "s21-bravo"
bravo_dir.mkdir(parents=True, exist_ok=True)
bravo = self.peer("s21-bravo", games_dir=bravo_dir)
connect_bidirectional(alpha, bravo)
assert len(alpha.list_peers()) == 1
stage_game_drop(bravo_dir, "cod5")
game = wait_remote_game(alpha, "cod5", peer_count=1)
return f"alpha saw {game['id']} from the existing bravo peer with peer_count={game['peer_count']}"
def s22_remove_game_propagation(self) -> str:
alpha = self.peer("s22-alpha")
bravo_dir = self.fixture_root / "s22-bravo"
copy_game("cod5", bravo_dir)
bravo = self.peer("s22-bravo", games_dir=bravo_dir)
connect_bidirectional(alpha, bravo)
wait_remote_game(alpha, "cod5", peer_count=1)
shutil.rmtree(bravo_dir / "cod5")
wait_remote_absent(alpha, "cod5")
peers = alpha.list_peers()
if len(peers) != 1:
raise ScenarioError(f"expected bravo peer to remain, got {peers}")
return "alpha removed cod5 while keeping one bravo peer"
def s23_version_bump_propagation(self) -> str:
alpha = self.peer("s23-alpha")
bravo_dir = self.fixture_root / "s23-bravo"
copy_game("cnc4", bravo_dir, version="20160101")
bravo = self.peer("s23-bravo", games_dir=bravo_dir)
connect_bidirectional(alpha, bravo)
wait_remote_absent(alpha, "cnc4", timeout=5)
(bravo_dir / "cnc4" / "version.ini").write_text(CATALOG_VERSIONS["cnc4"], encoding="utf-8")
wait_remote_game(alpha, "cnc4", peer_count=1, version=CATALOG_VERSIONS["cnc4"])
return "alpha observed stale cnc4 become catalog-version downloadable without reconnect"
def s24_two_clients_one_source(self) -> str:
source = self.peer("s24-alpha", games_dir=FIXTURES / "fixture-alpha", readonly_games=True)
c1 = self.peer("s24-client-a")
c2 = self.peer("s24-client-b")
connect_many(c1, [source])
connect_many(c2, [source])
waiter1 = LineWaiter(len(c1.output))
waiter2 = LineWaiter(len(c2.output))
c1.send({"cmd": "download", "game_id": "alienswarm", "install": False})
c2.send({"cmd": "download", "game_id": "alienswarm", "install": False})
c1.wait_for(event_is("download-finished", "alienswarm"), timeout=90, description="client-a finish", waiter=waiter1)
c2.wait_for(event_is("download-finished", "alienswarm"), timeout=90, description="client-b finish", waiter=waiter2)
diff_game_dirs(FIXTURES / "fixture-alpha" / "alienswarm", c1.host_games_dir / "alienswarm")
diff_game_dirs(FIXTURES / "fixture-alpha" / "alienswarm", c2.host_games_dir / "alienswarm")
wait_local_game(c1, "alienswarm", downloaded=True, installed=False)
wait_local_game(c2, "alienswarm", downloaded=True, installed=False)
# Responsiveness: the source still answers and still advertises its games.
if not any(g["id"] == "alienswarm" for g in source.list_games()["local"]):
raise ScenarioError("source no longer advertises alienswarm after serving two clients")
return "two concurrent clients finished alienswarm install=false; both diffs matched; source still responsive"
def s25_two_downloads_one_client(self) -> str:
source = self.peer("s25-bravo", games_dir=FIXTURES / "fixture-bravo", readonly_games=True)
client = self.peer("s25-client")
connect_many(client, [source])
start = len(client.output)
bfbc2_waiter = LineWaiter(start)
cnctw_waiter = LineWaiter(start)
client.send({"cmd": "download", "game_id": "bfbc2", "install": False})
client.send({"cmd": "download", "game_id": "cnctw", "install": False})
client.wait_for(
event_is("download-finished", "bfbc2"),
timeout=60,
description="bfbc2 finish",
waiter=bfbc2_waiter,
)
client.wait_for(
event_is("download-finished", "cnctw"),
timeout=60,
description="cnctw finish",
waiter=cnctw_waiter,
)
diff_game_dirs(FIXTURES / "fixture-bravo" / "bfbc2", client.host_games_dir / "bfbc2")
diff_game_dirs(FIXTURES / "fixture-bravo" / "cnctw", client.host_games_dir / "cnctw")
wait_local_game(client, "bfbc2", downloaded=True, installed=False)
wait_local_game(client, "cnctw", downloaded=True, installed=False)
return "bfbc2 and cnctw concurrent downloads both finished install=false and diffed cleanly"
def s26_duplicate_download_rejection(self) -> str:
game_id = "bf1942"
source_dir = self.fixture_root / "s26-source"
# A large sparse archive keeps the first download active long enough that
# the duplicate request is guaranteed to race against an in-progress
# operation rather than a near-instant 3 MB transfer that may already be
# finished by the time the second command is read.
create_large_sparse_game(source_dir / game_id, size=CHUNK_SIZE * 2)
catalog_dir = self.fixture_catalog(
"s26", source_dir / game_id, streamed_install=False
)
source = self.peer("s26-source", games_dir=source_dir, catalog_dir=catalog_dir)
client = self.peer("s26-client", catalog_dir=catalog_dir)
connect_many(client, [source])
wait_remote_game(client, game_id, peer_count=1)
waiter = LineWaiter(len(client.output))
client.send({"cmd": "download", "game_id": game_id, "install": False})
active = client.wait_for(
lambda item: item.get("type") == "event"
and item.get("event") == "active-operations-changed"
and any(
op.get("game_id") == game_id
for op in item.get("data", {}).get("active_operations", [])
),
timeout=20,
description="active operation",
waiter=waiter,
)
# Verify the operation kind, not just its presence (the only scenario that
# inspects the active_operations 'operation' field).
op = next(o for o in active["data"]["active_operations"] if o.get("game_id") == game_id)
if op.get("operation") != "Downloading":
raise ScenarioError(f"expected Downloading active operation, got {op}")
err = client.send(
{"cmd": "download", "game_id": game_id, "install": False},
expect_error=True,
)
if "operation already in progress" not in err["error"]:
raise ScenarioError(f"unexpected duplicate error: {err}")
client.wait_for(event_is("download-finished", game_id), timeout=180, description="first download finish", waiter=waiter)
diff_game_dirs(source_dir / game_id, client.host_games_dir / game_id)
return f"duplicate rejected while Downloading active ('{err['error']}'); first download diff matched"
def s27_self_connect_rejection(self) -> str:
alpha = self.peer("s27-alpha")
err = alpha.send(
{"cmd": "connect", **alpha.ready_endpoint()}, expect_error=True
)
if "cannot connect peer to itself" not in err["error"]:
raise ScenarioError(f"unexpected self-connect error: {err}")
peers = alpha.list_peers()
if peers:
raise ScenarioError(f"self-connect created peers: {peers}")
alpha.status()
return f"self-connect errored '{err['error']}'; peer list stayed empty"
def s28_reconnect_generation_unit(self) -> str:
output = run_just_test()
test_name = (
"peer_db::tests::"
"reconnect_gets_new_generation_and_stale_removal_loses_authority"
)
if f"{test_name} ... ok" not in output:
raise ScenarioError(f"S28 unit proof did not run-and-pass in just test output:\n{output}")
return (
"`just test` passed including reconnect generation fencing against "
"stale removal"
)
def s29_empty_peer_participates(self) -> str:
source_network = self.create_scenario_network(
"s29-source", internal=True
)
source = self.peer(
"s29-alpha",
network=source_network,
games_dir=FIXTURES / "fixture-alpha",
readonly_games=True,
)
empty = self.peer("s29-empty")
observer = self.peer("s29-observer")
connect_bidirectional(observer, empty)
peers = observer.list_peers()
if (
{peer.get("peer_id") for peer in peers} != {empty.peer_id}
or len(peers) != 1
or peers[0]["game_count"] != 0
):
raise ScenarioError(f"expected empty peer with zero games, got {peers}")
self.connect_peer_to_network(empty, source_network)
empty.connect_to(source)
content_id = catalog_content_id(source, "alienswarm")
wait_peer_has_game(empty, source.peer_id, "alienswarm", content_id)
wait_remote_library_content(empty, "alienswarm", content_id, peer_count=1)
wait_remote_game(empty, "alienswarm", peer_count=1)
waiter = LineWaiter(len(empty.output))
empty.send({"cmd": "download", "game_id": "alienswarm", "install": False})
empty.wait_for(event_is("download-finished", "alienswarm"), timeout=90, description="empty download finish", waiter=waiter)
assert_chunk_authority(empty, "alienswarm", content_id, {source.peer_id})
diff_game_dirs(FIXTURES / "fixture-alpha" / "alienswarm", empty.host_games_dir / "alienswarm")
wait_peer_has_game(observer, empty.peer_id, "alienswarm", content_id)
wait_remote_library_content(observer, "alienswarm", content_id, peer_count=1)
wait_remote_game(observer, "alienswarm", peer_count=1)
assert_exact_peer_ids(observer, {empty.peer_id})
return (
"observer remained authenticated only to the formerly empty peer, then saw "
"its exact alienswarm ContentId with peer_count=1 after the peer downloaded "
"directly from an isolated source; raw snapshot, remote view, and chunks agreed"
)
def s30_mesh_aggregation(self) -> str:
dirs = []
specs = [
("s30-a", [("ggoo", CATALOG_VERSIONS["ggoo"]), ("bf1942", CATALOG_VERSIONS["bf1942"])]),
("s30-b", [("ggoo", CATALOG_VERSIONS["ggoo"]), ("cnc4", CATALOG_VERSIONS["cnc4"])]),
("s30-c", [("cnc4", CATALOG_VERSIONS["cnc4"]), ("cod5", CATALOG_VERSIONS["cod5"])]),
("s30-d", [("cnctw", CATALOG_VERSIONS["cnctw"]), ("coh", CATALOG_VERSIONS["coh"])]),
("s30-e", [("cnctw", CATALOG_VERSIONS["cnctw"]), ("bf1942", CATALOG_VERSIONS["bf1942"])]),
]
peers = []
for name, games in specs:
game_dir = self.fixture_root / name
for game_id, version in games:
copy_game(game_id, game_dir, version=version)
dirs.append(game_dir)
peers.append(self.peer(name, games_dir=game_dir))
client = self.peer("s30-client")
connect_many(client, peers)
expected = {
"ggoo": (2, CATALOG_VERSIONS["ggoo"]),
"bf1942": (2, CATALOG_VERSIONS["bf1942"]),
"cnc4": (2, CATALOG_VERSIONS["cnc4"]),
"cod5": (1, CATALOG_VERSIONS["cod5"]),
"cnctw": (2, CATALOG_VERSIONS["cnctw"]),
"coh": (1, CATALOG_VERSIONS["coh"]),
}
for game_id, (peer_count, version) in expected.items():
wait_remote_game(client, game_id, peer_count=peer_count, version=version)
game_rows = client.list_games()["remote"]
ids = [game["id"] for game in game_rows]
if len(ids) != len(set(ids)):
raise ScenarioError(f"duplicate game rows: {ids}")
if any(peer["peer_id"] == client.peer_id for peer in client.list_peers()):
raise ScenarioError("client listed itself as a peer")
return f"client aggregated {len(expected)} IDs from 5 peers with expected peer_count/catalog versions"
def s31_bootstrapped_peer_source(self) -> str:
game_id = "alienswarm"
source = self.peer("s31-alpha", games_dir=FIXTURES / "fixture-alpha", readonly_games=True)
bootstrap = self.peer("s31-bootstrap")
connect_many(bootstrap, [source])
content_id = catalog_content_id(bootstrap, game_id)
wait_peer_has_game(bootstrap, source.peer_id, game_id, content_id)
waiter = LineWaiter(len(bootstrap.output))
bootstrap.send({"cmd": "download", "game_id": game_id, "install": False})
bootstrap.wait_for(event_is("download-finished", game_id), timeout=90, description="bootstrap finish", waiter=waiter)
assert_chunk_authority(bootstrap, game_id, content_id, {source.peer_id})
diff_game_dirs(FIXTURES / "fixture-alpha" / game_id, bootstrap.host_games_dir / game_id)
source.kill()
third = self.peer("s31-third")
connect_many(third, [bootstrap])
wait_peer_has_game(third, bootstrap.peer_id, game_id, content_id)
wait_remote_library_content(third, game_id, content_id, peer_count=1)
assert_exact_peer_ids(third, {bootstrap.peer_id})
waiter = LineWaiter(len(third.output))
third.send({"cmd": "download", "game_id": game_id, "install": False})
third.wait_for(event_is("download-finished", game_id), timeout=90, description="third finish", waiter=waiter)
assert_chunk_authority(third, game_id, content_id, {bootstrap.peer_id})
diff_game_dirs(FIXTURES / "fixture-alpha" / game_id, third.host_games_dir / game_id)
return (
"bootstrap downloaded exact content from the original source, then served "
"every verified chunk to a third peer after the original source was killed; "
"diff matched original"
)
def s32_reinstall_after_uninstall(self) -> str:
source = self.peer("s32-bravo", games_dir=FIXTURES / "fixture-bravo", readonly_games=True)
client = self.peer("s32-client")
connect_many(client, [source])
waiter = LineWaiter(len(client.output))
client.send({"cmd": "download", "game_id": "bfbc2", "install": False})
client.wait_for(event_is("download-finished", "bfbc2"), timeout=60, description="download finish", waiter=waiter)
client.send({"cmd": "install", "game_id": "bfbc2"})
client.wait_for(event_is("install-finished", "bfbc2"), timeout=30, description="install finish", waiter=waiter)
client.send({"cmd": "uninstall", "game_id": "bfbc2"})
client.wait_for(event_is("uninstall-finished", "bfbc2"), timeout=30, description="uninstall finish", waiter=waiter)
before = len(client.output)
client.send({"cmd": "install", "game_id": "bfbc2"})
reinstall_waiter = LineWaiter(before)
client.wait_for(event_is("install-finished", "bfbc2"), timeout=30, description="reinstall finish", waiter=reinstall_waiter)
recent = client.output[before:]
if any(item.get("event") == "download-chunk-finished" for item in recent):
raise ScenarioError("reinstall produced transfer chunk events")
wait_local_game(client, "bfbc2", downloaded=True, installed=True)
if not (client.host_games_dir / "bfbc2" / "local").is_dir():
raise ScenarioError("local/ was not recreated")
return "reinstall recreated local/, local state installed=true, no transfer events during reinstall"
def s33_install_after_mutation(self) -> str:
source = self.peer("s33-bravo", games_dir=FIXTURES / "fixture-bravo", readonly_games=True)
client = self.peer("s33-client")
connect_many(client, [source])
waiter = LineWaiter(len(client.output))
client.send({"cmd": "download", "game_id": "bfbc2", "install": False})
client.wait_for(event_is("download-finished", "bfbc2"), timeout=60, description="download finish", waiter=waiter)
client.docker_exec(
"sh",
"-c",
"printf 'mutated archive bytes\\n' > /games/bfbc2/bfbc2.eti",
)
client.send({"cmd": "install", "game_id": "bfbc2"})
client.wait_for(event_is("install-finished", "bfbc2"), timeout=30, description="install finish", waiter=waiter)
client.docker_exec(
"cmp",
"/games/bfbc2/bfbc2.eti",
"/games/bfbc2/local/fixture-payload.txt",
)
return "fixture installer installed current mutated archive bytes exactly"
def s34_many_small_files(self) -> str:
source_dir = self.fixture_root / "s34-source"
create_many_small_game(source_dir / "bf1942")
catalog_dir = self.fixture_catalog(
"s34", source_dir / "bf1942", streamed_install=True
)
source = self.peer("s34-source", games_dir=source_dir, catalog_dir=catalog_dir)
client = self.peer("s34-client", catalog_dir=catalog_dir)
connect_many(client, [source])
wait_remote_game(client, "bf1942", peer_count=1)
waiter = LineWaiter(len(client.output))
client.send({"cmd": "download", "game_id": "bf1942", "install": False})
client.wait_for(event_is("download-finished", "bf1942"), timeout=60, description="download finish", waiter=waiter)
diff_game_dirs(source_dir / "bf1942", client.host_games_dir / "bf1942")
chunks = [
item
for item in client.output
if item.get("type") == "event"
and item.get("event") == "download-chunk-finished"
and item.get("data", {}).get("game_id") == "bf1942"
]
# 20 small files + version.ini, each a single coherent chunk: exactly 21
# chunk events, 21 distinct relative paths, no splits and no duplicates.
if len(chunks) != 21:
raise ScenarioError(f"expected exactly 21 file chunks (20 files + version.ini), got {len(chunks)}")
assert_no_duplicate_chunks(client, "bf1942")
distinct_paths = {item.get("data", {}).get("relative_path") for item in chunks}
if len(distinct_paths) != 21:
raise ScenarioError(f"expected 21 distinct file paths, got {len(distinct_paths)}: {sorted(distinct_paths)}")
return f"20 small files plus version.ini each transferred as one coherent chunk; diff matched; chunk events={len(chunks)}"
def s35_unknown_game_filtered(self) -> str:
game_id = "cod2"
source = self.peer(
"s35-source",
games_dir=FIXTURES / "fixture-unknown",
readonly_games=True,
catalog_dir=CATALOG_PROFILES / "unknown",
)
client = self.peer("s35-client")
connect_many(client, [source])
# The source has an honest, source-only catalog for cod2. The client uses
# the default reduced profile, where cod2 is intentionally unknown.
content_id = catalog_content_id(source, game_id)
wait_peer_has_game(client, source.peer_id, game_id, content_id)
wait_remote_library_content(client, game_id, content_id, peer_count=1)
wait_remote_absent(client, game_id)
err = client.send(
{"cmd": "download", "game_id": game_id, "install": False},
expect_error=True,
)
if "not in the local catalog" not in err["error"]:
raise ScenarioError(f"unexpected unknown game error: {err}")
assert_not_exists(client.host_games_dir / game_id)
return (
f"raw v8 availability retained unknown {game_id}/{content_id}, but the local "
f"catalog excluded it from list-games; download errored '{err['error']}'"
)
def s36_catalog_singleton(self) -> str:
peers = []
for index in range(5):
game_dir = self.fixture_root / f"s36-{index}"
version = CATALOG_VERSIONS["cnc4"] if index == 0 else "20160101"
copy_game("cnc4", game_dir, version=version)
peers.append(self.peer(f"s36-{index}", games_dir=game_dir))
client = self.peer("s36-client")
connect_many(client, peers)
content_id = catalog_content_id(client, "cnc4")
wait_peer_has_game(client, peers[0].peer_id, "cnc4", content_id)
for stale in peers[1:]:
wait_peer_without_game(client, stale.peer_id, "cnc4")
wait_remote_library_content(client, "cnc4", content_id, peer_count=1)
wait_remote_game(client, "cnc4", peer_count=1, version=CATALOG_VERSIONS["cnc4"])
waiter = LineWaiter(len(client.output))
client.send({"cmd": "download", "game_id": "cnc4", "install": False})
client.wait_for(event_is("download-finished", "cnc4"), timeout=60, description="download finish", waiter=waiter)
assert_chunk_authority(client, "cnc4", content_id, {peers[0].peer_id})
diff_game_dirs(peers[0].host_games_dir / "cnc4", client.host_games_dir / "cnc4")
return (
"catalog manifest, raw availability, remote view, and every verified chunk used "
"one exact ContentId/source; four stale peers advertised no transferable game"
)
def s37_single_source_download_throughput(self) -> str:
source_dir = self.fixture_root / "s37-source"
create_large_sparse_game(source_dir / PERF_GAME_ID, size=PERF_GAME_SIZE)
catalog_dir = self.fixture_catalog(
"s37", source_dir / PERF_GAME_ID, streamed_install=False
)
source = self.peer("s37-source", games_dir=source_dir, catalog_dir=catalog_dir)
client = self.peer("s37-client", catalog_dir=catalog_dir)
connect_many(client, [source])
content_id = catalog_content_id(client, PERF_GAME_ID)
wait_peer_has_game(client, source.peer_id, PERF_GAME_ID, content_id)
wait_remote_library_content(
client, PERF_GAME_ID, content_id, peer_count=1
)
wait_remote_game(client, PERF_GAME_ID, peer_count=1, version=PERF_GAME_VERSION)
start = len(client.output)
waiter = LineWaiter(start)
client.send({"cmd": "download", "game_id": PERF_GAME_ID, "install": False})
finished = client.wait_for(
event_is("download-finished", PERF_GAME_ID),
timeout=300,
description=f"{PERF_GAME_ID} throughput download",
waiter=waiter,
)
assert_download_status_trace(
client,
PERF_GAME_ID,
start=start,
terminal="download-finished",
)
destination_archive = client.host_games_dir / PERF_GAME_ID / f"{PERF_GAME_ID}.eti"
if destination_archive.stat().st_size != PERF_GAME_SIZE:
raise ScenarioError(
f"downloaded archive size mismatch: {destination_archive.stat().st_size} != {PERF_GAME_SIZE}"
)
diff_game_dirs(
source_dir / PERF_GAME_ID,
client.host_games_dir / PERF_GAME_ID,
)
throughput = finished.get("data", {}).get("throughput")
if not throughput:
raise ScenarioError(f"download-finished did not include throughput: {finished}")
expected_bytes = PERF_GAME_SIZE + len(PERF_GAME_VERSION)
if int(throughput["bytes"]) != expected_bytes:
raise ScenarioError(
f"throughput byte count mismatch: {throughput['bytes']} != {expected_bytes}"
)
chunks = [
item["data"]
for item in client.output[start:]
if item.get("type") == "event"
and item.get("event") == "download-chunk-finished"
and item.get("data", {}).get("game_id") == PERF_GAME_ID
]
if len(chunks) != 17 or int(throughput["chunks"]) != 17:
raise ScenarioError(
"expected exactly 17 verified chunks "
f"(16 archive + version.ini), got events={len(chunks)} throughput={throughput['chunks']}"
)
assert_chunk_authority(client, PERF_GAME_ID, content_id, {source.peer_id})
archive_path = f"{PERF_GAME_ID}.eti"
expected_paths = {archive_path, "version.ini"}
actual_paths = {item.get("relative_path") for item in chunks}
if actual_paths != expected_paths:
raise ScenarioError(
f"verified chunk paths are not exact: {actual_paths} != {expected_paths}"
)
archive_chunks = [item for item in chunks if item.get("relative_path") == archive_path]
version_chunks = [
item
for item in chunks
if item.get("relative_path") == "version.ini"
]
expected_archive_chunks = [
(archive_path, index * CHUNK_SIZE, CHUNK_SIZE) for index in range(16)
]
actual_archive_chunks = sorted(
(
item.get("relative_path"),
int(item["offset"]),
int(item["length"]),
)
for item in archive_chunks
)
if actual_archive_chunks != expected_archive_chunks:
raise ScenarioError(
"archive chunk path/offset/length tuples are not exact: "
f"{actual_archive_chunks}"
)
expected_version_chunk = [("version.ini", 0, 8)]
actual_version_chunks = [
(
item.get("relative_path"),
int(item["offset"]),
int(item["length"]),
)
for item in version_chunks
]
if len(PERF_GAME_VERSION.encode("utf-8")) != 8:
raise ScenarioError(f"performance fixture version is not 8 bytes: {PERF_GAME_VERSION!r}")
if actual_version_chunks != expected_version_chunk:
raise ScenarioError(f"version.ini was not one exact chunk: {version_chunks}")
assert_no_duplicate_chunks(client, PERF_GAME_ID)
terminal_events = [
item.get("event")
for item in client.output[start:]
if item.get("type") == "event"
and item.get("data", {}).get("game_id") == PERF_GAME_ID
and item.get("event")
in {"download-finished", "download-failed"}
]
if terminal_events != ["download-finished"]:
raise ScenarioError(
f"expected exactly one successful terminal event: {terminal_events}"
)
game = wait_local_game(
client,
PERF_GAME_ID,
downloaded=True,
installed=False,
)
assert_game_state(
game,
downloaded=True,
installed=False,
availability="Ready",
)
wait_no_active(client, PERF_GAME_ID)
# The byte count alone never exercises the rate math. Validate the rate
# fields are positive and mutually consistent so a units/divisor bug
# (MiB vs MB, off-by-1000, zero duration) cannot slip through.
if throughput["duration_ms"] <= 0:
raise ScenarioError(f"throughput duration_ms not positive: {throughput}")
if throughput["mib_per_s"] <= 0 or throughput["mbit_per_s"] <= 0:
raise ScenarioError(f"throughput rates not positive: {throughput}")
derived_mib = (int(throughput["bytes"]) / 1_048_576) / (throughput["duration_ms"] / 1000.0)
if abs(derived_mib - throughput["mib_per_s"]) > max(1.0, derived_mib * 0.02):
raise ScenarioError(
f"mib_per_s {throughput['mib_per_s']} disagrees with bytes/duration {derived_mib}"
)
# mbit_per_s = bytes*8/s/1e6; mib_per_s = bytes/s/1048576 -> ratio is fixed.
ratio = throughput["mbit_per_s"] / throughput["mib_per_s"]
expected_ratio = 1_048_576 * 8 / 1_000_000 # 8.388608
if abs(ratio - expected_ratio) > 0.01:
raise ScenarioError(
f"mbit/mib ratio {ratio} != expected {expected_ratio}: {throughput}"
)
minimum_mib_per_s = float(os.environ.get("LANSPREAD_S37_MIN_MIB_PER_S", "100"))
if throughput["mib_per_s"] < minimum_mib_per_s:
raise ScenarioError(
f"throughput {throughput['mib_per_s']:.2f} MiB/s is below configured "
f"minimum {minimum_mib_per_s:.2f} MiB/s"
)
return (
f"{PERF_GAME_ID} {format_bytes(PERF_GAME_SIZE)} single-source download: "
f"{throughput['mib_per_s']:.2f} MiB/s, "
f"{throughput['mbit_per_s']:.2f} Mbit/s, "
f"{throughput['duration_ms'] / 1000.0:.3f}s, exactly 17 verified chunks; "
f"host={os.uname().nodename}, storage={RUN_ROOT}, profile=dynamic-s37, "
f"date={time.strftime('%Y-%m-%d')}"
)
def s38_first_play_launch_settings(self) -> str:
client_dir = self.fixture_root / "s38-client"
copy_game("css", client_dir)
client = self.peer(
"s38-client",
games_dir=client_dir,
extra_args=["--unrar", "/usr/local/bin/unrar"],
)
waiter = LineWaiter(len(client.output))
client.send({"cmd": "install", "game_id": "css"})
client.wait_for(
event_is("install-finished", "css"),
timeout=30,
description="css install",
waiter=waiter,
)
wait_local_game(client, "css", downloaded=True, installed=True)
marker = client.host_state_dir / "games" / "css" / "launch_settings_applied"
if marker.exists():
raise ScenarioError("launch settings marker existed before first play")
local_root = client.host_games_dir / "css" / "local"
account_file = local_root / "profiles" / "local" / "account_name.txt"
language_file = local_root / "profiles" / "local" / "language.txt"
ini_file = (
local_root
/ "engine"
/ "bin"
/ "win64"
/ "steam_settings"
/ "SmartSteamEmu.ini"
)
for path in [account_file, language_file, ini_file]:
if not path.is_file():
raise ScenarioError(f"expected installed launch settings file: {path}")
if account_file.read_bytes() != b"stubaccount":
raise ScenarioError("installed account_name.txt did not contain the exact stub bytes")
if language_file.read_bytes() != b"english":
raise ScenarioError("installed language.txt did not contain the exact stub bytes")
expected_ini = (
b"[Settings]\r\n"
b"AppId = 240\r\n"
b"PersonaName = stubplayer\r\n"
b"Language = english\r\n"
)
if ini_file.read_bytes() != expected_ini:
raise ScenarioError(
"installed SmartSteamEmu.ini did not contain the exact CRLF stub bytes"
)
first = client.send(
{
"cmd": "play",
"game_id": "css",
"username": "Lan Hero",
"language": "german",
}
)["data"]["outcome"]
expected_first = {
"already_applied": False,
"account_name_written": True,
"language_written": True,
"persona_name_written": True,
}
if first != expected_first:
raise ScenarioError(f"unexpected first play outcome: {first}")
if not marker.is_file():
raise ScenarioError("launch settings marker was not written after first play")
if account_file.read_text(encoding="utf-8") != "Lan Hero":
raise ScenarioError("account_name.txt was not stamped with username")
if language_file.read_text(encoding="utf-8") != "german":
raise ScenarioError("language.txt was not stamped with language")
stamped_ini = ini_file.read_bytes()
if b"PersonaName = Lan Hero\r\n" not in stamped_ini:
raise ScenarioError("PersonaName was not stamped with CRLF preserved")
if b"AppId = 240\r\n" not in stamped_ini or b"Language = english\r\n" not in stamped_ini:
raise ScenarioError("SmartSteamEmu.ini sibling lines were not preserved")
client.docker_exec(
"sh",
"-c",
"printf resetaccount > /games/css/local/profiles/local/account_name.txt",
)
client.docker_exec(
"sh",
"-c",
"printf resetlang > /games/css/local/profiles/local/language.txt",
)
client.docker_exec(
"sh",
"-c",
"printf '[Settings]\\r\\nAppId = 240\\r\\n"
"PersonaName = resetplayer\\r\\nLanguage = english\\r\\n' > "
"/games/css/local/engine/bin/win64/steam_settings/SmartSteamEmu.ini",
)
second = client.send(
{
"cmd": "play",
"game_id": "css",
"username": "Second User",
"language": "french",
}
)["data"]["outcome"]
expected_second = {
"already_applied": True,
"account_name_written": False,
"language_written": False,
"persona_name_written": False,
}
if second != expected_second:
raise ScenarioError(f"unexpected second play outcome: {second}")
if account_file.read_text(encoding="utf-8") != "resetaccount":
raise ScenarioError("second play rewrote account_name.txt despite marker")
if language_file.read_text(encoding="utf-8") != "resetlang":
raise ScenarioError("second play rewrote language.txt despite marker")
if b"PersonaName = resetplayer\r\n" not in ini_file.read_bytes():
raise ScenarioError("second play rewrote PersonaName despite marker")
return "css first play stamped launch settings once; second play respected the marker"
def stream_install_cnctw(self, prefix: str) -> tuple[Peer, Peer]:
source_dir = self.fixture_root / f"{prefix}-bravo"
copy_game("cnctw", source_dir, version="20160128")
network = self.create_scenario_network(f"{prefix}-stream", internal=True)
source = self.peer(
f"{prefix}-bravo",
network=network,
games_dir=source_dir,
)
client = self.peer(f"{prefix}-client", network=network)
connect_many(client, [source])
assert_exact_peer_ids(client, {source.peer_id})
content_id = catalog_content_id(client, "cnctw")
wait_peer_has_game(client, source.peer_id, "cnctw", content_id)
wait_remote_library_content(client, "cnctw", content_id, peer_count=1)
wait_remote_game(client, "cnctw", peer_count=1, version="20160128")
start = len(client.output)
waiter = LineWaiter(start)
client.send({"cmd": "stream-install", "game_id": "cnctw"})
client.wait_for(
event_is("download-begin", "cnctw"),
timeout=20,
description="stream begin cnctw",
waiter=waiter,
)
client.wait_for(
event_is("download-finished", "cnctw"),
timeout=60,
description="stream finish cnctw",
waiter=waiter,
)
client.wait_for(
event_is("install-finished", "cnctw"),
timeout=30,
description="stream install cnctw",
waiter=waiter,
)
assert_stream_install_lifecycle(
client,
"cnctw",
start=start,
content_id=content_id,
source_peer_id=source.peer_id,
source_addr=source.ready_addr,
)
return source, client
def s39_streamed_install_local_only(self) -> str:
source, client = self.stream_install_cnctw("s39")
game = wait_local_game(client, "cnctw", downloaded=False, installed=True)
assert_game_state(
game,
downloaded=False,
installed=True,
availability="LocalOnly",
)
game_root = client.host_games_dir / "cnctw"
assert_not_exists(game_root / "version.ini")
assert_not_exists(game_root / "cnctw.eti")
expected = {
"bin/cnctw-payload.bin": unrar_entry_sha256(
source, "cnctw", "bin/cnctw-payload.bin"
),
"data/cnctw-assets.dat": unrar_entry_sha256(
source, "cnctw", "data/cnctw-assets.dat"
),
}
actual = {
rel: sha256_file(game_root / "local" / rel)
for rel in expected
}
if actual != expected:
raise ScenarioError(f"streamed local payload hashes mismatched: {actual} != {expected}")
streamed_bytes = sum(
int(item.get("data", {}).get("length", 0))
for item in client.output
if item.get("type") == "event"
and item.get("event") == "download-chunk-finished"
and item.get("data", {}).get("game_id") == "cnctw"
)
expected_bytes = 3 * 1024 * 1024
if streamed_bytes != expected_bytes:
raise ScenarioError(
f"streamed byte count mismatch: {streamed_bytes} != {expected_bytes}"
)
wait_no_outbound_transfer(source, "cnctw")
return (
"cnctw streamed into local/ only; root archive and version.ini absent; "
f"payload hashes={actual}; source outbound transfer drained"
)
def s40_streamed_receiver_not_source(self) -> str:
source, receiver = self.stream_install_cnctw("s40")
source.shutdown()
observer_network = self.create_scenario_network(
"s40-observer",
internal=True,
)
observer = self.peer("s40-observer", network=observer_network)
assert_exact_peer_ids(observer, set())
remote_view_start = len(observer.output)
receiver_addr = self.connect_peer_to_network(receiver, observer_network)
observer.connect_to_at(receiver, receiver_addr, 1)
assert_exact_peer_ids(observer, {receiver.peer_id})
wait_peer_without_game(observer, receiver.peer_id, "cnctw")
wait_remote_library_without_game(
observer,
"cnctw",
start=remote_view_start,
)
wait_remote_absent(observer, "cnctw", timeout=5)
start = len(observer.output)
waiter = LineWaiter(start)
queued = observer.send(
{"cmd": "download", "game_id": "cnctw", "install": False}
)
if queued.get("data", {}).get("queued") is not True:
raise ScenarioError(f"no-source cnctw download was not queued: {queued}")
observer.wait_for(
event_is("download-failed", "cnctw"),
timeout=20,
description="asynchronous no-source cnctw failure",
waiter=waiter,
)
assert_download_status_trace(
observer,
"cnctw",
start=start,
terminal="download-failed",
reason="verified-catalog-sources-exhausted",
expect_begin=False,
expect_verification=False,
)
assert_no_event_since(observer, start, "download-begin", "cnctw")
assert_no_event_since(observer, start, "download-finished", "cnctw")
active_events = [
item
for item in observer.output[start:]
if item.get("type") == "event"
and item.get("event") == "active-operations-changed"
and any(
operation.get("game_id") == "cnctw"
for operation in item.get("data", {}).get("active_operations", [])
)
]
if active_events:
raise ScenarioError(
f"no-source cnctw download entered active operations: {active_events}"
)
assert_no_active(observer, "cnctw")
assert_exact_peer_ids(observer, {receiver.peer_id})
wait_peer_without_game(observer, receiver.peer_id, "cnctw")
wait_remote_absent(observer, "cnctw", timeout=5)
assert_local_absent(observer, "cnctw")
assert_not_exists(observer.host_games_dir / "cnctw")
return (
"isolated observer roster changed from empty to exactly the stream-installed "
"receiver; raw/remote views omitted cnctw, and queued failure had no begin, "
"success, active-operation event, or local mutation"
)
def s41_solid_archive_streamed_install(self) -> str:
source_dir = self.fixture_root / "s41-solid-source"
source_game = source_dir / "cnctw"
shutil.copytree(FIXTURES / "fixture-solid" / "cnctw", source_game)
catalog_dir = CATALOG_PROFILES / "solid"
network = self.create_scenario_network("s41-stream", internal=True)
source = self.peer(
"s41-solid-source",
network=network,
games_dir=source_dir,
catalog_dir=catalog_dir,
)
assert_peer_rar_archive_solid(source, "cnctw")
client = self.peer(
"s41-solid-client",
network=network,
catalog_dir=catalog_dir,
)
connect_many(client, [source])
assert_exact_peer_ids(client, {source.peer_id})
content_id = catalog_content_id(client, "cnctw")
wait_peer_has_game(client, source.peer_id, "cnctw", content_id)
wait_remote_library_content(client, "cnctw", content_id, peer_count=1)
wait_remote_game(client, "cnctw", peer_count=1, version="20160128")
start = len(client.output)
waiter = LineWaiter(start)
client.send({"cmd": "stream-install", "game_id": "cnctw"})
client.wait_for(
event_is("download-finished", "cnctw"),
timeout=60,
description="solid stream finish cnctw",
waiter=waiter,
)
client.wait_for(
event_is("install-finished", "cnctw"),
timeout=30,
description="solid stream install cnctw",
waiter=waiter,
)
assert_chunk_authority(
client,
"cnctw",
content_id,
{source.peer_id},
start=start,
)
game = wait_local_game(client, "cnctw", downloaded=False, installed=True)
assert_game_state(
game,
downloaded=False,
installed=True,
availability="LocalOnly",
)
game_root = client.host_games_dir / "cnctw"
assert_not_exists(game_root / "version.ini")
assert_not_exists(game_root / "cnctw.eti")
expected = {
"bin/cnctw-solid-payload.bin": unrar_entry_sha256(
source, "cnctw", "bin/cnctw-solid-payload.bin"
),
"data/cnctw-solid-assets.dat": unrar_entry_sha256(
source, "cnctw", "data/cnctw-solid-assets.dat"
),
}
actual = {
rel: sha256_file(game_root / "local" / rel)
for rel in expected
}
if actual != expected:
raise ScenarioError(
f"solid streamed payload hashes mismatched: {actual} != {expected}"
)
streamed_bytes = sum(
int(item.get("data", {}).get("length", 0))
for item in client.output
if item.get("type") == "event"
and item.get("event") == "download-chunk-finished"
and item.get("data", {}).get("game_id") == "cnctw"
)
expected_bytes = sum((game_root / "local" / rel).stat().st_size for rel in expected)
if streamed_bytes != expected_bytes:
raise ScenarioError(
f"solid streamed byte count mismatch: {streamed_bytes} != {expected_bytes}"
)
return (
"solid cnctw archive streamed through one local-only install; "
f"payload hashes={actual}, bytes={streamed_bytes}"
)
def s42_streamed_install_retries_next_source(self) -> str:
bad_dir = self.fixture_root / "s42-bad-source"
good_dir = self.fixture_root / "s42-good-source"
shutil.copytree(FIXTURES / "fixture-solid" / "cnctw", bad_dir / "cnctw")
copy_game("cnctw", good_dir, version="20160128")
network = self.create_scenario_network("s42-stream", internal=True)
# Learn and persist both installation identities before assigning the
# mismatching profile. Production orders sources by authenticated
# PeerId, not by Docker address or connection order.
source_a_probe = self.peer("s42-source-a", network=network)
source_b_probe = self.peer("s42-source-b", network=network)
lower_probe, upper_probe = sorted(
[source_a_probe, source_b_probe],
key=lambda peer: peer_id_sort_key(peer.peer_id),
)
lower_id = lower_probe.peer_id
upper_id = upper_probe.peer_id
lower_name = lower_probe.name
upper_name = upper_probe.name
source_a_probe.shutdown()
source_b_probe.shutdown()
bad = self.peer(lower_name, network=network, games_dir=bad_dir)
good = self.peer(upper_name, network=network, games_dir=good_dir)
if bad.peer_id != lower_id or good.peer_id != upper_id:
raise ScenarioError(
"S42 source identities changed across the profile restart: "
f"bad={bad.peer_id}/{lower_id}, good={good.peer_id}/{upper_id}"
)
assert_peer_rar_archive_solid(bad, "cnctw")
if peer_id_sort_key(bad.peer_id) >= peer_id_sort_key(good.peer_id):
raise ScenarioError(
"S42 requires the catalog-mismatched source to have the lower authenticated "
f"PeerId; bad={bad.peer_id}, good={good.peer_id}"
)
client = self.peer("s42-client", network=network)
connect_many(client, [bad, good])
assert_exact_peer_ids(client, {bad.peer_id, good.peer_id})
content_id = catalog_content_id(client, "cnctw")
source_content_ids = {
catalog_content_id(bad, "cnctw"),
catalog_content_id(good, "cnctw"),
}
if source_content_ids != {content_id}:
raise ScenarioError(
f"S42 sources did not share the client's exact ContentId: "
f"{source_content_ids} != {content_id}"
)
wait_peer_has_game(client, bad.peer_id, "cnctw", content_id)
wait_peer_has_game(client, good.peer_id, "cnctw", content_id)
wait_remote_library_content(client, "cnctw", content_id, peer_count=2)
wait_remote_game(client, "cnctw", peer_count=2, version="20160128")
expected = {
"bin/cnctw-payload.bin": unrar_entry_sha256(
good, "cnctw", "bin/cnctw-payload.bin"
),
"data/cnctw-assets.dat": unrar_entry_sha256(
good, "cnctw", "data/cnctw-assets.dat"
),
}
if client.process is None:
raise ScenarioError("S42 client process was not retained")
client_pid = client.process.pid
first_start = len(client.output)
first_waiter = LineWaiter(first_start)
bad_first_edges = LineWaiter(len(bad.output))
good_first_edges = LineWaiter(len(good.output))
client.send({"cmd": "stream-install", "game_id": "cnctw"})
wait_outbound_transfer_cycle(
bad,
"cnctw",
waiter=bad_first_edges,
phase="first streamed install",
)
wait_outbound_transfer_cycle(
good,
"cnctw",
waiter=good_first_edges,
phase="first streamed install",
)
client.wait_for(
event_is("download-finished", "cnctw"),
timeout=60,
description="first retry stream finish cnctw",
waiter=first_waiter,
)
client.wait_for(
event_is("install-finished", "cnctw"),
timeout=30,
description="first retry stream install cnctw",
waiter=first_waiter,
)
assert_stream_install_lifecycle(
client,
"cnctw",
start=first_start,
content_id=content_id,
source_peer_id=good.peer_id,
source_addr=good.ready_addr,
expect_invalid_source_retry=True,
)
assert_chunk_authority(
client,
"cnctw",
content_id,
{good.peer_id},
start=first_start,
)
game = wait_local_game(client, "cnctw", downloaded=False, installed=True)
assert_game_state(
game,
downloaded=False,
installed=True,
availability="LocalOnly",
)
game_root = client.host_games_dir / "cnctw"
assert_not_exists(game_root / ".local.installing")
assert_not_exists(game_root / ".local.backup")
assert_not_exists(game_root / "version.ini")
assert_not_exists(game_root / "cnctw.eti")
first_actual = {
rel: sha256_file(game_root / "local" / rel)
for rel in expected
}
if first_actual != expected:
raise ScenarioError(
f"first retry payload hashes mismatched: {first_actual} != {expected}"
)
first_streamed_bytes = sum(
int(item.get("data", {}).get("length", 0))
for item in client.output[first_start:]
if item.get("type") == "event"
and item.get("event") == "download-chunk-finished"
and item.get("data", {}).get("game_id") == "cnctw"
)
expected_bytes = 3 * 1024 * 1024
if first_streamed_bytes != expected_bytes:
raise ScenarioError(
f"first retry byte count mismatch: {first_streamed_bytes} != {expected_bytes}"
)
uninstall_start = len(client.output)
uninstall_waiter = LineWaiter(uninstall_start)
client.send({"cmd": "uninstall", "game_id": "cnctw"})
client.wait_for(
event_is("uninstall-finished", "cnctw"),
timeout=30,
description="uninstall before quarantine persistence retry",
waiter=uninstall_waiter,
)
wait_no_active(client, "cnctw")
assert_local_absent(client, "cnctw")
assert_not_exists(game_root / "local")
assert_not_exists(game_root / ".local.installing")
assert_not_exists(game_root / ".local.backup")
if client.process.poll() is not None or client.process.pid != client_pid:
raise ScenarioError("S42 client runtime changed before the quarantine retry")
assert_exact_peer_ids(client, {bad.peer_id, good.peer_id})
wait_peer_has_game(client, bad.peer_id, "cnctw", content_id)
wait_peer_has_game(client, good.peer_id, "cnctw", content_id)
wait_remote_library_content(client, "cnctw", content_id, peer_count=2)
wait_no_outbound_transfer(bad, "cnctw")
wait_no_outbound_transfer(good, "cnctw")
assert_no_outbound_edge_during(
bad,
start=bad_first_edges.seen,
phase="first streamed install post-drain",
timeout=0.5,
)
assert_no_outbound_edge_during(
good,
start=good_first_edges.seen,
phase="first streamed install post-drain",
timeout=0.5,
)
second_bad_start = len(bad.output)
second_good_start = len(good.output)
good_second_edges = LineWaiter(second_good_start)
second_start = len(client.output)
second_waiter = LineWaiter(second_start)
client.send({"cmd": "stream-install", "game_id": "cnctw"})
wait_outbound_transfer_cycle(
good,
"cnctw",
waiter=good_second_edges,
phase="second streamed install",
)
client.wait_for(
event_is("download-finished", "cnctw"),
timeout=60,
description="quarantine persistence stream finish cnctw",
waiter=second_waiter,
)
client.wait_for(
event_is("install-finished", "cnctw"),
timeout=30,
description="quarantine persistence install finish cnctw",
waiter=second_waiter,
)
assert_stream_install_lifecycle(
client,
"cnctw",
start=second_start,
content_id=content_id,
source_peer_id=good.peer_id,
source_addr=good.ready_addr,
)
wait_no_outbound_transfer(bad, "cnctw")
wait_no_outbound_transfer(good, "cnctw")
assert_no_outbound_edge_during(
good,
start=good_second_edges.seen,
phase="second streamed install post-drain",
timeout=0.5,
)
assert_chunk_authority(
client,
"cnctw",
content_id,
{good.peer_id},
start=second_start,
)
second_game = wait_local_game(
client,
"cnctw",
downloaded=False,
installed=True,
)
assert_game_state(
second_game,
downloaded=False,
installed=True,
availability="LocalOnly",
)
assert_not_exists(game_root / ".local.installing")
assert_not_exists(game_root / ".local.backup")
assert_not_exists(game_root / "version.ini")
assert_not_exists(game_root / "cnctw.eti")
second_actual = {
rel: sha256_file(game_root / "local" / rel)
for rel in expected
}
if second_actual != expected:
raise ScenarioError(
f"second retry payload hashes mismatched: {second_actual} != {expected}"
)
second_streamed_bytes = sum(
int(item.get("data", {}).get("length", 0))
for item in client.output[second_start:]
if item.get("type") == "event"
and item.get("event") == "download-chunk-finished"
and item.get("data", {}).get("game_id") == "cnctw"
)
if second_streamed_bytes != expected_bytes:
raise ScenarioError(
f"second retry byte count mismatch: {second_streamed_bytes} != {expected_bytes}"
)
wait_no_outbound_transfer(bad, "cnctw")
wait_no_outbound_transfer(good, "cnctw")
bad.shutdown_gracefully_for_proof()
good.shutdown_gracefully_for_proof()
second_bad_edges = [
item
for item in bad.output[second_bad_start:]
if item.get("type") == "event"
and item.get("event") == "outbound-transfer-count-changed"
]
if second_bad_edges:
raise ScenarioError(
"second streamed install admitted the quarantined bad source: "
f"{second_bad_edges}"
)
second_good_edges = [
item
for item in good.output[second_good_start:]
if item.get("type") == "event"
and item.get("event") == "outbound-transfer-count-changed"
]
if len(second_good_edges) != 2:
raise ScenarioError(
"second streamed install did not emit exactly good admission/drain edges: "
f"{second_good_edges}"
)
return (
"first install admitted bad then good exact-content sources; after uninstall, "
"the same runtime skipped the quarantined bad source and admitted only good; "
f"good={good.peer_id}, bad={bad.peer_id}, bytes={second_streamed_bytes}"
)
def s43_streamed_install_rejects_installed_game(self) -> str:
_source, client = self.stream_install_cnctw("s43")
start = len(client.output)
waiter = LineWaiter(start)
client.send({"cmd": "stream-install", "game_id": "cnctw"})
client.wait_for(
event_is("download-failed", "cnctw"),
timeout=20,
description="already-installed stream rejection",
waiter=waiter,
)
assert_download_status_trace(
client,
"cnctw",
start=start,
terminal="download-failed",
reason="operation-failed",
expect_begin=False,
expect_verification=False,
)
assert_no_event_since(client, start, "install-finished", "cnctw")
assert_no_event_since(client, start, "download-finished", "cnctw")
wait_no_active(client, "cnctw")
game = wait_local_game(client, "cnctw", downloaded=False, installed=True)
assert_game_state(
game,
downloaded=False,
installed=True,
availability="LocalOnly",
)
return "already-installed cnctw rejected a second streamed install without state drift"
def s44_corrupt_stream_rolls_back(self) -> str:
source_dir = self.fixture_root / "s44-mismatched-source"
shutil.copytree(FIXTURES / "fixture-solid" / "cnctw", source_dir / "cnctw")
network = self.create_scenario_network("s44-stream", internal=True)
source = self.peer(
"s44-mismatched-source",
network=network,
games_dir=source_dir,
)
assert_peer_rar_archive_solid(source, "cnctw")
client = self.peer("s44-client", network=network)
connect_many(client, [source])
assert_exact_peer_ids(client, {source.peer_id})
content_id = catalog_content_id(client, "cnctw")
wait_peer_has_game(client, source.peer_id, "cnctw", content_id)
wait_remote_library_content(client, "cnctw", content_id, peer_count=1)
wait_remote_game(client, "cnctw", peer_count=1, version="20160128")
start = len(client.output)
waiter = LineWaiter(start)
source_edges = LineWaiter(len(source.output))
client.send({"cmd": "stream-install", "game_id": "cnctw"})
wait_outbound_transfer_cycle(
source,
"cnctw",
waiter=source_edges,
phase="all-bad streamed install",
)
client.wait_for(
event_is("download-failed", "cnctw"),
timeout=30,
description="catalog-mismatched stream failed",
waiter=waiter,
)
assert_download_status_trace(
client,
"cnctw",
start=start,
terminal="download-failed",
reason="verified-catalog-sources-exhausted",
)
assert_no_event_since(client, start, "download-finished", "cnctw")
assert_no_event_since(client, start, "install-finished", "cnctw")
wait_no_active(client, "cnctw")
assert_failed_stream_left_no_local(client, "cnctw")
return (
"only source had a valid solid archive whose extracted output mismatched the "
"default catalog; integrity failure left no local install"
)
def s45_sender_disconnect_mid_stream(self) -> str:
source_dir = self.fixture_root / "s45-source"
copy_game("alienswarm", source_dir, version="20190317")
network = self.create_scenario_network("s45-stream", internal=True)
source = self.peer("s45-source", network=network, games_dir=source_dir)
client = self.peer("s45-client", network=network)
connect_many(client, [source])
assert_exact_peer_ids(client, {source.peer_id})
content_id = catalog_content_id(client, "alienswarm")
wait_peer_has_game(client, source.peer_id, "alienswarm", content_id)
wait_remote_library_content(client, "alienswarm", content_id, peer_count=1)
wait_remote_game(client, "alienswarm", peer_count=1, version="20190317")
start = len(client.output)
waiter = LineWaiter(start)
client.send({"cmd": "stream-install", "game_id": "alienswarm"})
first_chunk = client.wait_for(
event_is("download-chunk-finished", "alienswarm"),
timeout=30,
description="first alienswarm stream chunk before source drop",
waiter=waiter,
)
first_data = first_chunk.get("data", {})
if (
first_data.get("peer_id") != source.peer_id
or first_data.get("content_id") != content_id
):
raise ScenarioError(
f"sender-drop barrier used the wrong source/content: {first_data}"
)
source.kill()
client.wait_for(
event_is("download-failed", "alienswarm"),
timeout=60,
description="prompt sender transport failure",
waiter=waiter,
)
assert_download_status_trace(
client,
"alienswarm",
start=start,
terminal="download-failed",
reason="verified-catalog-sources-exhausted",
)
assert_no_event_since(client, start, "download-finished", "alienswarm")
assert_no_event_since(client, start, "install-finished", "alienswarm")
assert_chunk_authority(
client,
"alienswarm",
content_id,
{source.peer_id},
start=start,
)
wait_no_active(client, "alienswarm")
assert_failed_stream_left_no_local(client, "alienswarm")
return (
"sender disconnect after first alienswarm chunk emitted prompt download-failed "
"and rolled back the stream before stale-peer liveness elapsed"
)
def s46_receiver_cancel_mid_stream(self) -> str:
source_dir = self.fixture_root / "s46-source"
copy_game("alienswarm", source_dir, version="20190317")
network = self.create_scenario_network("s46-stream", internal=True)
source = self.peer("s46-source", network=network, games_dir=source_dir)
client = self.peer("s46-client", network=network)
connect_many(client, [source])
assert_exact_peer_ids(client, {source.peer_id})
content_id = catalog_content_id(client, "alienswarm")
wait_peer_has_game(client, source.peer_id, "alienswarm", content_id)
wait_remote_library_content(client, "alienswarm", content_id, peer_count=1)
wait_remote_game(client, "alienswarm", peer_count=1, version="20190317")
start = len(client.output)
waiter = LineWaiter(start)
client.send({"cmd": "stream-install", "game_id": "alienswarm"})
first_chunk = client.wait_for(
event_is("download-chunk-finished", "alienswarm"),
timeout=30,
description="first alienswarm stream chunk before receiver cancel",
waiter=waiter,
)
first_data = first_chunk.get("data", {})
if (
first_data.get("peer_id") != source.peer_id
or first_data.get("content_id") != content_id
):
raise ScenarioError(
f"receiver-cancel barrier used the wrong source/content: {first_data}"
)
client.send({"cmd": "cancel-download", "game_id": "alienswarm"})
wait_no_active(client, "alienswarm", timeout=60)
assert_failed_stream_left_no_local(client, "alienswarm")
client.shutdown_gracefully_for_proof()
assert_download_status_trace(
client,
"alienswarm",
start=start,
terminal=None,
)
assert_no_event_since(client, start, "download-finished", "alienswarm")
assert_no_event_since(client, start, "download-failed", "alienswarm")
assert_no_event_since(client, start, "install-finished", "alienswarm")
assert_chunk_authority(
client,
"alienswarm",
content_id,
{source.peer_id},
start=start,
)
return "receiver cancel after first alienswarm chunk rolled back without failed event"
def s47_multi_archive_streams_in_sorted_order(self) -> str:
source_dir = self.fixture_root / "s47-source"
source_game = source_dir / "cnctw"
shutil.copytree(FIXTURES / "fixture-multi" / "cnctw", source_game)
catalog_dir = CATALOG_PROFILES / "multi"
network = self.create_scenario_network("s47-stream", internal=True)
source = self.peer(
"s47-source",
network=network,
games_dir=source_dir,
catalog_dir=catalog_dir,
)
client = self.peer(
"s47-client",
network=network,
catalog_dir=catalog_dir,
)
connect_many(client, [source])
assert_exact_peer_ids(client, {source.peer_id})
content_id = catalog_content_id(client, "cnctw")
wait_peer_has_game(client, source.peer_id, "cnctw", content_id)
wait_remote_library_content(client, "cnctw", content_id, peer_count=1)
wait_remote_game(client, "cnctw", peer_count=1, version="20160128")
start = len(client.output)
waiter = LineWaiter(start)
client.send({"cmd": "stream-install", "game_id": "cnctw"})
client.wait_for(
event_is("download-finished", "cnctw"),
timeout=30,
description="multi-archive stream finish",
waiter=waiter,
)
client.wait_for(
event_is("install-finished", "cnctw"),
timeout=30,
description="multi-archive stream install",
waiter=waiter,
)
assert_chunk_authority(
client,
"cnctw",
content_id,
{source.peer_id},
start=start,
)
game = wait_local_game(client, "cnctw", downloaded=False, installed=True)
assert_game_state(
game,
downloaded=False,
installed=True,
availability="LocalOnly",
)
game_root = client.host_games_dir / "cnctw"
assert_not_exists(game_root / "version.ini")
assert_not_exists(game_root / "a-first.eti")
assert_not_exists(game_root / "z-second.eti")
chunk_paths = streamed_chunk_paths(client, "cnctw")
expected_paths = [
"order/first.txt",
"order/second.txt",
]
if chunk_paths != expected_paths:
raise ScenarioError(f"multi-archive stream order mismatch: {chunk_paths}")
first = (game_root / "local" / "order" / "first.txt").read_text(encoding="utf-8")
second = (game_root / "local" / "order" / "second.txt").read_text(encoding="utf-8")
if first != "first archive payload\n" or second != "second archive payload\n":
raise ScenarioError(f"multi-archive payload mismatch: {first!r}, {second!r}")
return f"multi-archive cnctw streamed in sorted order: {chunk_paths}"
def s48_call_to_play_replication_and_late_join(self) -> str:
alice = self.peer("s48-alice")
bob = self.peer("s48-bob")
alice.set_call_to_play_display_name("Alice")
bob.set_call_to_play_display_name("Bob")
connect_bidirectional(alice, bob)
now = int(time.time() * 1000)
create = alice.publish_call_to_play({
"call_id": None,
"action": {
"Create": {
"game_id": "cnctw",
"max_players": 4,
"scheduled_for": None,
"deadline": now + 600_000,
}
},
})
call_id = create["call_id"]
wait_call_to_play_events(bob, {create["event_id"]})
rsvp = bob.publish_call_to_play({
"call_id": call_id,
"action": "Rsvp",
})
message = bob.publish_call_to_play({
"call_id": call_id,
"action": {
"SendMessage": {
"text": "I am in",
}
},
})
expected = {create["event_id"], rsvp["event_id"], message["event_id"]}
wait_call_to_play_events(alice, expected)
# Keep Alice<->Bob live on the ordinary network so Alice must retain
# Bob's slice throughout the no-relay proof. Charlie starts on an
# internal-only bridge shared with Alice: it has no route to Bob's
# ordinary-network address until Bob is explicitly attached below.
late_join_network = self.create_scenario_network(
"s48-late-join", internal=True
)
alice_late_join_addr = self.connect_peer_to_network(alice, late_join_network)
charlie = self.peer("s48-charlie", network=late_join_network)
charlie.set_call_to_play_display_name("Charlie")
charlie.connect_to_at(alice, alice_late_join_addr, 1)
wait_call_to_play_exact_events(alice, expected)
assert_exact_peer_ids(charlie, {alice.peer_id})
alice_only = wait_call_to_play_exact_events_from_exact_peers(
charlie,
{create["event_id"]},
{alice.peer_id},
)
wait_call_to_play_exact_events(alice, expected)
if any(event.get("id") in {rsvp["event_id"], message["event_id"]}
for event in alice_only):
raise ScenarioError(f"Alice relayed Bob-owned events to Charlie: {alice_only}")
bob_late_join_addr = self.connect_peer_to_network(bob, late_join_network)
charlie.connect_to_at(bob, bob_late_join_addr, 2)
events = wait_call_to_play_exact_events(charlie, expected)
if len(events) != 3:
raise ScenarioError(f"direct-author late-join view contains duplicates: {events}")
authors = {event["id"]: event["author_id"] for event in events}
if authors.get(create["event_id"]) != alice.peer_id or any(
authors.get(event_id) != bob.peer_id
for event_id in [rsvp["event_id"], message["event_id"]]
):
raise ScenarioError(f"direct-author attribution was not preserved: {events}")
bob.shutdown()
wait_call_to_play_departure_with_exact_remaining_peers(
charlie,
{create["event_id"]},
"Bob participant",
{alice.peer_id},
bob.peer_id,
)
alice.shutdown()
wait_call_to_play_departure_with_exact_remaining_peers(
charlie,
set(),
"Alice creator",
set(),
alice.peer_id,
)
return (
"late joiner pulled Alice and Bob directly; full-view replacement removed Bob's "
"participant slice on departure, then removed the call when Alice departed"
)
def s49_terminal_call_to_play_late_join(self) -> str:
alice = self.peer("s49-alice")
bob = self.peer("s49-bob")
alice.set_call_to_play_display_name("Alice")
bob.set_call_to_play_display_name("Bob")
connect_bidirectional(alice, bob)
now = int(time.time() * 1000)
create = alice.publish_call_to_play({
"call_id": None,
"action": {
"Create": {
"game_id": "cnctw",
"max_players": 4,
"scheduled_for": None,
"deadline": now + 600_000,
}
},
})
call_id = create["call_id"]
wait_call_to_play_events(bob, {create["event_id"]})
ready = bob.publish_call_to_play({
"call_id": call_id,
"action": {"Respond": {"ready_at": None}},
})
message = bob.publish_call_to_play({
"call_id": call_id,
"action": {
"SendMessage": {
"text": "Ready to launch",
}
},
})
participant_ids = {ready["event_id"], message["event_id"]}
wait_call_to_play_events(alice, {create["event_id"], *participant_ids})
start = alice.publish_call_to_play({
"call_id": call_id,
"action": "Start",
})
expected = {create["event_id"], *participant_ids, start["event_id"]}
wait_call_to_play_events(bob, expected)
late_join_network = self.create_scenario_network(
"s49-late-join", internal=True
)
alice_late_join_addr = self.connect_peer_to_network(alice, late_join_network)
charlie = self.peer("s49-charlie", network=late_join_network)
charlie.set_call_to_play_display_name("Charlie")
charlie.connect_to_at(alice, alice_late_join_addr, 1)
alice_ids = {create["event_id"], start["event_id"]}
wait_call_to_play_exact_events(alice, expected)
assert_exact_peer_ids(charlie, {alice.peer_id})
alice_only = wait_call_to_play_exact_events_from_exact_peers(
charlie,
alice_ids,
{alice.peer_id},
)
wait_call_to_play_exact_events(alice, expected)
if any(event.get("id") in participant_ids for event in alice_only):
raise ScenarioError(
f"Alice relayed Bob-owned terminal-call events to Charlie: {alice_only}"
)
bob_late_join_addr = self.connect_peer_to_network(bob, late_join_network)
charlie.connect_to_at(bob, bob_late_join_addr, 2)
events = wait_call_to_play_exact_events(charlie, expected)
if len(events) != 4:
raise ScenarioError(f"direct-author terminal late-join view is incomplete: {events}")
alice.shutdown()
wait_call_to_play_departure_with_exact_remaining_peers(
charlie,
set(),
"Alice creator",
{bob.peer_id},
alice.peer_id,
)
return (
"late joiner pulled terminal creator and participant slices directly; creator "
"departure removed the complete call despite the live participant author"
)
def run(command: list[str], description: str) -> subprocess.CompletedProcess[str]:
result = subprocess.run(
command,
cwd=REPO,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
if result.returncode != 0:
raise ScenarioError(f"{description} failed:\n{result.stdout}")
return result
def run_just_test() -> str:
env = os.environ.copy()
env["RUSTC_WRAPPER"] = ""
result = subprocess.run(
["just", "test"],
cwd=REPO,
env=env,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
if result.returncode != 0:
raise ScenarioError(f"just test failed:\n{result.stdout}")
return result.stdout
def reset_run_root() -> None:
if not RUN_ROOT.exists():
return
try:
shutil.rmtree(RUN_ROOT)
return
except PermissionError:
pass
subprocess.run(
[
"docker",
"run",
"--rm",
"-v",
f"{RUN_ROOT}:/cleanup",
"--entrypoint",
"/bin/sh",
"debian:bookworm-slim",
"-c",
"rm -rf /cleanup/* /cleanup/.[!.]* /cleanup/..?*",
],
cwd=REPO,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False,
)
shutil.rmtree(RUN_ROOT)
def copy_game(game_id: str, destination_games_dir: Path, *, version: str | None = None) -> None:
source = find_fixture_game(game_id)
destination = destination_games_dir / game_id
if destination.exists():
shutil.rmtree(destination)
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(source, destination)
version = version if version is not None else CATALOG_VERSIONS.get(game_id)
if version is not None:
(destination / "version.ini").write_text(version, encoding="utf-8")
def find_fixture_game(game_id: str) -> Path:
# Sorted so resolution is deterministic: several fixtures define the same
# game id (e.g. cnctw exists under fixture-bravo, fixture-multi and
# fixture-solid), and downstream assertions expect the fixture-bravo layout.
for fixture_dir in sorted(FIXTURES.iterdir()):
candidate = fixture_dir / game_id
if candidate.exists():
return candidate
raise ScenarioError(f"fixture game not found: {game_id}")
def stage_game_drop(destination_games_dir: Path, game_id: str) -> None:
source = find_fixture_game(game_id)
root = destination_games_dir / game_id
if root.exists():
shutil.rmtree(root)
root.mkdir(parents=True)
for child in source.iterdir():
if child.name == "version.ini":
continue
target = root / child.name
if child.is_dir():
shutil.copytree(child, target)
else:
shutil.copy2(child, target)
shutil.copy2(source / "version.ini", root / "version.ini")
def create_many_small_game(root: Path) -> None:
if root.exists():
shutil.rmtree(root)
root.mkdir(parents=True)
for index in range(20):
child = root / f"file-{index:02}.bin"
child.write_bytes(hashlib.sha256(f"small-{index}".encode()).digest() * 8)
(root / "version.ini").write_text(CATALOG_VERSIONS.get(root.name, "20250101"), encoding="utf-8")
def create_large_sparse_game(root: Path, *, size: int, version: str | None = None) -> None:
if root.exists():
shutil.rmtree(root)
root.mkdir(parents=True)
resolved = version if version is not None else CATALOG_VERSIONS.get(root.name, PERF_GAME_VERSION)
(root / "version.ini").write_text(resolved, encoding="utf-8")
archive = root / f"{root.name}.eti"
with archive.open("wb") as handle:
handle.truncate(size)
def inflate_archive_sparse(game_root: Path, game_id: str, size: int) -> None:
"""Replace a copied game's `.eti` with a sparse file of `size` bytes so the
archive spans multiple 128 MiB download chunks. Sparse zero bytes are
identical across copies, so one publisher-derived catalog manifest
authorizes every copy and a `diff` against any source still matches."""
archive = game_root / f"{game_id}.eti"
with archive.open("wb") as handle:
handle.truncate(size)
def corrupt_file_same_length(path: Path) -> None:
"""Flip one payload byte without changing the catalog-owned file shape."""
with path.open("r+b") as handle:
first = handle.read(1)
if not first:
raise ScenarioError(f"cannot corrupt empty fixture file: {path}")
handle.seek(0)
handle.write(bytes([first[0] ^ 0xFF]))
def sha256_file(path: Path) -> str:
hasher = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
hasher.update(chunk)
return hasher.hexdigest()
def unrar_entry_sha256(peer: Peer, game_id: str, relative_path: str) -> str:
command = (
f"unrar p -inul /games/{shlex.quote(game_id)}/{shlex.quote(game_id)}.eti "
f"{shlex.quote(relative_path)} | sha256sum"
)
output = peer.docker_exec("sh", "-c", command).stdout.strip()
if not output:
raise ScenarioError(f"empty sha256 output for {game_id}:{relative_path}")
return output.split()[0]
def assert_peer_rar_archive_solid(peer: Peer, game_id: str) -> None:
output = peer.docker_exec(
"unrar",
"lt",
"-cfg-",
f"/games/{game_id}/{game_id}.eti",
).stdout
for line in output.splitlines():
stripped = line.strip()
if stripped.startswith("Details:"):
if "solid" in stripped.lower():
return
raise ScenarioError(f"RAR archive is not solid: {game_id}")
raise ScenarioError(f"RAR archive details were not reported: {game_id}")
def peer_id_sort_key(peer_id: str | None) -> bytes:
if peer_id is None:
raise ScenarioError("cannot sort missing peer identity")
padded = peer_id.upper() + "=" * (-len(peer_id) % 8)
try:
decoded = base64.b32decode(padded)
except (binascii.Error, ValueError) as error:
raise ScenarioError(f"invalid peer identity {peer_id!r}") from error
if len(decoded) != 32:
raise ScenarioError(f"invalid peer identity length for {peer_id!r}")
return decoded
def format_bytes(size: int) -> str:
return f"{size / 1024 / 1024 / 1024:.2f} GiB"
def connect_many(client: Peer, peers: list[Peer]) -> None:
for peer in peers:
client.connect_to(peer)
client.send({"cmd": "wait-peers", "count": len(peers), "timeout_ms": 15000})
def connect_bidirectional(left: Peer, right: Peer) -> None:
left.connect_to(right)
right.connect_to(left)
wait_peer_visible(left, right.peer_id)
wait_peer_visible(right, left.peer_id)
def catalog_content_id(peer: Peer, game_id: str) -> str:
catalog_dir = peer.catalog_dir or CATALOG_PROFILES / "default"
manifest_path = catalog_dir / "manifests" / f"{game_id}.json"
try:
artifact = json.loads(manifest_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
raise ScenarioError(
f"failed to load catalog manifest for {peer.name}/{game_id}: {manifest_path}: {error}"
) from error
content_id = artifact.get("content_id")
if (
not isinstance(content_id, str)
or len(content_id) != 64
or content_id.lower() != content_id
or any(char not in "0123456789abcdef" for char in content_id)
):
raise ScenarioError(
f"catalog manifest has invalid content_id for {peer.name}/{game_id}: {content_id!r}"
)
return content_id
def wait_remote_library_content(
peer: Peer,
game_id: str,
content_id: str,
*,
peer_count: int,
timeout: float = 20,
) -> dict[str, Any]:
deadline = time.monotonic() + timeout
last_view: list[dict[str, Any]] = []
while time.monotonic() < deadline:
latest = next(
(
item
for item in reversed(peer.output)
if item.get("type") == "event"
and item.get("event") == "remote-library-view"
),
None,
)
if latest is not None:
last_view = latest.get("data", {}).get("view", {}).get("games", [])
for row in last_view:
if (
row.get("game_id") == game_id
and row.get("content_id") == content_id
and row.get("peer_count") == peer_count
):
return row
time.sleep(0.2)
raise ScenarioError(
f"{peer.name} never received remote-library-view {game_id}/{content_id} "
f"peer_count={peer_count}; latest={last_view}"
)
def wait_remote_library_without_game(
peer: Peer,
game_id: str,
*,
start: int,
timeout: float = 20,
) -> list[dict[str, Any]]:
deadline = time.monotonic() + timeout
last_view: list[dict[str, Any]] | None = None
while time.monotonic() < deadline:
latest = next(
(
item
for item in reversed(peer.output[start:])
if item.get("type") == "event"
and item.get("event") == "remote-library-view"
),
None,
)
if latest is not None:
last_view = latest.get("data", {}).get("view", {}).get("games", [])
if all(row.get("game_id") != game_id for row in last_view):
return last_view
time.sleep(0.2)
raise ScenarioError(
f"{peer.name} never received a post-connect remote-library-view without "
f"{game_id}; latest={last_view}"
)
def wait_peer_visible(
observer: Peer,
peer_id: str | None,
timeout: float = 20,
) -> dict[str, Any]:
if peer_id is None:
raise ScenarioError("cannot wait for a peer without peer_id")
deadline = time.monotonic() + timeout
last_peers: list[dict[str, Any]] = []
while time.monotonic() < deadline:
last_peers = observer.list_peers()
for peer in last_peers:
if peer.get("peer_id") == peer_id:
return peer
time.sleep(0.2)
raise ScenarioError(
f"{observer.name} never saw peer {peer_id}; peers={last_peers}"
)
def wait_remote_game(
peer: Peer,
game_id: str,
*,
peer_count: int | None = None,
version: str | None = None,
timeout: float = 20,
) -> dict[str, Any]:
deadline = time.monotonic() + timeout
last_rows: list[dict[str, Any]] = []
while time.monotonic() < deadline:
rows = peer.list_games()["remote"]
last_rows = rows
for row in rows:
if row["id"] != game_id:
continue
if peer_count is not None and row.get("peer_count") != peer_count:
continue
if version is not None and row.get("eti_game_version") != version:
continue
return row
time.sleep(0.4)
raise ScenarioError(
f"{peer.name} never saw remote {game_id} peer_count={peer_count} version={version}; rows={last_rows}"
)
def wait_remote_absent(peer: Peer, game_id: str, timeout: float = 20) -> None:
deadline = time.monotonic() + timeout
last_rows: list[dict[str, Any]] = []
while time.monotonic() < deadline:
rows = peer.list_games()["remote"]
last_rows = rows
if all(row["id"] != game_id for row in rows):
return
time.sleep(0.4)
raise ScenarioError(f"{peer.name} still lists remote {game_id}; rows={last_rows}")
def wait_departure_topology(
observer: Peer,
*,
expected_peer_ids: set[str | None],
expected_remote_game_ids: set[str],
phase: str,
) -> None:
if None in expected_peer_ids:
raise ScenarioError(f"{phase} cannot wait on a missing peer identity")
deadline = time.monotonic() + PEER_DEPARTURE_TIMEOUT_SECONDS
last_peers: list[dict[str, Any]] = []
last_remote: list[dict[str, Any]] = []
while time.monotonic() < deadline:
last_peers = observer.list_peers()
last_remote = observer.list_games()["remote"]
actual_peer_ids = {peer.get("peer_id") for peer in last_peers}
actual_remote_game_ids = {game.get("id") for game in last_remote}
if (
actual_peer_ids == expected_peer_ids
and actual_remote_game_ids == expected_remote_game_ids
):
return
time.sleep(0.4)
raise ScenarioError(
f"{phase} did not converge after the {PEER_DEPARTURE_TIMEOUT_SECONDS}s "
f"liveness allowance: expected peers={expected_peer_ids}, "
f"remote_games={expected_remote_game_ids}; peers={last_peers}, "
f"remote={last_remote}"
)
def wait_local_game(
peer: Peer,
game_id: str,
*,
downloaded: bool | None = None,
installed: bool | None = None,
timeout: float = 20,
) -> dict[str, Any]:
deadline = time.monotonic() + timeout
last_rows: list[dict[str, Any]] = []
while time.monotonic() < deadline:
rows = peer.list_games()["local"]
last_rows = rows
for row in rows:
if row["id"] != game_id:
continue
if downloaded is not None and row.get("downloaded") != downloaded:
continue
if installed is not None and row.get("installed") != installed:
continue
return row
time.sleep(0.4)
raise ScenarioError(
f"{peer.name} never reached local {game_id} downloaded={downloaded} installed={installed}; rows={last_rows}"
)
def wait_no_active(peer: Peer, game_id: str, timeout: float = 20) -> None:
deadline = time.monotonic() + timeout
last_active: list[dict[str, Any]] = []
while time.monotonic() < deadline:
active = peer.status()["active_operations"]
last_active = active
if all(item["game_id"] != game_id for item in active):
return
time.sleep(0.4)
raise ScenarioError(f"{peer.name} still has active operation for {game_id}: {last_active}")
def wait_outbound_transfer(
peer: Peer,
game_id: str,
*,
minimum: int = 1,
timeout: float = 20,
) -> int:
deadline = time.monotonic() + timeout
last_active: dict[str, int] = {}
while time.monotonic() < deadline:
active = peer.status()["active_outbound_transfers"]
last_active = active
count = int(active.get(game_id, 0))
if count >= minimum:
return count
time.sleep(0.02)
raise ScenarioError(
f"{peer.name} never reached {minimum} active outbound transfers for {game_id}: "
f"{last_active}"
)
def wait_no_outbound_transfer(peer: Peer, game_id: str, timeout: float = 20) -> None:
deadline = time.monotonic() + timeout
last_active: dict[str, int] = {}
while time.monotonic() < deadline:
active = peer.status()["active_outbound_transfers"]
last_active = active
if active.get(game_id, 0) == 0:
return
time.sleep(0.4)
raise ScenarioError(
f"{peer.name} still has outbound transfer for {game_id}: {last_active}"
)
def wait_outbound_transfer_cycle(
peer: Peer,
game_id: str,
*,
waiter: LineWaiter,
phase: str,
timeout: float = 30,
) -> None:
for edge in ["admission", "drain"]:
peer.wait_for(
event_is("outbound-transfer-count-changed"),
timeout=timeout,
description=f"{phase} {peer.name} outbound {edge} edge",
waiter=waiter,
)
wait_no_outbound_transfer(peer, game_id, timeout=timeout)
def assert_no_outbound_edge_during(
peer: Peer,
*,
start: int,
phase: str,
timeout: float = 1.0,
) -> None:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
edges = [
item
for item in peer.output[start:]
if item.get("type") == "event"
and item.get("event") == "outbound-transfer-count-changed"
]
if edges:
raise ScenarioError(
f"{phase} unexpectedly admitted an outbound transfer on {peer.name}: "
f"{edges}"
)
time.sleep(0.05)
def wait_call_to_play_events(
peer: Peer,
expected_ids: set[str],
timeout: float = 20,
) -> list[dict[str, Any]]:
deadline = time.monotonic() + timeout
last_events: list[dict[str, Any]] = []
while time.monotonic() < deadline:
events = peer.call_to_play_events()
last_events = events
if expected_ids <= {event.get("id") for event in events}:
return events
time.sleep(0.2)
raise ScenarioError(
f"{peer.name} never received Call to Play events {expected_ids}: {last_events}"
)
def wait_call_to_play_exact_events(
peer: Peer,
expected_ids: set[str],
timeout: float = 20,
) -> list[dict[str, Any]]:
deadline = time.monotonic() + timeout
last_events: list[dict[str, Any]] = []
while time.monotonic() < deadline:
events = peer.call_to_play_events()
last_events = events
if {event.get("id") for event in events} == expected_ids:
return events
time.sleep(0.2)
raise ScenarioError(
f"{peer.name} never converged to exact Call to Play replacement {expected_ids}: "
f"{last_events}"
)
def wait_call_to_play_departure_with_exact_remaining_peers(
peer: Peer,
expected_event_ids: set[str],
departed_author: str,
expected_remaining_peer_ids: set[str | None],
departed_peer_id: str | None,
) -> list[dict[str, Any]]:
if None in expected_remaining_peer_ids or departed_peer_id is None:
raise ScenarioError("cannot prove departure liveness with a missing identity")
allowed_peer_ids = {*expected_remaining_peer_ids, departed_peer_id}
deadline = time.monotonic() + PEER_DEPARTURE_TIMEOUT_SECONDS
last_events: list[dict[str, Any]] = []
last_peers: list[dict[str, Any]] = []
while time.monotonic() < deadline:
peers = peer.list_peers()
last_peers = peers
actual_peer_ids = {item.get("peer_id") for item in peers}
if not expected_remaining_peer_ids <= actual_peer_ids:
raise ScenarioError(
f"{departed_author} departure lost a peer that must remain live: "
f"expected {expected_remaining_peer_ids}, found {actual_peer_ids}; "
f"peers={peers}"
)
if not actual_peer_ids <= allowed_peer_ids:
raise ScenarioError(
f"{departed_author} departure observed unexpected authenticated peers: "
f"{actual_peer_ids}; peers={peers}"
)
events = peer.call_to_play_events()
last_events = events
if (
{event.get("id") for event in events} == expected_event_ids
and actual_peer_ids == expected_remaining_peer_ids
):
return events
time.sleep(0.2)
raise ScenarioError(
f"{departed_author} departure did not converge after the "
f"{PEER_DEPARTURE_TIMEOUT_SECONDS}s liveness allowance while peers "
f"{expected_remaining_peer_ids} stayed live: events={last_events}, "
f"peers={last_peers}"
)
def assert_exact_peer_ids(peer: Peer, expected_ids: set[str | None]) -> None:
if None in expected_ids:
raise ScenarioError("cannot assert an exact peer set with a missing identity")
peers = peer.list_peers()
actual_ids = {item.get("peer_id") for item in peers}
if actual_ids != expected_ids:
raise ScenarioError(
f"{peer.name} authenticated unexpected peers: {actual_ids} != "
f"{expected_ids}; peers={peers}"
)
def wait_call_to_play_exact_events_from_exact_peers(
peer: Peer,
expected_event_ids: set[str],
expected_peer_ids: set[str | None],
timeout: float = 20,
) -> list[dict[str, Any]]:
deadline = time.monotonic() + timeout
last_events: list[dict[str, Any]] = []
while time.monotonic() < deadline:
assert_exact_peer_ids(peer, expected_peer_ids)
events = peer.call_to_play_events()
last_events = events
if {event.get("id") for event in events} == expected_event_ids:
assert_exact_peer_ids(peer, expected_peer_ids)
return events
time.sleep(0.2)
raise ScenarioError(
f"{peer.name} never converged to exact Call to Play replacement "
f"{expected_event_ids} while restricted to peers {expected_peer_ids}: "
f"{last_events}"
)
def assert_game_state(
game: dict[str, Any],
*,
downloaded: bool,
installed: bool,
availability: str,
) -> None:
if (
game.get("downloaded") != downloaded
or game.get("installed") != installed
or game.get("availability") != availability
):
raise ScenarioError(
f"unexpected game state for {game.get('id')}: "
f"downloaded={game.get('downloaded')} installed={game.get('installed')} "
f"availability={game.get('availability')}"
)
def wait_peer_has_game(
observer: Peer,
peer_id: str | None,
game_id: str,
content_id: str,
timeout: float = 20,
) -> dict[str, Any]:
if peer_id is None:
raise ScenarioError("cannot wait for a peer without peer_id")
deadline = time.monotonic() + timeout
last_peers: list[dict[str, Any]] = []
while time.monotonic() < deadline:
peers = observer.list_peers()
last_peers = peers
for peer in peers:
if peer.get("peer_id") != peer_id:
continue
if any(
game.get("game_id") == game_id
and game.get("content_id") == content_id
for game in peer.get("games", [])
):
return peer
time.sleep(0.4)
raise ScenarioError(
f"{observer.name} never saw peer {peer_id} advertise exact {game_id}/{content_id}; "
f"peers={last_peers}"
)
def wait_peer_without_game(
observer: Peer,
peer_id: str | None,
game_id: str,
timeout: float = 20,
) -> dict[str, Any]:
if peer_id is None:
raise ScenarioError("cannot wait for a peer without peer_id")
deadline = time.monotonic() + timeout
last_peer: dict[str, Any] | None = None
while time.monotonic() < deadline:
for peer in observer.list_peers():
if peer.get("peer_id") != peer_id:
continue
last_peer = peer
if peer.get("library_revision") is None:
break
if all(game.get("game_id") != game_id for game in peer.get("games", [])):
return peer
time.sleep(0.4)
raise ScenarioError(
f"{observer.name} never observed an authoritative snapshot without {game_id} "
f"from {peer_id}; peer={last_peer}"
)
def assert_local_absent(peer: Peer, game_id: str) -> None:
rows = peer.list_games()["local"]
if any(
row["id"] == game_id and (row.get("downloaded") or row.get("installed"))
for row in rows
):
raise ScenarioError(f"{peer.name} advertises failed local {game_id}: {rows}")
def assert_no_active(peer: Peer, game_id: str) -> None:
status = peer.status()
active = status["active_operations"]
if any(item["game_id"] == game_id for item in active):
raise ScenarioError(f"{peer.name} still has active operation for {game_id}: {active}")
def assert_not_exists(path: Path) -> None:
if path.exists():
raise ScenarioError(f"expected path to be absent: {path}")
def assert_failed_stream_left_no_local(peer: Peer, game_id: str) -> None:
game_root = peer.host_games_dir / game_id
assert_local_absent(peer, game_id)
assert_not_exists(game_root / "local")
assert_not_exists(game_root / ".local.installing")
assert_not_exists(game_root / "version.ini")
assert_not_exists(game_root / f"{game_id}.eti")
def assert_download_status_trace(
peer: Peer,
game_id: str,
*,
start: int,
terminal: str | None,
reason: str | None = None,
expect_begin: bool = True,
expect_verification: bool = True,
expect_invalid_source_retry: bool = False,
) -> str:
status_names = {
"download-begin",
"download-progress",
"download-activity-changed",
"download-finished",
"download-failed",
}
status_events = [
item
for item in peer.output[start:]
if item.get("type") == "event"
and item.get("event") in status_names
and item.get("data", {}).get("game_id") == game_id
]
if not status_events:
raise ScenarioError(f"no attempt-keyed download status events for {game_id}")
attempt_ids = {
item.get("data", {}).get("attempt_id") for item in status_events
}
if len(attempt_ids) != 1:
raise ScenarioError(
f"download status crossed attempt IDs for {game_id}: {status_events}"
)
attempt_id = next(iter(attempt_ids))
if (
not isinstance(attempt_id, str)
or not attempt_id.isascii()
or not attempt_id.isdigit()
or (len(attempt_id) > 1 and attempt_id.startswith("0"))
):
raise ScenarioError(f"attempt ID is not a canonical decimal string: {attempt_id!r}")
begins = [item for item in status_events if item.get("event") == "download-begin"]
expected_begin_count = 1 if expect_begin else 0
if len(begins) != expected_begin_count:
raise ScenarioError(
f"unexpected begin count for {game_id}: {len(begins)} != {expected_begin_count}"
)
activities = [
item.get("data", {}).get("activity")
for item in status_events
if item.get("event") == "download-activity-changed"
]
expected_activities: list[str | None] = []
if expect_verification:
expected_activities.append("verifying-downloaded-chunks")
if expect_invalid_source_retry:
expected_activities.append("retrying-invalid-source")
expected_activities.append(None)
if activities != expected_activities:
raise ScenarioError(
f"verification activity trace for {game_id} was {activities}, "
f"expected {expected_activities}"
)
activity_indices = [
index
for index, item in enumerate(status_events)
if item.get("event") == "download-activity-changed"
]
if begins and activity_indices and status_events.index(begins[0]) >= activity_indices[0]:
raise ScenarioError(
f"verification activity preceded download begin for {game_id}: {status_events}"
)
terminals = [
item
for item in status_events
if item.get("event") in {"download-finished", "download-failed"}
]
expected_terminal_count = 0 if terminal is None else 1
if len(terminals) != expected_terminal_count:
raise ScenarioError(
f"unexpected terminal count for {game_id}: {terminals}"
)
if terminal is not None:
observed = terminals[0]
if observed.get("event") != terminal:
raise ScenarioError(
f"download terminal for {game_id} was {observed.get('event')}, expected {terminal}"
)
observed_reason = observed.get("data", {}).get("reason")
if observed_reason != reason:
raise ScenarioError(
f"download terminal reason for {game_id} was {observed_reason!r}, "
f"expected {reason!r}"
)
terminal_index = status_events.index(observed)
if begins and status_events.index(begins[0]) >= terminal_index:
raise ScenarioError(
f"download terminal preceded begin for {game_id}: {status_events}"
)
if activities and not any(
index < terminal_index
and item.get("event") == "download-activity-changed"
and item.get("data", {}).get("activity") is None
for index, item in enumerate(status_events)
):
raise ScenarioError(
f"download activity did not clear before terminal for {game_id}: {status_events}"
)
elif reason is not None:
raise ScenarioError("a reason cannot be expected without a terminal event")
observed_milestones: list[tuple[str, str | None]] = []
for item in status_events:
event = item.get("event")
data = item.get("data", {})
if event == "download-begin":
observed_milestones.append((event, None))
elif event == "download-activity-changed":
observed_milestones.append((event, data.get("activity")))
elif event in {"download-finished", "download-failed"}:
observed_milestones.append((event, data.get("reason")))
expected_milestones: list[tuple[str, str | None]] = []
if expect_begin:
expected_milestones.append(("download-begin", None))
if expect_verification:
expected_milestones.append(
("download-activity-changed", "verifying-downloaded-chunks")
)
if expect_invalid_source_retry:
expected_milestones.append(
("download-activity-changed", "retrying-invalid-source")
)
expected_milestones.append(("download-activity-changed", None))
if terminal is not None:
expected_milestones.append((terminal, reason))
if observed_milestones != expected_milestones:
raise ScenarioError(
f"download status milestones for {game_id} were {observed_milestones}, "
f"expected {expected_milestones}"
)
final_status = status_events[-1]
if terminal is not None:
final_status_is_expected = final_status.get("event") == terminal
else:
final_status_is_expected = (
final_status.get("event") == "download-activity-changed"
and final_status.get("data", {}).get("activity") is None
)
if not final_status_is_expected:
raise ScenarioError(
f"download status continued after settlement for {game_id}: {status_events}"
)
return attempt_id
def assert_stream_install_lifecycle(
peer: Peer,
game_id: str,
*,
start: int,
content_id: str,
source_peer_id: str | None,
source_addr: str | None,
expect_invalid_source_retry: bool = False,
) -> None:
if source_peer_id is None or source_addr is None:
raise ScenarioError("cannot prove streamed lifecycle without an exact source endpoint")
window = peer.output[start:]
failure_events = [
item
for item in window
if item.get("type") == "event"
and item.get("event")
in {"download-failed", "install-failed"}
and item.get("data", {}).get("game_id") == game_id
]
if failure_events:
raise ScenarioError(
f"successful streamed install emitted failure events for {game_id}: "
f"{failure_events}"
)
lifecycle: list[tuple[Any, ...]] = []
expected_chunks = {
"bin/cnctw-payload.bin": 2 * 1024 * 1024,
"data/cnctw-assets.dat": 1024 * 1024,
}
for item in window:
if item.get("type") != "event":
continue
event = item.get("event")
data = item.get("data", {})
if event == "active-operations-changed":
active_operations = data.get("active_operations", [])
matching = [
active
for active in active_operations
if active.get("game_id") == game_id
]
if matching:
if len(matching) != 1:
raise ScenarioError(
f"streamed install published duplicate active entries: {item}"
)
lifecycle.append(("active", matching[0].get("operation")))
elif active_operations == []:
lifecycle.append(("active", "empty"))
continue
if data.get("game_id") != game_id:
continue
if event == "download-chunk-finished":
path = data.get("relative_path")
if (
path not in expected_chunks
or int(data.get("offset", -1)) != 0
or int(data.get("length", -1)) != expected_chunks[path]
or data.get("content_id") != content_id
or data.get("peer_id") != source_peer_id
or data.get("peer_addr") != source_addr
):
raise ScenarioError(
f"streamed install emitted a non-authoritative verified chunk: {data}"
)
lifecycle.append(("chunk", path))
continue
if event == "download-activity-changed":
lifecycle.append(("activity", data.get("activity")))
continue
if event == "download-finished":
throughput = data.get("throughput", {})
if throughput.get("bytes") != 3 * 1024 * 1024 or throughput.get("chunks") != 2:
raise ScenarioError(
f"streamed install throughput was not exact: {throughput}"
)
if event in {
"download-begin",
"download-finished",
"install-finished",
"download-failed",
"install-failed",
}:
lifecycle.append((event,))
expected_lifecycle = [
("active", "Downloading"),
("download-begin",),
("activity", "verifying-downloaded-chunks"),
]
if expect_invalid_source_retry:
expected_lifecycle.append(("activity", "retrying-invalid-source"))
expected_lifecycle.extend([
("chunk", "bin/cnctw-payload.bin"),
("chunk", "data/cnctw-assets.dat"),
("activity", None),
("active", "Installing"),
("download-finished",),
("active", "empty"),
("install-finished",),
])
if lifecycle != expected_lifecycle:
raise ScenarioError(
f"streamed install lifecycle for {game_id} was not exact: "
f"{lifecycle} != {expected_lifecycle}"
)
assert_download_status_trace(
peer,
game_id,
start=start,
terminal="download-finished",
expect_invalid_source_retry=expect_invalid_source_retry,
)
def event_is(event: str, game_id: str | None = None) -> Callable[[dict[str, Any]], bool]:
def predicate(item: dict[str, Any]) -> bool:
if item.get("type") != "event" or item.get("event") != event:
return False
if game_id is None:
return True
return item.get("data", {}).get("game_id") == game_id
return predicate
def event_name_in(events: set[str], game_id: str | None = None) -> Callable[[dict[str, Any]], bool]:
def predicate(item: dict[str, Any]) -> bool:
if item.get("type") != "event" or item.get("event") not in events:
return False
if game_id is None:
return True
return item.get("data", {}).get("game_id") == game_id
return predicate
def assert_no_event_since(peer: Peer, start: int, event: str, game_id: str) -> None:
for item in peer.output[start:]:
if item.get("type") == "event" and item.get("event") == event:
if item.get("data", {}).get("game_id") == game_id:
raise ScenarioError(f"unexpected {event} for {game_id}: {item}")
def assert_only_chunk_sources(
peer: Peer,
game_id: str,
allowed_sources: set[str | None],
) -> None:
allowed = {source for source in allowed_sources if source is not None}
if not allowed:
raise ScenarioError("no allowed chunk sources supplied")
seen: set[str] = set()
for item in peer.output:
if item.get("type") != "event" or item.get("event") != "download-chunk-finished":
continue
data = item["data"]
if data.get("game_id") != game_id:
continue
source = data.get("peer_id")
seen.add(source)
if source not in allowed:
raise ScenarioError(f"unexpected chunk source for {game_id}: {data}")
if not seen:
raise ScenarioError(f"no chunk events recorded for {game_id}")
def assert_chunk_authority(
peer: Peer,
game_id: str,
content_id: str,
allowed_peer_ids: set[str | None],
*,
start: int = 0,
) -> None:
allowed = {peer_id for peer_id in allowed_peer_ids if peer_id is not None}
if not allowed:
raise ScenarioError("no authenticated chunk source identities supplied")
chunks = [
item["data"]
for item in peer.output[start:]
if item.get("type") == "event"
and item.get("event") == "download-chunk-finished"
and item.get("data", {}).get("game_id") == game_id
]
if not chunks:
raise ScenarioError(f"no verified chunk events recorded for {game_id}")
for chunk in chunks:
if chunk.get("content_id") != content_id:
raise ScenarioError(
f"chunk used wrong content identity for {game_id}: {chunk}"
)
if chunk.get("peer_id") not in allowed:
raise ScenarioError(
f"chunk used unauthenticated/unexpected source for {game_id}: {chunk}"
)
def streamed_chunk_paths(peer: Peer, game_id: str) -> list[str]:
return [
item["data"]["relative_path"]
for item in peer.output
if item.get("type") == "event"
and item.get("event") == "download-chunk-finished"
and item.get("data", {}).get("game_id") == game_id
]
def chunk_totals(peer: Peer, game_id: str, relative_path: str) -> dict[str, int]:
totals: dict[str, int] = {}
for item in peer.output:
if item.get("type") != "event" or item.get("event") != "download-chunk-finished":
continue
data = item["data"]
if data.get("game_id") != game_id or data.get("relative_path") != relative_path:
continue
totals[data["peer_id"]] = totals.get(data["peer_id"], 0) + int(data["length"])
return totals
def chunk_sources(peer: Peer, game_id: str) -> set[str]:
return {
item["data"]["peer_id"]
for item in peer.output
if item.get("type") == "event"
and item.get("event") == "download-chunk-finished"
and item.get("data", {}).get("game_id") == game_id
}
def assert_no_duplicate_chunks(peer: Peer, game_id: str) -> None:
seen: set[tuple[str | None, int]] = set()
for item in peer.output:
if item.get("type") != "event" or item.get("event") != "download-chunk-finished":
continue
data = item["data"]
if data.get("game_id") != game_id:
continue
key = (data.get("relative_path"), int(data.get("offset", 0)))
if key in seen:
raise ScenarioError(f"{peer.name} downloaded a duplicate chunk for {game_id}: {key}")
seen.add(key)
def count_events(peer: Peer, event: str, game_id: str) -> int:
return sum(
1
for item in peer.output
if item.get("type") == "event"
and item.get("event") == event
and item.get("data", {}).get("game_id") == game_id
)
def diff_game_dirs(source: Path, destination: Path) -> None:
source_manifest = manifest(source)
destination_manifest = manifest(destination)
if source_manifest != destination_manifest:
diff = run_diff(source, destination)
raise ScenarioError(
f"manifest mismatch between {source} and {destination}\n{diff}"
)
diff = run_diff(source, destination)
if diff:
raise ScenarioError(f"diff mismatch between {source} and {destination}\n{diff}")
def manifest(root: Path) -> dict[str, str]:
if not root.exists():
raise ScenarioError(f"missing manifest root: {root}")
entries: dict[str, str] = {}
for path in sorted(root.rglob("*")):
if not path.is_file():
continue
rel = path.relative_to(root)
if any(part in IGNORED_DIFF_NAMES for part in rel.parts):
continue
hasher = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
hasher.update(chunk)
entries[str(rel)] = hasher.hexdigest()
return entries
def run_diff(source: Path, destination: Path) -> str:
command = [
"diff",
"-r",
"-x",
".lanspread",
"-x",
".lanspread.json",
"-x",
"local",
str(source),
str(destination),
]
result = subprocess.run(
command,
cwd=REPO,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
return result.stdout if result.returncode else ""
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"scenarios",
nargs="*",
help="Scenario IDs to run, e.g. S18 S20. Defaults to all implemented scenarios.",
)
parser.add_argument(
"--build-image",
action="store_true",
help="Run `just peer-cli-image` before starting scenarios.",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
selected = {item.lower() for item in args.scenarios} if args.scenarios else None
try:
Runner(selected=selected, build_image=args.build_image).run()
except ScenarioError as error:
print(f"\nFAILED: {error}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())