• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.tradefed.result.ddmlib;
17 
18 import static com.android.ddmlib.testrunner.IInstrumentationResultParser.StatusKeys.KNOWN_KEYS;
19 
20 import com.android.annotations.NonNull;
21 import com.android.ddmlib.IShellOutputReceiver;
22 import com.android.ddmlib.MultiLineReceiver;
23 import com.android.ddmlib.testrunner.IInstrumentationResultParser;
24 import com.android.ddmlib.testrunner.ITestRunListener;
25 import com.android.ddmlib.testrunner.TestIdentifier;
26 import com.android.tradefed.log.Log;
27 
28 import java.text.NumberFormat;
29 import java.text.ParseException;
30 import java.util.ArrayList;
31 import java.util.Collection;
32 import java.util.Collections;
33 import java.util.HashMap;
34 import java.util.LinkedHashMap;
35 import java.util.Map;
36 import java.util.regex.Matcher;
37 import java.util.regex.Pattern;
38 
39 /**
40  * Parses the 'raw output mode' results of an instrumentation test run from shell and informs a
41  * ITestRunListener of the results.
42  *
43  * <p>Expects the following output:
44  *
45  * <p>If fatal error occurred when attempted to run the tests:
46  *
47  * <pre>
48  * INSTRUMENTATION_STATUS: Error=error Message
49  * INSTRUMENTATION_FAILED:
50  * </pre>
51  *
52  * <p>or
53  *
54  * <pre>
55  * INSTRUMENTATION_RESULT: shortMsg=error Message
56  * </pre>
57  *
58  * <p>Otherwise, expect a series of test results, each one containing a set of status key/value
59  * pairs, delimited by a start(1)/pass(0)/fail(-2)/error(-1) status code result. At end of test run,
60  * expects that the elapsed test time in seconds will be displayed
61  *
62  * <p>For example:
63  *
64  * <pre>
65  * INSTRUMENTATION_STATUS_CODE: 1
66  * INSTRUMENTATION_STATUS: class=com.foo.FooTest
67  * INSTRUMENTATION_STATUS: test=testFoo
68  * INSTRUMENTATION_STATUS: numtests=2
69  * INSTRUMENTATION_STATUS: stack=com.foo.FooTest#testFoo:312
70  *    com.foo.X
71  * INSTRUMENTATION_STATUS_CODE: -2
72  * ...
73  *
74  * Time: X
75  * </pre>
76  *
77  * <p>Note that the "value" portion of the key-value pair may wrap over several text lines
78  *
79  * <p>Use {@link InstrumentationProtoResultParser} instead. The proto based parser has additional
80  * information such as logcat message.
81  */
82 public class InstrumentationResultParser extends MultiLineReceiver
83         implements IInstrumentationResultParser {
84 
85     /** Prefixes used to identify output. */
86     private static class Prefixes {
87         private static final String STATUS = "INSTRUMENTATION_STATUS: ";
88         private static final String STATUS_CODE = "INSTRUMENTATION_STATUS_CODE: ";
89         private static final String STATUS_FAILED = "INSTRUMENTATION_FAILED: ";
90         private static final String STATUS_ABORTED = "INSTRUMENTATION_ABORTED: ";
91         private static final String ON_ERROR = "onError:";
92         private static final String CODE = "INSTRUMENTATION_CODE: ";
93         private static final String RESULT = "INSTRUMENTATION_RESULT: ";
94         private static final String TIME_REPORT = "Time: ";
95     }
96 
97     private final Collection<ITestRunListener> mTestListeners;
98 
99     /** Test result data */
100     private static class TestResult {
101         private Integer mCode = null;
102         private String mTestName = null;
103         private String mTestClass = null;
104         private String mStackTrace = null;
105         private Integer mNumTests = null;
106         private String mCurrentTestNumber = null;
107 
108         /** Returns true if all expected values have been parsed */
isComplete()109         boolean isComplete() {
110             return mCode != null && mTestName != null && mTestClass != null;
111         }
112 
113         /** Provides a more user readable string for TestResult, if possible */
114         @Override
toString()115         public String toString() {
116             StringBuilder output = new StringBuilder();
117             if (mTestClass != null) {
118                 output.append(mTestClass);
119                 output.append('#');
120             }
121             if (mTestName != null) {
122                 output.append(mTestName);
123             }
124             if (output.length() > 0) {
125                 return output.toString();
126             }
127             return "unknown result";
128         }
129     }
130 
131     /** the name to provide to {@link ITestRunListener#testRunStarted(String, int)} */
132     private final String mTestRunName;
133 
134     /** Stores the status values for the test result currently being parsed */
135     private TestResult mCurrentTestResult = null;
136 
137     /** Stores the status values for the test result last parsed */
138     private TestResult mLastTestResult = null;
139 
140     /** Stores the current "key" portion of the status key-value being parsed. */
141     private String mCurrentKey = null;
142 
143     /** Stores the current "value" portion of the status key-value being parsed. */
144     private StringBuilder mCurrentValue = null;
145 
146     /** True if start of test has already been reported to listener. */
147     private boolean mTestStartReported = false;
148 
149     /** True if the completion of the test run has been detected. */
150     private boolean mTestRunFinished = false;
151 
152     /** True if test run failure has already been reported to listener. */
153     private boolean mTestRunFailReported = false;
154 
155     /** The elapsed time of the test run, in milliseconds. */
156     private Long mTestTime = null;
157 
158     /** True if current test run has been canceled by user. */
159     private boolean mIsCancelled = false;
160 
161     /** The number of tests currently run */
162     private int mNumTestsRun = 0;
163 
164     /** The number of tests expected to run */
165     private int mNumTestsExpected = 0;
166 
167     /** True if the parser is parsing a line beginning with "INSTRUMENTATION_RESULT" */
168     private boolean mInInstrumentationResultKey = false;
169 
170     /** Contains the full error available in 'stream=' in case of test runner fatal exception. */
171     private String mStreamError = null;
172 
173     /** Contains the error message associated with the onError callback output. */
174     private String mOnError = null;
175 
176     /**
177      * Stores key-value pairs under INSTRUMENTATION_RESULT header, keeping the order in which they
178      * were reported. The {@link ITestRunListener}s may choose to display some or all of them when
179      * the test run ends.
180      */
181     private Map<String, String> mInstrumentationResultBundle = new LinkedHashMap<>();
182 
183     /**
184      * Stores key-value pairs of metrics emitted during the execution of each test case, keeping the
185      * order in which they were reported. Note that standard keys that are stored in the TestResults
186      * class are filtered out of this Map. The {@link ITestRunListener}s may choose to display some
187      * or all of them when the test case ends.
188      */
189     private Map<String, String> mTestMetrics = new LinkedHashMap<>();
190 
191     private static final String LOG_TAG = "InstrumentationResultParser";
192 
193     /** Error message supplied when no parseable test results are received from test run. */
194     static final String NO_TEST_RESULTS_MSG = "No test results";
195 
196     /** Error message supplied when a test start bundle is parsed, but not the test end bundle. */
197     static final String INCOMPLETE_TEST_ERR_MSG_PREFIX = "Test failed to run to completion";
198 
199     static final String INCOMPLETE_TEST_ERR_MSG_POSTFIX = "Check device logcat for details";
200 
201     /** Error message supplied when the test run is incomplete. */
202     static final String INCOMPLETE_RUN_ERR_MSG_PREFIX = "Test run failed to complete";
203 
204     /** Error message supplied from the test runner when some critical failure occurred */
205     static final String FATAL_EXCEPTION_MSG = "Fatal exception when running tests";
206 
207     /**
208      * Pattern for the instrumentation reported errors (fatal & non-fatal) printed at the end of the
209      * instrumentation.
210      */
211     static final Pattern INSTRUMENTATION_FAILURES_PATTERN =
212             Pattern.compile("There (was|were) (\\d+) failure(.*)", Pattern.DOTALL);
213 
214     /** Error message supplied when the test run output doesn't contain a valid time stamp. */
215     static final String INVALID_OUTPUT_ERR_MSG =
216             "Output from instrumentation is missing its time stamp";
217 
218     /**
219      * Creates the InstrumentationResultParser.
220      *
221      * @param runName the test run name to provide to {@link ITestRunListener#testRunStarted(String,
222      *     int)}
223      * @param listeners informed of test results as the tests are executing
224      */
InstrumentationResultParser(String runName, Collection<ITestRunListener> listeners)225     public InstrumentationResultParser(String runName, Collection<ITestRunListener> listeners) {
226         mTestRunName = runName;
227         mTestListeners = new ArrayList<ITestRunListener>(listeners);
228     }
229 
230     /**
231      * Creates the InstrumentationResultParser for a single listener.
232      *
233      * @param runName the test run name to provide to {@link ITestRunListener#testRunStarted(String,
234      *     int)}
235      * @param listener informed of test results as the tests are executing
236      */
InstrumentationResultParser(String runName, ITestRunListener listener)237     public InstrumentationResultParser(String runName, ITestRunListener listener) {
238         this(runName, Collections.singletonList(listener));
239     }
240 
241     /**
242      * Processes the instrumentation test output from shell.
243      *
244      * @see MultiLineReceiver#processNewLines
245      */
246     @Override
processNewLines(@onNull String[] lines)247     public void processNewLines(@NonNull String[] lines) {
248         for (String line : lines) {
249             parse(line);
250             // in verbose mode, dump all adb output to log
251             Log.v(LOG_TAG, line);
252         }
253     }
254 
255     /**
256      * Parse an individual output line. Expects a line that is one of:
257      *
258      * <ul>
259      *   <li>The start of a new status line (starts with Prefixes.STATUS or Prefixes.STATUS_CODE),
260      *       and thus there is a new key=value pair to parse, and the previous key-value pair is
261      *       finished.
262      *   <li>A continuation of the previous status (the "value" portion of the key has wrapped to
263      *       the next line).
264      *   <li>A line reporting a fatal error in the test run (Prefixes.STATUS_FAILED)
265      *   <li>A line reporting the total elapsed time of the test run. (Prefixes.TIME_REPORT)
266      * </ul>
267      *
268      * @param line Text output line
269      */
parse(String line)270     private void parse(String line) {
271         if (line.startsWith(Prefixes.STATUS_CODE)) {
272             // Previous status key-value has been collected. Store it.
273             submitCurrentKeyValue();
274             mInInstrumentationResultKey = false;
275             parseStatusCode(line);
276         } else if (line.startsWith(Prefixes.STATUS)) {
277             // Previous status key-value has been collected. Store it.
278             submitCurrentKeyValue();
279             mInInstrumentationResultKey = false;
280             parseKey(line, Prefixes.STATUS.length());
281         } else if (line.startsWith(Prefixes.RESULT)) {
282             // Previous status key-value has been collected. Store it.
283             submitCurrentKeyValue();
284             mInInstrumentationResultKey = true;
285             parseKey(line, Prefixes.RESULT.length());
286         } else if (line.startsWith(Prefixes.STATUS_FAILED) || line.startsWith(Prefixes.CODE)) {
287             // Previous status key-value has been collected. Store it.
288             submitCurrentKeyValue();
289             mInInstrumentationResultKey = false;
290             // these codes signal the end of the instrumentation run
291             mTestRunFinished = true;
292             // just ignore the remaining data on this line
293         } else if (line.startsWith(Prefixes.TIME_REPORT)) {
294             parseTime(line);
295         } else if (line.startsWith(Prefixes.ON_ERROR)) {
296             mOnError = line;
297         } else if (line.startsWith(Prefixes.STATUS_ABORTED)) {
298             if (mOnError == null) {
299                 mOnError = line;
300             }
301         } else {
302             if (mCurrentValue != null) {
303                 // this is a value that has wrapped to next line.
304                 mCurrentValue.append("\r\n");
305                 mCurrentValue.append(line);
306             } else if (!line.trim().isEmpty()) {
307                 Log.d(LOG_TAG, "unrecognized line " + line);
308             }
309         }
310     }
311 
312     /** Stores the currently parsed key-value pair in the appropriate place. */
submitCurrentKeyValue()313     private void submitCurrentKeyValue() {
314         if (mCurrentKey != null && mCurrentValue != null) {
315             String statusValue = mCurrentValue.toString();
316             if (mInInstrumentationResultKey) {
317                 if (!KNOWN_KEYS.contains(mCurrentKey)) {
318                     mInstrumentationResultBundle.put(mCurrentKey, statusValue);
319                 } else if (mCurrentKey.equals(StatusKeys.SHORTMSG)) {
320                     // test run must have failed
321                     handleTestRunFailed(
322                             String.format("Instrumentation run failed due to '%1$s'", statusValue));
323                 } else if (StatusKeys.STREAM.equals(mCurrentKey)) {
324                     if (statusValue != null) {
325                         if (statusValue.contains(FATAL_EXCEPTION_MSG)) {
326                             mStreamError = statusValue.trim();
327                         } else if (INSTRUMENTATION_FAILURES_PATTERN
328                                 .matcher(statusValue.trim())
329                                 .matches()) {
330                             mStreamError = statusValue.trim();
331                         }
332                     }
333                 }
334             } else {
335                 TestResult testInfo = getCurrentTestInfo();
336 
337                 if (mCurrentKey.equals(StatusKeys.CLASS)) {
338                     testInfo.mTestClass = statusValue.trim();
339                 } else if (mCurrentKey.equals(StatusKeys.TEST)) {
340                     testInfo.mTestName = statusValue.trim();
341                 } else if (mCurrentKey.equals(StatusKeys.NUMTESTS)) {
342                     try {
343                         testInfo.mNumTests = Integer.parseInt(statusValue);
344                     } catch (NumberFormatException e) {
345                         Log.w(
346                                 LOG_TAG,
347                                 "Unexpected integer number of tests, received " + statusValue);
348                     }
349                 } else if (mCurrentKey.equals(StatusKeys.ERROR)) {
350                     // test run must have failed
351                     handleTestRunFailed(statusValue);
352                 } else if (mCurrentKey.equals(StatusKeys.STACK)) {
353                     testInfo.mStackTrace = statusValue;
354                 } else if (StatusKeys.CURRENT.equals(mCurrentKey)) {
355                     testInfo.mCurrentTestNumber = statusValue;
356                 } else if (!KNOWN_KEYS.contains(mCurrentKey)) {
357                     // Not one of the recognized key/value pairs, so dump it in mTestMetrics
358                     mTestMetrics.put(mCurrentKey, statusValue);
359                 }
360             }
361 
362             mCurrentKey = null;
363             mCurrentValue = null;
364         }
365     }
366 
367     /**
368      * A utility method to return the test metrics from the current test case execution and get
369      * ready for the next one.
370      */
getAndResetTestMetrics()371     private Map<String, String> getAndResetTestMetrics() {
372         Map<String, String> retVal = mTestMetrics;
373         mTestMetrics = new HashMap<String, String>();
374         return retVal;
375     }
376 
getCurrentTestInfo()377     private TestResult getCurrentTestInfo() {
378         if (mCurrentTestResult == null) {
379             mCurrentTestResult = new TestResult();
380         }
381         return mCurrentTestResult;
382     }
383 
clearCurrentTestInfo()384     private void clearCurrentTestInfo() {
385         mLastTestResult = mCurrentTestResult;
386         mCurrentTestResult = null;
387     }
388 
389     /**
390      * Parses the key from the current line. Expects format of "key=value".
391      *
392      * @param line full line of text to parse
393      * @param keyStartPos the starting position of the key in the given line
394      */
parseKey(String line, int keyStartPos)395     private void parseKey(String line, int keyStartPos) {
396         int endKeyPos = line.indexOf('=', keyStartPos);
397         if (endKeyPos != -1) {
398             mCurrentKey = line.substring(keyStartPos, endKeyPos).trim();
399             parseValue(line, endKeyPos + 1);
400         }
401     }
402 
403     /**
404      * Parses the start of a key=value pair.
405      *
406      * @param line - full line of text to parse
407      * @param valueStartPos - the starting position of the value in the given line
408      */
parseValue(String line, int valueStartPos)409     private void parseValue(String line, int valueStartPos) {
410         mCurrentValue = new StringBuilder();
411         mCurrentValue.append(line.substring(valueStartPos));
412     }
413 
414     /** Parses out a status code result. */
parseStatusCode(String line)415     private void parseStatusCode(String line) {
416         String value = line.substring(Prefixes.STATUS_CODE.length()).trim();
417         TestResult testInfo = getCurrentTestInfo();
418         testInfo.mCode = StatusCodes.ERROR;
419         try {
420             testInfo.mCode = Integer.parseInt(value);
421         } catch (NumberFormatException e) {
422             Log.w(LOG_TAG, "Expected integer status code, received: " + value);
423             testInfo.mCode = StatusCodes.ERROR;
424         }
425         if (testInfo.mCode != StatusCodes.IN_PROGRESS) {
426             // this means we're done with current test result bundle
427             reportResult(testInfo);
428             clearCurrentTestInfo();
429         }
430     }
431 
432     /**
433      * Returns true if test run canceled.
434      *
435      * @see IShellOutputReceiver#isCancelled()
436      */
437     @Override
isCancelled()438     public boolean isCancelled() {
439         return mIsCancelled;
440     }
441 
442     /** Requests cancellation of test run. */
443     @Override
cancel()444     public void cancel() {
445         mIsCancelled = true;
446     }
447 
448     /**
449      * Reports a test result to the test run listener. Must be called when a individual test result
450      * has been fully parsed.
451      *
452      * @param testInfo The {@link TestResult} holding the current test infos.
453      */
reportResult(TestResult testInfo)454     private void reportResult(TestResult testInfo) {
455         if (!testInfo.isComplete()) {
456             Log.w(LOG_TAG, "invalid instrumentation status bundle " + testInfo.toString());
457             return;
458         }
459         reportTestRunStarted(testInfo);
460         TestIdentifier testId = new TestIdentifier(testInfo.mTestClass, testInfo.mTestName);
461         Map<String, String> metrics;
462 
463         switch (testInfo.mCode) {
464             case StatusCodes.START:
465                 for (ITestRunListener listener : mTestListeners) {
466                     listener.testStarted(testId);
467                 }
468                 break;
469             case StatusCodes.FAILURE:
470                 // If a test failure was already reported for the same test number
471                 // ('current' number), we avoid reporting a second repeated failure since it would
472                 // cause inconsistent events.
473                 if (mLastTestResult.mCurrentTestNumber != null
474                         && mLastTestResult.mCurrentTestNumber.equals(
475                                 mCurrentTestResult.mCurrentTestNumber)
476                         && mLastTestResult.mStackTrace != null) {
477                     Log.e(
478                             LOG_TAG,
479                             String.format(
480                                     "Ignoring repeated failed event for %s. Stack: %s",
481                                     mCurrentTestResult.toString(), mCurrentTestResult.mStackTrace));
482                     break;
483                 }
484                 metrics = getAndResetTestMetrics();
485                 for (ITestRunListener listener : mTestListeners) {
486                     listener.testFailed(testId, getTrace(testInfo));
487                     listener.testEnded(testId, metrics);
488                 }
489                 mNumTestsRun++;
490                 break;
491             case StatusCodes.ERROR:
492                 // we're dealing with a legacy JUnit3 runner that still reports errors.
493                 // just report this as a failure, since thats what upstream JUnit4 does
494                 metrics = getAndResetTestMetrics();
495                 for (ITestRunListener listener : mTestListeners) {
496                     listener.testFailed(testId, getTrace(testInfo));
497                     listener.testEnded(testId, metrics);
498                 }
499                 mNumTestsRun++;
500                 break;
501             case StatusCodes.IGNORED:
502                 metrics = getAndResetTestMetrics();
503                 for (ITestRunListener listener : mTestListeners) {
504                     listener.testIgnored(testId);
505                     listener.testEnded(testId, metrics);
506                 }
507                 mNumTestsRun++;
508                 break;
509             case StatusCodes.ASSUMPTION_FAILURE:
510                 metrics = getAndResetTestMetrics();
511                 for (ITestRunListener listener : mTestListeners) {
512                     listener.testAssumptionFailure(testId, getTrace(testInfo));
513                     listener.testEnded(testId, metrics);
514                 }
515                 mNumTestsRun++;
516                 break;
517             case StatusCodes.OK:
518                 metrics = getAndResetTestMetrics();
519                 for (ITestRunListener listener : mTestListeners) {
520                     listener.testEnded(testId, metrics);
521                 }
522                 mNumTestsRun++;
523                 break;
524             default:
525                 metrics = getAndResetTestMetrics();
526                 Log.e(LOG_TAG, "Unknown status code received: " + testInfo.mCode);
527                 for (ITestRunListener listener : mTestListeners) {
528                     listener.testEnded(testId, metrics);
529                 }
530                 mNumTestsRun++;
531                 break;
532         }
533     }
534 
535     /**
536      * Reports the start of a test run, and the total test count, if it has not been previously
537      * reported.
538      *
539      * @param testInfo current test status values
540      */
reportTestRunStarted(TestResult testInfo)541     private void reportTestRunStarted(TestResult testInfo) {
542         // if start test run not reported yet
543         if (!mTestStartReported && testInfo.mNumTests != null) {
544             for (ITestRunListener listener : mTestListeners) {
545                 listener.testRunStarted(mTestRunName, testInfo.mNumTests);
546             }
547             mNumTestsExpected = testInfo.mNumTests;
548             mTestStartReported = true;
549         }
550     }
551 
552     /** Returns the stack trace of the current failed test, from the provided testInfo. */
getTrace(TestResult testInfo)553     private String getTrace(TestResult testInfo) {
554         if (testInfo.mStackTrace != null) {
555             return testInfo.mStackTrace;
556         } else {
557             Log.e(LOG_TAG, "Could not find stack trace for failed test ");
558             return new Throwable("Unknown failure").toString();
559         }
560     }
561 
562     /**
563      * Parses out and store the elapsed time. Elapsed time format use comma separation above 1000.
564      * For example: "Time: 1,745.755" which should be handled.
565      */
parseTime(String line)566     private void parseTime(String line) {
567         final Pattern timePattern =
568                 Pattern.compile(String.format("%s\\s*([\\d\\,]*[\\d\\.]+)", Prefixes.TIME_REPORT));
569         Matcher timeMatcher = timePattern.matcher(line);
570         if (timeMatcher.find()) {
571             String timeString = timeMatcher.group(1);
572             try {
573                 Number n = NumberFormat.getInstance().parse(timeString);
574                 float timeSeconds = n.floatValue();
575                 mTestTime = (long) (timeSeconds * 1000);
576             } catch (ParseException e) {
577                 Log.w(LOG_TAG, String.format("Unexpected time format %1$s", line));
578             }
579         } else {
580             Log.w(LOG_TAG, String.format("Unexpected time format %1$s", line));
581         }
582     }
583 
584     @Override
handleTestRunFailed(@onNull String errorMsg)585     public void handleTestRunFailed(@NonNull String errorMsg) {
586         Log.i(LOG_TAG, String.format("test run failed: '%1$s'", errorMsg));
587         if (mLastTestResult != null
588                 && mLastTestResult.isComplete()
589                 && StatusCodes.START == mLastTestResult.mCode) {
590 
591             // received test start msg, but not test complete
592             // assume test caused this, report as test failure
593             TestIdentifier testId =
594                     new TestIdentifier(mLastTestResult.mTestClass, mLastTestResult.mTestName);
595             for (ITestRunListener listener : mTestListeners) {
596                 listener.testFailed(
597                         testId,
598                         String.format(
599                                 "%1$s. Reason: '%2$s'. %3$s",
600                                 INCOMPLETE_TEST_ERR_MSG_PREFIX,
601                                 errorMsg,
602                                 INCOMPLETE_TEST_ERR_MSG_POSTFIX));
603                 listener.testEnded(testId, getAndResetTestMetrics());
604             }
605         }
606         for (ITestRunListener listener : mTestListeners) {
607             if (!mTestStartReported) {
608                 // test run wasn't started - must have crashed before it started
609                 listener.testRunStarted(mTestRunName, 0);
610             }
611             String runErrorMsg = errorMsg;
612             if (mOnError != null) {
613                 runErrorMsg = String.format("%s. %s", errorMsg, mOnError);
614             } else if (mStreamError != null) {
615                 runErrorMsg = String.format("%s. %s", errorMsg, mStreamError);
616             }
617             listener.testRunFailed(runErrorMsg);
618 
619             if (mTestTime == null) {
620                 // We don't report an extra failure due to missing time stamp.
621                 mTestTime = 0L;
622             }
623             listener.testRunEnded(mTestTime, mInstrumentationResultBundle);
624         }
625         mOnError = null;
626         mTestStartReported = true;
627         mTestRunFailReported = true;
628     }
629 
630     /** Called by parent when adb session is complete. */
631     @Override
done()632     public void done() {
633         super.done();
634         if (!mTestRunFailReported) {
635             handleOutputDone();
636         }
637     }
638 
639     /** Handles the end of the adb session when a test run failure has not been reported yet */
handleOutputDone()640     private void handleOutputDone() {
641         if (!mTestStartReported && !mTestRunFinished) {
642             // no results
643             handleTestRunFailed(NO_TEST_RESULTS_MSG);
644         } else if (mNumTestsExpected > mNumTestsRun) {
645             final String message =
646                     String.format(
647                             "%1$s. Expected %2$d tests, received %3$d",
648                             INCOMPLETE_RUN_ERR_MSG_PREFIX, mNumTestsExpected, mNumTestsRun);
649             handleTestRunFailed(message);
650         } else {
651             if (!mTestStartReported) {
652                 // test run wasn't started, but it finished successfully. Must be a run with
653                 // no tests
654                 for (ITestRunListener listener : mTestListeners) {
655                     listener.testRunStarted(mTestRunName, 0);
656                 }
657             }
658             if (mTestTime == null) {
659                 mTestTime = 0L;
660             }
661             for (ITestRunListener listener : mTestListeners) {
662                 // If we haven't reported a failure yet
663                 if (!mTestRunFailReported
664                         && mStreamError != null
665                         && mStreamError.contains(FATAL_EXCEPTION_MSG)) {
666                     // If we reach here, this means the instrumentation fatally failed while being
667                     // in -e log true mode. Resulting in only the stream containing the exception.
668                     listener.testRunFailed(mStreamError.trim());
669                 }
670                 listener.testRunEnded(mTestTime, mInstrumentationResultBundle);
671             }
672         }
673     }
674 }
675