diff --git a/.cursor/rules/e2e_tests.mdc b/.cursor/rules/e2e_tests.mdc index 3de11557f8..8295253b07 100644 --- a/.cursor/rules/e2e_tests.mdc +++ b/.cursor/rules/e2e_tests.mdc @@ -56,11 +56,14 @@ match on values only they produce. ## The OpenTelemetry agent The agent must not run in the test's JVM, because it would instrument the test's own HTTP client -and server. The Spring Boot modules have an `agentFixture` source set with the app. `AgentApp` -starts it once per test JVM in a child process, with `ChildJvmApp` and the agent jar passed through +and server. The `io.sentry.otel-agent-tests` plugin in `build-logic` gives the Spring Boot modules +an `agentFixture` source set with the app, the `integrationTestOtelAgent` suite and the built agent +jar; each module adds only the dependencies of its Spring Boot line. `AgentApp` starts the app once +per test JVM in a child process, with `ChildJvmApp` and the agent jar passed through `ChildJvmTestArguments`. The suite's task runs with the agent initializing Sentry, and `integrationTestOtelAgentWithoutAutoInit` runs the same tests with the Spring Boot starter doing -it. Gradle runs the two in parallel. +it. Gradle runs the two in parallel. `otelAgentTests()` in a `springBootMatrix` block registers +both on each version. ## Running them diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index d11d2dcdc3..2c48083cdc 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -46,5 +46,9 @@ gradlePlugin { id = "io.sentry.spring-boot-matrix" implementationClass = "io.sentry.gradle.SpringBootMatrixPlugin" } + register("sentryOtelAgentTests") { + id = "io.sentry.otel-agent-tests" + implementationClass = "io.sentry.gradle.OtelAgentTestsPlugin" + } } } diff --git a/build-logic/src/main/kotlin/io/sentry/gradle/OtelAgentTestsPlugin.kt b/build-logic/src/main/kotlin/io/sentry/gradle/OtelAgentTestsPlugin.kt new file mode 100644 index 0000000000..33271f731a --- /dev/null +++ b/build-logic/src/main/kotlin/io/sentry/gradle/OtelAgentTestsPlugin.kt @@ -0,0 +1,107 @@ +package io.sentry.gradle + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.attributes.Bundling +import org.gradle.api.attributes.Usage +import org.gradle.api.file.FileCollection +import org.gradle.api.plugins.JavaPlugin +import org.gradle.api.plugins.jvm.JvmTestSuite +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.testing.Test +import org.gradle.language.base.plugins.LifecycleBasePlugin +import org.gradle.testing.base.TestingExtension + +private const val AGENT_FIXTURE = "agentFixture" +private const val OTEL_AGENT_JAR = "otelAgentJar" +private const val SUITE = "integrationTestOtelAgent" +private const val WITHOUT_AUTO_INIT = "${SUITE}WithoutAutoInit" + +/** + * Runs a Spring Boot module's OpenTelemetry agent tests against an app in a child JVM, with the + * built agent attached. + * + * The agent must not run in the test's JVM, because it would instrument the test's own HTTP client + * and server. So the app lives in the `agentFixture` source set and runs on its own classpath, like + * a real app. The `integrationTestOtelAgent` suite's task runs the tests with the agent + * initializing Sentry, and `integrationTestOtelAgentWithoutAutoInit` runs them with the Spring Boot + * starter doing it. Each test JVM starts one app, so the second mode needs a task of its own, which + * Gradle runs in parallel with the suite's. + * + * The module adds the app's and the suite's dependencies, which differ per Spring Boot line. + */ +class OtelAgentTestsPlugin : Plugin { + override fun apply(project: Project) { + project.pluginManager.apply(JavaPlugin::class.java) + val sourceSets = project.extensions.getByType(SourceSetContainer::class.java) + val agentFixture = sourceSets.create(AGENT_FIXTURE) + + // A plain project dependency on the agent module resolves to its unshadowed jar, which holds + // only AgentMarker. The agent itself is the shadowed jar, which already bundles its + // dependencies. + project.configurations.create(OTEL_AGENT_JAR) { + isCanBeResolved = true + isCanBeConsumed = false + isTransitive = false + attributes { + attribute( + Usage.USAGE_ATTRIBUTE, + project.objects.named(Usage::class.java, Usage.JAVA_RUNTIME), + ) + attribute( + Bundling.BUNDLING_ATTRIBUTE, + project.objects.named(Bundling::class.java, Bundling.SHADOWED), + ) + } + } + project.dependencies.add( + OTEL_AGENT_JAR, + project.dependencies.project( + mapOf("path" to ":sentry-opentelemetry:sentry-opentelemetry-agent") + ), + ) + + val testing = project.extensions.getByType(TestingExtension::class.java) + testing.suites.register(SUITE, JvmTestSuite::class.java) { + useJUnit() + targets.all { + testTask.configure { runAgentFixture(autoInit = true, agentFixture.runtimeClasspath) } + } + } + + project.tasks.register(WITHOUT_AUTO_INIT, Test::class.java) { + description = "Runs the OpenTelemetry agent tests with SENTRY_AUTO_INIT=false." + group = LifecycleBasePlugin.VERIFICATION_GROUP + val sources = sourceSets.getByName(SUITE) + testClassesDirs = sources.output.classesDirs + classpath = sources.runtimeClasspath + useJUnit() + runAgentFixture(autoInit = false, agentFixture.runtimeClasspath) + } + } +} + +/** Registers both OpenTelemetry agent test tasks on each Spring Boot version of the matrix. */ +fun SpringBootMatrixExtension.otelAgentTests() { + test(SUITE) { version -> + runAgentFixture(autoInit = true, runtimeClasspath(AGENT_FIXTURE, version)) + } + test(SUITE, name = WITHOUT_AUTO_INIT) { version -> + runAgentFixture(autoInit = false, runtimeClasspath(AGENT_FIXTURE, version)) + } +} + +/** + * Has the tests start the app on [appClasspath] 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. + */ +private fun Test.runAgentFixture(autoInit: Boolean, appClasspath: FileCollection) { + jvmArgumentProviders.add( + project.objects.newInstance(ChildJvmTestArguments::class.java).apply { + agentJar.from(project.configurations.getByName(OTEL_AGENT_JAR)) + this.appClasspath.from(appClasspath) + } + ) + systemProperty("sentry.test.agentAutoInit", autoInit) +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-agent/build.gradle.kts b/sentry-opentelemetry/sentry-opentelemetry-agent/build.gradle.kts index 81e690fbe0..67c9feea2d 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-agent/build.gradle.kts +++ b/sentry-opentelemetry/sentry-opentelemetry-agent/build.gradle.kts @@ -108,7 +108,9 @@ fun isolateClasses(jars: Iterable): CopySpec = copySpec { } } -// Don't publish non-shadowed jar (shadowJar is in shadowRuntimeElements) +// Only the agent, the shadow jar in shadowRuntimeElements, is published. The plain jar in +// runtimeElements holds just AgentMarker. A project dependency on this module resolves to it, so +// the Spring Boot modules' unit tests get AgentMarker on their classpath without the agent. with(components["java"] as AdhocComponentWithVariants) { configurations.forEach { withVariantsFromConfiguration(configurations["apiElements"]) { skip() } diff --git a/sentry-spring-boot-4/build.gradle.kts b/sentry-spring-boot-4/build.gradle.kts index f1c3d0cc72..f848860526 100644 --- a/sentry-spring-boot-4/build.gradle.kts +++ b/sentry-spring-boot-4/build.gradle.kts @@ -1,4 +1,4 @@ -import io.sentry.gradle.ChildJvmTestArguments +import io.sentry.gradle.otelAgentTests import net.ltgt.gradle.errorprone.errorprone import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.springframework.boot.gradle.plugin.SpringBootPlugin @@ -7,6 +7,7 @@ plugins { `java-library` id("io.sentry.javadoc") id("io.sentry.spring-boot-matrix") + id("io.sentry.otel-agent-tests") alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.spring) alias(libs.plugins.errorprone) @@ -163,22 +164,7 @@ kotlin { } } -// 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") - -// The built agent jar, rather than the plain jar the unit tests compile against. -val otelAgentJar = - configurations.create("otelAgentJar") { - isCanBeResolved = true - isCanBeConsumed = false - isTransitive = false - attributes { - attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME)) - attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.SHADOWED)) - } - } - +// The app the OpenTelemetry agent tests run in a child JVM. dependencies { "agentFixtureImplementation"(platform(SpringBootPlugin.BOM_COORDINATES)) "agentFixtureImplementation"(projects.sentrySpringBoot4Starter) @@ -201,8 +187,6 @@ dependencies { "agentFixtureImplementation"(libs.springboot4.starter.webclient) "agentFixtureImplementation"(libs.springboot4.starter.webflux) "agentFixtureRuntimeOnly"(libs.hsqldb) - - otelAgentJar(projects.sentryOpentelemetry.sentryOpentelemetryAgent) } // Swaps Logback for Log4j2 where a classpath has both starters, which only @@ -337,9 +321,8 @@ testing { } } - // Runs agentFixture's app with the agent initializing Sentry. - register("integrationTestOtelAgent") { - useJUnit() + // The OpenTelemetry agent tests, against agentFixture's app. + named("integrationTestOtelAgent") { dependencies { implementation(platform(SpringBootPlugin.BOM_COORDINATES)) implementation(projects.sentryTestSupport) @@ -351,7 +334,6 @@ testing { implementation(libs.springboot4.starter.test) implementation(libs.spring.kafka4.test) } - targets.all { testTask.configure { runAgentFixture(autoInit = true) } } } // The servlet app with the OpenTelemetry Spring Boot starter, set up by @@ -389,37 +371,6 @@ 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. [appClasspath] is the app's classpath for - * another Spring Boot version. - */ -fun Test.runAgentFixture( - autoInit: Boolean, - appClasspath: FileCollection = agentFixture.runtimeClasspath, -) { - jvmArgumentProviders.add( - objects.newInstance().apply { - agentJar.from(otelAgentJar) - this.appClasspath.from(appClasspath) - } - ) - systemProperty("sentry.test.agentAutoInit", autoInit) -} - -// The same tests with the Spring Boot starter initializing Sentry. Each test JVM starts one app, so -// this mode needs a task of its own, which Gradle runs in parallel with the suite's. -tasks.register("integrationTestOtelAgentWithoutAutoInit") { - description = "Runs the OpenTelemetry agent tests with SENTRY_AUTO_INIT=false." - group = LifecycleBasePlugin.VERIFICATION_GROUP - val sources = sourceSets["integrationTestOtelAgent"] - testClassesDirs = sources.output.classesDirs - classpath = sources.runtimeClasspath - useJUnit() - runAgentFixture(autoInit = false) -} - // The Spring Boot Gradle plugin compiles apps with parameter names, which Spring reads, for example // to resolve `#id` in a cache key. listOf( @@ -441,10 +392,5 @@ springBootMatrix { 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)) - } + otelAgentTests() } diff --git a/sentry-spring-boot-jakarta/build.gradle.kts b/sentry-spring-boot-jakarta/build.gradle.kts index cfee8ecb5c..55fd0d6af1 100644 --- a/sentry-spring-boot-jakarta/build.gradle.kts +++ b/sentry-spring-boot-jakarta/build.gradle.kts @@ -1,4 +1,4 @@ -import io.sentry.gradle.ChildJvmTestArguments +import io.sentry.gradle.otelAgentTests import net.ltgt.gradle.errorprone.errorprone import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.springframework.boot.gradle.plugin.SpringBootPlugin @@ -7,6 +7,7 @@ plugins { `java-library` id("io.sentry.javadoc") id("io.sentry.spring-boot-matrix") + id("io.sentry.otel-agent-tests") alias(libs.plugins.kotlin.jvm) alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -146,22 +147,7 @@ tasks.jar { } } -// 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") - -// The built agent jar, rather than the plain jar the unit tests compile against. -val otelAgentJar = - configurations.create("otelAgentJar") { - isCanBeResolved = true - isCanBeConsumed = false - isTransitive = false - attributes { - attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME)) - attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.SHADOWED)) - } - } - +// The app the OpenTelemetry agent tests run in a child JVM. dependencies { "agentFixtureImplementation"(platform(SpringBootPlugin.BOM_COORDINATES)) "agentFixtureImplementation"(projects.sentrySpringBootStarterJakarta) @@ -182,8 +168,6 @@ dependencies { "agentFixtureImplementation"(libs.springboot3.starter.webflux) "agentFixtureImplementation"(libs.spring.kafka3) "agentFixtureRuntimeOnly"(libs.hsqldb) - - otelAgentJar(projects.sentryOpentelemetry.sentryOpentelemetryAgent) } // Swaps Logback for Log4j2 where a classpath has both starters, which only @@ -311,9 +295,8 @@ testing { } } - // Runs agentFixture's app with the agent initializing Sentry. - register("integrationTestOtelAgent") { - useJUnit() + // The OpenTelemetry agent tests, against agentFixture's app. + named("integrationTestOtelAgent") { dependencies { implementation(platform(SpringBootPlugin.BOM_COORDINATES)) implementation(projects.sentryTestSupport) @@ -324,42 +307,10 @@ testing { implementation(libs.springboot3.starter.web) implementation(libs.spring.kafka3.test) } - targets.all { testTask.configure { runAgentFixture(autoInit = true) } } } } } -/** - * 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. [appClasspath] is the app's classpath for - * another Spring Boot version. - */ -fun Test.runAgentFixture( - autoInit: Boolean, - appClasspath: FileCollection = agentFixture.runtimeClasspath, -) { - jvmArgumentProviders.add( - objects.newInstance().apply { - agentJar.from(otelAgentJar) - this.appClasspath.from(appClasspath) - } - ) - systemProperty("sentry.test.agentAutoInit", autoInit) -} - -// The same tests with the Spring Boot starter initializing Sentry. Each test JVM starts one app, so -// this mode needs a task of its own, which Gradle runs in parallel with the suite's. -tasks.register("integrationTestOtelAgentWithoutAutoInit") { - description = "Runs the OpenTelemetry agent tests with SENTRY_AUTO_INIT=false." - group = LifecycleBasePlugin.VERIFICATION_GROUP - val sources = sourceSets["integrationTestOtelAgent"] - testClassesDirs = sources.output.classesDirs - classpath = sources.runtimeClasspath - useJUnit() - runAgentFixture(autoInit = false) -} - // The Spring Boot Gradle plugin compiles apps with parameter names, which Spring reads, for example // to resolve `#id` in a cache key. listOf( @@ -379,10 +330,5 @@ springBootMatrix { 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)) - } + otelAgentTests() } diff --git a/sentry-spring-boot/build.gradle.kts b/sentry-spring-boot/build.gradle.kts index 18c6097823..09092b9d7c 100644 --- a/sentry-spring-boot/build.gradle.kts +++ b/sentry-spring-boot/build.gradle.kts @@ -1,4 +1,4 @@ -import io.sentry.gradle.ChildJvmTestArguments +import io.sentry.gradle.otelAgentTests import net.ltgt.gradle.errorprone.errorprone import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile @@ -7,6 +7,7 @@ plugins { `java-library` id("io.sentry.javadoc") id("io.sentry.spring-boot-matrix") + id("io.sentry.otel-agent-tests") alias(libs.plugins.kotlin.jvm) alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -141,22 +142,7 @@ tasks.jar { } } -// 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") - -// The built agent jar, rather than the plain jar the unit tests compile against. -val otelAgentJar = - configurations.create("otelAgentJar") { - isCanBeResolved = true - isCanBeConsumed = false - isTransitive = false - attributes { - attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME)) - attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.SHADOWED)) - } - } - +// The app the OpenTelemetry agent tests run in a child JVM. dependencies { "agentFixtureImplementation"(platform(libs.springboot2.bom)) "agentFixtureImplementation"(projects.sentrySpringBootStarter) @@ -173,8 +159,6 @@ dependencies { "agentFixtureImplementation"(libs.springboot.starter.webflux) "agentFixtureImplementation"(libs.spring.kafka2) "agentFixtureRuntimeOnly"(libs.hsqldb) - - otelAgentJar(projects.sentryOpentelemetry.sentryOpentelemetryAgent) } testing { @@ -256,9 +240,8 @@ testing { } } - // Runs agentFixture's app with the agent initializing Sentry. - register("integrationTestOtelAgent") { - useJUnit() + // The OpenTelemetry agent tests, against agentFixture's app. + named("integrationTestOtelAgent") { dependencies { implementation(platform(libs.springboot2.bom)) implementation(projects.sentryTestSupport) @@ -269,42 +252,10 @@ testing { implementation(libs.springboot.starter.web) implementation(libs.spring.kafka2.test) } - targets.all { testTask.configure { runAgentFixture(autoInit = true) } } } } } -/** - * 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. [appClasspath] is the app's classpath for - * another Spring Boot version. - */ -fun Test.runAgentFixture( - autoInit: Boolean, - appClasspath: FileCollection = agentFixture.runtimeClasspath, -) { - jvmArgumentProviders.add( - objects.newInstance().apply { - agentJar.from(otelAgentJar) - this.appClasspath.from(appClasspath) - } - ) - systemProperty("sentry.test.agentAutoInit", autoInit) -} - -// The same tests with the Spring Boot starter initializing Sentry. Each test JVM starts one app, so -// this mode needs a task of its own, which Gradle runs in parallel with the suite's. -tasks.register("integrationTestOtelAgentWithoutAutoInit") { - description = "Runs the OpenTelemetry agent tests with SENTRY_AUTO_INIT=false." - group = LifecycleBasePlugin.VERIFICATION_GROUP - val sources = sourceSets["integrationTestOtelAgent"] - testClassesDirs = sources.output.classesDirs - classpath = sources.runtimeClasspath - useJUnit() - runAgentFixture(autoInit = false) -} - // The Spring Boot Gradle plugin compiles apps with parameter names, which Spring reads, for example // to resolve `#id` in a cache key. listOf( @@ -324,10 +275,5 @@ springBootMatrix { 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)) - } + otelAgentTests() }