diff --git a/.cursor/rules/e2e_tests.mdc b/.cursor/rules/e2e_tests.mdc index 1785e61b5c..3603794cd5 100644 --- a/.cursor/rules/e2e_tests.mdc +++ b/.cursor/rules/e2e_tests.mdc @@ -64,9 +64,18 @@ it. Gradle runs the two in parallel. ## Running them -- The `spring-boot-{2,3,4}-matrix.yml` workflows run the suites against each supported Spring Boot - version, by setting the version in `gradle/libs.versions.toml`. The suites aren't part of - `check`, so a new suite only runs on CI once it's added to its matrix. +- The `springBootMatrix` block in each Spring module's build file runs its suites on every version + of its Spring Boot line in `gradle/spring-boot-versions.json`, with a task per suite and version, + e.g. `integrationTestWebfluxBoot3_2_12`. The suites and the SDK compile once, against the + catalog's version; only the runtime classpath changes. `allIntegrationTestsBoot3_2_12` runs one + version's suites, `allIntegrationTestsBoot3` every version's. +- The `spring-boot-{2,3,4}-matrix.yml` workflows read the same file and run each version in its own + job, e.g. `./gradlew :sentry-spring-jakarta:allIntegrationTestsBoot3_2_12 + :sentry-spring-boot-jakarta:allIntegrationTestsBoot3_2_12`. The suites aren't part of `check`, so + a new suite only runs on CI once it's added to its module's `springBootMatrix` block, and a new + module once it's added to its workflow. +- Suite code may only use APIs that exist in the oldest version of its line. Compiled against a + newer one, a missing API fails at runtime with `NoSuchMethodError` or `NoClassDefFoundError`. ## Gotchas diff --git a/.github/workflows/spring-boot-2-matrix.yml b/.github/workflows/spring-boot-2-matrix.yml index edfe9e74fc..02b166fca4 100644 --- a/.github/workflows/spring-boot-2-matrix.yml +++ b/.github/workflows/spring-boot-2-matrix.yml @@ -15,13 +15,30 @@ concurrency: cancel-in-progress: true jobs: + versions: + runs-on: ubuntu-latest + name: Spring Boot 2.x versions + outputs: + versions: ${{ steps.versions.outputs.versions }} + + steps: + - name: Checkout Repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + sparse-checkout: gradle + + - name: Read Spring Boot 2.x versions + id: versions + run: echo "versions=$(jq -c .boot2 gradle/spring-boot-versions.json)" >> "$GITHUB_OUTPUT" + spring-boot-2-matrix: + needs: versions timeout-minutes: 45 runs-on: ubuntu-latest strategy: fail-fast: false matrix: - springboot-version: [ '2.4.13', '2.5.15', '2.6.15', '2.7.0', '2.7.18' ] + springboot-version: ${{ fromJSON(needs.versions.outputs.versions) }} name: Spring Boot ${{ matrix.springboot-version }} env: @@ -51,21 +68,13 @@ jobs: with: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - - name: Update Spring Boot 2.x version - run: | - springboot_version="${{ matrix.springboot-version }}" - perl -0pi -e 'BEGIN { $v = shift } s/^springboot2[[:space:]]*=[[:space:]]*"\K[^"]*/$v/m or die "::error::springboot2 version entry not found in gradle/libs.versions.toml\n"' "$springboot_version" gradle/libs.versions.toml - echo "Updated Spring Boot 2.x version to $springboot_version" - - name: Test integration suites + # Project paths, unlike a bare task name, keep Gradle from configuring every project. run: | - ./gradlew \ - :sentry-spring:integrationTest \ - :sentry-spring-boot:integrationTest \ - :sentry-spring-boot:integrationTestWebflux \ - :sentry-spring-boot:integrationTestOtelNoAgent \ - :sentry-spring-boot:integrationTestOtelAgent \ - :sentry-spring-boot:integrationTestOtelAgentWithoutAutoInit + task="allIntegrationTestsBoot${SPRINGBOOT_VERSION//./_}" + ./gradlew ":sentry-spring:$task" ":sentry-spring-boot:$task" + env: + SPRINGBOOT_VERSION: ${{ matrix.springboot-version }} - name: Upload test results if: always() diff --git a/.github/workflows/spring-boot-3-matrix.yml b/.github/workflows/spring-boot-3-matrix.yml index f698683cdc..0cbe496cda 100644 --- a/.github/workflows/spring-boot-3-matrix.yml +++ b/.github/workflows/spring-boot-3-matrix.yml @@ -15,13 +15,30 @@ concurrency: cancel-in-progress: true jobs: + versions: + runs-on: ubuntu-latest + name: Spring Boot 3.x versions + outputs: + versions: ${{ steps.versions.outputs.versions }} + + steps: + - name: Checkout Repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + sparse-checkout: gradle + + - name: Read Spring Boot 3.x versions + id: versions + run: echo "versions=$(jq -c .boot3 gradle/spring-boot-versions.json)" >> "$GITHUB_OUTPUT" + spring-boot-3-matrix: + needs: versions timeout-minutes: 45 runs-on: ubuntu-latest strategy: fail-fast: false matrix: - springboot-version: [ '3.2.12', '3.3.13', '3.4.13', '3.5.13' ] + springboot-version: ${{ fromJSON(needs.versions.outputs.versions) }} name: Spring Boot ${{ matrix.springboot-version }} env: @@ -51,22 +68,13 @@ jobs: with: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - - name: Update Spring Boot 3.x version - run: | - springboot_version="${{ matrix.springboot-version }}" - perl -0pi -e 'BEGIN { $v = shift } s/^springboot3[[:space:]]*=[[:space:]]*"\K[^"]*/$v/m or die "::error::springboot3 version entry not found in gradle/libs.versions.toml\n"' "$springboot_version" gradle/libs.versions.toml - echo "Updated Spring Boot 3.x version to $springboot_version" - - name: Test integration suites + # Project paths, unlike a bare task name, keep Gradle from configuring every project. run: | - ./gradlew \ - :sentry-spring-jakarta:integrationTest \ - :sentry-spring-boot-jakarta:integrationTest \ - :sentry-spring-boot-jakarta:integrationTestWebflux \ - :sentry-spring-boot-jakarta:integrationTestOtelNoAgent \ - :sentry-spring-boot-jakarta:integrationTestLog4j2 \ - :sentry-spring-boot-jakarta:integrationTestOtelAgent \ - :sentry-spring-boot-jakarta:integrationTestOtelAgentWithoutAutoInit + task="allIntegrationTestsBoot${SPRINGBOOT_VERSION//./_}" + ./gradlew ":sentry-spring-jakarta:$task" ":sentry-spring-boot-jakarta:$task" + env: + SPRINGBOOT_VERSION: ${{ matrix.springboot-version }} - name: Upload test results if: always() diff --git a/.github/workflows/spring-boot-4-matrix.yml b/.github/workflows/spring-boot-4-matrix.yml index 8f91289db9..75070612eb 100644 --- a/.github/workflows/spring-boot-4-matrix.yml +++ b/.github/workflows/spring-boot-4-matrix.yml @@ -15,13 +15,30 @@ concurrency: cancel-in-progress: true jobs: + versions: + runs-on: ubuntu-latest + name: Spring Boot 4.x versions + outputs: + versions: ${{ steps.versions.outputs.versions }} + + steps: + - name: Checkout Repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + sparse-checkout: gradle + + - name: Read Spring Boot 4.x versions + id: versions + run: echo "versions=$(jq -c .boot4 gradle/spring-boot-versions.json)" >> "$GITHUB_OUTPUT" + spring-boot-4-matrix: + needs: versions timeout-minutes: 45 runs-on: ubuntu-latest strategy: fail-fast: false matrix: - springboot-version: [ '4.0.0', '4.0.5', '4.1.0' ] + springboot-version: ${{ fromJSON(needs.versions.outputs.versions) }} name: Spring Boot ${{ matrix.springboot-version }} env: @@ -51,23 +68,13 @@ jobs: with: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - - name: Update Spring Boot 4.x version - run: | - springboot_version="${{ matrix.springboot-version }}" - perl -0pi -e 'BEGIN { $v = shift } s/^springboot4[[:space:]]*=[[:space:]]*"\K[^"]*/$v/m or die "::error::springboot4 version entry not found in gradle/libs.versions.toml\n"' "$springboot_version" gradle/libs.versions.toml - echo "Updated Spring Boot 4.x version to $springboot_version" - - name: Test integration suites + # Project paths, unlike a bare task name, keep Gradle from configuring every project. run: | - ./gradlew \ - :sentry-spring-7:integrationTest \ - :sentry-spring-boot-4:integrationTest \ - :sentry-spring-boot-4:integrationTestWebflux \ - :sentry-spring-boot-4:integrationTestOtelNoAgent \ - :sentry-spring-boot-4:integrationTestLog4j2 \ - :sentry-spring-boot-4:integrationTestOtelAgent \ - :sentry-spring-boot-4:integrationTestOtelAgentWithoutAutoInit \ - :sentry-spring-boot-4:integrationTestOtlp + task="allIntegrationTestsBoot${SPRINGBOOT_VERSION//./_}" + ./gradlew ":sentry-spring-7:$task" ":sentry-spring-boot-4:$task" + env: + SPRINGBOOT_VERSION: ${{ matrix.springboot-version }} - name: Upload test results if: always() diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index 60a340c9b8..29760f38a9 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -24,5 +24,9 @@ gradlePlugin { id = "io.sentry.animalsniffer.android" implementationClass = "io.sentry.gradle.SentryAnimalSnifferAndroidPlugin" } + register("sentrySpringBootMatrix") { + id = "io.sentry.spring-boot-matrix" + implementationClass = "io.sentry.gradle.SpringBootMatrixPlugin" + } } } diff --git a/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootMatrixPlugin.kt b/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootMatrixPlugin.kt new file mode 100644 index 0000000000..c75a316651 --- /dev/null +++ b/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootMatrixPlugin.kt @@ -0,0 +1,109 @@ +package io.sentry.gradle + +import groovy.json.JsonSlurper +import javax.inject.Inject +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.file.FileCollection +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.testing.Test +import org.gradle.language.base.plugins.LifecycleBasePlugin + +/** + * Runs integration suites on each Spring Boot version of a line, without recompiling anything. + * + * The suites and the SDK compile once, against the version catalog's Spring Boot version. For each + * version, a suite's test task reuses its compiled classes on a runtime classpath resolved for that + * version, which is how users run our published jars on their Spring Boot version. + */ +class SpringBootMatrixPlugin : Plugin { + override fun apply(project: Project) { + project.extensions.create("springBootMatrix", SpringBootMatrixExtension::class.java) + } +} + +abstract class SpringBootMatrixExtension @Inject constructor(private val project: Project) { + private var versions = emptyList() + + /** + * Registers `allIntegrationTestsBoot` for each version of Spring Boot [line] in + * `gradle/spring-boot-versions.json`, and `allIntegrationTestsBoot` for all of them. The CI + * workflows read the same file and run each version in its own job. A WAR module uses its Boot + * module's line, because the Boot BOM decides its Spring version. + */ + fun versions(line: Int) { + val file = project.layout.settingsDirectory.file("gradle/spring-boot-versions.json") + val lines = + JsonSlurper().parseText(project.providers.fileContents(file).asText.get()) as Map<*, *> + val versions = (lines["boot$line"] as List<*>).map { it as String } + this.versions = versions + project.tasks.register("allIntegrationTestsBoot$line") { + description = "Runs the integration suites on Spring Boot ${versions.joinToString()}." + group = LifecycleBasePlugin.VERIFICATION_GROUP + dependsOn(versions.map { aggregateName(it) }) + } + versions.forEach { version -> + project.tasks.register(aggregateName(version)) { + description = "Runs the integration suites on Spring Boot $version." + group = LifecycleBasePlugin.VERIFICATION_GROUP + } + } + } + + /** + * Registers `Boot` for each version, running [suite]'s tests. [name] tells apart + * several tasks of one suite, and [configure] adds what the suite's own task has beyond its + * classes and classpath. + */ + fun test(suite: String, name: String = suite, configure: Test.(version: String) -> Unit = {}) { + check(versions.isNotEmpty()) { "Call versions() before test()" } + versions.forEach { version -> + // Created now rather than with the task, so `dependencies --configuration` finds it. + val runtimeClasspath = runtimeClasspath(suite, version) + val task = + project.tasks.register("$name${suffix(version)}", Test::class.java) { + description = "Runs $name on Spring Boot $version." + group = LifecycleBasePlugin.VERIFICATION_GROUP + testClassesDirs = sourceSet(suite).output.classesDirs + classpath = runtimeClasspath + useJUnit() + configure(version) + } + project.tasks.named(aggregateName(version)) { dependsOn(task) } + } + } + + /** [sourceSet]'s output and runtime dependencies, with Spring Boot at [version]. */ + fun runtimeClasspath(sourceSet: String, version: String): FileCollection { + val sources = sourceSet(sourceSet) + val name = "$sourceSet${suffix(version)}RuntimeClasspath" + val configurations = project.configurations + val dependencies = + configurations.findByName(name) + ?: configurations + .resolvable(name) { + extendsFrom( + configurations.getByName(sources.implementationConfigurationName), + configurations.getByName(sources.runtimeOnlyConfigurationName), + ) + attributes.addAllLater( + configurations.getByName(sources.runtimeClasspathConfigurationName).attributes + ) + // What setting the version in the catalog does: every Spring Boot artifact, the BOM + // included, is in this group. Libraries pinned to their own version still win + // conflicts. + resolutionStrategy.eachDependency { + if (requested.group == "org.springframework.boot") useVersion(version) + } + } + .get() + return sources.output + dependencies + } + + private fun sourceSet(name: String) = + project.extensions.getByType(SourceSetContainer::class.java).getByName(name) + + private fun aggregateName(version: String) = "allIntegrationTests${suffix(version)}" + + private fun suffix(version: String) = "Boot" + version.replace('.', '_') +} diff --git a/gradle/spring-boot-versions.json b/gradle/spring-boot-versions.json new file mode 100644 index 0000000000..efe0fdd062 --- /dev/null +++ b/gradle/spring-boot-versions.json @@ -0,0 +1,5 @@ +{ + "boot2": ["2.7.0", "2.7.18"], + "boot3": ["3.2.12", "3.3.13", "3.4.13", "3.5.13"], + "boot4": ["4.0.0", "4.0.5", "4.1.0"] +} diff --git a/sentry-spring-7/build.gradle.kts b/sentry-spring-7/build.gradle.kts index b9d418abe4..5b31c03162 100644 --- a/sentry-spring-7/build.gradle.kts +++ b/sentry-spring-7/build.gradle.kts @@ -5,6 +5,7 @@ import org.springframework.boot.gradle.plugin.SpringBootPlugin plugins { `java-library` id("io.sentry.javadoc") + id("io.sentry.spring-boot-matrix") alias(libs.plugins.kotlin.jvm) alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -142,3 +143,9 @@ testing { } } } + +// The suites on each Spring Boot 4 version the CI matrix tests. +springBootMatrix { + versions(line = 4) + test("integrationTest") +} diff --git a/sentry-spring-boot-4/build.gradle.kts b/sentry-spring-boot-4/build.gradle.kts index 7f4428551b..947657f62c 100644 --- a/sentry-spring-boot-4/build.gradle.kts +++ b/sentry-spring-boot-4/build.gradle.kts @@ -6,6 +6,7 @@ import org.springframework.boot.gradle.plugin.SpringBootPlugin plugins { `java-library` id("io.sentry.javadoc") + id("io.sentry.spring-boot-matrix") alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.spring) alias(libs.plugins.errorprone) @@ -387,13 +388,17 @@ testing { /** * Has the OpenTelemetry agent tests start agentFixture's app in a child JVM, with the built agent * attached. [autoInit] decides whether the agent initializes Sentry or, as with - * `SENTRY_AUTO_INIT=false`, the Spring Boot starter. + * `SENTRY_AUTO_INIT=false`, the Spring Boot starter. [appClasspath] is the app's classpath for + * another Spring Boot version. */ -fun Test.runAgentFixture(autoInit: Boolean) { +fun Test.runAgentFixture( + autoInit: Boolean, + appClasspath: FileCollection = agentFixture.runtimeClasspath, +) { jvmArgumentProviders.add( objects.newInstance().apply { agentJar.from(otelAgentJar) - appClasspath.from(agentFixture.runtimeClasspath) + this.appClasspath.from(appClasspath) } ) systemProperty("sentry.test.agentAutoInit", autoInit) @@ -423,3 +428,19 @@ listOf( .forEach { tasks.named(it) { compilerOptions.javaParameters = true } } + +// The suites on each Spring Boot 4 version the CI matrix tests. +springBootMatrix { + versions(line = 4) + test("integrationTest") + test("integrationTestLog4j2") + test("integrationTestWebflux") + test("integrationTestOtelNoAgent") + test("integrationTestOtlp") + test("integrationTestOtelAgent") { version -> + runAgentFixture(autoInit = true, appClasspath = runtimeClasspath("agentFixture", version)) + } + test("integrationTestOtelAgent", name = "integrationTestOtelAgentWithoutAutoInit") { version -> + runAgentFixture(autoInit = false, appClasspath = runtimeClasspath("agentFixture", version)) + } +} diff --git a/sentry-spring-boot-jakarta/build.gradle.kts b/sentry-spring-boot-jakarta/build.gradle.kts index 9451cf9511..8587e9e2a6 100644 --- a/sentry-spring-boot-jakarta/build.gradle.kts +++ b/sentry-spring-boot-jakarta/build.gradle.kts @@ -6,6 +6,7 @@ import org.springframework.boot.gradle.plugin.SpringBootPlugin plugins { `java-library` id("io.sentry.javadoc") + id("io.sentry.spring-boot-matrix") alias(libs.plugins.kotlin.jvm) alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -327,13 +328,17 @@ testing { /** * Has the OpenTelemetry agent tests start agentFixture's app in a child JVM, with the built agent * attached. [autoInit] decides whether the agent initializes Sentry or, as with - * `SENTRY_AUTO_INIT=false`, the Spring Boot starter. + * `SENTRY_AUTO_INIT=false`, the Spring Boot starter. [appClasspath] is the app's classpath for + * another Spring Boot version. */ -fun Test.runAgentFixture(autoInit: Boolean) { +fun Test.runAgentFixture( + autoInit: Boolean, + appClasspath: FileCollection = agentFixture.runtimeClasspath, +) { jvmArgumentProviders.add( objects.newInstance().apply { agentJar.from(otelAgentJar) - appClasspath.from(agentFixture.runtimeClasspath) + this.appClasspath.from(appClasspath) } ) systemProperty("sentry.test.agentAutoInit", autoInit) @@ -362,3 +367,18 @@ listOf( .forEach { tasks.named(it) { compilerOptions.javaParameters = true } } + +// The suites on each Spring Boot 3 version the CI matrix tests. +springBootMatrix { + versions(line = 3) + test("integrationTest") + test("integrationTestLog4j2") + test("integrationTestWebflux") + test("integrationTestOtelNoAgent") + test("integrationTestOtelAgent") { version -> + runAgentFixture(autoInit = true, appClasspath = runtimeClasspath("agentFixture", version)) + } + test("integrationTestOtelAgent", name = "integrationTestOtelAgentWithoutAutoInit") { version -> + runAgentFixture(autoInit = false, appClasspath = runtimeClasspath("agentFixture", version)) + } +} diff --git a/sentry-spring-boot/build.gradle.kts b/sentry-spring-boot/build.gradle.kts index 151d5a829d..266142dd2b 100644 --- a/sentry-spring-boot/build.gradle.kts +++ b/sentry-spring-boot/build.gradle.kts @@ -6,6 +6,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` id("io.sentry.javadoc") + id("io.sentry.spring-boot-matrix") alias(libs.plugins.kotlin.jvm) alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -21,9 +22,8 @@ tasks.named("compileKotlin") { // Spring scans the integration test apps' own bytecode with its repackaged ASM at startup, and // the ASM in Spring before 5.3.26 fails on class files as new as the toolchain's: "Unsupported -// class file major version 65". In the Spring Boot 2.x matrix, 2.4.13 (Spring 5.3.13) reads up -// to Java 18 and 2.7.0 (Spring 5.3.20) up to Java 19, so the suites target 17. Remove this once -// the matrix drops 2.4.13 and moves 2.7.0 to 2.7.10 or later. +// class file major version 65". The Spring Boot 2.x matrix's 2.7.0 (Spring 5.3.20) reads up to +// Java 19, so the suites target 17. Remove this once the matrix moves 2.7.0 to 2.7.10 or later. // The suites are Kotlin only. Their Java tasks keep the toolchain's level, because it decides // which Java version their classpaths ask for, and sentry-test-support needs the toolchain's. tasks @@ -137,13 +137,6 @@ tasks.jar { } } -// Spring Boot supports GraphQL only from 2.7, and its Kafka auto-configuration needs 2.7 for the -// spring-kafka version Sentry integrates with. The Spring Boot 2 matrix also covers older versions. -val bootSupportsGraphqlAndKafka = - libs.versions.springboot2.get().split(".").let { (major, minor) -> - major.toInt() > 2 || minor.toInt() >= 7 - } - // The OpenTelemetry agent tests run this app in a child JVM, on its own classpath like a real app // with the agent attached, rather than on the test's classpath. val agentFixture = sourceSets.create("agentFixture") @@ -164,20 +157,18 @@ dependencies { "agentFixtureImplementation"(platform(libs.springboot2.bom)) "agentFixtureImplementation"(projects.sentrySpringBootStarter) "agentFixtureImplementation"(projects.sentryAsyncProfiler) + "agentFixtureImplementation"(projects.sentryGraphql) "agentFixtureImplementation"(projects.sentryJdbc) + "agentFixtureImplementation"(projects.sentryKafka) "agentFixtureImplementation"(projects.sentryLogback) "agentFixtureImplementation"(kotlin(Config.kotlinStdLib)) "agentFixtureImplementation"(libs.otel) + "agentFixtureImplementation"(libs.springboot.starter.graphql) "agentFixtureImplementation"(libs.springboot.starter.jdbc) "agentFixtureImplementation"(libs.springboot.starter.web) "agentFixtureImplementation"(libs.springboot.starter.webflux) + "agentFixtureImplementation"(libs.spring.kafka2) "agentFixtureRuntimeOnly"(libs.hsqldb) - if (bootSupportsGraphqlAndKafka) { - "agentFixtureImplementation"(projects.sentryGraphql) - "agentFixtureImplementation"(projects.sentryKafka) - "agentFixtureImplementation"(libs.springboot.starter.graphql) - "agentFixtureImplementation"(libs.spring.kafka2) - } otelAgentJar(projects.sentryOpentelemetry.sentryOpentelemetryAgent) } @@ -192,7 +183,9 @@ testing { implementation(platform(libs.springboot2.bom)) implementation(projects.sentrySpringBootStarter) implementation(projects.sentryAsyncProfiler) + implementation(projects.sentryGraphql) implementation(projects.sentryJdbc) + implementation(projects.sentryKafka) implementation(projects.sentryLogback) implementation(projects.sentryOkhttp) implementation(projects.sentryTestSupport) @@ -202,18 +195,14 @@ testing { implementation(libs.okhttp) implementation(libs.springboot.starter.aop) implementation(libs.springboot.starter.cache) + implementation(libs.springboot.starter.graphql) implementation(libs.springboot.starter.jdbc) implementation(libs.springboot.starter.test) implementation(libs.springboot.starter.web) implementation(libs.springboot.starter.webflux) + implementation(libs.spring.kafka2) + implementation(libs.spring.kafka2.test) runtimeOnly(libs.hsqldb) - if (bootSupportsGraphqlAndKafka) { - implementation(projects.sentryGraphql) - implementation(projects.sentryKafka) - implementation(libs.springboot.starter.graphql) - implementation(libs.spring.kafka2) - implementation(libs.spring.kafka2.test) - } } } @@ -224,16 +213,14 @@ testing { implementation(platform(libs.springboot2.bom)) implementation(projects.sentrySpringBootStarter) implementation(projects.sentryAsyncProfiler) + implementation(projects.sentryGraphql) implementation(projects.sentryLogback) implementation(projects.sentryTestSupport) implementation(libs.kotlin.test.junit) implementation(libs.google.truth) + implementation(libs.springboot.starter.graphql) implementation(libs.springboot.starter.test) implementation(libs.springboot.starter.webflux) - if (bootSupportsGraphqlAndKafka) { - implementation(projects.sentryGraphql) - implementation(libs.springboot.starter.graphql) - } } } @@ -246,24 +233,22 @@ testing { implementation(projects.sentrySpringBootStarter) implementation(projects.sentryOpentelemetry.sentryOpentelemetryAgentlessSpring) implementation(projects.sentryAsyncProfiler) + implementation(projects.sentryGraphql) implementation(projects.sentryJdbc) + implementation(projects.sentryKafka) implementation(projects.sentryLogback) implementation(projects.sentryTestSupport) implementation(libs.kotlin.test.junit) implementation(libs.google.truth) implementation(libs.springboot.starter.aop) + implementation(libs.springboot.starter.graphql) implementation(libs.springboot.starter.jdbc) implementation(libs.springboot.starter.test) implementation(libs.springboot.starter.web) implementation(libs.springboot.starter.webflux) + implementation(libs.spring.kafka2) + implementation(libs.spring.kafka2.test) runtimeOnly(libs.hsqldb) - if (bootSupportsGraphqlAndKafka) { - implementation(projects.sentryGraphql) - implementation(projects.sentryKafka) - implementation(libs.springboot.starter.graphql) - implementation(libs.spring.kafka2) - implementation(libs.spring.kafka2.test) - } } } @@ -278,9 +263,7 @@ testing { // For TestRestTemplate. Only the app's JVM runs the agent, so it doesn't trace the tests. implementation(libs.springboot.starter.test) implementation(libs.springboot.starter.web) - if (bootSupportsGraphqlAndKafka) { - implementation(libs.spring.kafka2.test) - } + implementation(libs.spring.kafka2.test) } targets.all { testTask.configure { runAgentFixture(autoInit = true) } } } @@ -290,13 +273,17 @@ testing { /** * Has the OpenTelemetry agent tests start agentFixture's app in a child JVM, with the built agent * attached. [autoInit] decides whether the agent initializes Sentry or, as with - * `SENTRY_AUTO_INIT=false`, the Spring Boot starter. + * `SENTRY_AUTO_INIT=false`, the Spring Boot starter. [appClasspath] is the app's classpath for + * another Spring Boot version. */ -fun Test.runAgentFixture(autoInit: Boolean) { +fun Test.runAgentFixture( + autoInit: Boolean, + appClasspath: FileCollection = agentFixture.runtimeClasspath, +) { jvmArgumentProviders.add( objects.newInstance().apply { agentJar.from(otelAgentJar) - appClasspath.from(agentFixture.runtimeClasspath) + this.appClasspath.from(appClasspath) } ) systemProperty("sentry.test.agentAutoInit", autoInit) @@ -324,10 +311,19 @@ listOf( "compileIntegrationTestOtelAgentKotlin", ) .forEach { - tasks.named(it) { - compilerOptions.javaParameters = true - if (!bootSupportsGraphqlAndKafka) { - exclude("**/Graphql*", "**/Greeting*", "**/Kafka*") - } - } + tasks.named(it) { compilerOptions.javaParameters = true } } + +// The suites on each Spring Boot 2 version the CI matrix tests. +springBootMatrix { + versions(line = 2) + test("integrationTest") + test("integrationTestWebflux") + test("integrationTestOtelNoAgent") + test("integrationTestOtelAgent") { version -> + runAgentFixture(autoInit = true, appClasspath = runtimeClasspath("agentFixture", version)) + } + test("integrationTestOtelAgent", name = "integrationTestOtelAgentWithoutAutoInit") { version -> + runAgentFixture(autoInit = false, appClasspath = runtimeClasspath("agentFixture", version)) + } +} diff --git a/sentry-spring-boot/src/agentFixture/kotlin/io/sentry/spring/boot/agentfixture/KafkaController.kt b/sentry-spring-boot/src/agentFixture/kotlin/io/sentry/spring/boot/agentfixture/KafkaController.kt index 7566632434..28cf59e737 100644 --- a/sentry-spring-boot/src/agentFixture/kotlin/io/sentry/spring/boot/agentfixture/KafkaController.kt +++ b/sentry-spring-boot/src/agentFixture/kotlin/io/sentry/spring/boot/agentfixture/KafkaController.kt @@ -11,8 +11,8 @@ import org.springframework.web.bind.annotation.RestController const val KAFKA_TOPIC = "sentry-topic" -// Kafka only works with Spring Boot 2.7 and later, so its beans belong to a profile rather than to -// the app most tests share. See KafkaOtelCoexistenceIntegrationTest. +// The beans belong to a profile, so only the app KafkaOtelCoexistenceIntegrationTest starts needs a +// broker. @RestController @Profile("kafka") diff --git a/sentry-spring-boot/src/integrationTest/kotlin/io/sentry/spring/boot/integrationtest/KafkaController.kt b/sentry-spring-boot/src/integrationTest/kotlin/io/sentry/spring/boot/integrationtest/KafkaController.kt index 04922bdae0..1613809885 100644 --- a/sentry-spring-boot/src/integrationTest/kotlin/io/sentry/spring/boot/integrationtest/KafkaController.kt +++ b/sentry-spring-boot/src/integrationTest/kotlin/io/sentry/spring/boot/integrationtest/KafkaController.kt @@ -11,8 +11,7 @@ import org.springframework.web.bind.annotation.RestController const val KAFKA_TOPIC = "sentry-topic" -// Kafka only works with Spring Boot 2.7 and later, so its beans and broker belong to a profile -// rather than to every test's context. See KafkaIntegrationTest. +// The beans belong to a profile, so only KafkaIntegrationTest's context needs a broker. @RestController @Profile("kafka") diff --git a/sentry-spring-boot/src/integrationTest/kotlin/io/sentry/spring/boot/integrationtest/KafkaIntegrationTest.kt b/sentry-spring-boot/src/integrationTest/kotlin/io/sentry/spring/boot/integrationtest/KafkaIntegrationTest.kt index 6bd290baf7..e6e8241268 100644 --- a/sentry-spring-boot/src/integrationTest/kotlin/io/sentry/spring/boot/integrationtest/KafkaIntegrationTest.kt +++ b/sentry-spring-boot/src/integrationTest/kotlin/io/sentry/spring/boot/integrationtest/KafkaIntegrationTest.kt @@ -11,8 +11,8 @@ import org.springframework.test.context.TestPropertySource /** * A message sent through Kafka is traced on both sides: where it's published and processed. * - * Unlike the other tests, this one runs its own application context with an in-JVM broker, because - * the shared context has to work on Spring Boot versions without Kafka support. + * Unlike the other tests, this one runs its own application context with an in-JVM broker, so the + * shared context doesn't need one. */ @ActiveProfiles("kafka") @EmbeddedKafka(topics = [KAFKA_TOPIC], bootstrapServersProperty = "spring.kafka.bootstrap-servers") diff --git a/sentry-spring-boot/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/otelagentintegrationtest/KafkaOtelCoexistenceIntegrationTest.kt b/sentry-spring-boot/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/otelagentintegrationtest/KafkaOtelCoexistenceIntegrationTest.kt index c786274b9e..6913a908fa 100644 --- a/sentry-spring-boot/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/otelagentintegrationtest/KafkaOtelCoexistenceIntegrationTest.kt +++ b/sentry-spring-boot/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/otelagentintegrationtest/KafkaOtelCoexistenceIntegrationTest.kt @@ -12,8 +12,8 @@ import org.springframework.kafka.test.EmbeddedKafkaBroker * With the agent active, OpenTelemetry traces Kafka instead of Sentry's own Spring Kafka * integration. * - * Unlike the other tests, this one starts an app of its own, with Kafka and an in-JVM broker. The - * shared app leaves them out, because it has to work on Spring Boot versions without Kafka support. + * Unlike the other tests, this one starts an app of its own, with Kafka and an in-JVM broker, so + * the shared app doesn't need a broker. */ class KafkaOtelCoexistenceIntegrationTest : IntegrationTest() { override val app: AgentApp diff --git a/sentry-spring-boot/src/integrationTestOtelNoAgent/kotlin/io/sentry/spring/boot/otelnoagentintegrationtest/KafkaController.kt b/sentry-spring-boot/src/integrationTestOtelNoAgent/kotlin/io/sentry/spring/boot/otelnoagentintegrationtest/KafkaController.kt index e564146043..24f6ed7b6b 100644 --- a/sentry-spring-boot/src/integrationTestOtelNoAgent/kotlin/io/sentry/spring/boot/otelnoagentintegrationtest/KafkaController.kt +++ b/sentry-spring-boot/src/integrationTestOtelNoAgent/kotlin/io/sentry/spring/boot/otelnoagentintegrationtest/KafkaController.kt @@ -11,8 +11,8 @@ import org.springframework.web.bind.annotation.RestController const val KAFKA_TOPIC = "sentry-topic" -// Kafka only works with Spring Boot 2.7 and later, so its beans and broker belong to a profile -// rather than to every test's context. See KafkaOtelCoexistenceIntegrationTest. +// The beans belong to a profile, so only KafkaOtelCoexistenceIntegrationTest's context needs a +// broker. @RestController @Profile("kafka") diff --git a/sentry-spring-boot/src/integrationTestOtelNoAgent/kotlin/io/sentry/spring/boot/otelnoagentintegrationtest/KafkaOtelCoexistenceIntegrationTest.kt b/sentry-spring-boot/src/integrationTestOtelNoAgent/kotlin/io/sentry/spring/boot/otelnoagentintegrationtest/KafkaOtelCoexistenceIntegrationTest.kt index a2a01ce0c4..5efef8875b 100644 --- a/sentry-spring-boot/src/integrationTestOtelNoAgent/kotlin/io/sentry/spring/boot/otelnoagentintegrationtest/KafkaOtelCoexistenceIntegrationTest.kt +++ b/sentry-spring-boot/src/integrationTestOtelNoAgent/kotlin/io/sentry/spring/boot/otelnoagentintegrationtest/KafkaOtelCoexistenceIntegrationTest.kt @@ -11,8 +11,8 @@ import org.springframework.test.context.TestPropertySource /** * With OpenTelemetry active, it traces Kafka instead of Sentry's own Spring Kafka integration. * - * Unlike the other tests, this one adds Kafka and an in-JVM broker to its application context. The - * others leave them out, because they have to work on Spring Boot versions without Kafka support. + * Unlike the other tests, this one adds Kafka and an in-JVM broker to its application context, so + * the others don't need a broker. */ @ActiveProfiles("kafka") @EmbeddedKafka(topics = [KAFKA_TOPIC], bootstrapServersProperty = "spring.kafka.bootstrap-servers") diff --git a/sentry-spring-jakarta/build.gradle.kts b/sentry-spring-jakarta/build.gradle.kts index 701d5a0420..34e9b80bf6 100644 --- a/sentry-spring-jakarta/build.gradle.kts +++ b/sentry-spring-jakarta/build.gradle.kts @@ -5,6 +5,7 @@ import org.springframework.boot.gradle.plugin.SpringBootPlugin plugins { `java-library` id("io.sentry.javadoc") + id("io.sentry.spring-boot-matrix") alias(libs.plugins.kotlin.jvm) alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -129,3 +130,9 @@ testing { } } } + +// The suites on each Spring Boot 3 version the CI matrix tests. +springBootMatrix { + versions(line = 3) + test("integrationTest") +} diff --git a/sentry-spring/build.gradle.kts b/sentry-spring/build.gradle.kts index f11087c1b0..b5a8440e2b 100644 --- a/sentry-spring/build.gradle.kts +++ b/sentry-spring/build.gradle.kts @@ -5,6 +5,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` id("io.sentry.javadoc") + id("io.sentry.spring-boot-matrix") alias(libs.plugins.kotlin.jvm) alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -117,3 +118,9 @@ testing { } } } + +// The suites on each Spring Boot 2 version the CI matrix tests. +springBootMatrix { + versions(line = 2) + test("integrationTest") +}