blob: 71f329a94869d57a915f0130ff33cb796025d79c [file] [log] [blame]
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ddmlib.testrunner;
import com.android.ddmlib.IShellOutputReceiver;
import com.android.ddmlib.Log;
import com.android.ddmlib.MultiLineReceiver;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Parses the 'raw output mode' results of an instrumentation test run from shell and informs a
* ITestRunListener of the results.
*
* <p>Expects the following output:
*
* <p>If fatal error occurred when attempted to run the tests:
* <pre>
* INSTRUMENTATION_STATUS: Error=error Message
* INSTRUMENTATION_FAILED:
* </pre>
* <p>or
* <pre>
* INSTRUMENTATION_RESULT: shortMsg=error Message
* </pre>
*
* <p>Otherwise, expect a series of test results, each one containing a set of status key/value
* pairs, delimited by a start(1)/pass(0)/fail(-2)/error(-1) status code result. At end of test
* run, expects that the elapsed test time in seconds will be displayed
*
* <p>For example:
* <pre>
* INSTRUMENTATION_STATUS_CODE: 1
* INSTRUMENTATION_STATUS: class=com.foo.FooTest
* INSTRUMENTATION_STATUS: test=testFoo
* INSTRUMENTATION_STATUS: numtests=2
* INSTRUMENTATION_STATUS: stack=com.foo.FooTest#testFoo:312
* com.foo.X
* INSTRUMENTATION_STATUS_CODE: -2
* ...
*
* Time: X
* </pre>
* <p>Note that the "value" portion of the key-value pair may wrap over several text lines
*/
public class InstrumentationResultParser extends MultiLineReceiver {
/** Relevant test status keys. */
private static class StatusKeys {
private static final String TEST = "test";
private static final String CLASS = "class";
private static final String STACK = "stack";
private static final String NUMTESTS = "numtests";
private static final String ERROR = "Error";
private static final String SHORTMSG = "shortMsg";
}
/** The set of expected status keys. Used to filter which keys should be stored as metrics */
private static final Set<String> KNOWN_KEYS = new HashSet<String>();
static {
KNOWN_KEYS.add(StatusKeys.TEST);
KNOWN_KEYS.add(StatusKeys.CLASS);
KNOWN_KEYS.add(StatusKeys.STACK);
KNOWN_KEYS.add(StatusKeys.NUMTESTS);
KNOWN_KEYS.add(StatusKeys.ERROR);
KNOWN_KEYS.add(StatusKeys.SHORTMSG);
// unused, but regularly occurring status keys.
KNOWN_KEYS.add("stream");
KNOWN_KEYS.add("id");
KNOWN_KEYS.add("current");
}
/** Test result status codes. */
private static class StatusCodes {
private static final int FAILURE = -2;
private static final int START = 1;
private static final int ERROR = -1;
private static final int OK = 0;
private static final int IN_PROGRESS = 2;
}
/** Prefixes used to identify output. */
private static class Prefixes {
private static final String STATUS = "INSTRUMENTATION_STATUS: ";
private static final String STATUS_CODE = "INSTRUMENTATION_STATUS_CODE: ";
private static final String STATUS_FAILED = "INSTRUMENTATION_FAILED: ";
private static final String CODE = "INSTRUMENTATION_CODE: ";
private static final String RESULT = "INSTRUMENTATION_RESULT: ";
private static final String TIME_REPORT = "Time: ";
}
private final Collection<ITestRunListener> mTestListeners;
/**
* Test result data
*/
private static class TestResult {
private Integer mCode = null;
private String mTestName = null;
private String mTestClass = null;
private String mStackTrace = null;
private Integer mNumTests = null;
/** Returns true if all expected values have been parsed */
boolean isComplete() {
return mCode != null && mTestName != null && mTestClass != null;
}
/** Provides a more user readable string for TestResult, if possible */
@Override
public String toString() {
StringBuilder output = new StringBuilder();
if (mTestClass != null ) {
output.append(mTestClass);
output.append('#');
}
if (mTestName != null) {
output.append(mTestName);
}
if (output.length() > 0) {
return output.toString();
}
return "unknown result";
}
}
/** the name to provide to {@link ITestRunListener#testRunStarted(String, int)} */
private final String mTestRunName;
/** Stores the status values for the test result currently being parsed */
private TestResult mCurrentTestResult = null;
/** Stores the status values for the test result last parsed */
private TestResult mLastTestResult = null;
/** Stores the current "key" portion of the status key-value being parsed. */
private String mCurrentKey = null;
/** Stores the current "value" portion of the status key-value being parsed. */
private StringBuilder mCurrentValue = null;
/** True if start of test has already been reported to listener. */
private boolean mTestStartReported = false;
/** True if the completion of the test run has been detected. */
private boolean mTestRunFinished = false;
/** True if test run failure has already been reported to listener. */
private boolean mTestRunFailReported = false;
/** The elapsed time of the test run, in milliseconds. */
private long mTestTime = 0;
/** True if current test run has been canceled by user. */
private boolean mIsCancelled = false;
/** The number of tests currently run */
private int mNumTestsRun = 0;
/** The number of tests expected to run */
private int mNumTestsExpected = 0;
/** True if the parser is parsing a line beginning with "INSTRUMENTATION_RESULT" */
private boolean mInInstrumentationResultKey = false;
/**
* Stores key-value pairs under INSTRUMENTATION_RESULT header, these are printed at the
* end of a test run, if applicable
*/
private Map<String, String> mInstrumentationResultBundle = new HashMap<String, String>();
/**
* Stores key-value pairs of metrics emitted during the execution of each test case. Note that
* standard keys that are stored in the TestResults class are filtered out of this Map.
*/
private Map<String, String> mTestMetrics = new HashMap<String, String>();
private static final String LOG_TAG = "InstrumentationResultParser";
/** Error message supplied when no parseable test results are received from test run. */
static final String NO_TEST_RESULTS_MSG = "No test results";
/** Error message supplied when a test start bundle is parsed, but not the test end bundle. */
static final String INCOMPLETE_TEST_ERR_MSG_PREFIX = "Test failed to run to completion";
static final String INCOMPLETE_TEST_ERR_MSG_POSTFIX = "Check device logcat for details";
/** Error message supplied when the test run is incomplete. */
static final String INCOMPLETE_RUN_ERR_MSG_PREFIX = "Test run failed to complete";
/**
* Creates the InstrumentationResultParser.
*
* @param runName the test run name to provide to
* {@link ITestRunListener#testRunStarted(String, int)}
* @param listeners informed of test results as the tests are executing
*/
public InstrumentationResultParser(String runName, Collection<ITestRunListener> listeners) {
mTestRunName = runName;
mTestListeners = new ArrayList<ITestRunListener>(listeners);
}
/**
* Creates the InstrumentationResultParser for a single listener.
*
* @param runName the test run name to provide to
* {@link ITestRunListener#testRunStarted(String, int)}
* @param listener informed of test results as the tests are executing
*/
public InstrumentationResultParser(String runName, ITestRunListener listener) {
this(runName, Collections.singletonList(listener));
}
/**
* Processes the instrumentation test output from shell.
*
* @see MultiLineReceiver#processNewLines
*/
@Override
public void processNewLines(String[] lines) {
for (String line : lines) {
parse(line);
// in verbose mode, dump all adb output to log
Log.v(LOG_TAG, line);
}
}
/**
* Parse an individual output line. Expects a line that is one of:
* <ul>
* <li>
* The start of a new status line (starts with Prefixes.STATUS or Prefixes.STATUS_CODE),
* and thus there is a new key=value pair to parse, and the previous key-value pair is
* finished.
* </li>
* <li>
* A continuation of the previous status (the "value" portion of the key has wrapped
* to the next line).
* </li>
* <li> A line reporting a fatal error in the test run (Prefixes.STATUS_FAILED) </li>
* <li> A line reporting the total elapsed time of the test run. (Prefixes.TIME_REPORT) </li>
* </ul>
*
* @param line Text output line
*/
private void parse(String line) {
if (line.startsWith(Prefixes.STATUS_CODE)) {
// Previous status key-value has been collected. Store it.
submitCurrentKeyValue();
mInInstrumentationResultKey = false;
parseStatusCode(line);
} else if (line.startsWith(Prefixes.STATUS)) {
// Previous status key-value has been collected. Store it.
submitCurrentKeyValue();
mInInstrumentationResultKey = false;
parseKey(line, Prefixes.STATUS.length());
} else if (line.startsWith(Prefixes.RESULT)) {
// Previous status key-value has been collected. Store it.
submitCurrentKeyValue();
mInInstrumentationResultKey = true;
parseKey(line, Prefixes.RESULT.length());
} else if (line.startsWith(Prefixes.STATUS_FAILED) ||
line.startsWith(Prefixes.CODE)) {
// Previous status key-value has been collected. Store it.
submitCurrentKeyValue();
mInInstrumentationResultKey = false;
// these codes signal the end of the instrumentation run
mTestRunFinished = true;
// just ignore the remaining data on this line
} else if (line.startsWith(Prefixes.TIME_REPORT)) {
parseTime(line);
} else {
if (mCurrentValue != null) {
// this is a value that has wrapped to next line.
mCurrentValue.append("\r\n");
mCurrentValue.append(line);
} else if (line.trim().length() > 0){
Log.d(LOG_TAG, "unrecognized line " + line);
}
}
}
/**
* Stores the currently parsed key-value pair in the appropriate place.
*/
private void submitCurrentKeyValue() {
if (mCurrentKey != null && mCurrentValue != null) {
String statusValue = mCurrentValue.toString();
if (mInInstrumentationResultKey) {
if (!KNOWN_KEYS.contains(mCurrentKey)) {
mInstrumentationResultBundle.put(mCurrentKey, statusValue);
} else if (mCurrentKey.equals(StatusKeys.SHORTMSG)) {
// test run must have failed
handleTestRunFailed(String.format("Instrumentation run failed due to '%1$s'",
statusValue));
}
} else {
TestResult testInfo = getCurrentTestInfo();
if (mCurrentKey.equals(StatusKeys.CLASS)) {
testInfo.mTestClass = statusValue.trim();
} else if (mCurrentKey.equals(StatusKeys.TEST)) {
testInfo.mTestName = statusValue.trim();
} else if (mCurrentKey.equals(StatusKeys.NUMTESTS)) {
try {
testInfo.mNumTests = Integer.parseInt(statusValue);
} catch (NumberFormatException e) {
Log.w(LOG_TAG, "Unexpected integer number of tests, received "
+ statusValue);
}
} else if (mCurrentKey.equals(StatusKeys.ERROR)) {
// test run must have failed
handleTestRunFailed(statusValue);
} else if (mCurrentKey.equals(StatusKeys.STACK)) {
testInfo.mStackTrace = statusValue;
} else if (!KNOWN_KEYS.contains(mCurrentKey)) {
// Not one of the recognized key/value pairs, so dump it in mTestMetrics
mTestMetrics.put(mCurrentKey, statusValue);
}
}
mCurrentKey = null;
mCurrentValue = null;
}
}
/**
* A utility method to return the test metrics from the current test case execution and get
* ready for the next one.
*/
private Map<String, String> getAndResetTestMetrics() {
Map<String, String> retVal = mTestMetrics;
mTestMetrics = new HashMap<String, String>();
return retVal;
}
private TestResult getCurrentTestInfo() {
if (mCurrentTestResult == null) {
mCurrentTestResult = new TestResult();
}
return mCurrentTestResult;
}
private void clearCurrentTestInfo() {
mLastTestResult = mCurrentTestResult;
mCurrentTestResult = null;
}
/**
* Parses the key from the current line.
* Expects format of "key=value".
*
* @param line full line of text to parse
* @param keyStartPos the starting position of the key in the given line
*/
private void parseKey(String line, int keyStartPos) {
int endKeyPos = line.indexOf('=', keyStartPos);
if (endKeyPos != -1) {
mCurrentKey = line.substring(keyStartPos, endKeyPos).trim();
parseValue(line, endKeyPos + 1);
}
}
/**
* Parses the start of a key=value pair.
*
* @param line - full line of text to parse
* @param valueStartPos - the starting position of the value in the given line
*/
private void parseValue(String line, int valueStartPos) {
mCurrentValue = new StringBuilder();
mCurrentValue.append(line.substring(valueStartPos));
}
/**
* Parses out a status code result.
*/
private void parseStatusCode(String line) {
String value = line.substring(Prefixes.STATUS_CODE.length()).trim();
TestResult testInfo = getCurrentTestInfo();
testInfo.mCode = StatusCodes.ERROR;
try {
testInfo.mCode = Integer.parseInt(value);
} catch (NumberFormatException e) {
Log.w(LOG_TAG, "Expected integer status code, received: " + value);
testInfo.mCode = StatusCodes.ERROR;
}
if (testInfo.mCode != StatusCodes.IN_PROGRESS) {
// this means we're done with current test result bundle
reportResult(testInfo);
clearCurrentTestInfo();
}
}
/**
* Returns true if test run canceled.
*
* @see IShellOutputReceiver#isCancelled()
*/
@Override
public boolean isCancelled() {
return mIsCancelled;
}
/**
* Requests cancellation of test run.
*/
public void cancel() {
mIsCancelled = true;
}
/**
* Reports a test result to the test run listener. Must be called when a individual test
* result has been fully parsed.
*
* @param statusMap key-value status pairs of test result
*/
private void reportResult(TestResult testInfo) {
if (!testInfo.isComplete()) {
Log.w(LOG_TAG, "invalid instrumentation status bundle " + testInfo.toString());
return;
}
reportTestRunStarted(testInfo);
TestIdentifier testId = new TestIdentifier(testInfo.mTestClass, testInfo.mTestName);
Map<String, String> metrics;
switch (testInfo.mCode) {
case StatusCodes.START:
for (ITestRunListener listener : mTestListeners) {
listener.testStarted(testId);
}
break;
case StatusCodes.FAILURE:
metrics = getAndResetTestMetrics();
for (ITestRunListener listener : mTestListeners) {
listener.testFailed(ITestRunListener.TestFailure.FAILURE, testId,
getTrace(testInfo));
listener.testEnded(testId, metrics);
}
mNumTestsRun++;
break;
case StatusCodes.ERROR:
metrics = getAndResetTestMetrics();
for (ITestRunListener listener : mTestListeners) {
listener.testFailed(ITestRunListener.TestFailure.ERROR, testId,
getTrace(testInfo));
listener.testEnded(testId, metrics);
}
mNumTestsRun++;
break;
case StatusCodes.OK:
metrics = getAndResetTestMetrics();
for (ITestRunListener listener : mTestListeners) {
listener.testEnded(testId, metrics);
}
mNumTestsRun++;
break;
default:
metrics = getAndResetTestMetrics();
Log.e(LOG_TAG, "Unknown status code received: " + testInfo.mCode);
for (ITestRunListener listener : mTestListeners) {
listener.testEnded(testId, metrics);
}
mNumTestsRun++;
break;
}
}
/**
* Reports the start of a test run, and the total test count, if it has not been previously
* reported.
*
* @param testInfo current test status values
*/
private void reportTestRunStarted(TestResult testInfo) {
// if start test run not reported yet
if (!mTestStartReported && testInfo.mNumTests != null) {
for (ITestRunListener listener : mTestListeners) {
listener.testRunStarted(mTestRunName, testInfo.mNumTests);
}
mNumTestsExpected = testInfo.mNumTests;
mTestStartReported = true;
}
}
/**
* Returns the stack trace of the current failed test, from the provided testInfo.
*/
private String getTrace(TestResult testInfo) {
if (testInfo.mStackTrace != null) {
return testInfo.mStackTrace;
} else {
Log.e(LOG_TAG, "Could not find stack trace for failed test ");
return new Throwable("Unknown failure").toString();
}
}
/**
* Parses out and store the elapsed time.
*/
private void parseTime(String line) {
final Pattern timePattern = Pattern.compile(String.format("%s\\s*([\\d\\.]+)",
Prefixes.TIME_REPORT));
Matcher timeMatcher = timePattern.matcher(line);
if (timeMatcher.find()) {
String timeString = timeMatcher.group(1);
try {
float timeSeconds = Float.parseFloat(timeString);
mTestTime = (long) (timeSeconds * 1000);
} catch (NumberFormatException e) {
Log.w(LOG_TAG, String.format("Unexpected time format %1$s", line));
}
} else {
Log.w(LOG_TAG, String.format("Unexpected time format %1$s", line));
}
}
/**
* Inform the parser of a instrumentation run failure. Should be called when the adb command
* used to run the test fails.
*/
public void handleTestRunFailed(String errorMsg) {
errorMsg = (errorMsg == null ? "Unknown error" : errorMsg);
Log.i(LOG_TAG, String.format("test run failed: '%1$s'", errorMsg));
if (mLastTestResult != null &&
mLastTestResult.isComplete() &&
StatusCodes.START == mLastTestResult.mCode) {
// received test start msg, but not test complete
// assume test caused this, report as test failure
TestIdentifier testId = new TestIdentifier(mLastTestResult.mTestClass,
mLastTestResult.mTestName);
for (ITestRunListener listener : mTestListeners) {
listener.testFailed(ITestRunListener.TestFailure.ERROR, testId,
String.format("%1$s. Reason: '%2$s'. %3$s", INCOMPLETE_TEST_ERR_MSG_PREFIX,
errorMsg, INCOMPLETE_TEST_ERR_MSG_POSTFIX));
listener.testEnded(testId, getAndResetTestMetrics());
}
}
for (ITestRunListener listener : mTestListeners) {
if (!mTestStartReported) {
// test run wasn't started - must have crashed before it started
listener.testRunStarted(mTestRunName, 0);
}
listener.testRunFailed(errorMsg);
listener.testRunEnded(mTestTime, mInstrumentationResultBundle);
}
mTestStartReported = true;
mTestRunFailReported = true;
}
/**
* Called by parent when adb session is complete.
*/
@Override
public void done() {
super.done();
if (!mTestRunFailReported) {
handleOutputDone();
}
}
/**
* Handles the end of the adb session when a test run failure has not been reported yet
*/
private void handleOutputDone() {
if (!mTestStartReported && !mTestRunFinished) {
// no results
handleTestRunFailed(NO_TEST_RESULTS_MSG);
} else if (mNumTestsExpected > mNumTestsRun) {
final String message =
String.format("%1$s. Expected %2$d tests, received %3$d",
INCOMPLETE_RUN_ERR_MSG_PREFIX, mNumTestsExpected, mNumTestsRun);
handleTestRunFailed(message);
} else {
for (ITestRunListener listener : mTestListeners) {
if (!mTestStartReported) {
// test run wasn't started, but it finished successfully. Must be a run with
// no tests
listener.testRunStarted(mTestRunName, 0);
}
listener.testRunEnded(mTestTime, mInstrumentationResultBundle);
}
}
}
}