diff --git a/pom.xml b/pom.xml index ad46a49..03f975b 100644 --- a/pom.xml +++ b/pom.xml @@ -56,8 +56,8 @@ 2.7.3 - ${project.build.directory}/test-results - + ${project.build.directory}/test-results + ${project.basedir}/target/generated-test-sources/test-annotations @@ -84,6 +84,7 @@ https://oss.sonatype.org/service/local/staging/deploy/maven2/ + @@ -142,6 +143,13 @@ test + + junit + junit + 4.13.1 + test + + org.assertj assertj-core @@ -159,10 +167,70 @@ --> + + + jakarta.xml.bind + jakarta.xml.bind-api + 2.3.3 + compile + + + com.sun.xml.bind + jaxb-impl + 2.3.3 + compile + + + + org.codehaus.mojo + jaxb2-maven-plugin + 2.5.0 + + + testXjc + + testXjc + + generate-test-sources + + + + com.societegenerale.commons.plugin.maven.test.generated + ${generated.test.source.directory} + + ${project.basedir}/src/test/resources/generate/book.xsd + + + ${project.basedir}/src/test/resources/generate/book.xsd + + true + + + + org.glassfish.jaxb + jaxb-xjc + 2.3.2 + compile + + + jakarta.xml.bind + jakarta.xml.bind-api + 2.3.3 + compile + + + com.sun.xml.bind + jaxb-impl + 2.3.3 + compile + + + + org.apache.maven.plugins maven-compiler-plugin @@ -171,6 +239,28 @@ ${maven.compiler.source} ${maven.compiler.target} + + + default + compile + + compile + + + + compile-generated-test-source + test-compile + + testCompile + + + + ${generated.test.source.directory} + + ${generated.test.source.directory} + + + @@ -183,6 +273,9 @@ **/*Test.java + + **/Abstract*Test.java + diff --git a/src/main/java/com/societegenerale/commons/plugin/maven/ArchUnitMojo.java b/src/main/java/com/societegenerale/commons/plugin/maven/ArchUnitMojo.java index 079cdc5..271d7df 100644 --- a/src/main/java/com/societegenerale/commons/plugin/maven/ArchUnitMojo.java +++ b/src/main/java/com/societegenerale/commons/plugin/maven/ArchUnitMojo.java @@ -5,10 +5,12 @@ import java.net.URL; import java.util.ArrayList; import java.util.List; +import java.util.Set; import com.societegenerale.commons.plugin.maven.model.MavenRules; import com.societegenerale.commons.plugin.model.Rules; import com.societegenerale.commons.plugin.service.RuleInvokerService; +import com.tngtech.archunit.thirdparty.com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.StringUtils; import org.apache.maven.artifact.DependencyResolutionRequiredException; import org.apache.maven.plugin.AbstractMojo; @@ -54,11 +56,14 @@ public class ArchUnitMojo extends AbstractMojo { @Parameter(defaultValue = "${project}", required = true, readonly = true) private MavenProject mavenProject; + @Parameter(defaultValue = "${project.build.directory}") + private String projectBuildDir; + public MavenRules getRules() { return rules; } - private RuleInvokerService ruleInvokerService ; + private RuleInvokerService ruleInvokerService; private static final String PREFIX_ARCH_VIOLATION_MESSAGE = "ArchUnit Maven plugin reported architecture failures listed below :"; @@ -82,8 +87,10 @@ public void execute() throws MojoFailureException { String ruleFailureMessage; try { configureContextClassLoader(); + final MavenLogAdapter mavenLogAdapter = new MavenLogAdapter(getLog()); - ruleInvokerService = new RuleInvokerService(new MavenLogAdapter(getLog()), new MavenScopePathProvider(mavenProject), excludedPaths); + final Set excludes = new ExcludedPathsPreProcessor().processExcludedPaths(getLog(), projectBuildDir, excludedPaths); + ruleInvokerService = new RuleInvokerService(mavenLogAdapter, new MavenScopePathProvider(mavenProject), excludes); ruleFailureMessage = ruleInvokerService.invokeRules(coreRules); } catch (final Exception e) { @@ -107,4 +114,8 @@ private void configureContextClassLoader() throws DependencyResolutionRequiredEx Thread.currentThread().setContextClassLoader(contextClassLoader); } + @VisibleForTesting + void setProjectBuildDir(final String projectBuildDir) { + this.projectBuildDir = projectBuildDir; + } } diff --git a/src/main/java/com/societegenerale/commons/plugin/maven/ExcludedPathsPreProcessor.java b/src/main/java/com/societegenerale/commons/plugin/maven/ExcludedPathsPreProcessor.java new file mode 100644 index 0000000..d02d797 --- /dev/null +++ b/src/main/java/com/societegenerale/commons/plugin/maven/ExcludedPathsPreProcessor.java @@ -0,0 +1,211 @@ +package com.societegenerale.commons.plugin.maven; + +import java.io.IOException; +import java.io.Serializable; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import com.societegenerale.commons.plugin.maven.JavaFileParser.JavaFile; +import com.tngtech.archunit.thirdparty.com.google.common.annotations.VisibleForTesting; +import org.apache.commons.io.FilenameUtils; +import org.apache.maven.plugin.logging.Log; + +/** + * This class analyses exclusion paths. It looks in directories and sub directories for java files and collects them to + * an exclusion set. + */ +class ExcludedPathsPreProcessor implements Serializable +{ + private static final String FILE_TYPE_JAVA = ".java"; + + static final String GENERATTED_SOURCES = "generated-sources"; + static final String PACKAGE_INFO_JAVA = "package-info.java"; + + /** + * This method analyses the excludedPaths. It looks into directories and sub directories for java files + * and collects them to an exclusion set. The directories which containing java files will be replaced by class + * names generated by the java files. + * + * @param mavenLogger - not null + * @param projectBuildDir - the target folder + * @param excludedPaths - relative or absoltue paths or the keywords generated-test-sources, generated-sources + * + * @return a set of classes to be exclude, not null + */ + Set processExcludedPaths(@Nonnull final Log mavenLogger, final String projectBuildDir, + final Collection excludedPaths) + { + final Set result = excludedPaths != null ? new HashSet<>(excludedPaths) : new HashSet<>(); + if (excludedPaths == null) + { + return result; + } + + mavenLogger.debug("projectBuildDir: " + projectBuildDir); + + final String targetDir = projectBuildDir != null ? projectBuildDir.trim() : ""; + + for (final String pathAsString : excludedPaths) + { + final Path path = convertToPath(mavenLogger, targetDir, pathAsString.trim()); + final Set javaFiles = findJavaFiles(path, mavenLogger); + + final Set classNames = determineClassNames(javaFiles, mavenLogger); + result.addAll(classNames); + } + + return result; + } + + /** + * Searchs for *.java files in the given path and sub paths. + * + * @param path - a filesystem path + * + * @return a set of java files, not null + */ + @VisibleForTesting + Set findJavaFiles(final Path path, @Nonnull final Log mavenLogger) + { + Set files = new HashSet<>(); + try + { + if (path != null) + { + if (Files.isDirectory(path)) + { + // search in directories and collect all java files + files = Files.find(path, Integer.MAX_VALUE, + (p, basicFileAttributes) -> isJavaFile(p.toString())) + .collect(Collectors.toSet()); + } + else + { + // is it a java file? + if (isJavaFile(path.toString())) + { + files.add(path); + } + } + } + } + catch (final IOException e) + { + mavenLogger.warn("unabble to collect java files in path: " + path, e); + } + + return files; + } + + @VisibleForTesting + boolean isJavaFile(final String pathString) + { + final String path = pathString != null ? pathString.toLowerCase().trim() : ""; + return !path.endsWith(PACKAGE_INFO_JAVA) && path.endsWith(FILE_TYPE_JAVA); + } + + /** + * Tries to determine a directory path. + * + * @param projectBuildDir - project build directory, not null + * @param pathAsString - a string which could be a directory path or a file path or part of path (projectBuildDir + * + pathAsString), not null + * + * @return the {@link Path} + * + * @throws InvalidPathException if pathAsString cannot be converted into a {@link Path} + */ + @Nullable + @VisibleForTesting + Path convertToPath(@Nonnull final Log mavenLogger, final String projectBuildDir, @Nonnull final String pathAsString) + throws InvalidPathException + { + Path result; + + // if pathAsString itself represents a valid path + try + { + result = Paths.get(pathAsString); + if (Files.exists(result)) + { + return result; + } + } + catch (final InvalidPathException e) + { + mavenLogger.debug("invalid path: " + pathAsString, e); + } + + // maybe the combination of projectBuildDir and pathAsString is a valid path + try + { + final String concatPath = FilenameUtils.concat(projectBuildDir, pathAsString); + mavenLogger.debug("concatPath: " + concatPath); + result = Paths.get(concatPath); + + if (!Files.exists(result)) + { + mavenLogger.warn("invalid exclusion path: " + pathAsString); + mavenLogger.warn("invalid exclusion path: " + concatPath); + result = null; + } + } + catch (final InvalidPathException e) + { + mavenLogger.warn("invalid path: " + pathAsString, e); + result = null; + } + + return result; + } + + /** + * Determine the class names by given java files (package.filename). In assumption that the file is equal to the + * class name within the file content. + * + * @param javaFilePaths - not null + * @param mavenLogger - not null + * + * @return not null + */ + @VisibleForTesting + Set determineClassNames(final Set javaFilePaths, final Log mavenLogger) + { + final JavaFileParser fileParser = new JavaFileParser(); + + final Set packageWithClassNames = new HashSet<>(); + + for (final Path path : javaFilePaths) + { + final JavaFile file; + try + { + file = fileParser.parse(path, mavenLogger); + + if (file.getClassName() != null) + { + final String packageWithClassName = + file.getPackageString() != null ? file.getPackageString() + "." + file.getClassName() : + file.getClassName(); + packageWithClassNames.add(packageWithClassName); + } + } + catch (final IOException e) + { + mavenLogger.warn("unable to read file: " + path); + } + } + + return packageWithClassNames; + } + +} diff --git a/src/main/java/com/societegenerale/commons/plugin/maven/JavaFileParser.java b/src/main/java/com/societegenerale/commons/plugin/maven/JavaFileParser.java new file mode 100644 index 0000000..32db24c --- /dev/null +++ b/src/main/java/com/societegenerale/commons/plugin/maven/JavaFileParser.java @@ -0,0 +1,217 @@ +package com.societegenerale.commons.plugin.maven; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import com.tngtech.archunit.thirdparty.com.google.common.annotations.VisibleForTesting; +import org.apache.maven.plugin.logging.Log; + +class JavaFileParser +{ + private static final String PACKAGE = "package"; + private static final String CLASS = "class"; + static final String REGEX_LINE_BREAKS = "[\n\r]"; + private static final String BLOCK_COMMENT_START = "/**"; + private static final String BLOCK_COMMENT_END = "*/"; + private static final String LINE_COMMENT_START = "//"; + private static final String LINE_COMMENT_END = "\n"; + + /** + * Parse the file and returns {@link JavaFile} + * + * @param javafilePath - not null + * @param mavenLogger - not null + * + * @return not null + */ + JavaFile parse(final Path javafilePath, final Log mavenLogger) throws IOException + { + final String fileContent = readFile(javafilePath); + String packageString = null; + String className = null; + + if (fileContent.isEmpty()) + { + mavenLogger.warn("empty file: " + javafilePath); + } + else + { + packageString = extractPackage(fileContent, mavenLogger); + className = extractClassName(fileContent, mavenLogger); + } + + return new JavaFile(packageString, className); + } + + /** + * Extracts the package out of the file content. + * + * @param javaFileContent - file content as string + * @param mavenLogger - not null + * + * @return null if the package could not be extracted + */ + @VisibleForTesting + String extractPackage(final String javaFileContent, @Nonnull final Log mavenLogger) + { + if (javaFileContent == null || javaFileContent.isEmpty() || !javaFileContent.contains(PACKAGE)) + { + return null; + } + + final String substring = javaFileContent.substring(javaFileContent.indexOf(PACKAGE)); + + // split at the first ";" + final String[] split = substring.split(";", 2); + // there should be at least 1 part which contains the package declaration + if (split.length < 1) + { + mavenLogger.warn("unexpected file content: " + javaFileContent); + return null; + } + + final String packageDeclaration = split[0].trim(); + if (!packageDeclaration.startsWith(PACKAGE)) + { + mavenLogger.warn("unabble to find package declaration in: " + packageDeclaration); + return null; + } + + // get the part after the keyword package + return packageDeclaration.substring(PACKAGE.length()).trim(); + } + + /** + * Reads the java file. + * + * @param javafilePath - not null + * + * @return the file content as String if there is a content, not null + * + * @throws IOException when + */ + @VisibleForTesting + String readFile(@Nonnull final Path javafilePath) throws IOException + { + final StringBuilder builder = new StringBuilder(); + Files.readAllLines(javafilePath).forEach(builder::append); + + return builder.toString(); + } + + /** + * Extracts the class name. + * + * @param javaFileContent - file content as String + * @param mavenLogger - not null + * + * @return class name or null + */ + @VisibleForTesting + @Nullable + String extractClassName(final String javaFileContent, @Nonnull final Log mavenLogger) + { + if (javaFileContent == null || javaFileContent.isEmpty() || !javaFileContent.contains(CLASS)) + { + return null; + } + + final String commentsRemoved = removeComments(javaFileContent); + final String classOffsetContent = commentsRemoved.substring(commentsRemoved.indexOf(CLASS)); + + // split at the first "{" + final String[] split = classOffsetContent.split("\\{", 2); + // there should be at least 1 part which contains the class declaration + if (split.length < 1) + { + mavenLogger.warn("unexpected file content: " + javaFileContent); + return null; + } + + final String classDeclaration = split[0].trim(); + if (!classDeclaration.contains(CLASS)) + { + mavenLogger.warn("unabble to find class declaration in: " + classDeclaration); + return null; + } + + // get the part after the keyword class + final String substring = classDeclaration.substring(classDeclaration.lastIndexOf(CLASS) + CLASS.length()); + final String withoutLineBreaks = substring.replaceAll(REGEX_LINE_BREAKS, " ").trim(); + // remove extends or implements to get only the class name + return withoutLineBreaks.split(" ", 2)[0]; + } + + @VisibleForTesting + String removeComments(@Nonnull final String javaFileContent) + { + final String withOutBlockComments = removeAllBlocks(javaFileContent, BLOCK_COMMENT_START, BLOCK_COMMENT_END); + return removeAllBlocks(withOutBlockComments, LINE_COMMENT_START, LINE_COMMENT_END); + + } + + /** + * Removes all text blocks defined by blockStartString and blockEndString. + * + * @param javaFileContent - not null + * @param blockStartString - not null + * @param blockEndString - not null + * + * @return string without the defined text blocks + */ + private String removeAllBlocks(@Nonnull final String javaFileContent, + @Nonnull final String blockStartString, + @Nonnull final String blockEndString) + { + int startIndex = javaFileContent.indexOf(blockStartString); + int endIndex = calEndIndex(blockEndString, startIndex, javaFileContent); + String content = javaFileContent; + + while (startIndex >= 0 && endIndex > startIndex) + { + content = content.substring(0, startIndex) + content.substring(endIndex); + + startIndex = content.indexOf(blockStartString); + endIndex = calEndIndex(blockEndString, startIndex, content); + } + + return content; + } + + private int calEndIndex(@Nonnull final String blockEndString, + final int startIndex, + @Nonnull final String content) + { + final int endIndex = content.indexOf(blockEndString, startIndex) + blockEndString.length(); + return endIndex > content.length() ? content.length() : endIndex; + } + + /** + * Class that holds relevant java file information + */ + static class JavaFile + { + private final String packageString; + private final String className; + + JavaFile(final String packageString, final String className) + { + this.packageString = packageString == null || packageString.isEmpty() ? null : packageString; + this.className = className == null || className.isEmpty() ? null : className; + } + + String getPackageString() + { + return packageString; + } + + String getClassName() + { + return className; + } + } +} diff --git a/src/test/java/com/societegenerale/commons/plugin/maven/AbstractArchUnitMojoTest.java b/src/test/java/com/societegenerale/commons/plugin/maven/AbstractArchUnitMojoTest.java new file mode 100644 index 0000000..b1c958a --- /dev/null +++ b/src/test/java/com/societegenerale/commons/plugin/maven/AbstractArchUnitMojoTest.java @@ -0,0 +1,98 @@ +package com.societegenerale.commons.plugin.maven; + +import java.io.File; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.google.common.collect.ImmutableSet; +import org.assertj.core.api.AbstractThrowableAssert; +import org.assertj.core.api.Condition; +import org.codehaus.plexus.configuration.DefaultPlexusConfiguration; +import org.codehaus.plexus.configuration.PlexusConfiguration; + +import static java.util.Arrays.stream; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +abstract class AbstractArchUnitMojoTest +{ + protected static ExpectedRuleFailure.Creator expectRuleFailure(String ruleDescription) { + return new ExpectedRuleFailure.Creator(ruleDescription); + } + + protected String getBasedir() { + String basedir = System.getProperty("basedir"); + + if (basedir == null) { + basedir = new File("").getAbsolutePath(); + } + + return basedir; + } + + protected void executeAndExpectViolations(ArchUnitMojo mojo, ExpectedRuleFailure... expectedRuleFailures) { + AbstractThrowableAssert throwableAssert = assertThatThrownBy(mojo::execute); + stream(expectedRuleFailures).forEach(expectedFailure -> { + throwableAssert.hasMessageContaining(String.format("Rule '%s' was violated", expectedFailure.ruleDescription)); + expectedFailure.details.forEach(throwableAssert::hasMessageContaining); + }); + throwableAssert.has(exactNumberOfViolatedRules(expectedRuleFailures.length)); + } + + private Condition exactNumberOfViolatedRules(final int number) { + return new Condition("exactly " + number + " violated rules") { + @Override + public boolean matches(Throwable throwable) { + Matcher matcher = Pattern.compile("Rule '.*' was violated").matcher(throwable.getMessage()); + int numberOfOccurrences = 0; + while (matcher.find()) { + numberOfOccurrences++; + } + return numberOfOccurrences == number; + } + }; + } + + protected PlexusConfiguration buildApplyOnBlock(String packageName, String scope) { + + PlexusConfiguration packageNameElement = new DefaultPlexusConfiguration("packageName", packageName); + PlexusConfiguration scopeElement = new DefaultPlexusConfiguration("scope", scope); + PlexusConfiguration applyOnElement = new DefaultPlexusConfiguration("applyOn"); + applyOnElement.addChild(packageNameElement); + applyOnElement.addChild(scopeElement); + + return applyOnElement; + } + + protected PlexusConfiguration buildChecksBlock(String... checks) { + PlexusConfiguration checksElement = new DefaultPlexusConfiguration("checks"); + stream(checks).map(c -> new DefaultPlexusConfiguration("check", c)).forEach(checksElement::addChild); + return checksElement; + } + + public static class ExpectedRuleFailure { + private final String ruleDescription; + private final Set details; + + private ExpectedRuleFailure(String ruleDescription, Set details) { + this.ruleDescription = ruleDescription; + this.details = details; + } + + public static class Creator { + private final String ruleDescription; + + Creator(String ruleDescription) { + this.ruleDescription = ruleDescription; + } + + ExpectedRuleFailure ofAnyKind() { + return withDetails(); + } + + ExpectedRuleFailure withDetails(String... detals) { + return new ExpectedRuleFailure(ruleDescription, ImmutableSet.copyOf(detals)); + } + } + } +} diff --git a/src/test/java/com/societegenerale/commons/plugin/maven/AbstractExcludePathTest.java b/src/test/java/com/societegenerale/commons/plugin/maven/AbstractExcludePathTest.java new file mode 100644 index 0000000..ee9dddb --- /dev/null +++ b/src/test/java/com/societegenerale/commons/plugin/maven/AbstractExcludePathTest.java @@ -0,0 +1,122 @@ +package com.societegenerale.commons.plugin.maven; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.apache.commons.io.FileUtils; +import org.apache.maven.plugin.logging.Log; +import org.mockito.Mock; + +abstract class AbstractExcludePathTest +{ + private static final String fileComment = "//\n // This file is generated by X \n //"; + static final String CLASS_NAME = "ExcludePathTestFile"; + static final String CLASS_NAME_WITH_FILE_COMMENT = "ExcludePathTestFileWithFileComment"; + static final String CONTENT_WITH_DEFAULT_PACKAGE = "import org.junit.Test; \r\n" + + "// line comment \r\n" + + "/**" + + "* Block comment " + + "*/" + + "public class " + CLASS_NAME + + " implements Serializable\r\n" + + "{ " + + " public static class InnerClass{}" + + "}"; + + private static final String CONTENT_WITH_FILE_COMMENT = "import org.junit.Test; \r\n" + + "// line comment, this is a genetated class foo \r\n" + + "/**" + + "* Block comment, this is a generated class bar " + + "*/\n" + + "public class " + CLASS_NAME_WITH_FILE_COMMENT + + "{ \r\n" + + " public static class InnerClass{}" + + "}"; + + static final String PACKAGE_NAME = AbstractExcludePathTest.class.getPackage().getName(); + static final String CONTENT_WITH_PACKAGE = "package " + PACKAGE_NAME + ";\r\n" + + CONTENT_WITH_DEFAULT_PACKAGE; + + static final String CONTENT_WITH_FILE_COMMENT_AND_PACKAGE = + fileComment + "package " + PACKAGE_NAME + ";\r\n" + + CONTENT_WITH_FILE_COMMENT; + + private static final String PROJECT_BUILD_DIR_NAME = "target"; + + private static Path testTempRootDirectory = null; + private static Path testProjectBuildDirectory = null; + private static Path tempJavaFile = null; + private static Path tempJavaFileWithFileComment = null; + private static Path tempJavaFileWithDefaultPackage = null; + + @Mock + private Log mavenLogger; + + static void init() throws IOException + { + testTempRootDirectory = Files.createTempDirectory("AbstractExcludePathTestRoot"); + + testProjectBuildDirectory = Paths.get(testTempRootDirectory.toString(), PROJECT_BUILD_DIR_NAME); + Files.createDirectory(testProjectBuildDirectory); + + tempJavaFileWithDefaultPackage = + Files.createFile( + Paths.get(testProjectBuildDirectory.toString(), CLASS_NAME + ".java")); + Files.write(tempJavaFileWithDefaultPackage, CONTENT_WITH_DEFAULT_PACKAGE.getBytes()); + + final Path packagePath = Files.createDirectories(Paths.get(testProjectBuildDirectory.toString(), + AbstractExcludePathTest.class.getPackage() + .getName() + .split("\\."))); + + tempJavaFile = Files.createFile( + Paths.get(packagePath.toString(), CLASS_NAME + ".java")); + Files.write(tempJavaFile, CONTENT_WITH_PACKAGE.getBytes()); + + tempJavaFileWithFileComment = + Files.createFile( + Paths.get(packagePath.toString(), + CLASS_NAME_WITH_FILE_COMMENT + ".java")); + Files.write(tempJavaFileWithFileComment, CONTENT_WITH_FILE_COMMENT_AND_PACKAGE.getBytes()); + } + + static void cleanup() throws IOException + { + if (testTempRootDirectory != null) + { + FileUtils.forceDelete(testTempRootDirectory.toFile()); + } + } + + Path getTempJavaFile() + { + return tempJavaFile; + } + + static Path getTempJavaFileWithFileComment() + { + return tempJavaFileWithFileComment; + } + + Path getTempJavaFileWithDefaultPackage() + { + return tempJavaFileWithDefaultPackage; + } + + Log getMavenLogger() + { + return mavenLogger; + } + + Path getTestTempRootDirectory() + { + return testTempRootDirectory; + } + + static Path getTestProjectBuildDirectory() + { + return testProjectBuildDirectory; + } +} diff --git a/src/test/java/com/societegenerale/commons/plugin/maven/ArchUnitMojoExcludeTest.java b/src/test/java/com/societegenerale/commons/plugin/maven/ArchUnitMojoExcludeTest.java new file mode 100644 index 0000000..c1d3869 --- /dev/null +++ b/src/test/java/com/societegenerale/commons/plugin/maven/ArchUnitMojoExcludeTest.java @@ -0,0 +1,143 @@ +package com.societegenerale.commons.plugin.maven; + +import java.io.StringReader; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import com.societegenerale.commons.plugin.rules.StringFieldsThatAreActuallyDatesRuleTest; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import org.apache.maven.model.Build; +import org.apache.maven.plugin.testing.MojoRule; +import org.apache.maven.project.MavenProject; +import org.assertj.core.api.AbstractThrowableAssert; +import org.codehaus.plexus.configuration.PlexusConfiguration; +import org.codehaus.plexus.util.xml.Xpp3DomBuilder; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith (DataProviderRunner.class) +public class ArchUnitMojoExcludeTest extends AbstractArchUnitMojoTest +{ + + @Rule + public final MojoRule mojoRule1 = new MojoRule(); + + @Rule + public final MojoRule mojoRule2 = new MojoRule(); + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @InjectMocks + private ArchUnitMojo archUnitMojo; + + @Mock + private MavenProject mavenProject; + + + // @formatter:off + private static final String pomWithNoRuleNoExcludes = + "" + + "" + + "" + + "" + + "arch-unit-maven-plugin" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + // @formatter:on + + // @formatter:off + // "./" + + private static final String pomWithExcludes = + "" + + "" + + + "" + + " ${project.basedir}/target" + + "" + + "" + + "arch-unit-maven-plugin" + + "" + + "" + + "generated-test-sources" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + // @formatter:on + + + @Before + public void setUp() + { + + final Build mockBuild = mock(Build.class); + + when(mavenProject.getBuild()).thenReturn(mockBuild); + when(mockBuild.getOutputDirectory()).thenReturn("target/test-classes"); + when(mockBuild.getTestOutputDirectory()).thenReturn("target/test-classes"); + + archUnitMojo.setProjectBuildDir(getBasedir() + "/target"); + } + + @Test + public void generatedSourcesShouldViolatePreconfiguredRule() throws Exception + { + final PlexusConfiguration pluginConfiguration = createPluginConfiguration(mojoRule1, pomWithNoRuleNoExcludes); + + final String exceptionMessageRule = ":Rule Violated - " + StringFieldsThatAreActuallyDatesRuleTest.class.getPackage() + .getName(); + final String exceptionMessageClass = "class: com.societegenerale.commons.plugin.maven.test.generated.Book"; + + // add single rule + final PlexusConfiguration preConfiguredRules = pluginConfiguration.getChild("rules").getChild("preConfiguredRules"); + preConfiguredRules.addChild("rule", StringFieldsThatAreActuallyDatesRuleTest.class.getName()); + + final ArchUnitMojo mojo = (ArchUnitMojo) mojoRule1.configureMojo(archUnitMojo, pluginConfiguration); + + final AbstractThrowableAssert throwableAssert = assertThatThrownBy(mojo::execute); + throwableAssert.hasMessageContaining(exceptionMessageRule); + throwableAssert.hasMessageContaining(exceptionMessageClass); + } + + @Test + public void excludedGeneratedSourcesShouldNotViolatePreconfiguredRule() throws Exception + { + final PlexusConfiguration pluginConfiguration = createPluginConfiguration(mojoRule2, pomWithExcludes); + // add single rule + final PlexusConfiguration preConfiguredRules = pluginConfiguration.getChild("rules").getChild("preConfiguredRules"); + preConfiguredRules.addChild("rule", StringFieldsThatAreActuallyDatesRuleTest.class.getName()); + + final ArchUnitMojo mojo = (ArchUnitMojo) mojoRule2.configureMojo(archUnitMojo, pluginConfiguration); + mojo.execute(); + } + + private PlexusConfiguration createPluginConfiguration(final MojoRule mojoRule, final String pom) throws Exception + { + return mojoRule.extractPluginConfiguration("arch-unit-maven-plugin", Xpp3DomBuilder + .build(new StringReader(pom))); + } +} diff --git a/src/test/java/com/societegenerale/commons/plugin/maven/ArchUnitMojoTest.java b/src/test/java/com/societegenerale/commons/plugin/maven/ArchUnitMojoTest.java index 81e3e90..be9e0a4 100644 --- a/src/test/java/com/societegenerale/commons/plugin/maven/ArchUnitMojoTest.java +++ b/src/test/java/com/societegenerale/commons/plugin/maven/ArchUnitMojoTest.java @@ -2,11 +2,7 @@ import java.io.File; import java.io.StringReader; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import com.google.common.collect.ImmutableSet; import com.societegenerale.aut.test.TestClassWithPowerMock; import com.societegenerale.commons.plugin.rules.MyCustomAndDummyRules; import com.societegenerale.commons.plugin.rules.NoPowerMockRuleTest; @@ -18,8 +14,6 @@ import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.testing.MojoRule; import org.apache.maven.project.MavenProject; -import org.assertj.core.api.AbstractThrowableAssert; -import org.assertj.core.api.Condition; import org.codehaus.plexus.configuration.DefaultPlexusConfiguration; import org.codehaus.plexus.configuration.PlexusConfiguration; import org.codehaus.plexus.logging.LoggerManager; @@ -43,7 +37,8 @@ import static org.mockito.Mockito.when; @RunWith(DataProviderRunner.class) -public class ArchUnitMojoTest { +public class ArchUnitMojoTest extends AbstractArchUnitMojoTest +{ @Rule public final MojoRule mojoRule = new MojoRule(); @@ -75,8 +70,7 @@ public class ArchUnitMojoTest { "" + "" + "" + - ""; - // @formatter:on + ""; // @formatter:on @Before public void setUp() throws Exception { @@ -223,16 +217,6 @@ public void shouldNotExecuteConfigurableRule_and_PreConfiguredRule_IfSkipIsTrue( }).doesNotThrowAnyException(); } - private String getBasedir() { - String basedir = System.getProperty("basedir"); - - if (basedir == null) { - basedir = new File("").getAbsolutePath(); - } - - return basedir; - } - @Test public void shouldSkipIfPackagingIsPom() throws Exception { InterceptingLog interceptingLogger = new InterceptingLog( @@ -251,73 +235,4 @@ public void shouldSkipIfPackagingIsPom() throws Exception { assertThat(interceptingLogger.debugLogs).containsExactly("module packaging is 'pom', so skipping execution"); } - private void executeAndExpectViolations( ArchUnitMojo mojo, ExpectedRuleFailure... expectedRuleFailures) { - AbstractThrowableAssert throwableAssert = assertThatThrownBy(mojo::execute); - stream(expectedRuleFailures).forEach(expectedFailure -> { - throwableAssert.hasMessageContaining(String.format("Rule '%s' was violated", expectedFailure.ruleDescription)); - expectedFailure.details.forEach(throwableAssert::hasMessageContaining); - }); - throwableAssert.has(exactNumberOfViolatedRules(expectedRuleFailures.length)); - } - - private Condition exactNumberOfViolatedRules(final int number) { - return new Condition("exactly " + number + " violated rules") { - @Override - public boolean matches(Throwable throwable) { - Matcher matcher = Pattern.compile("Rule '.*' was violated").matcher(throwable.getMessage()); - int numberOfOccurrences = 0; - while (matcher.find()) { - numberOfOccurrences++; - } - return numberOfOccurrences == number; - } - }; - } - - private PlexusConfiguration buildApplyOnBlock(String packageName, String scope) { - - PlexusConfiguration packageNameElement = new DefaultPlexusConfiguration("packageName", packageName); - PlexusConfiguration scopeElement = new DefaultPlexusConfiguration("scope", scope); - PlexusConfiguration applyOnElement = new DefaultPlexusConfiguration("applyOn"); - applyOnElement.addChild(packageNameElement); - applyOnElement.addChild(scopeElement); - - return applyOnElement; - } - - private PlexusConfiguration buildChecksBlock(String... checks) { - PlexusConfiguration checksElement = new DefaultPlexusConfiguration("checks"); - stream(checks).map(c -> new DefaultPlexusConfiguration("check", c)).forEach(checksElement::addChild); - return checksElement; - } - - private static ExpectedRuleFailure.Creator expectRuleFailure(String ruleDescription) { - return new ExpectedRuleFailure.Creator(ruleDescription); - } - - private static class ExpectedRuleFailure { - private final String ruleDescription; - private final Set details; - - private ExpectedRuleFailure(String ruleDescription, Set details) { - this.ruleDescription = ruleDescription; - this.details = details; - } - - private static class Creator { - private final String ruleDescription; - - Creator(String ruleDescription) { - this.ruleDescription = ruleDescription; - } - - ExpectedRuleFailure ofAnyKind() { - return withDetails(); - } - - ExpectedRuleFailure withDetails(String... detals) { - return new ExpectedRuleFailure(ruleDescription, ImmutableSet.copyOf(detals)); - } - } - } } diff --git a/src/test/java/com/societegenerale/commons/plugin/maven/ExcludedPathsPreProcessorTest.java b/src/test/java/com/societegenerale/commons/plugin/maven/ExcludedPathsPreProcessorTest.java new file mode 100644 index 0000000..07a0fd9 --- /dev/null +++ b/src/test/java/com/societegenerale/commons/plugin/maven/ExcludedPathsPreProcessorTest.java @@ -0,0 +1,168 @@ +package com.societegenerale.commons.plugin.maven; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.mockito.junit.MockitoJUnitRunner; + +import static com.societegenerale.commons.plugin.maven.ExcludedPathsPreProcessor.PACKAGE_INFO_JAVA; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@RunWith (MockitoJUnitRunner.class) +public class ExcludedPathsPreProcessorTest extends AbstractExcludePathTest +{ + private final ExcludedPathsPreProcessor preProcessor = new ExcludedPathsPreProcessor(); + + @BeforeClass + public static void init() throws IOException + { + AbstractExcludePathTest.init(); + } + + @AfterClass + public static void cleanup() throws IOException + { + AbstractExcludePathTest.cleanup(); + } + + @Test + public void testFindJavaFiles_EmptyDir() throws IOException + { + final Path emptyDir = + Files.createDirectory(Paths.get(getTestTempRootDirectory().toString() + "/EmptyDirectory")); + + final Set actual = preProcessor.findJavaFiles(emptyDir, getMavenLogger()); + + assertNotNull(actual); + assertTrue(actual.isEmpty()); + } + + @Test + public void testFindJavaFiles() throws IOException + { + final Path notAJavaFilePath = + Files.createFile(Paths.get(getTestTempRootDirectory().toString() + "/NotAJavaFile.txt")); + + final Path packageInfoFilePath = + Files.createFile(Paths.get( + getTestTempRootDirectory().toString() + "/" + PACKAGE_INFO_JAVA)); + + final Set expected = new HashSet<>(); + expected.add(getTempJavaFile()); + expected.add(getTempJavaFileWithDefaultPackage()); + expected.add(AbstractExcludePathTest.getTempJavaFileWithFileComment()); + + final Set actual = preProcessor.findJavaFiles(getTestTempRootDirectory(), getMavenLogger()); + + assertNotNull(actual); + assertFalse(actual.contains(notAJavaFilePath)); + assertFalse(actual.contains(packageInfoFilePath)); + assertEquals(expected.size(), actual.size()); + assertEquals(expected, actual); + } + + @Test + public void testDetermineClassNames_EmptySet() + { + final Set actual = preProcessor.determineClassNames(new HashSet<>(), getMavenLogger()); + assertNotNull(actual); + assertTrue(actual.isEmpty()); + } + + @Test + public void testDetermineClassNames() + { + final Set expected = new HashSet<>(); + expected.add(AbstractExcludePathTest.CLASS_NAME); + expected.add(AbstractExcludePathTest.PACKAGE_NAME + "." + AbstractExcludePathTest.CLASS_NAME); + expected.add(AbstractExcludePathTest.PACKAGE_NAME + "." + AbstractExcludePathTest.CLASS_NAME_WITH_FILE_COMMENT); + + final Set paths = new HashSet<>(); + paths.add(getTempJavaFile()); + paths.add(getTempJavaFileWithDefaultPackage()); + paths.add(AbstractExcludePathTest.getTempJavaFileWithFileComment()); + + final Set actual = preProcessor.determineClassNames(paths, getMavenLogger()); + assertNotNull(actual); + assertEquals(expected, actual); + } + + @Test + public void testConvertToPath() throws IOException + { + final Path targetDir = AbstractExcludePathTest.getTestProjectBuildDirectory(); + + final Path generatedSourcesDir = Files.createDirectory( + Paths.get(targetDir.toString(), ExcludedPathsPreProcessor.GENERATTED_SOURCES)); + + final Path actualGenSrc = preProcessor.convertToPath(getMavenLogger(), targetDir.toString(), + ExcludedPathsPreProcessor.GENERATTED_SOURCES); + assertEquals(generatedSourcesDir, actualGenSrc); + + // Directory outsite of the project + final Path dirOutsiteTarget = + Files.createDirectory(Paths.get(getTestTempRootDirectory().toString(), "outsideDir")); + + final Path actualOutsiteDir = preProcessor.convertToPath(getMavenLogger(), targetDir.toString(), + dirOutsiteTarget.toString()); + assertEquals(dirOutsiteTarget, actualOutsiteDir); + + // path that point a to a file + final Path javaFile = AbstractExcludePathTest.getTempJavaFileWithFileComment(); + final Path actualJavaFile = preProcessor.convertToPath(getMavenLogger(), targetDir.toString(), + javaFile.toString()); + assertEquals(javaFile, actualJavaFile); + } + + @Test + public void testProcessExcludedPaths() + { + assertNotNull(preProcessor.processExcludedPaths(getMavenLogger(), null, null)); + + final Path targetDir = AbstractExcludePathTest.getTestProjectBuildDirectory(); + + //exclude all java files under directory com (...tempDir../target/com/...) + final Set excludes = preProcessor.processExcludedPaths(getMavenLogger(), targetDir.toString(), + Collections.singletonList("com")); + + assertNotNull(excludes); + assertEquals(3, excludes.size()); + assertTrue(excludes.contains("com")); + assertTrue(excludes.contains(AbstractExcludePathTest.PACKAGE_NAME + "." + AbstractExcludePathTest.CLASS_NAME)); + assertTrue(excludes.contains( + AbstractExcludePathTest.PACKAGE_NAME + "." + AbstractExcludePathTest.CLASS_NAME_WITH_FILE_COMMENT)); + } + + @Test + public void testIsJavaFile() + { + assertFalse(preProcessor.isJavaFile(null)); + assertFalse(preProcessor.isJavaFile("")); + assertFalse(preProcessor.isJavaFile("abc")); + + assertFalse(preProcessor.isJavaFile("c:\\" + PACKAGE_INFO_JAVA.toUpperCase())); + assertFalse(preProcessor.isJavaFile("c:\\" + PACKAGE_INFO_JAVA.toLowerCase())); + assertFalse(preProcessor.isJavaFile("c:\\" + PACKAGE_INFO_JAVA + " ")); + assertFalse(preProcessor.isJavaFile("c:\\" + PACKAGE_INFO_JAVA)); + + final String pathString = "c:\\MyJavaFile.java"; + assertTrue(preProcessor.isJavaFile(pathString.toUpperCase())); + assertTrue(preProcessor.isJavaFile(pathString.toLowerCase())); + assertTrue(preProcessor.isJavaFile(pathString + " ")); + assertTrue(preProcessor.isJavaFile(pathString)); + + } +} \ No newline at end of file diff --git a/src/test/java/com/societegenerale/commons/plugin/maven/JavaFileParserTest.java b/src/test/java/com/societegenerale/commons/plugin/maven/JavaFileParserTest.java new file mode 100644 index 0000000..f54791d --- /dev/null +++ b/src/test/java/com/societegenerale/commons/plugin/maven/JavaFileParserTest.java @@ -0,0 +1,143 @@ +package com.societegenerale.commons.plugin.maven; + +import java.io.IOException; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import com.societegenerale.commons.plugin.maven.JavaFileParser.JavaFile; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +@RunWith (MockitoJUnitRunner.class) +public class JavaFileParserTest extends AbstractExcludePathTest +{ + private final JavaFileParser javaFileParser = new JavaFileParser(); + + @BeforeClass + public static void init() throws IOException + { + AbstractExcludePathTest.init(); + } + + @AfterClass + public static void cleanup() throws IOException + { + AbstractExcludePathTest.cleanup(); + } + + @Test + public void testParse() throws IOException + { + final JavaFile fileWithPackage = javaFileParser.parse(getTempJavaFile(), getMavenLogger()); + assertEquals(AbstractExcludePathTest.CLASS_NAME, fileWithPackage.getClassName()); + assertEquals(AbstractExcludePathTest.PACKAGE_NAME, fileWithPackage.getPackageString()); + + final JavaFile fileWithoutPackage = javaFileParser.parse(getTempJavaFileWithDefaultPackage(), getMavenLogger()); + assertEquals(AbstractExcludePathTest.CLASS_NAME, fileWithoutPackage.getClassName()); + assertNull(fileWithoutPackage.getPackageString()); + + final JavaFile fileWithFileComment = javaFileParser.parse( + AbstractExcludePathTest.getTempJavaFileWithFileComment(), getMavenLogger()); + assertEquals(AbstractExcludePathTest.CLASS_NAME_WITH_FILE_COMMENT, fileWithFileComment.getClassName()); + assertEquals(AbstractExcludePathTest.PACKAGE_NAME, fileWithFileComment.getPackageString()); + } + + @Test + public void testExtractPackage_FileContentNull() + { + javaFileParser.extractPackage(null, getMavenLogger()); + } + + @Test + public void testExtractPackage_FileContentEmpty() + { + javaFileParser.extractPackage("", getMavenLogger()); + } + + @Test + public void testExtractPackage_DefaultPackage() + { + final String defaultPackage = javaFileParser.extractPackage( + AbstractExcludePathTest.CONTENT_WITH_DEFAULT_PACKAGE, getMavenLogger()); + assertNull(defaultPackage); + } + + @Test + public void testExtractPackage() + { + final String extractPackage = javaFileParser.extractPackage(AbstractExcludePathTest.CONTENT_WITH_PACKAGE, + getMavenLogger()); + assertEquals(AbstractExcludePathTest.PACKAGE_NAME, extractPackage); + + final String defaultPackage = javaFileParser.extractPackage( + AbstractExcludePathTest.CONTENT_WITH_DEFAULT_PACKAGE, getMavenLogger()); + assertNull(defaultPackage); + } + + @Test + public void testReadFile() throws IOException + { + final String fileContentDefaultPackage = javaFileParser.readFile(getTempJavaFileWithDefaultPackage()); + assertEquals(removeLineBreaks(AbstractExcludePathTest.CONTENT_WITH_DEFAULT_PACKAGE), fileContentDefaultPackage); + + final String fileContent = javaFileParser.readFile(getTempJavaFile()); + assertEquals(removeLineBreaks(AbstractExcludePathTest.CONTENT_WITH_PACKAGE), fileContent); + + final String fileContentWithFileComment = javaFileParser.readFile( + AbstractExcludePathTest.getTempJavaFileWithFileComment()); + assertEquals(removeLineBreaks(AbstractExcludePathTest.CONTENT_WITH_FILE_COMMENT_AND_PACKAGE), + fileContentWithFileComment); + } + + @Test + public void testExtractClassName() + { + final String className1 = javaFileParser.extractClassName(AbstractExcludePathTest.CONTENT_WITH_PACKAGE, + getMavenLogger()); + assertEquals(AbstractExcludePathTest.CLASS_NAME, className1); + + final String className2 = javaFileParser.extractClassName(AbstractExcludePathTest.CONTENT_WITH_DEFAULT_PACKAGE, + getMavenLogger()); + assertEquals(AbstractExcludePathTest.CLASS_NAME, className2); + + final String className3 = javaFileParser.extractClassName( + AbstractExcludePathTest.CONTENT_WITH_FILE_COMMENT_AND_PACKAGE, + getMavenLogger()); + assertEquals(AbstractExcludePathTest.CLASS_NAME_WITH_FILE_COMMENT, className3); + } + + @Test + public void testRemoveComments() + { + final String blockComment = "/**\n\r" + + "* block comment for class foo\n\r" + + " */"; + + final String onelineBlockcomment = "/** one line block comment for class foo */"; + final String lineComment = "// line comment for class foo \n"; + + final String expected = "bar"; + + assertTrue(javaFileParser.removeComments(lineComment).isEmpty()); + assertEquals(expected, javaFileParser.removeComments(lineComment + expected)); + assertEquals(expected, javaFileParser.removeComments(lineComment + expected + lineComment)); + + assertEquals(expected, javaFileParser.removeComments(onelineBlockcomment + expected)); + assertEquals(expected, javaFileParser.removeComments(onelineBlockcomment + expected + onelineBlockcomment)); + + assertEquals(expected, javaFileParser.removeComments(blockComment + expected)); + assertEquals(expected, javaFileParser.removeComments(blockComment + expected + blockComment)); + + } + + private String removeLineBreaks(final String s) + { + return s.replaceAll(JavaFileParser.REGEX_LINE_BREAKS, ""); + } +} \ No newline at end of file diff --git a/src/test/resources/generate/book.xsd b/src/test/resources/generate/book.xsd new file mode 100644 index 0000000..c9b20ac --- /dev/null +++ b/src/test/resources/generate/book.xsd @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file