Home | History | Annotate | Download | only in result
      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 
     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 
     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 
    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 
    132         protected List<SubPredicate> getPredicate() {
    133             return mSubPredicates;
    134         }
    135 
    136         protected List<SubPredicate> getFilterPredicate() {
    137             return mFilterSubPredicates;
    138         }
    139 
    140         public boolean partialMatch(Predicate otherP) {
    141             return mSubPredicates.equals(otherP.getPredicate());
    142         }
    143 
    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
    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
    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
    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 
    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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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 
    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 
    313     boolean check(Relation relation, Noun noun) {
    314         return check(relation, noun, null);
    315     }
    316 
    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.getRunResults().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.getRunResults().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 
    413     void reset() {
    414         mCapturedBugreport = false;
    415     }
    416 
    417     /**
    418      * Convenience method to build a predicate from subpredicates
    419      */
    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")
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    551     public TestSummary getSummary() {
    552         return mListener.getSummary();
    553     }
    554 
    555     @Override
    556     public void testIgnored(TestDescription test) {
    557         // ignore
    558     }
    559 }
    560 
    561