Add bootTestRun to run app using test source set output and classpath
Closes gh-35248
This commit is contained in:
		
							parent
							
								
									9cadc6ffbb
								
							
						
					
					
						commit
						19d7973776
					
				|  | @ -14,12 +14,13 @@ When Gradle's {java-plugin}[`java` plugin] is applied to a project, the Spring B | ||||||
| 2. Configures the `assemble` task to depend on the `bootJar` task. | 2. Configures the `assemble` task to depend on the `bootJar` task. | ||||||
| 3. Configures the `jar` task to use `plain` as the convention for its archive classifier. | 3. Configures the `jar` task to use `plain` as the convention for its archive classifier. | ||||||
| 4. Creates a {boot-build-image-javadoc}[`BootBuildImage`] task named `bootBuildImage` that will create a OCI image using a https://buildpacks.io[buildpack]. | 4. Creates a {boot-build-image-javadoc}[`BootBuildImage`] task named `bootBuildImage` that will create a OCI image using a https://buildpacks.io[buildpack]. | ||||||
| 5. Creates a {boot-run-javadoc}[`BootRun`] task named `bootRun` that can be used to run your application. | 5. Creates a {boot-run-javadoc}[`BootRun`] task named `bootRun` that can be used to run your application using the `main` source set to find its main method and provide its runtime classpath. | ||||||
| 6. Creates a configuration named `bootArchives` that contains the artifact produced by the `bootJar` task. | 6. Creates a {boot-run-javadoc}['BootRun`] task named `bootTestRun` that can be used to run your application using the `test` source set to find its main method and provide its runtime classpath. | ||||||
| 7. Creates a configuration named `developmentOnly` for dependencies that are only required at development time, such as Spring Boot's Devtools, and should not be packaged in executable jars and wars. | 7. Creates a configuration named `bootArchives` that contains the artifact produced by the `bootJar` task. | ||||||
| 8. Creates a configuration named `productionRuntimeClasspath`. It is equivalent to `runtimeClasspath` minus any dependencies that only appear in the `developmentOnly` configuration. | 8. Creates a configuration named `developmentOnly` for dependencies that are only required at development time, such as Spring Boot's Devtools, and should not be packaged in executable jars and wars. | ||||||
| 9. Configures any `JavaCompile` tasks with no configured encoding to use `UTF-8`. | 9. Creates a configuration named `productionRuntimeClasspath`. It is equivalent to `runtimeClasspath` minus any dependencies that only appear in the `developmentOnly` configuration. | ||||||
| 10. Configures any `JavaCompile` tasks to use the `-parameters` compiler argument. | 10. Configures any `JavaCompile` tasks with no configured encoding to use `UTF-8`. | ||||||
|  | 11. Configures any `JavaCompile` tasks to use the `-parameters` compiler argument. | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -59,7 +60,7 @@ When Gradle's {application-plugin}[`application` plugin] is applied to a project | ||||||
|    The task is configured to use the `applicationDefaultJvmArgs` property as a convention for its `defaultJvmOpts` property. |    The task is configured to use the `applicationDefaultJvmArgs` property as a convention for its `defaultJvmOpts` property. | ||||||
| 2. Creates a new distribution named `boot` and configures it to contain the artifact in the `bootArchives` configuration in its `lib` directory and the start scripts in its `bin` directory. | 2. Creates a new distribution named `boot` and configures it to contain the artifact in the `bootArchives` configuration in its `lib` directory and the start scripts in its `bin` directory. | ||||||
| 3. Configures the `bootRun` task to use the `mainClassName` property as a convention for its `main` property. | 3. Configures the `bootRun` task to use the `mainClassName` property as a convention for its `main` property. | ||||||
| 4. Configures the `bootRun` task to use the `applicationDefaultJvmArgs` property as a convention for its `jvmArgs` property. | 4. Configures the `bootRun` and `bootTestRun` tasks to use the `applicationDefaultJvmArgs` property as a convention for their `jvmArgs` property. | ||||||
| 5. Configures the `bootJar` task to use the `mainClassName` property as a convention for the `Start-Class` entry in its manifest. | 5. Configures the `bootJar` task to use the `mainClassName` property as a convention for the `Start-Class` entry in its manifest. | ||||||
| 6. Configures the `bootWar` task to use the `mainClassName` property as a convention for the `Start-Class` entry in its manifest. | 6. Configures the `bootWar` task to use the `mainClassName` property as a convention for the `Start-Class` entry in its manifest. | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -141,3 +141,12 @@ include::../gradle/running/boot-run-source-resources.gradle.kts[tags=source-reso | ||||||
| ---- | ---- | ||||||
| 
 | 
 | ||||||
| This makes them reloadable in the live application which can be helpful at development time. | This makes them reloadable in the live application which can be helpful at development time. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | [[running-your-application.using-a-test-main-class]] | ||||||
|  | == Using a Test Main Class | ||||||
|  | In addition to `bootRun` a `bootTestRun` task is also registered. | ||||||
|  | Like `bootRun`, `bootTestRun` is an instance of `BootRun` but it's configured to use a main class found in the output of the test source set rather than the main source set. | ||||||
|  | It also uses the test source set's runtime classpath rather than the main source set's runtime classpath. | ||||||
|  | As `bootTestRun` is an instance of `BootRun`, all of the configuration options described above for `bootRun` can also be used with `bootTestRun`. | ||||||
|  |  | ||||||
|  | @ -85,6 +85,8 @@ final class JavaPluginAction implements PluginApplicationAction { | ||||||
| 		configureBootBuildImageTask(project, bootJar); | 		configureBootBuildImageTask(project, bootJar); | ||||||
| 		configureArtifactPublication(bootJar); | 		configureArtifactPublication(bootJar); | ||||||
| 		configureBootRunTask(project, resolveMainClassName); | 		configureBootRunTask(project, resolveMainClassName); | ||||||
|  | 		TaskProvider<ResolveMainClassName> resolveMainTestClassName = configureResolveMainTestClassNameTask(project); | ||||||
|  | 		configureBootTestRunTask(project, resolveMainTestClassName); | ||||||
| 		project.afterEvaluate(this::configureUtf8Encoding); | 		project.afterEvaluate(this::configureUtf8Encoding); | ||||||
| 		configureParametersCompilerArg(project); | 		configureParametersCompilerArg(project); | ||||||
| 		configureAdditionalMetadataLocations(project); | 		configureAdditionalMetadataLocations(project); | ||||||
|  | @ -128,6 +130,23 @@ final class JavaPluginAction implements PluginApplicationAction { | ||||||
| 					}); | 					}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	private TaskProvider<ResolveMainClassName> configureResolveMainTestClassNameTask(Project project) { | ||||||
|  | 		return project.getTasks() | ||||||
|  | 			.register(SpringBootPlugin.RESOLVE_TEST_MAIN_CLASS_NAME_TASK_NAME, ResolveMainClassName.class, | ||||||
|  | 					(resolveMainClassName) -> { | ||||||
|  | 						resolveMainClassName.setDescription("Resolves the name of the application's test main class."); | ||||||
|  | 						resolveMainClassName.setGroup(BasePlugin.BUILD_GROUP); | ||||||
|  | 						Callable<FileCollection> classpath = () -> { | ||||||
|  | 							SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); | ||||||
|  | 							return project.files(sourceSets.getByName(SourceSet.TEST_SOURCE_SET_NAME).getOutput(), | ||||||
|  | 									sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME).getOutput()); | ||||||
|  | 						}; | ||||||
|  | 						resolveMainClassName.setClasspath(classpath); | ||||||
|  | 						resolveMainClassName.getOutputFile() | ||||||
|  | 							.set(project.getLayout().getBuildDirectory().file("resolvedMainTestClassName")); | ||||||
|  | 					}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	private static String getJavaApplicationMainClass(ExtensionContainer extensions) { | 	private static String getJavaApplicationMainClass(ExtensionContainer extensions) { | ||||||
| 		JavaApplication javaApplication = extensions.findByType(JavaApplication.class); | 		JavaApplication javaApplication = extensions.findByType(JavaApplication.class); | ||||||
| 		if (javaApplication == null) { | 		if (javaApplication == null) { | ||||||
|  | @ -194,6 +213,26 @@ final class JavaPluginAction implements PluginApplicationAction { | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	private void configureBootTestRunTask(Project project, TaskProvider<ResolveMainClassName> resolveMainClassName) { | ||||||
|  | 		Callable<FileCollection> classpath = () -> javaPluginExtension(project).getSourceSets() | ||||||
|  | 			.findByName(SourceSet.TEST_SOURCE_SET_NAME) | ||||||
|  | 			.getRuntimeClasspath() | ||||||
|  | 			.filter(new JarTypeFileSpec()); | ||||||
|  | 		project.getTasks().register("bootTestRun", BootRun.class, (run) -> { | ||||||
|  | 			run.setDescription("Runs this project as a Spring Boot application using the test runtime classpath."); | ||||||
|  | 			run.setGroup(ApplicationPlugin.APPLICATION_GROUP); | ||||||
|  | 			run.classpath(classpath); | ||||||
|  | 			run.getConventionMapping().map("jvmArgs", () -> { | ||||||
|  | 				if (project.hasProperty("applicationDefaultJvmArgs")) { | ||||||
|  | 					return project.property("applicationDefaultJvmArgs"); | ||||||
|  | 				} | ||||||
|  | 				return Collections.emptyList(); | ||||||
|  | 			}); | ||||||
|  | 			run.getMainClass().convention(resolveMainClassName.flatMap(ResolveMainClassName::readMainClassName)); | ||||||
|  | 			configureToolchainConvention(project, run); | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	private void configureToolchainConvention(Project project, BootRun run) { | 	private void configureToolchainConvention(Project project, BootRun run) { | ||||||
| 		JavaToolchainSpec toolchain = project.getExtensions().getByType(JavaPluginExtension.class).getToolchain(); | 		JavaToolchainSpec toolchain = project.getExtensions().getByType(JavaPluginExtension.class).getToolchain(); | ||||||
| 		JavaToolchainService toolchainService = project.getExtensions().getByType(JavaToolchainService.class); | 		JavaToolchainService toolchainService = project.getExtensions().getByType(JavaToolchainService.class); | ||||||
|  |  | ||||||
|  | @ -22,6 +22,7 @@ import java.nio.file.Files; | ||||||
| import java.nio.file.Path; | import java.nio.file.Path; | ||||||
| import java.nio.file.StandardOpenOption; | import java.nio.file.StandardOpenOption; | ||||||
| import java.util.Objects; | import java.util.Objects; | ||||||
|  | import java.util.stream.Collectors; | ||||||
| 
 | 
 | ||||||
| import org.gradle.api.DefaultTask; | import org.gradle.api.DefaultTask; | ||||||
| import org.gradle.api.InvalidUserDataException; | import org.gradle.api.InvalidUserDataException; | ||||||
|  | @ -149,16 +150,29 @@ public class ResolveMainClassName extends DefaultTask { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	Provider<String> readMainClassName() { | 	Provider<String> readMainClassName() { | ||||||
| 		return this.outputFile.map(new ClassNameReader()); | 		String classpath = getClasspath().filter(File::isDirectory) | ||||||
|  | 			.getFiles() | ||||||
|  | 			.stream() | ||||||
|  | 			.map((directory) -> getProject().getProjectDir().toPath().relativize(directory.toPath())) | ||||||
|  | 			.map(Path::toString) | ||||||
|  | 			.collect(Collectors.joining(",")); | ||||||
|  | 		return this.outputFile.map(new ClassNameReader(classpath)); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	private static final class ClassNameReader implements Transformer<String, RegularFile> { | 	private static final class ClassNameReader implements Transformer<String, RegularFile> { | ||||||
| 
 | 
 | ||||||
|  | 		private final String classpath; | ||||||
|  | 
 | ||||||
|  | 		private ClassNameReader(String classpath) { | ||||||
|  | 			this.classpath = classpath; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		@Override | 		@Override | ||||||
| 		public String transform(RegularFile file) { | 		public String transform(RegularFile file) { | ||||||
| 			if (file.getAsFile().length() == 0) { | 			if (file.getAsFile().length() == 0) { | ||||||
| 				throw new InvalidUserDataException( | 				throw new InvalidUserDataException( | ||||||
| 						"Main class name has not been configured and it could not be resolved"); | 						"Main class name has not been configured and it could not be resolved from classpath " | ||||||
|  | 								+ this.classpath); | ||||||
| 			} | 			} | ||||||
| 			Path output = file.getAsFile().toPath(); | 			Path output = file.getAsFile().toPath(); | ||||||
| 			try { | 			try { | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
|  * Copyright 2012-2022 the original author or authors. |  * Copyright 2012-2023 the original author or authors. | ||||||
|  * |  * | ||||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  * you may not use this file except in compliance with the License. |  * you may not use this file except in compliance with the License. | ||||||
|  | @ -82,11 +82,20 @@ public class SpringBootPlugin implements Plugin<Project> { | ||||||
| 	public static final String PRODUCTION_RUNTIME_CLASSPATH_CONFIGURATION_NAME = "productionRuntimeClasspath"; | 	public static final String PRODUCTION_RUNTIME_CLASSPATH_CONFIGURATION_NAME = "productionRuntimeClasspath"; | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
| 	 * The name of the {@link ResolveMainClassName} task. | 	 * The name of the {@link ResolveMainClassName} task used to resolve a main class from | ||||||
|  | 	 * the output of the {@code main} source set. | ||||||
| 	 * @since 3.0.0 | 	 * @since 3.0.0 | ||||||
| 	 */ | 	 */ | ||||||
| 	public static final String RESOLVE_MAIN_CLASS_NAME_TASK_NAME = "resolveMainClassName"; | 	public static final String RESOLVE_MAIN_CLASS_NAME_TASK_NAME = "resolveMainClassName"; | ||||||
| 
 | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * The name of the {@link ResolveMainClassName} task used to resolve a main class from | ||||||
|  | 	 * the output of the {@code test} source set then, if needed, the output of the | ||||||
|  | 	 * {@code main} source set. | ||||||
|  | 	 * @since 3.1.0 | ||||||
|  | 	 */ | ||||||
|  | 	public static final String RESOLVE_TEST_MAIN_CLASS_NAME_TASK_NAME = "resolveTestMainClassName"; | ||||||
|  | 
 | ||||||
| 	/** | 	/** | ||||||
| 	 * The coordinates {@code (group:name:version)} of the | 	 * The coordinates {@code (group:name:version)} of the | ||||||
| 	 * {@code spring-boot-dependencies} bom. | 	 * {@code spring-boot-dependencies} bom. | ||||||
|  |  | ||||||
|  | @ -0,0 +1,41 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright 2012-2023 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 com.example.boottestrun.classpath; | ||||||
|  | 
 | ||||||
|  | import java.io.File; | ||||||
|  | import java.lang.management.ManagementFactory; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Application used for testing {@code bootTestRun}'s classpath handling. | ||||||
|  |  * | ||||||
|  |  * @author Andy Wilkinson | ||||||
|  |  */ | ||||||
|  | public class BootTestRunClasspathApplication { | ||||||
|  | 
 | ||||||
|  | 	protected BootTestRunClasspathApplication() { | ||||||
|  | 
 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	public static void main(String[] args) { | ||||||
|  | 		System.out.println("Main class name = " + BootTestRunClasspathApplication.class.getName()); | ||||||
|  | 		int i = 1; | ||||||
|  | 		for (String entry : ManagementFactory.getRuntimeMXBean().getClassPath().split(File.pathSeparator)) { | ||||||
|  | 			System.out.println(i++ + ". " + entry); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,39 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright 2012-2023 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 com.example.boottestrun.jvmargs; | ||||||
|  | 
 | ||||||
|  | import java.lang.management.ManagementFactory; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Application used for testing {@code bootTestRun}'s JVM argument handling. | ||||||
|  |  * | ||||||
|  |  * @author Andy Wilkinson | ||||||
|  |  */ | ||||||
|  | public class BootTestRunJvmArgsApplication { | ||||||
|  | 
 | ||||||
|  | 	protected BootTestRunJvmArgsApplication() { | ||||||
|  | 
 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	public static void main(String[] args) { | ||||||
|  | 		int i = 1; | ||||||
|  | 		for (String entry : ManagementFactory.getRuntimeMXBean().getInputArguments()) { | ||||||
|  | 			System.out.println(i++ + ". " + entry); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,26 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright 2012-2023 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 com.example.boottestrun.nomain; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Application used for testing {@code bootTestRun}'s handling of no test main method | ||||||
|  |  * | ||||||
|  |  * @author Andy Wilkinson | ||||||
|  |  */ | ||||||
|  | public class BootTestRunNoMain { | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -60,11 +60,21 @@ class JavaPluginActionIntegrationTests { | ||||||
| 		assertThat(this.gradleBuild.build("tasks").getOutput()).doesNotContain("bootRun"); | 		assertThat(this.gradleBuild.build("tasks").getOutput()).doesNotContain("bootRun"); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	@TestTemplate | ||||||
|  | 	void noBootTestRunTaskWithoutJavaPluginApplied() { | ||||||
|  | 		assertThat(this.gradleBuild.build("tasks").getOutput()).doesNotContain("bootTestRun"); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	@TestTemplate | 	@TestTemplate | ||||||
| 	void applyingJavaPluginCreatesBootRunTask() { | 	void applyingJavaPluginCreatesBootRunTask() { | ||||||
| 		assertThat(this.gradleBuild.build("tasks").getOutput()).contains("bootRun"); | 		assertThat(this.gradleBuild.build("tasks").getOutput()).contains("bootRun"); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	@TestTemplate | ||||||
|  | 	void applyingJavaPluginCreatesBootTestRunTask() { | ||||||
|  | 		assertThat(this.gradleBuild.build("tasks").getOutput()).contains("bootTestRun"); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	@TestTemplate | 	@TestTemplate | ||||||
| 	void javaCompileTasksUseUtf8Encoding() { | 	void javaCompileTasksUseUtf8Encoding() { | ||||||
| 		assertThat(this.gradleBuild.build("build").getOutput()).contains("compileJava = UTF-8") | 		assertThat(this.gradleBuild.build("build").getOutput()).contains("compileJava = UTF-8") | ||||||
|  |  | ||||||
|  | @ -0,0 +1,156 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright 2012-2023 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.boot.gradle.tasks.run; | ||||||
|  | 
 | ||||||
|  | import java.io.File; | ||||||
|  | import java.io.FileOutputStream; | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.util.function.Consumer; | ||||||
|  | import java.util.jar.Attributes; | ||||||
|  | import java.util.jar.JarOutputStream; | ||||||
|  | import java.util.jar.Manifest; | ||||||
|  | 
 | ||||||
|  | import org.assertj.core.api.Assumptions; | ||||||
|  | import org.gradle.testkit.runner.BuildResult; | ||||||
|  | import org.gradle.testkit.runner.TaskOutcome; | ||||||
|  | import org.gradle.util.GradleVersion; | ||||||
|  | import org.junit.jupiter.api.TestTemplate; | ||||||
|  | 
 | ||||||
|  | import org.springframework.boot.gradle.junit.GradleCompatibility; | ||||||
|  | import org.springframework.boot.testsupport.gradle.testkit.GradleBuild; | ||||||
|  | import org.springframework.util.FileSystemUtils; | ||||||
|  | 
 | ||||||
|  | import static org.assertj.core.api.Assertions.assertThat; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Integration tests for the {@link BootRun} task configured to use the test source set. | ||||||
|  |  * | ||||||
|  |  * @author Andy Wilkinson | ||||||
|  |  */ | ||||||
|  | @GradleCompatibility(configurationCache = true) | ||||||
|  | class BootTestRunIntegrationTests { | ||||||
|  | 
 | ||||||
|  | 	GradleBuild gradleBuild; | ||||||
|  | 
 | ||||||
|  | 	@TestTemplate | ||||||
|  | 	void basicExecution() throws IOException { | ||||||
|  | 		copyClasspathApplication(); | ||||||
|  | 		BuildResult result = this.gradleBuild.build("bootTestRun"); | ||||||
|  | 		assertThat(result.task(":bootTestRun").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); | ||||||
|  | 		assertThat(result.getOutput()).contains("1. " + canonicalPathOf("build/classes/java/test")) | ||||||
|  | 			.contains("2. " + canonicalPathOf("build/resources/test")) | ||||||
|  | 			.contains("3. " + canonicalPathOf("build/classes/java/main")) | ||||||
|  | 			.contains("4. " + canonicalPathOf("build/resources/main")); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@TestTemplate | ||||||
|  | 	void defaultJvmArgs() throws IOException { | ||||||
|  | 		copyJvmArgsApplication(); | ||||||
|  | 		BuildResult result = this.gradleBuild.build("bootTestRun"); | ||||||
|  | 		assertThat(result.task(":bootTestRun").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); | ||||||
|  | 		assertThat(result.getOutput()).contains("-XX:TieredStopAtLevel=1"); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@TestTemplate | ||||||
|  | 	void optimizedLaunchDisabledJvmArgs() throws IOException { | ||||||
|  | 		copyJvmArgsApplication(); | ||||||
|  | 		BuildResult result = this.gradleBuild.build("bootTestRun"); | ||||||
|  | 		assertThat(result.task(":bootTestRun").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); | ||||||
|  | 		assertThat(result.getOutput()).doesNotContain("-Xverify:none").doesNotContain("-XX:TieredStopAtLevel=1"); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@TestTemplate | ||||||
|  | 	void applicationPluginJvmArgumentsAreUsed() throws IOException { | ||||||
|  | 		if (this.gradleBuild.isConfigurationCache()) { | ||||||
|  | 			// https://github.com/gradle/gradle/pull/23924 | ||||||
|  | 			GradleVersion gradleVersion = GradleVersion.version(this.gradleBuild.getGradleVersion()); | ||||||
|  | 			Assumptions.assumeThat(gradleVersion) | ||||||
|  | 				.isLessThan(GradleVersion.version("8.0")) | ||||||
|  | 				.isGreaterThanOrEqualTo(GradleVersion.version("8.1-rc-1")); | ||||||
|  | 		} | ||||||
|  | 		copyJvmArgsApplication(); | ||||||
|  | 		BuildResult result = this.gradleBuild.build("bootTestRun"); | ||||||
|  | 		assertThat(result.task(":bootTestRun").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); | ||||||
|  | 		assertThat(result.getOutput()).contains("-Dcom.bar=baz") | ||||||
|  | 			.contains("-Dcom.foo=bar") | ||||||
|  | 			.contains("-XX:TieredStopAtLevel=1"); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@TestTemplate | ||||||
|  | 	void jarTypeFilteringIsAppliedToTheClasspath() throws IOException { | ||||||
|  | 		copyClasspathApplication(); | ||||||
|  | 		File flatDirRepository = new File(this.gradleBuild.getProjectDir(), "repository"); | ||||||
|  | 		createDependenciesStarterJar(new File(flatDirRepository, "starter.jar")); | ||||||
|  | 		createStandardJar(new File(flatDirRepository, "standard.jar")); | ||||||
|  | 		BuildResult result = this.gradleBuild.build("bootTestRun"); | ||||||
|  | 		assertThat(result.task(":bootTestRun").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); | ||||||
|  | 		assertThat(result.getOutput()).contains("standard.jar").doesNotContain("starter.jar"); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@TestTemplate | ||||||
|  | 	void failsGracefullyWhenNoTestMainMethodIsFound() throws IOException { | ||||||
|  | 		copyApplication("nomain"); | ||||||
|  | 		BuildResult result = this.gradleBuild.buildAndFail("bootTestRun"); | ||||||
|  | 		assertThat(result.task(":bootTestRun").getOutcome()).isEqualTo(TaskOutcome.FAILED); | ||||||
|  | 		if (this.gradleBuild.isConfigurationCache() && this.gradleBuild.gradleVersionIsAtLeast("8.0")) { | ||||||
|  | 			assertThat(result.getOutput()) | ||||||
|  | 				.contains("Main class name has not been configured and it could not be resolved from classpath"); | ||||||
|  | 		} | ||||||
|  | 		else { | ||||||
|  | 			assertThat(result.getOutput()) | ||||||
|  | 				.contains("Main class name has not been configured and it could not be resolved from classpath " | ||||||
|  | 						+ "build/classes/java/test"); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private void copyClasspathApplication() throws IOException { | ||||||
|  | 		copyApplication("classpath"); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private void copyJvmArgsApplication() throws IOException { | ||||||
|  | 		copyApplication("jvmargs"); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private void copyApplication(String name) throws IOException { | ||||||
|  | 		File output = new File(this.gradleBuild.getProjectDir(), "src/test/java/com/example/boottestrun/" + name); | ||||||
|  | 		output.mkdirs(); | ||||||
|  | 		FileSystemUtils.copyRecursively(new File("src/test/java/com/example/boottestrun/" + name), output); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private String canonicalPathOf(String path) throws IOException { | ||||||
|  | 		return new File(this.gradleBuild.getProjectDir(), path).getCanonicalPath(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private void createStandardJar(File location) throws IOException { | ||||||
|  | 		createJar(location, (attributes) -> { | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private void createDependenciesStarterJar(File location) throws IOException { | ||||||
|  | 		createJar(location, (attributes) -> attributes.putValue("Spring-Boot-Jar-Type", "dependencies-starter")); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private void createJar(File location, Consumer<Attributes> attributesConfigurer) throws IOException { | ||||||
|  | 		location.getParentFile().mkdirs(); | ||||||
|  | 		Manifest manifest = new Manifest(); | ||||||
|  | 		Attributes attributes = manifest.getMainAttributes(); | ||||||
|  | 		attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0"); | ||||||
|  | 		attributesConfigurer.accept(attributes); | ||||||
|  | 		new JarOutputStream(new FileOutputStream(location), manifest).close(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,4 @@ | ||||||
|  | plugins { | ||||||
|  | 	id 'org.springframework.boot' version '{version}' | ||||||
|  | 	id 'java' | ||||||
|  | } | ||||||
|  | @ -0,0 +1,3 @@ | ||||||
|  | plugins { | ||||||
|  | 	id 'org.springframework.boot' version '{version}' | ||||||
|  | } | ||||||
|  | @ -0,0 +1,8 @@ | ||||||
|  | plugins { | ||||||
|  | 	id 'application' | ||||||
|  | 	id 'org.springframework.boot' version '{version}' | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | application { | ||||||
|  | 	applicationDefaultJvmArgs = ['-Dcom.foo=bar', '-Dcom.bar=baz'] | ||||||
|  | } | ||||||
|  | @ -0,0 +1,4 @@ | ||||||
|  | plugins { | ||||||
|  | 	id 'java' | ||||||
|  | 	id 'org.springframework.boot' version '{version}' | ||||||
|  | } | ||||||
|  | @ -0,0 +1,4 @@ | ||||||
|  | plugins { | ||||||
|  | 	id 'application' | ||||||
|  | 	id 'org.springframework.boot' version '{version}' | ||||||
|  | } | ||||||
|  | @ -0,0 +1,4 @@ | ||||||
|  | plugins { | ||||||
|  | 	id 'java' | ||||||
|  | 	id 'org.springframework.boot' version '{version}' | ||||||
|  | } | ||||||
|  | @ -0,0 +1,15 @@ | ||||||
|  | plugins { | ||||||
|  | 	id 'java' | ||||||
|  | 	id 'org.springframework.boot' version '{version}' | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | repositories { | ||||||
|  | 	flatDir { | ||||||
|  | 		dirs 'repository' | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | dependencies { | ||||||
|  | 	implementation(name: "standard") | ||||||
|  | 	implementation(name: "starter") | ||||||
|  | } | ||||||
|  | @ -0,0 +1,8 @@ | ||||||
|  | plugins { | ||||||
|  | 	id 'application' | ||||||
|  | 	id 'org.springframework.boot' version '{version}' | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | bootTestRun { | ||||||
|  | 	optimizedLaunch = false | ||||||
|  | } | ||||||
|  | @ -189,6 +189,10 @@ public class GradleBuild { | ||||||
| 		return this; | 		return this; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	public boolean gradleVersionIsAtLeast(String version) { | ||||||
|  | 		return GradleVersion.version(this.gradleVersion).compareTo(GradleVersion.version(version)) >= 0; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	public BuildResult build(String... arguments) { | 	public BuildResult build(String... arguments) { | ||||||
| 		try { | 		try { | ||||||
| 			BuildResult result = prepareRunner(arguments).build(); | 			BuildResult result = prepareRunner(arguments).build(); | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue