blob: 1b26db68e6d82d0cdd35677e08a21a3b9a8385ae [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.tradefed.result;
import com.android.ddmlib.testrunner.TestResult.TestStatus;
import com.android.tradefed.metrics.proto.MetricMeasurement.Measurements;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.retry.MergeStrategy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/** Container for a result of a single test. */
public class TestResult {
// Key that mark that an aggregation is hiding a failure.
public static final String IS_FLAKY = "is_flaky";
private TestStatus mStatus;
private FailureDescription mFailureDescription;
private Map<String, String> mMetrics;
private HashMap<String, Metric> mProtoMetrics;
private Map<String, LogFile> mLoggedFiles;
// the start and end time of the test, measured via {@link System#currentTimeMillis()}
private long mStartTime = 0;
private long mEndTime = 0;
public TestResult() {
mStatus = TestStatus.INCOMPLETE;
mStartTime = System.currentTimeMillis();
mLoggedFiles = new LinkedHashMap<String, LogFile>();
mMetrics = new HashMap<>();
mProtoMetrics = new HashMap<>();
}
/** Get the {@link TestStatus} result of the test. */
public TestStatus getStatus() {
return mStatus;
}
/**
* Get the associated {@link String} stack trace. Should be <code>null</code> if {@link
* #getStatus()} is {@link TestStatus#PASSED}.
*/
public String getStackTrace() {
if (mFailureDescription == null) {
return null;
}
return mFailureDescription.toString();
}
/**
* Get the associated {@link FailureDescription}. Should be <code>null</code> if {@link
* #getStatus()} is {@link TestStatus#PASSED}.
*/
public FailureDescription getFailure() {
return mFailureDescription;
}
/** Get the associated test metrics. */
public Map<String, String> getMetrics() {
return mMetrics;
}
/** Get the associated test metrics in proto format. */
public HashMap<String, Metric> getProtoMetrics() {
return mProtoMetrics;
}
/** Set the test metrics, overriding any previous values. */
public void setMetrics(Map<String, String> metrics) {
mMetrics = metrics;
}
/** Set the test proto metrics format, overriding any previous values. */
public void setProtoMetrics(HashMap<String, Metric> metrics) {
mProtoMetrics = metrics;
}
/** Add a logged file tracking associated with that test case */
public void addLoggedFile(String dataName, LogFile loggedFile) {
mLoggedFiles.put(dataName, loggedFile);
}
/** Returns a copy of the map containing all the logged file associated with that test case. */
public Map<String, LogFile> getLoggedFiles() {
return new LinkedHashMap<>(mLoggedFiles);
}
/**
* Return the {@link System#currentTimeMillis()} time that the {@link
* ITestInvocationListener#testStarted(TestDescription)} event was received.
*/
public long getStartTime() {
return mStartTime;
}
/**
* Allows to set the time when the test was started, to be used with {@link
* ITestInvocationListener#testStarted(TestDescription, long)}.
*/
public void setStartTime(long startTime) {
mStartTime = startTime;
}
/**
* Return the {@link System#currentTimeMillis()} time that the {@link
* ITestInvocationListener#testEnded(TestDescription, Map)} event was received.
*/
public long getEndTime() {
return mEndTime;
}
/** Set the {@link TestStatus}. */
public TestResult setStatus(TestStatus status) {
mStatus = status;
return this;
}
/** Set the stack trace. */
public void setStackTrace(String stackTrace) {
mFailureDescription = FailureDescription.create(stackTrace);
}
/** Set the stack trace. */
public void setFailure(FailureDescription failureDescription) {
mFailureDescription = failureDescription;
}
/** Sets the end time */
public void setEndTime(long currentTimeMillis) {
mEndTime = currentTimeMillis;
}
@Override
public int hashCode() {
return Arrays.hashCode(new Object[] {mMetrics, mFailureDescription, mStatus});
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
TestResult other = (TestResult) obj;
return Objects.equals(mMetrics, other.mMetrics)
&& Objects.equals(
String.valueOf(mFailureDescription),
String.valueOf(other.mFailureDescription))
&& Objects.equals(mStatus, other.mStatus);
}
private void markFlaky() {
mProtoMetrics.put(
IS_FLAKY,
Metric.newBuilder()
.setMeasurements(Measurements.newBuilder().setSingleString("true").build())
.build());
}
/**
* Merge the attempts for a same test case based on the merging strategy.
*
* @param results List of {@link TestResult} that will be merged
* @param strategy the {@link MergeStrategy} to be used to determine the merging outcome.
* @return the merged {@link TestResult} or null if there is nothing to merge.
*/
public static TestResult merge(List<TestResult> results, MergeStrategy strategy) {
if (results.isEmpty()) {
return null;
}
if (MergeStrategy.NO_MERGE.equals(strategy)) {
throw new IllegalArgumentException(
"TestResult#merge cannot be called with NO_MERGE strategy.");
}
TestResult mergedResult = new TestResult();
long earliestStartTime = Long.MAX_VALUE;
long latestEndTime = Long.MIN_VALUE;
List<FailureDescription> errors = new ArrayList<>();
int pass = 0;
int fail = 0;
int assumption_failure = 0;
int ignored = 0;
int incomplete = 0;
TestStatus lastStatus = null;
for (TestResult attempt : results) {
mergedResult.mProtoMetrics.putAll(attempt.getProtoMetrics());
mergedResult.mMetrics.putAll(attempt.getMetrics());
mergedResult.mLoggedFiles.putAll(attempt.getLoggedFiles());
earliestStartTime = Math.min(attempt.getStartTime(), earliestStartTime);
latestEndTime = Math.max(attempt.getEndTime(), latestEndTime);
switch (attempt.getStatus()) {
case PASSED:
pass++;
break;
case FAILURE:
fail++;
if (attempt.getFailure() != null) {
errors.add(attempt.getFailure());
}
break;
case INCOMPLETE:
incomplete++;
errors.add(FailureDescription.create("incomplete test case result."));
break;
case ASSUMPTION_FAILURE:
assumption_failure++;
if (attempt.getFailure() != null) {
errors.add(attempt.getFailure());
}
break;
case IGNORED:
ignored++;
break;
}
lastStatus = attempt.getStatus();
}
switch (strategy) {
case ANY_PASS_IS_PASS:
case ONE_TESTCASE_PASS_IS_PASS:
// We prioritize passing the test due to the merging strategy.
if (pass > 0) {
mergedResult.setStatus(TestStatus.PASSED);
if (fail > 0) {
mergedResult.markFlaky();
}
} else if (fail == 0) {
if (ignored > 0) {
mergedResult.setStatus(TestStatus.IGNORED);
} else if (assumption_failure > 0) {
mergedResult.setStatus(TestStatus.ASSUMPTION_FAILURE);
} else if (incomplete > 0) {
mergedResult.setStatus(TestStatus.INCOMPLETE);
}
} else {
if (TestStatus.ASSUMPTION_FAILURE.equals(lastStatus)) {
mergedResult.setStatus(TestStatus.ASSUMPTION_FAILURE);
} else if (TestStatus.IGNORED.equals(lastStatus)) {
mergedResult.setStatus(TestStatus.IGNORED);
} else {
mergedResult.setStatus(TestStatus.FAILURE);
}
}
break;
default:
// We keep a default of one failure is a failure that should be reported.
if (fail > 0) {
mergedResult.setStatus(TestStatus.FAILURE);
} else {
if (ignored > 0) {
mergedResult.setStatus(TestStatus.IGNORED);
} else if (assumption_failure > 0) {
mergedResult.setStatus(TestStatus.ASSUMPTION_FAILURE);
} else if (incomplete > 0) {
mergedResult.setStatus(TestStatus.INCOMPLETE);
} else {
mergedResult.setStatus(TestStatus.PASSED);
}
}
break;
}
if (errors.isEmpty()) {
mergedResult.mFailureDescription = null;
} else if (errors.size() == 1) {
mergedResult.mFailureDescription = errors.get(0);
} else {
mergedResult.mFailureDescription = new MultiFailureDescription(errors);
}
mergedResult.setStartTime(earliestStartTime);
mergedResult.setEndTime(latestEndTime);
return mergedResult;
}
}