View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugin.surefire;
20  
21  import javax.annotation.Nonnull;
22  
23  import java.io.File;
24  import java.util.ArrayList;
25  import java.util.Collection;
26  import java.util.Deque;
27  import java.util.LinkedList;
28  import java.util.List;
29  
30  import org.apache.maven.execution.MavenExecutionRequest;
31  import org.apache.maven.execution.MavenSession;
32  import org.apache.maven.plugin.MojoExecutionException;
33  import org.apache.maven.plugin.MojoFailureException;
34  import org.apache.maven.plugin.surefire.log.PluginConsoleLogger;
35  import org.apache.maven.surefire.api.cli.CommandLineOption;
36  import org.apache.maven.surefire.api.suite.RunResult;
37  import org.apache.maven.surefire.api.testset.TestSetFailedException;
38  import org.apache.maven.surefire.booter.SurefireBooterForkException;
39  
40  import static java.util.Collections.unmodifiableList;
41  import static org.apache.maven.surefire.api.booter.DumpErrorSingleton.DUMPSTREAM_FILE_EXT;
42  import static org.apache.maven.surefire.api.booter.DumpErrorSingleton.DUMP_FILE_EXT;
43  import static org.apache.maven.surefire.api.cli.CommandLineOption.LOGGING_LEVEL_DEBUG;
44  import static org.apache.maven.surefire.api.cli.CommandLineOption.LOGGING_LEVEL_ERROR;
45  import static org.apache.maven.surefire.api.cli.CommandLineOption.LOGGING_LEVEL_INFO;
46  import static org.apache.maven.surefire.api.cli.CommandLineOption.LOGGING_LEVEL_WARN;
47  import static org.apache.maven.surefire.api.cli.CommandLineOption.SHOW_ERRORS;
48  import static org.apache.maven.surefire.api.util.internal.DumpFileUtils.newFormattedDateFileName;
49  import static org.apache.maven.surefire.shared.lang3.SystemUtils.IS_OS_WINDOWS;
50  
51  /**
52   * Helper class for surefire plugins
53   */
54  public final class SurefireHelper {
55      private static final String DUMP_FILE_DATE = newFormattedDateFileName();
56  
57      public static final String DUMP_FILE_PREFIX = DUMP_FILE_DATE + "-jvmRun";
58  
59      public static final String DUMP_FILENAME_FORMATTER = DUMP_FILE_PREFIX + "%d" + DUMP_FILE_EXT;
60  
61      public static final String DUMPSTREAM_FILENAME_FORMATTER = DUMP_FILE_PREFIX + "%d" + DUMPSTREAM_FILE_EXT;
62  
63      public static final String DUMPSTREAM_FILENAME = DUMP_FILE_DATE + DUMPSTREAM_FILE_EXT;
64  
65      public static final String DUMP_FILENAME = DUMP_FILE_DATE + DUMP_FILE_EXT;
66  
67      public static final String EVENTS_BINARY_DUMP_FILENAME_FORMATTER = DUMP_FILE_DATE + "-jvmRun%d-events.bin";
68  
69      /**
70       * The maximum path that does not require long path prefix on Windows.<br>
71       * See {@code sun/nio/fs/WindowsPath} in
72       * <a href=
73       * "http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/7534523b4174/src/windows/classes/sun/nio/fs/WindowsPath.java#l46">
74       * OpenJDK</a>
75       * and <a href="https://msdn.microsoft.com/en-us/library/aa365247(VS.85).aspx#maxpath">MSDN article</a>.
76       * <br>
77       * The maximum path is 260 minus 1 (NUL) but for directories it is 260
78       * minus 12 minus 1 (to allow for the creation of a 8.3 file in the directory).
79       */
80      private static final int MAX_PATH_LENGTH_WINDOWS = 247;
81  
82      private static final String[] DUMP_FILES_PRINT = {
83          "[date]" + DUMP_FILE_EXT,
84          "[date]-jvmRun[N]" + DUMP_FILE_EXT,
85          "[date]" + DUMPSTREAM_FILE_EXT,
86          "[date]-jvmRun[N]" + DUMPSTREAM_FILE_EXT
87      };
88  
89      /**
90       * The placeholder that is replaced by the executing thread's running number. The thread number
91       * range starts with 1
92       * Deprecated.
93       */
94      private static final String THREAD_NUMBER_PLACEHOLDER = "${surefire.threadNumber}";
95  
96      /**
97       * The placeholder that is replaced by the executing fork's running number. The fork number
98       * range starts with 1
99       */
100     private static final String FORK_NUMBER_PLACEHOLDER = "${surefire.forkNumber}";
101 
102     /**
103      * Do not instantiate.
104      */
105     private SurefireHelper() {
106         throw new IllegalAccessError("Utility class");
107     }
108 
109     @Nonnull
110     public static String replaceThreadNumberPlaceholders(@Nonnull String argLine, int threadNumber) {
111         String threadNumberAsString = String.valueOf(threadNumber);
112         return argLine.replace(THREAD_NUMBER_PLACEHOLDER, threadNumberAsString)
113                 .replace(FORK_NUMBER_PLACEHOLDER, threadNumberAsString);
114     }
115 
116     public static File replaceForkThreadsInPath(File path, int replacement) {
117         Deque<String> dirs = new LinkedList<>();
118         File root = path;
119         while (!root.exists()) {
120             dirs.addFirst(replaceThreadNumberPlaceholders(root.getName(), replacement));
121             root = root.getParentFile();
122         }
123         File replacedPath = root;
124         for (String dir : dirs) {
125             replacedPath = new File(replacedPath, dir);
126         }
127         return replacedPath;
128     }
129 
130     public static String[] getDumpFilesToPrint() {
131         return DUMP_FILES_PRINT.clone();
132     }
133 
134     public static void reportExecution(
135             SurefireReportParameters reportParameters,
136             RunResult result,
137             PluginConsoleLogger log,
138             Exception firstForkException)
139             throws MojoFailureException, MojoExecutionException {
140         boolean isError = firstForkException != null || result.isTimeout() || !result.isErrorFree();
141         boolean isTooFlaky = isTooFlaky(result, reportParameters);
142         if (!isError && !isTooFlaky) {
143             if (result.getCompletedCount() == 0 && failIfNoTests(reportParameters)) {
144                 throw new MojoFailureException(
145                         "No tests were executed!  " + "(Set -DfailIfNoTests=false to ignore this error.)");
146             }
147             return;
148         }
149 
150         if (reportParameters.isTestFailureIgnore()) {
151             String errorMessage = createErrorMessage(reportParameters, result, firstForkException);
152 
153             if (firstForkException instanceof SurefireBooterForkException) {
154                 throw new MojoExecutionException(errorMessage, firstForkException);
155             }
156 
157             log.error(errorMessage);
158         } else {
159             throwException(reportParameters, result, firstForkException);
160         }
161     }
162 
163     public static List<CommandLineOption> commandLineOptions(MavenSession session, PluginConsoleLogger log) {
164         List<CommandLineOption> cli = new ArrayList<>();
165         if (log.isErrorEnabled()) {
166             cli.add(LOGGING_LEVEL_ERROR);
167         }
168 
169         if (log.isWarnEnabled()) {
170             cli.add(LOGGING_LEVEL_WARN);
171         }
172 
173         if (log.isInfoEnabled()) {
174             cli.add(LOGGING_LEVEL_INFO);
175         }
176 
177         if (log.isDebugEnabled()) {
178             cli.add(LOGGING_LEVEL_DEBUG);
179         }
180 
181         MavenExecutionRequest request = session.getRequest();
182 
183         if (request.isShowErrors()) {
184             cli.add(SHOW_ERRORS);
185         }
186 
187         String failureBehavior = request.getReactorFailureBehavior();
188         if (failureBehavior != null) {
189             try {
190                 cli.add(CommandLineOption.valueOf(failureBehavior));
191             } catch (IllegalArgumentException e) {
192                 // CommandLineOption does not have specified enum as string. See getRequest() method in Maven Session.
193             }
194         }
195 
196         return unmodifiableList(cli);
197     }
198 
199     public static void logDebugOrCliShowErrors(String s, PluginConsoleLogger log, Collection<CommandLineOption> cli) {
200         if (cli.contains(LOGGING_LEVEL_DEBUG)) {
201             log.debug(s);
202         } else if (cli.contains(SHOW_ERRORS)) {
203             if (log.isDebugEnabled()) {
204                 log.debug(s);
205             } else {
206                 log.info(s);
207             }
208         }
209     }
210 
211     /**
212      * Escape file path for Windows when the path is too long; otherwise returns {@code path}.
213      * <br>
214      * See <a href=
215      * "http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/7534523b4174/src/windows/classes/sun/nio/fs/WindowsPath.java#l46">
216      * sun/nio/fs/WindowsPath</a> for "long path" value explanation (=247), and
217      * <a href="https://msdn.microsoft.com/en-us/library/aa365247(VS.85).aspx#maxpath">MSDN article</a>
218      * for detailed escaping strategy explanation: in short, {@code \\?\} prefix for path with drive letter
219      * or {@code \\?\UNC\} for UNC path.
220      *
221      * @param path    source path
222      * @return escaped to platform path
223      */
224     public static String escapeToPlatformPath(String path) {
225         if (IS_OS_WINDOWS && path.length() > MAX_PATH_LENGTH_WINDOWS) {
226             path = path.startsWith("\\\\") ? "\\\\?\\UNC\\" + path.substring(2) : "\\\\?\\" + path;
227         }
228         return path;
229     }
230 
231     private static boolean failIfNoTests(SurefireReportParameters reportParameters) {
232         return reportParameters.getFailIfNoTests();
233     }
234 
235     private static boolean isFatal(Exception firstForkException) {
236         return firstForkException != null && !(firstForkException instanceof TestSetFailedException);
237     }
238 
239     private static void throwException(
240             SurefireReportParameters reportParameters, RunResult result, Exception firstForkException)
241             throws MojoFailureException, MojoExecutionException {
242         if (isFatal(firstForkException) || result.isInternalError()) {
243             throw new MojoExecutionException(
244                     createErrorMessage(reportParameters, result, firstForkException), firstForkException);
245         } else {
246             throw new MojoFailureException(
247                     createErrorMessage(reportParameters, result, firstForkException), firstForkException);
248         }
249     }
250 
251     private static String createErrorMessage(
252             SurefireReportParameters reportParameters, RunResult result, Exception firstForkException) {
253         StringBuilder msg = new StringBuilder(512);
254 
255         if (result.isTimeout()) {
256             msg.append("There was a timeout in the fork");
257         } else {
258             if (result.getFailures() > 0) {
259                 msg.append("There are test failures.");
260             }
261             if (isTooFlaky(result, reportParameters)) {
262                 if (result.getFailures() > 0) {
263                     msg.append("\n");
264                 }
265                 msg.append("There")
266                         .append(result.getFlakes() == 1 ? " is " : " are ")
267                         .append(result.getFlakes())
268                         .append(result.getFlakes() == 1 ? " flake " : " flakes ")
269                         .append("and failOnFlakeCount is set to ")
270                         .append(reportParameters.getFailOnFlakeCount())
271                         .append(".");
272             }
273             msg.append("\n\nPlease refer to ")
274                     .append(reportParameters.getReportsDirectory())
275                     .append(" for the individual test results.")
276                     .append('\n')
277                     .append("Please refer to dump files (if any exist) ")
278                     .append(DUMP_FILES_PRINT[0])
279                     .append(", ")
280                     .append(DUMP_FILES_PRINT[1])
281                     .append(" and ")
282                     .append(DUMP_FILES_PRINT[2])
283                     .append(".");
284         }
285 
286         if (firstForkException != null && firstForkException.getLocalizedMessage() != null) {
287             msg.append('\n').append(firstForkException.getLocalizedMessage());
288         }
289 
290         if (result.isFailure()) {
291             msg.append('\n').append(result.getFailure());
292         }
293 
294         return msg.toString();
295     }
296 
297     private static boolean isTooFlaky(RunResult result, SurefireReportParameters reportParameters) {
298         int failOnFlakeCount = reportParameters.getFailOnFlakeCount();
299         return failOnFlakeCount > 0 && result.getFlakes() >= failOnFlakeCount;
300     }
301 }