blob: 4c29b0195575b41926c046206cfc0153bdb15ad8 [file] [log] [blame]
/*
* Copyright (C) 2010 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.cts.tradefed.result;
import com.android.cts.tradefed.device.DeviceInfoCollector;
import com.android.cts.tradefed.targetsetup.CtsBuildHelper;
import com.android.ddmlib.Log;
import com.android.ddmlib.Log.LogLevel;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.android.tradefed.config.Option;
import com.android.tradefed.result.CollectingTestListener;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.result.TestResult;
import com.android.tradefed.result.TestRunResult;
import com.android.tradefed.result.TestResult.TestStatus;
import com.android.tradefed.targetsetup.IBuildInfo;
import com.android.tradefed.targetsetup.IFolderBuildInfo;
import com.android.tradefed.util.FileUtil;
import org.kxml2.io.KXmlSerializer;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* Writes results to an XML files in the CTS format.
* <p/>
* Collects all test info in memory, then dumps to file when invocation is complete.
* <p/>
* Outputs xml in format governed by the cts_result.xsd
*/
public class CtsXmlResultReporter extends CollectingTestListener {
private static final String LOG_TAG = "CtsXmlResultReporter";
private static final String TEST_RESULT_FILE_NAME = "testResult.xml";
private static final String CTS_RESULT_FILE_VERSION = "2.0";
private static final String CTS_VERSION = "99";
private static final String[] CTS_RESULT_RESOURCES = {"cts_result.xsl", "cts_result.css",
"logo.gif", "newrule-green.png"};
/** the XML namespace */
private static final String ns = null;
private static final String REPORT_DIR_NAME = "output-file-path";
@Option(name=REPORT_DIR_NAME, description="root file system path to directory to store xml " +
"test results and associated logs. If not specified, results will be stored at " +
"<cts root>/repository/results")
protected File mReportDir = null;
protected IBuildInfo mBuildInfo;
private String mStartTime;
public void setReportDir(File reportDir) {
mReportDir = reportDir;
}
/**
* {@inheritDoc}
*/
@Override
public void invocationStarted(IBuildInfo buildInfo) {
super.invocationStarted(buildInfo);
if (mReportDir == null) {
if (!(buildInfo instanceof IFolderBuildInfo)) {
throw new IllegalArgumentException("build info is not a IFolderBuildInfo");
}
IFolderBuildInfo ctsBuild = (IFolderBuildInfo)buildInfo;
try {
CtsBuildHelper buildHelper = new CtsBuildHelper(ctsBuild.getRootDir());
mReportDir = buildHelper.getResultsDir();
} catch (FileNotFoundException e) {
throw new IllegalArgumentException("unrecognized cts structure", e);
}
}
// create a unique directory for saving results, using old cts host convention
// TODO: in future, consider using LogFileSaver to create build-specific directories
mReportDir = new File(mReportDir, getResultTimestamp());
mReportDir.mkdirs();
mStartTime = getTimestamp();
}
/**
* {@inheritDoc}
*/
@Override
public void testLog(String dataName, LogDataType dataType, InputStream dataStream) {
// TODO: implement this
}
/**
* {@inheritDoc}
*/
@Override
public void testFailed(TestFailure status, TestIdentifier test, String trace) {
super.testFailed(status, test, trace);
Log.i(LOG_TAG, String.format("Test %s#%s: %s\n%s", test.getClassName(), test.getTestName(),
status.toString(), trace));
}
/**
* {@inheritDoc}
*/
@Override
public void testRunEnded(long elapsedTime, Map<String, String> runMetrics) {
super.testRunEnded(elapsedTime, runMetrics);
Log.i(LOG_TAG, String.format("Test run %s complete. Tests passed %d, failed %d, error %d",
getCurrentRunResults().getName(), getCurrentRunResults().getNumPassedTests(),
getCurrentRunResults().getNumFailedTests(),
getCurrentRunResults().getNumErrorTests()));
}
/**
* {@inheritDoc}
*/
@Override
public void invocationEnded(long elapsedTime) {
super.invocationEnded(elapsedTime);
createXmlResult(mReportDir, mStartTime, elapsedTime);
copyFormattingFiles(mReportDir);
zipResults(mReportDir);
}
/**
* Creates a report file and populates it with the report data from the completed tests.
*/
private void createXmlResult(File reportDir, String startTimestamp, long elapsedTime) {
String endTime = getTimestamp();
OutputStream stream = null;
try {
stream = createOutputResultStream(reportDir);
KXmlSerializer serializer = new KXmlSerializer();
serializer.setOutput(stream, "UTF-8");
serializer.startDocument("UTF-8", false);
serializer.setFeature(
"http://xmlpull.org/v1/doc/features.html#indent-output", true);
serializer.processingInstruction("xml-stylesheet type=\"text/xsl\" " +
"href=\"cts_result.xsl\"");
serializeResultsDoc(serializer, startTimestamp, endTime);
serializer.endDocument();
// TODO: output not executed timeout omitted counts
String msg = String.format("XML test result file generated at %s. Total tests %d, " +
"Failed %d, Error %d", reportDir.getAbsolutePath(), getNumTotalTests(),
getNumFailedTests(), getNumErrorTests());
Log.logAndDisplay(LogLevel.INFO, LOG_TAG, msg);
Log.logAndDisplay(LogLevel.INFO, LOG_TAG, String.format("Time: %s",
formatElapsedTime(elapsedTime)));
} catch (IOException e) {
Log.e(LOG_TAG, "Failed to generate report data");
} finally {
if (stream != null) {
try {
stream.close();
} catch (IOException ignored) {
}
}
}
}
/**
* Output the results XML.
*
* @param serializer the {@link KXmlSerializer} to use
* @param startTime the user-friendly starting time of the test invocation
* @param endTime the user-friendly ending time of the test invocation
* @throws IOException
*/
private void serializeResultsDoc(KXmlSerializer serializer, String startTime, String endTime)
throws IOException {
serializer.startTag(ns, "TestResult");
// TODO: output test plan and profile values
serializer.attribute(ns, "testPlan", "unknown");
serializer.attribute(ns, "profile", "unknown");
serializer.attribute(ns, "starttime", startTime);
serializer.attribute(ns, "endtime", endTime);
serializer.attribute(ns, "version", CTS_RESULT_FILE_VERSION);
serializeDeviceInfo(serializer);
serializeHostInfo(serializer);
serializeTestSummary(serializer);
serializeTestResults(serializer);
}
/**
* Output the device info XML.
*
* @param serializer
*/
private void serializeDeviceInfo(KXmlSerializer serializer) throws IOException {
serializer.startTag(ns, "DeviceInfo");
TestRunResult deviceInfoResult = findRunResult(DeviceInfoCollector.APP_PACKAGE_NAME);
if (deviceInfoResult == null) {
Log.w(LOG_TAG, String.format("Could not find device info run %s",
DeviceInfoCollector.APP_PACKAGE_NAME));
return;
}
// Extract metrics that need extra handling, and then dump the remainder into BuildInfo
Map<String, String> metricsCopy = new HashMap<String, String>(
deviceInfoResult.getRunMetrics());
serializer.startTag(ns, "Screen");
String screenWidth = metricsCopy.remove(DeviceInfoCollector.SCREEN_WIDTH);
String screenHeight = metricsCopy.remove(DeviceInfoCollector.SCREEN_HEIGHT);
serializer.attribute(ns, "resolution", String.format("%sx%s", screenWidth, screenHeight));
serializer.endTag(ns, "Screen");
serializer.startTag(ns, "PhoneSubInfo");
serializer.attribute(ns, "subscriberId", metricsCopy.remove(
DeviceInfoCollector.PHONE_NUMBER));
serializer.endTag(ns, "PhoneSubInfo");
String featureData = metricsCopy.remove(DeviceInfoCollector.FEATURES);
String processData = metricsCopy.remove(DeviceInfoCollector.PROCESSES);
// dump the remaining metrics without translation
serializer.startTag(ns, "BuildInfo");
for (Map.Entry<String, String> metricEntry : metricsCopy.entrySet()) {
serializer.attribute(ns, metricEntry.getKey(), metricEntry.getValue());
}
serializer.endTag(ns, "BuildInfo");
serializeFeatureInfo(serializer, featureData);
serializeProcessInfo(serializer, processData);
serializer.endTag(ns, "DeviceInfo");
}
/**
* Prints XML indicating what features are supported by the device. It parses a string from the
* featureData argument that is in the form of "feature1:true;feature2:false;featuer3;true;"
* with a trailing semi-colon.
*
* <pre>
* <FeatureInfo>
* <Feature name="android.name.of.feature" available="true" />
* ...
* </FeatureInfo>
* </pre>
*
* @param serializer used to create XML
* @param featureData raw unparsed feature data
*/
private void serializeFeatureInfo(KXmlSerializer serializer, String featureData) throws IOException {
serializer.startTag(ns, "FeatureInfo");
if (featureData == null) {
featureData = "";
}
String[] featurePairs = featureData.split(";");
for (String featurePair : featurePairs) {
String[] nameTypeAvailability = featurePair.split(":");
if (nameTypeAvailability.length >= 3) {
serializer.startTag(ns, "Feature");
serializer.attribute(ns, "name", nameTypeAvailability[0]);
serializer.attribute(ns, "type", nameTypeAvailability[1]);
serializer.attribute(ns, "available", nameTypeAvailability[2]);
serializer.endTag(ns, "Feature");
}
}
serializer.endTag(ns, "FeatureInfo");
}
/**
* Prints XML data indicating what particular processes of interest were running on the device.
* It parses a string from the rootProcesses argument that is in the form of
* "processName1;processName2;..." with a trailing semi-colon.
*
* <pre>
* <ProcessInfo>
* <Process name="long_cat_viewer" uid="0" />
* ...
* </ProcessInfo>
* </pre>
*
* @param document
* @param parentNode
* @param deviceInfo
*/
private void serializeProcessInfo(KXmlSerializer serializer, String rootProcesses)
throws IOException {
serializer.startTag(ns, "ProcessInfo");
if (rootProcesses == null) {
rootProcesses = "";
}
String[] processNames = rootProcesses.split(";");
for (String processName : processNames) {
processName = processName.trim();
if (processName.length() > 0) {
serializer.startTag(ns, "Process");
serializer.attribute(ns, "name", processName);
serializer.attribute(ns, "uid", "0");
serializer.endTag(ns, "Process");
}
}
serializer.endTag(ns, "ProcessInfo");
}
/**
* Finds the {@link TestRunResult} with the given name.
*
* @param runName
* @return the {@link TestRunResult}
*/
private TestRunResult findRunResult(String runName) {
for (TestRunResult runResult : getRunResults()) {
if (runResult.getName().equals(runName)) {
return runResult;
}
}
return null;
}
/**
* Output the host info XML.
*
* @param serializer
*/
private void serializeHostInfo(KXmlSerializer serializer) throws IOException {
serializer.startTag(ns, "HostInfo");
String hostName = "";
try {
hostName = InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException ignored) {}
serializer.attribute(ns, "name", hostName);
serializer.startTag(ns, "Os");
serializer.attribute(ns, "name", System.getProperty("os.name"));
serializer.attribute(ns, "version", System.getProperty("os.version"));
serializer.attribute(ns, "arch", System.getProperty("os.arch"));
serializer.endTag(ns, "Os");
serializer.startTag(ns, "Java");
serializer.attribute(ns, "name", System.getProperty("java.vendor"));
serializer.attribute(ns, "version", System.getProperty("java.version"));
serializer.endTag(ns, "Java");
serializer.startTag(ns, "Cts");
serializer.attribute(ns, "version", CTS_VERSION);
// TODO: consider outputting tradefed options here
serializer.endTag(ns, "Cts");
serializer.endTag(ns, "HostInfo");
}
/**
* Output the test summary XML containing summary totals for all tests.
*
* @param serializer
* @throws IOException
*/
private void serializeTestSummary(KXmlSerializer serializer) throws IOException {
serializer.startTag(ns, "Summary");
serializer.attribute(ns, "failed", Integer.toString(getNumErrorTests() +
getNumFailedTests()));
// TODO: output notExecuted, timeout, and omitted count
serializer.attribute(ns, "notExecuted", "0");
serializer.attribute(ns, "timeout", "0");
serializer.attribute(ns, "omitted", "0");
serializer.attribute(ns, "pass", Integer.toString(getNumPassedTests()));
serializer.attribute(ns, "total", Integer.toString(getNumTotalTests()));
serializer.endTag(ns, "Summary");
}
/**
* Output the detailed test results XML.
*
* @param serializer
* @throws IOException
*/
private void serializeTestResults(KXmlSerializer serializer) throws IOException {
for (TestRunResult runResult : getRunResults()) {
serializeTestRunResult(serializer, runResult);
}
}
/**
* Output the XML for one test run aka test package.
*
* @param serializer
* @param runResult the {@link TestRunResult}
* @throws IOException
*/
private void serializeTestRunResult(KXmlSerializer serializer, TestRunResult runResult)
throws IOException {
if (runResult.getName().equals(DeviceInfoCollector.APP_PACKAGE_NAME)) {
// ignore run results for the info collecting packages
return;
}
serializer.startTag(ns, "TestPackage");
serializer.attribute(ns, "name", runResult.getName());
serializer.attribute(ns, "runTime", formatElapsedTime(runResult.getElapsedTime()));
// TODO: generate digest
serializer.attribute(ns, "digest", "");
serializer.attribute(ns, "failed", Integer.toString(runResult.getNumErrorTests() +
runResult.getNumFailedTests()));
// TODO: output notExecuted, timeout, and omitted count
serializer.attribute(ns, "notExecuted", "0");
serializer.attribute(ns, "timeout", "0");
serializer.attribute(ns, "omitted", "0");
serializer.attribute(ns, "pass", Integer.toString(runResult.getNumPassedTests()));
serializer.attribute(ns, "total", Integer.toString(runResult.getNumTests()));
// the results XML needs to organize test's by class. Build a nested data structure that
// group's the results by class name
Map<String, Map<TestIdentifier, TestResult>> classResultsMap = buildClassNameMap(
runResult.getTestResults());
for (Map.Entry<String, Map<TestIdentifier, TestResult>> resultsEntry :
classResultsMap.entrySet()) {
serializer.startTag(ns, "TestCase");
serializer.attribute(ns, "name", resultsEntry.getKey());
serializeTests(serializer, resultsEntry.getValue());
serializer.endTag(ns, "TestCase");
}
serializer.endTag(ns, "TestPackage");
}
/**
* Organizes the test run results into a format organized by class name.
*/
private Map<String, Map<TestIdentifier, TestResult>> buildClassNameMap(
Map<TestIdentifier, TestResult> results) {
// use a linked hashmap to have predictable iteration order
Map<String, Map<TestIdentifier, TestResult>> classResultMap =
new LinkedHashMap<String, Map<TestIdentifier, TestResult>>();
for (Map.Entry<TestIdentifier, TestResult> resultEntry : results.entrySet()) {
String className = resultEntry.getKey().getClassName();
Map<TestIdentifier, TestResult> resultsForClass = classResultMap.get(className);
if (resultsForClass == null) {
resultsForClass = new LinkedHashMap<TestIdentifier, TestResult>();
classResultMap.put(className, resultsForClass);
}
resultsForClass.put(resultEntry.getKey(), resultEntry.getValue());
}
return classResultMap;
}
/**
* Output XML for given map of tests their results
*
* @param serializer
* @param results
* @throws IOException
*/
private void serializeTests(KXmlSerializer serializer, Map<TestIdentifier, TestResult> results)
throws IOException {
for (Map.Entry<TestIdentifier, TestResult> resultEntry : results.entrySet()) {
serializeTest(serializer, resultEntry.getKey(), resultEntry.getValue());
}
}
/**
* Output the XML for given test and result.
*
* @param serializer
* @param testId
* @param result
* @throws IOException
*/
private void serializeTest(KXmlSerializer serializer, TestIdentifier testId, TestResult result)
throws IOException {
serializer.startTag(ns, "Test");
serializer.attribute(ns, "name", testId.getTestName());
serializer.attribute(ns, "result", convertStatus(result.getStatus()));
if (result.getStackTrace() != null) {
String sanitizedStack = sanitizeStackTrace(result.getStackTrace());
serializer.startTag(ns, "FailedScene");
serializer.attribute(ns, "message", getFailureMessageFromStackTrace(sanitizedStack));
serializer.text(sanitizedStack);
serializer.endTag(ns, "FailedScene");
}
serializer.endTag(ns, "Test");
}
/**
* Convert a {@link TestStatus} to the result text to output in XML
*
* @param status the {@link TestStatus}
* @return
*/
private String convertStatus(TestStatus status) {
switch (status) {
case ERROR:
return "fail";
case FAILURE:
return "fail";
case PASSED:
return "pass";
// TODO add notExecuted, omitted timeout
}
return "omitted";
}
/**
* Strip out any invalid XML characters that might cause the report to be unviewable.
* http://www.w3.org/TR/REC-xml/#dt-character
*/
private static String sanitizeStackTrace(String trace) {
if (trace != null) {
return trace.replaceAll("[^\\u0009\\u000A\\u000D\\u0020-\\uD7FF\\uE000-\\uFFFD]", "");
} else {
return null;
}
}
private static String getFailureMessageFromStackTrace(String stack) {
// This is probably too simplistic to work in all cases, but for now, just return first
// line of stack as failure message
int firstNewLine = stack.indexOf('\n');
if (firstNewLine != -1) {
return stack.substring(0, firstNewLine);
}
return stack;
}
/**
* Return the current timestamp as a {@link String} suitable for displaying.
* <p/>
* Example: Fri Aug 20 15:13:03 PDT 2010
*/
String getTimestamp() {
SimpleDateFormat dateFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy");
return dateFormat.format(new Date());
}
/**
* Return the current timestamp in a compressed format, used to uniquely identify results.
* <p/>
* Example: 2010.08.16_11.42.12
*/
private String getResultTimestamp() {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd_HH.mm.ss");
return dateFormat.format(new Date());
}
/**
* Return a prettified version of the given elapsed time
* @return
*/
private String formatElapsedTime(long elapsedTimeMs) {
long seconds = TimeUnit.MILLISECONDS.toSeconds(elapsedTimeMs) % 60;
long minutes = TimeUnit.MILLISECONDS.toMinutes(elapsedTimeMs) % 60;
long hours = TimeUnit.MILLISECONDS.toHours(elapsedTimeMs);
StringBuilder time = new StringBuilder();
if (hours > 0) {
time.append(hours);
time.append("h ");
}
if (minutes > 0) {
time.append(minutes);
time.append("m ");
}
time.append(seconds);
time.append("s");
return time.toString();
}
/**
* Creates the output stream to use for test results. Exposed for mocking.
*/
OutputStream createOutputResultStream(File reportDir) throws IOException {
File reportFile = new File(reportDir, TEST_RESULT_FILE_NAME);
Log.i(LOG_TAG, String.format("Created xml report file at %s",
reportFile.getAbsolutePath()));
return new FileOutputStream(reportFile);
}
/**
* Copy the xml formatting files stored in this jar to the results directory
*
* @param resultsDir
*/
private void copyFormattingFiles(File resultsDir) {
for (String resultFileName : CTS_RESULT_RESOURCES) {
InputStream configStream = getClass().getResourceAsStream(
String.format("/result/%s", resultFileName));
if (configStream != null) {
File resultFile = new File(resultsDir, resultFileName);
try {
FileUtil.writeToFile(configStream, resultFile);
} catch (IOException e) {
Log.w(LOG_TAG, String.format("Failed to write %s to file", resultFileName));
}
} else {
Log.w(LOG_TAG, String.format("Failed to load %s from jar", resultFileName));
}
}
}
/**
* Zip the contents of the given results directory.
*
* @param resultsDir
*/
private void zipResults(File resultsDir) {
// TODO: implement this
}
}