1 package org.apache.maven.surefire.report;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
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
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 }