Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .clue/id-ledger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,11 @@ events:
- {id: M-148, kind: numeric, state: live, prefix: M, component: "148"}
- {id: AN-016, kind: numeric, state: live, prefix: AN, component: "16"}
- {id: TASKS-011, kind: numeric, state: live, prefix: TASKS, component: "11"}
- {id: CH-019, kind: numeric, state: reserved, prefix: CH, component: "19"}
- {id: TASKS-012, kind: numeric, state: reserved, prefix: TASKS, component: "12"}
- {id: CH-019, kind: numeric, state: live, prefix: CH, component: "19"}
- {id: TASKS-012, kind: numeric, state: live, prefix: TASKS, component: "12"}
- {id: M-148, kind: numeric, state: retired, prefix: M, component: "148"}
high-water:
- {id: ADR-002, kind: numeric, state: reserved, prefix: ADR, component: "2"}
- {id: AN-014, kind: numeric, state: reserved, prefix: AN, component: "14"}
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
- Preserved classic handler-exception reporting by surfacing legacy callback failures at the bridge boundary while retaining Tank Royale's event queue.
- Added grouped team-robot staging and two-engine conformance evidence for team membership, teammate messages, directed-recipient isolation, droid no-scan behavior, and arbitrary serializable message payloads.
- Restored classic team names, including battle-wide duplicate suffixes, through the Tank Royale name map for team identity, teammate lookup, directed messaging, and message-event senders when paired with the matching Tank Royale Bot API and runner.
- Added supported integration evidence that checks skipped-turn telemetry, including warm-up events and completed empty captures, in the persisted parity registry.
6 changes: 3 additions & 3 deletions compat-test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,7 @@ the failure.
> deferred same-priority events, e.g. every other scan event for robots that call a
> blocking method such as `fire()` inside `onScannedRobot`.

All paths are defaults only — override with CLI flags (`--collection-dir`,
`--robocode-home`, `--runner-jar`, `--bridge-api-jar`, `--wrapper-jar`, `--bot-api-jar`)
or the corresponding `COMPAT_*` environment variables.
All paths are defaults only — override with CLI flags (`--collection-dir`, `--robocode-home`, `--runner-jar`, `--bridge-api-jar`, `--wrapper-jar`, `--bot-api-jar`) or the corresponding `COMPAT_*` environment variables. Set `COMPAT_DATA_DIR` to redirect the checkpoint, report, error logs, and parity registry to an isolated directory; set `COMPAT_WORK_DIR` to isolate temporary engine and bot files.

## Division setups

Expand Down Expand Up @@ -208,6 +206,8 @@ Full error details land in `errors/robocode/<robot>.log` and

Pass `--capture-skipped-turns` to enable opt-in records for Tank Royale bridge callbacks; capture is off by default. Each registry observation stores `tank_royale.skipped_turn_telemetry` with a status and, only when capture is complete, an `events` list of unique `{bot_id, round, turn}` values sorted by round and turn, including warm-up. `disabled`, `unavailable`, and `incomplete` use `events: null`; `captured` with `events: []` means the completed run delivered no skipped-turn callbacks to the bridge. Per-bot readiness and completion-count markers let the harness detect an older bridge jar or a truncated capture. The data describes callbacks delivered to the bridge and does not claim to record server detections that never reach it.

`--turn-timeout-micros` overrides Tank Royale's per-turn timeout for a local probe. A large value makes a completed no-event capture practical to verify without depending on a bot narrowly meeting the default turn deadline.

Use the conformance probe to check a forced-skip run locally:

```bash
Expand Down
25 changes: 20 additions & 5 deletions compat-test/compat_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,10 @@ def local_bot_api_jar():
# by the classic client, so an unrecognised install must not create evidence.
LITERUMBLE_ALLOWED_ROBOCODE_CLIENTS = ("1.10.3", "1.11.0", "1.11.1")

STATE_FILE = BASE_DIR / "test_progress.json"
REPORT_FILE = BASE_DIR / "compatibility_report.md"
ERRORS_DIR = BASE_DIR / "errors"
DATA_DIR = Path(os.environ.get("COMPAT_DATA_DIR", BASE_DIR))
STATE_FILE = DATA_DIR / "test_progress.json"
REPORT_FILE = DATA_DIR / "compatibility_report.md"
ERRORS_DIR = DATA_DIR / "errors"
WORK_DIR = Path(os.environ.get("COMPAT_WORK_DIR", BASE_DIR / "work"))

RC_WORKER = BASE_DIR / "RcBattleWorker.java"
Expand All @@ -150,8 +151,8 @@ def local_bot_api_jar():
# verdict is stated as movement from a recorded baseline rather than as an absolute delta:
# a bot that has always differed by a given margin and still does has not regressed.
REGRESSION_SET_FILE = BASE_DIR / "regression-set.json"
PARITY_REGISTRY_FILE = BASE_DIR / "parity-registry.json"
PARITY_REGISTRY_REPORT = BASE_DIR / "parity-registry.md"
PARITY_REGISTRY_FILE = DATA_DIR / "parity-registry.json"
PARITY_REGISTRY_REPORT = DATA_DIR / "parity-registry.md"
MELEE_OPPONENTS_FILE = BASE_DIR / "melee-opponents.json"
DEFAULT_BATCH_SIZE = 25
REGRESSION_REPEATS = 5
Expand Down Expand Up @@ -1139,6 +1140,8 @@ def run_tr_battle(jar_path: Path, classname, version, opts, setup, rc_signatures
"--out", str(out_file),
"--timeout", str(max(30, opts.timeout - 15)),
]
if opts.turn_timeout_micros is not None:
cmd.extend(["--turn-timeout", str(opts.turn_timeout_micros)])
started = time.monotonic()
watcher = None
if rc_signatures is not None:
Expand Down Expand Up @@ -1179,6 +1182,13 @@ def run_tr_battle(jar_path: Path, classname, version, opts, setup, rc_signatures
result["skipped_turn_telemetry"] = skipped_turn_telemetry(
bot_dirs, capture_skipped_turns, result.get("completed", False),
len(staged_log_dirs(bot_dirs)))
test_status = opts.test_skipped_turn_status
if test_status is not None:
if not capture_skipped_turns:
raise ValueError("invalid compatibility-test skipped-turn telemetry status")
# Let the JVM integration test send a synthetic status through the same checkpoint
# and registry write path after separately checking how the status is produced.
result["skipped_turn_telemetry"] = {"status": test_status, "events": None}
return result


Expand Down Expand Up @@ -1461,6 +1471,10 @@ def parse_args():
"(auto-detected when not given)")
p.add_argument("--capture-skipped-turns", action="store_true",
help="record Tank Royale bridge skipped-turn events, including warm-up turns")
p.add_argument("--turn-timeout-micros", type=int, default=None,
help="override Tank Royale's turn timeout in microseconds for local probes")
p.add_argument("--test-skipped-turn-status", choices=("unavailable", "incomplete"),
default=None, help=argparse.SUPPRESS)

gate = p.add_argument_group("regression gate (C-004)")
gate.add_argument("--regression", action="store_true",
Expand Down Expand Up @@ -2082,6 +2096,7 @@ def main():
if opts.trace:
return run_trace(opts)

DATA_DIR.mkdir(parents=True, exist_ok=True)
state = load_state()
state["settings"] = {"rounds": opts.rounds, "threshold": opts.threshold,
"divisions": DIVISIONS}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package conformance.probes;

import robocode.AdvancedRobot;

/** Responds to turns immediately so a completed capture can exercise the empty-event case. */
public class NoSkippedTurnProbe extends AdvancedRobot {

@Override
public void run() {
while (true) {
execute();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,16 @@ final class BattleOutcome {
private final List<String> errors;
private final Double score;
private final String detail;
private final String skippedTurnTelemetry;

BattleOutcome(boolean completed, List<String> consoles, List<String> errors,
Double score, String detail) {
Double score, String detail, String skippedTurnTelemetry) {
this.completed = completed;
this.consoles = List.copyOf(consoles);
this.errors = List.copyOf(errors);
this.score = score;
this.detail = detail;
this.skippedTurnTelemetry = skippedTurnTelemetry;
}

boolean completed() {
Expand All @@ -49,6 +51,10 @@ String detail() {
return detail;
}

String skippedTurnTelemetry() {
return skippedTurnTelemetry;
}

/** True when any participant printed the marker. */
boolean anyConsoleContains(String marker) {
return consoles.stream().anyMatch(c -> c.contains(marker));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ static Path repoRoot() {
return REPO_ROOT;
}

/** Jar produced by a conformance-source run, ready for the normal parity-sweep path. */
static Path conformanceRobotJar(String robotClass) {
return WORK_DIR.resolve("conformance").resolve(robotClass + "_1.0.jar");
}

/** The number of rounds every battle this harness runs is configured for. */
int rounds() {
return rounds;
Expand Down Expand Up @@ -112,6 +117,77 @@ BattleOutcome run(Engine engine, String robotClass, Path source, int participant
return run(engine, robotClass, source, null, participants);
}

BattleOutcome run(Engine engine, String robotClass, Path source, boolean captureSkippedTurns) {
return run(engine, robotClass, source, null, null, captureSkippedTurns, null);
}

BattleOutcome run(Engine engine, String robotClass, Path source, boolean captureSkippedTurns,
int timeoutSeconds) {
return run(engine, robotClass, source, null, null, captureSkippedTurns, timeoutSeconds);
}

/** Runs the ordinary two-engine sweep for one fixture and isolates its durable output. */
String runCompatibilityMeasurement(Path collectionDir, Path dataDir, Path workDir,
String selectedRobot, boolean captureSkippedTurns,
Integer turnTimeoutMicros) {
return runCompatibilityMeasurement(collectionDir, dataDir, workDir, selectedRobot,
captureSkippedTurns, turnTimeoutMicros, null);
}

String runCompatibilityMeasurement(Path collectionDir, Path dataDir, Path workDir,
String selectedRobot, boolean captureSkippedTurns,
Integer turnTimeoutMicros, String testTelemetryStatus) {
List<String> command = new ArrayList<>(List.of(
python,
HARNESS.toString(),
"--collections", "roborumble",
"--collection-dir", collectionDir.toString(),
"--only", selectedRobot,
"--limit", "1",
"--rounds", String.valueOf(rounds),
"--timeout", "300",
"--robocode-home", robocodeHome.toString()));
addBridgeArtifacts(command);
if (captureSkippedTurns) {
command.add("--capture-skipped-turns");
}
if (turnTimeoutMicros != null) {
command.add("--turn-timeout-micros");
command.add(String.valueOf(turnTimeoutMicros));
}
if (testTelemetryStatus != null) {
command.add("--test-skipped-turn-status");
command.add(testTelemetryStatus);
}

try {
ProcessBuilder builder = new ProcessBuilder(command)
.directory(HARNESS.getParent().toFile())
.redirectErrorStream(true);
builder.environment().put("COMPAT_WORK_DIR", workDir.toString());
builder.environment().put("COMPAT_DATA_DIR", dataDir.toString());
Process process = builder.start();

AtomicReference<String> output = new AtomicReference<>("");
Thread pumpOutput = pump(process.getInputStream(), output);
if (!process.waitFor(20, TimeUnit.MINUTES)) {
process.destroyForcibly();
join(pumpOutput);
return "the compatibility measurement did not finish within 20 minutes: "
+ trim(output.get());
}
join(pumpOutput);
return process.exitValue() == 0 ? null
: "the compatibility measurement exited " + process.exitValue() + ": "
+ trim(output.get());
} catch (IOException e) {
return "could not start the compatibility measurement: " + e.getMessage();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return "interrupted while waiting for the compatibility measurement";
}
}

/** Runs a bridge-owned team fixture on one engine. */
BattleOutcome runTeam(Engine engine, String teamClass, Path source) {
List<String> command = new ArrayList<>(List.of(
Expand Down Expand Up @@ -159,6 +235,12 @@ BattleOutcome runTeam(Engine engine, String teamClass, Path source) {

private BattleOutcome run(Engine engine, String robotClass, Path source, String enemyClass,
Integer participants) {
return run(engine, robotClass, source, enemyClass, participants, false, null);
}

private BattleOutcome run(Engine engine, String robotClass, Path source, String enemyClass,
Integer participants, boolean captureSkippedTurns,
Integer timeoutSeconds) {
List<String> command = new ArrayList<>(List.of(
python,
HARNESS.toString(),
Expand All @@ -180,6 +262,13 @@ private BattleOutcome run(Engine engine, String robotClass, Path source, String
command.add("--participants");
command.add(String.valueOf(participants));
}
if (captureSkippedTurns) {
command.add("--capture-skipped-turns");
}
if (timeoutSeconds != null) {
command.add("--timeout");
command.add(String.valueOf(timeoutSeconds));
}

try {
Process process = startHarness(command);
Expand Down Expand Up @@ -219,7 +308,7 @@ private BattleOutcome run(Engine engine, String robotClass, Path source, String
}

private static BattleOutcome failed(String detail) {
return new BattleOutcome(false, List.of(), List.of(), null, detail);
return new BattleOutcome(false, List.of(), List.of(), null, detail, null);
}

/** Starts a harness process with workspace isolation for this Gradle test worker. */
Expand Down Expand Up @@ -266,7 +355,8 @@ private static BattleOutcome parse(String json) {
Json.stringArray(json, "consoles"),
Json.stringArray(json, "errors"),
score,
Json.scalar(json, "fatal"));
Json.scalar(json, "fatal"),
Json.object(json, "skipped_turn_telemetry"));
}

/** Starts a daemon thread that drains a pipe to EOF into {@code sink}. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,34 @@ BattleOutcome outcomeFor(Engine engine, String robotClass, Path source, int part
return outcomeForFixture(engine, robotClass, source, null, participants);
}

/** Runs one compatibility-harness battle with the opt-in telemetry setting under test. */
BattleOutcome telemetryOutcome(Engine engine, String robotClass, Path source,
boolean captureSkippedTurns) {
return harness.run(engine, robotClass, source, captureSkippedTurns);
}

/** Runs a telemetry battle with a short per-engine timeout for the incomplete path. */
BattleOutcome telemetryOutcome(Engine engine, String robotClass, Path source,
boolean captureSkippedTurns, int timeoutSeconds) {
return harness.run(engine, robotClass, source, captureSkippedTurns, timeoutSeconds);
}

/** Runs one standard parity measurement with isolated checkpoint and registry files. */
String runRecordedMeasurement(Path collectionDir, Path dataDir, Path workDir,
String selectedRobot, boolean captureSkippedTurns,
Integer turnTimeoutMicros) {
return runRecordedMeasurement(collectionDir, dataDir, workDir, selectedRobot,
captureSkippedTurns, turnTimeoutMicros, null);
}

/** Injects a non-captured test status after the ordinary runner result and before persistence. */
String runRecordedMeasurement(Path collectionDir, Path dataDir, Path workDir,
String selectedRobot, boolean captureSkippedTurns,
Integer turnTimeoutMicros, String testTelemetryStatus) {
return harness.runCompatibilityMeasurement(collectionDir, dataDir, workDir,
selectedRobot, captureSkippedTurns, turnTimeoutMicros, testTelemetryStatus);
}

private BattleOutcome outcomeForFixture(Engine engine, String robotClass, Path source,
String enemyClass) {
return outcomeForFixture(engine, robotClass, source, enemyClass, null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,51 @@ static String scalar(String json, String field) {
return json.substring(cursor, end).trim();
}

/** The raw object value for a field, or null when the field is absent or not an object. */
static String object(String json, String field) {
int at = indexOfKey(json, field);
if (at < 0) {
return null;
}
int cursor = at;
while (cursor < json.length() && json.charAt(cursor) != ':') {
cursor++;
}
cursor++;
while (cursor < json.length() && Character.isWhitespace(json.charAt(cursor))) {
cursor++;
}
if (cursor >= json.length() || json.charAt(cursor) != '{') {
return null;
}

int start = cursor;
int depth = 0;
boolean quoted = false;
boolean escaped = false;
for (; cursor < json.length(); cursor++) {
char c = json.charAt(cursor);
if (quoted) {
if (escaped) {
escaped = false;
} else if (c == '\\') {
escaped = true;
} else if (c == '"') {
quoted = false;
}
continue;
}
if (c == '"') {
quoted = true;
} else if (c == '{') {
depth++;
} else if (c == '}' && --depth == 0) {
return json.substring(start, cursor + 1);
}
}
return null;
}

/** The elements of an array of strings, or an empty list when the field is absent. */
static List<String> stringArray(String json, String field) {
List<String> values = new ArrayList<>();
Expand Down
Loading
Loading