feat: add shared integration test framework for garages (#126)

Implements:
- BattleResult type and JSON-lines parser (#127)
- TR server lifecycle manager (#128)
- Bot compiler using nim c (#129)
- runBattle() orchestrator (#130)
- Example test in OscillatorBot_garage (#131)
- Framework usage guide (#132)
- TestBattleRunner.java for external server (#133)
- BattleRunner process lifecycle (#134)

Fix: runner_process.nim was redefining TimeoutError locally; now
uses std/net.TimeoutError consistently with server_manager.nim.
This commit is contained in:
2026-08-30 12:03:35 +02:00
parent a50571e6da
commit 57a1915b10
13 changed files with 654 additions and 2 deletions
+205
View File
@@ -0,0 +1,205 @@
# test_framework
Integration test framework for Tank Royale bots. Spins up a TR server JAR on a random port, compiles your Nim bots, runs a battle via a Java `TestBattleRunner` subprocess, and returns structured results you can assert on. Local development tool only — no CI support.
---
## Prerequisites
- **Java JRE** (11+) — `java` must be on `PATH`
- **Tank Royale server JAR** — `robocode-tankroyale-server-*-all.jar`
- **Tank Royale runner JAR** — `robocode-tankroyale-runner.jar`
### Environment variables
| Variable | Purpose | Default |
|---|---|---|
| `TR_SERVER_JAR` | Path to the TR server JAR | hardcoded dev path in `server_manager.nim` |
| `TR_BATTLE_RUNNER` | Path to the TR runner JAR | hardcoded dev path in `runner_process.nim` |
| `TR_BATTLE_RUNNER_DIR` | Directory containing compiled `TestBattleRunner.class` | `tools/battle_runner/` |
Set at minimum `TR_SERVER_JAR` and `TR_BATTLE_RUNNER` to point at your local JAR builds.
### One-time Java compilation
```sh
cd tools/battle_runner
TANK_ROYALE_JAR=/path/to/robocode-tankroyale-runner.jar ./run_test_battle.sh --help
```
`run_test_battle.sh` auto-compiles `TestBattleRunner.java` when `.class` is missing or stale. After this, `TR_BATTLE_RUNNER_DIR` defaults to `tools/battle_runner/` and the class file is found automatically.
---
## Setup for your garage
**`tests/config.nims`** — add the common_libs path so the import resolves:
```nim
--path:"../../common_libs"
```
Adjust the relative depth to match your garage's position in the repo.
**`YourBot.nimble`** — add a test task:
```nim
task test, "Compile and run integration tests":
exec "nim c -r --path:../common_libs tests/test_basic_battle.nim"
```
Run with:
```sh
nimble test
```
---
## Writing tests
```nim
import std/[os, unittest]
import test_framework/test_framework
const
myBotDir = currentSourcePath().parentDir.parentDir # garage root
sittingDuck = currentSourcePath().parentDir / "bots" / "SittingDuck"
suite "MyBot basic battle":
test "beats SittingDuck in 3 rounds":
let r = runBattle(@[myBotDir, sittingDuck], rounds = 3)
check r.rounds.len == 3
check r.results.len == 2
let me = block:
var found: BotResult
for b in r.results:
if b.name == "MyBot": found = b
found
check me.totalScore > 0
check "MyBot" in r.winners
```
`runBattle` blocks until the battle finishes (or `timeout` ms elapses). The server process is started once per test binary run and killed on exit.
---
## API reference
### `runBattle`
```nim
proc runBattle*(botDirs: seq[string], rounds: int = 10,
timeout: int = 120000): BattleResult
```
| Parameter | Description |
|---|---|
| `botDirs` | Absolute or relative paths to bot directories (one per participating bot) |
| `rounds` | Number of rounds to play (default: 10) |
| `timeout` | Milliseconds to wait for the battle runner to finish (default: 120000) |
Each bot directory must have `src/<BotName>.nim` and the compiler writes the binary to `out/<BotName>`.
### `BattleResult`
```nim
BattleResult = object
bots*: seq[string] # bot names in participation order
rounds*: seq[RoundResult] # one entry per round
results*: seq[BotResult] # final standings, sorted by rank
winners*: seq[string] # names of bots with rank == 1
```
### `BotResult`
```nim
BotResult = object
name*: string
totalScore*: int
rank*: int
firstPlaces*: int
survivalCount*: int
```
### `RoundResult`
```nim
RoundResult = object
round*: int
results*: seq[BotRoundResult]
```
### `BotRoundResult`
```nim
BotRoundResult = object
name*: string
score*: int
rank*: int
survived*: bool
```
---
## Writing adversary bots
Minimal bot — does nothing, useful as a baseline target:
**`tests/bots/SittingDuck/src/SittingDuck.nim`**
```nim
import std/os
import tankroyale_botapi
const botJsonPath = currentSourcePath().parentDir / "SittingDuck.json"
type SittingDuck = ref object of Bot
method run*(bot: SittingDuck) =
while isRunning():
go()
when isMainModule:
var bot = SittingDuck()
start(bot, botJsonPath)
```
**`tests/bots/SittingDuck/src/SittingDuck.json`** — required metadata:
```json
{
"name": "SittingDuck",
"version": "0.1.0",
"authors": ["Test"],
"description": "Does nothing — test adversary",
"gameTypes": ["classic", "1v1"],
"platform": "Nim",
"programmingLang": "Nim"
}
```
Place adversary bots under `tests/bots/<BotName>/src/`. The framework infers the binary name from the directory name, so the directory name must match the `.nim` filename.
---
## Troubleshooting
**`TR server JAR not found`**
Set `TR_SERVER_JAR` to the full path of your `robocode-tankroyale-server-*-all.jar`.
**`TR server did not start within 15s`**
The server took too long — check that the JAR is valid and the port was free. Also check for leftover `java` processes from a previous crashed run.
**`BattleRunner timed out after Nms`**
The `timeout` parameter (default 120 000 ms) was exceeded. Increase it: `runBattle(..., timeout = 300_000)`. Also check that bots are connecting to the server (look for compilation errors in test output).
**`BattleRunner exited with code N`**
`TestBattleRunner.class` is missing or `TR_BATTLE_RUNNER` points at the wrong JAR. Re-run `run_test_battle.sh` once to recompile.
**Zombie `java` processes after a crash**
The server process is registered with `addExitProc` and killed on normal exit. A hard kill (SIGKILL) of the test binary will leave the server process behind. Kill manually: `pkill -f robocode-tankroyale-server`.
**`No .nim source found in .../src/`**
The bot directory's `src/` folder is empty or the `.nim` file is missing. The compiler expects `src/<DirName>.nim` (stripping a `_garage` suffix if present).