View Javadoc
1   package org.apache.maven.surefire.report;
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 java.util.ArrayList;
23  import java.util.List;
24  
25  import static java.lang.Math.min;
26  import static java.util.Arrays.asList;
27  import static java.util.Collections.reverse;
28  import static org.apache.maven.shared.utils.StringUtils.chompLast;
29  import static org.apache.maven.shared.utils.StringUtils.isNotEmpty;
30  
31  /**
32   * @author Kristian Rosenvold
33   */
34  @SuppressWarnings( "ThrowableResultOfMethodCallIgnored" )
35  public class SmartStackTraceParser
36  {
37      private static final int MAX_LINE_LENGTH = 77;
38  
39      private final SafeThrowable throwable;
40  
41      private final StackTraceElement[] stackTrace;
42  
43      private final String testClassName;
44  
45      private final Class<?> testClass;
46  
47      private final String testMethodName;
48  
49      public SmartStackTraceParser( String testClassName, Throwable throwable, String testMethodName )
50      {
51          this.testMethodName = testMethodName;
52          this.testClassName = testClassName;
53          testClass = toClass( testClassName );
54          this.throwable = new SafeThrowable( throwable );
55          stackTrace = throwable.getStackTrace();
56      }
57  
58      private static Class<?> toClass( String name )
59      {
60          try
61          {
62              ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
63              return classLoader == null ? null : classLoader.loadClass( name );
64          }
65          catch ( ClassNotFoundException e )
66          {
67              return null;
68          }
69      }
70  
71      private static String toSimpleClassName( String className )
72      {
73          int i = className.lastIndexOf( "." );
74          return className.substring( i + 1 );
75      }
76  
77      @SuppressWarnings( "ThrowableResultOfMethodCallIgnored" )
78      public String getString()
79      {
80          if ( testClass == null )
81          {
82              return throwable.getLocalizedMessage();
83          }
84  
85          final StringBuilder result = new StringBuilder();
86          final List<StackTraceElement> stackTraceElements = focusOnClass( stackTrace, testClass );
87          reverse( stackTraceElements );
88          final String testClassSimpleName = toSimpleClassName( testClassName );
89          if ( stackTraceElements.isEmpty() )
90          {
91              result.append( testClassSimpleName );
92              if ( isNotEmpty( testMethodName ) )
93              {
94                  result.append( "." )
95                      .append( testMethodName );
96              }
97          }
98          else
99          {
100             for ( int i = 0, size = stackTraceElements.size(); i < size; i++ )
101             {
102                 final StackTraceElement stackTraceElement = stackTraceElements.get( i );
103                 final boolean isTestClassName = stackTraceElement.getClassName().equals( testClassName );
104                 if ( i == 0 )
105                 {
106                     result.append( testClassSimpleName )
107                             .append( isTestClassName ? '.' : '>' );
108                 }
109 
110                 if ( !isTestClassName )
111                 {
112                     result.append( toSimpleClassName( stackTraceElement.getClassName() ) )
113                         .append( '.' );
114                 }
115 
116                 result.append( stackTraceElement.getMethodName() )
117                     .append( ':' )
118                     .append( stackTraceElement.getLineNumber() )
119                     .append( "->" );
120             }
121 
122             if ( result.length() >= 2 )
123             {
124                 result.setLength( result.length() - 2 );
125             }
126         }
127 
128         final Throwable target = throwable.getTarget();
129         final Class<?> excType = target.getClass();
130         final String excClassName = excType.getName();
131         final String msg = throwable.getMessage();
132 
133         if ( target instanceof AssertionError
134                 || "junit.framework.AssertionFailedError".equals( excClassName )
135                 || "junit.framework.ComparisonFailure".equals( excClassName )
136                 || excClassName.startsWith( "org.opentest4j." ) )
137         {
138             if ( isNotEmpty( msg ) )
139             {
140                 result.append( ' ' )
141                     .append( msg );
142             }
143         }
144         else
145         {
146             result.append( rootIsInclass() ? " " : " ยป " )
147                     .append( toMinimalThrowableMiniMessage( excType ) );
148 
149             result.append( truncateMessage( msg, MAX_LINE_LENGTH - result.length() ) );
150         }
151         return result.toString();
152     }
153 
154     private static String toMinimalThrowableMiniMessage( Class<?> excType )
155     {
156         String name = excType.getSimpleName();
157         if ( name.endsWith( "Exception" ) )
158         {
159             return chompLast( name, "Exception" );
160         }
161         if ( name.endsWith( "Error" ) )
162         {
163             return chompLast( name, "Error" );
164         }
165         return name;
166     }
167 
168     private static String truncateMessage( String msg, int i )
169     {
170         StringBuilder truncatedMessage = new StringBuilder();
171         if ( i >= 0 && msg != null )
172         {
173             truncatedMessage.append( ' ' )
174                     .append( msg.substring( 0, min( i, msg.length() ) ) );
175 
176             if ( i < msg.length() )
177             {
178                 truncatedMessage.append( "..." );
179             }
180         }
181         return truncatedMessage.toString();
182     }
183 
184     private boolean rootIsInclass()
185     {
186         return stackTrace.length > 0 && stackTrace[0].getClassName().equals( testClassName );
187     }
188 
189     private static List<StackTraceElement> focusOnClass( StackTraceElement[] stackTrace, Class<?> clazz )
190     {
191         List<StackTraceElement> result = new ArrayList<>();
192         for ( StackTraceElement element : stackTrace )
193         {
194             if ( element != null && isInSupers( clazz, element.getClassName() ) )
195             {
196                 result.add( element );
197             }
198         }
199         return result;
200     }
201 
202     private static boolean isInSupers( Class<?> testClass, String lookFor )
203     {
204         if ( lookFor.startsWith( "junit.framework." ) )
205         {
206             return false;
207         }
208         while ( !testClass.getName().equals( lookFor ) && testClass.getSuperclass() != null )
209         {
210             testClass = testClass.getSuperclass();
211         }
212         return testClass.getName().equals( lookFor );
213     }
214 
215     static Throwable findTopmostWithClass( final Throwable t, StackTraceFilter filter )
216     {
217         Throwable n = t;
218         do
219         {
220             if ( containsClassName( n.getStackTrace(), filter ) )
221             {
222                 return n;
223             }
224 
225             n = n.getCause();
226         }
227         while ( n != null );
228         return t;
229     }
230 
231     public static String stackTraceWithFocusOnClassAsString( Throwable t, String className )
232     {
233         StackTraceFilter filter = new ClassNameStackTraceFilter( className );
234         Throwable topmost = findTopmostWithClass( t, filter );
235         List<StackTraceElement> stackTraceElements = focusInsideClass( topmost.getStackTrace(), filter );
236         String s = causeToString( topmost.getCause(), filter );
237         return toString( t, stackTraceElements, filter ) + s;
238     }
239 
240     static List<StackTraceElement> focusInsideClass( StackTraceElement[] stackTrace, StackTraceFilter filter )
241     {
242         List<StackTraceElement> result = new ArrayList<>();
243         for ( StackTraceElement element : stackTrace )
244         {
245             if ( filter.matches( element ) )
246             {
247                 result.add( element );
248             }
249         }
250         return result;
251     }
252 
253     private static boolean containsClassName( StackTraceElement[] stackTrace, StackTraceFilter filter )
254     {
255         for ( StackTraceElement element : stackTrace )
256         {
257             if ( filter.matches( element ) )
258             {
259                 return true;
260             }
261         }
262         return false;
263     }
264 
265     private static String causeToString( Throwable cause, StackTraceFilter filter )
266     {
267         StringBuilder resp = new StringBuilder();
268         while ( cause != null )
269         {
270             resp.append( "Caused by: " );
271             resp.append( toString( cause, asList( cause.getStackTrace() ), filter ) );
272             cause = cause.getCause();
273         }
274         return resp.toString();
275     }
276 
277     private static String toString( Throwable t, Iterable<StackTraceElement> elements, StackTraceFilter filter )
278     {
279         StringBuilder result = new StringBuilder();
280         if ( t != null )
281         {
282             result.append( t.getClass().getName() );
283             String msg = t.getMessage();
284             if ( msg != null )
285             {
286                 result.append( ": " );
287                 if ( isMultiLine( msg ) )
288                 {
289                     // SUREFIRE-986
290                     result.append( '\n' );
291                 }
292                 result.append( msg );
293             }
294             result.append( '\n' );
295         }
296 
297         for ( StackTraceElement element : elements )
298         {
299             if ( filter.matches( element ) )
300             {
301                 result.append( "\tat " )
302                         .append( element )
303                         .append( '\n' );
304             }
305         }
306         return result.toString();
307     }
308 
309     private static boolean isMultiLine( String msg )
310     {
311         int countNewLines = 0;
312         for ( int i = 0, length = msg.length(); i < length; i++ )
313         {
314             if ( msg.charAt( i ) == '\n' )
315             {
316                 if ( ++countNewLines == 2 )
317                 {
318                     break;
319                 }
320             }
321         }
322         return countNewLines > 1 || countNewLines == 1 && !msg.trim().endsWith( "\n" );
323     }
324 }