1 /*
2  * Copyright (C) 2010 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.build.IBuildInfo;
20 import com.android.tradefed.config.Option;
21 import com.android.tradefed.invoker.IInvocationContext;
22 import com.android.tradefed.log.LogUtil.CLog;
23 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
24 import com.android.tradefed.retry.MergeStrategy;
25 
26 import com.google.common.annotations.VisibleForTesting;
27 
28 import java.util.ArrayList;
29 import java.util.Collection;
30 import java.util.Collections;
31 import java.util.HashMap;
32 import java.util.LinkedHashMap;
33 import java.util.LinkedList;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.Map.Entry;
37 import java.util.concurrent.atomic.AtomicBoolean;
38 
39 /**
40  * A {@link ITestInvocationListener} that will collect all test results.
41  *
42  * <p>Although the data structures used in this object are thread-safe, the {@link
43  * ITestInvocationListener} callbacks must be called in the correct order.
44  */
45 public class CollectingTestListener implements ITestInvocationListener, ILogSaverListener {
46 
47     @Option(
48             name = "aggregate-metrics",
49             description = "attempt to add test metrics values for test runs with the same name.")
50     private boolean mIsAggregateMetrics = false;
51 
52     /** Toggle the 'aggregate metrics' option */
setIsAggregrateMetrics(boolean aggregate)53     protected void setIsAggregrateMetrics(boolean aggregate) {
54         mIsAggregateMetrics = aggregate;
55     }
56 
57     private IInvocationContext mContext;
58     private IBuildInfo mBuildInfo;
59     private Map<String, IInvocationContext> mModuleContextMap = new HashMap<>();
60     // Use LinkedHashMap to provide consistent iterations over the keys.
61     private Map<String, List<TestRunResult>> mTestRunResultMap =
62             Collections.synchronizedMap(new LinkedHashMap<>());
63 
64     private IInvocationContext mCurrentModuleContext = null;
65     private TestRunResult mCurrentTestRunResult = new TestRunResult();
66     /** True if the default initialized mCurrentTestRunResult has its original value. */
67     private boolean mDefaultRun = true;
68     /** Track whether or not a test run is currently in progress */
69     private boolean mRunInProgress = false;
70 
71     private Map<String, LogFile> mNonAssociatedLogFiles = new LinkedHashMap<>();
72 
73     // Tracks if mStatusCounts are accurate, or if they need to be recalculated
74     private AtomicBoolean mIsCountDirty = new AtomicBoolean(true);
75     // Tracks if the expected count is accurate, or if it needs to be recalculated.
76     private AtomicBoolean mIsExpectedCountDirty = new AtomicBoolean(true);
77     private int mExpectedCount = 0;
78 
79     // Represents the merged test results. This should not be accessed directly since it's only
80     // calculated when needed.
81     private final List<TestRunResult> mMergedTestRunResults = new ArrayList<>();
82     // Represents the number of tests in each TestStatus state of the merged test results. Indexed
83     // by TestStatus.ordinal()
84     private int[] mStatusCounts = new int[TestStatus.values().length];
85 
86     private MergeStrategy mStrategy = MergeStrategy.ONE_TESTCASE_PASS_IS_PASS;
87 
88     /** Sets the {@link MergeStrategy} to use when merging results. */
setMergeStrategy(MergeStrategy strategy)89     public void setMergeStrategy(MergeStrategy strategy) {
90         mStrategy = strategy;
91     }
92 
93     /**
94      * Return the primary build info that was reported via {@link
95      * #invocationStarted(IInvocationContext)}. Primary build is the build returned by the first
96      * build provider of the running configuration. Returns null if there is no context (no build to
97      * test case).
98      */
getPrimaryBuildInfo()99     public IBuildInfo getPrimaryBuildInfo() {
100         if (mContext == null) {
101             return null;
102         } else {
103             return mContext.getBuildInfos().get(0);
104         }
105     }
106 
107     /**
108      * Return the invocation context that was reported via {@link
109      * #invocationStarted(IInvocationContext)}
110      */
getInvocationContext()111     public IInvocationContext getInvocationContext() {
112         return mContext;
113     }
114 
115     /**
116      * Returns the build info.
117      *
118      * @deprecated rely on the {@link IBuildInfo} from {@link #getInvocationContext()}.
119      */
120     @Deprecated
getBuildInfo()121     public IBuildInfo getBuildInfo() {
122         return mBuildInfo;
123     }
124 
125     /**
126      * Set the build info. Should only be used for testing.
127      *
128      * @deprecated Not necessary for testing anymore.
129      */
130     @VisibleForTesting
131     @Deprecated
setBuildInfo(IBuildInfo buildInfo)132     public void setBuildInfo(IBuildInfo buildInfo) {
133         mBuildInfo = buildInfo;
134     }
135 
136     /** {@inheritDoc} */
137     @Override
invocationStarted(IInvocationContext context)138     public void invocationStarted(IInvocationContext context) {
139         mContext = context;
140         mBuildInfo = getPrimaryBuildInfo();
141     }
142 
143     /** {@inheritDoc} */
144     @Override
invocationEnded(long elapsedTime)145     public void invocationEnded(long elapsedTime) {
146         // ignore
147     }
148 
149     /** {@inheritDoc} */
150     @Override
invocationFailed(Throwable cause)151     public void invocationFailed(Throwable cause) {
152         // ignore
153     }
154 
155     @Override
testModuleStarted(IInvocationContext moduleContext)156     public void testModuleStarted(IInvocationContext moduleContext) {
157         mCurrentModuleContext = moduleContext;
158     }
159 
160     @Override
testModuleEnded()161     public void testModuleEnded() {
162         mCurrentModuleContext = null;
163     }
164 
165     /** {@inheritDoc} */
166     @Override
testRunStarted(String name, int numTests)167     public void testRunStarted(String name, int numTests) {
168         testRunStarted(name, numTests, 0);
169     }
170 
getNewRunResult()171     private TestRunResult getNewRunResult() {
172         TestRunResult result = new TestRunResult();
173         if (mDefaultRun) {
174             result = mCurrentTestRunResult;
175             mDefaultRun = false;
176         }
177         result.setAggregateMetrics(mIsAggregateMetrics);
178         return result;
179     }
180 
181     /** {@inheritDoc} */
182     @Override
testRunStarted(String name, int numTests, int attemptNumber)183     public void testRunStarted(String name, int numTests, int attemptNumber) {
184         testRunStarted(name, numTests, attemptNumber, System.currentTimeMillis());
185     }
186 
187     /** {@inheritDoc} */
188     @Override
testRunStarted(String name, int numTests, int attemptNumber, long startTime)189     public void testRunStarted(String name, int numTests, int attemptNumber, long startTime) {
190         setCountDirty();
191         // Only testRunStarted can affect the expected count.
192         mIsExpectedCountDirty.set(true);
193 
194         // Associate the run name with the current module context
195         if (mCurrentModuleContext != null) {
196             mModuleContextMap.put(name, mCurrentModuleContext);
197         }
198 
199         // Add the list of maps if the run doesn't exist
200         if (!mTestRunResultMap.containsKey(name)) {
201             mTestRunResultMap.put(name, new LinkedList<>());
202         }
203         List<TestRunResult> results = mTestRunResultMap.get(name);
204 
205         // Set the current test run result based on the attempt
206         if (attemptNumber < results.size()) {
207             if (results.get(attemptNumber) == null) {
208                 throw new RuntimeException(
209                         "Test run results should never be null in internal structure.");
210             }
211         } else if (attemptNumber == results.size()) {
212             // new run
213             TestRunResult result = getNewRunResult();
214             results.add(result);
215         } else {
216             int size = results.size();
217             for (int i = size; i < attemptNumber; i++) {
218                 TestRunResult result = getNewRunResult();
219                 result.testRunStarted(name, numTests, startTime);
220                 String errorMessage =
221                         String.format(
222                                 "Run attempt %s of %s did not exists, but got attempt %s."
223                                         + " This is a placeholder for the missing attempt.",
224                                 i, name, attemptNumber);
225                 result.testRunFailed(errorMessage);
226                 result.testRunEnded(0L, new HashMap<String, Metric>());
227                 results.add(result);
228             }
229             // New current run
230             TestRunResult newResult = getNewRunResult();
231             results.add(newResult);
232         }
233         mCurrentTestRunResult = results.get(attemptNumber);
234 
235         mCurrentTestRunResult.testRunStarted(name, numTests, startTime);
236         mRunInProgress = true;
237     }
238 
239     /** {@inheritDoc} */
240     @Override
testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics)241     public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
242         setCountDirty();
243         mCurrentTestRunResult.testRunEnded(elapsedTime, runMetrics);
244         mRunInProgress = false;
245     }
246 
247     /** {@inheritDoc} */
248     @Override
testRunFailed(String errorMessage)249     public void testRunFailed(String errorMessage) {
250         setCountDirty();
251         mCurrentTestRunResult.testRunFailed(errorMessage);
252     }
253 
254     /** {@inheritDoc} */
255     @Override
testRunFailed(FailureDescription failure)256     public void testRunFailed(FailureDescription failure) {
257         setCountDirty();
258         mCurrentTestRunResult.testRunFailed(failure);
259     }
260 
261     /** {@inheritDoc} */
262     @Override
testRunStopped(long elapsedTime)263     public void testRunStopped(long elapsedTime) {
264         setCountDirty();
265         mCurrentTestRunResult.testRunStopped(elapsedTime);
266     }
267 
268     /** {@inheritDoc} */
269     @Override
testStarted(TestDescription test)270     public void testStarted(TestDescription test) {
271         testStarted(test, System.currentTimeMillis());
272     }
273 
274     /** {@inheritDoc} */
275     @Override
testStarted(TestDescription test, long startTime)276     public void testStarted(TestDescription test, long startTime) {
277         setCountDirty();
278         mCurrentTestRunResult.testStarted(test, startTime);
279     }
280 
281     /** {@inheritDoc} */
282     @Override
testEnded(TestDescription test, HashMap<String, Metric> testMetrics)283     public void testEnded(TestDescription test, HashMap<String, Metric> testMetrics) {
284         testEnded(test, System.currentTimeMillis(), testMetrics);
285     }
286 
287     /** {@inheritDoc} */
288     @Override
testEnded(TestDescription test, long endTime, HashMap<String, Metric> testMetrics)289     public void testEnded(TestDescription test, long endTime, HashMap<String, Metric> testMetrics) {
290         setCountDirty();
291         mCurrentTestRunResult.testEnded(test, endTime, testMetrics);
292     }
293 
294     /** {@inheritDoc} */
295     @Override
testFailed(TestDescription test, String trace)296     public void testFailed(TestDescription test, String trace) {
297         setCountDirty();
298         mCurrentTestRunResult.testFailed(test, trace);
299     }
300 
301     @Override
testFailed(TestDescription test, FailureDescription failure)302     public void testFailed(TestDescription test, FailureDescription failure) {
303         setCountDirty();
304         mCurrentTestRunResult.testFailed(test, failure);
305     }
306 
307     @Override
testAssumptionFailure(TestDescription test, String trace)308     public void testAssumptionFailure(TestDescription test, String trace) {
309         setCountDirty();
310         mCurrentTestRunResult.testAssumptionFailure(test, trace);
311     }
312 
313     @Override
testIgnored(TestDescription test)314     public void testIgnored(TestDescription test) {
315         setCountDirty();
316         mCurrentTestRunResult.testIgnored(test);
317     }
318 
319     /** {@inheritDoc} */
320     @Override
logAssociation(String dataName, LogFile logFile)321     public void logAssociation(String dataName, LogFile logFile) {
322         if (mRunInProgress) {
323             mCurrentTestRunResult.testLogSaved(dataName, logFile);
324         } else {
325             mNonAssociatedLogFiles.put(dataName, logFile);
326         }
327     }
328 
329     /**
330      * Gets the results for the current test run.
331      *
332      * <p>Note the results may not be complete. It is recommended to test the value of {@link
333      * TestRunResult#isRunComplete()} and/or (@link TestRunResult#isRunFailure()} as appropriate
334      * before processing the results.
335      *
336      * @return the {@link TestRunResult} representing data collected during last test run
337      */
getCurrentRunResults()338     public TestRunResult getCurrentRunResults() {
339         return mCurrentTestRunResult;
340     }
341 
342     /** Returns the total number of complete tests for all runs. */
getNumTotalTests()343     public int getNumTotalTests() {
344         computeMergedResults();
345         int total = 0;
346         for (TestStatus s : TestStatus.values()) {
347             total += mStatusCounts[s.ordinal()];
348         }
349         return total;
350     }
351 
352     /**
353      * Returns the number of expected tests count. Could differ from {@link #getNumTotalTests()} if
354      * some tests did not run.
355      */
getExpectedTests()356     public synchronized int getExpectedTests() {
357         // If expected count is not dirty, no need to do anything
358         if (!mIsExpectedCountDirty.compareAndSet(true, false)) {
359             return mExpectedCount;
360         }
361 
362         computeMergedResults();
363         mExpectedCount = 0;
364         for (TestRunResult result : getMergedTestRunResults()) {
365             mExpectedCount += result.getExpectedTestCount();
366         }
367         return mExpectedCount;
368     }
369 
370     /** Returns the number of tests in given state for this run. */
getNumTestsInState(TestStatus status)371     public int getNumTestsInState(TestStatus status) {
372         computeMergedResults();
373         return mStatusCounts[status.ordinal()];
374     }
375 
376     /** Returns if the invocation had any failed or assumption failed tests. */
hasFailedTests()377     public boolean hasFailedTests() {
378         return getNumAllFailedTests() > 0;
379     }
380 
381     /** Returns the total number of test runs in a failure state */
getNumAllFailedTestRuns()382     public int getNumAllFailedTestRuns() {
383         int count = 0;
384         for (TestRunResult result : getMergedTestRunResults()) {
385             if (result.isRunFailure()) {
386                 count++;
387             }
388         }
389         return count;
390     }
391 
392     /**
393      * Returns the total number of tests in a failure state (only failed, assumption failures do not
394      * count toward it).
395      */
getNumAllFailedTests()396     public int getNumAllFailedTests() {
397         return getNumTestsInState(TestStatus.FAILURE);
398     }
399 
400     /**
401      * Return the merged collection of results for all runs across different attempts.
402      *
403      * <p>If there are multiple results, each test run is merged, with the latest test result
404      * overwriting test results of previous runs. Test runs are ordered by attempt number.
405      *
406      * <p>Metrics for the same attempt will be merged based on the preference set by {@code
407      * aggregate-metrics}. The final metrics will be the metrics of the last attempt.
408      */
getMergedTestRunResults()409     public List<TestRunResult> getMergedTestRunResults() {
410         computeMergedResults();
411         return new ArrayList<>(mMergedTestRunResults);
412     }
413 
414     /**
415      * Returns the results for all test runs.
416      *
417      * @deprecated Use {@link #getMergedTestRunResults()}
418      */
419     @Deprecated
getRunResults()420     public Collection<TestRunResult> getRunResults() {
421         return getMergedTestRunResults();
422     }
423 
424     /**
425      * Computes and stores the merged results and the total status counts since both operations are
426      * expensive.
427      */
computeMergedResults()428     private synchronized void computeMergedResults() {
429         // If not dirty, nothing to be done
430         if (!mIsCountDirty.compareAndSet(true, false)) {
431             return;
432         }
433 
434         mMergedTestRunResults.clear();
435         // Merge results
436         if (mTestRunResultMap.isEmpty() && mCurrentTestRunResult.isRunFailure()) {
437             // In case of early failure that is a bit untracked, still add it to the list to
438             // not loose it.
439             CLog.e(
440                     "Early failure resulting in no testRunStart. Results might be inconsistent:"
441                             + "\n%s",
442                     mCurrentTestRunResult.getRunFailureMessage());
443             mMergedTestRunResults.add(mCurrentTestRunResult);
444         } else {
445             for (Entry<String, List<TestRunResult>> results : mTestRunResultMap.entrySet()) {
446                 TestRunResult res = TestRunResult.merge(results.getValue(), mStrategy);
447                 if (res == null) {
448                     // Merge can return null in case of results being empty.
449                     CLog.w("No results for %s", results.getKey());
450                 } else {
451                     mMergedTestRunResults.add(res);
452                 }
453             }
454         }
455         // Reset counts
456         for (TestStatus s : TestStatus.values()) {
457             mStatusCounts[s.ordinal()] = 0;
458         }
459 
460         // Calculate results
461         for (TestRunResult result : mMergedTestRunResults) {
462             for (TestStatus s : TestStatus.values()) {
463                 mStatusCounts[s.ordinal()] += result.getNumTestsInState(s);
464             }
465         }
466     }
467 
468     /**
469      * Keep dirty count as AtomicBoolean to ensure when accessed from another thread the state is
470      * consistent.
471      */
setCountDirty()472     private void setCountDirty() {
473         mIsCountDirty.set(true);
474     }
475 
476     /**
477      * Return all the names for all the test runs.
478      *
479      * <p>These test runs may have run multiple times with different attempts.
480      */
getTestRunNames()481     public Collection<String> getTestRunNames() {
482         return new ArrayList<String>(mTestRunResultMap.keySet());
483     }
484 
485     /**
486      * Gets all the attempts for a {@link TestRunResult} of a given test run.
487      *
488      * @param testRunName The name given by {{@link #testRunStarted(String, int)}.
489      * @return All {@link TestRunResult} for a given test run, ordered by attempts.
490      */
getTestRunAttempts(String testRunName)491     public List<TestRunResult> getTestRunAttempts(String testRunName) {
492         return mTestRunResultMap.get(testRunName);
493     }
494 
495     /**
496      * Gets all the results for a given attempt.
497      *
498      * @param attempt The attempt we want results for.
499      * @return All {@link TestRunResult} for a given attempt.
500      */
getTestRunForAttempts(int attempt)501     public List<TestRunResult> getTestRunForAttempts(int attempt) {
502         List<TestRunResult> allResultForAttempts = new ArrayList<>();
503         for (Entry<String, List<TestRunResult>> runInfo : mTestRunResultMap.entrySet()) {
504             if (attempt < runInfo.getValue().size()) {
505                 TestRunResult attemptRes = runInfo.getValue().get(attempt);
506                 allResultForAttempts.add(attemptRes);
507             }
508         }
509         return allResultForAttempts;
510     }
511 
512     /**
513      * Returns whether a given test run name has any results.
514      *
515      * @param testRunName The name given by {{@link #testRunStarted(String, int)}.
516      */
hasTestRunResultsForName(String testRunName)517     public boolean hasTestRunResultsForName(String testRunName) {
518         return mTestRunResultMap.containsKey(testRunName);
519     }
520 
521     /**
522      * Returns the number of attempts for a given test run name.
523      *
524      * @param testRunName The name given by {{@link #testRunStarted(String, int)}.
525      */
getTestRunAttemptCount(String testRunName)526     public int getTestRunAttemptCount(String testRunName) {
527         List<TestRunResult> results = mTestRunResultMap.get(testRunName);
528         if (results == null) {
529             return 0;
530         }
531         return results.size();
532     }
533 
534     /**
535      * Return the {@link TestRunResult} for a single attempt.
536      *
537      * @param testRunName The name given by {{@link #testRunStarted(String, int)}.
538      * @param attempt The attempt id.
539      * @return The {@link TestRunResult} for the given name and attempt id or {@code null} if it
540      *     does not exist.
541      */
getTestRunAtAttempt(String testRunName, int attempt)542     public TestRunResult getTestRunAtAttempt(String testRunName, int attempt) {
543         List<TestRunResult> results = mTestRunResultMap.get(testRunName);
544         if (results == null || attempt < 0 || attempt >= results.size()) {
545             return null;
546         }
547 
548         return results.get(attempt);
549     }
550 
551     /**
552      * Returns the {@link IInvocationContext} of the module associated with the results.
553      *
554      * @param testRunName The name given by {{@link #testRunStarted(String, int)}.
555      * @return The {@link IInvocationContext} of the module for a given test run name {@code null}
556      *     if there are no results for that name.
557      */
getModuleContextForRunResult(String testRunName)558     public IInvocationContext getModuleContextForRunResult(String testRunName) {
559         return mModuleContextMap.get(testRunName);
560     }
561 
562     /** Returns a copy of the map containing all the logged file not associated with a test run. */
getNonAssociatedLogFiles()563     public Map<String, LogFile> getNonAssociatedLogFiles() {
564         return new LinkedHashMap<>(mNonAssociatedLogFiles);
565     }
566 
567     /**
568      * Allows to clear the results for a given run name. Should only be used in some cases like the
569      * aggregator of results.
570      */
clearResultsForName(String testRunName)571     protected final synchronized void clearResultsForName(String testRunName) {
572         setCountDirty();
573         mTestRunResultMap.remove(testRunName);
574     }
575 }
576