From ed025c817e2c726da7635e0f26598fa40141d1dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Mon, 19 Jan 2026 19:22:12 +0100 Subject: [PATCH 1/9] chore: Support Java 25 in integration tests --- agent/bin/test_projects | 18 ++++++++- agent/test/petclinic/pom-java25.patch | 55 +++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 agent/test/petclinic/pom-java25.patch diff --git a/agent/bin/test_projects b/agent/bin/test_projects index 541d20e1..c1bce35f 100755 --- a/agent/bin/test_projects +++ b/agent/bin/test_projects @@ -87,7 +87,23 @@ else install_petclinic "land-of-apps/spring-petclinic" old-java-support fi -patch -N -p1 -d build/fixtures/spring-petclinic < test/petclinic/pom.patch +# Select the appropriate patch file based on Java version +if is_java 25; then + PATCH_FILE="test/petclinic/pom-java25.patch" +else + PATCH_FILE="test/petclinic/pom.patch" +fi + +# Apply patch, but only ignore if already applied (not other failures) +if ! patch -N -p1 -d build/fixtures/spring-petclinic < "$PATCH_FILE" 2>&1 | tee /tmp/patch_output.txt; then + if grep -q "Reversed (or previously applied) patch detected" /tmp/patch_output.txt; then + echo "Patch already applied, continuing..." + else + echo "ERROR: Patch failed to apply!" + cat /tmp/patch_output.txt + exit 1 + fi +fi install_scala_test_app diff --git a/agent/test/petclinic/pom-java25.patch b/agent/test/petclinic/pom-java25.patch new file mode 100644 index 00000000..45d16060 --- /dev/null +++ b/agent/test/petclinic/pom-java25.patch @@ -0,0 +1,55 @@ +diff --git a/pom.xml b/pom.xml +index fb38cc3..6cb8530 100644 +--- a/pom.xml ++++ b/pom.xml +@@ -65,6 +65,23 @@ + + org.springframework.boot + spring-boot-starter-webmvc ++ ++ ++ org.springframework.boot ++ spring-boot-starter-tomcat ++ ++ ++ ++ ++ com.appland ++ annotation ++ LATEST ++ system ++ ${env.ANNOTATION_JAR} ++ ++ ++ com.fasterxml.jackson.core ++ jackson-databind + + + +@@ -407,5 +424,26 @@ + + + ++ ++ tomcat ++ ++ true ++ ++ ++ ++ org.springframework.boot ++ spring-boot-starter-tomcat ++ ++ ++ ++ ++ jetty ++ ++ ++ org.springframework.boot ++ spring-boot-starter-jetty ++ ++ ++ + + From 24caf48455dbbe2bdb77e06315ea6ee1458819d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Tue, 20 Jan 2026 06:30:54 +0100 Subject: [PATCH 2/9] ci: Run smoke tests on Java 25 --- .github/workflows/build-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index a5482109..627cc8ee 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -55,7 +55,7 @@ jobs: --health-retries 5 strategy: matrix: - java: ['21', '17', '11', '8'] + java: ['25', '21', '17', '11', '8'] runs-on: ubuntu-latest name: Run test suite with Java ${{ matrix.java }} needs: build-and-check From 41488c314c3c3da205a375197d495419677c1393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Tue, 20 Jan 2026 06:49:27 +0100 Subject: [PATCH 3/9] feat: Update bytebuddy dependency to support Java 25 --- agent/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/build.gradle b/agent/build.gradle index b479bbb7..c7dceb37 100644 --- a/agent/build.gradle +++ b/agent/build.gradle @@ -52,7 +52,7 @@ dependencies { implementation 'com.alibaba:fastjson:1.2.83' implementation "org.javassist:javassist:${javassistVersion}" implementation 'org.reflections:reflections:0.10.2' - implementation 'net.bytebuddy:byte-buddy:1.14.10' + implementation 'net.bytebuddy:byte-buddy:1.18.4' implementation 'org.apache.commons:commons-lang3:3.20.0' implementation 'commons-io:commons-io:2.15.1' implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.16.1' From c1183dc938225e3936cf371afd9a7db9887c5830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Tue, 20 Jan 2026 17:29:10 +0100 Subject: [PATCH 4/9] chore: Use gradle 9.1 for java 25 --- agent/test/helper.bash | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/agent/test/helper.bash b/agent/test/helper.bash index edc2b985..95a0206a 100644 --- a/agent/test/helper.bash +++ b/agent/test/helper.bash @@ -151,7 +151,13 @@ find_annotation_jar() { export ANNOTATION_JAR="$(find_annotation_jar)" # absolute gradle wrapper path (in the same directory as this file) -GRADLE_WRAPPER="$TOP_LEVEL/agent/test/gradlew" +if is_java 25; then + # use 9.1 for newer java + GRADLE_WRAPPER="$TOP_LEVEL/gradlew" +else + # this is 8.5 for older javas + GRADLE_WRAPPER="$TOP_LEVEL/agent/test/gradlew" +fi # Shared gradle wrapper function gradlew() { From 407d2b8278d04eb9b3e456c014a573b2bba5c856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Tue, 20 Jan 2026 17:29:33 +0100 Subject: [PATCH 5/9] chore: Use system java version for classloading tests --- agent/test/classloading/app/build.gradle | 8 -------- agent/test/classloading/lib/build.gradle | 7 ------- agent/test/classloading/settings.gradle | 14 -------------- 3 files changed, 29 deletions(-) diff --git a/agent/test/classloading/app/build.gradle b/agent/test/classloading/app/build.gradle index b29e9257..8e783789 100644 --- a/agent/test/classloading/app/build.gradle +++ b/agent/test/classloading/app/build.gradle @@ -35,14 +35,6 @@ dependencies { implementation files(appmapJar) } -// Apply a specific Java toolchain to ease working on different environments. -java { - toolchain { - languageVersion = JavaLanguageVersion.of(8) - } -} - - application { // Define the main class for the application. mainClass = 'com.appland.appmap.test.fixture.Runner' diff --git a/agent/test/classloading/lib/build.gradle b/agent/test/classloading/lib/build.gradle index e0b2609f..1949f0d3 100644 --- a/agent/test/classloading/lib/build.gradle +++ b/agent/test/classloading/lib/build.gradle @@ -18,10 +18,3 @@ repositories { dependencies { } - -// Apply a specific Java toolchain to ease working on different environments. -java { - toolchain { - languageVersion = JavaLanguageVersion.of(8) - } -} diff --git a/agent/test/classloading/settings.gradle b/agent/test/classloading/settings.gradle index a18cde8a..82be1b4a 100644 --- a/agent/test/classloading/settings.gradle +++ b/agent/test/classloading/settings.gradle @@ -1,16 +1,2 @@ -/* - * This file was generated by the Gradle 'init' task. - * - * The settings file is used to specify which projects to include in your build. - * - * Detailed information about configuring a multi-project build in Gradle can be found - * in the user manual at https://docs.gradle.org/8.1.1/userguide/multi_project_builds.html - */ - -plugins { - // Apply the foojay-resolver plugin to allow automatic download of JDKs - id 'org.gradle.toolchains.foojay-resolver-convention' version '0.4.0' -} - rootProject.name = 'classloading' include 'app', 'lib' From a4dabdf742ba5a8407f2e9809809815be5459b60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Tue, 20 Jan 2026 17:42:00 +0100 Subject: [PATCH 6/9] chore: use compatible gretty version for newer gradles --- agent/test/gretty-tomcat/build.gradle | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/agent/test/gretty-tomcat/build.gradle b/agent/test/gretty-tomcat/build.gradle index 4d8da04f..fe6000d5 100644 --- a/agent/test/gretty-tomcat/build.gradle +++ b/agent/test/gretty-tomcat/build.gradle @@ -1,8 +1,25 @@ +buildscript { + def grettyVersion = '4.1.6' + + // Gretty 4 does not support Gradle 9 and above + if (GradleVersion.current() >= GradleVersion.version('9.0')) { + grettyVersion = '5.0.1' + } + + repositories { + mavenCentral() + } + dependencies { + classpath "org.gretty:gretty:$grettyVersion" + } +} + plugins { id 'war' - id 'org.gretty' version '4.1.6' } +apply plugin: 'org.gretty' + repositories { mavenCentral() } From 95d527d4ecbd63c784fcfd6b6af2a99c9a32dcfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Wed, 21 Jan 2026 06:40:31 +0100 Subject: [PATCH 7/9] skip intellij tests in java 25 --- agent/test/intellij/intellij.bats | 1 + 1 file changed, 1 insertion(+) diff --git a/agent/test/intellij/intellij.bats b/agent/test/intellij/intellij.bats index 2fa1841b..35f497f5 100644 --- a/agent/test/intellij/intellij.bats +++ b/agent/test/intellij/intellij.bats @@ -6,6 +6,7 @@ init_plugin() { setup_file() { is_java 17 || skip "needs Java 17" + is_java 25 && skip "incompatible with Java 25" export AGENT_JAR="$(find_agent_jar)" From e72c08b68c51c14cdb24479ce718b300644c5906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Wed, 21 Jan 2026 14:11:51 +0100 Subject: [PATCH 8/9] More Java 25 support fixes --- .../appland/appmap/reflect/HttpHeaders.java | 3 +- agent/test/intellij/intellij.bats | 0 agent/test/petclinic/petclinic-tests.bats | 8 +++-- agent/test/petclinic/petclinic.bats | 3 +- agent/test/test-frameworks/build.gradle | 31 +++++++++---------- 5 files changed, 24 insertions(+), 21 deletions(-) mode change 100644 => 100755 agent/test/intellij/intellij.bats diff --git a/agent/src/main/java/com/appland/appmap/reflect/HttpHeaders.java b/agent/src/main/java/com/appland/appmap/reflect/HttpHeaders.java index e7af8558..5695bb75 100644 --- a/agent/src/main/java/com/appland/appmap/reflect/HttpHeaders.java +++ b/agent/src/main/java/com/appland/appmap/reflect/HttpHeaders.java @@ -32,13 +32,14 @@ default Enumeration getHeaderNames() { /** * Non-standard utility method. Retrieves all headers. + * Header names are normalized to lowercase since HTTP headers are case-insensitive. */ default Map getHeaders() { HashMap headers = new HashMap<>(); for (Enumeration e = getHeaderNames(); e.hasMoreElements();) { String headerName = (String) e.nextElement(); - headers.put(headerName, this.getHeader(headerName)); + headers.put(headerName.toLowerCase(), this.getHeader(headerName)); } return headers; diff --git a/agent/test/intellij/intellij.bats b/agent/test/intellij/intellij.bats old mode 100644 new mode 100755 diff --git a/agent/test/petclinic/petclinic-tests.bats b/agent/test/petclinic/petclinic-tests.bats index bfa11557..c7c6f7ab 100644 --- a/agent/test/petclinic/petclinic-tests.bats +++ b/agent/test/petclinic/petclinic-tests.bats @@ -43,9 +43,11 @@ run_petclinic_test() { run cat ./tmp/appmap/junit/org_springframework_samples_petclinic_${TEST_NAME}_testOwnerDetails.appmap.json assert_success - # Thread 1 is the test runner's main thread. Check to make sure that parent_id - # of the "return" event matches the id of the "call" event. - assert_json_eq '.events | map(select(.thread_id == 1)) | ((.[0].event == "call" and .[1].event == "return") and (.[1].parent_id == .[0].id))' "true" + # Find the test runner's main thread (the one with testOwnerDetails method). + # Check to make sure that parent_id of the "return" event matches the id of the "call" event. + # Note: In Java 21 the main thread is typically thread 1, but in Java 25 it can be thread 3. + local main_thread_id=$(jq -r '[.events[] | select(.method_id == "testOwnerDetails")][0].thread_id' ./tmp/appmap/junit/org_springframework_samples_petclinic_${TEST_NAME}_testOwnerDetails.appmap.json) + assert_json_eq ".events | map(select(.thread_id == ${main_thread_id})) | ((.[0].event == \"call\" and .[1].event == \"return\") and (.[1].parent_id == .[0].id))" "true" } @test "extra properties in appmap.yml are ignored when config is loaded" { diff --git a/agent/test/petclinic/petclinic.bats b/agent/test/petclinic/petclinic.bats index 31b956b9..9288d718 100644 --- a/agent/test/petclinic/petclinic.bats +++ b/agent/test/petclinic/petclinic.bats @@ -211,7 +211,8 @@ setup() { assert_success stop_recording - assert_json_eq '.events[] | .http_server_response | .headers["Content-Type"]' "text/html;charset=UTF-8" + # HTTP headers are case-insensitive and are normalized to lowercase + assert_json_eq '.events[] | .http_server_response | .headers["content-type"]' "text/html;charset=UTF-8" } @test "recordings capture elapsed time" { diff --git a/agent/test/test-frameworks/build.gradle b/agent/test/test-frameworks/build.gradle index 0097825b..629a1be8 100644 --- a/agent/test/test-frameworks/build.gradle +++ b/agent/test/test-frameworks/build.gradle @@ -36,6 +36,9 @@ def addAgentArgs = [ ] task test_junit(type: Test) { + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath + testLogging { events TestLogEvent.STANDARD_OUT } @@ -44,6 +47,9 @@ task test_junit(type: Test) { } task test_testng(type: Test) { + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath + useTestNG() testLogging { showStandardStreams = true @@ -57,21 +63,14 @@ task test_testng(type: Test) { def jdk8Dependency = 'org.testng:testng:7.5.1' def jdk11Dependency = 'org.testng:testng:7.9.0' -tasks.register('setTestDependencies') { - doLast { - def jdkVersion = javaToolchains.launcherFor(java.toolchain).get().metadata.languageVersion.asInt() - if (jdkVersion <= 8) { - dependencies { - testImplementation jdk8Dependency - } - } else { - dependencies { - testImplementation jdk11Dependency - } - } +// Add TestNG dependency during configuration phase (not execution phase) +def jdkVersion = System.getProperty('java.version').split('\\.')[0] as Integer +if (jdkVersion <= 8) { + dependencies { + testImplementation jdk8Dependency + } +} else { + dependencies { + testImplementation jdk11Dependency } -} - -tasks.named("compileTestJava") { - dependsOn 'setTestDependencies' } From 066730185d25b26d7adb36350b587c816df145c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Wed, 21 Jan 2026 15:58:38 +0100 Subject: [PATCH 9/9] Fix: use newer spring for newer java --- agent/test/jdbc/build.gradle | 44 +++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/agent/test/jdbc/build.gradle b/agent/test/jdbc/build.gradle index 162848ca..fb3c9c76 100644 --- a/agent/test/jdbc/build.gradle +++ b/agent/test/jdbc/build.gradle @@ -1,16 +1,32 @@ +buildscript { + def isModern = JavaVersion.current() >= JavaVersion.VERSION_25 + project.ext { + useJakarta = isModern + springBootVersion = isModern ? '3.1.4' : '2.7.0' + javaVersion = isModern ? JavaVersion.VERSION_17 : JavaVersion.VERSION_1_8 + } + + repositories { + mavenCentral() + } + dependencies { + classpath "org.springframework.boot:spring-boot-gradle-plugin:${project.springBootVersion}" + } +} + plugins { - id 'org.springframework.boot' version '2.7.0' - id 'io.spring.dependency-management' version '1.0.11.RELEASE' + id 'io.spring.dependency-management' version '1.1.7' id 'java' } +apply plugin: 'org.springframework.boot' + group = 'com.example' version = '0.0.1-SNAPSHOT' -sourceCompatibility = '1.8' -// suppress warnings about source compatibility -tasks.withType(JavaCompile) { - options.compilerArgs << '-Xlint:-options' +java { + sourceCompatibility = javaVersion + targetCompatibility = javaVersion } repositories { @@ -22,10 +38,26 @@ dependencies { runtimeOnly 'com.h2database:h2' runtimeOnly 'com.oracle.database.jdbc:ojdbc8:21.9.0.0' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } def appmapJar = "$System.env.AGENT_JAR" +// Spring boot 3.x uses Jakarta EE namespaces +task transformSources(type: Copy) { + from 'src/main/java' + into "$buildDir/generated-sources/transformed" + + if (useJakarta) { + filter { line -> + line.replaceAll('javax.persistence', 'jakarta.persistence') + } + } +} + +sourceSets.main.java.srcDirs = [transformSources.destinationDir] +compileJava.dependsOn transformSources + test { useJUnitPlatform() if (System.env.ORACLE_URL) {