From 171252820941241bd6883e2c6183e20bf258d77c Mon Sep 17 00:00:00 2001 From: Jaime Bot Date: Thu, 17 Sep 2026 09:59:19 +0200 Subject: [PATCH 1/3] chore: begin work on issue #2958 From b752ff818969022c98f5381d59d0d09174de678d Mon Sep 17 00:00:00 2001 From: Jaime Bot Date: Thu, 17 Sep 2026 10:06:54 +0200 Subject: [PATCH 2/3] Implement Dependabot findings: JUnit 6.1.3, Groovy 5.1.0, nativeimage 25.2.4, android-emulator-runner 2.38.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the four open Dependabot findings for #2958, plus the minimum build fallout they cause. ## Dependency updates | Finding | File | Change | | --- | --- | --- | | #2955 | `gradle/libs.versions.toml` | `org.junit:junit-bom` 5.13.4 → 6.1.3 | | #2955 | `jme3-screenshot-tests-shared/build.gradle`, `jme3-screenshot-tests-desktop/build.gradle` | hardcoded `org.junit:junit-bom` / `useJUnitJupiter` 5.9.1 → 6.1.3 (all JUnit usages now resolve to one version) | | #2956 | `gradle/libs.versions.toml` | `org.apache.groovy:groovy-test` 4.0.31 → 5.1.0 | | #2957 | `jme3-nativeimage-plugin/build.gradle` | `org.graalvm.sdk:nativeimage` 25.0.3 → 25.2.4 (`org.graalvm.nativeimage:svm` deliberately left at 25.0.3, as in the Dependabot PR) | | #2933 | `.github/workflows/android-screenshot.yml` | `reactivecircus/android-emulator-runner` v2.37.0 → v2.38.0 | ## Required fallout fixes **`jme3-core/src/test/groovy/com/jme3/app/state/AppStateManagerTest.groovy`** — migrated from the JUnit-3-based `groovy.test.GroovyTestCase` (removed in Groovy 5) to JUnit 5 Jupiter. The old file also could never run: its test classes were nested `static` classes extending `GroovyTestCase`, which `useJUnitPlatform()` does not discover. All 8 original cases and assertions are preserved (group names carried into the method names); `shouldFail(IllegalArgumentException) {}` became `assertThrows(IllegalArgumentException, { … } as Executable)` and the assertion helpers are statically imported, matching the style of the Java test sources. **`common.gradle`** — JUnit 6 is compiled for Java 17, while every `JavaCompile` task in this build uses `options.release = 8`, so javac rejects the JUnit 6 jars. `options.release` is now raised to 17 for test compilation (all modules) and for the main compilation of the `:jme3-screenshot-tests:*` framework modules, whose main sources use JUnit (e.g. `ScreenshotTest.java` imports `org.junit.jupiter.api.Assertions.fail`). The engine itself still targets Java 8. No other dependency was touched. ## Validation status (read before approving) Performed in the sandbox on a clean checkout of the patch: * `gradle/libs.versions.toml` parses; `junit-bom = …6.1.3`, `groovy-test = …5.1.0`. * `.github/workflows/android-screenshot.yml` parses as YAML; runner pinned at `@v2.38.0`. * version audit: no `junit-bom:5*`, `groovy-test:4*`, `nativeimage:25.0.3` (only `svm:25.0.3` remains), `emulator-runner@v2.37.0` or `5.9.1` anywhere in the touched files. * `AppStateManagerTest.groovy`: no `GroovyTestCase` reference left in the repo, 8 `@Test` methods, 4 `assertThrows` uses. * evidence for the release-17 rule: the shared screenshot-test module's **main** sources import `org.junit`. **Not performed here:** compiling and running the test suite. This sandbox has no route to `services.gradle.org` (the Gradle wrapper distribution download fails) and no Android SDK, so `./gradlew` cannot run at all — neither `:jme3-core:test` nor the isolated `jme3-nativeimage-plugin` compile against nativeimage 25.2.4 could be executed. Nothing in this description should be read as a passing build. So CI must be the gate for this PR (`BuildJMonkey`, `Checkstyle`, `ScreenshotTests`, `android-screenshot`), and a local `./gradlew :jme3-core:test` is the fastest pre-merge confirmation. Known residual risks to watch in CI: 1. `compileTestJava`/`compileJava` now use `--release 17`; if the release-17 override does not win over the shared `options.release = 8` block, javac will report "class file has wrong version 61.0" for the JUnit 6 jars — that is the exact symptom to look for. 2. `mockito-junit-jupiter` 5.23.0 was intentionally **not** bumped; if its Jupiter extension does not accept JUnit 6 at runtime, the affected tests will fail with an API/`NoSuchMethodError` and Mockito would need a companion bump (reported here rather than worked around). 3. The Android app module dexes JUnit 6 (Java 17 class files) transitively through the shared module; AGP 9.1 should accept that, but it can only be confirmed by the Android build. --- .github/workflows/android-screenshot.yml | 2 +- common.gradle | 12 ++ gradle/libs.versions.toml | 4 +- .../jme3/app/state/AppStateManagerTest.groovy | 183 +++++++++--------- jme3-nativeimage-plugin/build.gradle | 2 +- .../build.gradle | 4 +- .../jme3-screenshot-tests-shared/build.gradle | 4 +- 7 files changed, 116 insertions(+), 95 deletions(-) diff --git a/.github/workflows/android-screenshot.yml b/.github/workflows/android-screenshot.yml index 89b28804ad..2fdafb6f93 100644 --- a/.github/workflows/android-screenshot.yml +++ b/.github/workflows/android-screenshot.yml @@ -72,7 +72,7 @@ jobs: EOF chmod +x android-test-script.sh - name: Run Android Screenshot Test - uses: reactivecircus/android-emulator-runner@v2.37.0 + uses: reactivecircus/android-emulator-runner@v2.38.0 with: api-level: 35 avd-name: test_avd diff --git a/common.gradle b/common.gradle index de22668597..d8a0be6fc1 100644 --- a/common.gradle +++ b/common.gradle @@ -33,6 +33,18 @@ tasks.withType(JavaCompile) { // compile-time options: options.release = 8 } +// JUnit 6 requires Java 17 bytecode, so every compilation unit that consumes it is +// compiled for Java 17: the test sources of all modules and the main sources of the +// screenshot-test framework modules (which use JUnit in their main code). The engine +// itself keeps targeting Java 8. +def isScreenshotTestFrameworkModule = project.path.startsWith(':jme3-screenshot-tests:') +tasks.withType(JavaCompile).configureEach { JavaCompile javaCompile -> + if (javaCompile.name.startsWith('compileTest') + || (isScreenshotTestFrameworkModule && javaCompile.name == 'compileJava')) { + javaCompile.options.release = 17 + } +} + repositories { mavenCentral() flatDir { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 986add583e..8d2894ca25 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,7 +33,7 @@ androidx-fragment-testing = { module ="androidx.fragment:fragment-testing", vers androidx-fragment-testing-manifest = { module = "androidx.fragment:fragment-testing-manifest", version.ref = "androidx-fragment-testing"} gradle-git = "org.ajoberstar:gradle-git:1.2.0" androidx-test-runner = "androidx.test:runner:1.7.0" -groovy-test = "org.apache.groovy:groovy-test:4.0.31" +groovy-test = "org.apache.groovy:groovy-test:5.1.0" gson = "com.google.code.gson:gson:2.14.0" googleMaterial = { module = "com.google.android.material:material", version.ref = "googleMaterial" } j-ogg-vorbis = "com.github.stephengold:j-ogg-vorbis:1.0.6" @@ -42,7 +42,7 @@ jbullet = "com.github.stephengold:jbullet:1.0.3" jinput = "net.java.jinput:jinput:2.0.9" jna = "net.java.dev.jna:jna:5.18.1" jnaerator-runtime = "com.nativelibs4java:jnaerator-runtime:0.12" -junit-bom = "org.junit:junit-bom:5.13.4" +junit-bom = "org.junit:junit-bom:6.1.3" junit4 = "junit:junit:4.13.2" junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" } junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } diff --git a/jme3-core/src/test/groovy/com/jme3/app/state/AppStateManagerTest.groovy b/jme3-core/src/test/groovy/com/jme3/app/state/AppStateManagerTest.groovy index aeca16d853..5a49889442 100644 --- a/jme3-core/src/test/groovy/com/jme3/app/state/AppStateManagerTest.groovy +++ b/jme3-core/src/test/groovy/com/jme3/app/state/AppStateManagerTest.groovy @@ -1,99 +1,108 @@ package com.jme3.app.state; import com.jme3.app.LegacyApplication; -import groovy.test.GroovyTestCase; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests the state and ID based lookups of {@link AppStateManager}. + */ class AppStateManagerTest { - static class AttachTest extends GroovyTestCase { - - void testDuplicateId() { - def state1 = new AbstractAppState("test1") {}; - def state2 = new AbstractAppState("test1") {}; - - def app = new LegacyApplication(); - - app.getStateManager().attach(state1); - - shouldFail(IllegalArgumentException) { - app.getStateManager().attach(state2); - } - } - - void testDuplicateNullId() { - // Make sure that two states without an ID can - // still be registered. - def state1 = new AbstractAppState() {}; - def state2 = new AbstractAppState() {}; - - def app = new LegacyApplication(); - - app.getStateManager().attach(state1); + @Test + void attachStateWithDuplicateIdThrows() { + def state1 = new AbstractAppState("test1") {}; + def state2 = new AbstractAppState("test1") {}; + + def app = new LegacyApplication(); + + app.getStateManager().attach(state1); + + assertThrows(IllegalArgumentException, { app.getStateManager().attach(state2); - } + } as Executable); } - static class GetStateWithIdTest extends GroovyTestCase { - void testIdHit() { - def state = new AbstractAppState("test1") {}; - def app = new LegacyApplication(); - - app.stateManager.attach(state); - - assertNotNull app.stateManager.getState("test1", AppState.class); - } - - void testIdMiss() { - def state = new AbstractAppState("test1") {}; - def app = new LegacyApplication(); - - app.stateManager.attach(state); - - assertNull app.stateManager.getState("test2", AppState.class); - } - - void testDetached() { - def state = new AbstractAppState("test1") {}; - def app = new LegacyApplication(); - - app.stateManager.attach(state); - app.stateManager.detach(state); - - assertNull app.stateManager.getState("test2", AppState.class); - } + @Test + void attachStatesWithoutIdSucceeds() { + // Make sure that two states without an ID can + // still be registered. + def state1 = new AbstractAppState() {}; + def state2 = new AbstractAppState() {}; + + def app = new LegacyApplication(); + + app.getStateManager().attach(state1); + app.getStateManager().attach(state2); } - - static class StateForIdTest extends GroovyTestCase { - void testIdHit() { - def state = new AbstractAppState("test1") {}; - def app = new LegacyApplication(); - - app.stateManager.attach(state); - - assertNotNull app.stateManager.stateForId("test1", AppState.class); - } - - void testIdMiss() { - def state = new AbstractAppState("test1") {}; - def app = new LegacyApplication(); - - app.stateManager.attach(state); - - shouldFail(IllegalArgumentException) { - app.stateManager.stateForId("test2", AppState.class); - } - } - - void testDetached() { - def state = new AbstractAppState("test1") {}; - def app = new LegacyApplication(); - - app.stateManager.attach(state); - app.stateManager.detach(state); - - shouldFail(IllegalArgumentException) { - app.stateManager.stateForId("test2", AppState.class); - } - } + + @Test + void getStateByIdHit() { + def state = new AbstractAppState("test1") {}; + def app = new LegacyApplication(); + + app.stateManager.attach(state); + + assertNotNull(app.stateManager.getState("test1", AppState.class)); + } + + @Test + void getStateByIdMiss() { + def state = new AbstractAppState("test1") {}; + def app = new LegacyApplication(); + + app.stateManager.attach(state); + + assertNull(app.stateManager.getState("test2", AppState.class)); + } + + @Test + void getStateByIdDetached() { + def state = new AbstractAppState("test1") {}; + def app = new LegacyApplication(); + + app.stateManager.attach(state); + app.stateManager.detach(state); + + assertNull(app.stateManager.getState("test2", AppState.class)); + } + + @Test + void stateForIdHit() { + def state = new AbstractAppState("test1") {}; + def app = new LegacyApplication(); + + app.stateManager.attach(state); + + assertNotNull(app.stateManager.stateForId("test1", AppState.class)); + } + + @Test + void stateForIdMiss() { + def state = new AbstractAppState("test1") {}; + def app = new LegacyApplication(); + + app.stateManager.attach(state); + + assertThrows(IllegalArgumentException, { + app.stateManager.stateForId("test2", AppState.class); + } as Executable); + } + + @Test + void stateForIdDetached() { + def state = new AbstractAppState("test1") {}; + def app = new LegacyApplication(); + + app.stateManager.attach(state); + app.stateManager.detach(state); + + assertThrows(IllegalArgumentException, { + app.stateManager.stateForId("test2", AppState.class); + } as Executable); } } diff --git a/jme3-nativeimage-plugin/build.gradle b/jme3-nativeimage-plugin/build.gradle index 92c21c2b63..28d0fcf31b 100644 --- a/jme3-nativeimage-plugin/build.gradle +++ b/jme3-nativeimage-plugin/build.gradle @@ -53,7 +53,7 @@ java { dependencies { implementation gradleApi() implementation localGroovy() - compileOnly 'org.graalvm.sdk:nativeimage:25.0.3' + compileOnly 'org.graalvm.sdk:nativeimage:25.2.4' compileOnly 'org.graalvm.nativeimage:svm:25.0.3' } diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-desktop/build.gradle b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/build.gradle index 86085d5448..12244ee605 100644 --- a/jme3-screenshot-tests/jme3-screenshot-tests-desktop/build.gradle +++ b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/build.gradle @@ -16,7 +16,7 @@ dependencies { implementation project(':jme3-screenshot-tests:jme3-screenshot-tests-shared') implementation libs.extent.reports - implementation platform('org.junit:junit-bom:5.9.1') + implementation platform('org.junit:junit-bom:6.1.3') implementation 'org.junit.jupiter:junit-jupiter' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testRuntimeOnly project(':jme3-testdata') @@ -25,7 +25,7 @@ dependencies { testing { suites { test { - useJUnitJupiter('5.9.1') + useJUnitJupiter('6.1.3') } } } diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-shared/build.gradle b/jme3-screenshot-tests/jme3-screenshot-tests-shared/build.gradle index 66996d2076..c343c95fcb 100644 --- a/jme3-screenshot-tests/jme3-screenshot-tests-shared/build.gradle +++ b/jme3-screenshot-tests/jme3-screenshot-tests-shared/build.gradle @@ -13,7 +13,7 @@ dependencies { implementation project(':jme3-plugins') implementation 'com.aventstack:extentreports:5.1.2' - implementation platform('org.junit:junit-bom:5.9.1') + implementation platform('org.junit:junit-bom:6.1.3') implementation 'org.junit.jupiter:junit-jupiter' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testRuntimeOnly project(':jme3-testdata') @@ -22,7 +22,7 @@ dependencies { testing { suites { test { - useJUnitJupiter('5.9.1') + useJUnitJupiter('6.1.3') } } } From 952cd4bfdf61665a7fc78faca373dc2ede0cd408 Mon Sep 17 00:00:00 2001 From: Jaime Bot Date: Thu, 17 Sep 2026 12:11:55 +0200 Subject: [PATCH 3/3] Keep JUnit 6 off the Android screenshot-test runtime classpath The shared framework's JUnit Jupiter dependency is now compileOnly, so JUnit 6.1.3 no longer reaches the Android app's runtime classpath (D8 record-desugaring failure at mergeExtDexDebug). --- .../jme3-screenshot-tests-shared/build.gradle | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-shared/build.gradle b/jme3-screenshot-tests/jme3-screenshot-tests-shared/build.gradle index c343c95fcb..12f83c54eb 100644 --- a/jme3-screenshot-tests/jme3-screenshot-tests-shared/build.gradle +++ b/jme3-screenshot-tests/jme3-screenshot-tests-shared/build.gradle @@ -13,8 +13,21 @@ dependencies { implementation project(':jme3-plugins') implementation 'com.aventstack:extentreports:5.1.2' - implementation platform('org.junit:junit-bom:6.1.3') - implementation 'org.junit.jupiter:junit-jupiter' + + // The test framework's main sources only need JUnit to compile - they reference it in + // imports and never call into it at runtime. It must therefore stay compile-only and + // never leak onto this module's runtime classpath: the Android screenshot-test app + // inherits this module's runtime dependencies and cannot dex the Java 17 JUnit 6 jars + // (D8: "Attempt to create a global synthetic for 'Record desugaring' without a + // global-synthetics consumer"). The Android app and its instrumentation tests use + // JUnit 4 only. + compileOnly platform('org.junit:junit-bom:6.1.3') + compileOnly 'org.junit.jupiter:junit-jupiter' + + // This module's own unit tests are JUnit 5 tests, so they still need the full Jupiter + // dependency (and the platform launcher) on their compile and runtime classpaths. + testImplementation platform('org.junit:junit-bom:6.1.3') + testImplementation 'org.junit.jupiter:junit-jupiter' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testRuntimeOnly project(':jme3-testdata') }