1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.tradefed.result;
17 
18 import com.android.ddmlib.testrunner.TestResult.TestStatus;
19 import com.android.tradefed.metrics.proto.MetricMeasurement.Measurements;
20 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
21 import com.android.tradefed.retry.MergeStrategy;
22 
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.HashMap;
26 import java.util.LinkedHashMap;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Objects;
30 
31 /** Container for a result of a single test. */
32 public class TestResult {
33     // Key that mark that an aggregation is hiding a failure.
34     public static final String IS_FLAKY = "is_flaky";
35 
36     private TestStatus mStatus;
37     private FailureDescription mFailureDescription;
38     private Map<String, String> mMetrics;
39     private HashMap<String, Metric> mProtoMetrics;
40     private Map<String, LogFile> mLoggedFiles;
41     // the start and end time of the test, measured via {@link System#currentTimeMillis()}
42     private long mStartTime = 0;
43     private long mEndTime = 0;
44 
TestResult()45     public TestResult() {
46         mStatus = TestStatus.INCOMPLETE;
47         mStartTime = System.currentTimeMillis();
48         mLoggedFiles = new LinkedHashMap<String, LogFile>();
49         mMetrics = new HashMap<>();
50         mProtoMetrics = new HashMap<>();
51     }
52 
53     /** Get the {@link TestStatus} result of the test. */
getStatus()54     public TestStatus getStatus() {
55         return mStatus;
56     }
57 
58     /**
59      * Get the associated {@link String} stack trace. Should be <code>null</code> if {@link
60      * #getStatus()} is {@link TestStatus#PASSED}.
61      */
getStackTrace()62     public String getStackTrace() {
63         if (mFailureDescription == null) {
64             return null;
65         }
66         return mFailureDescription.toString();
67     }
68 
69     /**
70      * Get the associated {@link FailureDescription}. Should be <code>null</code> if {@link
71      * #getStatus()} is {@link TestStatus#PASSED}.
72      */
getFailure()73     public FailureDescription getFailure() {
74         return mFailureDescription;
75     }
76 
77     /** Get the associated test metrics. */
getMetrics()78     public Map<String, String> getMetrics() {
79         return mMetrics;
80     }
81 
82     /** Get the associated test metrics in proto format. */
getProtoMetrics()83     public HashMap<String, Metric> getProtoMetrics() {
84         return mProtoMetrics;
85     }
86 
87     /** Set the test metrics, overriding any previous values. */
setMetrics(Map<String, String> metrics)88     public void setMetrics(Map<String, String> metrics) {
89         mMetrics = metrics;
90     }
91 
92     /** Set the test proto metrics format, overriding any previous values. */
setProtoMetrics(HashMap<String, Metric> metrics)93     public void setProtoMetrics(HashMap<String, Metric> metrics) {
94         mProtoMetrics = metrics;
95     }
96 
97     /** Add a logged file tracking associated with that test case */
addLoggedFile(String dataName, LogFile loggedFile)98     public void addLoggedFile(String dataName, LogFile loggedFile) {
99         mLoggedFiles.put(dataName, loggedFile);
100     }
101 
102     /** Returns a copy of the map containing all the logged file associated with that test case. */
getLoggedFiles()103     public Map<String, LogFile> getLoggedFiles() {
104         return new LinkedHashMap<>(mLoggedFiles);
105     }
106 
107     /**
108      * Return the {@link System#currentTimeMillis()} time that the {@link
109      * ITestInvocationListener#testStarted(TestDescription)} event was received.
110      */
getStartTime()111     public long getStartTime() {
112         return mStartTime;
113     }
114 
115     /**
116      * Allows to set the time when the test was started, to be used with {@link
117      * ITestInvocationListener#testStarted(TestDescription, long)}.
118      */
setStartTime(long startTime)119     public void setStartTime(long startTime) {
120         mStartTime = startTime;
121     }
122 
123     /**
124      * Return the {@link System#currentTimeMillis()} time that the {@link
125      * ITestInvocationListener#testEnded(TestDescription, Map)} event was received.
126      */
getEndTime()127     public long getEndTime() {
128         return mEndTime;
129     }
130 
131     /** Set the {@link TestStatus}. */
setStatus(TestStatus status)132     public TestResult setStatus(TestStatus status) {
133         mStatus = status;
134         return this;
135     }
136 
137     /** Set the stack trace. */
setStackTrace(String stackTrace)138     public void setStackTrace(String stackTrace) {
139         mFailureDescription = FailureDescription.create(stackTrace);
140     }
141 
142     /** Set the stack trace. */
setFailure(FailureDescription failureDescription)143     public void setFailure(FailureDescription failureDescription) {
144         mFailureDescription = failureDescription;
145     }
146 
147     /** Sets the end time */
setEndTime(long currentTimeMillis)148     public void setEndTime(long currentTimeMillis) {
149         mEndTime = currentTimeMillis;
150     }
151 
152     @Override
hashCode()153     public int hashCode() {
154         return Arrays.hashCode(new Object[] {mMetrics, mFailureDescription, mStatus});
155     }
156 
157     @Override
equals(Object obj)158     public boolean equals(Object obj) {
159         if (this == obj) {
160             return true;
161         }
162         if (obj == null) {
163             return false;
164         }
165         if (getClass() != obj.getClass()) {
166             return false;
167         }
168         TestResult other = (TestResult) obj;
169         return Objects.equals(mMetrics, other.mMetrics)
170                 && Objects.equals(
171                         String.valueOf(mFailureDescription),
172                         String.valueOf(other.mFailureDescription))
173                 && Objects.equals(mStatus, other.mStatus);
174     }
175 
markFlaky()176     private void markFlaky() {
177         mProtoMetrics.put(
178                 IS_FLAKY,
179                 Metric.newBuilder()
180                         .setMeasurements(Measurements.newBuilder().setSingleString("true").build())
181                         .build());
182     }
183 
184     /**
185      * Merge the attempts for a same test case based on the merging strategy.
186      *
187      * @param results List of {@link TestResult} that will be merged
188      * @param strategy the {@link MergeStrategy} to be used to determine the merging outcome.
189      * @return the merged {@link TestResult} or null if there is nothing to merge.
190      */
merge(List<TestResult> results, MergeStrategy strategy)191     public static TestResult merge(List<TestResult> results, MergeStrategy strategy) {
192         if (results.isEmpty()) {
193             return null;
194         }
195         if (MergeStrategy.NO_MERGE.equals(strategy)) {
196             throw new IllegalArgumentException(
197                     "TestResult#merge cannot be called with NO_MERGE strategy.");
198         }
199         TestResult mergedResult = new TestResult();
200 
201         long earliestStartTime = Long.MAX_VALUE;
202         long latestEndTime = Long.MIN_VALUE;
203 
204         List<FailureDescription> errors = new ArrayList<>();
205         int pass = 0;
206         int fail = 0;
207         int assumption_failure = 0;
208         int ignored = 0;
209         int incomplete = 0;
210 
211         for (TestResult attempt : results) {
212             mergedResult.mProtoMetrics.putAll(attempt.getProtoMetrics());
213             mergedResult.mMetrics.putAll(attempt.getMetrics());
214             mergedResult.mLoggedFiles.putAll(attempt.getLoggedFiles());
215             earliestStartTime = Math.min(attempt.getStartTime(), earliestStartTime);
216             latestEndTime = Math.max(attempt.getEndTime(), latestEndTime);
217             switch (attempt.getStatus()) {
218                 case PASSED:
219                     pass++;
220                     break;
221                 case FAILURE:
222                     fail++;
223                     if (attempt.getFailure() != null) {
224                         errors.add(attempt.getFailure());
225                     }
226                     break;
227                 case INCOMPLETE:
228                     incomplete++;
229                     errors.add(FailureDescription.create("incomplete test case result."));
230                     break;
231                 case ASSUMPTION_FAILURE:
232                     assumption_failure++;
233                     if (attempt.getFailure() != null) {
234                         errors.add(attempt.getFailure());
235                     }
236                     break;
237                 case IGNORED:
238                     ignored++;
239                     break;
240             }
241         }
242 
243         switch (strategy) {
244             case ANY_PASS_IS_PASS:
245             case ONE_TESTCASE_PASS_IS_PASS:
246                 // We prioritize passing the test due to the merging strategy.
247                 if (pass > 0) {
248                     mergedResult.setStatus(TestStatus.PASSED);
249                     if (fail > 0) {
250                         mergedResult.markFlaky();
251                     }
252                 } else if (fail == 0) {
253                     if (ignored > 0) {
254                         mergedResult.setStatus(TestStatus.IGNORED);
255                     } else if (assumption_failure > 0) {
256                         mergedResult.setStatus(TestStatus.ASSUMPTION_FAILURE);
257                     } else if (incomplete > 0) {
258                         mergedResult.setStatus(TestStatus.INCOMPLETE);
259                     }
260                 } else {
261                     mergedResult.setStatus(TestStatus.FAILURE);
262                 }
263                 break;
264             default:
265                 // We keep a sane default of one failure is a failure that should be reported.
266                 if (fail > 0) {
267                     mergedResult.setStatus(TestStatus.FAILURE);
268                 } else {
269                     if (ignored > 0) {
270                         mergedResult.setStatus(TestStatus.IGNORED);
271                     } else if (assumption_failure > 0) {
272                         mergedResult.setStatus(TestStatus.ASSUMPTION_FAILURE);
273                     } else if (incomplete > 0) {
274                         mergedResult.setStatus(TestStatus.INCOMPLETE);
275                     } else {
276                         mergedResult.setStatus(TestStatus.PASSED);
277                     }
278                 }
279                 break;
280         }
281         if (errors.isEmpty()) {
282             mergedResult.mFailureDescription = null;
283         } else if (errors.size() == 1) {
284             mergedResult.mFailureDescription = errors.get(0);
285         } else {
286             mergedResult.mFailureDescription = new MultiFailureDescription(errors);
287         }
288         mergedResult.setStartTime(earliestStartTime);
289         mergedResult.setEndTime(latestEndTime);
290         return mergedResult;
291     }
292 }
293