Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit b012324

Browse files
brettchabotcopybara-androidxtest
authored andcommittedSep 2, 2020
Trim and truncate test failure stack traces for both orchestrator and classic/non-orchestrator modes.
This change should clean up test failure reporting by: - Remove test runner framework related stack frames - Truncate stack traces to a 64KB size when running under orchestrator to attempt to avoid binder transaction limits. This limit is already enforced when running in classic/non-orchestrator mode JUnit 4.13 has a really nice getTrimmedStackTrace feature, but androidx.test is fixed to 4.12 for the time being. So as a temporary workaround, copy the relevant JUnit utilty class into this project. Fixes #729, and hopefully #269 PiperOrigin-RevId: 329797783
1 parent 39788c7 commit b012324

File tree

8 files changed

+379
-39
lines changed

8 files changed

+379
-39
lines changed
 

‎runner/android_junit_runner/java/androidx/test/internal/runner/listener/InstrumentationResultPrinter.java

+3-13
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import android.os.Bundle;
2020
import androidx.annotation.VisibleForTesting;
2121
import android.util.Log;
22+
import androidx.test.services.events.internal.StackTrimmer;
2223
import java.io.PrintStream;
2324
import org.junit.internal.TextListener;
2425
import org.junit.runner.Description;
@@ -45,8 +46,6 @@ public class InstrumentationResultPrinter extends InstrumentationRunListener {
4546

4647
private static final String TAG = "InstrumentationResultPrinter";
4748

48-
@VisibleForTesting static final int MAX_TRACE_SIZE = 64 * 1024;
49-
5049
/**
5150
* This value, if stored with key {@link android.app.Instrumentation#REPORT_KEY_IDENTIFIER},
5251
* identifies AndroidJUnitRunner as the source of the report. This is sent with all status
@@ -175,21 +174,12 @@ public void testAssumptionFailure(Failure failure) {
175174
}
176175

177176
private void reportFailure(Failure failure) {
178-
String trace = failure.getTrace();
179-
if (trace.length() > MAX_TRACE_SIZE) {
180-
// Since AJUR needs to report failures back to AM via a binder IPC, we need to make sure that
181-
// we don't exceed the Binder transaction limit - which is 1MB per process.
182-
Log.w(
183-
TAG,
184-
String.format("Stack trace too long, trimmed to first %s characters.", MAX_TRACE_SIZE));
185-
trace = trace.substring(0, MAX_TRACE_SIZE) + "\n";
186-
}
177+
String trace = StackTrimmer.getTrimmedStackTrace(failure);
187178
testResult.putString(REPORT_KEY_STACK, trace);
188179
// pretty printing
189180
testResult.putString(
190181
Instrumentation.REPORT_KEY_STREAMRESULT,
191-
String.format(
192-
"\nError in %s:\n%s", failure.getDescription().getDisplayName(), failure.getTrace()));
182+
String.format("\nError in %s:\n%s", failure.getDescription().getDisplayName(), trace));
193183
}
194184

195185
@Override

‎runner/android_junit_runner/java/androidx/test/orchestrator/junit/ParcelableFailure.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@
1919
import android.os.Parcel;
2020
import android.os.Parcelable;
2121
import android.util.Log;
22+
import androidx.test.services.events.internal.StackTrimmer;
2223
import org.junit.runner.notification.Failure;
2324

24-
/** Parcelable imitation of a JUnit ParcelableFailure */
25+
/** Parcelable imitation of a JUnit Failure */
2526
public final class ParcelableFailure implements Parcelable {
2627

2728
private static final String TAG = "ParcelableFailure";
@@ -33,7 +34,7 @@ public final class ParcelableFailure implements Parcelable {
3334

3435
public ParcelableFailure(Failure failure) {
3536
this.description = new ParcelableDescription(failure.getDescription());
36-
this.trace = failure.getTrace();
37+
this.trace = StackTrimmer.getTrimmedStackTrace(failure);
3738
}
3839

3940
private ParcelableFailure(Parcel in) {

‎runner/android_junit_runner/javatests/androidx/test/internal/runner/listener/InstrumentationResultPrinterTest.java

+1-23
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package androidx.test.internal.runner.listener;
1818

19-
import static androidx.test.internal.runner.listener.InstrumentationResultPrinter.MAX_TRACE_SIZE;
2019
import static androidx.test.internal.runner.listener.InstrumentationResultPrinter.REPORT_KEY_STACK;
2120
import static junit.framework.Assert.assertEquals;
2221
import static junit.framework.Assert.assertTrue;
@@ -63,27 +62,6 @@ public void sendStatus(int code, Bundle bundle) {
6362
assertTrue(resultBundle[0].containsKey(REPORT_KEY_STACK));
6463
}
6564

66-
@Test
67-
public void verifyFailureStackTraceIsTruncated() throws Exception {
68-
InstrumentationResultPrinter intrResultPrinter = new InstrumentationResultPrinter();
69-
intrResultPrinter.testNum = 1;
70-
71-
Failure testFailure = new Failure(Description.EMPTY, new Exception(getVeryLargeString()));
72-
intrResultPrinter.testFailure(testFailure);
73-
74-
int testResultTraceLength =
75-
intrResultPrinter.testResult.getString(REPORT_KEY_STACK).length() - 1;
76-
assertTrue(
77-
String.format(
78-
"The stack trace length: %s, exceeds the max: %s",
79-
testResultTraceLength, MAX_TRACE_SIZE),
80-
testResultTraceLength <= MAX_TRACE_SIZE);
81-
}
82-
83-
private static String getVeryLargeString() {
84-
return new String(new char[1000000]);
85-
}
86-
8765
@Test
8866
public void verifyFailureDescriptionPropagatedToStartAndFinishMethods() throws Exception {
8967
Description[] descriptions = new Description[2];
@@ -101,7 +79,7 @@ public void testFinished(Description description) throws Exception {
10179
};
10280

10381
Description d = Description.createTestDescription(this.getClass(), "Failure Description");
104-
Failure testFailure = new Failure(d, new Exception(getVeryLargeString()));
82+
Failure testFailure = new Failure(d, new Exception());
10583
intrResultPrinter.testFailure(testFailure);
10684

10785
assertEquals(d, descriptions[0]);

‎services/events/java/androidx/test/services/events/ParcelableConverter.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import androidx.annotation.NonNull;
2222
import androidx.annotation.Nullable;
2323
import android.util.Log;
24+
import androidx.test.services.events.internal.StackTrimmer;
2425
import java.lang.annotation.Annotation;
2526
import java.lang.reflect.Array;
2627
import java.lang.reflect.Method;
@@ -99,7 +100,7 @@ public static FailureInfo getFailure(@NonNull Failure junitFailure) throws TestE
99100
return new FailureInfo(
100101
junitFailure.getMessage(),
101102
junitFailure.getTestHeader(),
102-
junitFailure.getTrace(),
103+
StackTrimmer.getTrimmedStackTrace(junitFailure),
103104
getTestCaseFromDescription(junitFailure.getDescription()));
104105
}
105106

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package androidx.test.services.events.internal;
2+
3+
import androidx.annotation.VisibleForTesting;
4+
import android.util.Log;
5+
import org.junit.runner.notification.Failure;
6+
7+
/** A utility for JUnit failure stack traces */
8+
public class StackTrimmer {
9+
10+
private static final String TAG = "StackTrimmer";
11+
12+
@VisibleForTesting static final int MAX_TRACE_SIZE = 64 * 1024;
13+
14+
/**
15+
* Returns the stack trace, trimming to remove frames from the test runner, and truncating if its
16+
* too large.
17+
*/
18+
public static String getTrimmedStackTrace(Failure failure) {
19+
// TODO(b/128614857): switch to JUnit 4.13 Failure.getTrimmedTrace once its available
20+
String trace = Throwables.getTrimmedStackTrace(failure.getException());
21+
if (trace.length() > MAX_TRACE_SIZE) {
22+
// Since AJUR needs to report failures back to AM via a binder IPC, we need to make sure that
23+
// we don't exceed the Binder transaction limit - which is 1MB per process.
24+
Log.w(
25+
TAG,
26+
String.format("Stack trace too long, trimmed to first %s characters.", MAX_TRACE_SIZE));
27+
trace = trace.substring(0, MAX_TRACE_SIZE) + "\n";
28+
}
29+
return trace;
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
package androidx.test.services.events.internal;
2+
3+
import java.io.BufferedReader;
4+
import java.io.IOException;
5+
import java.io.PrintWriter;
6+
import java.io.StringReader;
7+
import java.io.StringWriter;
8+
import java.lang.reflect.Method;
9+
import java.util.AbstractList;
10+
import java.util.ArrayList;
11+
import java.util.Arrays;
12+
import java.util.Collections;
13+
import java.util.List;
14+
15+
/**
16+
* Copy of JUnit 4.13 org.junit.internal.Throwables.java
17+
*
18+
* <p>TODO(b/128614857): Remove once androidx.test can use 4.13 directly
19+
*/
20+
final class Throwables {
21+
22+
private Throwables() {}
23+
24+
/**
25+
* Rethrows the given {@code Throwable}, allowing the caller to declare that it throws {@code
26+
* Exception}. This is useful when your callers have nothing reasonable they can do when a {@code
27+
* Throwable} is thrown. This is declared to return {@code Exception} so it can be used in a
28+
* {@code throw} clause:
29+
*
30+
* <pre>
31+
* try {
32+
* doSomething();
33+
* } catch (Throwable e} {
34+
* throw Throwables.rethrowAsException(e);
35+
* }
36+
* doSomethingLater();
37+
* </pre>
38+
*
39+
* @param e exception to rethrow
40+
* @return does not return anything
41+
* @since 4.12
42+
*/
43+
public static Exception rethrowAsException(Throwable e) throws Exception {
44+
Throwables.<Exception>rethrow(e);
45+
return null; // we never get here
46+
}
47+
48+
@SuppressWarnings("unchecked")
49+
private static <T extends Throwable> void rethrow(Throwable e) throws T {
50+
throw (T) e;
51+
}
52+
53+
/**
54+
* Returns the stacktrace of the given Throwable as a String.
55+
*
56+
* @since 4.13
57+
*/
58+
public static String getStacktrace(Throwable exception) {
59+
StringWriter stringWriter = new StringWriter();
60+
PrintWriter writer = new PrintWriter(stringWriter);
61+
exception.printStackTrace(writer);
62+
return stringWriter.toString();
63+
}
64+
65+
/**
66+
* Gets a trimmed version of the stack trace of the given exception. Stack trace elements that are
67+
* below the test method are filtered out.
68+
*
69+
* @return a trimmed stack trace, or the original trace if trimming wasn't possible
70+
*/
71+
public static String getTrimmedStackTrace(Throwable exception) {
72+
List<String> trimmedStackTraceLines = getTrimmedStackTraceLines(exception);
73+
if (trimmedStackTraceLines.isEmpty()) {
74+
return getFullStackTrace(exception);
75+
}
76+
77+
StringBuilder result = new StringBuilder(exception.toString());
78+
appendStackTraceLines(trimmedStackTraceLines, result);
79+
appendStackTraceLines(getCauseStackTraceLines(exception), result);
80+
return result.toString();
81+
}
82+
83+
private static List<String> getTrimmedStackTraceLines(Throwable exception) {
84+
List<StackTraceElement> stackTraceElements = Arrays.asList(exception.getStackTrace());
85+
int linesToInclude = stackTraceElements.size();
86+
87+
State state = State.PROCESSING_OTHER_CODE;
88+
for (StackTraceElement stackTraceElement : asReversedList(stackTraceElements)) {
89+
state = state.processStackTraceElement(stackTraceElement);
90+
if (state == State.DONE) {
91+
List<String> trimmedLines = new ArrayList<String>(linesToInclude + 2);
92+
trimmedLines.add("");
93+
for (StackTraceElement each : stackTraceElements.subList(0, linesToInclude)) {
94+
trimmedLines.add("\tat " + each);
95+
}
96+
if (exception.getCause() != null) {
97+
trimmedLines.add(
98+
"\t... " + (stackTraceElements.size() - trimmedLines.size()) + " trimmed");
99+
}
100+
return trimmedLines;
101+
}
102+
linesToInclude--;
103+
}
104+
return Collections.emptyList();
105+
}
106+
107+
private static final Method getSuppressed = initGetSuppressed();
108+
109+
private static Method initGetSuppressed() {
110+
try {
111+
return Throwable.class.getMethod("getSuppressed");
112+
} catch (Throwable e) {
113+
return null;
114+
}
115+
}
116+
117+
private static boolean hasSuppressed(Throwable exception) {
118+
if (getSuppressed == null) {
119+
return false;
120+
}
121+
try {
122+
Throwable[] suppressed = (Throwable[]) getSuppressed.invoke(exception);
123+
return suppressed.length != 0;
124+
} catch (Throwable e) {
125+
return false;
126+
}
127+
}
128+
129+
private static List<String> getCauseStackTraceLines(Throwable exception) {
130+
if (exception.getCause() != null || hasSuppressed(exception)) {
131+
String fullTrace = getFullStackTrace(exception);
132+
BufferedReader reader =
133+
new BufferedReader(new StringReader(fullTrace.substring(exception.toString().length())));
134+
List<String> causedByLines = new ArrayList<String>();
135+
136+
try {
137+
String line;
138+
while ((line = reader.readLine()) != null) {
139+
if (line.startsWith("Caused by: ") || line.trim().startsWith("Suppressed: ")) {
140+
causedByLines.add(line);
141+
while ((line = reader.readLine()) != null) {
142+
causedByLines.add(line);
143+
}
144+
return causedByLines;
145+
}
146+
}
147+
} catch (IOException e) {
148+
// We should never get here, because we are reading from a StringReader
149+
}
150+
}
151+
152+
return Collections.emptyList();
153+
}
154+
155+
private static String getFullStackTrace(Throwable exception) {
156+
StringWriter stringWriter = new StringWriter();
157+
PrintWriter writer = new PrintWriter(stringWriter);
158+
exception.printStackTrace(writer);
159+
return stringWriter.toString();
160+
}
161+
162+
private static void appendStackTraceLines(
163+
List<String> stackTraceLines, StringBuilder destBuilder) {
164+
for (String stackTraceLine : stackTraceLines) {
165+
destBuilder.append(String.format("%s%n", stackTraceLine));
166+
}
167+
}
168+
169+
private static <T> List<T> asReversedList(final List<T> list) {
170+
return new AbstractList<T>() {
171+
172+
@Override
173+
public T get(int index) {
174+
return list.get(list.size() - index - 1);
175+
}
176+
177+
@Override
178+
public int size() {
179+
return list.size();
180+
}
181+
};
182+
}
183+
184+
private enum State {
185+
PROCESSING_OTHER_CODE {
186+
@Override
187+
public State processLine(String methodName) {
188+
if (isTestFrameworkMethod(methodName)) {
189+
return PROCESSING_TEST_FRAMEWORK_CODE;
190+
}
191+
return this;
192+
}
193+
},
194+
PROCESSING_TEST_FRAMEWORK_CODE {
195+
@Override
196+
public State processLine(String methodName) {
197+
if (isReflectionMethod(methodName)) {
198+
return PROCESSING_REFLECTION_CODE;
199+
} else if (isTestFrameworkMethod(methodName)) {
200+
return this;
201+
}
202+
return PROCESSING_OTHER_CODE;
203+
}
204+
},
205+
PROCESSING_REFLECTION_CODE {
206+
@Override
207+
public State processLine(String methodName) {
208+
if (isReflectionMethod(methodName)) {
209+
return this;
210+
} else if (isTestFrameworkMethod(methodName)) {
211+
// This is here to handle TestCase.runBare() calling TestCase.runTest().
212+
return PROCESSING_TEST_FRAMEWORK_CODE;
213+
}
214+
return DONE;
215+
}
216+
},
217+
DONE {
218+
@Override
219+
public State processLine(String methodName) {
220+
return this;
221+
}
222+
};
223+
224+
/** Processes a stack trace element method name, possibly moving to a new state. */
225+
protected abstract State processLine(String methodName);
226+
227+
/** Processes a stack trace element, possibly moving to a new state. */
228+
public final State processStackTraceElement(StackTraceElement element) {
229+
return processLine(element.getClassName() + "." + element.getMethodName() + "()");
230+
}
231+
}
232+
233+
private static final String[] TEST_FRAMEWORK_METHOD_NAME_PREFIXES = {
234+
"org.junit.runner.",
235+
"org.junit.runners.",
236+
"org.junit.experimental.runners.",
237+
"org.junit.internal.",
238+
"junit.",
239+
};
240+
241+
private static final String[] TEST_FRAMEWORK_TEST_METHOD_NAME_PREFIXES = {
242+
"org.junit.internal.StackTracesTest",
243+
};
244+
245+
private static boolean isTestFrameworkMethod(String methodName) {
246+
return isMatchingMethod(methodName, TEST_FRAMEWORK_METHOD_NAME_PREFIXES)
247+
&& !isMatchingMethod(methodName, TEST_FRAMEWORK_TEST_METHOD_NAME_PREFIXES);
248+
}
249+
250+
private static final String[] REFLECTION_METHOD_NAME_PREFIXES = {
251+
"sun.reflect.",
252+
"java.lang.reflect.",
253+
"jdk.internal.reflect.",
254+
"org.junit.rules.RunRules.<init>(",
255+
"org.junit.rules.RunRules.applyAll(", // calls TestRules
256+
"org.junit.runners.RuleContainer.apply(", // calls MethodRules & TestRules
257+
"junit.framework.TestCase.runBare(", // runBare() directly calls setUp() and tearDown()
258+
};
259+
260+
private static boolean isReflectionMethod(String methodName) {
261+
return isMatchingMethod(methodName, REFLECTION_METHOD_NAME_PREFIXES);
262+
}
263+
264+
private static boolean isMatchingMethod(String methodName, String[] methodNamePrefixes) {
265+
for (String methodNamePrefix : methodNamePrefixes) {
266+
if (methodName.startsWith(methodNamePrefix)) {
267+
return true;
268+
}
269+
}
270+
271+
return false;
272+
}
273+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Tests for On-Device Infrastructure - Orchestrator
2+
licenses(["notice"])
3+
4+
android_local_test(
5+
name = "StackTrimmerTest",
6+
size = "small",
7+
srcs = [
8+
"StackTrimmerTest.java",
9+
],
10+
manifest_values = {
11+
"minSdkVersion": "14",
12+
},
13+
tags = ["robolectric"],
14+
deps = [
15+
"//ext/junit",
16+
"//services/events/java/androidx/test/services/events",
17+
"@maven//:junit_junit",,
18+
"@maven//:truth_truth",,
19+
],
20+
)
21+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package androidx.test.services.events.internal;
2+
3+
import static androidx.test.services.events.internal.StackTrimmer.MAX_TRACE_SIZE;
4+
import static com.google.common.truth.Truth.assertThat;
5+
import static junit.framework.Assert.assertTrue;
6+
7+
import androidx.test.ext.junit.runners.AndroidJUnit4;
8+
import org.junit.Test;
9+
import org.junit.runner.Description;
10+
import org.junit.runner.RunWith;
11+
import org.junit.runner.notification.Failure;
12+
13+
@RunWith(AndroidJUnit4.class)
14+
public class StackTrimmerTest {
15+
16+
@Test
17+
public void verifyFailureStackTraceIsTrimmed() throws Exception {
18+
19+
Failure testFailure = new Failure(Description.EMPTY, new Exception());
20+
21+
// ensure trace contains the current method, but not any of the junit + androidx.test framework
22+
// traces
23+
String trace = StackTrimmer.getTrimmedStackTrace(testFailure);
24+
assertThat(trace).contains("StackTrimmerTest.verifyFailureStackTraceIsTrimmed");
25+
// negative test are suspect, but just check for a few known trace elements
26+
assertThat(trace).doesNotContain("androidx.test.runner.AndroidJUnitRunner.onStart");
27+
assertThat(trace).doesNotContain("org.junit.runners.ParentRunner.run");
28+
}
29+
30+
@Test
31+
public void verifyFailureStackTraceIsTruncated() throws Exception {
32+
Failure testFailure = new Failure(Description.EMPTY, new Exception(getVeryLargeString()));
33+
34+
int testResultTraceLength = StackTrimmer.getTrimmedStackTrace(testFailure).length() - 1;
35+
assertTrue(
36+
String.format(
37+
"The stack trace length: %s, exceeds the max: %s",
38+
testResultTraceLength, MAX_TRACE_SIZE),
39+
testResultTraceLength <= MAX_TRACE_SIZE);
40+
}
41+
42+
private static String getVeryLargeString() {
43+
return new String(new char[1000000]);
44+
}
45+
}

0 commit comments

Comments
 (0)
Please sign in to comment.