From 19be848d60085bd88ea3ee6603570f0e27263193 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 2 Jun 2026 14:11:39 +0200 Subject: [PATCH 1/5] tests on PR & main --- .github/workflows/ci.yml | 50 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..39766c930 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,50 @@ +name: CI + +on: + pull_request: + branches: [ main ] + push: + 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: + test: + name: Build & run unit tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Set up JDK 17 + uses: actions/setup-java@v5 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Run unit tests + run: ./gradlew test --stacktrace + + - name: Upload test reports + if: always() + uses: actions/upload-artifact@v7 + with: + name: test-reports + path: '**/build/reports/tests/test' + if-no-files-found: ignore + + - name: Publish test results summary + if: always() + uses: dorny/test-reporter@v2 + with: + name: Unit test results + path: '**/build/test-results/test/*.xml' + reporter: java-junit + fail-on-error: false From a7e4b25f67c1a1df8106257f159ca45c64d81eda Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 2 Jun 2026 14:30:16 +0200 Subject: [PATCH 2/5] fix flaky --- .github/workflows/ci.yml | 4 ++++ buildSrc/build.gradle.kts | 5 +++++ .../net.sf.robocode.java-conventions.gradle.kts | 11 +++++++++++ gradle/libs.versions.toml | 2 ++ 4 files changed, 22 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39766c930..ec7338ca9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,10 @@ on: permissions: contents: read +# Force JavaScript-based actions still pinned to Node.js 20 (e.g. dorny/test-reporter@v2) to run on Node.js 24 +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + # Cancel in-progress runs for the same ref (e.g. when pushing new commits to a PR) concurrency: group: ci-${{ github.ref }} 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] From 15a79b9e4f3b5e1c2b8aac542157cf47dbdee064 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 2 Jun 2026 14:48:23 +0200 Subject: [PATCH 3/5] deduplicate --- .github/workflows/assembly.yml | 29 ++++++++++++++++-- .github/workflows/ci.yml | 54 ---------------------------------- 2 files changed, 27 insertions(+), 56 deletions(-) delete mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/assembly.yml b/.github/workflows/assembly.yml index f192bbb8e..9613a4feb 100644 --- a/.github/workflows/assembly.yml +++ b/.github/workflows/assembly.yml @@ -1,6 +1,21 @@ name: Java CI -on: [push] +on: + push: + pull_request: + branches: [ main ] + +permissions: + contents: read + +# Force JavaScript-based actions still pinned to Node.js 20 (e.g. dorny/test-reporter@v2) to run on Node.js 24 +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +# 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 +28,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() @@ -38,6 +54,15 @@ jobs: name: robocode.tests-result path: robocode.tests/build/reports/tests/test + - name: Publish test results summary + if: always() + uses: dorny/test-reporter@v2 + with: + name: Unit test results + path: '**/build/test-results/test/*.xml' + reporter: java-junit + fail-on-error: false + - name: setup binary artifact uses: actions/upload-artifact@v7 with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index ec7338ca9..000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: CI - -on: - pull_request: - branches: [ main ] - push: - branches: [ main ] - -permissions: - contents: read - -# Force JavaScript-based actions still pinned to Node.js 20 (e.g. dorny/test-reporter@v2) to run on Node.js 24 -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -# 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: - test: - name: Build & run unit tests - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v6 - - - name: Set up JDK 17 - uses: actions/setup-java@v5 - with: - java-version: '17' - distribution: 'temurin' - cache: gradle - - - name: Run unit tests - run: ./gradlew test --stacktrace - - - name: Upload test reports - if: always() - uses: actions/upload-artifact@v7 - with: - name: test-reports - path: '**/build/reports/tests/test' - if-no-files-found: ignore - - - name: Publish test results summary - if: always() - uses: dorny/test-reporter@v2 - with: - name: Unit test results - path: '**/build/test-results/test/*.xml' - reporter: java-junit - fail-on-error: false From 0d09ea701eb65af41c4c55848582c4c45e500c8b Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 2 Jun 2026 15:11:39 +0200 Subject: [PATCH 4/5] improve RobotThreadManager resiliency --- .../host/security/RobotThreadManager.java | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) 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(); From ffdc5492a248ad9e3be277e0b16d332adf22d58f Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 2 Jun 2026 15:18:49 +0200 Subject: [PATCH 5/5] fail on failed tests --- .github/workflows/assembly.yml | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/.github/workflows/assembly.yml b/.github/workflows/assembly.yml index 9613a4feb..f7399d545 100644 --- a/.github/workflows/assembly.yml +++ b/.github/workflows/assembly.yml @@ -8,10 +8,6 @@ on: permissions: contents: read -# Force JavaScript-based actions still pinned to Node.js 20 (e.g. dorny/test-reporter@v2) to run on Node.js 24 -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - # Cancel in-progress runs for the same ref (e.g. when pushing new commits to a PR) concurrency: group: ci-${{ github.ref }} @@ -54,17 +50,17 @@ jobs: name: robocode.tests-result path: robocode.tests/build/reports/tests/test + - name: setup binary artifact + uses: actions/upload-artifact@v7 + with: + name: robocode-setup + path: build/robocode-*-setup.jar + - name: Publish test results summary if: always() - uses: dorny/test-reporter@v2 + uses: dorny/test-reporter@v3 with: name: Unit test results path: '**/build/test-results/test/*.xml' reporter: java-junit - fail-on-error: false - - - name: setup binary artifact - uses: actions/upload-artifact@v7 - with: - name: robocode-setup - path: build/robocode-*-setup.jar \ No newline at end of file + fail-on-error: true