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