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