Home | History | Annotate | Download | only in invoker
      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
     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
    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
    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
    154     protected IShardHelper createShardHelper() {
    155         return GlobalConfiguration.getInstance().getShardingStrategy();
    156     }
    157 
    158     @Override
    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. */
    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. */
    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
    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
    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
    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
    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      */
    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 
    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      */
    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. */
    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      */
    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 
    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
    563     File getExternalTestCasesDirs(EnvVariable envVar) {
    564         return SystemUtil.getExternalTestCasesDir(envVar);
    565     }
    566 }
    567