blob: f26c300ef0f6ea0e594551684aab09e7a9722339 [file] [log] [blame]
/*
* Copyright (C) 2018 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.game.qualification.reporter;
import com.android.ddmlib.Log;
import com.android.ddmlib.Log.LogLevel;
import com.android.ddmlib.testrunner.TestResult.TestStatus;
import com.android.game.qualification.CertificationRequirements;
import com.android.game.qualification.metric.GameQualificationFpsCollector;
import com.android.game.qualification.metric.MetricSummary;
import com.android.tradefed.config.Option;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.CollectingTestListener;
import com.android.tradefed.result.ILogSaver;
import com.android.tradefed.result.ILogSaverListener;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.result.LogFile;
import com.android.tradefed.result.TestDescription;
import com.android.tradefed.result.TestResult;
import com.android.tradefed.result.TestRunResult;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Result reporter for game core certification.
*/
public class GameQualificationResultReporter extends CollectingTestListener implements ILogSaverListener {
private static final String TAG = GameQualificationResultReporter.class.getSimpleName();
@Option(name = "suppress-passed-tests", description = "For functional tests, omit summary for "
+ "passing tests, only print failed and ignored ones")
private boolean mSuppressPassedTest = false;
private Map<TestDescription, MetricSummary> summaries = new ConcurrentHashMap<>();
private Map<TestDescription, CertificationRequirements> mRequirements = new ConcurrentHashMap<>();
private List<Throwable> invocationFailures = new ArrayList<>();
private List<LogFile> mLogFiles = new ArrayList<>();
private ILogSaver mLogSaver;
private int mTotalAllocated = 0;
public void putRequirements(TestDescription testId, CertificationRequirements requirements) {
mRequirements.put(testId, requirements);
}
@Override
public void invocationFailed(Throwable cause) {
super.invocationFailed(cause);
invocationFailures.add(cause);
}
/**
* Collect metrics produces by
* {@link GameQualificationFpsCollector}.
*/
@Override
public void testEnded(TestDescription testId, long elapsedTime, HashMap<String, Metric> metrics) {
super.testEnded(testId, elapsedTime, metrics);
if (!metrics.isEmpty()) {
MetricSummary summary = MetricSummary.parseRunMetrics(getInvocationContext(), metrics);
if (summary != null) {
summaries.put(testId, summary);
} else if (metrics.containsKey("memory_allocated")) {
mTotalAllocated = (int) metrics.get("memory_allocated").getMeasurements().getSingleInt();
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void testLogSaved(String dataName, LogDataType dataType, InputStreamSource dataStream,
LogFile logFile) {
super.testLogSaved(dataName, dataType, dataStream, logFile);
mLogFiles.add(logFile);
}
/**
* {@inheritDoc}
*/
@Override
public void setLogSaver(ILogSaver logSaver) {
mLogSaver = logSaver;
}
@Override
public void invocationEnded(long elapsedTime) {
super.invocationEnded(elapsedTime);
String summary = getInvocationSummary();
Log.logAndDisplay(LogLevel.INFO, TAG, summary);
try {
mLogSaver.saveLogData(
"GameQualification-summary",
LogDataType.TEXT,
new ByteArrayInputStream(summary.getBytes()));
} catch (IOException e) {
Log.logAndDisplay(
LogLevel.ERROR, TAG, "Failed writing summary to file: " + e.getMessage());
}
}
/**
* Get the invocation summary as a string.
*/
private String getInvocationSummary() {
if (getMergedTestRunResults().isEmpty() && mLogFiles.isEmpty()) {
return "No test results\n";
}
StringBuilder sb = new StringBuilder();
// Print location of log files.
if (!mLogFiles.isEmpty()) {
sb.append("Log Files:\n");
for (LogFile logFile : mLogFiles) {
final String url = logFile.getUrl();
sb.append(String.format(" %s\n", url != null ? url : logFile.getPath()));
}
sb.append("\n");
}
// Print invocation failures.
if (!invocationFailures.isEmpty()) {
sb.append("Unable to certify device. Invocation failed:\n");
for (Throwable t : invocationFailures) {
sb.append(" ");
sb.append(t.getMessage());
sb.append("\n");
}
return sb.toString();
}
// Print out device test results.
sb.append("Test Results:\n");
for (TestRunResult testRunResult : getMergedTestRunResults()) {
sb.append(getTestRunSummary(testRunResult));
}
// Print workload run metrics.
for (Map.Entry<TestDescription, MetricSummary> entry : summaries.entrySet()) {
sb.append(String.format("\n%s Metrics:\n%s\n", entry.getKey(), entry.getValue()));
}
// Print memory allocation metrics
sb.append("Total Memory Allocated During Allocation Stress Test: ");
sb.append(mTotalAllocated);
sb.append("\n\n");
// Determine certification level.
sb.append("Certification:\n");
boolean certified = true;
sb.append("Functional tests [");
sb.append(hasFailedTests() ? "FAILED" : "PASSED");
sb.append("]\n");
if (hasFailedTests()) {
certified = false;
sb.append("Certification failed because the following tests failed:\n");
for (TestRunResult testRunResult : getMergedTestRunResults()) {
for (TestDescription test : testRunResult.getFailedTests()) {
sb.append('\t');
sb.append(test.toString());
// Prints the first line of the stacktrace.
TestResult result = testRunResult.getTestResults().get(test);
String stacktrace = result.getStackTrace();
if (!stacktrace.isEmpty()) {
sb.append("\n\t\t");
sb.append(
stacktrace.contains("\n")
? stacktrace.split("\n", 2)[0]
: stacktrace);
}
sb.append('\n');
}
}
try {
LogFile logFile = createFunctionalTestFailureReport();
sb.append("Details can be found in ");
sb.append(logFile.getPath());
sb.append('\n');
} catch (IOException e) {
sb.append("Error generating test failures report: ");
sb.append(e.getMessage());
}
}
Report performanceReport = createPerformanceReport();
sb.append("Performance tests [");
sb.append(performanceReport.success ? "PASSED" : "FAILED");
sb.append("]\n");
sb.append(performanceReport.text);
if (!performanceReport.success) {
certified = false;
}
sb.append("\nGame Core Certification: ");
sb.append(certified ? "PASSED" : "FAILED");
return "Test results:\n" + sb.toString().trim() + "\n";
}
private LogFile createFunctionalTestFailureReport() throws IOException {
StringBuilder sb = new StringBuilder();
for (TestRunResult testRunResult : getMergedTestRunResults()) {
for (TestDescription test : testRunResult.getFailedTests()) {
sb.append("Test:\n");
sb.append(test.toString());
sb.append('\n');
sb.append("Error:\n");
TestResult result = testRunResult.getTestResults().get(test);
sb.append(result.getStackTrace());
sb.append("\n\n");
}
}
return mLogSaver.saveLogData(
"failure-report",
LogDataType.TEXT,
new ByteArrayInputStream(sb.toString().getBytes()));
}
/**
* Get the test run summary as a string including run metrics.
*/
private String getTestRunSummary(TestRunResult testRunResult) {
StringBuilder sb = new StringBuilder();
sb.append(String.format("%s:", testRunResult.getName()));
if (testRunResult.getNumTests() > 0) {
sb.append(String.format(" %d Test%s, %d Passed, %d Failed, %d Ignored",
testRunResult.getNumCompleteTests(),
testRunResult.getNumCompleteTests() == 1 ? "" : "s", // Pluralize Test
testRunResult.getNumTestsInState(TestStatus.PASSED),
testRunResult.getNumAllFailedTests(),
testRunResult.getNumTestsInState(TestStatus.IGNORED)));
} else if (testRunResult.getRunMetrics().size() == 0) {
sb.append(" No results");
}
sb.append("\n");
Map<TestDescription, TestResult> testResults = testRunResult.getTestResults();
for (Map.Entry<TestDescription, TestResult> entry : testResults.entrySet()) {
if (mSuppressPassedTest && TestStatus.PASSED.equals(entry.getValue().getStatus())) {
continue;
}
sb.append(getTestSummary(entry.getKey(), entry.getValue()));
}
sb.append("\n");
return sb.toString();
}
private static class Report {
boolean success;
String text;
public Report(boolean success, String text) {
this.success = success;
this.text = text;
}
}
/**
* Create a report on the performance metrics against certification requirements.
*
* Returns an empty String if all metrics passes certification.
*/
private Report createPerformanceReport() {
StringBuilder text = new StringBuilder();
boolean success = true;
for (Map.Entry<TestDescription, MetricSummary> entry : summaries.entrySet()) {
TestDescription testId = entry.getKey();
MetricSummary metrics = entry.getValue();
CertificationRequirements requirements = mRequirements.get(entry.getKey());
if (requirements == null) {
text.append("Warning: ");
text.append(testId.getTestName());
text.append(" was executed, but performance metrics was ignored because "
+ "certification requirements was not found.\n");
} else {
if (metrics.getJankRate() > requirements.getJankRate()) {
success = false;
text.append("Jank rate for ");
text.append(testId.getTestName());
text.append(" is too high, actual: ");
text.append(metrics.getJankRate());
text.append(", target: ");
text.append(requirements.getJankRate());
text.append("\n");
}
if (requirements.getLoadTime() >= 0) {
if (metrics.getLoadTimeMs() == -1) {
success = false;
text.append("Unable to determine load time for ");
text.append(testId.getTestName());
text.append(". Expected START_LOOP Intent was not received.\n");
} else if (metrics.getLoadTimeMs() > requirements.getLoadTime()) {
success = false;
text.append("Load time for ");
text.append(testId.getTestName());
text.append(" is too high, actual: ");
text.append(metrics.getLoadTimeMs());
text.append(" ms, target: ");
text.append(requirements.getLoadTime());
text.append(" ms\n");
}
}
}
}
return new Report(success, text.toString());
}
/**
* Get the test summary as string including test metrics.
*/
private String getTestSummary(TestDescription testId, TestResult testResult) {
StringBuilder sb = new StringBuilder();
sb.append(String.format(" %s: %s (%dms)\n", testId.toString(), testResult.getStatus(),
testResult.getEndTime() - testResult.getStartTime()));
String stack = testResult.getStackTrace();
if (stack != null && !stack.isEmpty()) {
sb.append(" stack=\n");
String lines[] = stack.split("\\r?\\n");
for (String line : lines) {
sb.append(String.format(" %s\n", line));
}
}
return sb.toString();
}
}