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.cli.CommandLineOption;
28  import org.apache.maven.surefire.suite.RunResult;
29  import org.apache.maven.surefire.testset.TestSetFailedException;
30  import org.apache.maven.surefire.util.internal.DumpFileUtils;
31  
32  import javax.annotation.Nonnull;
33  import java.io.File;
34  import java.lang.reflect.InvocationTargetException;
35  import java.lang.reflect.Method;
36  import java.util.ArrayList;
37  import java.util.Collection;
38  import java.util.Deque;
39  import java.util.LinkedList;
40  import java.util.List;
41  
42  import static java.util.Collections.unmodifiableList;
43  import static org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS;
44  import static org.apache.maven.surefire.booter.DumpErrorSingleton.DUMPSTREAM_FILE_EXT;
45  import static org.apache.maven.surefire.booter.DumpErrorSingleton.DUMP_FILE_EXT;
46  import static org.apache.maven.surefire.cli.CommandLineOption.LOGGING_LEVEL_DEBUG;
47  import static org.apache.maven.surefire.cli.CommandLineOption.LOGGING_LEVEL_ERROR;
48  import static org.apache.maven.surefire.cli.CommandLineOption.LOGGING_LEVEL_INFO;
49  import static org.apache.maven.surefire.cli.CommandLineOption.LOGGING_LEVEL_WARN;
50  import static org.apache.maven.surefire.cli.CommandLineOption.SHOW_ERRORS;
51  
52  /**
53   * Helper class for surefire plugins
54   */
55  public final class SurefireHelper
56  {
57      private static final String DUMP_FILE_DATE = DumpFileUtils.newFormattedDateFileName();
58  
59      public static final String DUMP_FILE_PREFIX = DUMP_FILE_DATE + "-jvmRun";
60  
61      public static final String DUMP_FILENAME_FORMATTER = DUMP_FILE_PREFIX + "%d" + DUMP_FILE_EXT;
62  
63      public static final String DUMPSTREAM_FILENAME_FORMATTER = DUMP_FILE_PREFIX + "%d" + DUMPSTREAM_FILE_EXT;
64  
65      public static final String DUMPSTREAM_FILENAME = DUMP_FILE_DATE + DUMPSTREAM_FILE_EXT;
66  
67      public static final String DUMP_FILENAME = DUMP_FILE_DATE + DUMP_FILE_EXT;
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              {
84                      "[date]" + DUMP_FILE_EXT,
85                      "[date]-jvmRun[N]" + DUMP_FILE_EXT,
86                      "[date]" + DUMPSTREAM_FILE_EXT,
87                      "[date]-jvmRun[N]" + DUMPSTREAM_FILE_EXT
88              };
89  
90      /**
91       * The placeholder that is replaced by the executing thread's running number. The thread number
92       * range starts with 1
93       * Deprecated.
94       */
95      private static final String THREAD_NUMBER_PLACEHOLDER = "${surefire.threadNumber}";
96  
97      /**
98       * The placeholder that is replaced by the executing fork's running number. The fork number
99       * range starts with 1
100      */
101     private static final String FORK_NUMBER_PLACEHOLDER = "${surefire.forkNumber}";
102 
103     /**
104      * Do not instantiate.
105      */
106     private SurefireHelper()
107     {
108         throw new IllegalAccessError( "Utility class" );
109     }
110 
111     @Nonnull
112     public static String replaceThreadNumberPlaceholders( @Nonnull String argLine, int threadNumber )
113     {
114         String threadNumberAsString = String.valueOf( threadNumber );
115         return argLine.replace( THREAD_NUMBER_PLACEHOLDER, threadNumberAsString )
116                 .replace( FORK_NUMBER_PLACEHOLDER, threadNumberAsString );
117     }
118 
119     public static File replaceForkThreadsInPath( File path, int replacement )
120     {
121         Deque<String> dirs = new LinkedList<String>();
122         File root = path;
123         while ( !root.exists() )
124         {
125             dirs.addFirst( replaceThreadNumberPlaceholders( root.getName(), replacement ) );
126             root = root.getParentFile();
127         }
128         File replacedPath = root;
129         for ( String dir : dirs )
130         {
131             replacedPath = new File( replacedPath, dir );
132         }
133         return replacedPath;
134     }
135 
136     public static String[] getDumpFilesToPrint()
137     {
138         return DUMP_FILES_PRINT.clone();
139     }
140 
141     public static void reportExecution( SurefireReportParameters reportParameters, RunResult result,
142                                         PluginConsoleLogger log, Exception firstForkException )
143         throws MojoFailureException, MojoExecutionException
144     {
145         if ( firstForkException == null && !result.isTimeout() && result.isErrorFree() )
146         {
147             if ( result.getCompletedCount() == 0 && failIfNoTests( reportParameters ) )
148             {
149                 throw new MojoFailureException( "No tests were executed!  "
150                                                         + "(Set -DfailIfNoTests=false to ignore this error.)" );
151             }
152             return;
153         }
154 
155         if ( reportParameters.isTestFailureIgnore() )
156         {
157             log.error( createErrorMessage( reportParameters, result, firstForkException ) );
158         }
159         else
160         {
161             throwException( reportParameters, result, firstForkException );
162         }
163     }
164 
165     public static List<CommandLineOption> commandLineOptions( MavenSession session, PluginConsoleLogger log )
166     {
167         List<CommandLineOption> cli = new ArrayList<CommandLineOption>();
168         if ( log.isErrorEnabled() )
169         {
170             cli.add( LOGGING_LEVEL_ERROR );
171         }
172 
173         if ( log.isWarnEnabled() )
174         {
175             cli.add( LOGGING_LEVEL_WARN );
176         }
177 
178         if ( log.isInfoEnabled() )
179         {
180             cli.add( LOGGING_LEVEL_INFO );
181         }
182 
183         if ( log.isDebugEnabled() )
184         {
185             cli.add( LOGGING_LEVEL_DEBUG );
186         }
187 
188         try
189         {
190             Method getRequestMethod = session.getClass().getMethod( "getRequest" );
191             MavenExecutionRequest request = (MavenExecutionRequest) getRequestMethod.invoke( session );
192 
193             if ( request.isShowErrors() )
194             {
195                 cli.add( SHOW_ERRORS );
196             }
197 
198             String f = getFailureBehavior( request );
199             if ( f != null )
200             {
201                 // compatible with enums Maven 3.0
202                 cli.add( CommandLineOption.valueOf( f.startsWith( "REACTOR_" ) ? f : "REACTOR_" + f ) );
203             }
204         }
205         catch ( Exception e )
206         {
207             // don't need to log the exception that Maven 2 does not have getRequest() method in Maven Session
208         }
209         return unmodifiableList( cli );
210     }
211 
212     public static void logDebugOrCliShowErrors( String s, PluginConsoleLogger log, Collection<CommandLineOption> cli )
213     {
214         if ( cli.contains( LOGGING_LEVEL_DEBUG ) )
215         {
216             log.debug( s );
217         }
218         else if ( cli.contains( SHOW_ERRORS ) )
219         {
220             if ( log.isDebugEnabled() )
221             {
222                 log.debug( s );
223             }
224             else
225             {
226                 log.info( s );
227             }
228         }
229     }
230 
231     /**
232      * Escape file path for Windows when the path is too long; otherwise returns {@code path}.
233      * <br>
234      * See <a href=
235      * "http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/7534523b4174/src/windows/classes/sun/nio/fs/WindowsPath.java#l46">
236      * sun/nio/fs/WindowsPath</a> for "long path" value explanation (=247), and
237      * <a href="https://msdn.microsoft.com/en-us/library/aa365247(VS.85).aspx#maxpath">MSDN article</a>
238      * for detailed escaping strategy explanation: in short, {@code \\?\} prefix for path with drive letter
239      * or {@code \\?\UNC\} for UNC path.
240      *
241      * @param path    source path
242      * @return escaped to platform path
243      */
244     public static String escapeToPlatformPath( String path )
245     {
246         if ( IS_OS_WINDOWS && path.length() > MAX_PATH_LENGTH_WINDOWS )
247         {
248             path = path.startsWith( "\\\\" ) ? "\\\\?\\UNC\\" + path.substring( 2 ) : "\\\\?\\" + path;
249         }
250         return path;
251     }
252 
253     private static String getFailureBehavior( MavenExecutionRequest request )
254         throws NoSuchMethodException, InvocationTargetException, IllegalAccessException
255     {
256         try
257         {
258             return request.getFailureBehavior();
259         }
260         catch ( NoSuchMethodError e )
261         {
262             return (String) request.getClass()
263                 .getMethod( "getReactorFailureBehavior" )
264                 .invoke( request );
265         }
266     }
267 
268     private static boolean failIfNoTests( SurefireReportParameters reportParameters )
269     {
270         return reportParameters.getFailIfNoTests() != null && reportParameters.getFailIfNoTests();
271     }
272 
273     private static boolean isFatal( Exception firstForkException )
274     {
275         return firstForkException != null && !( firstForkException instanceof TestSetFailedException );
276     }
277 
278     private static void throwException( SurefireReportParameters reportParameters, RunResult result,
279                                            Exception firstForkException )
280             throws MojoFailureException, MojoExecutionException
281     {
282         if ( isFatal( firstForkException ) || result.isInternalError()  )
283         {
284             throw new MojoExecutionException( createErrorMessage( reportParameters, result, firstForkException ),
285                                                     firstForkException );
286         }
287         else
288         {
289             throw new MojoFailureException( createErrorMessage( reportParameters, result, firstForkException ),
290                                                   firstForkException );
291         }
292     }
293 
294     private static String createErrorMessage( SurefireReportParameters reportParameters, RunResult result,
295                                               Exception firstForkException )
296     {
297         StringBuilder msg = new StringBuilder( 512 );
298 
299         if ( result.isTimeout() )
300         {
301             msg.append( "There was a timeout or other error in the fork" );
302         }
303         else
304         {
305             msg.append( "There are test failures.\n\nPlease refer to " )
306                     .append( reportParameters.getReportsDirectory() )
307                     .append( " for the individual test results." )
308                     .append( '\n' )
309                     .append( "Please refer to dump files (if any exist) " )
310                     .append( DUMP_FILES_PRINT[0] )
311                     .append( ", " )
312                     .append( DUMP_FILES_PRINT[1] )
313                     .append( " and " )
314                     .append( DUMP_FILES_PRINT[2] )
315                     .append( "." );
316         }
317 
318         if ( firstForkException != null && firstForkException.getLocalizedMessage() != null )
319         {
320             msg.append( '\n' )
321                     .append( firstForkException.getLocalizedMessage() );
322         }
323 
324         if ( result.isFailure() )
325         {
326             msg.append( '\n' )
327                     .append( result.getFailure() );
328         }
329 
330         return msg.toString();
331     }
332 
333 }