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
27 changes: 24 additions & 3 deletions .github/workflows/assembly.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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()
Expand All @@ -42,4 +54,13 @@ jobs:
uses: actions/upload-artifact@v7
with:
name: robocode-setup
path: build/robocode-*-setup.jar
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
5 changes: 5 additions & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@ plugins {

repositories {
mavenCentral()
gradlePluginPortal()
}

dependencies {
implementation(libs.test.retry)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ plugins {
java
signing
`maven-publish`
id("org.gradle.test-retry")
}

repositories {
Expand All @@ -26,6 +27,16 @@ tasks {
withType<JavaCompile> {
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<Test> {
retry {
maxRetries.set(2)
maxFailures.set(20)
failOnPassedAfterRetry.set(false)
}
}
}

publishing {
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand All @@ -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]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand Down