Files
openttd-client/docs/TESTING.md
T
kovagoadiandClaude f7ca395a4f
Continuous Integration / lint-and-security (pull_request) Failing after 19s
Continuous Integration / tests-and-coverage (pull_request) Successful in 24s
Put the AdminBridge GameScript under version control
The last commit noted in passing that the server-side half of the admin
GameScript channel "is not in this repo -- docker/config is gitignored --
so it has to be updated separately for any of this to work." That was true
of all nine features the README documents: list_vehicles, list_stations,
list_cargo, get_timetable, get_station, get_station_cargo, get_dispatch and
the event stream all answer from 705 lines of Squirrel that no clone could
reproduce, no reviewer could see, and CI never touched.

The bridge now lives in gamescript/AdminBridge/ with its own README, the
same arrangement docker/patches/ uses for the local JGRPP patches, and
docker-compose.yml bind-mounts it read-only over the container's
game/AdminBridge. docker/config stays ignored -- it also holds savegames,
downloaded content and generated config -- so the copy under it is now
shadowed and can be deleted. main.nut is byte-identical to what was running,
apart from the version work below.

Adds a version handshake, because the channel gives no way to tell a stale
bridge from a hung one: a bridge that does not recognise a command drops it
silently, so a client ahead of the server sees nothing but timeouts. The
bridge now answers get_version with its protocol version plus its command
and event catalogues, and get_bridge_version() raises when that is below
GS_BRIDGE_VERSION. It is opt-in rather than checked on connect: GameScripts
do not tick while the game is paused, so an automatic check would refuse to
connect to a paused server. A bridge older than 4 predates get_version
itself and can only fail by timing out, so the E2E test catches that and
reports it by name instead.

tests/test_gamescript.py gives CI a foothold on the GameScript without a
Squirrel toolchain: it parses the .nut files and pins the protocol version
across info.nut, main.nut and protocol.py, the event catalogue against
GameEventType, and the command table against the commands client.py sends.
The version has to be declared three times because a GameScript cannot read
its own info.nut at runtime -- GSController.GetVersion() returns the OpenTTD
version, not the script's.

info.nut also gains MinVersionToLoad() { return 1; }. The engine defaults it
to GetVersion(), so without it this bump would orphan every savegame pinned
to version 3: the scanner finds no compatible script and falls back with a
warning. The bridge keeps no savegame state, so any version can take over.

HandleCommand now dispatches through the same table get_version reports,
rather than an if/else chain, so the catalogue a client feature-detects
against cannot drift from what is implemented.

Co-Authored-By: Claude <[email protected]>
2026-08-31 19:06:33 +02:00

4.6 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_gamescript.py AdminBridge GameScript Reads gamescript/AdminBridge/*.nut and checks it against the client: protocol version, event catalogue, command table, and that the docker setup serves the tracked copy. No Squirrel toolchain needed — see gamescript/AdminBridge/README.md.
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 (OpenTTDClient and OpenTTDAdminClient) 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 local lib/ directory.
  • markers: Registers the custom e2e marker for pytest classification.