From 8ee2e86cc16be18e0791d875faf1b923568a4189 Mon Sep 17 00:00:00 2001 From: Nicklas Wallgren Date: Fri, 11 Sep 2026 13:31:12 +0200 Subject: [PATCH] chore: add cq.archunit.compileIfMissing property --- README.md | 33 ++++ .../archunit/ArchUnitAnalyzer.java | 38 ++--- .../configuration/ArchUnitConfiguration.java | 15 ++ .../codequality/step/ArchUnitStep.java | 142 +++++++++++++++++- .../archunit/ArchUnitAnalyzerUnitTest.java | 16 +- .../archunit/ArchUnitStepUnitTest.java | 114 ++++++++++++++ 6 files changed, 319 insertions(+), 39 deletions(-) create mode 100644 src/test/java/io/github/finoid/maven/plugins/codequality/archunit/ArchUnitStepUnitTest.java diff --git a/README.md b/README.md index 581c032..e405ba4 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,7 @@ by surefire and once here. | `rules` | Explicit rule references, see below. | `[]` | | `serviceLoaderEnabled` | Whether rule providers should be discovered from the test classpath. | `true` | | `analyzeTestClasses` | Whether the test classes should be analyzed alongside the main classes. | `false` | +| `compileIfMissing` | Whether to compile the module when it has not been compiled already. | `false` | | `severity` | The severity reported for a rule without an entry in `ruleSeverities`. | `MAJOR` | | `ruleSeverities` | Severity per rule name, overriding `severity`. | `{}` | @@ -241,6 +242,38 @@ META-INF/services/io.github.finoid.maven.plugins.codequality.archunit.ArchRulePr Provider names are chosen by the library, so keep them usable as XML element names if consumers should be able to override their severity. +#### Phase requirement + +The step reads the classes the build has already produced, rather than forking a compiler of its own the way Error +Prone and the Checker Framework do. It therefore has to run at or after `compile`. Bound to an earlier phase, or +invoked on the command line ahead of one, it reports a missing prerequisite and skips: + +``` +[INFO] Step ARCH_UNIT is missing prerequisites to run. Cause: the module has no compiled classes, + the goal has to run at or after the compile phase. Skipping... +``` + +Running the goal directly therefore needs a phase in front of it: + +```shell +mvn clean compile io.github.finoid:codequality-maven-plugin:code-quality@maven-code-quality +``` + +Or let the step compile the module itself: + +```xml + + true + true + +``` + +The compilation goes into `target/archunit-classes`, so it neither overwrites the output of the build nor leads a +later phase to believe the module is already built, and that directory is cleaned before each run. It reproduces the +release level and the annotation processors of the module, which covers a Lombok using service, but not a bespoke +compiler configuration - additional compiler arguments, generated source roots, a module path. Where that matters, +run the goal after the compile phase instead and analyze exactly what the build produced. + #### Where the rules live Both sources load from a jar just as happily as from the module's own classes, so a shared rule library can be wired diff --git a/src/main/java/io/github/finoid/maven/plugins/codequality/archunit/ArchUnitAnalyzer.java b/src/main/java/io/github/finoid/maven/plugins/codequality/archunit/ArchUnitAnalyzer.java index 69e0544..132d25e 100644 --- a/src/main/java/io/github/finoid/maven/plugins/codequality/archunit/ArchUnitAnalyzer.java +++ b/src/main/java/io/github/finoid/maven/plugins/codequality/archunit/ArchUnitAnalyzer.java @@ -11,12 +11,10 @@ import io.github.finoid.maven.plugins.codequality.report.Violation; import io.github.finoid.maven.plugins.codequality.step.ViolationConverter; import io.github.finoid.maven.plugins.codequality.util.Precondition; -import org.apache.maven.project.MavenProject; import javax.inject.Inject; import javax.inject.Singleton; import java.io.File; -import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; @@ -40,18 +38,19 @@ public ArchUnitAnalyzer(final ViolationConverter violationConverter) { /** * Evaluates the given rules. * - * @param rules the rules to evaluate - * @param configuration the step configuration - * @param context the context of the current mojo execution + * @param classDirectories the directories holding the compiled classes to analyze + * @param rules the rules to evaluate + * @param configuration the step configuration + * @param context the context of the current mojo execution * @return the violations found, empty when there is nothing to analyze */ - public List analyze(final List rules, final ArchUnitConfiguration configuration, - final ExecutionContext context) { - final List classDirectories = classDirectoriesOf(context.getProject(), configuration.isAnalyzeTestClasses()); - + public List analyze(final List classDirectories, final List rules, + final ArchUnitConfiguration configuration, final ExecutionContext context) { if (classDirectories.isEmpty()) { + // Normally unreachable, the step declares the output directory as a prerequisite. Warned about rather + // than debugged, so an empty analysis is never mistaken for a clean one. context.getLog() - .debug("No compiled classes to analyze with ArchUnit. Skipping..."); + .warn("No compiled classes to analyze with ArchUnit, every rule will report nothing. Skipping..."); return List.of(); } @@ -128,23 +127,4 @@ private static String singleLine(final String message) { .trim(); } - private static List classDirectoriesOf(final MavenProject project, final boolean includeTestClasses) { - final List directories = new ArrayList<>(); - - addIfDirectory(directories, project.getBuild().getOutputDirectory()); - - if (includeTestClasses) { - addIfDirectory(directories, project.getBuild().getTestOutputDirectory()); - } - - return directories; - } - - private static void addIfDirectory(final List directories, final String directory) { - final Path path = Path.of(directory); - - if (Files.isDirectory(path)) { - directories.add(path); - } - } } diff --git a/src/main/java/io/github/finoid/maven/plugins/codequality/configuration/ArchUnitConfiguration.java b/src/main/java/io/github/finoid/maven/plugins/codequality/configuration/ArchUnitConfiguration.java index 9e88f96..61dba53 100644 --- a/src/main/java/io/github/finoid/maven/plugins/codequality/configuration/ArchUnitConfiguration.java +++ b/src/main/java/io/github/finoid/maven/plugins/codequality/configuration/ArchUnitConfiguration.java @@ -48,6 +48,21 @@ public class ArchUnitConfiguration implements Configuration { @Parameter(property = "cq.archunit.serviceLoaderEnabled") private boolean serviceLoaderEnabled = true; + /** + * Whether the module should be compiled when it has not been already. + *

+ * Off by default. In the ordinary binding the goal runs at {@code verify}, where the classes are long since + * built, and a code quality goal quietly compiling the module is a side effect nobody asked for - a compilation + * failure would surface as a code quality failure. Switch it on to invoke the goal directly, without putting a + * phase in front of it. + *

+ * The compilation is a separate one, into {@code target/archunit-classes}, so it neither overwrites nor satisfies + * the output of the build itself. It reproduces the release level and the annotation processors of the module, + * but not a bespoke compiler configuration, so what is analyzed may differ from what the build produces. + */ + @Parameter(property = "cq.archunit.compileIfMissing") + private boolean compileIfMissing = false; + /** * Whether the test classes of the module should be analyzed alongside its main classes. *

diff --git a/src/main/java/io/github/finoid/maven/plugins/codequality/step/ArchUnitStep.java b/src/main/java/io/github/finoid/maven/plugins/codequality/step/ArchUnitStep.java index 4f295c6..e36a77b 100644 --- a/src/main/java/io/github/finoid/maven/plugins/codequality/step/ArchUnitStep.java +++ b/src/main/java/io/github/finoid/maven/plugins/codequality/step/ArchUnitStep.java @@ -1,6 +1,7 @@ package io.github.finoid.maven.plugins.codequality.step; import io.github.finoid.maven.plugins.codequality.ExecutionContext; +import io.github.finoid.maven.plugins.codequality.MavenAnnotationProcessorsManager; import io.github.finoid.maven.plugins.codequality.archunit.ArchRuleResolver; import io.github.finoid.maven.plugins.codequality.archunit.ArchUnitAnalyzer; import io.github.finoid.maven.plugins.codequality.archunit.NamedArchRule; @@ -9,18 +10,37 @@ import io.github.finoid.maven.plugins.codequality.configuration.CodeQualityConfiguration; import io.github.finoid.maven.plugins.codequality.exceptions.CodeQualityException; import io.github.finoid.maven.plugins.codequality.report.Violation; +import io.github.finoid.maven.plugins.codequality.util.CollectorUtils; +import io.github.finoid.maven.plugins.codequality.util.MojoUtils.ElementUtils; +import io.github.finoid.maven.plugins.codequality.util.MojoUtils.PluginUtils; import io.github.finoid.maven.plugins.codequality.util.Precondition; +import io.github.finoid.maven.plugins.codequality.util.PropertyUtils; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.BuildPluginManager; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.descriptor.PluginDescriptor; import org.apache.maven.project.MavenProject; +import org.twdata.maven.mojoexecutor.MojoExecutor; import javax.inject.Inject; import javax.inject.Singleton; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.net.URL; import java.net.URLClassLoader; +import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.stream.Collectors; +import static org.twdata.maven.mojoexecutor.MojoExecutor.configuration; +import static org.twdata.maven.mojoexecutor.MojoExecutor.element; +import static org.twdata.maven.mojoexecutor.MojoExecutor.executeMojo; +import static org.twdata.maven.mojoexecutor.MojoExecutor.executionEnvironment; +import static org.twdata.maven.mojoexecutor.MojoExecutor.goal; + /** * Step which evaluates ArchUnit rules against the compiled classes of the module. *

@@ -34,16 +54,25 @@ */ @Singleton public class ArchUnitStep implements Step { + private static final String ARCH_UNIT_CLASSES = "archunit-classes"; + private final ArchRuleResolver archRuleResolver; private final ArchUnitAnalyzer archUnitAnalyzer; private final TestClassPathResolver testClassPathResolver; + private final CodeQualityConfiguration codeQualityConfiguration; + private final MavenSession mavenSession; + private final BuildPluginManager pluginManager; @Inject public ArchUnitStep(final ArchRuleResolver archRuleResolver, final ArchUnitAnalyzer archUnitAnalyzer, - final TestClassPathResolver testClassPathResolver) { + final TestClassPathResolver testClassPathResolver, final CodeQualityConfiguration codeQualityConfiguration, + final MavenSession mavenSession, final BuildPluginManager pluginManager) { this.archRuleResolver = Precondition.nonNull(archRuleResolver, "ArchRuleResolver shouldn't be null"); this.archUnitAnalyzer = Precondition.nonNull(archUnitAnalyzer, "ArchUnitAnalyzer shouldn't be null"); this.testClassPathResolver = Precondition.nonNull(testClassPathResolver, "TestClassPathResolver shouldn't be null"); + this.codeQualityConfiguration = Precondition.nonNull(codeQualityConfiguration, "CodeQualityConfiguration shouldn't be null"); + this.mavenSession = Precondition.nonNull(mavenSession, "MavenSession shouldn't be null"); + this.pluginManager = Precondition.nonNull(pluginManager, "BuildPluginManager shouldn't be null"); } @Override @@ -57,6 +86,18 @@ public PrerequisiteResult hasPrerequisites(final ArchUnitConfiguration configura return PrerequisiteResult.notOK("no rules are configured and the service loader is disabled"); } + /* + * Unlike the analyzers which fork a compiler of their own, this step reads the classes the build has already + * produced. Bound to a phase before compile - or invoked directly on the command line ahead of one - there is + * nothing to read, and every rule would pass for the wrong reason. Reported as a missing prerequisite rather + * than as an empty result, so a run which cannot find anything never looks like a clean one. + */ + if (!configuration.isCompileIfMissing() && !Files.isDirectory(outputDirectoryOf(context.getProject()))) { + return PrerequisiteResult.notOK( + "the module has no compiled classes, the goal has to run at or after the compile phase." + + " Set archUnit.compileIfMissing to compile it instead"); + } + return PrerequisiteResult.OK; } @@ -73,8 +114,12 @@ public StepResult execute(final CodeQualityConfiguration codeQualityConfiguratio @Override public CleanContext getCleanContext() { - // Nothing is written between runs: the classes are re-imported and the rules re-evaluated on every execution. - return CleanContext.DO_NOTHING; + /* + * Only the output of an own compilation is cleaned, and only that. The classes of the build itself are left + * alone, but a stale class of ours - from a source file since deleted or renamed - would otherwise be + * analyzed forever, which is the sort of finding nobody can explain. + */ + return new CleanContext(CleanContext.CleanType.DIRECTORY, ARCH_UNIT_CLASSES, "**/*"); } private List executeStep(final ArchUnitConfiguration configuration, final ExecutionContext context) { @@ -93,7 +138,7 @@ private List executeStep(final ArchUnitConfiguration configuration, f warnOnUnmatchedSeverities(configuration, rules, context); - return archUnitAnalyzer.analyze(rules, configuration, context); + return archUnitAnalyzer.analyze(classDirectoriesOf(configuration, context), rules, configuration, context); } catch (final IOException e) { throw new CodeQualityException(String.format("Failed to close the ArchUnit class loader. Cause: %s", e.getMessage()), e); } @@ -119,6 +164,95 @@ private void warnOnUnmatchedSeverities(final ArchUnitConfiguration configuration .warn(String.format("ArchUnit severity override [%s] matches no resolved rule. Known rules: %s", name, resolvedNames))); } + /** + * The directories holding the classes to analyze. + *

+ * Ordinarily the output of the build. When the module has not been compiled and {@code compileIfMissing} is set, + * a compilation of its own is run first, into a directory of its own so that neither the output of the build is + * overwritten nor a later phase led to believe the module is already built. + */ + private List classDirectoriesOf(final ArchUnitConfiguration configuration, final ExecutionContext context) { + final MavenProject project = context.getProject(); + + final List directories = new ArrayList<>(); + + if (Files.isDirectory(outputDirectoryOf(project))) { + directories.add(outputDirectoryOf(project)); + } else if (configuration.isCompileIfMissing()) { + directories.add(compile(context)); + } + + if (configuration.isAnalyzeTestClasses() && Files.isDirectory(testOutputDirectoryOf(project))) { + directories.add(testOutputDirectoryOf(project)); + } + + return directories; + } + + /** + * Compiles the main sources of the module into {@code target/archunit-classes}. + * + *

The release level and the annotation processors of the module are reproduced, which covers the common case + * of a Lombok using service. A module with a bespoke compiler configuration - additional compiler arguments, + * generated source roots, a module path - is not fully reproduced, so what is analyzed can differ from what the + * build itself produces. Running the goal after the compile phase avoids the question entirely. + */ + private Path compile(final ExecutionContext context) { + final MavenProject project = context.getProject(); + + final PluginDescriptor descriptor = PluginUtils.pluginDescriptor("org.apache.maven.plugins", "maven-compiler-plugin", + codeQualityConfiguration.getVersions().getMavenCompiler()); + + final String javaVersion = PropertyUtils.valueOrFallback(project.getProperties(), "java.version", "21"); + final Path outputDirectory = Path.of(project.getBuild().getDirectory(), ARCH_UNIT_CLASSES); + + // The forked compile assigns an artifact file to the project, which would later be reported as + // 'The packaging for this project did not assign a file to the build artifact.' + final File originalArtifactFile = project.getArtifact() + .getFile(); + + context.getLog() + .info(String.format("Compiling %s for ArchUnit, no compiled classes were found", project.getArtifactId())); + + try { + executeMojo( + PluginUtils.pluginOfDescriptor(descriptor), + goal("compile"), + configuration( + element(MojoExecutor.name("source"), javaVersion), + element(MojoExecutor.name("target"), javaVersion), + element(MojoExecutor.name("release"), javaVersion), + element(MojoExecutor.name("outputDirectory"), outputDirectory.toString()), + element(MojoExecutor.name("annotationProcessorPaths"), annotationProcessorPathsOf(project) + .toArray(MojoExecutor.Element[]::new)) + ), + executionEnvironment(project, mavenSession, pluginManager)); + + return outputDirectory; + } catch (final MojoExecutionException e) { + throw new CodeQualityException( + String.format("Failed to compile module [%s] for ArchUnit. Cause: %s", project.getArtifactId(), e.getMessage()), e); + } finally { + project.getArtifact() + .setFile(originalArtifactFile); + } + } + + private List annotationProcessorPathsOf(final MavenProject project) { + return new MavenAnnotationProcessorsManager(project, codeQualityConfiguration).annotationPaths() + .stream() + .map(path -> ElementUtils.annotationProcessor(path.getGroupId(), path.getArtifactId(), path.getVersion())) + .collect(CollectorUtils.toMutableList()); + } + + private static Path outputDirectoryOf(final MavenProject project) { + return Path.of(project.getBuild().getOutputDirectory()); + } + + private static Path testOutputDirectoryOf(final MavenProject project) { + return Path.of(project.getBuild().getTestOutputDirectory()); + } + /** * A class loader over the test classpath of the module, delegating to the class loader of this plugin. *

diff --git a/src/test/java/io/github/finoid/maven/plugins/codequality/archunit/ArchUnitAnalyzerUnitTest.java b/src/test/java/io/github/finoid/maven/plugins/codequality/archunit/ArchUnitAnalyzerUnitTest.java index 3a89e83..281f973 100644 --- a/src/test/java/io/github/finoid/maven/plugins/codequality/archunit/ArchUnitAnalyzerUnitTest.java +++ b/src/test/java/io/github/finoid/maven/plugins/codequality/archunit/ArchUnitAnalyzerUnitTest.java @@ -68,7 +68,7 @@ void reportsViolationsWithTheirSourceLocation() { .haveSimpleName("SomethingElse") .allowEmptyShould(true); - final List violations = unit.analyze(List.of(NamedArchRule.of("NAMING", rule)), configuration(), context); + final List violations = unit.analyze(classDirectories(), List.of(NamedArchRule.of("NAMING", rule)), configuration(), context); Assertions.assertEquals(1, violations.size()); @@ -92,7 +92,7 @@ void reportsNothingForASatisfiedRule() { .haveSimpleName("ArchUnitAnalyzer") .allowEmptyShould(true); - Assertions.assertTrue(unit.analyze(List.of(NamedArchRule.of("NAMING", rule)), configuration(), context).isEmpty()); + Assertions.assertTrue(unit.analyze(classDirectories(), List.of(NamedArchRule.of("NAMING", rule)), configuration(), context).isEmpty()); } @Test @@ -109,7 +109,7 @@ void appliesThePerRuleSeverity() { .haveSimpleName("SomethingElse") .allowEmptyShould(true); - final List violations = unit.analyze(List.of(NamedArchRule.of("NAMING", rule)), configuration, context); + final List violations = unit.analyze(classDirectories(), List.of(NamedArchRule.of("NAMING", rule)), configuration, context); Assertions.assertEquals(Severity.BLOCKER, violations.getFirst().getSeverity()); } @@ -124,8 +124,8 @@ void producesAStableFingerprint() { .haveSimpleName("SomethingElse") .allowEmptyShould(true); - final List first = unit.analyze(List.of(NamedArchRule.of("NAMING", rule)), configuration(), context); - final List second = unit.analyze(List.of(NamedArchRule.of("NAMING", rule)), configuration(), context); + final List first = unit.analyze(classDirectories(), List.of(NamedArchRule.of("NAMING", rule)), configuration(), context); + final List second = unit.analyze(classDirectories(), List.of(NamedArchRule.of("NAMING", rule)), configuration(), context); Assertions.assertEquals(first.getFirst().getFingerprint(), second.getFirst().getFingerprint()); } @@ -142,7 +142,11 @@ void skipsAModuleWithoutCompiledClasses() { final ExecutionContext emptyContext = ExecutionContext.of(emptyProject, log); - Assertions.assertTrue(unit.analyze(List.of(), configuration(), emptyContext).isEmpty()); + Assertions.assertTrue(unit.analyze(List.of(), List.of(), configuration(), emptyContext).isEmpty()); + } + + private static List classDirectories() { + return List.of(WORKING_DIRECTORY.resolve("target/classes")); } private static ArchUnitConfiguration configuration() { diff --git a/src/test/java/io/github/finoid/maven/plugins/codequality/archunit/ArchUnitStepUnitTest.java b/src/test/java/io/github/finoid/maven/plugins/codequality/archunit/ArchUnitStepUnitTest.java new file mode 100644 index 0000000..b7a5f10 --- /dev/null +++ b/src/test/java/io/github/finoid/maven/plugins/codequality/archunit/ArchUnitStepUnitTest.java @@ -0,0 +1,114 @@ +package io.github.finoid.maven.plugins.codequality.archunit; + +import io.github.finoid.maven.plugins.codequality.ExecutionContext; +import io.github.finoid.maven.plugins.codequality.configuration.ArchUnitConfiguration; +import io.github.finoid.maven.plugins.codequality.fixtures.UnitTest; +import io.github.finoid.maven.plugins.codequality.step.ArchUnitStep; +import io.github.finoid.maven.plugins.codequality.step.Step; +import io.github.finoid.maven.plugins.codequality.configuration.CodeQualityConfiguration; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.model.Build; +import org.apache.maven.plugin.BuildPluginManager; +import org.apache.maven.plugin.logging.Log; +import org.apache.maven.project.MavenProject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; + +class ArchUnitStepUnitTest extends UnitTest { + @Mock + private ArchRuleResolver archRuleResolver; + @Mock + private ArchUnitAnalyzer archUnitAnalyzer; + @Mock + private TestClassPathResolver testClassPathResolver; + @Mock + private MavenSession mavenSession; + @Mock + private BuildPluginManager pluginManager; + @Mock + private Log log; + + @TempDir + private Path temporaryDirectory; + + private ArchUnitStep unit; + + @BeforeEach + void beforeEach() { + unit = new ArchUnitStep(archRuleResolver, archUnitAnalyzer, testClassPathResolver, + new CodeQualityConfiguration(), mavenSession, pluginManager); + } + + @Test + @DisplayName("Given a module which has not been compiled, Then the step reports a missing prerequisite") + void reportsMissingPrerequisiteWithoutCompiledClasses() { + final ArchUnitConfiguration configuration = new ArchUnitConfiguration(); + + final Step.PrerequisiteResult result = + unit.hasPrerequisites(configuration, context(temporaryDirectory.resolve("never-compiled"))); + + Assertions.assertFalse(result.hasAllPrerequisites()); + Assertions.assertNotNull(result.cause()); + Assertions.assertTrue(result.cause().contains("compile"), result.cause()); + } + + @Test + @DisplayName("Given compiled classes, Then the prerequisites are met") + void acceptsACompiledModule() throws IOException { + final Path classes = Files.createDirectory(temporaryDirectory.resolve("classes")); + + Assertions.assertTrue(unit.hasPrerequisites(new ArchUnitConfiguration(), context(classes)).hasAllPrerequisites()); + } + + @Test + @DisplayName("Given an uncompiled module and compileIfMissing, Then the prerequisites are met") + void acceptsAnUncompiledModuleWhenAllowedToCompileIt() { + final ArchUnitConfiguration configuration = new ArchUnitConfiguration(); + configuration.setCompileIfMissing(true); + + Assertions.assertTrue( + unit.hasPrerequisites(configuration, context(temporaryDirectory.resolve("never-compiled"))).hasAllPrerequisites()); + } + + @Test + @DisplayName("Given an uncompiled module, Then the cause names the option which would compile it") + void namesTheCompileOptionInTheCause() { + final Step.PrerequisiteResult result = + unit.hasPrerequisites(new ArchUnitConfiguration(), context(temporaryDirectory.resolve("never-compiled"))); + + Assertions.assertTrue(result.cause().contains("compileIfMissing"), result.cause()); + } + + @Test + @DisplayName("Given no rules and no service loader, Then the step reports a missing prerequisite") + void reportsMissingPrerequisiteWithoutRules() throws IOException { + final Path classes = Files.createDirectory(temporaryDirectory.resolve("classes")); + + final ArchUnitConfiguration configuration = new ArchUnitConfiguration(); + configuration.setServiceLoaderEnabled(false); + configuration.setRules(Set.of()); + + final Step.PrerequisiteResult result = unit.hasPrerequisites(configuration, context(classes)); + + Assertions.assertFalse(result.hasAllPrerequisites()); + } + + private ExecutionContext context(final Path outputDirectory) { + final MavenProject project = new MavenProject(); + + final Build build = new Build(); + build.setOutputDirectory(outputDirectory.toString()); + project.setBuild(build); + + return ExecutionContext.of(project, log); + } +}