• 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 
17 package com.android.ddmlib.testrunner;
18 
19 import com.android.ddmlib.IShellOutputReceiver;
20 import com.android.ddmlib.Log;
21 import com.android.ddmlib.MultiLineReceiver;
22 
23 import java.util.ArrayList;
24 import java.util.Collection;
25 import java.util.Collections;
26 import java.util.HashMap;
27 import java.util.HashSet;
28 import java.util.Map;
29 import java.util.Set;
30 import java.util.regex.Matcher;
31 import java.util.regex.Pattern;
32 
33 /**
34  * Parses the 'raw output mode' results of an instrumentation test run from shell and informs a
35  * ITestRunListener of the results.
36  *
37  * <p>Expects the following output:
38  *
39  * <p>If fatal error occurred when attempted to run the tests:
40  * <pre>
41  * INSTRUMENTATION_STATUS: Error=error Message
42  * INSTRUMENTATION_FAILED:
43  * </pre>
44  * <p>or
45  * <pre>
46  * INSTRUMENTATION_RESULT: shortMsg=error Message
47  * </pre>
48  *
49  * <p>Otherwise, expect a series of test results, each one containing a set of status key/value
50  * pairs, delimited by a start(1)/pass(0)/fail(-2)/error(-1) status code result. At end of test
51  * run, expects that the elapsed test time in seconds will be displayed
52  *
53  * <p>For example:
54  * <pre>
55  * INSTRUMENTATION_STATUS_CODE: 1
56  * INSTRUMENTATION_STATUS: class=com.foo.FooTest
57  * INSTRUMENTATION_STATUS: test=testFoo
58  * INSTRUMENTATION_STATUS: numtests=2
59  * INSTRUMENTATION_STATUS: stack=com.foo.FooTest#testFoo:312
60  *    com.foo.X
61  * INSTRUMENTATION_STATUS_CODE: -2
62  * ...
63  *
64  * Time: X
65  * </pre>
66  * <p>Note that the "value" portion of the key-value pair may wrap over several text lines
67  */
68 public class InstrumentationResultParser extends MultiLineReceiver {
69 
70     /** Relevant test status keys. */
71     private static class StatusKeys {
72         private static final String TEST = "test";
73         private static final String CLASS = "class";
74         private static final String STACK = "stack";
75         private static final String NUMTESTS = "numtests";
76         private static final String ERROR = "Error";
77         private static final String SHORTMSG = "shortMsg";
78     }
79 
80     /** The set of expected status keys. Used to filter which keys should be stored as metrics */
81     private static final Set<String> KNOWN_KEYS = new HashSet<String>();
82     static {
83         KNOWN_KEYS.add(StatusKeys.TEST);
84         KNOWN_KEYS.add(StatusKeys.CLASS);
85         KNOWN_KEYS.add(StatusKeys.STACK);
86         KNOWN_KEYS.add(StatusKeys.NUMTESTS);
87         KNOWN_KEYS.add(StatusKeys.ERROR);
88         KNOWN_KEYS.add(StatusKeys.SHORTMSG);
89         // unused, but regularly occurring status keys.
90         KNOWN_KEYS.add("stream");
91         KNOWN_KEYS.add("id");
92         KNOWN_KEYS.add("current");
93     }
94 
95     /** Test result status codes. */
96     private static class StatusCodes {
97         private static final int FAILURE = -2;
98         private static final int START = 1;
99         private static final int ERROR = -1;
100         private static final int OK = 0;
101         private static final int IN_PROGRESS = 2;
102     }
103 
104     /** Prefixes used to identify output. */
105     private static class Prefixes {
106         private static final String STATUS = "INSTRUMENTATION_STATUS: ";
107         private static final String STATUS_CODE = "INSTRUMENTATION_STATUS_CODE: ";
108         private static final String STATUS_FAILED = "INSTRUMENTATION_FAILED: ";
109         private static final String CODE = "INSTRUMENTATION_CODE: ";
110         private static final String RESULT = "INSTRUMENTATION_RESULT: ";
111         private static final String TIME_REPORT = "Time: ";
112     }
113 
114     private final Collection<ITestRunListener> mTestListeners;
115 
116     /**
117      * Test result data
118      */
119     private static class TestResult {
120         private Integer mCode = null;
121         private String mTestName = null;
122         private String mTestClass = null;
123         private String mStackTrace = null;
124         private Integer mNumTests = null;
125 
126         /** Returns true if all expected values have been parsed */
isComplete()127         boolean isComplete() {
128             return mCode != null && mTestName != null && mTestClass != null;
129         }
130 
131         /** Provides a more user readable string for TestResult, if possible */
132         @Override
toString()133         public String toString() {
134             StringBuilder output = new StringBuilder();
135             if (mTestClass != null ) {
136                 output.append(mTestClass);
137                 output.append('#');
138             }
139             if (mTestName != null) {
140                 output.append(mTestName);
141             }
142             if (output.length() > 0) {
143                 return output.toString();
144             }
145             return "unknown result";
146         }
147     }
148 
149     /** the name to provide to {@link ITestRunListener#testRunStarted(String, int)} */
150     private final String mTestRunName;
151 
152     /** Stores the status values for the test result currently being parsed */
153     private TestResult mCurrentTestResult = null;
154 
155     /** Stores the status values for the test result last parsed */
156     private TestResult mLastTestResult = null;
157 
158     /** Stores the current "key" portion of the status key-value being parsed. */
159     private String mCurrentKey = null;
160 
161     /** Stores the current "value" portion of the status key-value being parsed. */
162     private StringBuilder mCurrentValue = null;
163 
164     /** True if start of test has already been reported to listener. */
165     private boolean mTestStartReported = false;
166 
167     /** True if the completion of the test run has been detected. */
168     private boolean mTestRunFinished = false;
169 
170     /** True if test run failure has already been reported to listener. */
171     private boolean mTestRunFailReported = false;
172 
173     /** The elapsed time of the test run, in milliseconds. */
174     private long mTestTime = 0;
175 
176     /** True if current test run has been canceled by user. */
177     private boolean mIsCancelled = false;
178 
179     /** The number of tests currently run  */
180     private int mNumTestsRun = 0;
181 
182     /** The number of tests expected to run  */
183     private int mNumTestsExpected = 0;
184 
185     /** True if the parser is parsing a line beginning with "INSTRUMENTATION_RESULT" */
186     private boolean mInInstrumentationResultKey = false;
187 
188     /**
189      * Stores key-value pairs under INSTRUMENTATION_RESULT header, these are printed at the
190      * end of a test run, if applicable
191      */
192     private Map<String, String> mInstrumentationResultBundle = new HashMap<String, String>();
193 
194     /**
195      * Stores key-value pairs of metrics emitted during the execution of each test case.  Note that
196      * standard keys that are stored in the TestResults class are filtered out of this Map.
197      */
198     private Map<String, String> mTestMetrics = new HashMap<String, String>();
199 
200     private static final String LOG_TAG = "InstrumentationResultParser";
201 
202     /** Error message supplied when no parseable test results are received from test run. */
203     static final String NO_TEST_RESULTS_MSG = "No test results";
204 
205     /** Error message supplied when a test start bundle is parsed, but not the test end bundle. */
206     static final String INCOMPLETE_TEST_ERR_MSG_PREFIX = "Test failed to run to completion";
207     static final String INCOMPLETE_TEST_ERR_MSG_POSTFIX = "Check device logcat for details";
208 
209     /** Error message supplied when the test run is incomplete. */
210     static final String INCOMPLETE_RUN_ERR_MSG_PREFIX = "Test run failed to complete";
211 
212     /**
213      * Creates the InstrumentationResultParser.
214      *
215      * @param runName the test run name to provide to
216      *            {@link ITestRunListener#testRunStarted(String, int)}
217      * @param listeners informed of test results as the tests are executing
218      */
InstrumentationResultParser(String runName, Collection<ITestRunListener> listeners)219     public InstrumentationResultParser(String runName, Collection<ITestRunListener> listeners) {
220         mTestRunName = runName;
221         mTestListeners = new ArrayList<ITestRunListener>(listeners);
222     }
223 
224     /**
225      * Creates the InstrumentationResultParser for a single listener.
226      *
227      * @param runName the test run name to provide to
228      *            {@link ITestRunListener#testRunStarted(String, int)}
229      * @param listener informed of test results as the tests are executing
230      */
InstrumentationResultParser(String runName, ITestRunListener listener)231     public InstrumentationResultParser(String runName, ITestRunListener listener) {
232         this(runName, Collections.singletonList(listener));
233     }
234 
235     /**
236      * Processes the instrumentation test output from shell.
237      *
238      * @see MultiLineReceiver#processNewLines
239      */
240     @Override
processNewLines(String[] lines)241     public void processNewLines(String[] lines) {
242         for (String line : lines) {
243             parse(line);
244             // in verbose mode, dump all adb output to log
245             Log.v(LOG_TAG, line);
246         }
247     }
248 
249     /**
250      * Parse an individual output line. Expects a line that is one of:
251      * <ul>
252      * <li>
253      * The start of a new status line (starts with Prefixes.STATUS or Prefixes.STATUS_CODE),
254      * and thus there is a new key=value pair to parse, and the previous key-value pair is
255      * finished.
256      * </li>
257      * <li>
258      * A continuation of the previous status (the "value" portion of the key has wrapped
259      * to the next line).
260      * </li>
261      * <li> A line reporting a fatal error in the test run (Prefixes.STATUS_FAILED) </li>
262      * <li> A line reporting the total elapsed time of the test run. (Prefixes.TIME_REPORT) </li>
263      * </ul>
264      *
265      * @param line  Text output line
266      */
parse(String line)267     private void parse(String line) {
268         if (line.startsWith(Prefixes.STATUS_CODE)) {
269             // Previous status key-value has been collected. Store it.
270             submitCurrentKeyValue();
271             mInInstrumentationResultKey = false;
272             parseStatusCode(line);
273         } else if (line.startsWith(Prefixes.STATUS)) {
274             // Previous status key-value has been collected. Store it.
275             submitCurrentKeyValue();
276             mInInstrumentationResultKey = false;
277             parseKey(line, Prefixes.STATUS.length());
278         } else if (line.startsWith(Prefixes.RESULT)) {
279             // Previous status key-value has been collected. Store it.
280             submitCurrentKeyValue();
281             mInInstrumentationResultKey = true;
282             parseKey(line, Prefixes.RESULT.length());
283         } else if (line.startsWith(Prefixes.STATUS_FAILED) ||
284                    line.startsWith(Prefixes.CODE)) {
285             // Previous status key-value has been collected. Store it.
286             submitCurrentKeyValue();
287             mInInstrumentationResultKey = false;
288             // these codes signal the end of the instrumentation run
289             mTestRunFinished = true;
290             // just ignore the remaining data on this line
291         } else if (line.startsWith(Prefixes.TIME_REPORT)) {
292             parseTime(line);
293         } else {
294             if (mCurrentValue != null) {
295                 // this is a value that has wrapped to next line.
296                 mCurrentValue.append("\r\n");
297                 mCurrentValue.append(line);
298             } else if (line.trim().length() > 0){
299                 Log.d(LOG_TAG, "unrecognized line " + line);
300             }
301         }
302     }
303 
304     /**
305      * Stores the currently parsed key-value pair in the appropriate place.
306      */
submitCurrentKeyValue()307     private void submitCurrentKeyValue() {
308         if (mCurrentKey != null && mCurrentValue != null) {
309             String statusValue = mCurrentValue.toString();
310             if (mInInstrumentationResultKey) {
311                 if (!KNOWN_KEYS.contains(mCurrentKey)) {
312                     mInstrumentationResultBundle.put(mCurrentKey, statusValue);
313                 } else if (mCurrentKey.equals(StatusKeys.SHORTMSG)) {
314                     // test run must have failed
315                     handleTestRunFailed(String.format("Instrumentation run failed due to '%1$s'",
316                             statusValue));
317                 }
318             } else {
319                 TestResult testInfo = getCurrentTestInfo();
320 
321                 if (mCurrentKey.equals(StatusKeys.CLASS)) {
322                     testInfo.mTestClass = statusValue.trim();
323                 } else if (mCurrentKey.equals(StatusKeys.TEST)) {
324                     testInfo.mTestName = statusValue.trim();
325                 } else if (mCurrentKey.equals(StatusKeys.NUMTESTS)) {
326                     try {
327                         testInfo.mNumTests = Integer.parseInt(statusValue);
328                     } catch (NumberFormatException e) {
329                         Log.w(LOG_TAG, "Unexpected integer number of tests, received "
330                                 + statusValue);
331                     }
332                 } else if (mCurrentKey.equals(StatusKeys.ERROR)) {
333                     // test run must have failed
334                     handleTestRunFailed(statusValue);
335                 } else if (mCurrentKey.equals(StatusKeys.STACK)) {
336                     testInfo.mStackTrace = statusValue;
337                 } else if (!KNOWN_KEYS.contains(mCurrentKey)) {
338                     // Not one of the recognized key/value pairs, so dump it in mTestMetrics
339                     mTestMetrics.put(mCurrentKey, statusValue);
340                 }
341             }
342 
343             mCurrentKey = null;
344             mCurrentValue = null;
345         }
346     }
347 
348     /**
349      * A utility method to return the test metrics from the current test case execution and get
350      * ready for the next one.
351      */
getAndResetTestMetrics()352     private Map<String, String> getAndResetTestMetrics() {
353         Map<String, String> retVal = mTestMetrics;
354         mTestMetrics = new HashMap<String, String>();
355         return retVal;
356     }
357 
getCurrentTestInfo()358     private TestResult getCurrentTestInfo() {
359         if (mCurrentTestResult == null) {
360             mCurrentTestResult = new TestResult();
361         }
362         return mCurrentTestResult;
363     }
364 
clearCurrentTestInfo()365     private void clearCurrentTestInfo() {
366         mLastTestResult = mCurrentTestResult;
367         mCurrentTestResult = null;
368     }
369 
370     /**
371      * Parses the key from the current line.
372      * Expects format of "key=value".
373      *
374      * @param line full line of text to parse
375      * @param keyStartPos the starting position of the key in the given line
376      */
parseKey(String line, int keyStartPos)377     private void parseKey(String line, int keyStartPos) {
378         int endKeyPos = line.indexOf('=', keyStartPos);
379         if (endKeyPos != -1) {
380             mCurrentKey = line.substring(keyStartPos, endKeyPos).trim();
381             parseValue(line, endKeyPos + 1);
382         }
383     }
384 
385     /**
386      * Parses the start of a key=value pair.
387      *
388      * @param line - full line of text to parse
389      * @param valueStartPos - the starting position of the value in the given line
390      */
parseValue(String line, int valueStartPos)391     private void parseValue(String line, int valueStartPos) {
392         mCurrentValue = new StringBuilder();
393         mCurrentValue.append(line.substring(valueStartPos));
394     }
395 
396     /**
397      * Parses out a status code result.
398      */
parseStatusCode(String line)399     private void parseStatusCode(String line) {
400         String value = line.substring(Prefixes.STATUS_CODE.length()).trim();
401         TestResult testInfo = getCurrentTestInfo();
402         testInfo.mCode = StatusCodes.ERROR;
403         try {
404             testInfo.mCode = Integer.parseInt(value);
405         } catch (NumberFormatException e) {
406             Log.w(LOG_TAG, "Expected integer status code, received: " + value);
407             testInfo.mCode = StatusCodes.ERROR;
408         }
409         if (testInfo.mCode != StatusCodes.IN_PROGRESS) {
410             // this means we're done with current test result bundle
411             reportResult(testInfo);
412             clearCurrentTestInfo();
413         }
414     }
415 
416     /**
417      * Returns true if test run canceled.
418      *
419      * @see IShellOutputReceiver#isCancelled()
420      */
421     @Override
isCancelled()422     public boolean isCancelled() {
423         return mIsCancelled;
424     }
425 
426     /**
427      * Requests cancellation of test run.
428      */
cancel()429     public void cancel() {
430         mIsCancelled = true;
431     }
432 
433     /**
434      * Reports a test result to the test run listener. Must be called when a individual test
435      * result has been fully parsed.
436      *
437      * @param statusMap key-value status pairs of test result
438      */
reportResult(TestResult testInfo)439     private void reportResult(TestResult testInfo) {
440         if (!testInfo.isComplete()) {
441             Log.w(LOG_TAG, "invalid instrumentation status bundle " + testInfo.toString());
442             return;
443         }
444         reportTestRunStarted(testInfo);
445         TestIdentifier testId = new TestIdentifier(testInfo.mTestClass, testInfo.mTestName);
446         Map<String, String> metrics;
447 
448         switch (testInfo.mCode) {
449             case StatusCodes.START:
450                 for (ITestRunListener listener : mTestListeners) {
451                     listener.testStarted(testId);
452                 }
453                 break;
454             case StatusCodes.FAILURE:
455                 metrics = getAndResetTestMetrics();
456                 for (ITestRunListener listener : mTestListeners) {
457                     listener.testFailed(ITestRunListener.TestFailure.FAILURE, testId,
458                         getTrace(testInfo));
459 
460                     listener.testEnded(testId, metrics);
461                 }
462                 mNumTestsRun++;
463                 break;
464             case StatusCodes.ERROR:
465                 metrics = getAndResetTestMetrics();
466                 for (ITestRunListener listener : mTestListeners) {
467                     listener.testFailed(ITestRunListener.TestFailure.ERROR, testId,
468                         getTrace(testInfo));
469                     listener.testEnded(testId, metrics);
470                 }
471                 mNumTestsRun++;
472                 break;
473             case StatusCodes.OK:
474                 metrics = getAndResetTestMetrics();
475                 for (ITestRunListener listener : mTestListeners) {
476                     listener.testEnded(testId, metrics);
477                 }
478                 mNumTestsRun++;
479                 break;
480             default:
481                 metrics = getAndResetTestMetrics();
482                 Log.e(LOG_TAG, "Unknown status code received: " + testInfo.mCode);
483                 for (ITestRunListener listener : mTestListeners) {
484                     listener.testEnded(testId, metrics);
485                 }
486                 mNumTestsRun++;
487             break;
488         }
489 
490     }
491 
492     /**
493      * Reports the start of a test run, and the total test count, if it has not been previously
494      * reported.
495      *
496      * @param testInfo current test status values
497      */
reportTestRunStarted(TestResult testInfo)498     private void reportTestRunStarted(TestResult testInfo) {
499         // if start test run not reported yet
500         if (!mTestStartReported && testInfo.mNumTests != null) {
501             for (ITestRunListener listener : mTestListeners) {
502                 listener.testRunStarted(mTestRunName, testInfo.mNumTests);
503             }
504             mNumTestsExpected = testInfo.mNumTests;
505             mTestStartReported = true;
506         }
507     }
508 
509     /**
510      * Returns the stack trace of the current failed test, from the provided testInfo.
511      */
getTrace(TestResult testInfo)512     private String getTrace(TestResult testInfo) {
513         if (testInfo.mStackTrace != null) {
514             return testInfo.mStackTrace;
515         } else {
516             Log.e(LOG_TAG, "Could not find stack trace for failed test ");
517             return new Throwable("Unknown failure").toString();
518         }
519     }
520 
521     /**
522      * Parses out and store the elapsed time.
523      */
parseTime(String line)524     private void parseTime(String line) {
525         final Pattern timePattern = Pattern.compile(String.format("%s\\s*([\\d\\.]+)",
526                 Prefixes.TIME_REPORT));
527         Matcher timeMatcher = timePattern.matcher(line);
528         if (timeMatcher.find()) {
529             String timeString = timeMatcher.group(1);
530             try {
531                 float timeSeconds = Float.parseFloat(timeString);
532                 mTestTime = (long) (timeSeconds * 1000);
533             } catch (NumberFormatException e) {
534                 Log.w(LOG_TAG, String.format("Unexpected time format %1$s", line));
535             }
536         } else {
537             Log.w(LOG_TAG, String.format("Unexpected time format %1$s", line));
538         }
539     }
540 
541     /**
542      * Inform the parser of a instrumentation run failure. Should be called when the adb command
543      * used to run the test fails.
544      */
handleTestRunFailed(String errorMsg)545     public void handleTestRunFailed(String errorMsg) {
546         errorMsg = (errorMsg == null ? "Unknown error" : errorMsg);
547         Log.i(LOG_TAG, String.format("test run failed: '%1$s'", errorMsg));
548         if (mLastTestResult != null &&
549             mLastTestResult.isComplete() &&
550             StatusCodes.START == mLastTestResult.mCode) {
551 
552             // received test start msg, but not test complete
553             // assume test caused this, report as test failure
554             TestIdentifier testId = new TestIdentifier(mLastTestResult.mTestClass,
555                     mLastTestResult.mTestName);
556             for (ITestRunListener listener : mTestListeners) {
557                 listener.testFailed(ITestRunListener.TestFailure.ERROR, testId,
558                     String.format("%1$s. Reason: '%2$s'. %3$s", INCOMPLETE_TEST_ERR_MSG_PREFIX,
559                             errorMsg, INCOMPLETE_TEST_ERR_MSG_POSTFIX));
560                 listener.testEnded(testId, getAndResetTestMetrics());
561             }
562         }
563         for (ITestRunListener listener : mTestListeners) {
564             if (!mTestStartReported) {
565                 // test run wasn't started - must have crashed before it started
566                 listener.testRunStarted(mTestRunName, 0);
567             }
568             listener.testRunFailed(errorMsg);
569             listener.testRunEnded(mTestTime, mInstrumentationResultBundle);
570         }
571         mTestStartReported = true;
572         mTestRunFailReported = true;
573     }
574 
575     /**
576      * Called by parent when adb session is complete.
577      */
578     @Override
done()579     public void done() {
580         super.done();
581         if (!mTestRunFailReported) {
582             handleOutputDone();
583         }
584     }
585 
586     /**
587      * Handles the end of the adb session when a test run failure has not been reported yet
588      */
handleOutputDone()589     private void handleOutputDone() {
590         if (!mTestStartReported && !mTestRunFinished) {
591             // no results
592             handleTestRunFailed(NO_TEST_RESULTS_MSG);
593         } else if (mNumTestsExpected > mNumTestsRun) {
594             final String message =
595                 String.format("%1$s. Expected %2$d tests, received %3$d",
596                         INCOMPLETE_RUN_ERR_MSG_PREFIX, mNumTestsExpected, mNumTestsRun);
597             handleTestRunFailed(message);
598         } else {
599             for (ITestRunListener listener : mTestListeners) {
600                 if (!mTestStartReported) {
601                     // test run wasn't started, but it finished successfully. Must be a run with
602                     // no tests
603                     listener.testRunStarted(mTestRunName, 0);
604                 }
605                 listener.testRunEnded(mTestTime, mInstrumentationResultBundle);
606             }
607         }
608     }
609 }
610