diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..c861cca --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at joel.costigliola@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/README.md b/README.md index d18a6fa..4000ff3 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The Assertions Generator can create specific assertions for your classes. It comes with : * a CLI tool (this project) -* a [**maven plugin**](https://github.com/assertj/assertj-assertions-generator-maven-plugin). +* a [**maven plugin**](https://github.com/assertj/assertj-generator/tree/main/assertj-generator-maven-plugin). * a [**gradle plugin**](https://github.com/assertj/assertj-generator-gradle-plugin). Let's say you have a `Player` class with `name` and `team` properties. The generator will create a `PlayerAssert` assertions class with `hasName` and `hasTeam` assertions. This allows you to write : diff --git a/assertj-generator-maven-plugin/README.md b/assertj-generator-maven-plugin/README.md new file mode 100644 index 0000000..b2a2de7 --- /dev/null +++ b/assertj-generator-maven-plugin/README.md @@ -0,0 +1,60 @@ +Maven plugin to generate AssertJ assertions +== + +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.assertj/assertj-assertions-generator-maven-plugin/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.assertj/assertj-assertions-generator-maven-plugin) + +## Overview + +This plugin can generate [AssertJ assertions](https://github.com/joel-costigliola/assertj-core) for your own classes via maven (it is based on [assertj-assertions-generator](https://github.com/joel-costigliola/assertj-assertions-generator)). + +Let's say that you have a `Player` class with `name` and `team` attributes, the plugin is able to create a `PlayerAssert` assertions class with `hasName` and `hasTeam` assertions, to write code like : + +```java +assertThat(mvp).hasName("Lebron James").hasTeam("Miami Heat"); +``` + +The plugin can be launched with command `mvn generate-test-sources` (or simply `mvn test`) or with any IDE that supports maven. +By default, it generates the assertions source files in `target/generated-test-sources/assertions` as per maven convention (but this can be changed - see below). + + +**Example of plugin execution:** + +``` +==================================== +AssertJ assertions generation report +==================================== + +--- Generator input parameters --- + +Generating AssertJ assertions for classes in following packages and subpackages: +- org.assertj.examples.data + +--- Generator results --- + +Directory where custom assertions files have been generated : +- /home/joe/assertj/assertj-examples/target/generated-test-sources/assertj-assertions + +Custom assertions files generated : +- TeamAssert.java +- BasketBallPlayerAssert.java +- EmployeeAssert.java +- NameAssert.java +- MagicalAssert.java +- PersonAssert.java +- RaceAssert.java +- GameServiceAssert.java +- MansionAssert.java +- TitleAssert.java +- AlignmentAssert.java +- TolkienCharacterAssert.java +- RingAssert.java +- MovieAssert.java +- TeamManagerAssert.java + +Assertions entry point class has been generated in file: +- /home/joe/assertj/assertj-examples/target/generated-test-sources/assertj-assertions/org/assertj/examples/data/Assertions.java +``` + +## Documentation + +Please have a look at the complete documentation in [**assertj.org assertions generator section**](http://joel-costigliola.github.io/assertj/assertj-assertions-generator-maven-plugin.html), including a [**quickstart guide**](http://joel-costigliola.github.io/assertj/assertj-assertions-generator-maven-plugin.html#quickstart). diff --git a/assertj-generator-maven-plugin/javadoc-theme/assertj-theme.css b/assertj-generator-maven-plugin/javadoc-theme/assertj-theme.css new file mode 100644 index 0000000..ca3f1de --- /dev/null +++ b/assertj-generator-maven-plugin/javadoc-theme/assertj-theme.css @@ -0,0 +1,39 @@ +:root { + /* body, block and code fonts */ + --body-font-family: Verdana, Geneva, sans-serif; + --block-font-family: Verdana, Geneva, serif; + /* Text colors for body and block elements */ + --body-text-color: #000000; + --block-text-color: #000000; + /* Background colors for various elements */ + --body-background-color: #edd9a6; + --detail-background-color: #edd9a6; + /* Colors for navigation bar and table captions */ + --navbar-background-color: #232323; + --navbar-text-color: #edd9a6; + /* Background color for subnavigation and various headers */ + --subnav-background-color: #e5c880; + --subnav-link-color: var(--link-color); + /* Background and text colors for selected tabs and navigation items */ + --selected-background-color: #e5c880; + --selected-text-color: #000000; + --selected-link-color: #861203; + /* Background colors for generated tables */ + --table-header-color: #e7ce8e; + --even-row-color: #edd9a6; + --odd-row-color: #e9d195; + /* Text color for page title */ + --title-color: #000000; + /* Text colors for links */ + --link-color: #861203; + --link-color-active: #641003; + /* Table of contents */ + --toc-background-color: var(--body-background-color); + --toc-hover-color: #e9d195; + /* Snippet and pre colors */ + --snippet-background-color: var(--body-background-color); + /* Border colors for structural elements and user defined tables */ + --border-color: #e5c880; + /* Search input colors */ + --search-input-background-color: #edd9a6; +} diff --git a/assertj-generator-maven-plugin/javadoc-theme/hljs-theme.css b/assertj-generator-maven-plugin/javadoc-theme/hljs-theme.css new file mode 100644 index 0000000..1e2e177 --- /dev/null +++ b/assertj-generator-maven-plugin/javadoc-theme/hljs-theme.css @@ -0,0 +1,108 @@ +/* + +Railscasts-like style (c) Visoft, Inc. (Damien White) + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #232323; + color: #e6e1dc; + /* added by us */ + border-radius:8px; +} + +.hljs-comment, +.hljs-quote { + color: #bc9458; + font-style: italic; +} + +.hljs-keyword, +.hljs-selector-tag { + color: #c26230; +} + +.hljs-string, +.hljs-number, +.hljs-regexp, +.hljs-variable, +.hljs-template-variable { + color: #a5c261; +} + +.hljs-subst { + color: #519f50; +} + +.hljs-tag, +.hljs-name { + color: #e8bf6a; +} + +.hljs-type { + color: #ffc66d; +} + + +.hljs-symbol, +.hljs-bullet, +.hljs-built_in, +.hljs-builtin-name, +.hljs-attr, +.hljs-link { + color: #6d9cbe; +} + +.hljs-params { + color: #d0d0ff; +} + +.hljs-attribute { + color: #cda869; +} + +.hljs-meta { + color: #9b859d; +} + +.hljs-title, +.hljs-section { + color: #ffc66d; +} + +.hljs-addition { + background-color: #144212; + color: #e6e1dc; + display: inline-block; + width: 100%; +} + +.hljs-deletion { + background-color: #600; + color: #e6e1dc; + display: inline-block; + width: 100%; +} + +.hljs-selector-class { + color: #9b703f; +} + +.hljs-selector-id { + color: #8b98ab; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} + +.hljs-link { + text-decoration: underline; +} diff --git a/assertj-generator-maven-plugin/pom.xml b/assertj-generator-maven-plugin/pom.xml new file mode 100644 index 0000000..7e09361 --- /dev/null +++ b/assertj-generator-maven-plugin/pom.xml @@ -0,0 +1,160 @@ + + + 4.0.0 + + + org.assertj + assertj-generator-build + 3.0.0-SNAPSHOT + + + assertj-generator-maven-plugin + + Maven plugin for AssertJ assertions generator + + + + true + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.7 + 1.7 + + + + maven-plugin-plugin + 3.2 + + assertj + + true + + + + mojo-descriptor + + descriptor + + + + help-goal + + helpmojo + + + + + + + org.jacoco + jacoco-maven-plugin + + + org/assertj/maven/HelpMojo.class + org/assertj/maven/NoLog.class + + + + + + maven-surefire-plugin + + ${argLine} + + + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + org.apache.maven.plugins + maven-plugin-plugin + [3.2,) + + descriptor + helpmojo + + + + + + + + + + + + + + + + + + + org.assertj + assertj-assertions-generator + 2.2.0 + + + org.assertj + assertj-core + [2.0.0, 2.99.0] + + + commons-collections + commons-collections + 3.2.2 + + + commons-io + commons-io + 2.7 + + + + + org.apache.maven + maven-plugin-api + 2.2.1 + + + org.apache.maven + maven-project + 2.2.1 + + + org.apache.maven.plugin-tools + maven-plugin-annotations + 3.2 + provided + + + + + org.mockito + mockito-core + test + + + junit + junit + test + + + + diff --git a/assertj-generator-maven-plugin/src/main/java/org/assertj/assertions/generator/maven/AssertJAssertionsGeneratorMojo.java b/assertj-generator-maven-plugin/src/main/java/org/assertj/assertions/generator/maven/AssertJAssertionsGeneratorMojo.java new file mode 100644 index 0000000..2e469ba --- /dev/null +++ b/assertj-generator-maven-plugin/src/main/java/org/assertj/assertions/generator/maven/AssertJAssertionsGeneratorMojo.java @@ -0,0 +1,317 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * Copyright 2012-2025 the original author or authors. + */ +package org.assertj.assertions.generator.maven; + +import static com.google.common.base.Charsets.UTF_8; +import static java.lang.String.format; +import static org.apache.commons.io.FileUtils.write; +import static org.apache.commons.lang3.ArrayUtils.isEmpty; +import static org.apache.commons.lang3.StringUtils.equalsIgnoreCase; +import static org.apache.commons.lang3.StringUtils.isEmpty; +import static org.apache.maven.plugins.annotations.LifecyclePhase.GENERATE_TEST_SOURCES; +import static org.apache.maven.plugins.annotations.ResolutionScope.TEST; +import static org.assertj.assertions.generator.AssertionsEntryPointType.*; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +import org.apache.maven.artifact.DependencyResolutionRequiredException; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugin.logging.Log; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; +import org.assertj.core.util.VisibleForTesting; +import org.codehaus.plexus.util.FileUtils; + +/** + * Generates custom AssertJ assertions (*Assert) for all given classes and classes of given packages. + */ +@Mojo(name = "generate-assertions", defaultPhase = GENERATE_TEST_SOURCES, requiresDependencyResolution = TEST, requiresProject = true) +public class AssertJAssertionsGeneratorMojo extends AbstractMojo { + + private static final String[] INCLUDE_ALL_CLASSES = { ".*" }; + + private static final NoLog NO_LOG = new NoLog(); + /** + * Current maven project + */ + @Parameter(property = "project", required = true, readonly = true) + public MavenProject project; + + /** + * Destination dir to store generated assertion source files.
+ * Defaults to 'target/generated-test-sources/assertj-assertions'.
+ * Your IDE should be able to pick up files from this location as sources automatically when generated. + */ + @Parameter(defaultValue = "${project.build.directory}/generated-test-sources/assertj-assertions", property = "assertj.targetDir") + public String targetDir; + + /** + *

Package where generated assertion classes will reside.

+ *

+ * If not set (or set to empty), each assertion class is generated in the package of the corresponding class to assert. + * For example the generated assertion class for com.nba.Player will be com.nba.PlayerAssert (in the same package as Player). + * Defaults to ''. + *

+ *

Note that the Assertions entry point classes package is controlled by the entryPointClassPackage property.

+ */ + @Parameter(defaultValue = "", property = "assertj.generateAssertionsInPackage") + public String generateAssertionsInPackage; + + /** + * Flag specifying whether to clean the directory where assertions are generated. The default is false. + */ + @Parameter(defaultValue = "false", property = "assertj.cleanTargetDir") + public boolean cleanTargetDir; + + /** + * The scope of generates sources ('test' or 'compile') to be added to the maven build.
+ * Expected to be used in conjunction with {@link #targetDir}, for example: + *
{@code
+   *  ${project.build.directory}/generated-sources/assertj-assertions
+   *  compile
+   * }
+ * Defaults to 'test'.
+ */ + @Parameter(defaultValue = "test", property = "assertj.generatedSourcesScope") + public String generatedSourcesScope; + + /** + * List of packages to generate assertions for. + */ + @Parameter(property = "assertj.packages") + public String[] packages; + + /** + * List of classes to generate assertions for. + */ + @Parameter(property = "assertj.classes") + public String[] classes; + + /** + * Generated assertions are limited to classes matching one of the given regular expressions, default is to include + * all classes. + */ + @Parameter(property = "assertj.includes") + public String[] includes = INCLUDE_ALL_CLASSES; + + /** + * If class matches one of the given regex, no assertions will be generated for it, default is not to exclude + * anything. + */ + @Parameter(property = "assertj.excludes") + public String[] excludes = new String[0]; + + /** + * Flag specifying whether to generate hierarchical assertions. The default is false. + */ + @Parameter(defaultValue = "true", property = "assertj.hierarchical") + public boolean hierarchical; + + /** + * Flag specifying whether to generate assertions for all fields (including non public ones). The default is false. + */ + @Parameter(defaultValue = "false", property = "assertj.generateAssertionsForAllFields") + public boolean generateAssertionsForAllFields; + + /** + * An optional package name for the Assertions entry point class. If omitted, the package will be determined + * heuristically from the generated assertions. + */ + @Parameter(property = "assertj.entryPointClassPackage") + public String entryPointClassPackage; + + /** + * Skip generating classes, handy way to disable the plugin. + */ + @Parameter(property = "assertj.skip") + public boolean skip = false; + + /** + * Generate Assertions entry point class. + */ + @Parameter(property = "assertj.generate.Assertions") + public boolean generateAssertions = true; + + /** + * Generate generating BDD Assertions entry point class. + */ + @Parameter(property = "assertj.generate.BddAssertions") + public boolean generateBddAssertions = true; + + /** + * Generate generating JUnit Soft Assertions entry point class. + */ + @Parameter(property = "assertj.generate.JUnitSoftAssertions") + public boolean generateJUnitSoftAssertions = true; + + /** + * Generate generating Soft Assertions entry point class. + */ + @Parameter(property = "assertj.generate.SoftAssertions") + public boolean generateSoftAssertions = true; + + /** + * Do not log anything if true, false by default. + */ + @Parameter(property = "assertj.quiet") + public boolean quiet = false; + + /** + * The generated assertions report is written to the given file, if given a relative path the root path is where the plugin is executed. + */ + @Parameter(property = "assertj.writeReportInFile") + public String writeReportInFile; + + /** + * Generate generating Soft Assertions entry point class. + */ + @Parameter(property = "assertj.templates") + public Templates templates; + + /** + * Generate assertions for package private classes if true + */ + @Parameter(property = "assertj.includePackagePrivateClasses") + public boolean includePackagePrivateClasses = false; + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + if (skip) { + getLog().info("Assertions generator is disabled as 'skip' option is true."); + return; + } + failIfMojoParametersAreMissing(); + try { + ClassLoader projectClassLoader = getProjectClassLoader(); + AssertionsGenerator assertionGenerator = new AssertionsGenerator(projectClassLoader); + assertionGenerator.generateAssertionsForAllFields(this.generateAssertionsForAllFields); + assertionGenerator.setIncludePatterns(includes); + assertionGenerator.setExcludePatterns(excludes); + if (generateAssertions) assertionGenerator.enableEntryPointClassesGenerationFor(STANDARD); + if (generateBddAssertions) assertionGenerator.enableEntryPointClassesGenerationFor(BDD); + if (generateSoftAssertions) assertionGenerator.enableEntryPointClassesGenerationFor(SOFT); + if (generateJUnitSoftAssertions) { + if (junitFoundBy(projectClassLoader)) assertionGenerator.enableEntryPointClassesGenerationFor(JUNIT_SOFT); + else + getLog().info("JUnit not found in project classpath => JUnitSoftAssertions entry point class won't be generated."); + } + assertionGenerator.setLog(getLog()); + if (generateAssertionsInPackage != null) { + // user has set generateAssertionsInPackage (not that maven converts empty string param to null) + assertionGenerator.setGeneratedAssertionsPackage(generateAssertionsInPackage); + } + if (cleanTargetDir) cleanPreviouslyGeneratedSources(); + executeWithAssertionGenerator(assertionGenerator); + } catch (Exception e) { + throw new MojoExecutionException(e.getMessage(), e); + } + } + + @Override + public Log getLog() { + return quiet ? NO_LOG : super.getLog(); + } + + private void cleanPreviouslyGeneratedSources() { + try { + Path targetDirPath = Paths.get(targetDir); + if (Files.exists(targetDirPath) && targetDirPath.toFile().list().length > 0) { + getLog().info("Removing previously generated sources in " + targetDir); + FileUtils.cleanDirectory(targetDirPath.toFile()); + } + } catch (IOException e) { + getLog().warn("Fail to remove previously generated sources in " + targetDir, e); + } + } + + @VisibleForTesting + AssertionsGeneratorReport executeWithAssertionGenerator(AssertionsGenerator assertionGenerator) { + if (classes == null) classes = new String[0]; + AssertionsGeneratorReport generatorReport = assertionGenerator.generateAssertionsFor(packages, classes, targetDir, + entryPointClassPackage, hierarchical, + templates, includePackagePrivateClasses); + printReport(generatorReport); + if (isEmpty(generatedSourcesScope) || equalsIgnoreCase("test", generatedSourcesScope)) project.addTestCompileSourceRoot(targetDir); + else if (equalsIgnoreCase("compile", generatedSourcesScope)) project.addCompileSourceRoot(targetDir); + else getLog().warn(format("Unknown generated sources scope '%s' - no sources added to project", generatedSourcesScope)); + return generatorReport; + } + + private void printReport(AssertionsGeneratorReport assertionsGeneratorReport) { + String reportContent = assertionsGeneratorReport.getReportContent(); + if (shouldWriteReportInFile()) { + getLog().info("Writing the assertions generator report in file: " + writeReportInFile); + writeReportInFile(reportContent); + } else { + getLog().info(reportContent); + } + } + + private void writeReportInFile(String reportContent) { + try { + write(new File(writeReportInFile), reportContent, UTF_8); + } catch (IOException e) { + getLog().warn("Failed to write the assertions generation assertionsGeneratorReport in file " + + writeReportInFile, e); + } + } + + private boolean shouldWriteReportInFile() { + return writeReportInFile != null; + } + + private void failIfMojoParametersAreMissing() throws MojoFailureException { + if (isEmpty(packages) && isEmpty(classes)) { + throw new MojoFailureException(shouldHaveNonEmptyPackagesOrClasses()); + } + } + + @SuppressWarnings("unchecked") + private ClassLoader getProjectClassLoader() throws DependencyResolutionRequiredException, MalformedURLException { + List classpathElements = new ArrayList(project.getCompileClasspathElements()); + classpathElements.addAll(project.getTestClasspathElements()); + List classpathElementUrls = new ArrayList<>(classpathElements.size()); + for (String classpathElement : classpathElements) { + classpathElementUrls.add(new File(classpathElement).toURI().toURL()); + } + return new URLClassLoader(classpathElementUrls.toArray(new URL[0]), Thread.currentThread().getContextClassLoader()); + } + + @VisibleForTesting + static String shouldHaveNonEmptyPackagesOrClasses() { + return format( + "Parameter 'packages' or 'classes' must be set to generate assertions.%n[Help] https://github.com/joel-costigliola/assertj-assertions-generator-maven-plugin"); + } + + private boolean junitFoundBy(ClassLoader projectClassLoader) { + try { + Class.forName("org.junit.Rule", false, projectClassLoader); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + +} diff --git a/assertj-generator-maven-plugin/src/main/java/org/assertj/assertions/generator/maven/AssertionsGenerator.java b/assertj-generator-maven-plugin/src/main/java/org/assertj/assertions/generator/maven/AssertionsGenerator.java new file mode 100644 index 0000000..39ece2d --- /dev/null +++ b/assertj-generator-maven-plugin/src/main/java/org/assertj/assertions/generator/maven/AssertionsGenerator.java @@ -0,0 +1,204 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * Copyright 2012-2025 the original author or authors. + */ +package org.assertj.assertions.generator.maven; + +import static com.google.common.collect.Sets.newLinkedHashSet; +import static org.apache.commons.collections.CollectionUtils.subtract; +import static org.apache.commons.lang3.ArrayUtils.addAll; +import static org.assertj.assertions.generator.util.ClassUtil.collectClasses; +import static org.assertj.core.util.Arrays.isNullOrEmpty; +import static org.assertj.core.util.Sets.newHashSet; + +import java.io.File; +import java.io.IOException; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.regex.Pattern; + +import com.google.common.reflect.TypeToken; +import org.apache.maven.plugin.logging.Log; +import org.assertj.assertions.generator.AssertionsEntryPointType; +import org.assertj.assertions.generator.BaseAssertionGenerator; +import org.assertj.assertions.generator.Template; +import org.assertj.assertions.generator.description.ClassDescription; +import org.assertj.assertions.generator.description.converter.ClassToClassDescriptionConverter; +import org.assertj.core.util.VisibleForTesting; + +/** + * Is able to generate AssertJ assertions classes from packages. + */ +public class AssertionsGenerator { + + private static final Pattern INCLUDE_EVERYTHING = Pattern.compile(".*"); + private ClassToClassDescriptionConverter converter; + private ClassLoader classLoader; + private BaseAssertionGenerator generator; + private Pattern[] includePatterns; + private Pattern[] excludePatterns; + private Log log; + private Set assertionsEntryPointToGenerate; + + public AssertionsGenerator(ClassLoader classLoader) throws IOException { + this.generator = new BaseAssertionGenerator(); + this.converter = new ClassToClassDescriptionConverter(); + this.classLoader = classLoader; + this.includePatterns = new Pattern[] { INCLUDE_EVERYTHING }; + this.excludePatterns = new Pattern[0]; + this.assertionsEntryPointToGenerate = newHashSet(); + } + + public void setIncludePatterns(String[] includeRegexs) { + if (isNullOrEmpty(includeRegexs)) { + includePatterns = new Pattern[] { INCLUDE_EVERYTHING }; + return; + } + includePatterns = new Pattern[includeRegexs.length]; + for (int i = 0; i < includeRegexs.length; i++) { + includePatterns[i] = Pattern.compile(includeRegexs[i]); + } + } + + public void setExcludePatterns(String[] excludeRegexs) { + if (isNullOrEmpty(excludeRegexs)) { + return; + } + excludePatterns = new Pattern[excludeRegexs.length]; + for (int i = 0; i < excludeRegexs.length; i++) { + excludePatterns[i] = Pattern.compile(excludeRegexs[i]); + } + } + + /** + * Generates custom assertions for classes in given packages with the Assertions class entry point in given + * destination dir. + * + * @param inputPackages the packages containing the classes we want to generate Assert classes for. + * @param inputClassNames the packages containing the classes we want to generate Assert classes for. + * @param destDir the base directory where the classes are going to be generated. + * @param entryPointFilePackage the package of the assertions entry point class, may be null. + * @param includePackagePrivateClasses collect package private classes if true. + */ + @SuppressWarnings("unchecked") + public AssertionsGeneratorReport generateAssertionsFor(String[] inputPackages, String[] inputClassNames, + String destDir, String entryPointFilePackage, boolean hierarchical, + Templates userTemplates, boolean includePackagePrivateClasses) { + generator.setDirectoryWhereAssertionFilesAreGenerated(new File(destDir)); + AssertionsGeneratorReport report = new AssertionsGeneratorReport(); + report.setDirectoryPathWhereAssertionFilesAreGenerated(destDir); + registerUserTemplates(userTemplates, report); + Set classDescriptions = new HashSet<>(); + report.setInputPackages(inputPackages); + report.setInputClasses(inputClassNames); + try { + Set> classes = collectClasses(classLoader, includePackagePrivateClasses, addAll(inputPackages, inputClassNames)); + report.reportInputClassesNotFound(classes, inputClassNames); + Set> filteredClasses = removeAssertClasses(classes); + removeClassesAccordingToIncludeAndExcludePatterns(filteredClasses); + report.setExcludedClassesFromAssertionGeneration(subtract(classes, filteredClasses)); + if (hierarchical) { + for (TypeToken clazz : filteredClasses) { + ClassDescription classDescription = converter.convertToClassDescription(clazz); + File[] generatedCustomAssertionFiles = generator.generateHierarchicalCustomAssertionFor(classDescription, + filteredClasses); + report.addGeneratedAssertionFile(generatedCustomAssertionFiles[0]); + report.addGeneratedAssertionFile(generatedCustomAssertionFiles[1]); + classDescriptions.add(classDescription); + } + } else { + for (TypeToken clazz : filteredClasses) { + ClassDescription classDescription = converter.convertToClassDescription(clazz); + File generatedCustomAssertionFile = generator.generateCustomAssertionFor(classDescription); + report.addGeneratedAssertionFile(generatedCustomAssertionFile); + classDescriptions.add(classDescription); + } + } + + for (AssertionsEntryPointType assertionsEntryPointType : assertionsEntryPointToGenerate) { + File assertionsEntryPointFile = generator.generateAssertionsEntryPointClassFor(classDescriptions, + assertionsEntryPointType, + entryPointFilePackage); + report.reportEntryPointGeneration(assertionsEntryPointType, assertionsEntryPointFile); + } + } catch (Exception e) { + report.setException(e); + } + return report; + } + + private void registerUserTemplates(Templates userTemplates, AssertionsGeneratorReport report) { + if (userTemplates == null) return; + for (Template template : userTemplates.getTemplates(report)) { + generator.register(template); + } + } + + private void removeClassesAccordingToIncludeAndExcludePatterns(Set> filteredClasses) { + for (Iterator> it = filteredClasses.iterator(); it.hasNext();) { + TypeToken element = it.next(); + if (!isIncluded(element) || isExcluded(element)) it.remove(); + } + } + + private boolean isIncluded(TypeToken element) { + String className = element.getRawType().getName(); + for (Pattern includePattern : includePatterns) { + if (includePattern.matcher(className).matches()) return true; + } + log.debug("Won't generate assertions for " + className + " as it does not match any include regex."); + return false; + } + + private boolean isExcluded(TypeToken element) { + String className = element.getRawType().getName(); + for (Pattern excludePattern : excludePatterns) { + if (excludePattern.matcher(className).matches()) { + log.debug("Won't generate assertions for " + className + " as it matches exclude regex : " + excludePattern); + return true; + } + } + return false; + } + + private Set> removeAssertClasses(Set> classList) { + Set> filteredClassList = newLinkedHashSet(); + for (TypeToken clazz : classList) { + String classSimpleName = clazz.getRawType().getSimpleName(); + if (!classSimpleName.endsWith("Assert") && !classSimpleName.endsWith("Assertions")) { + filteredClassList.add(clazz); + } + } + return filteredClassList; + } + + @VisibleForTesting + public void setBaseGenerator(BaseAssertionGenerator generator) { + this.generator = generator; + } + + public void setLog(Log log) { + this.log = log; + } + + public void enableEntryPointClassesGenerationFor(AssertionsEntryPointType type) { + this.assertionsEntryPointToGenerate.add(type); + } + + public void generateAssertionsForAllFields(boolean generateAssertionsForAllFields) { + this.generator.setGenerateAssertionsForAllFields(generateAssertionsForAllFields); + } + + public void setGeneratedAssertionsPackage(String generateAssertionsInPackage) { + this.generator.setGeneratedAssertionsPackage(generateAssertionsInPackage); + } +} diff --git a/assertj-generator-maven-plugin/src/main/java/org/assertj/assertions/generator/maven/AssertionsGeneratorReport.java b/assertj-generator-maven-plugin/src/main/java/org/assertj/assertions/generator/maven/AssertionsGeneratorReport.java new file mode 100644 index 0000000..bf115f8 --- /dev/null +++ b/assertj-generator-maven-plugin/src/main/java/org/assertj/assertions/generator/maven/AssertionsGeneratorReport.java @@ -0,0 +1,238 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * Copyright 2012-2025 the original author or authors. + */ +package org.assertj.assertions.generator.maven; + +import static com.google.common.collect.Maps.newTreeMap; +import static com.google.common.collect.Sets.newTreeSet; +import static org.apache.commons.collections.CollectionUtils.isNotEmpty; +import static org.apache.commons.lang3.ArrayUtils.isNotEmpty; +import static org.apache.commons.lang3.StringUtils.remove; +import static org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.common.reflect.TypeToken; +import org.assertj.assertions.generator.AssertionsEntryPointType; + +public class AssertionsGeneratorReport { + + private static final String INDENT = "- "; + private static final String SECTION_START = "--- "; + private static final String SECTION_END = " ---\n"; + private String directoryPathWhereAssertionFilesAreGenerated; + private Set generatedCustomAssertionFileNames; + private Map assertionsEntryPointFilesByType; + private String[] inputPackages; + private String[] inputClasses; + private Exception exception; + private Collection> excludedClassesFromAssertionGeneration; + private Set inputClassesNotFound; + private List userTemplates; + + public AssertionsGeneratorReport() { + assertionsEntryPointFilesByType = newTreeMap(); + generatedCustomAssertionFileNames = newTreeSet(); + inputClassesNotFound = newTreeSet(); + directoryPathWhereAssertionFilesAreGenerated = "no directory set"; + userTemplates = new ArrayList<>(); + } + + public void setDirectoryPathWhereAssertionFilesAreGenerated(String directory) { + this.directoryPathWhereAssertionFilesAreGenerated = directory; + } + + public void addGeneratedAssertionFile(File generatedCustomAssertionFile) throws IOException { + generatedCustomAssertionFileNames.add(generatedCustomAssertionFile.getCanonicalPath()); + } + + public String getReportContent() { + StringBuilder reportBuilder = new StringBuilder(System.lineSeparator()); + reportBuilder.append(System.lineSeparator()); + reportBuilder.append("====================================\n"); + reportBuilder.append("AssertJ assertions generation report\n"); + reportBuilder.append("====================================\n"); + buildGeneratorParametersReport(reportBuilder); + reportBuilder.append(System.lineSeparator()); + reportBuilder.append(SECTION_START).append("Generator results").append(SECTION_END); + if (generationError()) { + buildGeneratorReportError(reportBuilder); + } else if (nothingGenerated()) { + buildGeneratorReportWhenNothingWasGenerated(reportBuilder); + } else { + buildGeneratorReportSuccess(reportBuilder); + } + return reportBuilder.toString(); + } + + private void buildGeneratorReportSuccess(StringBuilder reportBuilder) { + reportBuilder.append(System.lineSeparator()); + reportBuilder.append("Directory where custom assertions files have been generated:\n"); + reportBuilder.append(INDENT).append(directoryPathWhereAssertionFilesAreGenerated).append(System.lineSeparator()); + reportBuilder.append(System.lineSeparator()); + reportBuilder.append("Custom assertions files generated:\n"); + for (String fileName : generatedCustomAssertionFileNames) { + reportBuilder.append(INDENT).append(fileName).append(System.lineSeparator()); + } + if (!inputClassesNotFound.isEmpty()) { + reportBuilder.append(System.lineSeparator()); + reportBuilder.append("No custom assertions files generated for the following input classes as they were not found:\n"); + for (String inputClassNotFound : inputClassesNotFound) { + reportBuilder.append(INDENT).append(inputClassNotFound).append(System.lineSeparator()); + } + } + reportEntryPointClassesGeneration(reportBuilder); + } + + private void reportEntryPointClassesGeneration(StringBuilder reportBuilder) { + for (AssertionsEntryPointType type : assertionsEntryPointFilesByType.keySet()) { + if (assertionsEntryPointFilesByType.get(type) != null) { + String entryPointClassName = remove(type.getFileName(), ".java"); + reportBuilder.append(System.lineSeparator()) + .append(entryPointClassName).append(" entry point class has been generated in file:\n") + .append(INDENT).append(assertionsEntryPointFilesByType.get(type).getAbsolutePath()) + .append(System.lineSeparator()); + } + } + } + + private void buildGeneratorReportWhenNothingWasGenerated(StringBuilder reportBuilder) { + reportBuilder.append(System.lineSeparator()); + reportBuilder.append("No assertions generated as no classes have been found from given classes/packages.\n"); + if (isNotEmpty(inputClasses)) { + reportBuilder.append(INDENT).append("Given classes : ").append(Arrays.toString(inputClasses)); + reportBuilder.append(System.lineSeparator()); + } + if (isNotEmpty(inputPackages)) { + reportBuilder.append(INDENT).append("Given packages : ").append(Arrays.toString(inputPackages)); + reportBuilder.append(System.lineSeparator()); + } + if (isNotEmpty(excludedClassesFromAssertionGeneration)) { + reportBuilder.append(INDENT).append("Excluded classes : ").append(excludedClassesFromAssertionGeneration); + } + } + + private void buildGeneratorReportError(StringBuilder reportBuilder) { + reportBuilder.append(System.lineSeparator()); + reportBuilder.append("Assertions failed with error : ").append(exception.getMessage()); + reportBuilder.append(System.lineSeparator()); + if (isNotEmpty(inputClasses)) { + reportBuilder.append(INDENT).append("Given classes were : ").append(Arrays.toString(inputClasses)); + reportBuilder.append(System.lineSeparator()); + } + if (isNotEmpty(inputPackages)) { + reportBuilder.append(INDENT).append("Given packages were : ").append(Arrays.toString(inputPackages)); + reportBuilder.append(System.lineSeparator()); + } + reportBuilder.append(System.lineSeparator()); + reportBuilder.append("Full error stack : ").append(getStackTrace(exception)); + } + + private void buildGeneratorParametersReport(StringBuilder reportBuilder) { + reportBuilder.append(System.lineSeparator()); + reportBuilder.append(SECTION_START).append("Generator input parameters").append(SECTION_END) + .append(System.lineSeparator()); + if (isNotEmpty(userTemplates)) { + reportBuilder.append("The following templates will replace the ones provided by AssertJ when generating AssertJ assertions :\n"); + for (String inputPackage : userTemplates) { + reportBuilder.append(INDENT).append(inputPackage).append(System.lineSeparator()); + } + reportBuilder.append(System.lineSeparator()); + } + if (isNotEmpty(inputPackages)) { + reportBuilder.append("Generating AssertJ assertions for classes in following packages and subpackages:\n"); + for (String inputPackage : inputPackages) { + reportBuilder.append(INDENT).append(inputPackage).append(System.lineSeparator()); + } + } + if (isNotEmpty(inputClasses)) { + if (isNotEmpty(inputPackages)) { + reportBuilder.append(System.lineSeparator()); + } + reportBuilder.append("Generating AssertJ assertions for classes:\n"); + for (String inputClass : inputClasses) { + reportBuilder.append(INDENT).append(inputClass).append(System.lineSeparator()); + } + } + if (isNotEmpty(excludedClassesFromAssertionGeneration)) { + reportBuilder.append(System.lineSeparator()); + reportBuilder.append("Input classes excluded from assertions generation:\n"); + for (TypeToken excludedClass : excludedClassesFromAssertionGeneration) { + reportBuilder.append(INDENT).append(excludedClass.getRawType().getName()).append(System.lineSeparator()); + } + } + } + + private boolean generationError() { + return exception != null; + } + + private boolean nothingGenerated() { + return generatedCustomAssertionFileNames.isEmpty(); + } + + public void reportEntryPointGeneration(AssertionsEntryPointType assertionsEntryPointType, + File assertionsEntryPointFile) { + this.assertionsEntryPointFilesByType.put(assertionsEntryPointType, assertionsEntryPointFile); + } + + public void setInputPackages(String[] packages) { + this.inputPackages = packages; + } + + public void setInputClasses(String[] classes) { + this.inputClasses = classes; + } + + public void setException(Exception exception) { + this.exception = exception; + } + + public Exception getReportedException() { + return exception; + } + + public void setExcludedClassesFromAssertionGeneration(Collection> excludedClassSet) { + this.excludedClassesFromAssertionGeneration = excludedClassSet; + } + + public Set getInputClassesNotFound() { + return inputClassesNotFound; + } + + public void reportInputClassesNotFound(Set> classes, String[] inputClassNames) { + Set classesFound = newTreeSet(); + for (TypeToken clazz : classes) { + classesFound.add(clazz.getRawType().getName()); + } + for (String inputClass : inputClassNames) { + if (!classesFound.contains(inputClass)) { + inputClassesNotFound.add(inputClass); + } + } + } + + public void registerUserTemplate(String userTemplateDescription) { + userTemplates.add(userTemplateDescription); + } + + public List getUserTemplates() { + return userTemplates; + } +} diff --git a/assertj-generator-maven-plugin/src/main/java/org/assertj/assertions/generator/maven/NoLog.java b/assertj-generator-maven-plugin/src/main/java/org/assertj/assertions/generator/maven/NoLog.java new file mode 100644 index 0000000..34b2e3a --- /dev/null +++ b/assertj-generator-maven-plugin/src/main/java/org/assertj/assertions/generator/maven/NoLog.java @@ -0,0 +1,97 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * Copyright 2012-2025 the original author or authors. + */ +package org.assertj.assertions.generator.maven; + +import org.apache.maven.plugin.logging.Log; + +public class NoLog implements Log { + @Override + public boolean isDebugEnabled() { + return false; + } + + @Override + public void debug(CharSequence content) { + // do nothing + } + + @Override + public void debug(CharSequence content, Throwable error) { + // do nothing + } + + @Override + public void debug(Throwable error) { + // do nothing + } + + @Override + public boolean isInfoEnabled() { + return false; + } + + @Override + public void info(CharSequence content) { + // do nothing + } + + @Override + public void info(CharSequence content, Throwable error) { + // do nothing + } + + @Override + public void info(Throwable error) { + // do nothing + } + + @Override + public boolean isWarnEnabled() { + return false; + } + + @Override + public void warn(CharSequence content) { + // do nothing + } + + @Override + public void warn(CharSequence content, Throwable error) { + // do nothing + } + + @Override + public void warn(Throwable error) { + // do nothing + } + + @Override + public boolean isErrorEnabled() { + return false; + } + + @Override + public void error(CharSequence content) { + // do nothing + } + + @Override + public void error(CharSequence content, Throwable error) { + // do nothing + } + + @Override + public void error(Throwable error) { + // do nothing + } +} diff --git a/assertj-generator-maven-plugin/src/main/java/org/assertj/assertions/generator/maven/Templates.java b/assertj-generator-maven-plugin/src/main/java/org/assertj/assertions/generator/maven/Templates.java new file mode 100644 index 0000000..bd9d8e0 --- /dev/null +++ b/assertj-generator-maven-plugin/src/main/java/org/assertj/assertions/generator/maven/Templates.java @@ -0,0 +1,127 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * Copyright 2012-2025 the original author or authors. + */ +package org.assertj.assertions.generator.maven; + +import static org.assertj.assertions.generator.Template.Type.ABSTRACT_ASSERT_CLASS; +import static org.assertj.assertions.generator.Template.Type.ASSERTIONS_ENTRY_POINT_CLASS; +import static org.assertj.assertions.generator.Template.Type.ASSERTION_ENTRY_POINT; +import static org.assertj.assertions.generator.Template.Type.ASSERT_CLASS; +import static org.assertj.assertions.generator.Template.Type.BDD_ASSERTIONS_ENTRY_POINT_CLASS; +import static org.assertj.assertions.generator.Template.Type.BDD_ENTRY_POINT_METHOD_ASSERTION; +import static org.assertj.assertions.generator.Template.Type.HAS; +import static org.assertj.assertions.generator.Template.Type.HAS_FOR_ARRAY; +import static org.assertj.assertions.generator.Template.Type.HAS_FOR_CHAR; +import static org.assertj.assertions.generator.Template.Type.HAS_FOR_CHARACTER; +import static org.assertj.assertions.generator.Template.Type.HAS_FOR_ITERABLE; +import static org.assertj.assertions.generator.Template.Type.HAS_FOR_REAL_NUMBER; +import static org.assertj.assertions.generator.Template.Type.HAS_FOR_REAL_NUMBER_WRAPPER; +import static org.assertj.assertions.generator.Template.Type.HAS_FOR_WHOLE_NUMBER; +import static org.assertj.assertions.generator.Template.Type.HAS_FOR_WHOLE_NUMBER_WRAPPER; +import static org.assertj.assertions.generator.Template.Type.HIERARCHICAL_ASSERT_CLASS; +import static org.assertj.assertions.generator.Template.Type.IS; +import static org.assertj.assertions.generator.Template.Type.IS_WRAPPER; +import static org.assertj.assertions.generator.Template.Type.JUNIT_SOFT_ASSERTIONS_ENTRY_POINT_CLASS; +import static org.assertj.assertions.generator.Template.Type.SOFT_ASSERTIONS_ENTRY_POINT_CLASS; +import static org.assertj.assertions.generator.Template.Type.SOFT_ENTRY_POINT_METHOD_ASSERTION; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.CharEncoding; +import org.assertj.assertions.generator.Template; +import org.assertj.core.util.Files; +import org.assertj.core.util.VisibleForTesting; + +public class Templates { + + public String templatesDirectory; + // assertion class templates + public String assertionClass; + public String hierarchicalAssertionConcreteClass; + public String hierarchicalAssertionAbstractClass; + // assertion method templates + public String objectAssertion; + public String booleanAssertion; + public String booleanWrapperAssertion; + public String arrayAssertion; + public String iterableAssertion; + public String charAssertion; + public String characterAssertion; + public String realNumberAssertion; + public String realNumberWrapperAssertion; + public String wholeNumberAssertion; + public String wholeNumberWrapperAssertion; + // entry point templates + public String assertionsEntryPointClass; + public String assertionEntryPointMethod; + public String softEntryPointAssertionClass; + public String junitSoftEntryPointAssertionClass; + public String softEntryPointAssertionMethod; + public String bddEntryPointAssertionClass; + public String bddEntryPointAssertionMethod; + + public List