Skip to content
Open
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
15 changes: 12 additions & 3 deletions .cursor/rules/e2e_tests.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
37 changes: 23 additions & 14 deletions .github/workflows/spring-boot-2-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.7.0', '2.7.18' ]
springboot-version: ${{ fromJSON(needs.versions.outputs.versions) }}

name: Spring Boot ${{ matrix.springboot-version }}
env:
Expand Down Expand Up @@ -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()
Expand Down
38 changes: 23 additions & 15 deletions .github/workflows/spring-boot-3-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand Down
39 changes: 23 additions & 16 deletions .github/workflows/spring-boot-4-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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()
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 @@ -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"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
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<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 version of Spring Boot [line] in
* `gradle/spring-boot-versions.json`, and `allIntegrationTestsBoot<line>` 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 `<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('.', '_')
}
5 changes: 5 additions & 0 deletions gradle/spring-boot-versions.json
Original file line number Diff line number Diff line change
@@ -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"]
}
7 changes: 7 additions & 0 deletions sentry-spring-7/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -142,3 +143,9 @@ testing {
}
}
}

// The suites on each Spring Boot 4 version the CI matrix tests.
springBootMatrix {
versions(line = 4)
test("integrationTest")
}
27 changes: 24 additions & 3 deletions sentry-spring-boot-4/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<ChildJvmTestArguments>().apply {
agentJar.from(otelAgentJar)
appClasspath.from(agentFixture.runtimeClasspath)
this.appClasspath.from(appClasspath)
}
)
systemProperty("sentry.test.agentAutoInit", autoInit)
Expand Down Expand Up @@ -423,3 +428,19 @@ listOf(
.forEach {
tasks.named<KotlinCompile>(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))
}
}
Loading
Loading