@ -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 ( ) ;
}
}
}