diff --git a/.github/workflows/assembly.yml b/.github/workflows/assembly.yml index f192bbb8e..f7399d545 100644 --- a/.github/workflows/assembly.yml +++ b/.github/workflows/assembly.yml @@ -1,6 +1,17 @@ name: Java CI -on: [push] +on: + push: + pull_request: + branches: [ main ] + +permissions: + contents: read + +# Cancel in-progress runs for the same ref (e.g. when pushing new commits to a PR) +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true jobs: build: @@ -13,9 +24,10 @@ jobs: with: java-version: '17' distribution: 'temurin' + cache: gradle - name: Build with Gradle - run: ./gradlew build + run: ./gradlew build --stacktrace - name: robocode.core test artifacts if: always() @@ -42,4 +54,13 @@ jobs: uses: actions/upload-artifact@v7 with: name: robocode-setup - path: build/robocode-*-setup.jar \ No newline at end of file + path: build/robocode-*-setup.jar + + - name: Publish test results summary + if: always() + uses: dorny/test-reporter@v3 + with: + name: Unit test results + path: '**/build/test-results/test/*.xml' + reporter: java-junit + fail-on-error: true diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index dd987da73..4f7993d84 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -6,4 +6,9 @@ plugins { repositories { mavenCentral() + gradlePluginPortal() +} + +dependencies { + implementation(libs.test.retry) } \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/net.sf.robocode.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/net.sf.robocode.java-conventions.gradle.kts index 56d6484fc..29626ae58 100644 --- a/buildSrc/src/main/kotlin/net.sf.robocode.java-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/net.sf.robocode.java-conventions.gradle.kts @@ -4,6 +4,7 @@ plugins { java signing `maven-publish` + id("org.gradle.test-retry") } repositories { @@ -26,6 +27,16 @@ tasks { withType { options.encoding = "UTF-8" } + // Robot battle tests are timing-sensitive and can intermittently report transient + // engine errors (e.g. "Unable to stop thread") on slow/loaded CI machines. + // Retry failed tests a few times so such flakiness does not fail the build. + withType { + retry { + maxRetries.set(2) + maxFailures.set(20) + failOnPassedAfterRetry.set(false) + } + } } publishing { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 731a2d03d..6b578344a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,7 @@ ben-manes-versions = "0.54.0" eclipse-jdt = "3.45.0" kotlin = "2.3.21" bcel = "6.12.0" +test-retry = "1.6.2" [libraries] @@ -15,6 +16,7 @@ codesize = { module = "net.sf.robocode:codesize", version.ref = "codesize" } eclipse-jdt = { module = "org.eclipse.jdt:org.eclipse.jdt.core", version.ref = "eclipse-jdt" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } bcel = { module = "org.apache.bcel:bcel", version.ref = "bcel" } +test-retry = { module = "org.gradle.test-retry:org.gradle.test-retry.gradle.plugin", version.ref = "test-retry" } [bundles] diff --git a/robocode.host/src/main/java/net/sf/robocode/host/security/RobotThreadManager.java b/robocode.host/src/main/java/net/sf/robocode/host/security/RobotThreadManager.java index f4fe01380..c60ebe2f5 100644 --- a/robocode.host/src/main/java/net/sf/robocode/host/security/RobotThreadManager.java +++ b/robocode.host/src/main/java/net/sf/robocode/host/security/RobotThreadManager.java @@ -25,6 +25,19 @@ */ public class RobotThreadManager { + // Total time to wait for a thread to stop on its own after being interrupted. + // Generous enough to tolerate CPU starvation on slow/loaded machines (e.g. CI runners), + // where the target thread may simply not be scheduled rather than being stuck. + private static final long WAIT_STOP_TOTAL_MS = 3000; + // Polling granularity while waiting for a thread to stop. + private static final long WAIT_STOP_POLL_MS = 10; + // How often to re-assert the interrupt while waiting (a single early interrupt can be + // missed if the thread is not yet at an interruptible point). + private static final long REASSERT_INTERRUPT_EVERY_MS = 100; + // Bounded join used by interrupt()/stop() escalation steps. + private static final long INTERRUPT_JOIN_MS = 1000; + private static final long STOP_JOIN_MS = 3000; + private final IHostedThread robotProxy; private Thread runThread; private ThreadGroup runThreadGroup; @@ -182,7 +195,7 @@ private void stop(Thread t) { // noinspection deprecation t.interrupt(); try { - t.join(1500); + t.join(STOP_JOIN_MS); } catch (InterruptedException e) { // Immediately reasserts the exception by interrupting the caller thread itself Thread.currentThread().interrupt(); @@ -199,7 +212,7 @@ private void interrupt(Thread t) { } t.interrupt(); try { - t.join(500); + t.join(INTERRUPT_JOIN_MS); } catch (InterruptedException e) { // Immediately reasserts the exception by interrupting the caller thread itself Thread.currentThread().interrupt(); @@ -208,13 +221,26 @@ private void interrupt(Thread t) { } private void waitForStop(Thread thread) { - for (int j = 0; j < 100 && thread.isAlive(); j++) { - if (j == 50) { + final long maxIters = WAIT_STOP_TOTAL_MS / WAIT_STOP_POLL_MS; + final long reassertEvery = Math.max(1, REASSERT_INTERRUPT_EVERY_MS / WAIT_STOP_POLL_MS); + boolean loggedWaiting = false; + + for (long j = 0; j < maxIters && thread.isAlive(); j++) { + if (!loggedWaiting && j >= maxIters / 2) { + loggedWaiting = true; logMessage( "Waiting for robot " + robotProxy.getStatics().getName() + " to stop thread " + thread.getName()); } + // Re-assert the interrupt periodically in case the thread had not yet reached + // an interruptible point when it was first interrupted. + if (j % reassertEvery == 0) { + thread.interrupt(); + } try { - Thread.sleep(10); + Thread.sleep(WAIT_STOP_POLL_MS); + // Give the target thread a chance to be scheduled, especially when the CPU is + // contended and time passes without the thread making progress. + Thread.yield(); } catch (InterruptedException e) { // Immediately reasserts the exception by interrupting the caller thread itself Thread.currentThread().interrupt();