Skip to content

Commit e3a8b0d

Browse files
Isolate conformance test workers to fix parallel-run flakiness
Enabling parallel test forks let concurrent conformance battles collide on the shared compat-test/work directory, intermittently corrupting TurnBoundaryConformanceTest (and other conformance tests) under ./gradlew clean build. Each Gradle test worker now gets its own COMPAT_WORK_DIR, and the test task depends on the harness's own jars being built first rather than racing them. Verified with three consecutive `./gradlew clean build` runs: 0 failures across all 28 test result files. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TtiGhPTwQoK8CQkf7yffVm
1 parent 8e77024 commit e3a8b0d

4 files changed

Lines changed: 32 additions & 9 deletions

File tree

build.gradle.kts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
1+
import org.gradle.api.tasks.testing.Test
2+
13
plugins {
24
java
35
alias(libs.plugins.kotlin.jvm) apply false
46
}
57

8+
val testForks = providers.gradleProperty("test.maxParallelForks")
9+
.map(String::toInt)
10+
.orElse(Runtime.getRuntime().availableProcessors().coerceAtMost(8).coerceAtLeast(1))
11+
612
allprojects {
713
repositories {
814
mavenCentral()
915
}
1016
}
17+
18+
subprojects {
19+
tasks.withType<Test>().configureEach {
20+
// Test forks are isolated JVMs. Harness-backed tests assign each fork its own work dir.
21+
maxParallelForks = testForks.get()
22+
}
23+
}

compat-test/compat_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def local_bot_api_jar():
112112
STATE_FILE = BASE_DIR / "test_progress.json"
113113
REPORT_FILE = BASE_DIR / "compatibility_report.md"
114114
ERRORS_DIR = BASE_DIR / "errors"
115-
WORK_DIR = BASE_DIR / "work"
115+
WORK_DIR = Path(os.environ.get("COMPAT_WORK_DIR", BASE_DIR / "work"))
116116

117117
RC_WORKER = BASE_DIR / "RcBattleWorker.java"
118118
TR_WORKER = BASE_DIR / "TrBattleWorker.java"

conformance-test/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ java {
3434
// each engine runs in its own process (ARCH-003).
3535
tasks {
3636
test {
37+
// The harness consumes these files directly rather than resolving them as Gradle dependencies.
38+
dependsOn(":robocode-api:jar", ":robots-wrapper:fatJar")
39+
3740
useJUnitPlatform()
3841
testLogging {
3942
events("failed", "skipped")

conformance-test/src/test/java/dev/robocode/tankroyale/bridge/conformance/ConformanceHarness.java

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ final class ConformanceHarness {
2626

2727
private static final Path REPO_ROOT = locateRepoRoot();
2828
private static final Path HARNESS = REPO_ROOT.resolve("compat-test").resolve("compat_test.py");
29+
private static final Path WORK_ROOT = REPO_ROOT.resolve("compat-test").resolve("work");
30+
private static final Path WORK_DIR = WORK_ROOT.resolve(
31+
"gradle-worker-" + System.getProperty("org.gradle.test.worker", "1")
32+
+ "-" + ProcessHandle.current().pid());
2933

3034
private final String python;
3135
private final Path robocodeHome;
@@ -121,10 +125,7 @@ BattleOutcome runTeam(Engine engine, String teamClass, Path source) {
121125
"--robocode-home", robocodeHome.toString()));
122126

123127
try {
124-
Process process = new ProcessBuilder(command)
125-
.directory(HARNESS.getParent().toFile())
126-
.redirectErrorStream(false)
127-
.start();
128+
Process process = startHarness(command);
128129

129130
AtomicReference<String> out = new AtomicReference<>("");
130131
AtomicReference<String> err = new AtomicReference<>("");
@@ -179,10 +180,7 @@ private BattleOutcome run(Engine engine, String robotClass, Path source, String
179180
}
180181

181182
try {
182-
Process process = new ProcessBuilder(command)
183-
.directory(HARNESS.getParent().toFile())
184-
.redirectErrorStream(false)
185-
.start();
183+
Process process = startHarness(command);
186184

187185
// Both pipes are drained concurrently, and the wait comes before either is
188186
// read. Draining one to EOF first deadlocks as soon as the harness writes more
@@ -222,6 +220,15 @@ private static BattleOutcome failed(String detail) {
222220
return new BattleOutcome(false, List.of(), List.of(), null, detail);
223221
}
224222

223+
/** Starts a harness process with workspace isolation for this Gradle test worker. */
224+
private static Process startHarness(List<String> command) throws IOException {
225+
ProcessBuilder builder = new ProcessBuilder(command)
226+
.directory(HARNESS.getParent().toFile())
227+
.redirectErrorStream(false);
228+
builder.environment().put("COMPAT_WORK_DIR", WORK_DIR.toString());
229+
return builder.start();
230+
}
231+
225232
/**
226233
* The harness prints its result as a single JSON object on the last non-blank line;
227234
* anything before it is progress output.

0 commit comments

Comments
 (0)