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
@@ -0,0 +1,48 @@
## Compiles Nim bots before battle tests.
## config.nims in each bot dir already sets --path and --outdir, so we just
## run `nim c src/BotName.nim` from the bot dir and let config.nims do the rest.
import std/[os, osproc, streams, strformat, strutils, sequtils]
proc compileBots*(botDirs: seq[string]): seq[string] =
## Compiles each bot dir, returns absolute paths to compiled binaries.
## Raises on first compilation failure, message includes compiler output.
for dir in botDirs:
let absDir = absolutePath(dir)
# Infer main source: src/<stem>.nim where stem = last path component
# minus optional _garage suffix (e.g. OscillatorBot_garage → OscillatorBot)
var stem = lastPathPart(absDir)
if stem.endsWith("_garage"): stem = stem[0 ..< stem.len - "_garage".len]
let srcFile = absDir / "src" / stem & ".nim"
if not fileExists(srcFile):
# Fall back: pick the first .nim in src/
let candidates = toSeq(walkFiles(absDir / "src" / "*.nim"))
if candidates.len == 0:
raise newException(IOError, fmt"No .nim source found in {absDir}/src/")
stem = splitFile(candidates[0]).name
# ponytail: startProcess+streams for timeout; execCmdEx has no timeout param
let p = startProcess("nim", workingDir = absDir,
args = ["c", fmt"src/{stem}.nim"],
options = {poUsePath, poStdErrToStdOut})
let exitCode = waitForExit(p, timeout = 30_000)
let output = p.outputStream.readAll()
p.close()
if exitCode != 0:
raise newException(OSError,
fmt"Compilation failed for {absDir}:{'\n'}{output}")
result.add absDir / "out" / stem
when isMainModule:
let botDir = currentSourcePath().parentDir().parentDir().parentDir() /
"OscillatorBot_garage"
echo "Compiling OscillatorBot from: ", botDir
try:
let bins = compileBots(@[botDir])
echo "OK — binary: ", bins[0]
doAssert fileExists(bins[0]), "binary not found after compile"
except CatchableError as e:
echo "FAIL: ", e.msg
quit(1)