Skip to content
Draft
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
13 changes: 10 additions & 3 deletions .cursor/rules/e2e_tests.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,16 @@ 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 Spring
Boot version in `SpringBootVersions` (build-logic), 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 run `./gradlew allIntegrationTestsBoot<major>`,
which picks up every module of the line. 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.
- 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

Expand Down
27 changes: 5 additions & 22 deletions .github/workflows/spring-boot-2-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,7 @@ jobs:
spring-boot-2-matrix:
timeout-minutes: 45
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
springboot-version: [ '2.7.0', '2.7.18' ]

name: Spring Boot ${{ matrix.springboot-version }}
name: Spring Boot 2.x
env:
GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }}

Expand Down Expand Up @@ -51,27 +46,15 @@ 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
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
# Runs every version even after one fails, as separate jobs per version would.
run: ./gradlew allIntegrationTestsBoot2 --continue

- name: Upload test results
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
with:
name: test-results-springboot-2-${{ matrix.springboot-version }}
name: test-results-springboot-2
path: |
**/build/reports/*
**/build/test-results/**/*.xml
Expand All @@ -80,7 +63,7 @@ jobs:
uses: phoenix-actions/test-reporting@7317eea6e13c47348dd0bb318669485157c518d6 # pin@v16
if: always()
with:
name: JUnit Spring Boot 2.x ${{ matrix.springboot-version }}
name: JUnit Spring Boot 2.x
path: |
**/build/test-results/**/*.xml
reporter: java-junit
Expand Down
28 changes: 5 additions & 23 deletions .github/workflows/spring-boot-3-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,7 @@ jobs:
spring-boot-3-matrix:
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' ]

name: Spring Boot ${{ matrix.springboot-version }}
name: Spring Boot 3.x
env:
GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }}

Expand Down Expand Up @@ -51,28 +46,15 @@ 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
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
# Runs every version even after one fails, as separate jobs per version would.
run: ./gradlew allIntegrationTestsBoot3 --continue

- name: Upload test results
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
with:
name: test-results-springboot-3-${{ matrix.springboot-version }}
name: test-results-springboot-3
path: |
**/build/reports/*
**/build/test-results/**/*.xml
Expand All @@ -81,7 +63,7 @@ jobs:
uses: phoenix-actions/test-reporting@7317eea6e13c47348dd0bb318669485157c518d6 # pin@v16
if: always()
with:
name: JUnit Spring Boot 3.x ${{ matrix.springboot-version }}
name: JUnit Spring Boot 3.x
path: |
**/build/test-results/**/*.xml
reporter: java-junit
Expand Down
29 changes: 5 additions & 24 deletions .github/workflows/spring-boot-4-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,7 @@ jobs:
spring-boot-4-matrix:
timeout-minutes: 45
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
springboot-version: [ '4.0.0', '4.0.5', '4.1.0' ]

name: Spring Boot ${{ matrix.springboot-version }}
name: Spring Boot 4.x
env:
GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }}

Expand Down Expand Up @@ -51,29 +46,15 @@ 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
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
# Runs every version even after one fails, as separate jobs per version would.
run: ./gradlew allIntegrationTestsBoot4 --continue

- name: Upload test results
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
with:
name: test-results-springboot-4-${{ matrix.springboot-version }}
name: test-results-springboot-4
path: |
**/build/reports/*
**/build/test-results/**/*.xml
Expand All @@ -82,7 +63,7 @@ jobs:
uses: phoenix-actions/test-reporting@7317eea6e13c47348dd0bb318669485157c518d6 # pin@v16
if: always()
with:
name: JUnit Spring Boot 4.x ${{ matrix.springboot-version }}
name: JUnit Spring Boot 4.x
path: |
**/build/test-results/**/*.xml
reporter: java-junit
Expand Down
4 changes: 4 additions & 0 deletions build-logic/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,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"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package io.sentry.gradle

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<Project> {
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<String>()

/**
* Registers `allIntegrationTestsBoot<version>` for each of [versions], and
* `allIntegrationTestsBoot<major>` for all of them, which CI runs from the root project for every
* module of the line at once.
*/
fun versions(versions: List<String>) {
this.versions = versions
val major = versions.map { it.substringBefore('.') }.distinct().single()
project.tasks.register("allIntegrationTestsBoot$major") {
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 `<name>Boot<version>` 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.create(name) {
isCanBeConsumed = false
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)
}
}
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('.', '_')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.sentry.gradle

/**
* The Spring Boot versions the Spring integration suites run on, per major line. A WAR suite runs
* on the same versions as its Boot module, because the Boot BOM decides its Spring version.
*/
object SpringBootVersions {
val BOOT_2 = listOf("2.7.0", "2.7.18")
val BOOT_3 = listOf("3.2.12", "3.3.13", "3.4.13", "3.5.13")
val BOOT_4 = listOf("4.0.0", "4.0.5", "4.1.0")
}
8 changes: 8 additions & 0 deletions sentry-spring-7/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import io.sentry.gradle.SpringBootVersions
import net.ltgt.gradle.errorprone.errorprone
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
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)
Expand Down Expand Up @@ -142,3 +144,9 @@ testing {
}
}
}

// The suites on each Spring Boot 4 version the CI matrix tests.
springBootMatrix {
versions(SpringBootVersions.BOOT_4)
test("integrationTest")
}
28 changes: 25 additions & 3 deletions sentry-spring-boot-4/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import io.sentry.gradle.ChildJvmTestArguments
import io.sentry.gradle.SpringBootVersions
import net.ltgt.gradle.errorprone.errorprone
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
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)
Expand Down Expand Up @@ -387,13 +389,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<ChildJvmTestArguments>().apply {
agentJar.from(otelAgentJar)
appClasspath.from(agentFixture.runtimeClasspath)
this.appClasspath.from(appClasspath)
}
)
systemProperty("sentry.test.agentAutoInit", autoInit)
Expand Down Expand Up @@ -423,3 +429,19 @@ listOf(
.forEach {
tasks.named<KotlinCompile>(it) { compilerOptions.javaParameters = true }
}

// The suites on each Spring Boot 4 version the CI matrix tests.
springBootMatrix {
versions(SpringBootVersions.BOOT_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))
}
}
Loading
Loading