|
|
|
|
@ -16,20 +16,32 @@
@@ -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 @@ import static org.assertj.core.api.Assertions.assertThat;
@@ -43,161 +55,184 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|
|
|
|
*/ |
|
|
|
|
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 <org.springframework.boot.build.architecture.beans.privatebean.PrivateBean.myBean()> " |
|
|
|
|
+ "returns Class <org.springframework.boot.build.architecture.beans.privatebean.PrivateBean$MyBean>" |
|
|
|
|
+ " 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 @@ class ArchitectureCheckTests {
@@ -206,68 +241,171 @@ class ArchitectureCheckTests {
|
|
|
|
|
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<GradleRunner> 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 <org.springframework.boot.build.architecture.beans.privatebean.PrivateBean$MyBean>" |
|
|
|
|
+ " which is declared as [PRIVATE, STATIC, FINAL]"); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private Consumer<GradleRunner> 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<GradleRunner> 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<GradleRunner> 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<String> dependencies = new LinkedHashSet<>(); |
|
|
|
|
|
|
|
|
|
private final Map<Task, Boolean> 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(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|