1 /* 2 * Copyright (C) 2017 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.Log.LogLevel; 19 import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey; 20 import com.android.tradefed.build.BuildRetrievalError; 21 import com.android.tradefed.build.IBuildInfo; 22 import com.android.tradefed.build.IBuildInfo.BuildInfoProperties; 23 import com.android.tradefed.build.IBuildProvider; 24 import com.android.tradefed.build.IDeviceBuildInfo; 25 import com.android.tradefed.build.IDeviceBuildProvider; 26 import com.android.tradefed.config.GlobalConfiguration; 27 import com.android.tradefed.config.IConfiguration; 28 import com.android.tradefed.config.IDeviceConfiguration; 29 import com.android.tradefed.config.OptionCopier; 30 import com.android.tradefed.device.DeviceNotAvailableException; 31 import com.android.tradefed.device.ITestDevice; 32 import com.android.tradefed.device.StubDevice; 33 import com.android.tradefed.device.metric.IMetricCollector; 34 import com.android.tradefed.device.metric.IMetricCollectorReceiver; 35 import com.android.tradefed.invoker.TestInvocation.Stage; 36 import com.android.tradefed.invoker.shard.IShardHelper; 37 import com.android.tradefed.log.ITestLogger; 38 import com.android.tradefed.log.LogUtil.CLog; 39 import com.android.tradefed.result.ITestInvocationListener; 40 import com.android.tradefed.result.ITestLoggerReceiver; 41 import com.android.tradefed.result.InputStreamSource; 42 import com.android.tradefed.result.LogDataType; 43 import com.android.tradefed.suite.checker.ISystemStatusCheckerReceiver; 44 import com.android.tradefed.targetprep.BuildError; 45 import com.android.tradefed.targetprep.IHostCleaner; 46 import com.android.tradefed.targetprep.ITargetCleaner; 47 import com.android.tradefed.targetprep.ITargetPreparer; 48 import com.android.tradefed.targetprep.TargetSetupError; 49 import com.android.tradefed.targetprep.multi.IMultiTargetPreparer; 50 import com.android.tradefed.testtype.IBuildReceiver; 51 import com.android.tradefed.testtype.IDeviceTest; 52 import com.android.tradefed.testtype.IInvocationContextReceiver; 53 import com.android.tradefed.testtype.IMultiDeviceTest; 54 import com.android.tradefed.testtype.IRemoteTest; 55 import com.android.tradefed.util.FileUtil; 56 import com.android.tradefed.util.SystemUtil; 57 import com.android.tradefed.util.SystemUtil.EnvVariable; 58 import com.android.tradefed.util.TimeUtil; 59 60 import com.google.common.annotations.VisibleForTesting; 61 62 import java.io.File; 63 import java.io.IOException; 64 import java.util.ArrayList; 65 import java.util.List; 66 import java.util.ListIterator; 67 68 /** 69 * Class that describes all the invocation steps: build download, target_prep, run tests, clean up. 70 * Can be extended to override the default behavior of some steps. Order of the steps is driven by 71 * {@link TestInvocation}. 72 */ 73 public class InvocationExecution implements IInvocationExecution { 74 75 @Override fetchBuild( IInvocationContext context, IConfiguration config, IRescheduler rescheduler, ITestInvocationListener listener)76 public boolean fetchBuild( 77 IInvocationContext context, 78 IConfiguration config, 79 IRescheduler rescheduler, 80 ITestInvocationListener listener) 81 throws DeviceNotAvailableException, BuildRetrievalError { 82 String currentDeviceName = null; 83 try { 84 updateInvocationContext(context, config); 85 // TODO: evaluate fetching build in parallel 86 for (String deviceName : context.getDeviceConfigNames()) { 87 currentDeviceName = deviceName; 88 IBuildInfo info = null; 89 ITestDevice device = context.getDevice(deviceName); 90 IDeviceConfiguration deviceConfig = config.getDeviceConfigByName(deviceName); 91 IBuildProvider provider = deviceConfig.getBuildProvider(); 92 // Inject the context to the provider if it can receive it 93 if (provider instanceof IInvocationContextReceiver) { 94 ((IInvocationContextReceiver) provider).setInvocationContext(context); 95 } 96 // Get the build 97 if (provider instanceof IDeviceBuildProvider) { 98 // Download a device build if the provider can handle it. 99 info = ((IDeviceBuildProvider) provider).getBuild(device); 100 } else { 101 info = provider.getBuild(); 102 } 103 if (info != null) { 104 info.setDeviceSerial(device.getSerialNumber()); 105 context.addDeviceBuildInfo(deviceName, info); 106 device.setRecovery(deviceConfig.getDeviceRecovery()); 107 } else { 108 CLog.logAndDisplay( 109 LogLevel.WARN, 110 "No build found to test for device: %s", 111 device.getSerialNumber()); 112 return false; 113 } 114 // TODO: remove build update when reporting is done on context 115 updateBuild(info, config); 116 } 117 } catch (BuildRetrievalError e) { 118 CLog.e(e); 119 if (currentDeviceName != null) { 120 context.addDeviceBuildInfo(currentDeviceName, e.getBuildInfo()); 121 updateInvocationContext(context, config); 122 } 123 throw e; 124 } 125 return true; 126 } 127 128 @Override cleanUpBuilds(IInvocationContext context, IConfiguration config)129 public void cleanUpBuilds(IInvocationContext context, IConfiguration config) { 130 // Ensure build infos are always cleaned up at the end of invocation. 131 for (String cleanUpDevice : context.getDeviceConfigNames()) { 132 if (context.getBuildInfo(cleanUpDevice) != null) { 133 try { 134 config.getDeviceConfigByName(cleanUpDevice) 135 .getBuildProvider() 136 .cleanUp(context.getBuildInfo(cleanUpDevice)); 137 } catch (RuntimeException e) { 138 // We catch an simply log exception in cleanUp to avoid missing any final 139 // step of the invocation. 140 CLog.e(e); 141 } 142 } 143 } 144 } 145 146 @Override shardConfig( IConfiguration config, IInvocationContext context, IRescheduler rescheduler)147 public boolean shardConfig( 148 IConfiguration config, IInvocationContext context, IRescheduler rescheduler) { 149 return createShardHelper().shardConfig(config, context, rescheduler); 150 } 151 152 /** Create an return the {@link IShardHelper} to be used. */ 153 @VisibleForTesting createShardHelper()154 protected IShardHelper createShardHelper() { 155 return GlobalConfiguration.getInstance().getShardingStrategy(); 156 } 157 158 @Override doSetup( IInvocationContext context, IConfiguration config, final ITestInvocationListener listener)159 public void doSetup( 160 IInvocationContext context, 161 IConfiguration config, 162 final ITestInvocationListener listener) 163 throws TargetSetupError, BuildError, DeviceNotAvailableException { 164 long start = System.currentTimeMillis(); 165 try { 166 // Before all the individual setup, make the multi-pre-target-preparer devices setup 167 runMultiTargetPreparers( 168 config.getMultiPreTargetPreparers(), 169 listener, 170 context, 171 "multi pre target preparer setup"); 172 173 // TODO: evaluate doing device setup in parallel 174 for (String deviceName : context.getDeviceConfigNames()) { 175 ITestDevice device = context.getDevice(deviceName); 176 CLog.d("Starting setup for device: '%s'", device.getSerialNumber()); 177 if (device instanceof ITestLoggerReceiver) { 178 ((ITestLoggerReceiver) context.getDevice(deviceName)).setTestLogger(listener); 179 } 180 if (!config.getCommandOptions().shouldSkipPreDeviceSetup()) { 181 device.preInvocationSetup(context.getBuildInfo(deviceName)); 182 } 183 for (ITargetPreparer preparer : 184 config.getDeviceConfigByName(deviceName).getTargetPreparers()) { 185 // do not call the preparer if it was disabled 186 if (preparer.isDisabled()) { 187 CLog.d("%s has been disabled. skipping.", preparer); 188 continue; 189 } 190 if (preparer instanceof ITestLoggerReceiver) { 191 ((ITestLoggerReceiver) preparer).setTestLogger(listener); 192 } 193 CLog.d( 194 "starting preparer '%s' on device: '%s'", 195 preparer, device.getSerialNumber()); 196 preparer.setUp(device, context.getBuildInfo(deviceName)); 197 CLog.d( 198 "done with preparer '%s' on device: '%s'", 199 preparer, device.getSerialNumber()); 200 } 201 CLog.d("Done with setup of device: '%s'", device.getSerialNumber()); 202 } 203 // After all the individual setup, make the multi-devices setup 204 runMultiTargetPreparers( 205 config.getMultiTargetPreparers(), 206 listener, 207 context, 208 "multi target preparer setup"); 209 210 } finally { 211 // Note: These metrics are handled in a try in case of a kernel reset or device issue. 212 // Setup timing metric. It does not include flashing time on boot tests. 213 long setupDuration = System.currentTimeMillis() - start; 214 context.addInvocationTimingMetric(IInvocationContext.TimingEvent.SETUP, setupDuration); 215 CLog.d("Setup duration: %s'", TimeUtil.formatElapsedTime(setupDuration)); 216 // Upload the setup logcat after setup is complete. 217 for (String deviceName : context.getDeviceConfigNames()) { 218 reportLogs(context.getDevice(deviceName), listener, Stage.SETUP); 219 } 220 } 221 } 222 223 /** Runs the {@link IMultiTargetPreparer} specified. */ runMultiTargetPreparers( List<IMultiTargetPreparer> multiPreparers, ITestLogger logger, IInvocationContext context, String description)224 private void runMultiTargetPreparers( 225 List<IMultiTargetPreparer> multiPreparers, 226 ITestLogger logger, 227 IInvocationContext context, 228 String description) 229 throws TargetSetupError, BuildError, DeviceNotAvailableException { 230 for (IMultiTargetPreparer multiPreparer : multiPreparers) { 231 // do not call the preparer if it was disabled 232 if (multiPreparer.isDisabled()) { 233 CLog.d("%s has been disabled. skipping.", multiPreparer); 234 continue; 235 } 236 if (multiPreparer instanceof ITestLoggerReceiver) { 237 ((ITestLoggerReceiver) multiPreparer).setTestLogger(logger); 238 } 239 CLog.d("Starting %s '%s'", description, multiPreparer); 240 multiPreparer.setUp(context); 241 CLog.d("done with %s '%s'", description, multiPreparer); 242 } 243 } 244 245 /** Runs the {@link IMultiTargetPreparer} specified tearDown. */ runMultiTargetPreparersTearDown( List<IMultiTargetPreparer> multiPreparers, IInvocationContext context, Throwable throwable, String description)246 private void runMultiTargetPreparersTearDown( 247 List<IMultiTargetPreparer> multiPreparers, 248 IInvocationContext context, 249 Throwable throwable, 250 String description) 251 throws DeviceNotAvailableException { 252 ListIterator<IMultiTargetPreparer> iterator = 253 multiPreparers.listIterator(multiPreparers.size()); 254 while (iterator.hasPrevious()) { 255 IMultiTargetPreparer multipreparer = iterator.previous(); 256 if (multipreparer.isDisabled() || multipreparer.isTearDownDisabled()) { 257 CLog.d("%s has been disabled. skipping.", multipreparer); 258 continue; 259 } 260 CLog.d("Starting %s '%s'", description, multipreparer); 261 multipreparer.tearDown(context, throwable); 262 CLog.d("Done with %s '%s'", description, multipreparer); 263 } 264 } 265 266 @Override doTeardown(IInvocationContext context, IConfiguration config, Throwable exception)267 public void doTeardown(IInvocationContext context, IConfiguration config, Throwable exception) 268 throws Throwable { 269 Throwable throwable = null; 270 271 List<IMultiTargetPreparer> multiPreparers = config.getMultiTargetPreparers(); 272 runMultiTargetPreparersTearDown( 273 multiPreparers, context, throwable, "multi target preparer teardown"); 274 275 // Clear wifi settings, to prevent wifi errors from interfering with teardown process. 276 for (String deviceName : context.getDeviceConfigNames()) { 277 ITestDevice device = context.getDevice(deviceName); 278 device.clearLastConnectedWifiNetwork(); 279 List<ITargetPreparer> preparers = 280 config.getDeviceConfigByName(deviceName).getTargetPreparers(); 281 ListIterator<ITargetPreparer> itr = preparers.listIterator(preparers.size()); 282 while (itr.hasPrevious()) { 283 ITargetPreparer preparer = itr.previous(); 284 if (preparer instanceof ITargetCleaner) { 285 ITargetCleaner cleaner = (ITargetCleaner) preparer; 286 // do not call the cleaner if it was disabled 287 if (cleaner.isDisabled() || cleaner.isTearDownDisabled()) { 288 CLog.d("%s has been disabled. skipping.", cleaner); 289 continue; 290 } 291 try { 292 CLog.d( 293 "starting tearDown '%s' on device: '%s'", 294 preparer, device.getSerialNumber()); 295 cleaner.tearDown(device, context.getBuildInfo(deviceName), exception); 296 CLog.d( 297 "done with tearDown '%s' on device: '%s'", 298 preparer, device.getSerialNumber()); 299 } catch (Throwable e) { 300 // We catch it and rethrow later to allow each targetprep to be attempted. 301 // Only the first one will be thrown but all should be logged. 302 CLog.e("Deferring throw for:"); 303 CLog.e(e); 304 if (throwable == null) { 305 throwable = e; 306 } 307 } 308 } 309 } 310 // Extra tear down step for the device 311 if (!config.getCommandOptions().shouldSkipPreDeviceSetup()) { 312 device.postInvocationTearDown(); 313 } 314 } 315 316 // After all, run the multi_pre_target_preparer tearDown. 317 List<IMultiTargetPreparer> multiPrePreparers = config.getMultiPreTargetPreparers(); 318 runMultiTargetPreparersTearDown( 319 multiPrePreparers, context, throwable, "multi pre target preparer teardown"); 320 321 if (throwable != null) { 322 throw throwable; 323 } 324 } 325 326 @Override doCleanUp(IInvocationContext context, IConfiguration config, Throwable exception)327 public void doCleanUp(IInvocationContext context, IConfiguration config, Throwable exception) { 328 for (String deviceName : context.getDeviceConfigNames()) { 329 List<ITargetPreparer> preparers = 330 config.getDeviceConfigByName(deviceName).getTargetPreparers(); 331 ListIterator<ITargetPreparer> itr = preparers.listIterator(preparers.size()); 332 while (itr.hasPrevious()) { 333 ITargetPreparer preparer = itr.previous(); 334 if (preparer instanceof IHostCleaner) { 335 IHostCleaner cleaner = (IHostCleaner) preparer; 336 if (preparer.isDisabled() || preparer.isTearDownDisabled()) { 337 CLog.d("%s has been disabled. skipping.", cleaner); 338 continue; 339 } 340 cleaner.cleanUp(context.getBuildInfo(deviceName), exception); 341 } 342 } 343 } 344 } 345 346 @Override runTests( IInvocationContext context, IConfiguration config, ITestInvocationListener listener)347 public void runTests( 348 IInvocationContext context, IConfiguration config, ITestInvocationListener listener) 349 throws DeviceNotAvailableException { 350 for (IRemoteTest test : config.getTests()) { 351 // For compatibility of those receivers, they are assumed to be single device alloc. 352 if (test instanceof IDeviceTest) { 353 ((IDeviceTest) test).setDevice(context.getDevices().get(0)); 354 } 355 if (test instanceof IBuildReceiver) { 356 ((IBuildReceiver) test).setBuild(context.getBuildInfo(context.getDevices().get(0))); 357 } 358 if (test instanceof ISystemStatusCheckerReceiver) { 359 ((ISystemStatusCheckerReceiver) test) 360 .setSystemStatusChecker(config.getSystemStatusCheckers()); 361 } 362 363 // TODO: consider adding receivers for only the list of ITestDevice and IBuildInfo. 364 if (test instanceof IMultiDeviceTest) { 365 ((IMultiDeviceTest) test).setDeviceInfos(context.getDeviceBuildMap()); 366 } 367 if (test instanceof IInvocationContextReceiver) { 368 ((IInvocationContextReceiver) test).setInvocationContext(context); 369 } 370 371 // We clone the collectors for each IRemoteTest to ensure no state conflicts. 372 List<IMetricCollector> clonedCollectors = cloneCollectors(config.getMetricCollectors()); 373 if (test instanceof IMetricCollectorReceiver) { 374 ((IMetricCollectorReceiver) test).setMetricCollectors(clonedCollectors); 375 // If test can receive collectors then let it handle the how to set them up 376 test.run(listener); 377 } else { 378 // Wrap collectors in each other and collection will be sequential, do this in the 379 // loop to ensure they are always initialized against the right context. 380 ITestInvocationListener listenerWithCollectors = listener; 381 for (IMetricCollector collector : clonedCollectors) { 382 listenerWithCollectors = collector.init(context, listenerWithCollectors); 383 } 384 test.run(listenerWithCollectors); 385 } 386 } 387 } 388 389 @Override resetBuildAndReschedule( Throwable exception, ITestInvocationListener listener, IConfiguration config, IInvocationContext context)390 public boolean resetBuildAndReschedule( 391 Throwable exception, 392 ITestInvocationListener listener, 393 IConfiguration config, 394 IInvocationContext context) { 395 if (!(exception instanceof BuildError) && !(exception.getCause() instanceof BuildError)) { 396 for (String deviceName : context.getDeviceConfigNames()) { 397 config.getDeviceConfigByName(deviceName) 398 .getBuildProvider() 399 .buildNotTested(context.getBuildInfo(deviceName)); 400 } 401 return true; 402 } 403 return false; 404 } 405 406 /** 407 * Helper to clone {@link IMetricCollector}s in order for each {@link IRemoteTest} to get a 408 * different instance, and avoid internal state and multi-init issues. 409 */ cloneCollectors(List<IMetricCollector> originalCollectors)410 private List<IMetricCollector> cloneCollectors(List<IMetricCollector> originalCollectors) { 411 List<IMetricCollector> cloneList = new ArrayList<>(); 412 for (IMetricCollector collector : originalCollectors) { 413 try { 414 // TF object should all have a constructore with no args, so this should be safe. 415 IMetricCollector clone = collector.getClass().newInstance(); 416 OptionCopier.copyOptionsNoThrow(collector, clone); 417 cloneList.add(clone); 418 } catch (InstantiationException | IllegalAccessException e) { 419 throw new RuntimeException(e); 420 } 421 } 422 return cloneList; 423 } 424 reportLogs(ITestDevice device, ITestInvocationListener listener, Stage stage)425 private void reportLogs(ITestDevice device, ITestInvocationListener listener, Stage stage) { 426 if (device == null) { 427 return; 428 } 429 // non stub device 430 if (!(device.getIDevice() instanceof StubDevice)) { 431 try (InputStreamSource logcatSource = device.getLogcat()) { 432 device.clearLogcat(); 433 String name = TestInvocation.getDeviceLogName(stage); 434 listener.testLog(name, LogDataType.LOGCAT, logcatSource); 435 } 436 } 437 // emulator logs 438 if (device.getIDevice() != null && device.getIDevice().isEmulator()) { 439 try (InputStreamSource emulatorOutput = device.getEmulatorOutput()) { 440 // TODO: Clear the emulator log 441 String name = TestInvocation.getEmulatorLogName(stage); 442 listener.testLog(name, LogDataType.TEXT, emulatorOutput); 443 } 444 } 445 } 446 447 /** 448 * Update the {@link IInvocationContext} with additional info from the {@link IConfiguration}. 449 * 450 * @param context the {@link IInvocationContext} 451 * @param config the {@link IConfiguration} 452 */ updateInvocationContext(IInvocationContext context, IConfiguration config)453 private void updateInvocationContext(IInvocationContext context, IConfiguration config) { 454 // TODO: Once reporting on context is done, only set context attributes 455 if (config.getCommandLine() != null) { 456 // TODO: obfuscate the password if any. 457 context.addInvocationAttribute( 458 TestInvocation.COMMAND_ARGS_KEY, config.getCommandLine()); 459 } 460 if (config.getCommandOptions().getShardCount() != null) { 461 context.addInvocationAttribute( 462 "shard_count", config.getCommandOptions().getShardCount().toString()); 463 } 464 if (config.getCommandOptions().getShardIndex() != null) { 465 context.addInvocationAttribute( 466 "shard_index", config.getCommandOptions().getShardIndex().toString()); 467 } 468 context.setTestTag(getTestTag(config)); 469 } 470 471 /** Helper to create the test tag from the configuration. */ getTestTag(IConfiguration config)472 private String getTestTag(IConfiguration config) { 473 String testTag = config.getCommandOptions().getTestTag(); 474 if (config.getCommandOptions().getTestTagSuffix() != null) { 475 testTag = 476 String.format("%s-%s", testTag, config.getCommandOptions().getTestTagSuffix()); 477 } 478 return testTag; 479 } 480 481 /** 482 * Update the {@link IBuildInfo} with additional info from the {@link IConfiguration}. 483 * 484 * @param info the {@link IBuildInfo} 485 * @param config the {@link IConfiguration} 486 */ updateBuild(IBuildInfo info, IConfiguration config)487 private void updateBuild(IBuildInfo info, IConfiguration config) { 488 if (config.getCommandLine() != null) { 489 // TODO: obfuscate the password if any. 490 info.addBuildAttribute(TestInvocation.COMMAND_ARGS_KEY, config.getCommandLine()); 491 } 492 if (config.getCommandOptions().getShardCount() != null) { 493 info.addBuildAttribute( 494 "shard_count", config.getCommandOptions().getShardCount().toString()); 495 } 496 if (config.getCommandOptions().getShardIndex() != null) { 497 info.addBuildAttribute( 498 "shard_index", config.getCommandOptions().getShardIndex().toString()); 499 } 500 // TODO: update all the configs to only use test-tag from CommandOption and not build 501 // providers. 502 // When CommandOption is set, it overrides any test-tag from build_providers 503 if (!"stub".equals(config.getCommandOptions().getTestTag())) { 504 info.setTestTag(getTestTag(config)); 505 } else if (info.getTestTag() == null || info.getTestTag().isEmpty()) { 506 // We ensure that that a default test-tag is always available. 507 info.setTestTag("stub"); 508 } else { 509 CLog.w( 510 "Using the test-tag from the build_provider. Consider updating your config to" 511 + " have no alias/namespace in front of test-tag."); 512 } 513 514 if (info.getProperties().contains(BuildInfoProperties.DO_NOT_LINK_TESTS_DIR)) { 515 CLog.d("Skip linking external directory as FileProperty was set."); 516 return; 517 } 518 // Load environment tests dir. 519 if (info instanceof IDeviceBuildInfo) { 520 File testsDir = ((IDeviceBuildInfo) info).getTestsDir(); 521 if (testsDir != null && testsDir.exists()) { 522 handleLinkingExternalDirs( 523 (IDeviceBuildInfo) info, 524 testsDir, 525 EnvVariable.ANDROID_TARGET_OUT_TESTCASES, 526 BuildInfoFileKey.TARGET_LINKED_DIR.getFileKey()); 527 handleLinkingExternalDirs( 528 (IDeviceBuildInfo) info, 529 testsDir, 530 EnvVariable.ANDROID_HOST_OUT_TESTCASES, 531 BuildInfoFileKey.HOST_LINKED_DIR.getFileKey()); 532 } 533 } 534 } 535 handleLinkingExternalDirs( IDeviceBuildInfo info, File testsDir, EnvVariable var, String baseName)536 private void handleLinkingExternalDirs( 537 IDeviceBuildInfo info, File testsDir, EnvVariable var, String baseName) { 538 File externalDir = getExternalTestCasesDirs(var); 539 if (externalDir == null) { 540 return; 541 } 542 try { 543 // Avoid conflict by creating a randomized name for the arriving symlink file. 544 File subDir = FileUtil.createTempDir(baseName, testsDir); 545 subDir.delete(); 546 FileUtil.symlinkFile(externalDir, subDir); 547 // Tag the dir in the build info to be possibly cleaned. 548 info.setFile( 549 baseName, 550 subDir, 551 /** version */ 552 "v1"); 553 // Ensure we always delete the linking, no matter how the JVM exits. 554 subDir.deleteOnExit(); 555 } catch (IOException e) { 556 CLog.e("Failed to load external test dir %s. Ignoring it.", externalDir); 557 CLog.e(e); 558 } 559 } 560 561 /** Returns the external directory coming from the environment. */ 562 @VisibleForTesting getExternalTestCasesDirs(EnvVariable envVar)563 File getExternalTestCasesDirs(EnvVariable envVar) { 564 return SystemUtil.getExternalTestCasesDir(envVar); 565 } 566 } 567