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