1 /*
2  * Copyright (C) 2011 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.tradefed.device.DeviceNotAvailableException;
19 import com.android.tradefed.device.ITestDevice;
20 import com.android.tradefed.invoker.IInvocationContext;
21 import com.android.tradefed.log.LogUtil.CLog;
22 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
23 
24 import java.util.ArrayList;
25 import java.util.Collection;
26 import java.util.HashMap;
27 import java.util.LinkedList;
28 import java.util.List;
29 import java.util.ListIterator;
30 
31 /**
32  * A pass-through {@link ITestInvocationListener} that collects bugreports when configurable events
33  * occur and then calls {@link ITestInvocationListener#testLog} on its children after each
34  * bugreport is collected.
35  * <p />
36  * Behaviors: (FIXME: finish this)
37  * <ul>
38  *   <li>Capture after each if any testcases failed</li>
39  *   <li>Capture after each testcase</li>
40  *   <li>Capture after each failed testcase</li>
41  *   <li>Capture </li>
42  * </ul>
43  */
44 public class BugreportCollector implements ITestInvocationListener {
45     /** A predefined predicate which fires after each failed testcase */
46     public static final Predicate AFTER_FAILED_TESTCASES =
47             p(Relation.AFTER, Freq.EACH, Noun.FAILED_TESTCASE);
48     /** A predefined predicate which fires as the first invocation begins */
49     public static final Predicate AT_START =
50             p(Relation.AT_START_OF, Freq.EACH, Noun.INVOCATION);
51     // FIXME: add other useful predefined predicates
52 
53     public static interface SubPredicate {}
54 
55     public static enum Noun implements SubPredicate {
56         // FIXME: find a reasonable way to detect runtime restarts
57         // FIXME: try to make sure there aren't multiple ways to specify a single condition
58         TESTCASE,
59         FAILED_TESTCASE,
60         TESTRUN,
61         FAILED_TESTRUN,
62         INVOCATION,
63         FAILED_INVOCATION;
64     }
65 
66     public static enum Relation implements SubPredicate {
67         AFTER,
68         AT_START_OF;
69     }
70 
71     public static enum Freq implements SubPredicate {
72         EACH,
73         FIRST;
74     }
75 
76     public static enum Filter implements SubPredicate {
77         WITH_FAILING,
78         WITH_PASSING,
79         WITH_ANY;
80     }
81 
82     /**
83      * A full predicate describing when to capture a bugreport.  Has the following required elements
84      * and [optional elements]:
85      * RelationP TimingP Noun [FilterP Noun]
86      */
87     public static class Predicate {
88         List<SubPredicate> mSubPredicates = new ArrayList<SubPredicate>(3);
89         List<SubPredicate> mFilterSubPredicates = null;
90 
Predicate(Relation rp, Freq fp, Noun n)91         public Predicate(Relation rp, Freq fp, Noun n) throws IllegalArgumentException {
92             assertValidPredicate(rp, fp, n);
93 
94             mSubPredicates.add(rp);
95             mSubPredicates.add(fp);
96             mSubPredicates.add(n);
97         }
98 
Predicate(Relation rp, Freq fp, Noun fpN, Filter filterP, Noun filterPN)99         public Predicate(Relation rp, Freq fp, Noun fpN, Filter filterP, Noun filterPN)
100                 throws IllegalArgumentException {
101             mSubPredicates.add(rp);
102             mSubPredicates.add(fp);
103             mSubPredicates.add(fpN);
104             mFilterSubPredicates = new ArrayList<SubPredicate>(2);
105             mFilterSubPredicates.add(filterP);
106             mFilterSubPredicates.add(filterPN);
107         }
108 
assertValidPredicate(Relation rp, Freq fp, Noun n)109         public static void assertValidPredicate(Relation rp, Freq fp, Noun n)
110                 throws IllegalArgumentException {
111             if (rp == Relation.AT_START_OF) {
112                 // It doesn't make sense to say AT_START_OF FAILED_(x) since we'll only know that it
113                 // failed in the AFTER case.
114                 if (n == Noun.FAILED_TESTCASE || n == Noun.FAILED_TESTRUN ||
115                         n == Noun.FAILED_INVOCATION) {
116                     throw new IllegalArgumentException(String.format(
117                             "Illegal predicate: %s %s isn't valid since we can only check " +
118                             "failure on the AFTER event.", fp, n));
119                 }
120             }
121             if (n == Noun.INVOCATION || n == Noun.FAILED_INVOCATION) {
122                 // Block "FIRST INVOCATION" for disambiguation, since there will only ever be one
123                 // invocation
124                 if (fp == Freq.FIRST) {
125                     throw new IllegalArgumentException(String.format(
126                             "Illegal predicate: Since there is only one invocation, please use " +
127                             "%s %s rather than %s %s for disambiguation.", Freq.EACH, n, fp, n));
128                 }
129             }
130         }
131 
getPredicate()132         protected List<SubPredicate> getPredicate() {
133             return mSubPredicates;
134         }
135 
getFilterPredicate()136         protected List<SubPredicate> getFilterPredicate() {
137             return mFilterSubPredicates;
138         }
139 
partialMatch(Predicate otherP)140         public boolean partialMatch(Predicate otherP) {
141             return mSubPredicates.equals(otherP.getPredicate());
142         }
143 
fullMatch(Predicate otherP)144         public boolean fullMatch(Predicate otherP) {
145             if (partialMatch(otherP)) {
146                 if (mFilterSubPredicates == null) {
147                     return otherP.getFilterPredicate() == null;
148                 } else {
149                     return mFilterSubPredicates.equals(otherP.getFilterPredicate());
150                 }
151             }
152             return false;
153         }
154 
155         @Override
toString()156         public String toString() {
157             StringBuilder sb = new StringBuilder();
158             ListIterator<SubPredicate> iter = mSubPredicates.listIterator();
159             while (iter.hasNext()) {
160                 SubPredicate p = iter.next();
161                 sb.append(p.toString());
162                 if (iter.hasNext()) {
163                     sb.append("_");
164                 }
165             }
166 
167             return sb.toString();
168         }
169 
170         @Override
equals(Object other)171         public boolean equals(Object other) {
172             if (other instanceof Predicate) {
173                 Predicate otherP = (Predicate) other;
174                 return fullMatch(otherP);
175             } else {
176                 return false;
177             }
178         }
179 
180         @Override
hashCode()181         public int hashCode() {
182             return mSubPredicates.hashCode();
183         }
184     }
185 
186     // Now that the Predicate framework is done, actually start on the BugreportCollector class
187     /**
188      * We keep an internal {@link CollectingTestListener} instead of subclassing to make sure that
189      * we @Override all of the applicable interface methods (instead of having them fall through to
190      * implementations in {@link CollectingTestListener}).
191      */
192     private CollectingTestListener mCollector = new CollectingTestListener();
193     private ITestInvocationListener mListener;
194     private ITestDevice mTestDevice;
195     private List<Predicate> mPredicates = new LinkedList<Predicate>();
196     @SuppressWarnings("unused")
197     private boolean mAsynchronous = false;
198     @SuppressWarnings("unused")
199     private boolean mCapturedBugreport = false;
200 
201     /**
202      * How long to potentially wait for the device to be Online before we try to capture a
203      * bugreport.  If negative, no check will be performed
204      */
205     private int mDeviceWaitTimeSecs = 40; // default to 40s
206     private String mDescriptiveName = null;
207     // FIXME: Add support for minimum wait time between successive bugreports
208     // FIXME: get rid of reset() method
209 
210     // Caching for counts that CollectingTestListener doesn't store
211     private int mNumFailedRuns = 0;
212 
BugreportCollector(ITestInvocationListener listener, ITestDevice testDevice)213     public BugreportCollector(ITestInvocationListener listener, ITestDevice testDevice) {
214         if (listener == null) {
215             throw new NullPointerException("listener must be non-null.");
216         }
217         if (testDevice == null) {
218             throw new NullPointerException("device must be non-null.");
219         }
220         mListener = listener;
221         mTestDevice = testDevice;
222     }
223 
addPredicate(Predicate p)224     public void addPredicate(Predicate p) {
225         mPredicates.add(p);
226     }
227 
228     /**
229      * Set the time (in seconds) to wait for the device to be Online before we try to capture a
230      * bugreport.  If negative, no check will be performed.  Any {@link DeviceNotAvailableException}
231      * encountered during this check will be logged and ignored.
232      */
setDeviceWaitTime(int waitTime)233     public void setDeviceWaitTime(int waitTime) {
234         mDeviceWaitTimeSecs = waitTime;
235     }
236 
237     /**
238      * Block until the collector is not collecting any bugreports.  If the collector isn't actively
239      * collecting a bugreport, return immediately
240      */
blockUntilIdle()241     public void blockUntilIdle() {
242         // FIXME
243         return;
244     }
245 
246     /**
247      * Set whether bugreport collection should collect the bugreport in a different thread
248      * ({@code asynchronous = true}), or block the caller until the bugreport is captured
249      * ({@code asynchronous = false}).
250      */
setAsynchronous(boolean asynchronous)251     public void setAsynchronous(boolean asynchronous) {
252         // FIXME do something
253         mAsynchronous = asynchronous;
254     }
255 
256     /**
257      * Set the descriptive name to use when recording bugreports.  If {@code null},
258      * {@code BugreportCollector} will fall back to the default behavior of serializing the name of
259      * the event that caused the bugreport to be collected.
260      */
setDescriptiveName(String name)261     public void setDescriptiveName(String name) {
262         mDescriptiveName = name;
263     }
264 
265     /**
266      * Actually capture a bugreport and pass it to our child listener.
267      */
grabBugreport(String logDesc)268     void grabBugreport(String logDesc) {
269         CLog.v("About to grab bugreport for %s; custom name is %s.", logDesc, mDescriptiveName);
270         if (mDescriptiveName != null) {
271             logDesc = mDescriptiveName;
272         }
273         String logName = String.format("bug-%s.%d", logDesc, System.currentTimeMillis());
274         CLog.v("Log name is %s", logName);
275         if (mDeviceWaitTimeSecs >= 0) {
276             try {
277                 mTestDevice.waitForDeviceOnline((long)mDeviceWaitTimeSecs * 1000);
278             } catch (DeviceNotAvailableException e) {
279                 // Because we want to be as transparent as possible, we don't let this exception
280                 // bubble up; if a problem happens that actually affects the test, the test will
281                 // run into it.  If the test doesn't care (or, for instance, expects the device to
282                 // be unavailable for a period of time), then we don't care.
283                 CLog.e("Caught DeviceNotAvailableException while trying to capture bugreport");
284                 CLog.e(e);
285             }
286         }
287         try (InputStreamSource bugreport = mTestDevice.getBugreport()) {
288             mListener.testLog(logName, LogDataType.BUGREPORT, bugreport);
289         }
290     }
291 
getPredicate(Predicate predicate)292     Predicate getPredicate(Predicate predicate) {
293         for (Predicate p : mPredicates) {
294             if (p.partialMatch(predicate)) {
295                 return p;
296             }
297         }
298         return null;
299     }
300 
search(Relation relation, Collection<Freq> freqs, Noun noun)301     Predicate search(Relation relation, Collection<Freq> freqs, Noun noun) {
302         for (Predicate pred : mPredicates) {
303             for (Freq freq : freqs) {
304                 CLog.v("Search checking predicate %s", p(relation, freq, noun));
305                 if (pred.partialMatch(p(relation, freq, noun))) {
306                     return pred;
307                 }
308             }
309         }
310         return null;
311     }
312 
check(Relation relation, Noun noun)313     boolean check(Relation relation, Noun noun) {
314         return check(relation, noun, null);
315     }
316 
check(Relation relation, Noun noun, TestDescription test)317     boolean check(Relation relation, Noun noun, TestDescription test) {
318         // Expect to get something like "AFTER", "TESTCASE"
319 
320         // All freqs that could match _right now_.  Should be added in decreasing order of
321         // specificity (so the most specific option has the ability to match first)
322         List<Freq> applicableFreqs = new ArrayList<Freq>(2 /* total # freqs in enum */);
323         applicableFreqs.add(Freq.EACH);
324 
325         TestRunResult curResult = mCollector.getCurrentRunResults();
326         switch (relation) {
327             case AFTER:
328                 switch (noun) {
329                     case TESTCASE:
330                         // FIXME: grab the name of the testcase that just finished
331                         if (curResult.getNumTests() == 1) {
332                             applicableFreqs.add(Freq.FIRST);
333                         }
334                         break;
335 
336                     case FAILED_TESTCASE:
337                         if (curResult.getNumAllFailedTests() == 1) {
338                             applicableFreqs.add(Freq.FIRST);
339                         }
340                         break;
341 
342                     case TESTRUN:
343                         if (mCollector.getMergedTestRunResults().size() == 1) {
344                             applicableFreqs.add(Freq.FIRST);
345                         }
346                         break;
347 
348                     case FAILED_TESTRUN:
349                         if (mNumFailedRuns == 1) {
350                             applicableFreqs.add(Freq.FIRST);
351                         }
352                         break;
353                     default:
354                         break;
355                 }
356                 break; // case AFTER
357 
358             case AT_START_OF:
359                 switch (noun) {
360                     case TESTCASE:
361                         if (curResult.getNumTests() == 1) {
362                             applicableFreqs.add(Freq.FIRST);
363                         }
364                         break;
365 
366                     case TESTRUN:
367                         if (mCollector.getMergedTestRunResults().size() == 1) {
368                             applicableFreqs.add(Freq.FIRST);
369                         }
370                         break;
371                     default:
372                         break;
373                 }
374                 break; // case AT_START_OF
375         }
376 
377         Predicate storedP = search(relation, applicableFreqs, noun);
378         if (storedP != null) {
379             CLog.v("Found storedP %s for relation %s and noun %s", storedP, relation, noun);
380             String desc = storedP.toString();
381             // Try to generate a useful description
382             if (test != null) {
383                 // We use "__" instead of "#" here because of ambiguity in automatically making
384                 // HTML links containing the "#" character -- it could just as easily be a real hash
385                 // character as an HTML fragment specification.
386                 final String testName = String.format("%s__%s", test.getClassName(),
387                         test.getTestName());
388                 switch (noun) {
389                     case TESTCASE:
390                         // bug-FooBarTest#testMethodName
391                         desc = testName;
392                         break;
393 
394                     case FAILED_TESTCASE:
395                         // bug-FAILED-FooBarTest#testMethodName
396                         desc = String.format("FAILED-%s", testName);
397                         break;
398 
399                     default:
400                         break;
401                 }
402             }
403 
404             CLog.v("Grabbing bugreport.");
405             grabBugreport(desc);
406             mCapturedBugreport = true;
407             return true;
408         } else {
409             return false;
410         }
411     }
412 
reset()413     void reset() {
414         mCapturedBugreport = false;
415     }
416 
417     /**
418      * Convenience method to build a predicate from subpredicates
419      */
p(Relation rp, Freq fp, Noun n)420     private static Predicate p(Relation rp, Freq fp, Noun n) throws IllegalArgumentException {
421         return new Predicate(rp, fp, n);
422     }
423 
424     /**
425      * Convenience method to build a predicate from subpredicates
426      */
427     @SuppressWarnings("unused")
p(Relation rp, Freq fp, Noun fpN, Filter filterP, Noun filterPN)428     private static Predicate p(Relation rp, Freq fp, Noun fpN, Filter filterP, Noun filterPN)
429             throws IllegalArgumentException {
430         return new Predicate(rp, fp, fpN, filterP, filterPN);
431     }
432 
433 
434     // Methods from the {@link ITestInvocationListener} interface
435     @Override
testEnded(TestDescription test, HashMap<String, Metric> testMetrics)436     public void testEnded(TestDescription test, HashMap<String, Metric> testMetrics) {
437         mListener.testEnded(test, testMetrics);
438         mCollector.testEnded(test, testMetrics);
439         check(Relation.AFTER, Noun.TESTCASE, test);
440         reset();
441     }
442 
443     /** {@inheritDoc} */
444     @Override
testFailed(TestDescription test, String trace)445     public void testFailed(TestDescription test, String trace) {
446         mListener.testFailed(test, trace);
447         mCollector.testFailed(test, trace);
448         check(Relation.AFTER, Noun.FAILED_TESTCASE, test);
449         reset();
450     }
451 
452     /** {@inheritDoc} */
453     @Override
testAssumptionFailure(TestDescription test, String trace)454     public void testAssumptionFailure(TestDescription test, String trace) {
455         mListener.testAssumptionFailure(test, trace);
456         mCollector.testAssumptionFailure(test, trace);
457         check(Relation.AFTER, Noun.FAILED_TESTCASE, test);
458         reset();
459     }
460 
461     /** {@inheritDoc} */
462     @Override
testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics)463     public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
464         mListener.testRunEnded(elapsedTime, runMetrics);
465         mCollector.testRunEnded(elapsedTime, runMetrics);
466         check(Relation.AFTER, Noun.TESTRUN);
467     }
468 
469     /**
470      * {@inheritDoc}
471      */
472     @Override
testRunFailed(String errorMessage)473     public void testRunFailed(String errorMessage) {
474         mListener.testRunFailed(errorMessage);
475         mCollector.testRunFailed(errorMessage);
476         check(Relation.AFTER, Noun.FAILED_TESTRUN);
477     }
478 
479     /**
480      * {@inheritDoc}
481      */
482     @Override
testRunStarted(String runName, int testCount)483     public void testRunStarted(String runName, int testCount) {
484         mListener.testRunStarted(runName, testCount);
485         mCollector.testRunStarted(runName, testCount);
486         check(Relation.AT_START_OF, Noun.TESTRUN);
487     }
488 
489     /**
490      * {@inheritDoc}
491      */
492     @Override
testRunStopped(long elapsedTime)493     public void testRunStopped(long elapsedTime) {
494         mListener.testRunStopped(elapsedTime);
495         mCollector.testRunStopped(elapsedTime);
496         // FIXME: figure out how to expose this
497     }
498 
499     /** {@inheritDoc} */
500     @Override
testStarted(TestDescription test)501     public void testStarted(TestDescription test) {
502         mListener.testStarted(test);
503         mCollector.testStarted(test);
504         check(Relation.AT_START_OF, Noun.TESTCASE, test);
505     }
506 
507     // Methods from the {@link ITestInvocationListener} interface
508     /**
509      * {@inheritDoc}
510      */
511     @Override
invocationStarted(IInvocationContext context)512     public void invocationStarted(IInvocationContext context) {
513         mListener.invocationStarted(context);
514         mCollector.invocationStarted(context);
515         check(Relation.AT_START_OF, Noun.INVOCATION);
516     }
517 
518     /**
519      * {@inheritDoc}
520      */
521     @Override
testLog(String dataName, LogDataType dataType, InputStreamSource dataStream)522     public void testLog(String dataName, LogDataType dataType, InputStreamSource dataStream) {
523         mListener.testLog(dataName, dataType, dataStream);
524         mCollector.testLog(dataName, dataType, dataStream);
525     }
526 
527     /**
528      * {@inheritDoc}
529      */
530     @Override
invocationEnded(long elapsedTime)531     public void invocationEnded(long elapsedTime) {
532         mListener.invocationEnded(elapsedTime);
533         mCollector.invocationEnded(elapsedTime);
534         check(Relation.AFTER, Noun.INVOCATION);
535     }
536 
537     /**
538      * {@inheritDoc}
539      */
540     @Override
invocationFailed(Throwable cause)541     public void invocationFailed(Throwable cause) {
542         mListener.invocationFailed(cause);
543         mCollector.invocationFailed(cause);
544         check(Relation.AFTER, Noun.FAILED_INVOCATION);
545     }
546 
547     /**
548      * {@inheritDoc}
549      */
550     @Override
getSummary()551     public TestSummary getSummary() {
552         return mListener.getSummary();
553     }
554 
555     @Override
testIgnored(TestDescription test)556     public void testIgnored(TestDescription test) {
557         // ignore
558     }
559 }
560 
561