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