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