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 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
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
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 }