blob: fa8d8adfab85e030ea117f99860f5092317e49c8 [file] [log] [blame]
/*
* Copyright (C) 2015 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.compatibility.common.tradefed.result;
import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
import com.android.compatibility.common.tradefed.result.InvocationFailureHandler;
import com.android.compatibility.common.tradefed.testtype.CompatibilityTest;
import com.android.compatibility.common.util.ICaseResult;
import com.android.compatibility.common.util.IInvocationResult;
import com.android.compatibility.common.util.IModuleResult;
import com.android.compatibility.common.util.ITestResult;
import com.android.compatibility.common.util.InvocationResult;
import com.android.compatibility.common.util.MetricsStore;
import com.android.compatibility.common.util.ReportLog;
import com.android.compatibility.common.util.ResultHandler;
import com.android.compatibility.common.util.ResultUploader;
import com.android.compatibility.common.util.TestStatus;
import com.android.ddmlib.Log;
import com.android.ddmlib.Log.LogLevel;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.Option.Importance;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.config.OptionCopier;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.ILogSaver;
import com.android.tradefed.result.ILogSaverListener;
import com.android.tradefed.result.IShardableListener;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.ITestSummaryListener;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.result.LogFile;
import com.android.tradefed.result.LogFileSaver;
import com.android.tradefed.result.TestSummary;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.StreamUtil;
import com.android.tradefed.util.TimeUtil;
import com.android.tradefed.util.ZipUtil;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* Collect test results for an entire invocation and output test results to disk.
*/
@OptionClass(alias="result-reporter")
public class ResultReporter implements ILogSaverListener, ITestInvocationListener,
ITestSummaryListener, IShardableListener {
private static final String UNKNOWN_DEVICE = "unknown_device";
private static final String RESULT_KEY = "COMPATIBILITY_TEST_RESULT";
private static final String CTS_PREFIX = "cts:";
private static final String BUILD_INFO = CTS_PREFIX + "build_";
private static final String[] RESULT_RESOURCES = {
"compatibility_result.css",
"compatibility_result.xsd",
"compatibility_result.xsl",
"logo.png"};
@Option(name = CompatibilityTest.RETRY_OPTION,
shortName = 'r',
description = "retry a previous session.",
importance = Importance.IF_UNSET)
private Integer mRetrySessionId = null;
@Option(name = "result-server", description = "Server to publish test results.")
private String mResultServer;
@Option(name = "disable-result-posting", description = "Disable result posting into report server.")
private boolean mDisableResultPosting = false;
@Option(name = "include-test-log-tags", description = "Include test log tags in report.")
private boolean mIncludeTestLogTags = false;
@Option(name = "use-log-saver", description = "Also saves generated result with log saver")
private boolean mUseLogSaver = false;
private CompatibilityBuildHelper mBuildHelper;
private File mResultDir = null;
private File mLogDir = null;
private ResultUploader mUploader;
private String mReferenceUrl;
private ILogSaver mLogSaver;
private int invocationEndedCount = 0;
private IInvocationResult mResult = new InvocationResult();
private IModuleResult mCurrentModuleResult;
private ICaseResult mCurrentCaseResult;
private ITestResult mCurrentResult;
private String mDeviceSerial = UNKNOWN_DEVICE;
private Set<String> mMasterDeviceSerials = new HashSet<>();
private Set<IBuildInfo> mMasterBuildInfos = new HashSet<>();
// mCurrentTestNum and mTotalTestsInModule track the progress within the module
// Note that this count is not necessarily equal to the count of tests contained
// in mCurrentModuleResult because of how special cases like ignored tests are reported.
private int mCurrentTestNum;
private int mTotalTestsInModule;
// Nullable. If null, "this" is considered the master and must handle
// result aggregation and reporting. When not null, it should forward events
// to the master.
private final ResultReporter mMasterResultReporter;
/**
* Default constructor.
*/
public ResultReporter() {
this(null);
}
/**
* Construct a shard ResultReporter that forwards module results to the
* masterResultReporter.
*/
public ResultReporter(ResultReporter masterResultReporter) {
mMasterResultReporter = masterResultReporter;
}
/**
* {@inheritDoc}
*/
@Override
public void invocationStarted(IBuildInfo buildInfo) {
synchronized(this) {
if (mBuildHelper == null) {
mBuildHelper = new CompatibilityBuildHelper(buildInfo);
}
if (mDeviceSerial == null && buildInfo.getDeviceSerial() != null) {
mDeviceSerial = buildInfo.getDeviceSerial();
}
}
if (isShardResultReporter()) {
// Shard ResultReporters forward invocationStarted to the mMasterResultReporter
mMasterResultReporter.invocationStarted(buildInfo);
return;
}
// NOTE: Everything after this line only applies to the master ResultReporter.
synchronized(this) {
if (buildInfo.getDeviceSerial() != null) {
// The master ResultReporter collects all device serials being used
// for the current implementation.
mMasterDeviceSerials.add(buildInfo.getDeviceSerial());
}
// The master ResultReporter collects all buildInfos.
mMasterBuildInfos.add(buildInfo);
if (mResultDir == null) {
// For the non-sharding case, invocationStarted is only called once,
// but for the sharding case, this might be called multiple times.
// Logic used to initialize the result directory should not be
// invoked twice during the same invocation.
initializeResultDirectories();
}
}
}
/**
* Create directory structure where results and logs will be written.
*/
private void initializeResultDirectories() {
info("Initializing result directory");
try {
// Initialize the result directory. Either a new directory or reusing
// an existing session.
if (mRetrySessionId != null) {
// Overwrite the mResult with the test results of the previous session
mResult = ResultHandler.findResult(mBuildHelper.getResultsDir(), mRetrySessionId);
}
mResult.setStartTime(mBuildHelper.getStartTime());
mResultDir = mBuildHelper.getResultDir();
if (mResultDir != null) {
mResultDir.mkdirs();
}
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
if (mResultDir == null) {
throw new RuntimeException("Result Directory was not created");
}
if (!mResultDir.exists()) {
throw new RuntimeException("Result Directory was not created: " +
mResultDir.getAbsolutePath());
}
info("Results Directory: " + mResultDir.getAbsolutePath());
mUploader = new ResultUploader(mResultServer, mBuildHelper.getSuiteName());
try {
mLogDir = new File(mBuildHelper.getLogsDir(),
CompatibilityBuildHelper.getDirSuffix(mBuildHelper.getStartTime()));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
if (mLogDir != null && mLogDir.mkdirs()) {
info("Created log dir %s", mLogDir.getAbsolutePath());
}
if (mLogDir == null || !mLogDir.exists()) {
throw new IllegalArgumentException(String.format("Could not create log dir %s",
mLogDir.getAbsolutePath()));
}
}
/**
* {@inheritDoc}
*/
@Override
public void testRunStarted(String id, int numTests) {
if (mCurrentModuleResult != null && mCurrentModuleResult.getId().equals(id)) {
// In case we get another test run of a known module, update the complete
// status to false to indicate it is not complete.
if (mCurrentModuleResult.isDone()) {
// modules run with HostTest treat each test class as a separate module.
// TODO(aaronholden): remove this case when JarHostTest is no longer calls
// testRunStarted for each test class.
mTotalTestsInModule += numTests;
} else {
// treat new tests as not executed tests from current module
mTotalTestsInModule +=
Math.max(0, numTests - mCurrentModuleResult.getNotExecuted());
}
mCurrentModuleResult.setDone(false);
} else {
mCurrentModuleResult = mResult.getOrCreateModule(id);
mTotalTestsInModule = numTests;
// Reset counters
mCurrentTestNum = 0;
}
}
/**
* {@inheritDoc}
*/
@Override
public void testStarted(TestIdentifier test) {
mCurrentCaseResult = mCurrentModuleResult.getOrCreateResult(test.getClassName());
mCurrentResult = mCurrentCaseResult.getOrCreateResult(test.getTestName().trim());
if (mCurrentResult.isRetry()) {
mCurrentResult.reset(); // clear result status for this invocation
}
mCurrentTestNum++;
}
/**
* {@inheritDoc}
*/
@Override
public void testEnded(TestIdentifier test, Map<String, String> metrics) {
if (mCurrentResult.getResultStatus() == TestStatus.FAIL) {
// Test has previously failed.
return;
}
// device test can have performance results in test metrics
String perfResult = metrics.get(RESULT_KEY);
ReportLog report = null;
if (perfResult != null) {
try {
report = ReportLog.parse(perfResult);
} catch (XmlPullParserException | IOException e) {
e.printStackTrace();
}
} else {
// host test should be checked into MetricsStore.
report = MetricsStore.removeResult(mBuildHelper.getBuildInfo(),
mCurrentModuleResult.getAbi(), test.toString());
}
if (mCurrentResult.getResultStatus() == null) {
// Only claim that we passed when we're certain our result was
// not any other state.
mCurrentResult.passed(report);
}
}
/**
* {@inheritDoc}
*/
@Override
public void testIgnored(TestIdentifier test) {
// Ignored tests are not reported.
mCurrentTestNum--;
}
/**
* {@inheritDoc}
*/
@Override
public void testFailed(TestIdentifier test, String trace) {
mCurrentResult.failed(trace);
}
/**
* {@inheritDoc}
*/
@Override
public void testAssumptionFailure(TestIdentifier test, String trace) {
mCurrentResult.skipped();
}
/**
* {@inheritDoc}
*/
@Override
public void testRunStopped(long elapsedTime) {
// ignore
}
/**
* {@inheritDoc}
*/
@Override
public void testRunEnded(long elapsedTime, Map<String, String> metrics) {
mCurrentModuleResult.addRuntime(elapsedTime);
// Expect them to be equal, but greater than to be safe.
mCurrentModuleResult.setDone(mCurrentTestNum >= mTotalTestsInModule);
mCurrentModuleResult.setNotExecuted(Math.max(mTotalTestsInModule - mCurrentTestNum, 0));
if (isShardResultReporter()) {
// Forward module results to the master.
mMasterResultReporter.mergeModuleResult(mCurrentModuleResult);
}
}
/**
* Directly add a module result. Note: this method is meant to be used by
* a shard ResultReporter.
*/
private void mergeModuleResult(IModuleResult moduleResult) {
// This merges the results in moduleResult to any existing results already
// contained in mResult. This is useful for retries and allows the final
// report from a retry to contain all test results.
synchronized(this) {
mResult.mergeModuleResult(moduleResult);
}
}
/**
* {@inheritDoc}
*/
@Override
public void testRunFailed(String errorMessage) {
// ignore
}
/**
* {@inheritDoc}
*/
@Override
public TestSummary getSummary() {
// ignore
return null;
}
/**
* {@inheritDoc}
*/
@Override
public void putSummary(List<TestSummary> summaries) {
// This is safe to be invoked on either the master or a shard ResultReporter,
// but the value added to the report will be that of the master ResultReporter.
if (summaries.size() > 0) {
mReferenceUrl = summaries.get(0).getSummary().getString();
}
}
/**
* {@inheritDoc}
*/
@Override
public void invocationEnded(long elapsedTime) {
if (isShardResultReporter()) {
// Shard ResultReporters report
mMasterResultReporter.invocationEnded(elapsedTime);
return;
}
// NOTE: Everything after this line only applies to the master ResultReporter.
synchronized(this) {
// The master ResultReporter tracks the progress of all invocations across
// shard ResultReporters. Writing results should not proceed until all
// ResultReporters have completed.
if (++invocationEndedCount < mMasterBuildInfos.size()) {
return;
}
finalizeResults(elapsedTime);
}
}
private void finalizeResults(long elapsedTime) {
// Add all device serials into the result to be serialized
for (String deviceSerial : mMasterDeviceSerials) {
mResult.addDeviceSerial(deviceSerial);
}
Set<String> allExpectedModules = new HashSet<>();
// Add all build info to the result to be serialized
for (IBuildInfo buildInfo : mMasterBuildInfos) {
for (Map.Entry<String, String> entry : buildInfo.getBuildAttributes().entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (key.startsWith(BUILD_INFO)) {
mResult.addInvocationInfo(key.substring(CTS_PREFIX.length()), value);
}
if (key.equals(CompatibilityBuildHelper.MODULE_IDS) && value.length() > 0) {
Collections.addAll(allExpectedModules, value.split(","));
}
}
}
// Include a record in the report of all expected modules ids, even if they weren't
// executed.
for (String moduleId : allExpectedModules) {
mResult.getOrCreateModule(moduleId);
}
String moduleProgress = String.format("%d of %d",
mResult.getModuleCompleteCount(), mResult.getModules().size());
info("Invocation finished in %s. PASSED: %d, FAILED: %d, NOT EXECUTED: %d, MODULES: %s",
TimeUtil.formatElapsedTime(elapsedTime),
mResult.countResults(TestStatus.PASS),
mResult.countResults(TestStatus.FAIL),
mResult.getNotExecuted(),
moduleProgress);
long startTime = mResult.getStartTime();
try {
// Zip the full test results directory.
copyDynamicConfigFiles(mBuildHelper.getDynamicConfigFiles(), mResultDir);
copyFormattingFiles(mResultDir);
File resultFile = ResultHandler.writeResults(mBuildHelper.getSuiteName(),
mBuildHelper.getSuiteVersion(), mBuildHelper.getSuitePlan(),
mBuildHelper.getSuiteBuild(), mResult, mResultDir, startTime,
elapsedTime + startTime, mReferenceUrl, getLogUrl(),
mBuildHelper.getCommandLineArgs());
info("Test Result: %s", resultFile.getCanonicalPath());
File zippedResults = zipResults(mResultDir);
info("Full Result: %s", zippedResults.getCanonicalPath());
saveLog(resultFile, zippedResults);
uploadResult(resultFile);
} catch (IOException | XmlPullParserException e) {
CLog.e("[%s] Exception while saving result XML.", mDeviceSerial);
CLog.e(e);
}
}
/**
* {@inheritDoc}
*/
@Override
public void invocationFailed(Throwable cause) {
warn("Invocation failed: %s", cause);
InvocationFailureHandler.setFailed(mBuildHelper, cause);
}
/**
* {@inheritDoc}
*/
@Override
public void testLog(String name, LogDataType type, InputStreamSource stream) {
// This is safe to be invoked on either the master or a shard ResultReporter
if (isShardResultReporter()) {
// Shard ResultReporters forward testLog to the mMasterResultReporter
mMasterResultReporter.testLog(name, type, stream);
return;
}
try {
LogFileSaver saver = new LogFileSaver(mLogDir);
File logFile = saver.saveAndZipLogData(name, type, stream.createInputStream());
info("Saved logs for %s in %s", name, logFile.getAbsolutePath());
} catch (IOException e) {
warn("Failed to write log for %s", name);
e.printStackTrace();
}
}
/**
* {@inheritDoc}
*/
@Override
public void testLogSaved(String dataName, LogDataType dataType, InputStreamSource dataStream,
LogFile logFile) {
// This is safe to be invoked on either the master or a shard ResultReporter
if (mIncludeTestLogTags && mCurrentResult != null
&& dataName.startsWith(mCurrentResult.getFullName())) {
if (dataType == LogDataType.BUGREPORT) {
mCurrentResult.setBugReport(logFile.getUrl());
} else if (dataType == LogDataType.LOGCAT) {
mCurrentResult.setLog(logFile.getUrl());
} else if (dataType == LogDataType.PNG) {
mCurrentResult.setScreenshot(logFile.getUrl());
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void setLogSaver(ILogSaver saver) {
// This is safe to be invoked on either the master or a shard ResultReporter
mLogSaver = saver;
}
/**
* When enabled, save log data using log saver
*/
private void saveLog(File resultFile, File zippedResults) throws IOException {
if (!mUseLogSaver) {
return;
}
FileInputStream fis = null;
try {
fis = new FileInputStream(resultFile);
mLogSaver.saveLogData("log-result", LogDataType.XML, fis);
} catch (IOException ioe) {
CLog.e("[%s] error saving XML with log saver", mDeviceSerial);
CLog.e(ioe);
} finally {
StreamUtil.close(fis);
}
// Save the full results folder.
if (zippedResults != null) {
FileInputStream zipResultStream = null;
try {
zipResultStream = new FileInputStream(zippedResults);
mLogSaver.saveLogData("results", LogDataType.ZIP, zipResultStream);
} finally {
StreamUtil.close(zipResultStream);
}
}
}
/**
* Return the path in which log saver persists log files or null if
* logSaver is not enabled.
*/
private String getLogUrl() {
if (!mUseLogSaver || mLogSaver == null) {
return null;
}
return mLogSaver.getLogReportDir().getUrl();
}
@Override
public IShardableListener clone() {
ResultReporter clone = new ResultReporter(this);
OptionCopier.copyOptionsNoThrow(this, clone);
return clone;
}
/**
* Return true if this instance is a shard ResultReporter and should propagate
* certain events to the master.
*/
private boolean isShardResultReporter() {
return mMasterResultReporter != null;
}
/**
* When enabled, upload the result to a server.
*/
private void uploadResult(File resultFile) throws IOException {
if (mResultServer != null && !mResultServer.trim().isEmpty() && !mDisableResultPosting) {
try {
info("Result Server: %d", mUploader.uploadResult(resultFile, mReferenceUrl));
} catch (IOException ioe) {
CLog.e("[%s] IOException while uploading result.", mDeviceSerial);
CLog.e(ioe);
}
}
}
/**
* Copy the xml formatting files stored in this jar to the results directory
*
* @param resultsDir
*/
static void copyFormattingFiles(File resultsDir) {
for (String resultFileName : RESULT_RESOURCES) {
InputStream configStream = ResultHandler.class.getResourceAsStream(
String.format("/report/%s", resultFileName));
if (configStream != null) {
File resultFile = new File(resultsDir, resultFileName);
try {
FileUtil.writeToFile(configStream, resultFile);
} catch (IOException e) {
warn("Failed to write %s to file", resultFileName);
}
} else {
warn("Failed to load %s from jar", resultFileName);
}
}
}
/**
* move the dynamic config files to the results directory
*
* @param configFiles
* @param resultsDir
*/
static void copyDynamicConfigFiles(Map<String, File> configFiles, File resultsDir) {
if (configFiles.size() == 0) return;
File folder = new File(resultsDir, "config");
folder.mkdir();
for (String moduleName : configFiles.keySet()) {
File resultFile = new File(folder, moduleName+".dynamic");
try {
FileUtil.copyFile(configFiles.get(moduleName), resultFile);
FileUtil.deleteFile(configFiles.get(moduleName));
} catch (IOException e) {
warn("Failed to copy config file for %s to file", moduleName);
}
}
}
/**
* Zip the contents of the given results directory.
*
* @param resultsDir
*/
private static File zipResults(File resultsDir) {
File zipResultFile = null;
try {
// create a file in parent directory, with same name as resultsDir
zipResultFile = new File(resultsDir.getParent(), String.format("%s.zip",
resultsDir.getName()));
ZipUtil.createZip(resultsDir, zipResultFile);
} catch (IOException e) {
warn("Failed to create zip for %s", resultsDir.getName());
}
return zipResultFile;
}
/**
* Log info to the console.
*/
private static void info(String format, Object... args) {
log(LogLevel.INFO, format, args);
}
/**
* Log a warning to the console.
*/
private static void warn(String format, Object... args) {
log(LogLevel.WARN, format, args);
}
/**
* Log a message to the console
*/
private static void log(LogLevel level, String format, Object... args) {
CLog.logAndDisplay(level, format, args);
}
/**
* For testing
*/
IInvocationResult getResult() {
return mResult;
}
}