blob: 227fcf5878dc786931fb33711c63899b41148c1c [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.testtype.retry.RetryFactoryTest;
import com.android.compatibility.common.tradefed.testtype.suite.CompatibilityTestSuite;
import com.android.compatibility.common.tradefed.util.FingerprintComparisonException;
import com.android.compatibility.common.tradefed.util.RetryType;
import com.android.compatibility.common.util.ChecksumReporter;
import com.android.compatibility.common.util.DeviceInfo;
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.InvocationResult.RunHistory;
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.LogLevel;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.config.IConfigurationReceiver;
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.invoker.IInvocationContext;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.FileInputStreamSource;
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.TestDescription;
import com.android.tradefed.result.TestSummary;
import com.android.tradefed.result.suite.SuiteResultReporter;
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 com.android.tradefed.util.proto.TfMetricProtoUtil;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.xml.XmlEscapers;
import com.google.gson.Gson;
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.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* 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, IConfigurationReceiver {
public static final String INCLUDE_HTML_IN_ZIP = "html-in-zip";
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 LATEST_LINK_NAME = "latest";
/** Used to get run history from the test result of last run. */
private static final String RUN_HISTORY_KEY = "run_history";
public static final String BUILD_BRAND = "build_brand";
public static final String BUILD_DEVICE = "build_device";
public static final String BUILD_FINGERPRINT = "build_fingerprint";
public static final String BUILD_ID = "build_id";
public static final String BUILD_MANUFACTURER = "build_manufacturer";
public static final String BUILD_MODEL = "build_model";
public static final String BUILD_PRODUCT = "build_product";
public static final String BUILD_VERSION_RELEASE = "build_version_release";
private static final List<String> NOT_RETRY_FILES = Arrays.asList(
ChecksumReporter.NAME,
ChecksumReporter.PREV_NAME,
ResultHandler.FAILURE_REPORT_NAME,
"diffs");
@Option(name = RetryFactoryTest.RETRY_OPTION,
shortName = 'r',
description = "retry a previous session.",
importance = Importance.IF_UNSET)
private Integer mRetrySessionId = null;
@Option(name = RetryFactoryTest.RETRY_TYPE_OPTION,
description = "used with " + RetryFactoryTest.RETRY_OPTION
+ ", retry tests of a certain status. Possible values include \"failed\", "
+ "\"not_executed\", and \"custom\".",
importance = Importance.IF_UNSET)
private RetryType mRetryType = 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;
@Option(name = "compress-logs", description = "Whether logs will be saved with compression")
private boolean mCompressLogs = true;
@Option(name = INCLUDE_HTML_IN_ZIP,
description = "Whether failure summary report is included in the zip fie.")
private boolean mIncludeHtml = false;
@Option(
name = "result-attribute",
description =
"Extra key-value pairs to be added as attributes and corresponding"
+ "values of the \"Result\" tag in the result XML.")
private Map<String, String> mResultAttributes = new HashMap<String, String>();
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 CountDownLatch mFinalized = null;
protected 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<>();
// Whether or not we failed the fingerprint check
private boolean mFingerprintFailure = false;
// 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;
// Whether modules can be marked done for this invocation. Initialized in invocationStarted()
// Visible for unit testing
protected boolean mCanMarkDone;
// Whether the current test run has failed. If true, we will not mark the current module done
protected boolean mTestRunFailed;
// Whether the current module has previously been marked done
private boolean mModuleWasDone;
// 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;
private LogFileSaver mTestLogSaver;
// Elapsed time from invocation started to ended.
private long mElapsedTime;
/** Invocation level configuration */
private IConfiguration mConfiguration = null;
/**
* Default constructor.
*/
public ResultReporter() {
this(null);
mFinalized = new CountDownLatch(1);
}
/**
* Construct a shard ResultReporter that forwards module results to the
* masterResultReporter.
*/
public ResultReporter(ResultReporter masterResultReporter) {
mMasterResultReporter = masterResultReporter;
}
/** {@inheritDoc} */
@Override
public void setConfiguration(IConfiguration configuration) {
mConfiguration = configuration;
}
/**
* {@inheritDoc}
*/
@Override
public void invocationStarted(IInvocationContext context) {
IBuildInfo primaryBuild = context.getBuildInfos().get(0);
synchronized(this) {
if (mBuildHelper == null) {
mBuildHelper = new CompatibilityBuildHelper(primaryBuild);
}
if (mDeviceSerial == null && primaryBuild.getDeviceSerial() != null) {
mDeviceSerial = primaryBuild.getDeviceSerial();
}
mCanMarkDone = canMarkDone(mBuildHelper.getRecentCommandLineArgs());
}
if (isShardResultReporter()) {
// Shard ResultReporters forward invocationStarted to the mMasterResultReporter
mMasterResultReporter.invocationStarted(context);
return;
}
// NOTE: Everything after this line only applies to the master ResultReporter.
synchronized(this) {
if (primaryBuild.getDeviceSerial() != null) {
// The master ResultReporter collects all device serials being used
// for the current implementation.
mMasterDeviceSerials.add(primaryBuild.getDeviceSerial());
}
// The master ResultReporter collects all buildInfos.
mMasterBuildInfos.add(primaryBuild);
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() {
debug("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());
}
debug("Results Directory: %s", mResultDir.getAbsolutePath());
mUploader = new ResultUploader(mResultServer, mBuildHelper.getSuiteName());
try {
mLogDir = new File(mBuildHelper.getLogsDir(),
CompatibilityBuildHelper.getDirSuffix(mBuildHelper.getStartTime()));
} catch (FileNotFoundException e) {
CLog.e(e);
}
if (mLogDir != null && mLogDir.mkdirs()) {
debug("Created log dir %s", mLogDir.getAbsolutePath());
}
if (mLogDir == null || !mLogDir.exists()) {
throw new IllegalArgumentException(String.format("Could not create log dir %s",
mLogDir.getAbsolutePath()));
}
if (mTestLogSaver == null) {
mTestLogSaver = new LogFileSaver(mLogDir);
}
}
/**
* {@inheritDoc}
*/
@Override
public void testRunStarted(String id, int numTests) {
if (mCurrentModuleResult != null && mCurrentModuleResult.getId().equals(id)
&& mCurrentModuleResult.isDone()) {
// Modules run with JarHostTest treat each test class as a separate module,
// resulting in additional unexpected test runs.
// This case exists only for N
mTotalTestsInModule += numTests;
} else {
// Handle non-JarHostTest case
mCurrentModuleResult = mResult.getOrCreateModule(id);
mModuleWasDone = mCurrentModuleResult.isDone();
mTestRunFailed = false;
if (!mModuleWasDone) {
// we only want to update testRun variables if the IModuleResult is not yet done
// otherwise leave testRun variables alone so isDone evaluates to true.
if (mCurrentModuleResult.getExpectedTestRuns() == 0) {
mCurrentModuleResult.setExpectedTestRuns(TestRunHandler.getTestRuns(
mBuildHelper, mCurrentModuleResult.getId()));
}
mCurrentModuleResult.addTestRun();
}
// Reset counters
mTotalTestsInModule = numTests;
mCurrentTestNum = 0;
}
mCurrentModuleResult.inProgress(true);
}
/**
* {@inheritDoc}
*/
@Override
public void testStarted(TestDescription 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(TestDescription test, HashMap<String, Metric> metrics) {
if (mCurrentResult.getResultStatus() == TestStatus.FAIL) {
// Test has previously failed.
return;
}
// device test can have performance results in test metrics
Metric perfResult = metrics.get(RESULT_KEY);
ReportLog report = null;
if (perfResult != null) {
try {
report = ReportLog.parse(perfResult.getMeasurements().getSingleString());
} 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(TestDescription test) {
mCurrentResult.skipped();
}
/**
* {@inheritDoc}
*/
@Override
public void testFailed(TestDescription test, String trace) {
mCurrentResult.failed(sanitizeXmlContent(trace));
}
/**
* {@inheritDoc}
*/
@Override
public void testAssumptionFailure(TestDescription test, String trace) {
mCurrentResult.skipped();
}
/**
* {@inheritDoc}
*/
@Override
public void testRunStopped(long elapsedTime) {
// ignore
}
/**
* {@inheritDoc}
*/
@Override
public void testRunEnded(long elapsedTime, Map<String, String> metrics) {
testRunEnded(elapsedTime, TfMetricProtoUtil.upgradeConvert(metrics));
}
/**
* {@inheritDoc}
*/
@Override
public void testRunEnded(long elapsedTime, HashMap<String, Metric> metrics) {
mCurrentModuleResult.inProgress(false);
mCurrentModuleResult.addRuntime(elapsedTime);
if (!mModuleWasDone && mCanMarkDone) {
if (mTestRunFailed) {
// set done to false for test run failures
mCurrentModuleResult.setDone(false);
} else {
// Only mark module done if:
// - status of the invocation allows it (mCanMarkDone), and
// - module has not already been marked done, and
// - no test run failure has been detected
mCurrentModuleResult.setDone(mCurrentTestNum >= mTotalTestsInModule);
}
}
if (isShardResultReporter()) {
// Forward module results to the master.
mMasterResultReporter.mergeModuleResult(mCurrentModuleResult);
mCurrentModuleResult.resetTestRuns();
mCurrentModuleResult.resetRuntime();
}
}
/**
* 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) {
mTestRunFailed = true;
mCurrentModuleResult.setFailed();
}
/**
* {@inheritDoc}
*/
@Override
public TestSummary getSummary() {
// ignore
return null;
}
/**
* {@inheritDoc}
*/
@Override
public void putSummary(List<TestSummary> summaries) {
for (TestSummary summary : summaries) {
// If one summary is from SuiteResultReporter, log it as an extra file.
if (SuiteResultReporter.SUITE_REPORTER_SOURCE.equals(summary.getSource())) {
File summaryFile = null;
try {
summaryFile = FileUtil.createTempFile("summary", ".txt");
FileUtil.writeToFile(summary.getSummary().getString(), summaryFile);
try (InputStreamSource stream = new FileInputStreamSource(summaryFile)) {
testLog("summary", LogDataType.TEXT, stream);
}
} catch (IOException e) {
CLog.e(e);
} finally {
FileUtil.deleteFile(summaryFile);
}
} else if (mReferenceUrl == null && summary.getSummary().getString() != null) {
mReferenceUrl = summary.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;
}
mElapsedTime = elapsedTime;
finalizeResults();
mFinalized.countDown();
}
}
/**
* Returns whether a report creation should be skipped.
*/
protected boolean shouldSkipReportCreation() {
// This value is always false here for backwards compatibility.
// Extended classes have the option to override this.
return false;
}
private void finalizeResults() {
if (mFingerprintFailure) {
CLog.w("Failed the fingerprint check. Skip result reporting.");
return;
}
// Add all device serials into the result to be serialized
for (String deviceSerial : mMasterDeviceSerials) {
mResult.addDeviceSerial(deviceSerial);
}
addDeviceBuildInfoToResult();
Set<String> allExpectedModules = new HashSet<>();
for (IBuildInfo buildInfo : mMasterBuildInfos) {
for (Map.Entry<String, String> entry : buildInfo.getBuildAttributes().entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
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());
if (shouldSkipReportCreation()) {
return;
}
// Get run history from the test result of last run and add the run history of the current
// run to it.
// TODO(b/137973382): avoid casting by move the method to interface level.
Collection<RunHistory> runHistories = ((InvocationResult) mResult).getRunHistories();
String runHistoryJSON = mResult.getInvocationInfo().get(RUN_HISTORY_KEY);
Gson gson = new Gson();
if (runHistoryJSON != null) {
RunHistory[] runHistoryArray = gson.fromJson(runHistoryJSON, RunHistory[].class);
Collections.addAll(runHistories, runHistoryArray);
}
RunHistory newRun = new RunHistory();
newRun.startTime = mResult.getStartTime();
newRun.endTime = newRun.startTime + mElapsedTime;
runHistories.add(newRun);
mResult.addInvocationInfo(RUN_HISTORY_KEY, gson.toJson(runHistories));
try {
// Zip the full test results directory.
copyDynamicConfigFiles();
copyFormattingFiles(mResultDir, mBuildHelper.getSuiteName());
File resultFile = generateResultXmlFile();
if (mRetrySessionId != null) {
copyRetryFiles(ResultHandler.getResultDirectory(
mBuildHelper.getResultsDir(), mRetrySessionId), mResultDir);
}
File failureReport = null;
if (mIncludeHtml) {
// Create the html report before the zip file.
failureReport = ResultHandler.createFailureReport(resultFile);
}
File zippedResults = zipResults(mResultDir);
if (!mIncludeHtml) {
// Create failure report after zip file so extra data is not uploaded
failureReport = ResultHandler.createFailureReport(resultFile);
}
if (failureReport != null && failureReport.exists()) {
info("Test Result: %s", failureReport.getCanonicalPath());
} else {
info("Test Result: %s", resultFile.getCanonicalPath());
}
info("Test Logs: %s", mLogDir.getCanonicalPath());
debug("Full Result: %s", zippedResults.getCanonicalPath());
Path latestLink = createLatestLinkDirectory(mResultDir.toPath());
if (latestLink != null) {
info("Latest results link: " + latestLink.toAbsolutePath());
}
latestLink = createLatestLinkDirectory(mLogDir.toPath());
if (latestLink != null) {
info("Latest logs link: " + latestLink.toAbsolutePath());
}
saveLog(resultFile, zippedResults);
uploadResult(resultFile);
} catch (IOException | XmlPullParserException e) {
CLog.e("[%s] Exception while saving result XML.", mDeviceSerial);
CLog.e(e);
}
// print the run results last.
info("Invocation finished in %s. PASSED: %d, FAILED: %d, MODULES: %s",
TimeUtil.formatElapsedTime(mElapsedTime),
mResult.countResults(TestStatus.PASS),
mResult.countResults(TestStatus.FAIL),
moduleProgress);
}
private Path createLatestLinkDirectory(Path directory) {
Path link = null;
Path parent = directory.getParent();
if (parent != null) {
link = parent.resolve(LATEST_LINK_NAME);
try {
// if latest already exists, we have to remove it before creating
Files.deleteIfExists(link);
Files.createSymbolicLink(link, directory);
} catch (IOException ioe) {
CLog.e("Exception while attempting to create 'latest' link to: [%s]",
directory);
CLog.e(ioe);
return null;
} catch (UnsupportedOperationException uoe) {
CLog.e("Failed to create 'latest' symbolic link - unsupported operation");
return null;
}
}
return link;
}
/**
* {@inheritDoc}
*/
@Override
public void invocationFailed(Throwable cause) {
warn("Invocation failed: %s", cause);
InvocationFailureHandler.setFailed(mBuildHelper, cause);
if (cause instanceof FingerprintComparisonException) {
mFingerprintFailure = true;
}
}
/**
* {@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;
}
if (name.endsWith(DeviceInfo.FILE_SUFFIX)) {
// Handle device info file case
testLogDeviceInfo(name, stream);
} else {
// Handle default case
try {
File logFile = null;
if (mCompressLogs) {
try (InputStream inputStream = stream.createInputStream()) {
logFile = mTestLogSaver.saveAndGZipLogData(name, type, inputStream);
}
} else {
try (InputStream inputStream = stream.createInputStream()) {
logFile = mTestLogSaver.saveLogData(name, type, inputStream);
}
}
debug("Saved logs for %s in %s", name, logFile.getAbsolutePath());
} catch (IOException e) {
warn("Failed to write log for %s", name);
CLog.e(e);
}
}
}
/* Write device-info files to the result, invoked only by the master result reporter */
private void testLogDeviceInfo(String name, InputStreamSource stream) {
try {
File ediDir = new File(mResultDir, DeviceInfo.RESULT_DIR_NAME);
ediDir.mkdirs();
File ediFile = new File(ediDir, name);
if (!ediFile.exists()) {
// only write this file to the results if not already present
FileUtil.writeToFile(stream.createInputStream(), ediFile);
}
} catch (IOException e) {
warn("Failed to write device info %s to result", name);
CLog.e(e);
}
}
/**
* {@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;
LogFile logFile = null;
try {
fis = new FileInputStream(resultFile);
logFile = mLogSaver.saveLogData("log-result", LogDataType.XML, fis);
debug("Result XML URL: %s", logFile.getUrl());
logReportFiles(mConfiguration, resultFile, resultFile.getName(), LogDataType.XML);
} 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);
logFile = mLogSaver.saveLogData("results", LogDataType.ZIP, zipResultStream);
debug("Result zip URL: %s", logFile.getUrl());
logReportFiles(
mConfiguration, zippedResults, "results", LogDataType.ZIP);
} 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;
}
/**
* Create results file compatible with CTSv2 (xml) report format.
*/
protected File generateResultXmlFile()
throws IOException, XmlPullParserException {
return ResultHandler.writeResults(
mBuildHelper.getSuiteName(),
mBuildHelper.getSuiteVersion(),
getSuitePlan(mBuildHelper),
mBuildHelper.getSuiteBuild(),
mResult,
mResultDir,
mResult.getStartTime(),
mElapsedTime + mResult.getStartTime(),
mReferenceUrl,
getLogUrl(),
mBuildHelper.getCommandLineArgs(),
mResultAttributes);
}
/**
* Add build info collected from the device attributes to the results.
*/
protected void addDeviceBuildInfoToResult() {
// Add all build info to the result to be serialized
Map<String, String> buildProperties = mapBuildInfo();
addBuildInfoToResult(buildProperties, mResult);
}
/**
* Override specific build properties so the report will be associated with the
* build fingerprint being certified.
*/
protected void addDeviceBuildInfoToResult(String buildFingerprintOverride,
String manufactureOverride, String modelOverride) {
Map<String, String> buildProperties = mapBuildInfo();
// Extract and override values from build fingerprint.
// Build fingerprint format: brand/product/device:version/build_id/tags
String fingerprintPrefix = buildFingerprintOverride.split(":")[0];
String fingerprintTail = buildFingerprintOverride.split(":")[1];
String buildIdOverride = fingerprintTail.split("/")[1];
buildProperties.put(BUILD_ID, buildIdOverride);
String brandOverride = fingerprintPrefix.split("/")[0];
buildProperties.put(BUILD_BRAND, brandOverride);
String deviceOverride = fingerprintPrefix.split("/")[2];
buildProperties.put(BUILD_DEVICE, deviceOverride);
String productOverride = fingerprintPrefix.split("/")[1];
buildProperties.put(BUILD_PRODUCT, productOverride);
String versionOverride = fingerprintTail.split("/")[0];
buildProperties.put(BUILD_VERSION_RELEASE, versionOverride);
buildProperties.put(BUILD_FINGERPRINT, buildFingerprintOverride);
buildProperties.put(BUILD_MANUFACTURER, manufactureOverride);
buildProperties.put(BUILD_MODEL, modelOverride);
// Add modified values to results.
addBuildInfoToResult(buildProperties, mResult);
mResult.setBuildFingerprint(buildFingerprintOverride);
}
/** Aggregate build info from member device info. */
protected Map<String, String> mapBuildInfo() {
Map<String, String> buildProperties = new HashMap<>();
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)) {
buildProperties.put(key.substring(CTS_PREFIX.length()), value);
}
}
}
return buildProperties;
}
/**
* Add build info to results.
* @param buildProperties Build info to add.
*/
protected static void addBuildInfoToResult(Map<String, String> buildProperties,
IInvocationResult invocationResult) {
buildProperties.entrySet().stream().forEach(entry ->
invocationResult.addInvocationInfo(entry.getKey(), entry.getValue()));
}
/**
* Get the suite plan. This protected method was created for overrides.
* Extending classes can decide on the content of the output's suite_plan field.
*
* @param mBuildHelper Helper that contains build information.
* @return string Suite plan to use.
*/
protected String getSuitePlan(CompatibilityBuildHelper mBuildHelper) {
return mBuildHelper.getSuitePlan();
}
/**
* 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) {
if (mResultServer != null && !mResultServer.trim().isEmpty() && !mDisableResultPosting) {
try {
debug("Result Server: %d", mUploader.uploadResult(resultFile, mReferenceUrl));
} catch (IOException ioe) {
CLog.e("[%s] IOException while uploading result.", mDeviceSerial);
CLog.e(ioe);
}
}
}
/**
* Returns whether it is safe to mark modules as "done", given the invocation command-line
* arguments. Returns true unless this is a retry and specific filtering techniques are applied
* on the command-line, such as:
* --retry-type failed
* --include-filter
* --exclude-filter
* -t/--test
* --subplan
*/
private boolean canMarkDone(String args) {
if (mRetrySessionId == null) {
return true; // always allow modules to be marked done if not retry
}
return !(RetryType.FAILED.equals(mRetryType)
|| RetryType.CUSTOM.equals(mRetryType)
|| args.contains(CompatibilityTestSuite.INCLUDE_FILTER_OPTION)
|| args.contains(CompatibilityTestSuite.EXCLUDE_FILTER_OPTION)
|| args.contains(CompatibilityTestSuite.SUBPLAN_OPTION)
|| args.matches(String.format(".* (-%s|--%s) .*",
CompatibilityTestSuite.TEST_OPTION_SHORT_NAME, CompatibilityTestSuite.TEST_OPTION)));
}
/**
* Copy the xml formatting files stored in this jar to the results directory
*
* @param resultsDir
*/
static void copyFormattingFiles(File resultsDir, String suiteName) {
for (String resultFileName : ResultHandler.RESULT_RESOURCES) {
InputStream configStream = ResultHandler.class.getResourceAsStream(
String.format("/report/%s-%s", suiteName, resultFileName));
if (configStream == null) {
// If suite specific files are not available, fallback to common.
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
*/
private void copyDynamicConfigFiles() {
File configDir = new File(mResultDir, "config");
if (!configDir.mkdir()) {
warn("Failed to make dynamic config directory \"%s\" in the result",
configDir.getAbsolutePath());
}
Set<String> uniqueModules = new HashSet<>();
for (IBuildInfo buildInfo : mMasterBuildInfos) {
CompatibilityBuildHelper helper = new CompatibilityBuildHelper(buildInfo);
Map<String, File> dcFiles = helper.getDynamicConfigFiles();
for (String moduleName : dcFiles.keySet()) {
File srcFile = dcFiles.get(moduleName);
if (!uniqueModules.contains(moduleName)) {
// have not seen config for this module yet, copy into result
File destFile = new File(configDir, moduleName + ".dynamic");
try {
FileUtil.copyFile(srcFile, destFile);
uniqueModules.add(moduleName); // Add to uniqueModules if copy succeeds
} catch (IOException e) {
warn("Failure when copying config file \"%s\" to \"%s\" for module %s",
srcFile.getAbsolutePath(), destFile.getAbsolutePath(), moduleName);
CLog.e(e);
}
}
FileUtil.deleteFile(srcFile);
}
}
}
/**
* Recursively copy any other files found in the previous session's result directory to the
* new result directory, so long as they don't already exist. For example, a "screenshots"
* directory generated in a previous session by a passing test will not be generated on retry
* unless copied from the old result directory.
*
* @param oldDir
* @param newDir
*/
static void copyRetryFiles(File oldDir, File newDir) {
File[] oldChildren = oldDir.listFiles();
for (File oldChild : oldChildren) {
if (NOT_RETRY_FILES.contains(oldChild.getName())) {
continue; // do not copy this file/directory or its children
}
File newChild = new File(newDir, oldChild.getName());
if (!newChild.exists()) {
// If this old file or directory doesn't exist in new dir, simply copy it
try {
if (oldChild.isDirectory()) {
FileUtil.recursiveCopy(oldChild, newChild);
} else {
FileUtil.copyFile(oldChild, newChild);
}
} catch (IOException e) {
warn("Failed to copy file \"%s\" from previous session", oldChild.getName());
}
} else if (oldChild.isDirectory() && newChild.isDirectory()) {
// If both children exist as directories, make sure the children of the old child
// directory exist in the new child directory.
copyRetryFiles(oldChild, newChild);
}
}
}
/**
* 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 debug to the console.
*/
private static void debug(String format, Object... args) {
log(LogLevel.DEBUG, 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 purpose.
*/
@VisibleForTesting
public IInvocationResult getResult() {
return mResult;
}
/**
* Returns true if the reporter is finalized before the end of the timeout. False otherwise.
*/
@VisibleForTesting
public boolean waitForFinalized(long timeout, TimeUnit unit) throws InterruptedException {
return mFinalized.await(timeout, unit);
}
private static String sanitizeXmlContent(String s) {
return XmlEscapers.xmlContentEscaper().escape(s);
}
/** Re-log a result file to all reporters so they are aware of it. */
private void logReportFiles(
IConfiguration configuration, File resultFile, String dataName, LogDataType type) {
if (configuration == null) {
return;
}
List<ITestInvocationListener> listeners = configuration.getTestInvocationListeners();
try (FileInputStreamSource source = new FileInputStreamSource(resultFile)) {
for (ITestInvocationListener listener : listeners) {
if (listener.equals(this)) {
// Avoid logging agaisnt itself
continue;
}
listener.testLog(dataName, type, source);
}
}
}
}