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.Collections;
26  import java.util.List;
27  
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 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             result.append( rootIsInclass() ? " " : " ยป " )
139                   .append( toMinimalThrowableMiniMessage( excType ) );
140         }
141 
142         if ( isNotEmpty( msg ) )
143         {
144             result.append( ' ' )
145                   .append( msg );
146         }
147         return result.toString();
148     }
149 
150     private static String toMinimalThrowableMiniMessage( Class<?> excType )
151     {
152         String name = excType.getSimpleName();
153         if ( name.endsWith( "Exception" ) )
154         {
155             return chompLast( name, "Exception" );
156         }
157         if ( name.endsWith( "Error" ) )
158         {
159             return chompLast( name, "Error" );
160         }
161         return name;
162     }
163 
164     private boolean rootIsInclass()
165     {
166         return stackTrace != null && stackTrace.length > 0 && stackTrace[0].getClassName().equals( testClassName );
167     }
168 
169     private static List<StackTraceElement> focusOnClass( StackTraceElement[] stackTrace, Class<?> clazz )
170     {
171         if ( stackTrace == null )
172         {
173             return Collections.emptyList();
174         }
175         List<StackTraceElement> result = new ArrayList<>();
176         for ( StackTraceElement element : stackTrace )
177         {
178             if ( element != null && isInSupers( clazz, element.getClassName() ) )
179             {
180                 result.add( element );
181             }
182         }
183         return result;
184     }
185 
186     private static boolean isInSupers( Class<?> testClass, String lookFor )
187     {
188         if ( lookFor.startsWith( "junit.framework." ) )
189         {
190             return false;
191         }
192         while ( !testClass.getName().equals( lookFor ) && testClass.getSuperclass() != null )
193         {
194             testClass = testClass.getSuperclass();
195         }
196         return testClass.getName().equals( lookFor );
197     }
198 
199     static Throwable findTopmostWithClass( final Throwable t, StackTraceFilter filter )
200     {
201         Throwable n = t;
202         do
203         {
204             if ( containsClassName( n.getStackTrace(), filter ) )
205             {
206                 return n;
207             }
208 
209             n = n.getCause();
210         }
211         while ( n != null );
212         return t;
213     }
214 
215     public static String stackTraceWithFocusOnClassAsString( Throwable t, String className )
216     {
217         StackTraceFilter filter = new ClassNameStackTraceFilter( className );
218         Throwable topmost = findTopmostWithClass( t, filter );
219         List<StackTraceElement> stackTraceElements = focusInsideClass( topmost.getStackTrace(), filter );
220         String s = causeToString( topmost.getCause(), filter );
221         return toString( t, stackTraceElements, filter ) + s;
222     }
223 
224     static List<StackTraceElement> focusInsideClass( StackTraceElement[] stackTrace, StackTraceFilter filter )
225     {
226         List<StackTraceElement> result = new ArrayList<>();
227         for ( StackTraceElement element : stackTrace )
228         {
229             if ( filter.matches( element ) )
230             {
231                 result.add( element );
232             }
233         }
234         return result;
235     }
236 
237     private static boolean containsClassName( StackTraceElement[] stackTrace, StackTraceFilter filter )
238     {
239         for ( StackTraceElement element : stackTrace )
240         {
241             if ( filter.matches( element ) )
242             {
243                 return true;
244             }
245         }
246         return false;
247     }
248 
249     private static String causeToString( Throwable cause, StackTraceFilter filter )
250     {
251         StringBuilder resp = new StringBuilder();
252         while ( cause != null )
253         {
254             resp.append( "Caused by: " );
255             resp.append( toString( cause, asList( cause.getStackTrace() ), filter ) );
256             cause = cause.getCause();
257         }
258         return resp.toString();
259     }
260 
261     private static String toString( Throwable t, Iterable<StackTraceElement> elements, StackTraceFilter filter )
262     {
263         StringBuilder result = new StringBuilder();
264         if ( t != null )
265         {
266             result.append( t.getClass().getName() );
267             String msg = t.getMessage();
268             if ( msg != null )
269             {
270                 result.append( ": " );
271                 if ( isMultiLine( msg ) )
272                 {
273                     // SUREFIRE-986
274                     result.append( '\n' );
275                 }
276                 result.append( msg );
277             }
278             result.append( '\n' );
279         }
280 
281         for ( StackTraceElement element : elements )
282         {
283             if ( filter.matches( element ) )
284             {
285                 result.append( "\tat " )
286                         .append( element )
287                         .append( '\n' );
288             }
289         }
290         return result.toString();
291     }
292 
293     private static boolean isMultiLine( String msg )
294     {
295         int countNewLines = 0;
296         for ( int i = 0, length = msg.length(); i < length; i++ )
297         {
298             if ( msg.charAt( i ) == '\n' )
299             {
300                 if ( ++countNewLines == 2 )
301                 {
302                     break;
303                 }
304             }
305         }
306         return countNewLines > 1 || countNewLines == 1 && !msg.trim().endsWith( "\n" );
307     }
308 }