Merge branch 'main' into claude/silly-lederberg-231f6c
Conflicted only on the README feature list, where main's list_cargo() bullet and this branch's bridge bullet were added at the same spot. Kept both: list_cargo() with the other cargo queries, and the bridge one last, since it is about what all of them run against. list_cargo() landing also makes tests/test_gamescript.py's command check meaningful in the other direction -- the bridge has answered list_cargo all along with nothing sending it, which is exactly the asymmetry that check tolerates on purpose. Co-Authored-By: Claude <[email protected]>
This commit is contained in:
@@ -393,6 +393,40 @@ async def test_admin_get_station_cargo_error_response():
|
||||
await task
|
||||
assert client._gs_futures == {}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_list_cargo_request_and_response():
|
||||
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
|
||||
proto = MockProtocol()
|
||||
client._protocol = proto
|
||||
client._transport = MockTransport()
|
||||
|
||||
task = asyncio.ensure_future(client.list_cargo())
|
||||
await asyncio.sleep(0) # let the task send the request
|
||||
|
||||
# First use auto-subscribes to Gamescript updates, then sends the query.
|
||||
assert len(proto.sent) == 2
|
||||
assert proto.sent[0][2] == PacketAdminType.AdminUpdateFrequency
|
||||
assert decode_gamescript_payload(proto.sent[1]) == {
|
||||
"command": "list_cargo", "request_id": 1,
|
||||
}
|
||||
|
||||
response = {"command": "list_cargo", "request_id": 1,
|
||||
"cargo": [{"cargo_id": 0, "label": "PASS", "freight": 0},
|
||||
{"cargo_id": 1, "label": "COAL", "freight": 1}]}
|
||||
await client.receive_ServerGamescript(None, data=response)
|
||||
assert await task == response
|
||||
assert client._gs_futures == {}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_list_cargo_timeout():
|
||||
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
|
||||
client._protocol = MockProtocol()
|
||||
client._transport = MockTransport()
|
||||
|
||||
with pytest.raises(asyncio.TimeoutError):
|
||||
await client.list_cargo(timeout=0.05)
|
||||
assert client._gs_futures == {}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_get_dispatch_request_and_response():
|
||||
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
|
||||
|
||||
@@ -755,6 +755,44 @@ async def test_e2e_admin_get_station_cargo_invalid_cargo(connected_admin):
|
||||
with pytest.raises(ValueError, match="invalid_cargo"):
|
||||
await connected_admin.get_station_cargo(stations[0]["id"], 250, timeout=10.0)
|
||||
|
||||
@pytest.mark.e2e
|
||||
@pytest.mark.asyncio
|
||||
async def test_e2e_admin_list_cargo_table(connected_admin):
|
||||
# Public function: list_cargo()
|
||||
# Input 1: an explicit timeout. Every game has a cargo table, so an empty list would be a bug.
|
||||
data = await connected_admin.list_cargo(timeout=10.0)
|
||||
assert isinstance(data["cargo"], list) and data["cargo"]
|
||||
for cargo in data["cargo"]:
|
||||
for key in ("cargo_id", "label", "freight"):
|
||||
assert key in cargo
|
||||
assert cargo["freight"] in (0, 1)
|
||||
ids = [cargo["cargo_id"] for cargo in data["cargo"]]
|
||||
assert len(ids) == len(set(ids))
|
||||
# Any cargo set carries passengers as well as freight, so both kinds must show up.
|
||||
assert any(cargo["freight"] == 0 for cargo in data["cargo"])
|
||||
assert any(cargo["freight"] == 1 for cargo in data["cargo"])
|
||||
|
||||
@pytest.mark.e2e
|
||||
@pytest.mark.asyncio
|
||||
async def test_e2e_admin_list_cargo_resolves_station_cargo_ids(connected_admin):
|
||||
# Public function: list_cargo()
|
||||
# Input 2: the default timeout. The point of the call: naming the bare ids get_station() returns.
|
||||
responses = []
|
||||
connected_admin.on_gamescript = lambda data: responses.append(data)
|
||||
await connected_admin.update_frequency(AdminUpdateType.Gamescript, AdminUpdateFrequency.Automatic)
|
||||
await connected_admin.list_stations()
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
assert responses and "stations" in responses[-1]
|
||||
stations = responses[-1]["stations"]
|
||||
if not stations:
|
||||
pytest.skip("No stations on the test server to query.")
|
||||
|
||||
labels = {cargo["cargo_id"]: cargo["label"] for cargo in (await connected_admin.list_cargo())["cargo"]}
|
||||
detail = await connected_admin.get_station(stations[0]["id"], timeout=10.0)
|
||||
for cargo in detail["cargo"]:
|
||||
assert cargo["cargo_id"] in labels
|
||||
|
||||
|
||||
# --- Game Events ---
|
||||
|
||||
|
||||
@@ -67,7 +67,8 @@ def test_gamescript_implements_every_command_the_client_sends():
|
||||
"""Each command in a client payload must have a handler in the bridge's dispatch table.
|
||||
|
||||
A command the bridge does not know is silently dropped, so the caller only sees a timeout.
|
||||
The bridge may implement more than the client wraps (list_cargo currently has no method).
|
||||
Checked one way round only: the bridge is allowed to implement more than the client wraps,
|
||||
which is how list_cargo sat there answering nobody until a method was written for it.
|
||||
"""
|
||||
commands = set(re.findall(r"^\s*(\w+)\s*=\s*{ handler",
|
||||
_block(MAIN_NUT, "COMMANDS = {", "\n\t};"), re.MULTILINE))
|
||||
|
||||
Reference in New Issue
Block a user