View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.surefire.report;
20  
21  import java.util.ArrayList;
22  import java.util.Collections;
23  import java.util.List;
24  
25  import org.apache.maven.surefire.api.report.SafeThrowable;
26  
27  import static java.util.Arrays.asList;
28  import static java.util.Collections.reverse;
29  import static org.apache.maven.surefire.shared.utils.StringUtils.chompLast;
30  import static org.apache.maven.surefire.shared.utils.StringUtils.isNotEmpty;
31  
32  /**
33   * @author Kristian Rosenvold
34   */
35  @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
36  public class SmartStackTraceParser {
37      private final SafeThrowable throwable;
38  
39      private final StackTraceElement[] stackTrace;
40  
41      private final String testClassName;
42  
43      private final Class<?> testClass;
44  
45      private final String testMethodName;
46  
47      public SmartStackTraceParser(String testClassName, Throwable throwable, String testMethodName) {
48          this.testMethodName = testMethodName;
49          this.testClassName = testClassName;
50          testClass = toClass(testClassName);
51          this.throwable = new SafeThrowable(throwable);
52          stackTrace = throwable.getStackTrace();
53      }
54  
55      private static Class<?> toClass(String name) {
56          try {
57              ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
58              return classLoader == null ? null : classLoader.loadClass(name);
59          } catch (ClassNotFoundException e) {
60              return null;
61          }
62      }
63  
64      private static String toSimpleClassName(String className) {
65          int i = className.lastIndexOf(".");
66          return className.substring(i + 1);
67      }
68  
69      @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
70      public String getString() {
71          if (testClass == null) {
72              return throwable.getLocalizedMessage();
73          }
74  
75          final StringBuilder result = new StringBuilder();
76          final List<StackTraceElement> stackTraceElements = focusOnClass(stackTrace, testClass);
77          reverse(stackTraceElements);
78          final String testClassSimpleName = toSimpleClassName(testClassName);
79          if (stackTraceElements.isEmpty()) {
80              result.append(testClassSimpleName);
81              if (isNotEmpty(testMethodName)) {
82                  result.append(".").append(testMethodName);
83              }
84          } else {
85              for (int i = 0, size = stackTraceElements.size(); i < size; i++) {
86                  final StackTraceElement stackTraceElement = stackTraceElements.get(i);
87                  final boolean isTestClassName = stackTraceElement.getClassName().equals(testClassName);
88                  if (i == 0) {
89                      result.append(testClassSimpleName).append(isTestClassName ? '.' : '>');
90                  }
91  
92                  if (!isTestClassName) {
93                      result.append(toSimpleClassName(stackTraceElement.getClassName()))
94                              .append('.');
95                  }
96  
97                  result.append(stackTraceElement.getMethodName())
98                          .append(':')
99                          .append(stackTraceElement.getLineNumber())
100                         .append("->");
101             }
102 
103             if (result.length() >= 2) {
104                 result.setLength(result.length() - 2);
105             }
106         }
107 
108         final Throwable target = throwable.getTarget();
109         final Class<?> excType = target.getClass();
110         final String excClassName = excType.getName();
111         final String msg = throwable.getMessage();
112 
113         if (!(target instanceof AssertionError
114                 || "junit.framework.AssertionFailedError".equals(excClassName)
115                 || "junit.framework.ComparisonFailure".equals(excClassName)
116                 || excClassName.startsWith("org.opentest4j."))) {
117             result.append(rootIsInclass() ? " " : " ยป ").append(toMinimalThrowableMiniMessage(excType));
118         }
119 
120         if (isNotEmpty(msg)) {
121             result.append(' ').append(msg);
122         }
123         return result.toString();
124     }
125 
126     private static String toMinimalThrowableMiniMessage(Class<?> excType) {
127         String name = excType.getSimpleName();
128         if (name.endsWith("Exception")) {
129             return chompLast(name, "Exception");
130         }
131         if (name.endsWith("Error")) {
132             return chompLast(name, "Error");
133         }
134         return name;
135     }
136 
137     private boolean rootIsInclass() {
138         return stackTrace != null
139                 && stackTrace.length > 0
140                 && stackTrace[0].getClassName().equals(testClassName);
141     }
142 
143     private static List<StackTraceElement> focusOnClass(StackTraceElement[] stackTrace, Class<?> clazz) {
144         if (stackTrace == null) {
145             return Collections.emptyList();
146         }
147         List<StackTraceElement> result = new ArrayList<>();
148         for (StackTraceElement element : stackTrace) {
149             if (element != null && isInSupers(clazz, element.getClassName())) {
150                 result.add(element);
151             }
152         }
153         return result;
154     }
155 
156     private static boolean isInSupers(Class<?> testClass, String lookFor) {
157         if (lookFor.startsWith("junit.framework.")) {
158             return false;
159         }
160         while (!testClass.getName().equals(lookFor) && testClass.getSuperclass() != null) {
161             testClass = testClass.getSuperclass();
162         }
163         return testClass.getName().equals(lookFor);
164     }
165 
166     static Throwable findTopmostWithClass(final Throwable t, StackTraceFilter filter) {
167         Throwable n = t;
168         do {
169             if (containsClassName(n.getStackTrace(), filter)) {
170                 return n;
171             }
172 
173             n = n.getCause();
174         } while (n != null);
175         return t;
176     }
177 
178     public static String stackTraceWithFocusOnClassAsString(Throwable t, String className) {
179         StackTraceFilter filter = new ClassNameStackTraceFilter(className);
180         Throwable topmost = findTopmostWithClass(t, filter);
181         List<StackTraceElement> stackTraceElements = focusInsideClass(topmost.getStackTrace(), filter);
182         String s = causeToString(topmost.getCause(), filter);
183         return toString(t, stackTraceElements, filter) + s;
184     }
185 
186     static List<StackTraceElement> focusInsideClass(StackTraceElement[] stackTrace, StackTraceFilter filter) {
187         List<StackTraceElement> result = new ArrayList<>();
188         for (StackTraceElement element : stackTrace) {
189             if (filter.matches(element)) {
190                 result.add(element);
191             }
192         }
193         return result;
194     }
195 
196     private static boolean containsClassName(StackTraceElement[] stackTrace, StackTraceFilter filter) {
197         for (StackTraceElement element : stackTrace) {
198             if (filter.matches(element)) {
199                 return true;
200             }
201         }
202         return false;
203     }
204 
205     private static String causeToString(Throwable cause, StackTraceFilter filter) {
206         StringBuilder resp = new StringBuilder();
207         while (cause != null) {
208             resp.append("Caused by: ");
209             resp.append(toString(cause, asList(cause.getStackTrace()), filter));
210             cause = cause.getCause();
211         }
212         return resp.toString();
213     }
214 
215     private static String toString(Throwable t, Iterable<StackTraceElement> elements, StackTraceFilter filter) {
216         StringBuilder result = new StringBuilder();
217         if (t != null) {
218             result.append(t.getClass().getName());
219             String msg = t.getMessage();
220             if (msg != null) {
221                 result.append(": ");
222                 if (isMultiLine(msg)) {
223                     // SUREFIRE-986
224                     result.append('\n');
225                 }
226                 result.append(msg);
227             }
228             result.append('\n');
229         }
230 
231         for (StackTraceElement element : elements) {
232             if (filter.matches(element)) {
233                 result.append("\tat ").append(element).append('\n');
234             }
235         }
236         return result.toString();
237     }
238 
239     private static boolean isMultiLine(String msg) {
240         int countNewLines = 0;
241         for (int i = 0, length = msg.length(); i < length; i++) {
242             if (msg.charAt(i) == '\n') {
243                 if (++countNewLines == 2) {
244                     break;
245                 }
246             }
247         }
248         return countNewLines > 1 || countNewLines == 1 && !msg.trim().endsWith("\n");
249     }
250 }