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