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