|
|
|
@@ -28,7 +28,20 @@ CONTAINER_PREFIX = "lanspread-peer-cli-ext"
|
|
|
|
|
CATALOG_DB = "/app/game.db"
|
|
|
|
|
FIXTURES = REPO / "crates" / "lanspread-peer-cli" / "fixtures"
|
|
|
|
|
CHUNK_SIZE = 128 * 1024 * 1024
|
|
|
|
|
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"}
|
|
|
|
|
|
|
|
|
@@ -305,8 +318,8 @@ class Runner:
|
|
|
|
|
("S13", self.s13_exact_transfer_equality),
|
|
|
|
|
("S14", self.s14_large_multi_peer_chunking),
|
|
|
|
|
("S15", self.s15_three_way_version_skew),
|
|
|
|
|
("S16", self.s16_latest_fanout_with_stale),
|
|
|
|
|
("S17", self.s17_latest_conflict_rejection),
|
|
|
|
|
("S16", self.s16_catalog_fanout_with_stale),
|
|
|
|
|
("S17", self.s17_catalog_conflict_rejection),
|
|
|
|
|
("S18", self.s18_redundant_source_drop),
|
|
|
|
|
("S19", self.s19_sole_source_drop),
|
|
|
|
|
("S20", self.s20_receiver_write_failure),
|
|
|
|
@@ -325,8 +338,9 @@ class Runner:
|
|
|
|
|
("S33", self.s33_install_after_mutation),
|
|
|
|
|
("S34", self.s34_many_small_files),
|
|
|
|
|
("S35", self.s35_unknown_game_filtered),
|
|
|
|
|
("S36", self.s36_latest_singleton),
|
|
|
|
|
("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),
|
|
|
|
@@ -520,20 +534,20 @@ class Runner:
|
|
|
|
|
def s8_ambiguous_metadata_rejection(self) -> str:
|
|
|
|
|
dir_a = self.fixture_root / "s8-a"
|
|
|
|
|
dir_b = self.fixture_root / "s8-b"
|
|
|
|
|
copy_game("ggoo", dir_a, version="20260101")
|
|
|
|
|
copy_game("ggoo", dir_b, version="20260101")
|
|
|
|
|
copy_game("ggoo", dir_a)
|
|
|
|
|
copy_game("ggoo", dir_b)
|
|
|
|
|
with (dir_b / "ggoo" / "ggoo.eti").open("ab") as handle:
|
|
|
|
|
handle.write(b"conflict")
|
|
|
|
|
peer_a = self.peer("s8-a", games_dir=dir_a)
|
|
|
|
|
peer_b = self.peer("s8-b", games_dir=dir_b)
|
|
|
|
|
client = self.peer("s8-client")
|
|
|
|
|
connect_many(client, [peer_a, peer_b])
|
|
|
|
|
wait_remote_game(client, "ggoo", peer_count=2, version="20260101")
|
|
|
|
|
wait_remote_game(client, "ggoo", peer_count=2, version=CATALOG_VERSIONS["ggoo"])
|
|
|
|
|
waiter = LineWaiter(len(client.output))
|
|
|
|
|
client.send({"cmd": "download", "game_id": "ggoo", "install": False})
|
|
|
|
|
client.wait_for(event_is("download-failed", "ggoo"), timeout=30, description="ggoo failed", waiter=waiter)
|
|
|
|
|
assert_not_exists(client.host_games_dir / "ggoo" / "version.ini")
|
|
|
|
|
return "conflicting latest ggoo file sizes emitted download-failed and left no version.ini"
|
|
|
|
|
return "conflicting catalog-version ggoo file sizes emitted download-failed and left no version.ini"
|
|
|
|
|
|
|
|
|
|
def s9_missing_game(self) -> str:
|
|
|
|
|
client = self.peer("s9-client")
|
|
|
|
@@ -615,7 +629,7 @@ class Runner:
|
|
|
|
|
diff_game_dirs(source_dir / game_id, stage.host_games_dir / game_id)
|
|
|
|
|
client = self.peer("s14-client")
|
|
|
|
|
connect_many(client, [alpha, stage])
|
|
|
|
|
wait_remote_game(client, game_id, peer_count=2, version="20260520")
|
|
|
|
|
wait_remote_game(client, game_id, peer_count=2, version=PERF_GAME_VERSION)
|
|
|
|
|
waiter = LineWaiter(len(client.output))
|
|
|
|
|
client.send({"cmd": "download", "game_id": game_id, "install": False})
|
|
|
|
|
client.wait_for(event_is("download-finished", game_id), timeout=90, description="client finish", waiter=waiter)
|
|
|
|
@@ -629,7 +643,11 @@ class Runner:
|
|
|
|
|
return f"{game_id} downloaded from two sources, diff matched, chunk totals={totals}"
|
|
|
|
|
|
|
|
|
|
def s15_three_way_version_skew(self) -> str:
|
|
|
|
|
specs = [("s15-a", "20250101"), ("s15-b", "20250201"), ("s15-c", "20250301")]
|
|
|
|
|
specs = [
|
|
|
|
|
("s15-a", "20150101"),
|
|
|
|
|
("s15-b", "20160101"),
|
|
|
|
|
("s15-c", CATALOG_VERSIONS["cnc4"]),
|
|
|
|
|
]
|
|
|
|
|
peers = []
|
|
|
|
|
for name, version in specs:
|
|
|
|
|
game_dir = self.fixture_root / name
|
|
|
|
@@ -637,19 +655,19 @@ class Runner:
|
|
|
|
|
peers.append(self.peer(name, games_dir=game_dir))
|
|
|
|
|
client = self.peer("s15-client")
|
|
|
|
|
connect_many(client, peers)
|
|
|
|
|
wait_remote_game(client, "cnc4", peer_count=3, version="20250301")
|
|
|
|
|
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_only_chunk_sources(client, "cnc4", {peers[2].ready_addr})
|
|
|
|
|
diff_game_dirs(peers[2].host_games_dir / "cnc4", client.host_games_dir / "cnc4")
|
|
|
|
|
return "three-way skew selected only 20250301 peer and receiver diffed cleanly"
|
|
|
|
|
return "three-way skew exposed only the catalog-version peer and receiver diffed cleanly"
|
|
|
|
|
|
|
|
|
|
def s16_latest_fanout_with_stale(self) -> str:
|
|
|
|
|
def s16_catalog_fanout_with_stale(self) -> str:
|
|
|
|
|
specs = [
|
|
|
|
|
("s16-a", "20250101"),
|
|
|
|
|
("s16-b", "20250301"),
|
|
|
|
|
("s16-c", "20250301"),
|
|
|
|
|
("s16-a", "20180101"),
|
|
|
|
|
("s16-b", CATALOG_VERSIONS["alienswarm"]),
|
|
|
|
|
("s16-c", CATALOG_VERSIONS["alienswarm"]),
|
|
|
|
|
]
|
|
|
|
|
peers = []
|
|
|
|
|
for name, version in specs:
|
|
|
|
@@ -658,7 +676,7 @@ class Runner:
|
|
|
|
|
peers.append(self.peer(name, games_dir=game_dir))
|
|
|
|
|
client = self.peer("s16-client")
|
|
|
|
|
connect_many(client, peers)
|
|
|
|
|
wait_remote_game(client, "alienswarm", peer_count=3, version="20250301")
|
|
|
|
|
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=90, description="alienswarm finish", waiter=waiter)
|
|
|
|
@@ -667,13 +685,13 @@ class Runner:
|
|
|
|
|
if peers[0].ready_addr in totals:
|
|
|
|
|
raise ScenarioError(f"stale peer contributed chunks: {totals}")
|
|
|
|
|
diff_game_dirs(peers[1].host_games_dir / "alienswarm", client.host_games_dir / "alienswarm")
|
|
|
|
|
return f"latest B/C peers served alienswarm while stale A contributed zero; totals={totals}"
|
|
|
|
|
return f"catalog-version B/C peers served alienswarm while stale A contributed zero; totals={totals}"
|
|
|
|
|
|
|
|
|
|
def s17_latest_conflict_rejection(self) -> str:
|
|
|
|
|
def s17_catalog_conflict_rejection(self) -> str:
|
|
|
|
|
specs = [
|
|
|
|
|
("s17-a", "20250101", False),
|
|
|
|
|
("s17-b", "20250301", False),
|
|
|
|
|
("s17-c", "20250301", True),
|
|
|
|
|
("s17-a", "20150101", False),
|
|
|
|
|
("s17-b", CATALOG_VERSIONS["cnc4"], False),
|
|
|
|
|
("s17-c", CATALOG_VERSIONS["cnc4"], True),
|
|
|
|
|
]
|
|
|
|
|
peers = []
|
|
|
|
|
for name, version, conflict in specs:
|
|
|
|
@@ -685,12 +703,12 @@ class Runner:
|
|
|
|
|
peers.append(self.peer(name, games_dir=game_dir))
|
|
|
|
|
client = self.peer("s17-client")
|
|
|
|
|
connect_many(client, peers)
|
|
|
|
|
wait_remote_game(client, "cnc4", peer_count=3, version="20250301")
|
|
|
|
|
wait_remote_game(client, "cnc4", peer_count=2, version=CATALOG_VERSIONS["cnc4"])
|
|
|
|
|
waiter = LineWaiter(len(client.output))
|
|
|
|
|
client.send({"cmd": "download", "game_id": "cnc4", "install": False})
|
|
|
|
|
client.wait_for(event_is("download-failed", "cnc4"), timeout=30, description="cnc4 failed", waiter=waiter)
|
|
|
|
|
assert_not_exists(client.host_games_dir / "cnc4" / "version.ini")
|
|
|
|
|
return "latest-version file conflict failed download and left no committed version.ini"
|
|
|
|
|
return "catalog-version file conflict failed download and left no committed version.ini"
|
|
|
|
|
|
|
|
|
|
def s18_redundant_source_drop(self) -> str:
|
|
|
|
|
source_a_dir = self.fixture_root / "s18-a"
|
|
|
|
@@ -773,13 +791,13 @@ class Runner:
|
|
|
|
|
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="20250101")
|
|
|
|
|
copy_game("cnc4", bravo_dir, version="20160101")
|
|
|
|
|
bravo = self.peer("s23-bravo", games_dir=bravo_dir)
|
|
|
|
|
connect_many(alpha, [bravo])
|
|
|
|
|
wait_remote_game(alpha, "cnc4", peer_count=1, version="20250101")
|
|
|
|
|
(bravo_dir / "cnc4" / "version.ini").write_text("20260501", encoding="utf-8")
|
|
|
|
|
wait_remote_game(alpha, "cnc4", peer_count=1, version="20260501")
|
|
|
|
|
return "alpha observed cnc4 eti_game_version change 20250101 -> 20260501 without reconnect"
|
|
|
|
|
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)
|
|
|
|
@@ -876,11 +894,11 @@ class Runner:
|
|
|
|
|
def s30_mesh_aggregation(self) -> str:
|
|
|
|
|
dirs = []
|
|
|
|
|
specs = [
|
|
|
|
|
("s30-a", [("ggoo", "20250101"), ("bf1942", "20250101")]),
|
|
|
|
|
("s30-b", [("ggoo", "20250101"), ("cnc4", "20250101")]),
|
|
|
|
|
("s30-c", [("cnc4", "20250301"), ("cod5", "20250101")]),
|
|
|
|
|
("s30-d", [("cnctw", "20250101"), ("coh", "20250101")]),
|
|
|
|
|
("s30-e", [("cnctw", "20250201"), ("bf1942", "20250201")]),
|
|
|
|
|
("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:
|
|
|
|
@@ -892,12 +910,12 @@ class Runner:
|
|
|
|
|
client = self.peer("s30-client")
|
|
|
|
|
connect_many(client, peers)
|
|
|
|
|
expected = {
|
|
|
|
|
"ggoo": (2, "20250101"),
|
|
|
|
|
"bf1942": (2, "20250201"),
|
|
|
|
|
"cnc4": (2, "20250301"),
|
|
|
|
|
"cod5": (1, "20250101"),
|
|
|
|
|
"cnctw": (2, "20250201"),
|
|
|
|
|
"coh": (1, "20250101"),
|
|
|
|
|
"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)
|
|
|
|
@@ -907,7 +925,7 @@ class Runner:
|
|
|
|
|
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/latest versions"
|
|
|
|
|
return f"client aggregated {len(expected)} IDs from 5 peers with expected peer_count/catalog versions"
|
|
|
|
|
|
|
|
|
|
def s31_bootstrapped_peer_source(self) -> str:
|
|
|
|
|
source = self.peer("s31-alpha", games_dir=FIXTURES / "fixture-alpha", readonly_games=True)
|
|
|
|
@@ -1003,34 +1021,34 @@ class Runner:
|
|
|
|
|
assert_not_exists(client.host_games_dir / "mystery-game")
|
|
|
|
|
return f"unknown game absent from list-games; download errored '{err['error']}'; no local files"
|
|
|
|
|
|
|
|
|
|
def s36_latest_singleton(self) -> str:
|
|
|
|
|
def s36_catalog_singleton(self) -> str:
|
|
|
|
|
peers = []
|
|
|
|
|
for index in range(5):
|
|
|
|
|
game_dir = self.fixture_root / f"s36-{index}"
|
|
|
|
|
version = "20260501" if index == 0 else "20250101"
|
|
|
|
|
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)
|
|
|
|
|
wait_remote_game(client, "cnc4", peer_count=5, version="20260501")
|
|
|
|
|
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})
|
|
|
|
|
got = client.wait_for(event_is("got-game-files", "cnc4"), timeout=20, description="got game files", waiter=waiter)
|
|
|
|
|
client.wait_for(event_is("download-finished", "cnc4"), timeout=60, description="download finish", waiter=waiter)
|
|
|
|
|
latest_addr = peers[0].ready_addr
|
|
|
|
|
if latest_addr is None:
|
|
|
|
|
raise ScenarioError("latest peer had no ready addr")
|
|
|
|
|
catalog_addr = peers[0].ready_addr
|
|
|
|
|
if catalog_addr is None:
|
|
|
|
|
raise ScenarioError("catalog-version peer had no ready addr")
|
|
|
|
|
for item in client.output:
|
|
|
|
|
if item.get("type") != "event" or item.get("event") != "download-chunk-finished":
|
|
|
|
|
continue
|
|
|
|
|
data = item["data"]
|
|
|
|
|
if data.get("game_id") == "cnc4" and data.get("peer_addr") != latest_addr:
|
|
|
|
|
if data.get("game_id") == "cnc4" and data.get("peer_addr") != catalog_addr:
|
|
|
|
|
raise ScenarioError(f"stale peer contributed chunk: {data}")
|
|
|
|
|
diff_game_dirs(peers[0].host_games_dir / "cnc4", client.host_games_dir / "cnc4")
|
|
|
|
|
descs = got["data"]["file_descriptions"]
|
|
|
|
|
if not descs:
|
|
|
|
|
raise ScenarioError("got-game-files had no descriptors")
|
|
|
|
|
return "client reported latest 20260501 with peer_count=5; only singleton latest peer sent chunks; diff matched"
|
|
|
|
|
return "client reported singleton catalog-version peer; stale peers stayed hidden and sent no chunks; diff matched"
|
|
|
|
|
|
|
|
|
|
def s37_single_source_download_throughput(self) -> str:
|
|
|
|
|
source_dir = self.fixture_root / "s37-source"
|
|
|
|
@@ -1038,7 +1056,7 @@ class Runner:
|
|
|
|
|
source = self.peer("s37-source", games_dir=source_dir)
|
|
|
|
|
client = self.peer("s37-client")
|
|
|
|
|
connect_many(client, [source])
|
|
|
|
|
wait_remote_game(client, PERF_GAME_ID, peer_count=1, version="20260520")
|
|
|
|
|
wait_remote_game(client, PERF_GAME_ID, peer_count=1, version=PERF_GAME_VERSION)
|
|
|
|
|
|
|
|
|
|
waiter = LineWaiter(len(client.output))
|
|
|
|
|
client.send({"cmd": "download", "game_id": PERF_GAME_ID, "install": False})
|
|
|
|
@@ -1057,7 +1075,7 @@ class Runner:
|
|
|
|
|
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("20260520")
|
|
|
|
|
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}"
|
|
|
|
@@ -1071,6 +1089,116 @@ class Runner:
|
|
|
|
|
f"{throughput['chunks']} chunks"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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 b"PersonaName = stubplayer\r\n" not in ini_file.read_bytes():
|
|
|
|
|
raise ScenarioError("installed SmartSteamEmu.ini did not preserve CRLF stub PersonaName")
|
|
|
|
|
|
|
|
|
|
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")
|
|
|
|
@@ -1574,6 +1702,7 @@ def copy_game(game_id: str, destination_games_dir: Path, *, version: str | None
|
|
|
|
|
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")
|
|
|
|
|
|
|
|
|
@@ -1610,14 +1739,14 @@ def create_many_small_game(root: Path) -> None:
|
|
|
|
|
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("20250101", encoding="utf-8")
|
|
|
|
|
(root / "version.ini").write_text(CATALOG_VERSIONS.get(root.name, "20250101"), encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_large_sparse_game(root: Path, *, size: int) -> None:
|
|
|
|
|
if root.exists():
|
|
|
|
|
shutil.rmtree(root)
|
|
|
|
|
root.mkdir(parents=True)
|
|
|
|
|
(root / "version.ini").write_text("20260520", encoding="utf-8")
|
|
|
|
|
(root / "version.ini").write_text(PERF_GAME_VERSION, encoding="utf-8")
|
|
|
|
|
archive = root / f"{root.name}.eti"
|
|
|
|
|
with archive.open("wb") as handle:
|
|
|
|
|
handle.truncate(size)
|
|
|
|
|