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.invoker; 17 18 import com.android.ddmlib.IDevice; 19 import com.android.ddmlib.Log.LogLevel; 20 import com.android.tradefed.build.BuildRetrievalError; 21 import com.android.tradefed.build.IBuildInfo; 22 import com.android.tradefed.build.IBuildProvider; 23 import com.android.tradefed.build.IDeviceBuildProvider; 24 import com.android.tradefed.command.CommandRunner.ExitCode; 25 import com.android.tradefed.config.GlobalConfiguration; 26 import com.android.tradefed.config.IConfiguration; 27 import com.android.tradefed.config.IDeviceConfiguration; 28 import com.android.tradefed.device.DeviceNotAvailableException; 29 import com.android.tradefed.device.DeviceUnresponsiveException; 30 import com.android.tradefed.device.ITestDevice; 31 import com.android.tradefed.device.ITestDevice.RecoveryMode; 32 import com.android.tradefed.device.StubDevice; 33 import com.android.tradefed.device.TestDeviceState; 34 import com.android.tradefed.invoker.shard.IShardHelper; 35 import com.android.tradefed.invoker.shard.ShardBuildCloner; 36 import com.android.tradefed.log.ILeveledLogOutput; 37 import com.android.tradefed.log.ILogRegistry; 38 import com.android.tradefed.log.LogRegistry; 39 import com.android.tradefed.log.LogUtil.CLog; 40 import com.android.tradefed.result.AggregatingProfilerListener; 41 import com.android.tradefed.result.ITestInvocationListener; 42 import com.android.tradefed.result.ITestLoggerReceiver; 43 import com.android.tradefed.result.InputStreamSource; 44 import com.android.tradefed.result.LogDataType; 45 import com.android.tradefed.result.LogSaverResultForwarder; 46 import com.android.tradefed.result.ResultForwarder; 47 import com.android.tradefed.suite.checker.ISystemStatusCheckerReceiver; 48 import com.android.tradefed.targetprep.BuildError; 49 import com.android.tradefed.targetprep.DeviceFailedToBootError; 50 import com.android.tradefed.targetprep.IHostCleaner; 51 import com.android.tradefed.targetprep.ITargetCleaner; 52 import com.android.tradefed.targetprep.ITargetPreparer; 53 import com.android.tradefed.targetprep.TargetSetupError; 54 import com.android.tradefed.targetprep.multi.IMultiTargetPreparer; 55 import com.android.tradefed.testtype.IBuildReceiver; 56 import com.android.tradefed.testtype.IDeviceTest; 57 import com.android.tradefed.testtype.IInvocationContextReceiver; 58 import com.android.tradefed.testtype.IMultiDeviceTest; 59 import com.android.tradefed.testtype.IRemoteTest; 60 import com.android.tradefed.testtype.IResumableTest; 61 import com.android.tradefed.testtype.IRetriableTest; 62 import com.android.tradefed.util.IRunUtil; 63 import com.android.tradefed.util.RunInterruptedException; 64 import com.android.tradefed.util.RunUtil; 65 import com.android.tradefed.util.StreamUtil; 66 67 import com.google.common.annotations.VisibleForTesting; 68 69 import java.io.IOException; 70 import java.util.ArrayList; 71 import java.util.Arrays; 72 import java.util.List; 73 import java.util.ListIterator; 74 import java.util.Map.Entry; 75 import java.util.concurrent.ExecutionException; 76 import java.util.concurrent.TimeUnit; 77 78 /** 79 * Default implementation of {@link ITestInvocation}. 80 * <p/> 81 * Loads major objects based on {@link IConfiguration} 82 * - retrieves build 83 * - prepares target 84 * - runs tests 85 * - reports results 86 */ 87 public class TestInvocation implements ITestInvocation { 88 89 /** 90 * Format of the key in {@link IInvocationContext} to log the battery level for each step of the 91 * invocation. (Setup, test, tear down). 92 */ 93 private static final String BATTERY_ATTRIBUTE_FORMAT_KEY = "%s-battery-%s"; 94 95 static final String TRADEFED_LOG_NAME = "host_log"; 96 static final String DEVICE_LOG_NAME_PREFIX = "device_logcat_"; 97 static final String EMULATOR_LOG_NAME_PREFIX = "emulator_log_"; 98 static final String BUILD_ERROR_BUGREPORT_NAME = "build_error_bugreport"; 99 static final String DEVICE_UNRESPONSIVE_BUGREPORT_NAME = "device_unresponsive_bugreport"; 100 static final String INVOCATION_ENDED_BUGREPORT_NAME = "invocation_ended_bugreport"; 101 static final String TARGET_SETUP_ERROR_BUGREPORT_NAME = "target_setup_error_bugreport"; 102 static final String BATT_TAG = "[battery level]"; 103 104 public enum Stage { 105 ERROR("error"), 106 SETUP("setup"), 107 TEST("test"), 108 TEARDOWN("teardown"); 109 110 private final String mName; 111 112 Stage(String name) { 113 mName = name; 114 } 115 116 public String getName() { 117 return mName; 118 } 119 } 120 121 private String mStatus = "(not invoked)"; 122 private boolean mStopRequested = false; 123 124 /** 125 * A {@link ResultForwarder} for forwarding resumed invocations. 126 * <p/> 127 * It filters the invocationStarted event for the resumed invocation, and sums the invocation 128 * elapsed time 129 */ 130 private static class ResumeResultForwarder extends ResultForwarder { 131 132 long mCurrentElapsedTime; 133 134 /** 135 * @param listeners 136 */ 137 public ResumeResultForwarder(List<ITestInvocationListener> listeners, 138 long currentElapsedTime) { 139 super(listeners); 140 mCurrentElapsedTime = currentElapsedTime; 141 } 142 143 @Override 144 public void invocationStarted(IInvocationContext context) { 145 // ignore 146 } 147 148 @Override 149 public void invocationEnded(long newElapsedTime) { 150 super.invocationEnded(mCurrentElapsedTime + newElapsedTime); 151 } 152 } 153 154 /** 155 * Attempt to shard the configuration into sub-configurations, to be re-scheduled to run on 156 * multiple resources in parallel. 157 * 158 * <p>If a shard count is greater than 1, it will simply create configs for each shard by 159 * setting shard indices and reschedule them. If a shard count is not set,it would fallback to 160 * {@link IShardHelper#shardConfig}. 161 * 162 * @param config the current {@link IConfiguration}. 163 * @param context the {@link IInvocationContext} holding the info of the tests. 164 * @param rescheduler the {@link IRescheduler} 165 * @return true if test was sharded. Otherwise return <code>false</code> 166 */ 167 private boolean shardConfig( 168 IConfiguration config, IInvocationContext context, IRescheduler rescheduler) { 169 mStatus = "sharding"; 170 return createShardHelper().shardConfig(config, context, rescheduler); 171 } 172 173 /** Create an return the {@link IShardHelper} to be used. */ 174 @VisibleForTesting 175 protected IShardHelper createShardHelper() { 176 return GlobalConfiguration.getInstance().getShardingStrategy(); 177 } 178 179 /** 180 * Update the {@link IBuildInfo} with additional info from the {@link IConfiguration}. 181 * 182 * @param info the {@link IBuildInfo} 183 * @param config the {@link IConfiguration} 184 */ 185 private void updateBuild(IBuildInfo info, IConfiguration config) { 186 if (config.getCommandLine() != null) { 187 // TODO: obfuscate the password if any. 188 info.addBuildAttribute("command_line_args", config.getCommandLine()); 189 } 190 if (config.getCommandOptions().getShardCount() != null) { 191 info.addBuildAttribute("shard_count", 192 config.getCommandOptions().getShardCount().toString()); 193 } 194 if (config.getCommandOptions().getShardIndex() != null) { 195 info.addBuildAttribute("shard_index", 196 config.getCommandOptions().getShardIndex().toString()); 197 } 198 // TODO: update all the configs to only use test-tag from CommandOption and not build 199 // providers. 200 // When CommandOption is set, it overrides any test-tag from build_providers 201 if (!"stub".equals(config.getCommandOptions().getTestTag())) { 202 info.setTestTag(getTestTag(config)); 203 } else if (info.getTestTag() == null || info.getTestTag().isEmpty()) { 204 // We ensure that that a default test-tag is always available. 205 info.setTestTag("stub"); 206 } else { 207 CLog.w("Using the test-tag from the build_provider. Consider updating your config to" 208 + " have no alias/namespace in front of test-tag."); 209 } 210 } 211 212 /** 213 * Update the {@link IInvocationContext} with additional info from the {@link IConfiguration}. 214 * 215 * @param context the {@link IInvocationContext} 216 * @param config the {@link IConfiguration} 217 */ 218 private void updateInvocationContext(IInvocationContext context, IConfiguration config) { 219 // TODO: Once reporting on context is done, only set context attributes 220 if (config.getCommandLine() != null) { 221 // TODO: obfuscate the password if any. 222 context.addInvocationAttribute("command_line_args", config.getCommandLine()); 223 } 224 if (config.getCommandOptions().getShardCount() != null) { 225 context.addInvocationAttribute("shard_count", 226 config.getCommandOptions().getShardCount().toString()); 227 } 228 if (config.getCommandOptions().getShardIndex() != null) { 229 context.addInvocationAttribute("shard_index", 230 config.getCommandOptions().getShardIndex().toString()); 231 } 232 context.setTestTag(getTestTag(config)); 233 } 234 235 /** 236 * Helper to create the test tag from the configuration. 237 */ 238 private String getTestTag(IConfiguration config) { 239 String testTag = config.getCommandOptions().getTestTag(); 240 if (config.getCommandOptions().getTestTagSuffix() != null) { 241 testTag = String.format("%s-%s", testTag, 242 config.getCommandOptions().getTestTagSuffix()); 243 } 244 return testTag; 245 } 246 247 /** 248 * Display a log message informing the user of a invocation being started. 249 * 250 * @param context the {@link IInvocationContext} 251 * @param config the {@link IConfiguration} 252 */ 253 private void logStartInvocation(IInvocationContext context, IConfiguration config) { 254 String shardSuffix = ""; 255 if (config.getCommandOptions().getShardIndex() != null) { 256 shardSuffix = 257 String.format( 258 " (shard %d of %d)", 259 config.getCommandOptions().getShardIndex() + 1, 260 config.getCommandOptions().getShardCount()); 261 } 262 StringBuilder buildInfos = new StringBuilder(); 263 StringBuilder msg = new StringBuilder("Starting invocation for '"); 264 msg.append(context.getTestTag()); 265 msg.append("' with "); 266 for (Entry<ITestDevice, IBuildInfo> entry : context.getDeviceBuildMap().entrySet()) { 267 msg.append("'[ "); 268 msg.append(entry.getValue().toString()); 269 buildInfos.append(entry.getValue().toString()); 270 msg.append(" on device '"); 271 msg.append(entry.getKey().getSerialNumber()); 272 msg.append("'] "); 273 } 274 msg.append(shardSuffix); 275 CLog.logAndDisplay(LogLevel.INFO, msg.toString()); 276 mStatus = String.format("running %s on build(s) '%s'", context.getTestTag(), 277 buildInfos.toString()) + shardSuffix; 278 } 279 280 /** 281 * Performs the invocation 282 * 283 * @param config the {@link IConfiguration} 284 * @param context the {@link IInvocationContext} to use. 285 */ 286 private void performInvocation(IConfiguration config, IInvocationContext context, 287 IRescheduler rescheduler, ITestInvocationListener listener) throws Throwable { 288 289 boolean resumed = false; 290 String bugreportName = null; 291 long startTime = System.currentTimeMillis(); 292 long elapsedTime = -1; 293 Throwable exception = null; 294 Throwable tearDownException = null; 295 ITestDevice badDevice = null; 296 297 startInvocation(config, context, listener); 298 try { 299 logDeviceBatteryLevel(context, "initial"); 300 prepareAndRun(config, context, listener); 301 } catch (BuildError e) { 302 exception = e; 303 CLog.w("Build failed on device '%s'. Reason: %s", e.getDeviceDescriptor(), 304 e.toString()); 305 bugreportName = BUILD_ERROR_BUGREPORT_NAME; 306 badDevice = context.getDeviceBySerial(e.getDeviceDescriptor().getSerial()); 307 if (e instanceof DeviceFailedToBootError) { 308 if (badDevice == null) { 309 context.setRecoveryModeForAllDevices(RecoveryMode.NONE); 310 } else { 311 badDevice.setRecoveryMode(RecoveryMode.NONE); 312 } 313 } 314 reportFailure(e, listener, config, context, rescheduler); 315 } catch (TargetSetupError e) { 316 exception = e; 317 CLog.e("Caught exception while running invocation"); 318 CLog.e(e); 319 bugreportName = TARGET_SETUP_ERROR_BUGREPORT_NAME; 320 badDevice = context.getDeviceBySerial(e.getDeviceDescriptor().getSerial()); 321 reportFailure(e, listener, config, context, rescheduler); 322 } catch (DeviceNotAvailableException e) { 323 exception = e; 324 // log a warning here so its captured before reportLogs is called 325 CLog.w("Invocation did not complete due to device %s becoming not available. " + 326 "Reason: %s", e.getSerial(), e.getMessage()); 327 badDevice = context.getDeviceBySerial(e.getSerial()); 328 if ((e instanceof DeviceUnresponsiveException) && badDevice != null 329 && TestDeviceState.ONLINE.equals(badDevice.getDeviceState())) { 330 // under certain cases it might still be possible to grab a bugreport 331 bugreportName = DEVICE_UNRESPONSIVE_BUGREPORT_NAME; 332 } 333 resumed = resume(config, context, rescheduler, System.currentTimeMillis() - startTime); 334 if (!resumed) { 335 reportFailure(e, listener, config, context, rescheduler); 336 } else { 337 CLog.i("Rescheduled failed invocation for resume"); 338 } 339 // Upon reaching here after an exception, it is safe to assume that recovery 340 // has already been attempted so we disable it to avoid re-entry during clean up. 341 if (badDevice != null) { 342 badDevice.setRecoveryMode(RecoveryMode.NONE); 343 } 344 throw e; 345 } catch (RunInterruptedException e) { 346 CLog.w("Invocation interrupted"); 347 reportFailure(e, listener, config, context, rescheduler); 348 } catch (AssertionError e) { 349 exception = e; 350 CLog.e("Caught AssertionError while running invocation: %s", e.toString()); 351 CLog.e(e); 352 reportFailure(e, listener, config, context, rescheduler); 353 } catch (Throwable t) { 354 exception = t; 355 // log a warning here so its captured before reportLogs is called 356 CLog.e("Unexpected exception when running invocation: %s", t.toString()); 357 CLog.e(t); 358 reportFailure(t, listener, config, context, rescheduler); 359 throw t; 360 } finally { 361 for (ITestDevice device : context.getDevices()) { 362 reportLogs(device, listener, Stage.TEST); 363 } 364 getRunUtil().allowInterrupt(false); 365 if (config.getCommandOptions().takeBugreportOnInvocationEnded() || 366 config.getCommandOptions().takeBugreportzOnInvocationEnded()) { 367 if (bugreportName != null) { 368 CLog.i("Bugreport to be taken for failure instead of invocation ended."); 369 } else { 370 bugreportName = INVOCATION_ENDED_BUGREPORT_NAME; 371 } 372 } 373 if (bugreportName != null) { 374 if (badDevice == null) { 375 for (ITestDevice device : context.getDevices()) { 376 takeBugreport(device, listener, bugreportName, 377 config.getCommandOptions().takeBugreportzOnInvocationEnded()); 378 } 379 } else { 380 // If we have identified a faulty device only take the bugreport on it. 381 takeBugreport(badDevice, listener, bugreportName, 382 config.getCommandOptions().takeBugreportzOnInvocationEnded()); 383 } 384 } 385 mStatus = "tearing down"; 386 try { 387 doTeardown(config, context, exception); 388 } catch (Throwable e) { 389 tearDownException = e; 390 CLog.e("Exception when tearing down invocation: %s", tearDownException.toString()); 391 CLog.e(tearDownException); 392 if (exception == null) { 393 // only report when the exception is new during tear down 394 reportFailure(tearDownException, listener, config, context, rescheduler); 395 } 396 } 397 mStatus = "done running tests"; 398 try { 399 // Clean up host. 400 doCleanUp(config, context, exception); 401 if (config.getProfiler() != null) { 402 config.getProfiler().reportAllMetrics(listener); 403 } 404 for (ITestDevice device : context.getDevices()) { 405 reportLogs(device, listener, Stage.TEARDOWN); 406 } 407 if (mStopRequested) { 408 CLog.e( 409 "=====================================================================" 410 + "===="); 411 CLog.e( 412 "Invocation was interrupted due to TradeFed stop, results will be " 413 + "affected."); 414 CLog.e( 415 "=====================================================================" 416 + "===="); 417 } 418 reportHostLog(listener, config.getLogOutput()); 419 elapsedTime = System.currentTimeMillis() - startTime; 420 if (!resumed) { 421 listener.invocationEnded(elapsedTime); 422 } 423 } finally { 424 for (String deviceName : context.getDeviceConfigNames()) { 425 config.getDeviceConfigByName(deviceName).getBuildProvider() 426 .cleanUp(context.getBuildInfo(deviceName)); 427 } 428 } 429 } 430 if (tearDownException != null) { 431 // this means a DNAE or RTE has happened during teardown, need to throw 432 // if there was a preceding RTE or DNAE stored in 'exception', it would have already 433 // been thrown before exiting the previous try...catch...finally block 434 throw tearDownException; 435 } 436 } 437 438 /** Do setup and run the tests */ 439 private void prepareAndRun( 440 IConfiguration config, IInvocationContext context, ITestInvocationListener listener) 441 throws Throwable { 442 getRunUtil().allowInterrupt(true); 443 logDeviceBatteryLevel(context, "initial -> setup"); 444 doSetup(config, context, listener); 445 logDeviceBatteryLevel(context, "setup -> test"); 446 runTests(context, config, listener); 447 logDeviceBatteryLevel(context, "after test"); 448 } 449 450 @VisibleForTesting 451 void doSetup( 452 IConfiguration config, 453 IInvocationContext context, 454 final ITestInvocationListener listener) 455 throws TargetSetupError, BuildError, DeviceNotAvailableException { 456 // TODO: evaluate doing device setup in parallel 457 for (String deviceName : context.getDeviceConfigNames()) { 458 ITestDevice device = context.getDevice(deviceName); 459 CLog.d("Starting setup for device: '%s'", device.getSerialNumber()); 460 if (device instanceof ITestLoggerReceiver) { 461 ((ITestLoggerReceiver) context.getDevice(deviceName)) 462 .setTestLogger(listener); 463 } 464 if (!config.getCommandOptions().shouldSkipPreDeviceSetup()) { 465 device.preInvocationSetup(context.getBuildInfo(deviceName)); 466 } 467 for (ITargetPreparer preparer : config.getDeviceConfigByName(deviceName) 468 .getTargetPreparers()) { 469 if (preparer instanceof ITestLoggerReceiver) { 470 ((ITestLoggerReceiver) preparer).setTestLogger(listener); 471 } 472 CLog.d( 473 "starting preparer '%s' on device: '%s'", 474 preparer, device.getSerialNumber()); 475 preparer.setUp(device, context.getBuildInfo(deviceName)); 476 CLog.d( 477 "done with preparer '%s' on device: '%s'", 478 preparer, device.getSerialNumber()); 479 } 480 CLog.d("Done with setup of device: '%s'", device.getSerialNumber()); 481 } 482 // After all the individual setup, make the multi-devices setup 483 for (IMultiTargetPreparer multipreparer : config.getMultiTargetPreparers()) { 484 if (multipreparer instanceof ITestLoggerReceiver) { 485 ((ITestLoggerReceiver) multipreparer).setTestLogger(listener); 486 } 487 CLog.d("Starting multi target preparer '%s'", multipreparer); 488 multipreparer.setUp(context); 489 CLog.d("done with multi target preparer '%s'", multipreparer); 490 } 491 if (config.getProfiler() != null) { 492 config.getProfiler().setUp(context); 493 } 494 // Upload setup logcat after setup is complete 495 for (String deviceName : context.getDeviceConfigNames()) { 496 reportLogs(context.getDevice(deviceName), listener, Stage.SETUP); 497 } 498 } 499 500 private void doTeardown(IConfiguration config, IInvocationContext context, 501 Throwable exception) throws Throwable { 502 Throwable throwable = null; 503 504 List<IMultiTargetPreparer> multiPreparers = config.getMultiTargetPreparers(); 505 ListIterator<IMultiTargetPreparer> iterator = 506 multiPreparers.listIterator(multiPreparers.size()); 507 while (iterator.hasPrevious()) { 508 IMultiTargetPreparer multipreparer = iterator.previous(); 509 CLog.d("Starting multi target tearDown '%s'", multipreparer); 510 multipreparer.tearDown(context, throwable); 511 CLog.d("Done with multi target tearDown '%s'", multipreparer); 512 } 513 514 // Clear wifi settings, to prevent wifi errors from interfering with teardown process. 515 for (String deviceName : context.getDeviceConfigNames()) { 516 ITestDevice device = context.getDevice(deviceName); 517 device.clearLastConnectedWifiNetwork(); 518 List<ITargetPreparer> preparers = 519 config.getDeviceConfigByName(deviceName).getTargetPreparers(); 520 ListIterator<ITargetPreparer> itr = preparers.listIterator(preparers.size()); 521 while (itr.hasPrevious()) { 522 ITargetPreparer preparer = itr.previous(); 523 if(preparer instanceof ITargetCleaner) { 524 ITargetCleaner cleaner = (ITargetCleaner) preparer; 525 if (cleaner != null) { 526 try { 527 CLog.d("starting tearDown '%s' on device: '%s'", preparer, 528 device.getSerialNumber()); 529 cleaner.tearDown(device, context.getBuildInfo(deviceName), exception); 530 CLog.d("done with tearDown '%s' on device: '%s'", preparer, 531 device.getSerialNumber()); 532 } catch (Throwable e) { 533 // We catch it and rethrow later to allow each targetprep to be attempted. 534 // Only the last one will be thrown but all should be logged. 535 CLog.e("Deferring throw for: %s", e); 536 throwable = e; 537 } 538 } 539 } 540 } 541 // Extra tear down step for the device 542 device.postInvocationTearDown(); 543 } 544 545 if (throwable != null) { 546 throw throwable; 547 } 548 } 549 550 private void doCleanUp(IConfiguration config, IInvocationContext context, 551 Throwable exception) { 552 for (String deviceName : context.getDeviceConfigNames()) { 553 List<ITargetPreparer> preparers = 554 config.getDeviceConfigByName(deviceName).getTargetPreparers(); 555 ListIterator<ITargetPreparer> itr = preparers.listIterator(preparers.size()); 556 while (itr.hasPrevious()) { 557 ITargetPreparer preparer = itr.previous(); 558 if (preparer instanceof IHostCleaner) { 559 IHostCleaner cleaner = (IHostCleaner) preparer; 560 if (cleaner != null) { 561 cleaner.cleanUp(context.getBuildInfo(deviceName), exception); 562 } 563 } 564 } 565 } 566 } 567 568 /** 569 * Starts the invocation. 570 * <p/> 571 * Starts logging, and informs listeners that invocation has been started. 572 * 573 * @param config 574 * @param context 575 */ 576 private void startInvocation(IConfiguration config, IInvocationContext context, 577 ITestInvocationListener listener) { 578 logStartInvocation(context, config); 579 listener.invocationStarted(context); 580 } 581 582 /** 583 * Attempt to reschedule the failed invocation to resume where it left off. 584 * <p/> 585 * @see IResumableTest 586 * 587 * @param config 588 * @return <code>true</code> if invocation was resumed successfully 589 */ 590 private boolean resume(IConfiguration config, IInvocationContext context, 591 IRescheduler rescheduler, long elapsedTime) { 592 for (IRemoteTest test : config.getTests()) { 593 if (test instanceof IResumableTest) { 594 IResumableTest resumeTest = (IResumableTest)test; 595 if (resumeTest.isResumable()) { 596 // resume this config if any test is resumable 597 IConfiguration resumeConfig = config.clone(); 598 // reuse the same build for the resumed invocation 599 ShardBuildCloner.cloneBuildInfos(resumeConfig, resumeConfig, context); 600 601 // create a result forwarder, to prevent sending two invocationStarted events 602 resumeConfig.setTestInvocationListener(new ResumeResultForwarder( 603 config.getTestInvocationListeners(), elapsedTime)); 604 resumeConfig.setLogOutput(config.getLogOutput().clone()); 605 resumeConfig.setCommandOptions(config.getCommandOptions().clone()); 606 boolean canReschedule = rescheduler.scheduleConfig(resumeConfig); 607 if (!canReschedule) { 608 CLog.i("Cannot reschedule resumed config for build. Cleaning up build."); 609 for (String deviceName : context.getDeviceConfigNames()) { 610 resumeConfig.getDeviceConfigByName(deviceName).getBuildProvider() 611 .cleanUp(context.getBuildInfo(deviceName)); 612 } 613 } 614 // FIXME: is it a bug to return from here, when we may not have completed the 615 // FIXME: config.getTests iteration? 616 return canReschedule; 617 } 618 } 619 } 620 return false; 621 } 622 623 private void reportFailure(Throwable exception, ITestInvocationListener listener, 624 IConfiguration config, IInvocationContext context, IRescheduler rescheduler) { 625 listener.invocationFailed(exception); 626 if (!(exception instanceof BuildError) && !(exception.getCause() instanceof BuildError)) { 627 for (String deviceName : context.getDeviceConfigNames()) { 628 config.getDeviceConfigByName(deviceName) 629 .getBuildProvider() 630 .buildNotTested(context.getBuildInfo(deviceName)); 631 } 632 rescheduleTest(config, rescheduler); 633 } 634 } 635 636 private void rescheduleTest(IConfiguration config, IRescheduler rescheduler) { 637 for (IRemoteTest test : config.getTests()) { 638 if (!config.getCommandOptions().isLoopMode() && test instanceof IRetriableTest && 639 ((IRetriableTest) test).isRetriable()) { 640 rescheduler.rescheduleCommand(); 641 return; 642 } 643 } 644 } 645 646 private void reportLogs(ITestDevice device, ITestInvocationListener listener, Stage stage) { 647 InputStreamSource logcatSource = null; 648 InputStreamSource emulatorOutput = null; 649 try { 650 // only get logcat if we have an actual device available to avoid empty logs. 651 if (device != null && !(device.getIDevice() instanceof StubDevice)) { 652 logcatSource = device.getLogcat(); 653 device.clearLogcat(); 654 if (device.getIDevice() != null && device.getIDevice().isEmulator()) { 655 emulatorOutput = device.getEmulatorOutput(); 656 // TODO: Clear the emulator log 657 } 658 } 659 if (logcatSource != null) { 660 String name = getDeviceLogName(stage); 661 listener.testLog(name, LogDataType.LOGCAT, logcatSource); 662 } 663 if (emulatorOutput != null) { 664 String name = getEmulatorLogName(stage); 665 listener.testLog(name, LogDataType.TEXT, emulatorOutput); 666 } 667 } finally { 668 // Clean up after our ISSen 669 StreamUtil.cancel(logcatSource); 670 StreamUtil.cancel(emulatorOutput); 671 } 672 } 673 674 private void reportHostLog(ITestInvocationListener listener, ILeveledLogOutput logger) { 675 InputStreamSource globalLogSource = logger.getLog(); 676 listener.testLog(TRADEFED_LOG_NAME, LogDataType.TEXT, globalLogSource); 677 globalLogSource.cancel(); 678 // once tradefed log is reported, all further log calls for this invocation can get lost 679 // unregister logger so future log calls get directed to the tradefed global log 680 getLogRegistry().unregisterLogger(); 681 logger.closeLog(); 682 } 683 684 private void takeBugreport(ITestDevice device, ITestInvocationListener listener, 685 String bugreportName, boolean useBugreportz) { 686 if (device == null) { 687 return; 688 } 689 if (device.getIDevice() instanceof StubDevice) { 690 return; 691 } 692 if (useBugreportz) { 693 // logBugreport will report a regular bugreport if bugreportz is not supported. 694 device.logBugreport(String.format("%s_%s", bugreportName, device.getSerialNumber()), 695 listener); 696 } else { 697 InputStreamSource bugreport = device.getBugreport(); 698 try { 699 if (bugreport != null) { 700 listener.testLog(String.format("%s_%s", bugreportName, 701 device.getSerialNumber()), LogDataType.BUGREPORT, bugreport); 702 } else { 703 CLog.w("Error when collecting bugreport for device '%s'", 704 device.getSerialNumber()); 705 } 706 } finally { 707 StreamUtil.cancel(bugreport); 708 } 709 } 710 } 711 712 /** 713 * Gets the {@link ILogRegistry} to use. 714 * <p/> 715 * Exposed for unit testing. 716 */ 717 ILogRegistry getLogRegistry() { 718 return LogRegistry.getLogRegistry(); 719 } 720 721 /** 722 * Utility method to fetch the default {@link IRunUtil} singleton 723 * <p /> 724 * Exposed for unit testing. 725 */ 726 IRunUtil getRunUtil() { 727 return RunUtil.getDefault(); 728 } 729 730 /** 731 * Runs the test. 732 * 733 * @param context the {@link IInvocationContext} to run tests on 734 * @param config the {@link IConfiguration} to run 735 * @param listener the {@link ITestInvocationListener} of test results 736 * @throws DeviceNotAvailableException 737 */ 738 private void runTests(IInvocationContext context, IConfiguration config, 739 ITestInvocationListener listener) throws DeviceNotAvailableException { 740 for (IRemoteTest test : config.getTests()) { 741 // For compatibility of those receivers, they are assumed to be single device alloc. 742 if (test instanceof IDeviceTest) { 743 ((IDeviceTest)test).setDevice(context.getDevices().get(0)); 744 } 745 if (test instanceof IBuildReceiver) { 746 ((IBuildReceiver)test).setBuild(context.getBuildInfo( 747 context.getDevices().get(0))); 748 } 749 if (test instanceof ISystemStatusCheckerReceiver) { 750 ((ISystemStatusCheckerReceiver) test).setSystemStatusChecker( 751 config.getSystemStatusCheckers()); 752 } 753 754 // TODO: consider adding receivers for only the list of ITestDevice and IBuildInfo. 755 if (test instanceof IMultiDeviceTest) { 756 ((IMultiDeviceTest)test).setDeviceInfos(context.getDeviceBuildMap()); 757 } 758 if (test instanceof IInvocationContextReceiver) { 759 ((IInvocationContextReceiver)test).setInvocationContext(context); 760 } 761 test.run(listener); 762 } 763 } 764 765 @Override 766 public String toString() { 767 return mStatus; 768 } 769 770 /** 771 * Log the battery level of each device in the invocation. 772 * 773 * @param context the {@link IInvocationContext} of the invocation. 774 * @param event a {@link String} describing the context of the logging (initial, setup, etc.). 775 */ 776 @VisibleForTesting 777 void logDeviceBatteryLevel(IInvocationContext context, String event) { 778 for (ITestDevice testDevice : context.getDevices()) { 779 if (testDevice == null) { 780 continue; 781 } 782 IDevice device = testDevice.getIDevice(); 783 if (device == null || device instanceof StubDevice) { 784 continue; 785 } 786 try { 787 Integer batteryLevel = device.getBattery(500, TimeUnit.MILLISECONDS).get(); 788 CLog.v("%s - %s - %d%%", BATT_TAG, event, batteryLevel); 789 context.addInvocationAttribute( 790 String.format( 791 BATTERY_ATTRIBUTE_FORMAT_KEY, testDevice.getSerialNumber(), event), 792 batteryLevel.toString()); 793 continue; 794 } catch (InterruptedException | ExecutionException e) { 795 // fall through 796 } 797 798 CLog.v("Failed to get battery level for %s", testDevice.getSerialNumber()); 799 } 800 } 801 802 /** 803 * {@inheritDoc} 804 */ 805 @Override 806 public void invoke( 807 IInvocationContext context, IConfiguration config, IRescheduler rescheduler, 808 ITestInvocationListener... extraListeners) 809 throws DeviceNotAvailableException, Throwable { 810 List<ITestInvocationListener> allListeners = 811 new ArrayList<>(config.getTestInvocationListeners().size() + extraListeners.length); 812 allListeners.addAll(config.getTestInvocationListeners()); 813 allListeners.addAll(Arrays.asList(extraListeners)); 814 if (config.getProfiler() != null) { 815 allListeners.add(new AggregatingProfilerListener(config.getProfiler())); 816 } 817 ITestInvocationListener listener = new LogSaverResultForwarder(config.getLogSaver(), 818 allListeners); 819 String currentDeviceName = null; 820 try { 821 mStatus = "fetching build"; 822 config.getLogOutput().init(); 823 getLogRegistry().registerLogger(config.getLogOutput()); 824 for (String deviceName : context.getDeviceConfigNames()) { 825 context.getDevice(deviceName).clearLastConnectedWifiNetwork(); 826 context.getDevice(deviceName).setOptions( 827 config.getDeviceConfigByName(deviceName).getDeviceOptions()); 828 if (config.getDeviceConfigByName(deviceName).getDeviceOptions() 829 .isLogcatCaptureEnabled()) { 830 if (!(context.getDevice(deviceName).getIDevice() instanceof StubDevice)) { 831 context.getDevice(deviceName).startLogcat(); 832 } 833 } 834 } 835 836 String cmdLineArgs = config.getCommandLine(); 837 if (cmdLineArgs != null) { 838 CLog.i("Invocation was started with cmd: %s", cmdLineArgs); 839 } 840 updateInvocationContext(context, config); 841 // TODO: evaluate fetching build in parallel 842 for (String deviceName : context.getDeviceConfigNames()) { 843 currentDeviceName = deviceName; 844 IBuildInfo info = null; 845 ITestDevice device = context.getDevice(deviceName); 846 IDeviceConfiguration deviceConfig = config.getDeviceConfigByName(deviceName); 847 IBuildProvider provider = deviceConfig.getBuildProvider(); 848 // Set the provider test tag 849 if (provider instanceof IInvocationContextReceiver) { 850 ((IInvocationContextReceiver)provider).setInvocationContext(context); 851 } 852 // Get the build 853 if (provider instanceof IDeviceBuildProvider) { 854 info = ((IDeviceBuildProvider)provider).getBuild(device); 855 } else { 856 info = provider.getBuild(); 857 } 858 if (info != null) { 859 info.setDeviceSerial(device.getSerialNumber()); 860 context.addDeviceBuildInfo(deviceName, info); 861 device.setRecovery(deviceConfig.getDeviceRecovery()); 862 } else { 863 mStatus = "(no build to test)"; 864 CLog.logAndDisplay( 865 LogLevel.WARN, 866 "No build found to test for device: %s", 867 device.getSerialNumber()); 868 rescheduleTest(config, rescheduler); 869 // save current log contents to global log 870 getLogRegistry().dumpToGlobalLog(config.getLogOutput()); 871 // Set the exit code to error 872 setExitCode(ExitCode.NO_BUILD, 873 new BuildRetrievalError("No build found to test.")); 874 return; 875 } 876 // TODO: remove build update when reporting is done on context 877 updateBuild(info, config); 878 } 879 if (shardConfig(config, context, rescheduler)) { 880 CLog.i("Invocation for %s has been sharded, rescheduling", 881 context.getSerials().toString()); 882 } else { 883 if (config.getTests() == null || config.getTests().isEmpty()) { 884 CLog.e("No tests to run"); 885 } else { 886 performInvocation(config, context, rescheduler, listener); 887 setExitCode(ExitCode.NO_ERROR, null); 888 } 889 } 890 } catch (BuildRetrievalError e) { 891 CLog.e(e); 892 if (currentDeviceName != null) { 893 context.addDeviceBuildInfo(currentDeviceName, e.getBuildInfo()); 894 updateInvocationContext(context, config); 895 } 896 // report an empty invocation, so this error is sent to listeners 897 startInvocation(config, context, listener); 898 // don't want to use #reportFailure, since that will call buildNotTested 899 listener.invocationFailed(e); 900 for (ITestDevice device : context.getDevices()) { 901 reportLogs(device, listener, Stage.ERROR); 902 } 903 reportHostLog(listener, config.getLogOutput()); 904 listener.invocationEnded(0); 905 return; 906 } catch (IOException e) { 907 CLog.e(e); 908 } finally { 909 // ensure we always deregister the logger 910 for (String deviceName : context.getDeviceConfigNames()) { 911 if (!(context.getDevice(deviceName).getIDevice() instanceof StubDevice)) { 912 context.getDevice(deviceName).stopLogcat(); 913 } 914 } 915 // save remaining logs contents to global log 916 getLogRegistry().dumpToGlobalLog(config.getLogOutput()); 917 // Ensure log is unregistered and closed 918 getLogRegistry().unregisterLogger(); 919 config.getLogOutput().closeLog(); 920 } 921 } 922 923 /** 924 * Helper to set the exit code. Exposed for testing. 925 */ 926 protected void setExitCode(ExitCode code, Throwable stack) { 927 GlobalConfiguration.getInstance().getCommandScheduler() 928 .setLastInvocationExitCode(code, stack); 929 } 930 931 public static String getDeviceLogName(Stage stage) { 932 return DEVICE_LOG_NAME_PREFIX + stage.getName(); 933 } 934 935 public static String getEmulatorLogName(Stage stage) { 936 return EMULATOR_LOG_NAME_PREFIX + stage.getName(); 937 } 938 939 @Override 940 public void notifyInvocationStopped() { 941 mStopRequested = true; 942 } 943 } 944