1 /*
2  * Copyright (C) 2022 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 
17 package android.avf.test;
18 
19 import static com.android.tradefed.device.TestDevice.MicrodroidBuilder;
20 import static com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestMetrics;
21 
22 import static com.google.common.truth.Truth.assertWithMessage;
23 import static com.google.common.truth.TruthJUnit.assume;
24 
25 import static org.junit.Assert.assertNotNull;
26 import static org.junit.Assume.assumeFalse;
27 import static org.junit.Assume.assumeTrue;
28 
29 import android.platform.test.annotations.RootPermissionTest;
30 
31 import com.android.microdroid.test.common.MetricsProcessor;
32 import com.android.microdroid.test.host.CommandRunner;
33 import com.android.microdroid.test.host.KvmHypTracer;
34 import com.android.microdroid.test.host.MicrodroidHostTestCaseBase;
35 import com.android.tradefed.device.DeviceNotAvailableException;
36 import com.android.tradefed.device.ITestDevice;
37 import com.android.tradefed.device.TestDevice;
38 import com.android.tradefed.log.LogUtil.CLog;
39 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
40 import com.android.tradefed.util.CommandResult;
41 import com.android.tradefed.util.SimpleStats;
42 
43 import org.junit.After;
44 import org.junit.Before;
45 import org.junit.Rule;
46 import org.junit.Test;
47 import org.junit.runner.RunWith;
48 
49 import java.util.ArrayList;
50 import java.util.Collections;
51 import java.util.List;
52 import java.util.Map;
53 import java.util.regex.Matcher;
54 import java.util.regex.Pattern;
55 
56 @RootPermissionTest
57 @RunWith(DeviceJUnit4ClassRunner.class)
58 public final class AVFHostTestCase extends MicrodroidHostTestCaseBase {
59 
60     private static final String COMPOSD_CMD_BIN = "/apex/com.android.compos/bin/composd_cmd";
61 
62     // Files that define the "test" instance of CompOS
63     private static final String COMPOS_TEST_ROOT = "/data/misc/apexdata/com.android.compos/test/";
64 
65     private static final String BOOTLOADER_TIME_PROP_NAME = "ro.boot.boottime";
66     private static final String BOOTLOADER_PREFIX = "bootloader-";
67     private static final String BOOTLOADER_TIME = "bootloader_time";
68     private static final String BOOTLOADER_PHASE_SW = "SW";
69 
70     /** Boot time test related variables */
71     private static final int REINSTALL_APEX_RETRY_INTERVAL_MS = 5 * 1000;
72 
73     private static final int REINSTALL_APEX_TIMEOUT_SEC = 15;
74     private static final int COMPILE_STAGED_APEX_RETRY_INTERVAL_MS = 10 * 1000;
75     private static final int COMPILE_STAGED_APEX_TIMEOUT_SEC = 540;
76     private static final int BOOT_COMPLETE_TIMEOUT_MS = 10 * 60 * 1000;
77     private static final int ROUND_COUNT = 5;
78     private static final int ROUND_IGNORE_STARTUP_TIME = 3;
79     private static final String APK_NAME = "MicrodroidTestApp.apk";
80     private static final String PACKAGE_NAME = "com.android.microdroid.test";
81 
82     private MetricsProcessor mMetricsProcessor;
83     @Rule public TestMetrics mMetrics = new TestMetrics();
84 
85     private boolean mNeedTearDown = false;
86 
87     @Before
setUp()88     public void setUp() throws Exception {
89         mNeedTearDown = false;
90 
91         assumeDeviceIsCapable(getDevice());
92         mNeedTearDown = true;
93 
94         getDevice().installPackage(findTestFile(APK_NAME), /* reinstall */ false);
95 
96         mMetricsProcessor = new MetricsProcessor(getMetricPrefix() + "hostside/");
97     }
98 
99     @After
tearDown()100     public void tearDown() throws Exception {
101         if (!mNeedTearDown) {
102             // If we skipped setUp, we don't need to undo it, and that avoids potential exceptions
103             // incompatible hardware. (Note that tests can change what assumeDeviceIsCapable()
104             // sees, so we can't rely on that - b/268688303.)
105             return;
106         }
107 
108         CommandRunner android = new CommandRunner(getDevice());
109 
110         // Clear up any CompOS instance files we created.
111         android.tryRun("rm", "-rf", COMPOS_TEST_ROOT);
112     }
113 
114     @Test
testBootWithCompOS()115     public void testBootWithCompOS() throws Exception {
116         composTestHelper(true);
117     }
118 
119     @Test
testBootWithoutCompOS()120     public void testBootWithoutCompOS() throws Exception {
121         composTestHelper(false);
122     }
123 
124     @Test
testNoLongHypSections()125     public void testNoLongHypSections() throws Exception {
126         String[] hypEvents = {"hyp_enter", "hyp_exit"};
127 
128         assumeTrue(
129                 "Skip without hypervisor tracing",
130                 KvmHypTracer.isSupported(getDevice(), hypEvents));
131 
132         KvmHypTracer tracer = new KvmHypTracer(getDevice(), hypEvents);
133         String result = tracer.run(COMPOSD_CMD_BIN + " test-compile");
134         assertWithMessage("Failed to test compilation VM.")
135                 .that(result)
136                 .ignoringCase()
137                 .contains("all ok");
138 
139         SimpleStats stats = tracer.getDurationStats();
140         reportMetric(stats.getData(), "hyp_sections", "s");
141         CLog.i("Hypervisor traces parsed successfully.");
142     }
143 
144     @Test
testPsciMemProtect()145     public void testPsciMemProtect() throws Exception {
146         String[] hypEvents = {"psci_mem_protect"};
147 
148         assumeTrue(
149                 "Skip without hypervisor tracing",
150                 KvmHypTracer.isSupported(getDevice(), hypEvents));
151         KvmHypTracer tracer = new KvmHypTracer(getDevice(), hypEvents);
152 
153         /* We need to wait for crosvm to die so all the VM pages are reclaimed */
154         String result = tracer.run(COMPOSD_CMD_BIN + " test-compile && killall -w crosvm || true");
155         assertWithMessage("Failed to test compilation VM.")
156                 .that(result)
157                 .ignoringCase()
158                 .contains("all ok");
159 
160         List<Integer> values = tracer.getPsciMemProtect();
161 
162         assertWithMessage("PSCI MEM_PROTECT events not recorded")
163                 .that(values.size())
164                 .isGreaterThan(2);
165 
166         assertWithMessage("PSCI MEM_PROTECT counter not starting from 0")
167                 .that(values.get(0))
168                 .isEqualTo(0);
169 
170         assertWithMessage("PSCI MEM_PROTECT counter not ending with 0")
171                 .that(values.get(values.size() - 1))
172                 .isEqualTo(0);
173 
174         assertWithMessage("PSCI MEM_PROTECT counter didn't increment")
175                 .that(Collections.max(values))
176                 .isGreaterThan(0);
177     }
178 
179     @Test
testCameraAppStartupTime()180     public void testCameraAppStartupTime() throws Exception {
181         String[] launchIntentPackages = {
182             "com.android.camera2",
183             "com.google.android.GoogleCamera/com.android.camera.CameraLauncher"
184         };
185         String launchIntentPackage = findSupportedPackage(launchIntentPackages);
186         assume().withMessage("No supported camera package").that(launchIntentPackage).isNotNull();
187         appStartupHelper(launchIntentPackage);
188     }
189 
190     @Test
testSettingsAppStartupTime()191     public void testSettingsAppStartupTime() throws Exception {
192         String[] launchIntentPackages = {"com.android.settings"};
193         String launchIntentPackage = findSupportedPackage(launchIntentPackages);
194         assume().withMessage("No supported settings package").that(launchIntentPackage).isNotNull();
195         appStartupHelper(launchIntentPackage);
196     }
197 
appStartupHelper(String launchIntentPackage)198     private void appStartupHelper(String launchIntentPackage) throws Exception {
199         assumeTrue(
200                 "Skip on non-protected VMs",
201                 ((TestDevice) getDevice()).supportsMicrodroid(/* protectedVm= */ true));
202 
203         StartupTimeMetricCollection mCollection =
204                 new StartupTimeMetricCollection(getPackageName(launchIntentPackage), ROUND_COUNT);
205         getAppStartupTime(launchIntentPackage, mCollection);
206 
207         reportMetric(
208                 mCollection.mAppBeforeVmRunTotalTime,
209                 "app_startup/" + mCollection.getPkgName() + "/total_time/before_vm",
210                 "ms");
211         reportMetric(
212                 mCollection.mAppBeforeVmRunWaitTime,
213                 "app_startup/" + mCollection.getPkgName() + "/wait_time/before_vm",
214                 "ms");
215         reportMetric(
216                 mCollection.mAppDuringVmRunTotalTime,
217                 "app_startup/" + mCollection.getPkgName() + "/total_time/during_vm",
218                 "ms");
219         reportMetric(
220                 mCollection.mAppDuringVmRunWaitTime,
221                 "app_startup/" + mCollection.getPkgName() + "/wait_time/during_vm",
222                 "ms");
223         reportMetric(
224                 mCollection.mAppAfterVmRunTotalTime,
225                 "app_startup/" + mCollection.getPkgName() + "/total_time/after_vm",
226                 "ms");
227         reportMetric(
228                 mCollection.mAppAfterVmRunWaitTime,
229                 "app_startup/" + mCollection.getPkgName() + "/wait_time/after_vm",
230                 "ms");
231     }
232 
getPackageName(String launchIntentPackage)233     private String getPackageName(String launchIntentPackage) {
234         String appPkg = launchIntentPackage;
235 
236         // Does the appPkgName contain the intent ?
237         if (launchIntentPackage != null && launchIntentPackage.contains("/")) {
238             appPkg = launchIntentPackage.split("/")[0];
239         }
240         return appPkg;
241     }
242 
findSupportedPackage(String[] pkgNameList)243     private String findSupportedPackage(String[] pkgNameList) throws Exception {
244         CommandRunner android = new CommandRunner(getDevice());
245 
246         for (String pkgName : pkgNameList) {
247             String appPkg = getPackageName(pkgName);
248             String hasPackage =
249                     android.run(
250                             "pm list package | grep -w " + appPkg + " 1> /dev/null" + "; echo $?");
251             assertNotNull(hasPackage);
252 
253             if (hasPackage.equals("0")) {
254                 return pkgName;
255             }
256         }
257         return null;
258     }
259 
getColdRunStartupTimes(CommandRunner android, String pkgName)260     private AmStartupTimeCmdParser getColdRunStartupTimes(CommandRunner android, String pkgName)
261             throws DeviceNotAvailableException, InterruptedException {
262         unlockScreen(android);
263         // Ensure we are killing the app to get the cold app startup time
264         android.run("am force-stop " + pkgName);
265         android.run("echo 3 > /proc/sys/vm/drop_caches");
266         String vmStartAppLog = android.run("am", "start -W -S " + pkgName);
267         assertNotNull(vmStartAppLog);
268         assumeFalse(vmStartAppLog.isEmpty());
269         return new AmStartupTimeCmdParser(vmStartAppLog);
270     }
271 
272     // Returns an array of two elements containing the delta between the initial app startup time
273     // and the time measured after running the VM.
getAppStartupTime(String pkgName, StartupTimeMetricCollection metricColector)274     private void getAppStartupTime(String pkgName, StartupTimeMetricCollection metricColector)
275             throws Exception {
276         TestDevice device = (TestDevice) getDevice();
277 
278         // 1. Reboot the device to run the test without stage2 fragmentation
279         getDevice().rebootUntilOnline();
280         waitForBootCompleted();
281 
282         // 2. Start the app and ignore first runs to warm up caches
283         CommandRunner android = new CommandRunner(getDevice());
284         for (int i = 0; i < ROUND_IGNORE_STARTUP_TIME; i++) {
285             getColdRunStartupTimes(android, pkgName);
286         }
287 
288         // 3. Run the app before the VM run and collect app startup time statistics
289         for (int i = 0; i < ROUND_COUNT; i++) {
290             AmStartupTimeCmdParser beforeVmStartApp = getColdRunStartupTimes(android, pkgName);
291             metricColector.addStartupTimeMetricBeforeVmRun(beforeVmStartApp);
292         }
293 
294         // Clear up any test dir
295         android.tryRun("rm", "-rf", MicrodroidHostTestCaseBase.TEST_ROOT);
296 
297         // Donate 80% of the available device memory to the VM
298         final String configPath = "assets/vm_config.json";
299         final int vm_mem_mb = getFreeMemoryInfoMb(android) * 80 / 100;
300         ITestDevice microdroidDevice =
301                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
302                         .debugLevel("full")
303                         .memoryMib(vm_mem_mb)
304                         .cpuTopology("match_host")
305                         .build(device);
306         try {
307             microdroidDevice.waitForBootComplete(30000);
308             microdroidDevice.enableAdbRoot();
309 
310             CommandRunner microdroid = new CommandRunner(microdroidDevice);
311 
312             microdroid.run("mkdir -p /mnt/ramdisk && chmod 777 /mnt/ramdisk");
313             microdroid.run("mount -t tmpfs -o size=32G tmpfs /mnt/ramdisk");
314 
315             // Allocate memory for the VM until it fails and make sure that we touch
316             // the allocated memory in the guest to be able to create stage2 fragmentation.
317             try {
318                 microdroid.tryRun(
319                         String.format(
320                                 "cd /mnt/ramdisk && truncate -s %dM sprayMemory"
321                                         + " && dd if=/dev/zero of=sprayMemory bs=1MB count=%d",
322                                 vm_mem_mb, vm_mem_mb));
323             } catch (Exception expected) {
324             }
325 
326             // Run the app during the VM run and collect cold startup time.
327             for (int i = 0; i < ROUND_COUNT; i++) {
328                 AmStartupTimeCmdParser duringVmStartApp = getColdRunStartupTimes(android, pkgName);
329                 metricColector.addStartupTimeMetricDuringVmRun(duringVmStartApp);
330             }
331         } finally {
332             device.shutdownMicrodroid(microdroidDevice);
333         }
334 
335         // Run the app after the VM run and collect cold startup time.
336         for (int i = 0; i < ROUND_COUNT; i++) {
337             AmStartupTimeCmdParser afterVmStartApp = getColdRunStartupTimes(android, pkgName);
338             metricColector.addStartupTimerMetricAfterVmRun(afterVmStartApp);
339         }
340     }
341 
342     static class AmStartupTimeCmdParser {
343         private int mTotalTime;
344         private int mWaitTime;
345 
AmStartupTimeCmdParser(String startAppLog)346         AmStartupTimeCmdParser(String startAppLog) {
347             String[] lines = startAppLog.split("[\r\n]+");
348             mTotalTime = mWaitTime = 0;
349 
350             for (String line : lines) {
351                 if (line.contains("TotalTime:")) {
352                     mTotalTime = Integer.parseInt(line.replaceAll("\\D+", ""));
353                 }
354                 if (line.contains("WaitTime:")) {
355                     mWaitTime = Integer.parseInt(line.replaceAll("\\D+", ""));
356                 }
357             }
358         }
359     }
360 
361     static class StartupTimeMetricCollection {
362         List<Double> mAppBeforeVmRunTotalTime;
363         List<Double> mAppBeforeVmRunWaitTime;
364 
365         List<Double> mAppDuringVmRunTotalTime;
366         List<Double> mAppDuringVmRunWaitTime;
367 
368         List<Double> mAppAfterVmRunTotalTime;
369         List<Double> mAppAfterVmRunWaitTime;
370 
371         private final String mPkgName;
372 
StartupTimeMetricCollection(String pkgName, int size)373         StartupTimeMetricCollection(String pkgName, int size) {
374             mAppBeforeVmRunTotalTime = new ArrayList<>(size);
375             mAppBeforeVmRunWaitTime = new ArrayList<>(size);
376 
377             mAppDuringVmRunTotalTime = new ArrayList<>(size);
378             mAppDuringVmRunWaitTime = new ArrayList<>(size);
379 
380             mAppAfterVmRunTotalTime = new ArrayList<>(size);
381             mAppAfterVmRunWaitTime = new ArrayList<>(size);
382             mPkgName = pkgName;
383         }
384 
addStartupTimeMetricBeforeVmRun(AmStartupTimeCmdParser m)385         public void addStartupTimeMetricBeforeVmRun(AmStartupTimeCmdParser m) {
386             mAppBeforeVmRunTotalTime.add((double) m.mTotalTime);
387             mAppBeforeVmRunWaitTime.add((double) m.mWaitTime);
388         }
389 
addStartupTimeMetricDuringVmRun(AmStartupTimeCmdParser m)390         public void addStartupTimeMetricDuringVmRun(AmStartupTimeCmdParser m) {
391             mAppDuringVmRunTotalTime.add((double) m.mTotalTime);
392             mAppDuringVmRunWaitTime.add((double) m.mWaitTime);
393         }
394 
addStartupTimerMetricAfterVmRun(AmStartupTimeCmdParser m)395         public void addStartupTimerMetricAfterVmRun(AmStartupTimeCmdParser m) {
396             mAppAfterVmRunTotalTime.add((double) m.mTotalTime);
397             mAppAfterVmRunWaitTime.add((double) m.mWaitTime);
398         }
399 
getPkgName()400         public String getPkgName() {
401             return this.mPkgName;
402         }
403     }
404 
getFreeMemoryInfoMb(CommandRunner android)405     private int getFreeMemoryInfoMb(CommandRunner android)
406             throws DeviceNotAvailableException, IllegalArgumentException {
407         int freeMemory = 0;
408         String content = android.runForResult("cat /proc/meminfo").getStdout().trim();
409         String[] lines = content.split("[\r\n]+");
410 
411         for (String line : lines) {
412             if (line.contains("MemFree:")) {
413                 freeMemory = Integer.parseInt(line.replaceAll("\\D+", "")) / 1024;
414                 return freeMemory;
415             }
416         }
417 
418         throw new IllegalArgumentException();
419     }
420 
unlockScreen(CommandRunner android)421     private void unlockScreen(CommandRunner android)
422             throws DeviceNotAvailableException, InterruptedException {
423         android.run("input keyevent", "KEYCODE_WAKEUP");
424         Thread.sleep(500);
425         final String ret =
426                 android.runForResult("dumpsys nfc | grep 'mScreenState='").getStdout().trim();
427         if (ret != null && ret.contains("ON_LOCKED")) {
428             android.run("input keyevent", "KEYCODE_MENU");
429         }
430     }
431 
updateBootloaderTimeInfo(Map<String, List<Double>> bootloaderTime)432     private void updateBootloaderTimeInfo(Map<String, List<Double>> bootloaderTime)
433             throws Exception {
434 
435         String bootLoaderVal = getDevice().getProperty(BOOTLOADER_TIME_PROP_NAME);
436         // Sample Output : 1BLL:89,1BLE:590,2BLL:0,2BLE:1344,SW:6734,KL:1193
437         if (bootLoaderVal != null) {
438             String[] bootLoaderPhases = bootLoaderVal.split(",");
439             double bootLoaderTotalTime = 0d;
440             for (String bootLoaderPhase : bootLoaderPhases) {
441                 String[] bootKeyVal = bootLoaderPhase.split(":");
442                 String key = String.format("%s%s", BOOTLOADER_PREFIX, bootKeyVal[0]);
443 
444                 bootloaderTime
445                         .computeIfAbsent(key, k -> new ArrayList<>())
446                         .add(Double.parseDouble(bootKeyVal[1]));
447                 // SW is the time spent on the warning screen. So ignore it in
448                 // final boot time calculation.
449                 if (BOOTLOADER_PHASE_SW.equalsIgnoreCase(bootKeyVal[0])) {
450                     continue;
451                 }
452                 bootLoaderTotalTime += Double.parseDouble(bootKeyVal[1]);
453             }
454             bootloaderTime
455                     .computeIfAbsent(BOOTLOADER_TIME, k -> new ArrayList<>())
456                     .add(bootLoaderTotalTime);
457         }
458     }
459 
getDmesgBootTime()460     private Double getDmesgBootTime() throws Exception {
461 
462         CommandRunner android = new CommandRunner(getDevice());
463         String result = android.run("dmesg");
464         Pattern pattern = Pattern.compile("\\[(.*)].*sys.boot_completed=1.*");
465         for (String line : result.split("[\r\n]+")) {
466             Matcher matcher = pattern.matcher(line);
467             if (matcher.find()) {
468                 return Double.valueOf(matcher.group(1));
469             }
470         }
471         throw new IllegalArgumentException("Failed to get boot time info.");
472     }
473 
composTestHelper(boolean isWithCompos)474     private void composTestHelper(boolean isWithCompos) throws Exception {
475         assumeFalse("Skip on CF; too slow", isCuttlefish());
476 
477         List<Double> bootDmesgTime = new ArrayList<>(ROUND_COUNT);
478 
479         for (int round = 0; round < ROUND_COUNT; ++round) {
480             reInstallApex(REINSTALL_APEX_TIMEOUT_SEC);
481             try {
482                 if (isWithCompos) {
483                     compileStagedApex(COMPILE_STAGED_APEX_TIMEOUT_SEC);
484                 }
485             } finally {
486                 // If compilation fails, we still have a staged APEX, and we need to reboot to
487                 // clean that up for further tests.
488                 getDevice().nonBlockingReboot();
489                 waitForBootCompleted();
490             }
491 
492             double elapsedSec = getDmesgBootTime();
493             bootDmesgTime.add(elapsedSec);
494         }
495 
496         String suffix = "";
497         if (isWithCompos) {
498             suffix = "with_compos";
499         } else {
500             suffix = "without_compos";
501         }
502 
503         reportMetric(bootDmesgTime, "dmesg_boot_time_" + suffix, "s");
504     }
505 
reportMetric(List<Double> data, String name, String unit)506     private void reportMetric(List<Double> data, String name, String unit) {
507         CLog.d("Report metric " + name + "(" + unit + ") : " + data.toString());
508         Map<String, Double> stats = mMetricsProcessor.computeStats(data, name, unit);
509         for (Map.Entry<String, Double> entry : stats.entrySet()) {
510             CLog.d("Add test metrics " + entry.getKey() + " : " + entry.getValue().toString());
511             mMetrics.addTestMetric(entry.getKey(), entry.getValue().toString());
512         }
513     }
514 
waitForBootCompleted()515     private void waitForBootCompleted() throws Exception {
516         getDevice().waitForDeviceOnline(BOOT_COMPLETE_TIMEOUT_MS);
517         getDevice().waitForBootComplete(BOOT_COMPLETE_TIMEOUT_MS);
518         getDevice().enableAdbRoot();
519     }
520 
compileStagedApex(int timeoutSec)521     private void compileStagedApex(int timeoutSec) throws Exception {
522 
523         long timeStart = System.currentTimeMillis();
524         long timeEnd = timeStart + timeoutSec * 1000L;
525 
526         while (true) {
527 
528             try {
529                 CommandRunner android = new CommandRunner(getDevice());
530 
531                 String result =
532                         android.runWithTimeout(
533                                 3 * 60 * 1000, COMPOSD_CMD_BIN + " staged-apex-compile");
534                 assertWithMessage("Failed to compile staged APEX. Reason: " + result)
535                         .that(result)
536                         .ignoringCase()
537                         .contains("all ok");
538 
539                 CLog.i("Success to compile staged APEX. Result: " + result);
540 
541                 break;
542             } catch (AssertionError e) {
543                 CLog.i("Gets AssertionError when compile staged APEX. Detail: " + e);
544             }
545 
546             if (System.currentTimeMillis() > timeEnd) {
547                 CLog.e("Try to compile staged APEX several times but all fail.");
548                 throw new AssertionError("Failed to compile staged APEX.");
549             }
550 
551             Thread.sleep(COMPILE_STAGED_APEX_RETRY_INTERVAL_MS);
552         }
553     }
554 
reInstallApex(int timeoutSec)555     private void reInstallApex(int timeoutSec) throws Exception {
556 
557         long timeStart = System.currentTimeMillis();
558         long timeEnd = timeStart + timeoutSec * 1000L;
559 
560         while (true) {
561 
562             try {
563                 CommandRunner android = new CommandRunner(getDevice());
564 
565                 String packagesOutput = android.run("pm list packages -f --apex-only");
566 
567                 Pattern p =
568                         Pattern.compile(
569                                 "package:(.*)=(com(?:\\.google)?\\.android\\.art)$",
570                                 Pattern.MULTILINE);
571                 Matcher m = p.matcher(packagesOutput);
572                 assertWithMessage("ART module not found. Packages are:\n" + packagesOutput)
573                         .that(m.find())
574                         .isTrue();
575 
576                 String artApexPath = m.group(1);
577 
578                 CommandResult result = android.runForResult("pm install --apex " + artApexPath);
579                 assertWithMessage("Failed to install APEX. Reason: " + result)
580                         .that(result.getExitCode())
581                         .isEqualTo(0);
582 
583                 CLog.i("Success to install APEX. Result: " + result);
584 
585                 break;
586             } catch (AssertionError e) {
587                 CLog.i("Gets AssertionError when reinstall art APEX. Detail: " + e);
588             }
589 
590             if (System.currentTimeMillis() > timeEnd) {
591                 CLog.e("Try to reinstall art APEX several times but all fail.");
592                 throw new AssertionError("Failed to reinstall art APEX.");
593             }
594 
595             Thread.sleep(REINSTALL_APEX_RETRY_INTERVAL_MS);
596         }
597     }
598 }
599