From baa427069c0529244dbe7d74ebca7cd7a1871c27 Mon Sep 17 00:00:00 2001 From: Dmytro Nosan Date: Tue, 12 Aug 2025 23:52:08 +0300 Subject: [PATCH] Polish ArchitectureCheckTests Prior to this commit, it wasn't possible to write a test to verify that a specific ArchRule applied only to a specific SourceSet. This PR rewrites `ArchitectureCheckTests` to support different sources and also adds functionality to configure the plugin. With these changes, all tests now verify not only the `checkArchitectureMain` task but also `checkArchitectureTest`. Signed-off-by: Dmytro Nosan --- .../architecture/ArchitectureCheckTests.java | 454 ++++++++++++------ 1 file changed, 296 insertions(+), 158 deletions(-) diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java index fc59b48cbc56..f299aa596a9a 100644 --- a/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java @@ -16,20 +16,32 @@ package org.springframework.boot.build.architecture; -import java.io.FileNotFoundException; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.function.Consumer; - +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.gradle.api.tasks.SourceSet; +import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.GradleRunner; +import org.gradle.testkit.runner.TaskOutcome; +import org.gradle.testkit.runner.UnexpectedBuildFailure; +import org.gradle.testkit.runner.UnexpectedBuildSuccess; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; -import org.springframework.core.io.ClassPathResource; +import org.springframework.util.ClassUtils; import org.springframework.util.FileSystemUtils; +import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -43,161 +55,184 @@ */ class ArchitectureCheckTests { - private Path projectDir; + private static final String SPRING_CONTEXT = "org.springframework:spring-context:6.2.9"; + + private static final String SPRING_INTEGRATION_JMX = "org.springframework.integration:spring-integration-jmx:6.5.1"; - private Path buildFile; + private GradleBuild gradleBuild; @BeforeEach void setup(@TempDir Path projectDir) { - this.projectDir = projectDir; - this.buildFile = projectDir.resolve("build.gradle"); + this.gradleBuild = new GradleBuild(projectDir); } - @Test - void whenPackagesAreTangledTaskFailsAndWritesAReport() throws IOException { - runGradleWithCompiledClasses("tangled", - shouldHaveFailureReportWithMessages("slices matching '(**)' should be free of cycles")); + @ParameterizedTest(name = "{0}") + @EnumSource(Task.class) + void whenPackagesAreTangledShouldFailAndWriteReport(Task task) throws IOException { + prepareTask(task, "tangled"); + buildAndFail(this.gradleBuild, task, "slices matching '(**)' should be free of cycles"); } - @Test - void whenPackagesAreNotTangledTaskSucceedsAndWritesAnEmptyReport() throws IOException { - runGradleWithCompiledClasses("untangled", shouldHaveEmptyFailureReport()); + @ParameterizedTest(name = "{0}") + @EnumSource(Task.class) + void whenPackagesAreNotTangledShouldSucceedAndWriteEmptyReport(Task task) throws IOException { + prepareTask(task, "untangled"); + build(this.gradleBuild, task); } - @Test - void whenBeanPostProcessorBeanMethodIsNotStaticTaskFailsAndWritesAReport() throws IOException { - runGradleWithCompiledClasses("bpp/nonstatic", - shouldHaveFailureReportWithMessages( - "methods that are annotated with @Bean and have raw return type assignable " - + "to org.springframework.beans.factory.config.BeanPostProcessor")); + @ParameterizedTest(name = "{0}") + @EnumSource(Task.class) + void whenBeanPostProcessorBeanMethodIsNotStaticShouldFailAndWriteReport(Task task) throws IOException { + prepareTask(task, "bpp/nonstatic"); + buildAndFail(this.gradleBuild.withDependencies(SPRING_CONTEXT), task, + "methods that are annotated with @Bean and have raw return type assignable" + + " to org.springframework.beans.factory.config.BeanPostProcessor"); } - @Test - void whenBeanPostProcessorBeanMethodIsStaticAndHasUnsafeParametersTaskFailsAndWritesAReport() throws IOException { - runGradleWithCompiledClasses("bpp/unsafeparameters", - shouldHaveFailureReportWithMessages( - "methods that are annotated with @Bean and have raw return type assignable " - + "to org.springframework.beans.factory.config.BeanPostProcessor")); + @ParameterizedTest(name = "{0}") + @EnumSource(Task.class) + void whenBeanPostProcessorBeanMethodIsStaticAndHasUnsafeParametersShouldFailAndWriteReport(Task task) + throws IOException { + prepareTask(task, "bpp/unsafeparameters"); + buildAndFail(this.gradleBuild.withDependencies(SPRING_CONTEXT), task, + "methods that are annotated with @Bean and have raw return type assignable" + + " to org.springframework.beans.factory.config.BeanPostProcessor"); } - @Test - void whenBeanPostProcessorBeanMethodIsStaticAndHasSafeParametersTaskSucceedsAndWritesAnEmptyReport() + @ParameterizedTest(name = "{0}") + @EnumSource(Task.class) + void whenBeanPostProcessorBeanMethodIsStaticAndHasSafeParametersShouldSucceedAndWriteEmptyReport(Task task) throws IOException { - runGradleWithCompiledClasses("bpp/safeparameters", shouldHaveEmptyFailureReport()); + prepareTask(task, "bpp/safeparameters"); + build(this.gradleBuild.withDependencies(SPRING_CONTEXT), task); } - @Test - void whenBeanPostProcessorBeanMethodIsStaticAndHasNoParametersTaskSucceedsAndWritesAnEmptyReport() + @ParameterizedTest(name = "{0}") + @EnumSource(Task.class) + void whenBeanPostProcessorBeanMethodIsStaticAndHasNoParametersShouldSucceedAndWriteEmptyReport(Task task) throws IOException { - runGradleWithCompiledClasses("bpp/noparameters", shouldHaveEmptyFailureReport()); + prepareTask(task, "bpp/noparameters"); + build(this.gradleBuild.withDependencies(SPRING_CONTEXT), task); } - @Test - void whenBeanFactoryPostProcessorBeanMethodIsNotStaticTaskFailsAndWritesAReport() throws IOException { - runGradleWithCompiledClasses("bfpp/nonstatic", - shouldHaveFailureReportWithMessages("methods that are annotated with @Bean and have raw return " - + "type assignable to org.springframework.beans.factory.config.BeanFactoryPostProcessor")); + @ParameterizedTest(name = "{0}") + @EnumSource(Task.class) + void whenBeanFactoryPostProcessorBeanMethodIsNotStaticShouldFailAndWriteReport(Task task) throws IOException { + prepareTask(task, "bfpp/nonstatic"); + buildAndFail(this.gradleBuild.withDependencies(SPRING_CONTEXT), task, + "methods that are annotated with @Bean and have raw return type assignable" + + " to org.springframework.beans.factory.config.BeanFactoryPostProcessor"); } - @Test - void whenBeanFactoryPostProcessorBeanMethodIsStaticAndHasParametersTaskFailsAndWritesAReport() throws IOException { - runGradleWithCompiledClasses("bfpp/parameters", - shouldHaveFailureReportWithMessages("methods that are annotated with @Bean and have raw return " - + "type assignable to org.springframework.beans.factory.config.BeanFactoryPostProcessor")); + @ParameterizedTest(name = "{0}") + @EnumSource(Task.class) + void whenBeanFactoryPostProcessorBeanMethodIsStaticAndHasParametersShouldFailAndWriteReport(Task task) + throws IOException { + prepareTask(task, "bfpp/parameters"); + buildAndFail(this.gradleBuild.withDependencies(SPRING_CONTEXT), task, + "methods that are annotated with @Bean and have raw return type assignable" + + " to org.springframework.beans.factory.config.BeanFactoryPostProcessor"); } - @Test - void whenBeanFactoryPostProcessorBeanMethodIsStaticAndHasNoParametersTaskSucceedsAndWritesAnEmptyReport() + @ParameterizedTest(name = "{0}") + @EnumSource(Task.class) + void whenBeanFactoryPostProcessorBeanMethodIsStaticAndHasNoParametersShouldSucceedAndWriteEmptyReport(Task task) throws IOException { - runGradleWithCompiledClasses("bfpp/noparameters", shouldHaveEmptyFailureReport()); + prepareTask(task, "bfpp/noparameters"); + build(this.gradleBuild.withDependencies(SPRING_CONTEXT), task); } - @Test - void whenClassLoadsResourceUsingResourceUtilsTaskFailsAndWritesReport() throws IOException { - runGradleWithCompiledClasses("resources/loads", shouldHaveFailureReportWithMessages( - "no classes should call method where target owner type org.springframework.util.ResourceUtils and target name 'getURL'")); + @ParameterizedTest(name = "{0}") + @EnumSource(Task.class) + void whenClassLoadsResourceUsingResourceUtilsShouldFailAndWriteReport(Task task) throws IOException { + prepareTask(task, "resources/loads"); + buildAndFail(this.gradleBuild.withDependencies(SPRING_CONTEXT), task, + "no classes should call method where target owner type" + + " org.springframework.util.ResourceUtils and target name 'getURL'"); } - @Test - void whenClassUsesResourceUtilsWithoutLoadingResourcesTaskSucceedsAndWritesAnEmptyReport() throws IOException { - runGradleWithCompiledClasses("resources/noloads", shouldHaveEmptyFailureReport()); + @ParameterizedTest(name = "{0}") + @EnumSource(Task.class) + void whenClassUsesResourceUtilsWithoutLoadingResourcesShouldSucceedAndWriteEmptyReport(Task task) + throws IOException { + prepareTask(task, "resources/noloads"); + build(this.gradleBuild.withDependencies(SPRING_CONTEXT), task); } - @Test - void whenClassDoesNotCallObjectsRequireNonNullTaskSucceedsAndWritesAnEmptyReport() throws IOException { - runGradleWithCompiledClasses("objects/noRequireNonNull", shouldHaveEmptyFailureReport()); + @ParameterizedTest(name = "{0}") + @EnumSource(Task.class) + void whenClassDoesNotCallObjectsRequireNonNullShouldSucceedAndWriteEmptyReport(Task task) throws IOException { + prepareTask(task, "objects/noRequireNonNull"); + build(this.gradleBuild.withDependencies(SPRING_CONTEXT), task); } - @Test - void whenClassCallsObjectsRequireNonNullWithMessageTaskFailsAndWritesReport() throws IOException { - runGradleWithCompiledClasses("objects/requireNonNullWithString", shouldHaveFailureReportWithMessages( - "no classes should call method Objects.requireNonNull(Object, String)")); + @ParameterizedTest(name = "{0}") + @EnumSource(Task.class) + void whenClassCallsObjectsRequireNonNullWithMessageShouldFailAndWriteReport(Task task) throws IOException { + prepareTask(task, "objects/requireNonNullWithString"); + buildAndFail(this.gradleBuild, task, "no classes should call method Objects.requireNonNull(Object, String)"); } - @Test - void whenClassCallsObjectsRequireNonNullWithSupplierTaskFailsAndWritesReport() throws IOException { - runGradleWithCompiledClasses("objects/requireNonNullWithSupplier", shouldHaveFailureReportWithMessages( - "no classes should call method Objects.requireNonNull(Object, Supplier)")); + @ParameterizedTest(name = "{0}") + @EnumSource(Task.class) + void whenClassCallsObjectsRequireNonNullWithMessageAndProhibitObjectsRequireNonNullIsFalseShouldSucceedAndWriteEmptyReport( + Task task) throws IOException { + prepareTask(task, "objects/requireNonNullWithString"); + build(this.gradleBuild.withProhibitObjectsRequireNonNull(task, false), task); } - @Test - void whenClassCallsStringToUpperCaseWithoutLocaleFailsAndWritesReport() throws IOException { - runGradleWithCompiledClasses("string/toUpperCase", - shouldHaveFailureReportWithMessages("because String.toUpperCase(Locale.ROOT) should be used instead")); + @ParameterizedTest(name = "{0}") + @EnumSource(Task.class) + void whenClassCallsObjectsRequireNonNullWithSupplierShouldFailAndWriteReport(Task task) throws IOException { + prepareTask(task, "objects/requireNonNullWithSupplier"); + buildAndFail(this.gradleBuild, task, "no classes should call method Objects.requireNonNull(Object, Supplier)"); } - @Test - void whenClassCallsStringToLowerCaseWithoutLocaleFailsAndWritesReport() throws IOException { - runGradleWithCompiledClasses("string/toLowerCase", - shouldHaveFailureReportWithMessages("because String.toLowerCase(Locale.ROOT) should be used instead")); + @ParameterizedTest(name = "{0}") + @EnumSource(Task.class) + void whenClassCallsObjectsRequireNonNullWithSupplierAndProhibitObjectsRequireNonNullIsFalseShouldSucceedAndWriteEmptyReport( + Task task) throws IOException { + prepareTask(task, "objects/requireNonNullWithSupplier"); + build(this.gradleBuild.withProhibitObjectsRequireNonNull(task, false), task); } - @Test - void whenClassCallsStringToLowerCaseWithLocaleShouldNotFail() throws IOException { - runGradleWithCompiledClasses("string/toLowerCaseWithLocale", shouldHaveEmptyFailureReport()); + @ParameterizedTest(name = "{0}") + @EnumSource(Task.class) + void whenClassCallsStringToUpperCaseWithoutLocaleShouldFailAndWriteReport(Task task) throws IOException { + prepareTask(task, "string/toUpperCase"); + buildAndFail(this.gradleBuild, task, "because String.toUpperCase(Locale.ROOT) should be used instead"); } - @Test - void whenClassCallsStringToUpperCaseWithLocaleShouldNotFail() throws IOException { - runGradleWithCompiledClasses("string/toUpperCaseWithLocale", shouldHaveEmptyFailureReport()); + @ParameterizedTest(name = "{0}") + @EnumSource(Task.class) + void whenClassCallsStringToLowerCaseWithoutLocaleShouldFailAndWriteReport(Task task) throws IOException { + prepareTask(task, "string/toLowerCase"); + buildAndFail(this.gradleBuild, task, "because String.toLowerCase(Locale.ROOT) should be used instead"); } - @Test - void whenBeanMethodExposePrivateTypeShouldFailAndWriteReport() throws IOException { - runGradleWithCompiledClasses("beans/privatebean", shouldHaveFailureReportWithMessages( - "methods that are annotated with @Bean should not return types declared with the PRIVATE modifier," - + " as such types are incompatible with Spring AOT processing", - "Method " - + "returns Class " - + " which is declared as [PRIVATE, STATIC, FINAL]")); + @ParameterizedTest(name = "{0}") + @EnumSource(Task.class) + void whenClassCallsStringToLowerCaseWithLocaleShouldSucceedAndWriteEmptyReport(Task task) throws IOException { + prepareTask(task, "string/toLowerCaseWithLocale"); + build(this.gradleBuild, task); } - @Test - void whenBeanMethodExposeNonPrivateTypeShouldNotFail() throws IOException { - runGradleWithCompiledClasses("beans/regular", shouldHaveEmptyFailureReport()); + @ParameterizedTest(name = "{0}") + @EnumSource(Task.class) + void whenClassCallsStringToUpperCaseWithLocaleShouldSucceedAndWriteEmptyReport(Task task) throws IOException { + prepareTask(task, "string/toUpperCaseWithLocale"); + build(this.gradleBuild, task); } - @Test - void whenBeanPostProcessorBeanMethodIsNotStaticWithExternalClass() throws IOException { - Files.writeString(this.buildFile, """ - plugins { - id 'java' - id 'org.springframework.boot.architecture' - } - repositories { - mavenCentral() - } - java { - sourceCompatibility = 17 - } - dependencies { - implementation("org.springframework.integration:spring-integration-jmx:6.3.9") - } - """); - Path testClass = this.projectDir.resolve("src/main/java/boot/architecture/bpp/external/TestClass.java"); - Files.createDirectories(testClass.getParent()); - Files.writeString(testClass, """ - package org.springframework.boot.build.architecture.bpp.external; + @ParameterizedTest(name = "{0}") + @EnumSource(Task.class) + void whenBeanPostProcessorBeanMethodIsNotStaticWithExternalClassShouldFailAndWriteReport(Task task) + throws IOException { + Path sourceDirectory = task.getSourceDirectory(this.gradleBuild.getProjectDir()) + .resolve(ClassUtils.classPackageAsResourcePath(getClass())); + Files.createDirectories(sourceDirectory); + Files.writeString(sourceDirectory.resolve("TestClass.java"), """ + package %s; import org.springframework.context.annotation.Bean; import org.springframework.integration.monitor.IntegrationMBeanExporter; public class TestClass { @@ -206,68 +241,171 @@ IntegrationMBeanExporter integrationMBeanExporter() { return new IntegrationMBeanExporter(); } } - """); - runGradle(shouldHaveFailureReportWithMessages("methods that are annotated with @Bean and have raw return " - + "type assignable to org.springframework.beans.factory.config.BeanPostProcessor ")); + """.formatted(ClassUtils.getPackageName(getClass()))); + buildAndFail(this.gradleBuild.withDependencies(SPRING_INTEGRATION_JMX), task, + "methods that are annotated with @Bean and have raw return type assignable " + + "to org.springframework.beans.factory.config.BeanPostProcessor"); } - private Consumer shouldHaveEmptyFailureReport() { - return (gradleRunner) -> { - try { - assertThat(gradleRunner.build().getOutput()).contains("BUILD SUCCESSFUL") - .contains("Task :checkArchitectureMain"); - assertThat(failureReport()).isEmpty(); - } - catch (Exception ex) { - throw new AssertionError("Expected build to succeed but it failed\n" + failureReport(), ex); - } - }; + @Test + void whenBeanMethodExposesPrivateTypeWithMainSourcesShouldFailAndWriteReport() throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "beans/privatebean"); + buildAndFail(this.gradleBuild.withDependencies(SPRING_CONTEXT), Task.CHECK_ARCHITECTURE_MAIN, + "methods that are annotated with @Bean should not return types declared " + + "with the PRIVATE modifier, as such types are incompatible with Spring AOT processing", + "returns Class " + + " which is declared as [PRIVATE, STATIC, FINAL]"); } - private Consumer shouldHaveFailureReportWithMessages(String... messages) { - return (gradleRunner) -> { - assertThat(gradleRunner.buildAndFail().getOutput()).contains("BUILD FAILED") - .contains("Task :checkArchitectureMain FAILED"); - assertThat(failureReport()).contains(messages); - }; + @Test + void whenBeanMethodExposesPrivateTypeWithTestsSourcesShouldSucceedAndWriteEmptyReport() throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_TEST, "beans/privatebean"); + build(this.gradleBuild.withDependencies(SPRING_CONTEXT), Task.CHECK_ARCHITECTURE_TEST); } - private void runGradleWithCompiledClasses(String path, Consumer callback) throws IOException { - ClassPathResource classPathResource = new ClassPathResource(path, getClass()); - FileSystemUtils.copyRecursively(classPathResource.getFile().toPath(), - this.projectDir.resolve("classes").resolve(classPathResource.getPath())); - Files.writeString(this.buildFile, """ - plugins { - id 'java' - id 'org.springframework.boot.architecture' - } - sourceSets { - main { - output.classesDirs.setFrom(file("classes")) - } - } - """); - runGradle(callback); + @ParameterizedTest(name = "{0}") + @EnumSource(Task.class) + void whenBeanMethodExposesNonPrivateTypeShouldSucceedAndWriteEmptyReport(Task task) throws IOException { + prepareTask(task, "beans/regular"); + build(this.gradleBuild.withDependencies(SPRING_CONTEXT), task); } - private void runGradle(Consumer callback) { - callback.accept(GradleRunner.create() - .withProjectDir(this.projectDir.toFile()) - .withArguments("checkArchitectureMain") - .withPluginClasspath()); + private void prepareTask(Task task, String... sourceDirectories) throws IOException { + for (String sourceDirectory : sourceDirectories) { + FileSystemUtils.copyRecursively( + Paths.get("src/test/java") + .resolve(ClassUtils.classPackageAsResourcePath(getClass())) + .resolve(sourceDirectory), + task.getSourceDirectory(this.gradleBuild.getProjectDir()) + .resolve(ClassUtils.classPackageAsResourcePath(getClass())) + .resolve(sourceDirectory)); + } } - private String failureReport() { + private void build(GradleBuild gradleBuild, Task task) throws IOException { try { - Path failureReport = this.projectDir.resolve("build/checkArchitectureMain/failure-report.txt"); - return Files.readString(failureReport, StandardCharsets.UTF_8); + BuildResult buildResult = gradleBuild.build(task.toString()); + assertThat(buildResult.taskPaths(TaskOutcome.SUCCESS)).contains(":" + task); + assertThat(task.getFailureReport(gradleBuild.getProjectDir())).isEmpty(); } - catch (FileNotFoundException ex) { - return "Failure report does not exist"; + catch (UnexpectedBuildFailure ex) { + StringBuilder message = new StringBuilder("Expected build to succeed but it failed"); + if (Files.exists(task.getFailureReportFile(gradleBuild.getProjectDir()))) { + message.append('\n').append(task.getFailureReport(gradleBuild.getProjectDir())); + } + message.append('\n').append(ex.getBuildResult().getOutput()); + throw new AssertionError(message.toString(), ex); } - catch (IOException ex) { - return "Failure report could not be read: " + ex.getMessage(); + } + + private void buildAndFail(GradleBuild gradleBuild, Task task, String... messages) throws IOException { + try { + BuildResult buildResult = gradleBuild.buildAndFail(task.toString()); + assertThat(buildResult.taskPaths(TaskOutcome.FAILED)).contains(":" + task); + assertThat(task.getFailureReport(gradleBuild.getProjectDir())).contains(messages); + } + catch (UnexpectedBuildSuccess ex) { + throw new AssertionError("Expected build to fail but it succeeded\n" + ex.getBuildResult().getOutput(), ex); + } + } + + private enum Task { + + CHECK_ARCHITECTURE_MAIN(SourceSet.MAIN_SOURCE_SET_NAME), + + CHECK_ARCHITECTURE_TEST(SourceSet.TEST_SOURCE_SET_NAME); + + private final String sourceSetName; + + Task(String sourceSetName) { + this.sourceSetName = sourceSetName; + } + + String getFailureReport(Path projectDir) throws IOException { + return Files.readString(getFailureReportFile(projectDir), StandardCharsets.UTF_8); + } + + Path getFailureReportFile(Path projectDir) { + return projectDir.resolve("build/%s/failure-report.txt".formatted(toString())); + } + + Path getSourceDirectory(Path projectDir) { + return projectDir.resolve("src/%s/java".formatted(this.sourceSetName)); + } + + @Override + public String toString() { + return "checkArchitecture" + StringUtils.capitalize(this.sourceSetName); + } + + } + + private static final class GradleBuild { + + private final Path projectDir; + + private final Set dependencies = new LinkedHashSet<>(); + + private final Map prohibitObjectsRequireNonNull = new LinkedHashMap<>(); + + private GradleBuild(Path projectDir) { + this.projectDir = projectDir; + } + + Path getProjectDir() { + return this.projectDir; + } + + GradleBuild withProhibitObjectsRequireNonNull(Task task, boolean prohibitObjectsRequireNonNull) { + this.prohibitObjectsRequireNonNull.put(task, prohibitObjectsRequireNonNull); + return this; + } + + GradleBuild withDependencies(String... dependencies) { + this.dependencies.addAll(Arrays.asList(dependencies)); + return this; + } + + BuildResult build(String... arguments) throws IOException { + return prepareRunner(arguments).build(); + } + + BuildResult buildAndFail(String... arguments) throws IOException { + return prepareRunner(arguments).buildAndFail(); } + + private GradleRunner prepareRunner(String... arguments) throws IOException { + StringBuilder buildFile = new StringBuilder(); + buildFile.append("plugins {\n") + .append(" id 'java'\n") + .append(" id 'org.springframework.boot.architecture'\n") + .append("}\n\n") + .append("repositories {\n") + .append(" mavenCentral()\n") + .append("}\n\n") + .append("java {\n") + .append(" sourceCompatibility = '17'\n") + .append(" targetCompatibility = '17'\n") + .append("}\n\n"); + if (!this.dependencies.isEmpty()) { + buildFile.append("dependencies {\n"); + for (String dependency : this.dependencies) { + buildFile.append(" implementation '%s'\n".formatted(dependency)); + } + buildFile.append("}\n"); + } + this.prohibitObjectsRequireNonNull.forEach((task, prohibitObjectsRequireNonNull) -> buildFile.append(task) + .append(" {\n") + .append(" prohibitObjectsRequireNonNull = ") + .append(prohibitObjectsRequireNonNull) + .append("\n}\n\n")); + Files.writeString(this.projectDir.resolve("build.gradle"), buildFile, StandardCharsets.UTF_8); + return GradleRunner.create() + .withProjectDir(this.projectDir.toFile()) + .withArguments(arguments) + .withPluginClasspath(); + } + } }