You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by sl...@apache.org on 2026/09/15 23:42:32 UTC
(maven-invoker-plugin) branch agent/assert-failed-goal created (now 0799c84)
This is an automated email from the ASF dual-hosted git repository.
slachiewicz pushed a change to branch agent/assert-failed-goal
in repository https://gitbox.apache.org/repos/asf/maven-invoker-plugin.git
at 0799c84 Add invoker.failedGoal to assert which goal an IT build failed on
This branch includes the following new commits:
new 0799c84 Add invoker.failedGoal to assert which goal an IT build failed on
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(maven-invoker-plugin) 01/01: Add invoker.failedGoal to assert which goal an IT build failed on
Posted by sl...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
slachiewicz pushed a commit to branch agent/assert-failed-goal
in repository https://gitbox.apache.org/repos/asf/maven-invoker-plugin.git
commit 0799c84503d0c4ae87d81064040dcad4b6c0cc28
Author: Sylwester Lachiewicz <sl...@apache.org>
AuthorDate: Wed Sep 16 01:42:18 2026 +0200
Add invoker.failedGoal to assert which goal an IT build failed on
invoker.buildResult = failure accepts any failure, so a test stays green
when the IT build breaks for an unrelated reason. The exit code cannot
tell which goal failed, so the goal is read back from build.log.
Fixes #503
---
src/it/MINVOKER-229-failed-goal/invoker.properties | 19 ++
src/it/MINVOKER-229-failed-goal/pom.xml | 66 +++++++
.../src/it/expected-goal/invoker.properties | 19 ++
.../src/it/expected-goal/pom.xml | 55 ++++++
.../src/it/unexpected-goal/invoker.properties | 20 ++
.../src/it/unexpected-goal/pom.xml | 55 ++++++
src/it/MINVOKER-229-failed-goal/verify.groovy | 37 ++++
.../maven/plugins/invoker/AbstractInvokerMojo.java | 66 +++++++
.../maven/plugins/invoker/FailedGoalMatcher.java | 201 +++++++++++++++++++++
.../maven/plugins/invoker/InvokerProperties.java | 24 ++-
src/site/markdown/examples/invoker-properties.md | 9 +
src/site/markdown/faq.md | 5 +
.../plugins/invoker/FailedGoalMatcherTest.java | 157 ++++++++++++++++
.../plugins/invoker/InvokerPropertiesTest.java | 23 +++
14 files changed, 754 insertions(+), 2 deletions(-)
diff --git a/src/it/MINVOKER-229-failed-goal/invoker.properties b/src/it/MINVOKER-229-failed-goal/invoker.properties
new file mode 100644
index 0000000..31a396f
--- /dev/null
+++ b/src/it/MINVOKER-229-failed-goal/invoker.properties
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+# the unexpected-goal job is expected to fail, which fails this build too
+invoker.buildResult = failure
diff --git a/src/it/MINVOKER-229-failed-goal/pom.xml b/src/it/MINVOKER-229-failed-goal/pom.xml
new file mode 100644
index 0000000..819463e
--- /dev/null
+++ b/src/it/MINVOKER-229-failed-goal/pom.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>org.apache.maven.plugins.invoker.its</groupId>
+ <artifactId>failed-goal</artifactId>
+ <version>1.0-SNAPSHOT</version>
+ <packaging>pom</packaging>
+
+ <description>Test to check that invoker.failedGoal asserts which goal the forked build failed on.</description>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <version.maven-enforcer-plugin>@version.maven-enforcer-plugin@</version.maven-enforcer-plugin>
+ </properties>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-invoker-plugin</artifactId>
+ <version>@project.version@</version>
+ <configuration>
+ <cloneProjectsTo>${project.build.directory}/it</cloneProjectsTo>
+ <writeJunitReport>true</writeJunitReport>
+ <pomIncludes>
+ <pomInclude>*/pom.xml</pomInclude>
+ </pomIncludes>
+ <goals>
+ <goal>validate</goal>
+ </goals>
+ </configuration>
+ <executions>
+ <execution>
+ <id>integration-test</id>
+ <phase>initialize</phase>
+ <goals>
+ <goal>run</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/src/it/MINVOKER-229-failed-goal/src/it/expected-goal/invoker.properties b/src/it/MINVOKER-229-failed-goal/src/it/expected-goal/invoker.properties
new file mode 100644
index 0000000..7870999
--- /dev/null
+++ b/src/it/MINVOKER-229-failed-goal/src/it/expected-goal/invoker.properties
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+invoker.buildResult = failure
+invoker.failedGoal = maven-enforcer-plugin:enforce (enforce-rules)
diff --git a/src/it/MINVOKER-229-failed-goal/src/it/expected-goal/pom.xml b/src/it/MINVOKER-229-failed-goal/src/it/expected-goal/pom.xml
new file mode 100644
index 0000000..e8241be
--- /dev/null
+++ b/src/it/MINVOKER-229-failed-goal/src/it/expected-goal/pom.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.apache.maven.plugins.invoker.its</groupId>
+ <artifactId>expected-goal</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ <packaging>pom</packaging>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-enforcer-plugin</artifactId>
+ <version>@version.maven-enforcer-plugin@</version>
+ <executions>
+ <execution>
+ <id>enforce-rules</id>
+ <goals>
+ <goal>enforce</goal>
+ </goals>
+ <configuration>
+ <rules>
+ <AlwaysFail/>
+ </rules>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/src/it/MINVOKER-229-failed-goal/src/it/unexpected-goal/invoker.properties b/src/it/MINVOKER-229-failed-goal/src/it/unexpected-goal/invoker.properties
new file mode 100644
index 0000000..a6370b2
--- /dev/null
+++ b/src/it/MINVOKER-229-failed-goal/src/it/unexpected-goal/invoker.properties
@@ -0,0 +1,20 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+# the build fails on the enforcer goal, so this job must not pass
+invoker.buildResult = failure
+invoker.failedGoal = maven-clean-plugin:clean
diff --git a/src/it/MINVOKER-229-failed-goal/src/it/unexpected-goal/pom.xml b/src/it/MINVOKER-229-failed-goal/src/it/unexpected-goal/pom.xml
new file mode 100644
index 0000000..052f42d
--- /dev/null
+++ b/src/it/MINVOKER-229-failed-goal/src/it/unexpected-goal/pom.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.apache.maven.plugins.invoker.its</groupId>
+ <artifactId>unexpected-goal</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ <packaging>pom</packaging>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-enforcer-plugin</artifactId>
+ <version>@version.maven-enforcer-plugin@</version>
+ <executions>
+ <execution>
+ <id>enforce-rules</id>
+ <goals>
+ <goal>enforce</goal>
+ </goals>
+ <configuration>
+ <rules>
+ <AlwaysFail/>
+ </rules>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/src/it/MINVOKER-229-failed-goal/verify.groovy b/src/it/MINVOKER-229-failed-goal/verify.groovy
new file mode 100644
index 0000000..96af7ba
--- /dev/null
+++ b/src/it/MINVOKER-229-failed-goal/verify.groovy
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+import groovy.xml.XmlSlurper
+
+File invokerReports = new File( new File( basedir, 'target' ), 'invoker-reports-test' )
+assert invokerReports.exists()
+
+// the build failed on the goal named by invoker.failedGoal
+def expected = new XmlSlurper().parse( new File( invokerReports, 'BUILD-expected-goal.xml' ) )
+
+assert expected.@result.text() == 'success'
+
+// the build failed, but on another goal than invoker.failedGoal named
+def unexpected = new XmlSlurper().parse( new File( invokerReports, 'BUILD-unexpected-goal.xml' ) )
+
+assert unexpected.@result.text() == 'failure-build'
+assert unexpected.failureMessage.text().contains( 'maven-clean-plugin:clean' )
+assert unexpected.failureMessage.text().contains( 'maven-enforcer-plugin' )
+
+def buildLog = new File( basedir, 'build.log' ).text
+assert buildLog.contains( '[INFO] Passed: 1, Failed: 1, Errors: 0, Skipped: 0' )
diff --git a/src/main/java/org/apache/maven/plugins/invoker/AbstractInvokerMojo.java b/src/main/java/org/apache/maven/plugins/invoker/AbstractInvokerMojo.java
index c687ad9..80a7889 100644
--- a/src/main/java/org/apache/maven/plugins/invoker/AbstractInvokerMojo.java
+++ b/src/main/java/org/apache/maven/plugins/invoker/AbstractInvokerMojo.java
@@ -2067,6 +2067,72 @@ public abstract class AbstractInvokerMojo extends AbstractMojo {
buffer.append("See console output for details.");
}
throw new RunFailureException(buffer.toString(), BuildJob.Result.FAILURE_BUILD);
+ } else {
+ verifyFailedGoal(invocationIndex, invokerProperties, logger);
+ }
+ }
+
+ /**
+ * Verifies that the build failed on the goal named by <code>invoker.failedGoal</code>, if that property is set. The
+ * exit code alone only tells that the build failed, not which goal failed, so the goal is read back from the
+ * <code>Failed to execute goal ...</code> line the forked Maven wrote to the build log.
+ *
+ * @param invocationIndex The index of the invocation to check, must not be negative.
+ * @param invokerProperties The invoker properties of the build job, must not be <code>null</code>.
+ * @param logger The build logger, may be <code>null</code> if logging is disabled.
+ */
+ private void verifyFailedGoal(int invocationIndex, InvokerProperties invokerProperties, FileLogger logger)
+ throws RunFailureException {
+ Optional<String> expectedGoal = invokerProperties.getFailedGoal(invocationIndex);
+ if (!expectedGoal.isPresent()) {
+ return;
+ }
+
+ if (!invokerProperties.isExpectedFailure(invocationIndex)) {
+ throw new RunFailureException(
+ "The property invoker.failedGoal requires invoker.buildResult = failure.", BuildJob.Result.ERROR);
+ }
+
+ if (logger == null) {
+ throw new RunFailureException(
+ "The property invoker.failedGoal needs the build log, which is disabled by the noLog parameter.",
+ BuildJob.Result.ERROR);
+ }
+
+ logger.getPrintStream().flush();
+ File logFile = logger.getOutputFile();
+ List<String> failedGoals;
+ try {
+ failedGoals = FailedGoalMatcher.findFailedGoals(logFile);
+ } catch (IOException e) {
+ throw new RunFailureException(
+ "The property invoker.failedGoal could not be checked, " + logFile.getAbsolutePath()
+ + " is not readable. " + e.getMessage(),
+ BuildJob.Result.ERROR,
+ e);
+ }
+
+ FailedGoalMatcher matcher;
+ try {
+ matcher = new FailedGoalMatcher(expectedGoal.get());
+ } catch (IllegalArgumentException e) {
+ throw new RunFailureException(
+ "The property invoker.failedGoal is set to an invalid value: " + e.getMessage(),
+ BuildJob.Result.ERROR);
+ }
+
+ if (failedGoals.isEmpty()) {
+ throw new RunFailureException(
+ "The build was expected to fail on goal " + matcher + ", but " + logFile.getAbsolutePath()
+ + " reports no failed goal.",
+ BuildJob.Result.FAILURE_BUILD);
+ }
+
+ if (!matcher.matchesAny(failedGoals)) {
+ throw new RunFailureException(
+ "The build was expected to fail on goal " + matcher + ", but it failed on "
+ + String.join(", ", failedGoals) + ".",
+ BuildJob.Result.FAILURE_BUILD);
}
}
diff --git a/src/main/java/org/apache/maven/plugins/invoker/FailedGoalMatcher.java b/src/main/java/org/apache/maven/plugins/invoker/FailedGoalMatcher.java
new file mode 100644
index 0000000..5555ddb
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/invoker/FailedGoalMatcher.java
@@ -0,0 +1,201 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.maven.plugins.invoker;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CodingErrorAction;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/**
+ * Matches the goal a forked Maven build reported as failed against an <code>invoker.failedGoal</code> specification.
+ * <p>
+ * A specification is a colon separated list of coordinate fragments, optionally followed by an execution id in
+ * parentheses, for example <code>maven-enforcer-plugin:enforce (enforce-rules)</code>. The fragments have to occur, in
+ * the given order, among the <code>groupId:artifactId:version:goal</code> segments Maven logged, so a test can name as
+ * little or as much of the coordinates as it cares about. When an execution id is given, it has to match exactly.
+ */
+class FailedGoalMatcher {
+
+ /**
+ * The message Maven prints for a failed mojo execution, both in Maven 3 and Maven 4.
+ */
+ private static final Pattern FAILED_GOAL_LINE =
+ Pattern.compile("Failed to execute goal (\\S+?)(?: \\(([^)]*)\\))? on project ");
+
+ private final String spec;
+
+ private final Goal expected;
+
+ /**
+ * Creates a matcher for the given specification.
+ *
+ * @param spec the value of an <code>invoker.failedGoal</code> property, must not be <code>null</code>
+ * @throws IllegalArgumentException if the specification names no coordinate fragment
+ */
+ FailedGoalMatcher(String spec) {
+ this.spec = spec;
+ this.expected = Goal.parse(spec);
+ if (expected.coordinates.isEmpty()) {
+ throw new IllegalArgumentException("invalid failed goal specification: " + spec);
+ }
+ }
+
+ /**
+ * Collects the goals a build log reports as failed. A build log is not guaranteed to hold well-formed text, so
+ * bytes the platform encoding cannot decode are replaced rather than reported.
+ *
+ * @param logFile a <code>build.log</code>, must not be <code>null</code>
+ * @return the failed goals in order of appearance, each as logged by Maven, never <code>null</code>
+ * @throws IOException if the log file could not be read
+ */
+ static List<String> findFailedGoals(File logFile) throws IOException {
+ CharsetDecoder decoder = Charset.defaultCharset()
+ .newDecoder()
+ .onMalformedInput(CodingErrorAction.REPLACE)
+ .onUnmappableCharacter(CodingErrorAction.REPLACE);
+
+ List<String> failedGoals = new ArrayList<>();
+ try (BufferedReader reader =
+ new BufferedReader(new InputStreamReader(Files.newInputStream(logFile.toPath()), decoder))) {
+ for (String line = reader.readLine(); line != null; line = reader.readLine()) {
+ findFailedGoal(line).ifPresent(failedGoals::add);
+ }
+ }
+ return failedGoals;
+ }
+
+ /**
+ * Collects the goals a build log reports as failed.
+ *
+ * @param logLines the lines of a <code>build.log</code>, must not be <code>null</code>
+ * @return the failed goals in order of appearance, each as logged by Maven, never <code>null</code>
+ */
+ static List<String> findFailedGoals(Collection<String> logLines) {
+ List<String> failedGoals = new ArrayList<>();
+ for (String line : logLines) {
+ findFailedGoal(line).ifPresent(failedGoals::add);
+ }
+ return failedGoals;
+ }
+
+ private static Optional<String> findFailedGoal(String logLine) {
+ Matcher matcher = FAILED_GOAL_LINE.matcher(logLine);
+ if (!matcher.find()) {
+ return Optional.empty();
+ }
+ String executionId = matcher.group(2);
+ return Optional.of(executionId != null ? matcher.group(1) + " (" + executionId + ")" : matcher.group(1));
+ }
+
+ /**
+ * Checks whether any of the given failed goals matches this specification.
+ *
+ * @param failedGoals the failed goals as returned by {@link #findFailedGoals(Collection)}
+ * @return <code>true</code> if at least one goal matches
+ */
+ boolean matchesAny(Collection<String> failedGoals) {
+ return failedGoals.stream().anyMatch(this::matches);
+ }
+
+ /**
+ * Checks whether the given failed goal matches this specification.
+ *
+ * @param failedGoal a failed goal as logged by Maven
+ * @return <code>true</code> if the goal matches
+ */
+ boolean matches(String failedGoal) {
+ Goal actual = Goal.parse(failedGoal);
+ if (expected.executionId != null && !Objects.equals(expected.executionId, actual.executionId)) {
+ return false;
+ }
+ return isSubsequence(expected.coordinates, actual.coordinates);
+ }
+
+ @Override
+ public String toString() {
+ return spec;
+ }
+
+ private static boolean isSubsequence(List<String> fragments, List<String> segments) {
+ int segment = 0;
+ for (String fragment : fragments) {
+ while (segment < segments.size() && !fragment.equals(segments.get(segment))) {
+ segment++;
+ }
+ if (segment == segments.size()) {
+ return false;
+ }
+ segment++;
+ }
+ return true;
+ }
+
+ /**
+ * A goal reference, split into its coordinate segments and its optional execution id.
+ */
+ private static final class Goal {
+
+ private final List<String> coordinates;
+
+ private final String executionId;
+
+ private Goal(List<String> coordinates, String executionId) {
+ this.coordinates = coordinates;
+ this.executionId = executionId;
+ }
+
+ private static Goal parse(String value) {
+ String coordinates = value.trim();
+ String executionId = null;
+ int parenthesis = coordinates.indexOf('(');
+ if (parenthesis >= 0 && coordinates.endsWith(")")) {
+ executionId = coordinates
+ .substring(parenthesis + 1, coordinates.length() - 1)
+ .trim();
+ coordinates = coordinates.substring(0, parenthesis).trim();
+ }
+ return new Goal(split(coordinates), executionId);
+ }
+
+ private static List<String> split(String coordinates) {
+ if (coordinates.isEmpty()) {
+ return Collections.emptyList();
+ }
+ return Arrays.stream(coordinates.split(":"))
+ .map(String::trim)
+ .filter(s -> !s.isEmpty())
+ .collect(Collectors.toList());
+ }
+ }
+}
diff --git a/src/main/java/org/apache/maven/plugins/invoker/InvokerProperties.java b/src/main/java/org/apache/maven/plugins/invoker/InvokerProperties.java
index 4bf09e7..ccb5f61 100644
--- a/src/main/java/org/apache/maven/plugins/invoker/InvokerProperties.java
+++ b/src/main/java/org/apache/maven/plugins/invoker/InvokerProperties.java
@@ -64,6 +64,7 @@ class InvokerProperties {
private enum InvocationProperty {
PROJECT("invoker.project"),
BUILD_RESULT("invoker.buildResult"),
+ FAILED_GOAL("invoker.failedGoal"),
GOALS("invoker.goals"),
PROFILES("invoker.profiles"),
MAVEN_EXECUTABLE("invoker.mavenExecutable"),
@@ -476,9 +477,28 @@ class InvokerProperties {
* a failue was expected, <code>false</code> otherwise.
*/
public boolean isExpectedResult(int exitCode, int index) {
- boolean nonZeroExit = "failure"
+ return (exitCode != 0) == isExpectedFailure(index);
+ }
+
+ /**
+ * Checks whether the given invocation is expected to fail.
+ *
+ * @param index The index of the invocation to check, must not be negative.
+ * @return <code>true</code> if <code>invoker.buildResult</code> is set to <code>failure</code>.
+ */
+ public boolean isExpectedFailure(int index) {
+ return "failure"
.equalsIgnoreCase(get(InvocationProperty.BUILD_RESULT, index).orElse(null));
- return (exitCode != 0) == nonZeroExit;
+ }
+
+ /**
+ * Gets the goal which the given invocation is expected to fail on.
+ *
+ * @param index The index of the invocation, must not be negative.
+ * @return The value of <code>invoker.failedGoal</code> or an empty optional if not set.
+ */
+ public Optional<String> getFailedGoal(int index) {
+ return get(InvocationProperty.FAILED_GOAL, index);
}
/**
diff --git a/src/site/markdown/examples/invoker-properties.md b/src/site/markdown/examples/invoker-properties.md
index 51d974e..c3e876e 100644
--- a/src/site/markdown/examples/invoker-properties.md
+++ b/src/site/markdown/examples/invoker-properties.md
@@ -80,6 +80,15 @@ invoker.failureBehavior = fail-never
# can be indexed
invoker.buildResult = failure
+# The goal the build is expected to fail on, only allowed together with
+# invoker.buildResult = failure. The value is a colon separated list of fragments of the
+# groupId:artifactId:version:goal coordinates Maven logs for the failed goal, optionally
+# followed by the execution id in parentheses. The fragments have to occur in the given
+# order, so naming the plugin and the goal is usually enough.
+# Since plugin version 3.11.0
+# can be indexed
+invoker.failedGoal = maven-enforcer-plugin:enforce (enforce-rules)
+
# A boolean value controlling the aggregator mode of Maven, defaults to "false"
# can be indexed
invoker.nonRecursive = true
diff --git a/src/site/markdown/faq.md b/src/site/markdown/faq.md
index 2b389b8..027d046 100644
--- a/src/site/markdown/faq.md
+++ b/src/site/markdown/faq.md
@@ -50,6 +50,11 @@ plugin's [`invokerPropertiesFile`](run-mojo.html#invokerPropertiesFile) paramete
*invoker.buildResult=failure*. Now, the failure of the IT build will be interpreted as a test success.
Likewise, a successful IT build will be considered a test failure.
+On its own, this passes the test whatever made the IT build fail. To pin the test to one goal, add
+*invoker.failedGoal=maven-enforcer-plugin:enforce*; the test then fails when the IT build failed on
+any other goal. See [Invoker Properties](examples/invoker-properties.html#available-properties) for the
+accepted values.
+
<a id="question3"></a>
### How can I share common code between the pre-/post-build scripts?
diff --git a/src/test/java/org/apache/maven/plugins/invoker/FailedGoalMatcherTest.java b/src/test/java/org/apache/maven/plugins/invoker/FailedGoalMatcherTest.java
new file mode 100644
index 0000000..f71b272
--- /dev/null
+++ b/src/test/java/org/apache/maven/plugins/invoker/FailedGoalMatcherTest.java
@@ -0,0 +1,157 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.maven.plugins.invoker;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests matching of the goal reported as failed in a build log.
+ */
+class FailedGoalMatcherTest {
+
+ private static final String ENFORCER_LINE =
+ "[ERROR] Failed to execute goal org.apache.maven.plugins:maven-enforcer-plugin:3.6.2:enforce"
+ + " (enforce-rules) on project test: Some Enforcer rules have failed. -> [Help 1]";
+
+ private static final String COMPILER_LINE =
+ "[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.14.1:compile"
+ + " (default-compile) on project test: Compilation failure -> [Help 1]";
+
+ @Test
+ void findFailedGoalsReadsCoordinatesAndExecutionId() {
+ List<String> failedGoals = FailedGoalMatcher.findFailedGoals(
+ Arrays.asList("[INFO] Building test 1.0", ENFORCER_LINE, "[ERROR] -> [Help 1]"));
+
+ assertThat(failedGoals)
+ .containsExactly("org.apache.maven.plugins:maven-enforcer-plugin:3.6.2:enforce (enforce-rules)");
+ }
+
+ @Test
+ void findFailedGoalsKeepsEveryReportedGoal() {
+ List<String> failedGoals = FailedGoalMatcher.findFailedGoals(Arrays.asList(ENFORCER_LINE, COMPILER_LINE));
+
+ assertThat(failedGoals).hasSize(2);
+ }
+
+ @Test
+ void findFailedGoalsWithoutExecutionId() {
+ List<String> failedGoals = FailedGoalMatcher.findFailedGoals(Collections.singletonList(
+ "[ERROR] Failed to execute goal org.apache.maven.plugins:maven-clean-plugin:3.5.0:clean on project"
+ + " test: Failed to clean project"));
+
+ assertThat(failedGoals).containsExactly("org.apache.maven.plugins:maven-clean-plugin:3.5.0:clean");
+ }
+
+ @Test
+ void findFailedGoalsIgnoresLinesWithoutCoordinates() {
+ List<String> failedGoals = FailedGoalMatcher.findFailedGoals(Arrays.asList(
+ "[ERROR] Failed to execute goal on project test: Could not resolve dependencies",
+ "[ERROR] BUILD FAILURE"));
+
+ assertThat(failedGoals).isEmpty();
+ }
+
+ @Test
+ void findFailedGoalsReadsABuildLog(@TempDir Path tempDir) throws IOException {
+ Path logFile = tempDir.resolve("build.log");
+ Files.write(
+ logFile,
+ Arrays.asList("[INFO] Scanning for projects...", ENFORCER_LINE, "[ERROR] BUILD FAILURE"),
+ Charset.defaultCharset());
+
+ assertThat(FailedGoalMatcher.findFailedGoals(logFile.toFile()))
+ .containsExactly("org.apache.maven.plugins:maven-enforcer-plugin:3.6.2:enforce (enforce-rules)");
+ }
+
+ @Test
+ void matchesFullCoordinates() {
+ assertThat(matcher("org.apache.maven.plugins:maven-enforcer-plugin:3.6.2:enforce")
+ .matchesAny(failedGoals(ENFORCER_LINE)))
+ .isTrue();
+ }
+
+ @Test
+ void matchesCoordinateSubsequences() {
+ assertThat(matcher("maven-enforcer-plugin:enforce").matchesAny(failedGoals(ENFORCER_LINE)))
+ .isTrue();
+ assertThat(matcher("org.apache.maven.plugins:maven-enforcer-plugin:enforce")
+ .matchesAny(failedGoals(ENFORCER_LINE)))
+ .isTrue();
+ assertThat(matcher("enforce").matchesAny(failedGoals(ENFORCER_LINE))).isTrue();
+ }
+
+ @Test
+ void doesNotMatchAnotherGoal() {
+ assertThat(matcher("maven-compiler-plugin:compile").matchesAny(failedGoals(ENFORCER_LINE)))
+ .isFalse();
+ assertThat(matcher("maven-enforcer-plugin:enforce").matchesAny(failedGoals(COMPILER_LINE)))
+ .isFalse();
+ }
+
+ @Test
+ void doesNotMatchFragmentsOutOfOrder() {
+ assertThat(matcher("enforce:maven-enforcer-plugin").matchesAny(failedGoals(ENFORCER_LINE)))
+ .isFalse();
+ }
+
+ @Test
+ void matchesExecutionIdWhenGiven() {
+ assertThat(matcher("maven-enforcer-plugin:enforce (enforce-rules)").matchesAny(failedGoals(ENFORCER_LINE)))
+ .isTrue();
+ assertThat(matcher("maven-enforcer-plugin:enforce (other-rules)").matchesAny(failedGoals(ENFORCER_LINE)))
+ .isFalse();
+ }
+
+ @Test
+ void matchesOneOfSeveralFailedGoals() {
+ assertThat(matcher("maven-compiler-plugin:compile").matchesAny(failedGoals(ENFORCER_LINE, COMPILER_LINE)))
+ .isTrue();
+ }
+
+ @Test
+ void doesNotMatchWhenLogHasNoFailedGoal() {
+ assertThat(matcher("maven-enforcer-plugin:enforce").matchesAny(Collections.emptyList()))
+ .isFalse();
+ }
+
+ @Test
+ void rejectsSpecificationWithoutCoordinates() {
+ assertThatThrownBy(() -> matcher(" (enforce-rules) ")).isInstanceOf(IllegalArgumentException.class);
+ }
+
+ private static FailedGoalMatcher matcher(String spec) {
+ return new FailedGoalMatcher(spec);
+ }
+
+ private static List<String> failedGoals(String... logLines) {
+ return FailedGoalMatcher.findFailedGoals(Arrays.asList(logLines));
+ }
+}
diff --git a/src/test/java/org/apache/maven/plugins/invoker/InvokerPropertiesTest.java b/src/test/java/org/apache/maven/plugins/invoker/InvokerPropertiesTest.java
index c6cd218..80052f9 100644
--- a/src/test/java/org/apache/maven/plugins/invoker/InvokerPropertiesTest.java
+++ b/src/test/java/org/apache/maven/plugins/invoker/InvokerPropertiesTest.java
@@ -101,6 +101,29 @@ class InvokerPropertiesTest {
assertThat(facade.isExpectedResult(1, 0)).isTrue();
}
+ @Test
+ void getFailedGoal() {
+ Properties props = new Properties();
+ InvokerProperties facade = new InvokerProperties(props);
+
+ assertThat(facade.getFailedGoal(0)).isEmpty();
+
+ props.setProperty("invoker.failedGoal", "maven-enforcer-plugin:enforce");
+ assertThat(facade.getFailedGoal(1)).contains("maven-enforcer-plugin:enforce");
+
+ props.setProperty("invoker.failedGoal.2", "maven-compiler-plugin:compile (default-compile)");
+ assertThat(facade.getFailedGoal(1)).contains("maven-enforcer-plugin:enforce");
+ assertThat(facade.getFailedGoal(2)).contains("maven-compiler-plugin:compile (default-compile)");
+ }
+
+ @Test
+ void isInvocationDefinedForFailedGoal() {
+ Properties props = new Properties();
+ props.setProperty("invoker.failedGoal.2", "maven-enforcer-plugin:enforce");
+
+ assertThat(new InvokerProperties(props).isInvocationDefined(2)).isTrue();
+ }
+
@Test
void configureRequestEmptyProperties() {