Everything on the admin GameScript channel so far has been request/reply. This adds the other direction: subscribe_events() opens a push stream so a bot can react to the game instead of polling it, consumed either by awaiting wait_for_event() or via an on_event callback. Both see every event; an event goes to at most one waiter, and unclaimed ones sit in a bounded buffer. Sixteen kinds, from two sources. The engine raises no GameScript event for a vehicle reaching a stop or cargo arriving, so vehicle_arrive, vehicle_depart and cargo_waiting are synthesised by the bridge sampling state every `interval` ticks and diffing against the previous sample -- which means a stop shorter than the interval is never reported, and the first sample only establishes a baseline. The rest (crashes, industries, towns, companies, subsidies) are engine events forwarded verbatim. vehicle_lost, vehicle_waiting_in_depot and vehicle_unprofitable are deliberately absent: the engine raises those only for AI companies, so a GameScript can never observe them. The server-side half lives in the AdminBridge GameScript, which is not in this repo -- docker/config is gitignored -- so it has to be updated separately for any of this to work. Also repoints the scheduled-dispatch E2E test at a dedicated vehicle (DISPATCH_VEHICLE_ID). It had been silently skipping because vehicle 7 carries a hand-built annual dispatch schedule, which left eight dispatch methods unverified end to end while check_public_calls.py reported them green off static analysis of the call sites. Co-Authored-By: Claude <[email protected]>
4.2 KiB
Testing Guide for OpenTTD Python Client
This guide explains how the test suite is structured, the types of tests available, and how to execute them.
📂 Test Suite Structure
The tests are located in the tests/ directory:
| Test File | Target | Description |
|---|---|---|
test_admin.py |
OpenTTDAdminClient |
Tests admin client initialization, admin packet types, and basic protocol constants. |
test_protocol.py |
OpenTTDProtocol |
Tests binary serialization, custom parsers, and stream encryption/decryption (XChaCha20-Poly1305). |
test_logic.py |
OpenTTDClient |
Tests client connection lifecycle, company joining flow, authentication, and state management. |
test_events.py |
Game Events | Tests event subscription encoding, the push/pull consumption paths (on_event, wait_for_event()), buffering and waiter lifecycle. |
test_coverage.py |
Coverage Helpers | Auxiliary unit tests targeting connection errors, fallback packet handlers, and missing passwords to ensure high test coverage. |
test_e2e.py |
Integration / E2E | Connects to a running local OpenTTD server (e.g., in Docker) to verify full socket interactions, stream cryptography, and keep-alive frames. |
🚀 How to Run Tests
Pytest automatically uses pytest.ini to configure the Python import path (pythonpath = lib). You do not need to manually configure PYTHONPATH.
Make sure your virtual environment is active before running commands:
source venv/bin/activate
1. Run Normal (Non-E2E) Tests
These are unit and logic tests that run instantly in memory without external dependencies:
pytest -m "not e2e"
2. Run E2E Tests Only
Requires a running local OpenTTD server configured with password "asd". If the server is offline, this test will fail:
pytest -m "e2e"
🐳 Starting the OpenTTD Server (Docker)
To run E2E tests locally, you can start the dedicated JGRPP OpenTTD server using the provided Docker Compose configuration in the docker/ directory.
1. Build and Start the Server
Run this command from the project root directory:
docker compose -f docker/docker-compose.yml up -d --build
Note: The first build will clone and compile the JGRPP source code, which may take a few minutes.
2. Monitor Server Logs
To watch the server logs (e.g., to see client connections and events):
docker compose -f docker/docker-compose.yml logs -f
3. Stop the Server
To stop the server container:
docker compose -f docker/docker-compose.yml down
3. Run All Tests
Runs both unit/logic tests and E2E tests:
pytest
4. Run with Coverage Report
To view statement coverage statistics:
pytest --cov=openttd --cov-report=term-missing
📊 Testing Mandate
We enforce the following testing mandates:
- Normal/Unit tests (
pytest -m "not e2e") must achieve 100% code coverage on the codebase independently. - E2E tests (
pytest -m "e2e") do NOT require 100% code coverage. Instead, they are only required to cover every public function of the client classes (OpenTTDClientandOpenTTDAdminClient) and protocol classes with multiple (varied) inputs/scenarios.
To run/verify tests:
- Normal Tests (Must achieve 100% coverage):
pytest -m "not e2e" --cov=openttd --cov-report=term-missing --cov-fail-under=100 - E2E Tests (No coverage mandate, must cover all public functions with multiple inputs):
pytest -m "e2e"
🔧 Pytest Configuration (pytest.ini)
The project uses a pytest.ini file at the root:
pythonpath = lib: Simplifies invocation by resolving imports from the locallib/directory.markers: Registers the custome2emarker for pytest classification.