diff --git a/buildSrc/README.md b/buildSrc/README.md index ada387cf71..cc8c3e58f3 100644 --- a/buildSrc/README.md +++ b/buildSrc/README.md @@ -33,3 +33,40 @@ current working version with. You can generate the reports for all modules or a ``` The reports are located under `build/reports/api-diff/$OLDVERSION_to_$NEWVERSION/`. + + +### RuntimeHints Java Agent + +The `spring-core-test` project module contributes the `RuntimeHintsAgent` Java agent. + +The `RuntimeHintsAgentPlugin` Gradle plugin creates a dedicated `"runtimeHintsTest"` test task for each project. +This task will detect and execute [tests tagged](https://junit.org/junit5/docs/current/user-guide/#running-tests-build-gradle) +with the `"RuntimeHintsTests"` [JUnit tag](https://junit.org/junit5/docs/current/user-guide/#running-tests-tags). +In the Spring Framework test suite, those are usually annotated with the `@EnabledIfRuntimeHintsAgent` annotation. + +By default, the agent will instrument all classes located in the `"org.springframework"` package, as they are loaded. +The `RuntimeHintsAgentExtension` allows to customize this using a DSL: + +```groovy +// this applies the `RuntimeHintsAgentPlugin` to the project +plugins { + id 'org.springframework.build.runtimehints-agent' +} + +// You can configure the agent to include and exclude packages from the instrumentation process. +runtimeHintsAgent { + includedPackages = ["org.springframework", "io.spring"] + excludedPackages = ["org.example"] +} + +dependencies { + // to use the test infrastructure, the project should also depend on the "spring-core-test" module + testImplementation(project(":spring-core-test")) +} +``` + +With this configuration, `./gradlew runtimeHintsTest` will run all tests instrumented by this java agent. +The global `./gradlew check` task depends on `runtimeHintsTest`. + +NOTE: the "spring-core-test" module doesn't shade "spring-core" by design, so the agent should never instrument +code that doesn't have "spring-core" on its classpath. \ No newline at end of file diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 6f5712565d..1d4b7700cb 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -25,5 +25,9 @@ gradlePlugin { id = "org.springframework.build.optional-dependencies" implementationClass = "org.springframework.build.optional.OptionalDependenciesPlugin" } + runtimeHintsAgentPlugin { + id = "org.springframework.build.runtimehints-agent" + implementationClass = "org.springframework.build.hint.RuntimeHintsAgentPlugin" + } } } diff --git a/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentExtension.java b/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentExtension.java new file mode 100644 index 0000000000..45d7aad32c --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentExtension.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.build.hint; + +import java.util.Collections; + +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.SetProperty; + +/** + * Entry point to the DSL extension for the {@link RuntimeHintsAgentPlugin} Gradle plugin. + * @author Brian Clozel + */ +public class RuntimeHintsAgentExtension { + + private final SetProperty includedPackages; + + private final SetProperty excludedPackages; + + public RuntimeHintsAgentExtension(ObjectFactory objectFactory) { + this.includedPackages = objectFactory.setProperty(String.class).convention(Collections.singleton("org.springframework")); + this.excludedPackages = objectFactory.setProperty(String.class).convention(Collections.emptySet()); + } + + public SetProperty getIncludedPackages() { + return this.includedPackages; + } + + public SetProperty getExcludedPackages() { + return this.excludedPackages; + } + + String asJavaAgentArgument() { + StringBuilder builder = new StringBuilder(); + this.includedPackages.get().forEach(packageName -> builder.append('+').append(packageName).append(',')); + this.excludedPackages.get().forEach(packageName -> builder.append('-').append(packageName).append(',')); + return builder.toString(); + } +} diff --git a/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentPlugin.java b/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentPlugin.java new file mode 100644 index 0000000000..f8f4a513f5 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentPlugin.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.build.hint; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.tasks.bundling.Jar; +import org.gradle.api.tasks.testing.Test; + +/** + * {@link Plugin} that configures the {@code RuntimeHints} Java agent to test tasks. + * + * @author Brian Clozel + */ +public class RuntimeHintsAgentPlugin implements Plugin { + + public static final String RUNTIMEHINTS_TEST_TASK = "runtimeHintsTest"; + private static final String EXTENSION_NAME = "runtimeHintsAgent"; + + + @Override + public void apply(Project project) { + + project.getPlugins().withType(JavaPlugin.class, javaPlugin -> { + RuntimeHintsAgentExtension agentExtension = project.getExtensions().create(EXTENSION_NAME, + RuntimeHintsAgentExtension.class, project.getObjects()); + Test agentTest = project.getTasks().create(RUNTIMEHINTS_TEST_TASK, Test.class, test -> { + test.useJUnitPlatform(options -> { + options.includeTags("RuntimeHintsTests"); + }); + test.include("**/*Tests.class", "**/*Test.class"); + test.systemProperty("java.awt.headless", "true"); + }); + project.afterEvaluate(p -> { + Jar jar = project.getRootProject().project("spring-core-test").getTasks().withType(Jar.class).named("jar").get(); + agentTest.jvmArgs("-javaagent:" + jar.getArchiveFile().get().getAsFile() + "=" + agentExtension.asJavaAgentArgument()); + }); + project.getTasks().getByName("check", task -> task.dependsOn(agentTest)); + }); + } +}