@ -16,22 +16,20 @@
@@ -16,22 +16,20 @@
package org.springframework.boot.build.architecture ;
import java.io.File ;
import java.io.IOException ;
import java.nio.file.Files ;
import java.nio.file.Path ;
import java.util.function.Consumer ;
import org.gradle.api.GradleException ;
import org.gradle.api.Project ;
import org.gradle.testfixtures.ProjectBuilder ;
import org.gradle.testkit.runner.GradleRunner ;
import org.junit.jupiter.api.BeforeEach ;
import org.junit.jupiter.api.Test ;
import org.junit.jupiter.api.io.TempDir ;
import org.springframework.core.io.Resource ;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver ;
import org.springframework.core.io.ClassPathResource ;
import org.springframework.util.FileSystemUtils ;
import org.springframework.util.function.ThrowingConsumer ;
import static org.assertj.core.api.Assertions.assertThat ;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType ;
/ * *
* Tests for { @link ArchitectureCheck } .
@ -39,188 +37,206 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -39,188 +37,206 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
* @author Andy Wilkinson
* @author Scott Frederick
* @author Ivan Malutin
* @author Dmytro Nosan
* /
class ArchitectureCheckTests {
@TempDir
File temp ;
private Path projectDir ;
@Test
void whenPackagesAreTangledTaskFailsAndWritesAReport ( ) throws Exception {
prepareTask ( "tangled" , ( architectureCheck ) - > {
assertThatExceptionOfType ( GradleException . class ) . isThrownBy ( architectureCheck : : checkArchitecture ) ;
assertThat ( failureReport ( architectureCheck ) ) . isNotEmpty ( ) ;
} ) ;
private Path buildFile ;
@BeforeEach
void setup ( @TempDir Path projectDir ) {
this . projectDir = projectDir ;
this . buildFile = projectDir . resolve ( "build.gradle" ) ;
}
@Test
void whenPackagesAreNotTangledTaskSucceedsAndWritesAnEmptyReport ( ) throws Exception {
prepareTask ( "untangled" , ( architectureCheck ) - > {
architectureCheck . checkArchitecture ( ) ;
assertThat ( failureReport ( architectureCheck ) ) . isEmpty ( ) ;
} ) ;
void whenPackagesAreTangledTaskFailsAndWritesAReport ( ) throws IOException {
runGradleWithCompiledClasses ( "tangled" ,
shouldHaveFailureReportWithMessage ( "slices matching '(**)' should be free of cycles" ) ) ;
}
File failureReport ( ArchitectureCheck architectureCheck ) {
return architectureCheck . getProject ( )
. getLayout ( )
. getBuildDirectory ( )
. file ( "checkArchitecture/failure-report.txt" )
. get ( )
. getAsFile ( ) ;
@Test
void whenPackagesAreNotTangledTaskSucceedsAndWritesAnEmptyReport ( ) throws IOException {
runGradleWithCompiledClasses ( "untangled" , shouldHaveEmptyFailureReport ( ) ) ;
}
@Test
void whenBeanPostProcessorBeanMethodIsNotStaticTaskFailsAndWritesAReport ( ) throws Exception {
prepareTask ( "bpp/nonstatic" , ( architectureCheck ) - > {
assertThatExceptionOfType ( GradleException . class ) . isThrownBy ( architectureCheck : : checkArchitecture ) ;
assertThat ( failureReport ( architectureCheck ) ) . isNotEmpty ( ) ;
} ) ;
void whenBeanPostProcessorBeanMethodIsNotStaticTaskFailsAndWritesAReport ( ) throws IOException {
runGradleWithCompiledClasses ( "bpp/nonstatic" ,
shouldHaveFailureReportWithMessage (
"methods that are annotated with @Bean and have raw return type assignable "
+ "to org.springframework.beans.factory.config.BeanPostProcessor" ) ) ;
}
@Test
void whenBeanPostProcessorBeanMethodIsStaticAndHasUnsafeParametersTaskFailsAndWritesAReport ( ) throws Exception {
prepareTask ( "bpp/unsafeparameters" , ( architectureCheck ) - > {
assertThatExceptionOfType ( GradleException . class ) . isThrownBy ( architectureCheck : : checkArchitecture ) ;
assertThat ( failureReport ( architectureCheck ) ) . isNotEmpty ( ) ;
} ) ;
void whenBeanPostProcessorBeanMethodIsStaticAndHasUnsafeParametersTaskFailsAndWritesAReport ( ) throws IO Exception {
runGradleWithCompiledClasses ( "bpp/unsafeparameters" ,
shouldHaveFailureReportWithMessage (
"methods that are annotated with @Bean and have raw return type assignable "
+ "to org.springframework.beans.factory.config.BeanPostProcessor" ) ) ;
}
@Test
void whenBeanPostProcessorBeanMethodIsStaticAndHasSafeParametersTaskSucceedsAndWritesAnEmptyReport ( )
throws Exception {
prepareTask ( "bpp/safeparameters" , ( architectureCheck ) - > {
architectureCheck . checkArchitecture ( ) ;
assertThat ( failureReport ( architectureCheck ) ) . isEmpty ( ) ;
} ) ;
throws IOException {
runGradleWithCompiledClasses ( "bpp/safeparameters" , shouldHaveEmptyFailureReport ( ) ) ;
}
@Test
void whenBeanPostProcessorBeanMethodIsStaticAndHasNoParametersTaskSucceedsAndWritesAnEmptyReport ( )
throws Exception {
prepareTask ( "bpp/noparameters" , ( architectureCheck ) - > {
architectureCheck . checkArchitecture ( ) ;
assertThat ( failureReport ( architectureCheck ) ) . isEmpty ( ) ;
} ) ;
throws IOException {
runGradleWithCompiledClasses ( "bpp/noparameters" , shouldHaveEmptyFailureReport ( ) ) ;
}
@Test
void whenBeanFactoryPostProcessorBeanMethodIsNotStaticTaskFailsAndWritesAReport ( ) throws Exception {
prepareTask ( "bfpp/nonstatic" , ( architectureCheck ) - > {
assertThatExceptionOfType ( GradleException . class ) . isThrownBy ( architectureCheck : : checkArchitecture ) ;
assertThat ( failureReport ( architectureCheck ) ) . isNotEmpty ( ) ;
} ) ;
void whenBeanFactoryPostProcessorBeanMethodIsNotStaticTaskFailsAndWritesAReport ( ) throws IOException {
runGradleWithCompiledClasses ( "bfpp/nonstatic" ,
shouldHaveFailureReportWithMessage ( "methods that are annotated with @Bean and have raw return "
+ "type assignable to org.springframework.beans.factory.config.BeanFactoryPostProcessor" ) ) ;
}
@Test
void whenBeanFactoryPostProcessorBeanMethodIsStaticAndHasParametersTaskFailsAndWritesAReport ( ) throws Exception {
prepareTask ( "bfpp/parameters" , ( architectureCheck ) - > {
assertThatExceptionOfType ( GradleException . class ) . isThrownBy ( architectureCheck : : checkArchitecture ) ;
assertThat ( failureReport ( architectureCheck ) ) . isNotEmpty ( ) ;
} ) ;
void whenBeanFactoryPostProcessorBeanMethodIsStaticAndHasParametersTaskFailsAndWritesAReport ( ) throws IOException {
runGradleWithCompiledClasses ( "bfpp/parameters" ,
shouldHaveFailureReportWithMessage ( "methods that are annotated with @Bean and have raw return "
+ "type assignable to org.springframework.beans.factory.config.BeanFactoryPostProcessor" ) ) ;
}
@Test
void whenBeanFactoryPostProcessorBeanMethodIsStaticAndHasNoParametersTaskSucceedsAndWritesAnEmptyReport ( )
throws Exception {
prepareTask ( "bfpp/noparameters" , ( architectureCheck ) - > {
architectureCheck . checkArchitecture ( ) ;
assertThat ( failureReport ( architectureCheck ) ) . isEmpty ( ) ;
} ) ;
throws IOException {
runGradleWithCompiledClasses ( "bfpp/noparameters" , shouldHaveEmptyFailureReport ( ) ) ;
}
@Test
void whenClassLoadsResourceUsingResourceUtilsTaskFailsAndWritesReport ( ) throws IOException {
runGradleWithCompiledClasses ( "resources/loads" , shouldHaveFailureReportWithMessage (
"no classes should call method where target owner type org.springframework.util.ResourceUtils and target name 'getURL'" ) ) ;
}
@Test
void whenClassLoadsResourceUsingResourceUtilsTaskFailsAndWritesReport ( ) throws Exception {
prepareTask ( "resources/loads" , ( architectureCheck ) - > {
assertThatExceptionOfType ( GradleException . class ) . isThrownBy ( architectureCheck : : checkArchitecture ) ;
assertThat ( failureReport ( architectureCheck ) ) . isNotEmpty ( ) ;
} ) ;
void whenClassUsesResourceUtilsWithoutLoadingResourcesTaskSucceedsAndWritesAnEmptyReport ( ) throws IOException {
runGradleWithCompiledClasses ( "resources/noloads" , shouldHaveEmptyFailureReport ( ) ) ;
}
@Test
void whenClassUsesResourceUtilsWithoutLoadingResourcesTaskSucceedsAndWritesAnEmptyReport ( ) throws Exception {
prepareTask ( "resources/noloads" , ( architectureCheck ) - > {
architectureCheck . checkArchitecture ( ) ;
assertThat ( failureReport ( architectureCheck ) ) . isEmpty ( ) ;
} ) ;
void whenClassDoesNotCallObjectsRequireNonNullTaskSucceedsAndWritesAnEmptyReport ( ) throws IOException {
runGradleWithCompiledClasses ( "objects/noRequireNonNull" , shouldHaveEmptyFailureReport ( ) ) ;
}
@Test
void whenClassDoesNotCallObjectsRequireNonNullTaskSucceedsAndWritesAnEmptyReport ( ) throws Exception {
prepareTask ( "objects/noRequireNonNull" , ( architectureCheck ) - > {
architectureCheck . checkArchitecture ( ) ;
assertThat ( failureReport ( architectureCheck ) ) . isEmpty ( ) ;
} ) ;
void whenClassCallsObjectsRequireNonNullWithMessageTaskFailsAndWritesReport ( ) throws IOException {
runGradleWithCompiledClasses ( "objects/requireNonNullWithString" , shouldHaveFailureReportWithMessage (
"no classes should call method Objects.requireNonNull(Object, String)" ) ) ;
}
@Test
void whenClassCallsObjectsRequireNonNullWithMessageTaskFailsAndWritesReport ( ) throws Exception {
prepareTask ( "objects/requireNonNullWithString" , ( architectureCheck ) - > {
assertThatExceptionOfType ( GradleException . class ) . isThrownBy ( architectureCheck : : checkArchitecture ) ;
assertThat ( failureReport ( architectureCheck ) ) . isNotEmpty ( ) ;
} ) ;
void whenClassCallsObjectsRequireNonNullWithSupplierTaskFailsAndWritesReport ( ) throws IOException {
runGradleWithCompiledClasses ( "objects/requireNonNullWithSupplier" , shouldHaveFailureReportWithMessage (
"no classes should call method Objects.requireNonNull(Object, Supplier)" ) ) ;
}
@Test
void whenClassCallsObjectsRequireNonNullWithSupplierTaskFailsAndWritesReport ( ) throws Exception {
prepareTask ( "objects/requireNonNullWithSupplier" , ( architectureCheck ) - > {
assertThatExceptionOfType ( GradleException . class ) . isThrownBy ( architectureCheck : : checkArchitecture ) ;
assertThat ( failureReport ( architectureCheck ) ) . isNotEmpty ( ) ;
} ) ;
void whenClassCallsStringToUpperCaseWithoutLocaleFailsAndWritesReport ( ) throws IOException {
runGradleWithCompiledClasses ( "string/toUpperCase" ,
shouldHaveFailureReportWithMessage ( "because String.toUpperCase(Locale.ROOT) should be used instead" ) ) ;
}
@Test
void whenClassCallsStringToUpperCaseWithoutLocaleFailsAndWritesReport ( ) throws Exception {
prepareTask ( "string/toUpperCase" , ( architectureCheck ) - > {
assertThatExceptionOfType ( GradleException . class ) . isThrownBy ( architectureCheck : : checkArchitecture ) ;
assertThat ( failureReport ( architectureCheck ) ) . isNotEmpty ( )
. content ( )
. contains ( "because String.toUpperCase(Locale.ROOT) should be used instead" ) ;
} ) ;
void whenClassCallsStringToLowerCaseWithoutLocaleFailsAndWritesReport ( ) throws IOException {
runGradleWithCompiledClasses ( "string/toLowerCase" ,
shouldHaveFailureReportWithMessage ( "because String.toLowerCase(Locale.ROOT) should be used instead" ) ) ;
}
@Test
void whenClassCallsStringToLowerCaseWithoutLocaleFailsAndWritesReport ( ) throws Exception {
prepareTask ( "string/toLowerCase" , ( architectureCheck ) - > {
assertThatExceptionOfType ( GradleException . class ) . isThrownBy ( architectureCheck : : checkArchitecture ) ;
assertThat ( failureReport ( architectureCheck ) ) . isNotEmpty ( )
. content ( )
. contains ( "because String.toLowerCase(Locale.ROOT) should be used instead" ) ;
} ) ;
void whenClassCallsStringToLowerCaseWithLocaleShouldNotFail ( ) throws IOException {
runGradleWithCompiledClasses ( "string/toLowerCaseWithLocale" , shouldHaveEmptyFailureReport ( ) ) ;
}
@Test
void whenClassCallsStringToLowerCaseWithLocaleShouldNotFail ( ) throws Exception {
prepareTask ( "string/toLowerCaseWithLocale" , ( architectureCheck ) - > {
architectureCheck . checkArchitecture ( ) ;
assertThat ( failureReport ( architectureCheck ) ) . isEmpty ( ) ;
} ) ;
void whenClassCallsStringToUpperCaseWithLocaleShouldNotFail ( ) throws IOException {
runGradleWithCompiledClasses ( "string/toUpperCaseWithLocale" , shouldHaveEmptyFailureReport ( ) ) ;
}
@Test
void whenClassCallsStringToUpperCaseWithLocaleShouldNotFail ( ) throws Exception {
prepareTask ( "string/toUpperCaseWithLocale" , ( architectureCheck ) - > {
architectureCheck . checkArchitecture ( ) ;
assertThat ( failureReport ( architectureCheck ) ) . isEmpty ( ) ;
} ) ;
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 ;
import org.springframework.context.annotation.Bean ;
import org.springframework.integration.monitor.IntegrationMBeanExporter ;
public class TestClass {
@Bean
IntegrationMBeanExporter integrationMBeanExporter ( ) {
return new IntegrationMBeanExporter ( ) ;
}
}
"" " ) ;
runGradle ( shouldHaveFailureReportWithMessage ( "methods that are annotated with @Bean and have raw return "
+ "type assignable to org.springframework.beans.factory.config.BeanPostProcessor " ) ) ;
}
private void prepareTask ( String classes , ThrowingConsumer < ArchitectureCheck > callback ) throws Exception {
File projectDir = new File ( this . temp , "project" ) ;
projectDir . mkdirs ( ) ;
copyClasses ( classes , projectDir ) ;
Project project = ProjectBuilder . builder ( ) . withProjectDir ( projectDir ) . build ( ) ;
project . getTasks ( ) . register ( "checkArchitecture" , ArchitectureCheck . class , ( task ) - > {
task . setClasses ( project . files ( "classes" ) ) ;
callback . accept ( task ) ;
} ) ;
private Consumer < GradleRunner > shouldHaveEmptyFailureReport ( ) {
return ( gradleRunner ) - > {
assertThat ( gradleRunner . build ( ) . getOutput ( ) ) . contains ( "BUILD SUCCESSFUL" )
. contains ( "Task :checkArchitectureMain" ) ;
assertThat ( failureReport ( ) ) . isEmptyFile ( ) ;
} ;
}
private Consumer < GradleRunner > shouldHaveFailureReportWithMessage ( String message ) {
return ( gradleRunner ) - > {
assertThat ( gradleRunner . buildAndFail ( ) . getOutput ( ) ) . contains ( "BUILD FAILED" )
. contains ( "Task :checkArchitectureMain FAILED" ) ;
assertThat ( failureReport ( ) ) . content ( ) . contains ( message ) ;
} ;
}
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 ) ;
}
private void runGradle ( Consumer < GradleRunner > callback ) {
callback . accept ( GradleRunner . create ( )
. withProjectDir ( this . projectDir . toFile ( ) )
. withArguments ( "checkArchitectureMain" )
. withPluginClasspath ( ) ) ;
}
private void copyClasses ( String name , File projectDir ) throws IOException {
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver ( ) ;
Resource root = resolver . getResource ( "classpath:org/springframework/boot/build/architecture/" + name ) ;
FileSystemUtils . copyRecursively ( root . getFile ( ) ,
new File ( projectDir , "classes/org/springframework/boot/build/architecture/" + name ) ) ;
private Path failureReport ( ) {
return this . projectDir . resolve ( "build/checkArchitectureMain/failure-report.txt" ) ;
}
}