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.log.LogUtil.CLog;
20 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
21 import com.android.tradefed.util.proto.TfMetricProtoUtil;
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.LinkedHashSet;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Set;
31 
32 /**
33  * Holds results from a single test run.
34  *
35  * <p>Maintains an accurate count of tests, and tracks incomplete tests.
36  *
37  * <p>Not thread safe! The test* callbacks must be called in order
38  */
39 public class TestRunResult {
40     private String mTestRunName;
41     // Uses a LinkedHashMap to have predictable iteration order
42     private Map<TestDescription, TestResult> mTestResults =
43             new LinkedHashMap<TestDescription, TestResult>();
44     // Store the metrics for the run
45     private Map<String, String> mRunMetrics = new HashMap<>();
46     private HashMap<String, Metric> mRunProtoMetrics = new HashMap<>();
47     // Log files associated with the test run itself (testRunStart / testRunEnd).
48     private Map<String, LogFile> mRunLoggedFiles;
49     private boolean mIsRunComplete = false;
50     private long mElapsedTime = 0;
51 
52     private TestResult mCurrentTestResult;
53 
54     /** represents sums of tests in each TestStatus state. Indexed by TestStatus.ordinal() */
55     private int[] mStatusCounts = new int[TestStatus.values().length];
56     /** tracks if mStatusCounts is accurate, or if it needs to be recalculated */
57     private boolean mIsCountDirty = true;
58 
59     private String mRunFailureError = null;
60 
61     private boolean mAggregateMetrics = false;
62 
63     /** Create an empty{@link TestRunResult}. */
TestRunResult()64     public TestRunResult() {
65         mTestRunName = "not started";
66         mRunLoggedFiles = new LinkedHashMap<String, LogFile>();
67     }
68 
setAggregateMetrics(boolean metricAggregation)69     public void setAggregateMetrics(boolean metricAggregation) {
70         mAggregateMetrics = metricAggregation;
71     }
72 
73     /** @return the test run name */
getName()74     public String getName() {
75         return mTestRunName;
76     }
77 
78     /** Returns a map of the test results. */
getTestResults()79     public Map<TestDescription, TestResult> getTestResults() {
80         return mTestResults;
81     }
82 
83     /** @return a {@link Map} of the test run metrics. */
getRunMetrics()84     public Map<String, String> getRunMetrics() {
85         return mRunMetrics;
86     }
87 
88     /** @return a {@link Map} of the test run metrics with the new proto format. */
getRunProtoMetrics()89     public HashMap<String, Metric> getRunProtoMetrics() {
90         return mRunProtoMetrics;
91     }
92 
93     /** Gets the set of completed tests. */
getCompletedTests()94     public Set<TestDescription> getCompletedTests() {
95         List completedStatuses = new ArrayList<TestStatus>();
96         for (TestStatus s : TestStatus.values()) {
97             if (!s.equals(TestStatus.INCOMPLETE)) {
98                 completedStatuses.add(s);
99             }
100         }
101         return getTestsInState(completedStatuses);
102     }
103 
104     /** Gets the set of failed tests. */
getFailedTests()105     public Set<TestDescription> getFailedTests() {
106         return getTestsInState(Arrays.asList(TestStatus.FAILURE));
107     }
108 
109     /** Gets the set of tests in given statuses. */
getTestsInState(List<TestStatus> statuses)110     private Set<TestDescription> getTestsInState(List<TestStatus> statuses) {
111         Set<TestDescription> tests = new LinkedHashSet<>();
112         for (Map.Entry<TestDescription, TestResult> testEntry : getTestResults().entrySet()) {
113             TestStatus status = testEntry.getValue().getStatus();
114             if (statuses.contains(status)) {
115                 tests.add(testEntry.getKey());
116             }
117         }
118         return tests;
119     }
120 
121     /** @return <code>true</code> if test run failed. */
isRunFailure()122     public boolean isRunFailure() {
123         return mRunFailureError != null;
124     }
125 
126     /** @return <code>true</code> if test run finished. */
isRunComplete()127     public boolean isRunComplete() {
128         return mIsRunComplete;
129     }
130 
setRunComplete(boolean runComplete)131     public void setRunComplete(boolean runComplete) {
132         mIsRunComplete = runComplete;
133     }
134 
135     /** Gets the number of tests in given state for this run. */
getNumTestsInState(TestStatus status)136     public int getNumTestsInState(TestStatus status) {
137         if (mIsCountDirty) {
138             // clear counts
139             for (int i = 0; i < mStatusCounts.length; i++) {
140                 mStatusCounts[i] = 0;
141             }
142             // now recalculate
143             for (TestResult r : mTestResults.values()) {
144                 mStatusCounts[r.getStatus().ordinal()]++;
145             }
146             mIsCountDirty = false;
147         }
148         return mStatusCounts[status.ordinal()];
149     }
150 
151     /** Gets the number of tests in this run. */
getNumTests()152     public int getNumTests() {
153         return mTestResults.size();
154     }
155 
156     /** Gets the number of complete tests in this run ie with status != incomplete. */
getNumCompleteTests()157     public int getNumCompleteTests() {
158         return getNumTests() - getNumTestsInState(TestStatus.INCOMPLETE);
159     }
160 
161     /** @return <code>true</code> if test run had any failed or error tests. */
hasFailedTests()162     public boolean hasFailedTests() {
163         return getNumAllFailedTests() > 0;
164     }
165 
166     /** Return total number of tests in a failure state (failed, assumption failure) */
getNumAllFailedTests()167     public int getNumAllFailedTests() {
168         return getNumTestsInState(TestStatus.FAILURE);
169     }
170 
171     /** Returns the current run elapsed time. */
getElapsedTime()172     public long getElapsedTime() {
173         return mElapsedTime;
174     }
175 
176     /** Return the run failure error message, <code>null</code> if run did not fail. */
getRunFailureMessage()177     public String getRunFailureMessage() {
178         return mRunFailureError;
179     }
180 
181     /**
182      * Notify that a test run started.
183      *
184      * @param runName the name associated to the test run for tracking purpose.
185      * @param testCount the number of test cases associated with the test count.
186      */
testRunStarted(String runName, int testCount)187     public void testRunStarted(String runName, int testCount) {
188         mTestRunName = runName;
189         mIsRunComplete = false;
190         // Do not reset mRunFailureError since for re-run we want to preserve previous failures.
191     }
192 
testStarted(TestDescription test)193     public void testStarted(TestDescription test) {
194         testStarted(test, System.currentTimeMillis());
195     }
196 
testStarted(TestDescription test, long startTime)197     public void testStarted(TestDescription test, long startTime) {
198         mCurrentTestResult = new TestResult();
199         mCurrentTestResult.setStartTime(startTime);
200         addTestResult(test, mCurrentTestResult);
201     }
202 
addTestResult(TestDescription test, TestResult testResult)203     private void addTestResult(TestDescription test, TestResult testResult) {
204         mIsCountDirty = true;
205         mTestResults.put(test, testResult);
206     }
207 
updateTestResult(TestDescription test, TestStatus status, String trace)208     private void updateTestResult(TestDescription test, TestStatus status, String trace) {
209         TestResult r = mTestResults.get(test);
210         if (r == null) {
211             CLog.d("received test event without test start for %s", test);
212             r = new TestResult();
213         }
214         r.setStatus(status);
215         r.setStackTrace(trace);
216         addTestResult(test, r);
217     }
218 
testFailed(TestDescription test, String trace)219     public void testFailed(TestDescription test, String trace) {
220         updateTestResult(test, TestStatus.FAILURE, trace);
221     }
222 
testAssumptionFailure(TestDescription test, String trace)223     public void testAssumptionFailure(TestDescription test, String trace) {
224         updateTestResult(test, TestStatus.ASSUMPTION_FAILURE, trace);
225     }
226 
testIgnored(TestDescription test)227     public void testIgnored(TestDescription test) {
228         updateTestResult(test, TestStatus.IGNORED, null);
229     }
230 
testEnded(TestDescription test, HashMap<String, Metric> testMetrics)231     public void testEnded(TestDescription test, HashMap<String, Metric> testMetrics) {
232         testEnded(test, System.currentTimeMillis(), testMetrics);
233     }
234 
testEnded(TestDescription test, long endTime, HashMap<String, Metric> testMetrics)235     public void testEnded(TestDescription test, long endTime, HashMap<String, Metric> testMetrics) {
236         TestResult result = mTestResults.get(test);
237         if (result == null) {
238             result = new TestResult();
239         }
240         if (result.getStatus().equals(TestStatus.INCOMPLETE)) {
241             result.setStatus(TestStatus.PASSED);
242         }
243         result.setEndTime(endTime);
244         result.setMetrics(TfMetricProtoUtil.compatibleConvert(testMetrics));
245         result.setProtoMetrics(testMetrics);
246         addTestResult(test, result);
247         mCurrentTestResult = null;
248     }
249 
testRunFailed(String errorMessage)250     public void testRunFailed(String errorMessage) {
251         mRunFailureError = errorMessage;
252     }
253 
testRunStopped(long elapsedTime)254     public void testRunStopped(long elapsedTime) {
255         mElapsedTime += elapsedTime;
256         mIsRunComplete = true;
257     }
258 
testRunEnded(long elapsedTime, Map<String, String> runMetrics)259     public void testRunEnded(long elapsedTime, Map<String, String> runMetrics) {
260         if (mAggregateMetrics) {
261             for (Map.Entry<String, String> entry : runMetrics.entrySet()) {
262                 String existingValue = mRunMetrics.get(entry.getKey());
263                 String combinedValue = combineValues(existingValue, entry.getValue());
264                 mRunMetrics.put(entry.getKey(), combinedValue);
265             }
266         } else {
267             mRunMetrics.putAll(runMetrics);
268         }
269         // Also add to the new interface:
270         mRunProtoMetrics.putAll(TfMetricProtoUtil.upgradeConvert(runMetrics));
271 
272         mElapsedTime += elapsedTime;
273         mIsRunComplete = true;
274     }
275 
276     /** New interface using the new proto metrics. */
testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics)277     public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
278         // Internally store the information as backward compatible format
279         testRunEnded(elapsedTime, TfMetricProtoUtil.compatibleConvert(runMetrics));
280         // Store the new format directly too.
281         // TODO: See if aggregation should/can be done with the new format.
282         mRunProtoMetrics.putAll(runMetrics);
283 
284         // TODO: when old format is deprecated, do not forget to uncomment the next two lines
285         // mElapsedTime += elapsedTime;
286         // mIsRunComplete = true;
287     }
288 
289     /**
290      * Combine old and new metrics value
291      *
292      * @param existingValue
293      * @param newValue
294      * @return the combination of the two string as Long or Double value.
295      */
combineValues(String existingValue, String newValue)296     private String combineValues(String existingValue, String newValue) {
297         if (existingValue != null) {
298             try {
299                 Long existingLong = Long.parseLong(existingValue);
300                 Long newLong = Long.parseLong(newValue);
301                 return Long.toString(existingLong + newLong);
302             } catch (NumberFormatException e) {
303                 // not a long, skip to next
304             }
305             try {
306                 Double existingDouble = Double.parseDouble(existingValue);
307                 Double newDouble = Double.parseDouble(newValue);
308                 return Double.toString(existingDouble + newDouble);
309             } catch (NumberFormatException e) {
310                 // not a double either, fall through
311             }
312         }
313         // default to overriding existingValue
314         return newValue;
315     }
316 
317     /** Returns a user friendly string describing results. */
getTextSummary()318     public String getTextSummary() {
319         StringBuilder builder = new StringBuilder();
320         builder.append(String.format("Total tests %d, ", getNumTests()));
321         for (TestStatus status : TestStatus.values()) {
322             int count = getNumTestsInState(status);
323             // only add descriptive state for states that have non zero values, to avoid cluttering
324             // the response
325             if (count > 0) {
326                 builder.append(String.format("%s %d, ", status.toString().toLowerCase(), count));
327             }
328         }
329         return builder.toString();
330     }
331 
332     /**
333      * Information about a file being logged are stored and associated to the test case or test run
334      * in progress.
335      *
336      * @param dataName the name referencing the data.
337      * @param logFile The {@link LogFile} object representing where the object was saved and and
338      *     information about it.
339      */
testLogSaved(String dataName, LogFile logFile)340     public void testLogSaved(String dataName, LogFile logFile) {
341         if (mCurrentTestResult != null) {
342             // We have a test case in progress, we can associate the log to it.
343             mCurrentTestResult.addLoggedFile(dataName, logFile);
344         } else {
345             mRunLoggedFiles.put(dataName, logFile);
346         }
347     }
348 
349     /** Returns a copy of the map containing all the logged file associated with that test case. */
getRunLoggedFiles()350     public Map<String, LogFile> getRunLoggedFiles() {
351         return new LinkedHashMap<>(mRunLoggedFiles);
352     }
353 
354 
355     /**
356      * Merge multiple TestRunResults of the same testRunName. If a testcase shows up in multiple
357      * TestRunResults but has different results (e.g. "boottest-device" runs three times with result
358      * FAIL-FAIL-PASS), we concatenate all the stack traces from the FAILED runs and trust the final
359      * run result for status, metrics, log files, start/end time.
360      *
361      * @param testRunResults A list of TestRunResult to merge.
362      * @return the final TestRunResult containing the merged data from the testRunResults.
363      */
merge(List<TestRunResult> testRunResults)364     public static TestRunResult merge(List<TestRunResult> testRunResults) {
365         if (testRunResults.isEmpty()) {
366             return null;
367         }
368         TestRunResult finalRunResult = new TestRunResult();
369 
370         String testRunName = testRunResults.get(0).getName();
371         Map<String, String> finalRunMetrics = new HashMap<>();
372         HashMap<String, Metric> finalRunProtoMetrics = new HashMap<>();
373         Map<String, LogFile> finalRunLoggedFiles = new HashMap<>();
374         Map<TestDescription, TestResult> finalTestResults =
375                 new HashMap<TestDescription, TestResult>();
376 
377         for (TestRunResult eachRunResult : testRunResults) {
378             // Check all mTestRunNames are the same.
379             if (!testRunName.equals(eachRunResult.getName())) {
380                 throw new IllegalArgumentException(
381                         String.format(
382                                 "Unabled to merge TestRunResults: The run results names are "
383                                         + "different (%s, %s)",
384                                 testRunName, eachRunResult.getName()));
385             }
386             // Keep the last TestRunResult's RunMetrics, ProtoMetrics and logFiles.
387             // TODO: Currently we keep a single item when multiple TestRunResult have the same
388             // keys. In the future, we may want to improve this logic.
389             finalRunMetrics.putAll(eachRunResult.getRunMetrics());
390             finalRunProtoMetrics.putAll(eachRunResult.getRunProtoMetrics());
391             finalRunLoggedFiles.putAll(eachRunResult.getRunLoggedFiles());
392             // TODO: We are not handling the TestResult log files in the merging logic (different
393             // from the TestRunResult log files). Need to improve in the future.
394             for (Map.Entry<TestDescription, TestResult> testResultEntry :
395                     eachRunResult.getTestResults().entrySet()) {
396                 if (!finalTestResults.containsKey(testResultEntry.getKey())) {
397                     TestResult newResult = TestResult.clone(testResultEntry.getValue());
398                     finalTestResults.put(testResultEntry.getKey(), newResult);
399                 } else {
400                     /**
401                      * Merge the same testcase's TestResults. - Test status is the final run's
402                      * status, - Test stack trace is the concatenation of each TestResult's stack
403                      * traces. - Test start time is the first TestResult's start time. - Test end
404                      * time is the last TestResult's end time. - Test metrics is the first
405                      * TestResult's metrics.
406                      */
407                     TestResult existingResult = finalTestResults.get(testResultEntry.getKey());
408                     // If the test passes, then it doesn't have stack trace.
409                     if (testResultEntry.getValue().getStackTrace() != null) {
410                         if (existingResult.getStackTrace() != null) {
411                             String stackTrace =
412                                     String.format(
413                                             "%s\n%s",
414                                             existingResult.getStackTrace(),
415                                             testResultEntry.getValue().getStackTrace());
416                             existingResult.setStackTrace(stackTrace);
417                         } else {
418                             existingResult.setStackTrace(
419                                     testResultEntry.getValue().getStackTrace());
420                         }
421                     }
422                     existingResult.setStatus(testResultEntry.getValue().getStatus());
423                     existingResult.setEndTime(testResultEntry.getValue().getEndTime());
424                     finalTestResults.put(testResultEntry.getKey(), existingResult);
425                 }
426             }
427         }
428         finalRunResult.mTestRunName = testRunName;
429         finalRunResult.mRunMetrics = finalRunMetrics;
430         finalRunResult.mRunProtoMetrics = finalRunProtoMetrics;
431         finalRunResult.mRunLoggedFiles = finalRunLoggedFiles;
432         finalRunResult.mTestResults = finalTestResults;
433         return finalRunResult;
434     }
435 }
436