1 /* 2 * Copyright (C) 2020 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 com.android.internal.util.test; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.assertTrue; 21 22 import com.android.tradefed.device.DeviceNotAvailableException; 23 import com.android.tradefed.device.ITestDevice; 24 import com.android.tradefed.log.LogUtil; 25 26 import org.junit.Assert; 27 import org.junit.ClassRule; 28 import org.junit.rules.ExternalResource; 29 import org.junit.rules.TemporaryFolder; 30 import org.junit.rules.TestRule; 31 import org.junit.runner.Description; 32 import org.junit.runners.model.Statement; 33 34 import java.io.File; 35 import java.io.FileOutputStream; 36 import java.io.IOException; 37 import java.io.InputStream; 38 import java.nio.file.Path; 39 import java.nio.file.Paths; 40 import java.util.ArrayList; 41 42 import javax.annotation.Nullable; 43 44 /** 45 * Allows pushing files onto the device and various options for rebooting. Useful for installing 46 * APKs/files to system partitions which otherwise wouldn't be easily changed. 47 * 48 * It's strongly recommended to pass in a {@link ClassRule} annotated {@link TestRuleDelegate} to 49 * do a full reboot at the end of a test to ensure the device is in a valid state, assuming the 50 * default {@link RebootStrategy#FULL} isn't used. 51 */ 52 public class SystemPreparer extends ExternalResource { 53 private static final long OVERLAY_ENABLE_TIMEOUT_MS = 30000; 54 55 // The paths of the files pushed onto the device through this rule to be removed after. 56 private ArrayList<String> mPushedFiles = new ArrayList<>(); 57 58 // The package names of packages installed through this rule. 59 private ArrayList<String> mInstalledPackages = new ArrayList<>(); 60 61 private final TemporaryFolder mHostTempFolder; 62 private final DeviceProvider mDeviceProvider; 63 private final RebootStrategy mRebootStrategy; 64 private final TearDownRule mTearDownRule; 65 66 // When debugging, it may be useful to run a test case without rebooting the device afterwards, 67 // to manually verify the device state. 68 private boolean mDebugSkipAfterReboot; 69 SystemPreparer(TemporaryFolder hostTempFolder, DeviceProvider deviceProvider)70 public SystemPreparer(TemporaryFolder hostTempFolder, DeviceProvider deviceProvider) { 71 this(hostTempFolder, RebootStrategy.FULL, null, deviceProvider); 72 } 73 SystemPreparer(TemporaryFolder hostTempFolder, RebootStrategy rebootStrategy, @Nullable TestRuleDelegate testRuleDelegate, DeviceProvider deviceProvider)74 public SystemPreparer(TemporaryFolder hostTempFolder, RebootStrategy rebootStrategy, 75 @Nullable TestRuleDelegate testRuleDelegate, DeviceProvider deviceProvider) { 76 this(hostTempFolder, rebootStrategy, testRuleDelegate, false, deviceProvider); 77 } 78 SystemPreparer(TemporaryFolder hostTempFolder, RebootStrategy rebootStrategy, @Nullable TestRuleDelegate testRuleDelegate, boolean debugSkipAfterReboot, DeviceProvider deviceProvider)79 public SystemPreparer(TemporaryFolder hostTempFolder, RebootStrategy rebootStrategy, 80 @Nullable TestRuleDelegate testRuleDelegate, boolean debugSkipAfterReboot, 81 DeviceProvider deviceProvider) { 82 mHostTempFolder = hostTempFolder; 83 mDeviceProvider = deviceProvider; 84 mRebootStrategy = rebootStrategy; 85 mTearDownRule = new TearDownRule(mDeviceProvider); 86 if (testRuleDelegate != null) { 87 testRuleDelegate.setDelegate(mTearDownRule); 88 } 89 mDebugSkipAfterReboot = debugSkipAfterReboot; 90 } 91 92 /** Copies a file within the host test jar to a path on device. */ pushResourceFile(String filePath, String outputPath)93 public SystemPreparer pushResourceFile(String filePath, String outputPath) 94 throws DeviceNotAvailableException, IOException { 95 final ITestDevice device = mDeviceProvider.getDevice(); 96 remount(); 97 assertTrue(device.pushFile(copyResourceToTemp(filePath), outputPath)); 98 addPushedFile(device, outputPath); 99 return this; 100 } 101 102 /** Copies a file directly from the host file system to a path on device. */ pushFile(File file, String outputPath)103 public SystemPreparer pushFile(File file, String outputPath) 104 throws DeviceNotAvailableException { 105 final ITestDevice device = mDeviceProvider.getDevice(); 106 remount(); 107 assertTrue(device.pushFile(file, outputPath)); 108 addPushedFile(device, outputPath); 109 return this; 110 } 111 addPushedFile(ITestDevice device, String outputPath)112 private void addPushedFile(ITestDevice device, String outputPath) 113 throws DeviceNotAvailableException { 114 Path pathCreated = Paths.get(outputPath); 115 116 // Find the top most parent that is new to the device 117 while (pathCreated.getParent() != null 118 && !device.doesFileExist(pathCreated.getParent().toString())) { 119 pathCreated = pathCreated.getParent(); 120 } 121 122 mPushedFiles.add(pathCreated.toString()); 123 } 124 125 /** Deletes the given path from the device */ deleteFile(String file)126 public SystemPreparer deleteFile(String file) throws DeviceNotAvailableException { 127 final ITestDevice device = mDeviceProvider.getDevice(); 128 remount(); 129 device.deleteFile(file); 130 return this; 131 } 132 133 /** Installs an APK within the host test jar onto the device. */ installResourceApk(String resourcePath, String packageName)134 public SystemPreparer installResourceApk(String resourcePath, String packageName) 135 throws DeviceNotAvailableException, IOException { 136 final ITestDevice device = mDeviceProvider.getDevice(); 137 final File tmpFile = copyResourceToTemp(resourcePath); 138 final String result = device.installPackage(tmpFile, true /* reinstall */); 139 Assert.assertNull(result); 140 mInstalledPackages.add(packageName); 141 return this; 142 } 143 144 /** Stages multiple APEXs within the host test jar onto the device. */ stageMultiplePackages(String[] resourcePaths, String[] packageNames)145 public SystemPreparer stageMultiplePackages(String[] resourcePaths, String[] packageNames) 146 throws DeviceNotAvailableException, IOException { 147 assertEquals(resourcePaths.length, packageNames.length); 148 final ITestDevice device = mDeviceProvider.getDevice(); 149 final String[] adbCommandLine = new String[resourcePaths.length + 2]; 150 adbCommandLine[0] = "install-multi-package"; 151 adbCommandLine[1] = "--staged"; 152 for (int i = 0; i < resourcePaths.length; i++) { 153 final File tmpFile = copyResourceToTemp(resourcePaths[i]); 154 adbCommandLine[i + 2] = tmpFile.getAbsolutePath(); 155 mInstalledPackages.add(packageNames[i]); 156 } 157 final String output = device.executeAdbCommand(adbCommandLine); 158 assertTrue(output.contains("Success. Reboot device to apply staged session")); 159 return this; 160 } 161 162 /** Sets the enable state of an overlay package. */ setOverlayEnabled(String packageName, boolean enabled)163 public SystemPreparer setOverlayEnabled(String packageName, boolean enabled) 164 throws DeviceNotAvailableException { 165 final ITestDevice device = mDeviceProvider.getDevice(); 166 final String enable = enabled ? "enable" : "disable"; 167 168 // Wait for the overlay to change its enabled state. 169 final long endMillis = System.currentTimeMillis() + OVERLAY_ENABLE_TIMEOUT_MS; 170 String result; 171 while (System.currentTimeMillis() <= endMillis) { 172 device.executeShellCommand(String.format("cmd overlay %s %s", enable, packageName)); 173 result = device.executeShellCommand("cmd overlay dump isenabled " 174 + packageName); 175 if (((enabled) ? "true\n" : "false\n").equals(result)) { 176 return this; 177 } 178 179 try { 180 Thread.sleep(200); 181 } catch (InterruptedException ignore) { 182 } 183 } 184 185 throw new IllegalStateException(String.format("Failed to %s overlay %s:\n%s", enable, 186 packageName, device.executeShellCommand("cmd overlay list"))); 187 } 188 189 /** Restarts the device and waits until after boot is completed. */ reboot()190 public SystemPreparer reboot() throws DeviceNotAvailableException { 191 ITestDevice device = mDeviceProvider.getDevice(); 192 switch (mRebootStrategy) { 193 case FULL: 194 device.reboot(); 195 break; 196 case UNTIL_ONLINE: 197 device.rebootUntilOnline(); 198 break; 199 case USERSPACE: 200 device.rebootUserspace(); 201 break; 202 case USERSPACE_UNTIL_ONLINE: 203 device.rebootUserspaceUntilOnline(); 204 break; 205 // TODO(b/159540015): Make this START_STOP instead of default once it's fixed. Can't 206 // currently be done because START_STOP is commented out. 207 default: 208 device.executeShellCommand("stop"); 209 device.executeShellCommand("start"); 210 ITestDevice.RecoveryMode cachedRecoveryMode = device.getRecoveryMode(); 211 device.setRecoveryMode(ITestDevice.RecoveryMode.ONLINE); 212 213 if (device.isEncryptionSupported()) { 214 if (device.isDeviceEncrypted()) { 215 LogUtil.CLog.e("Device is encrypted after userspace reboot!"); 216 device.unlockDevice(); 217 } 218 } 219 220 device.setRecoveryMode(cachedRecoveryMode); 221 device.waitForDeviceAvailable(); 222 break; 223 } 224 return this; 225 } 226 remount()227 public SystemPreparer remount() throws DeviceNotAvailableException { 228 mTearDownRule.remount(); 229 return this; 230 } 231 getFileExtension(@ullable String path)232 private static @Nullable String getFileExtension(@Nullable String path) { 233 if (path == null) { 234 return null; 235 } 236 final int lastDot = path.lastIndexOf('.'); 237 if (lastDot >= 0) { 238 return path.substring(lastDot + 1); 239 } else { 240 return null; 241 } 242 } 243 244 /** Copies a file within the host test jar to a temporary file on the host machine. */ copyResourceToTemp(String resourcePath)245 private File copyResourceToTemp(String resourcePath) throws IOException { 246 final String ext = getFileExtension(resourcePath); 247 final File tempFile; 248 if (ext != null) { 249 tempFile = File.createTempFile("junit", "." + ext, mHostTempFolder.getRoot()); 250 } else { 251 tempFile = mHostTempFolder.newFile(); 252 } 253 final ClassLoader classLoader = getClass().getClassLoader(); 254 try (InputStream assetIs = classLoader.getResourceAsStream(resourcePath); 255 FileOutputStream assetOs = new FileOutputStream(tempFile)) { 256 if (assetIs == null) { 257 throw new IllegalStateException("Failed to find resource " + resourcePath); 258 } 259 260 int b; 261 while ((b = assetIs.read()) >= 0) { 262 assetOs.write(b); 263 } 264 } 265 266 return tempFile; 267 } 268 269 /** Removes installed packages and files that were pushed to the device. */ 270 @Override after()271 public void after() { 272 final ITestDevice device = mDeviceProvider.getDevice(); 273 try { 274 remount(); 275 for (final String file : mPushedFiles) { 276 device.deleteFile(file); 277 } 278 for (final String packageName : mInstalledPackages) { 279 device.uninstallPackage(packageName); 280 } 281 if (!mDebugSkipAfterReboot) { 282 reboot(); 283 } 284 } catch (DeviceNotAvailableException e) { 285 Assert.fail(e.toString()); 286 } 287 } 288 289 /** 290 * A hacky workaround since {@link org.junit.AfterClass} and {@link ClassRule} require static 291 * members. Will defer assignment of the actual {@link TestRule} to execute until after any 292 * test case has been run. 293 * 294 * In effect, this makes the {@link ITestDevice} to be accessible after all test cases have 295 * been executed, allowing {@link ITestDevice#reboot()} to be used to fully restore the device. 296 */ 297 public static class TestRuleDelegate implements TestRule { 298 299 private boolean mThrowOnNull; 300 301 @Nullable 302 private TestRule mTestRule; 303 TestRuleDelegate(boolean throwOnNull)304 public TestRuleDelegate(boolean throwOnNull) { 305 mThrowOnNull = throwOnNull; 306 } 307 setDelegate(TestRule testRule)308 public void setDelegate(TestRule testRule) { 309 mTestRule = testRule; 310 } 311 312 @Override apply(Statement base, Description description)313 public Statement apply(Statement base, Description description) { 314 if (mTestRule == null) { 315 if (mThrowOnNull) { 316 throw new IllegalStateException("TestRule delegate was not set"); 317 } else { 318 return new Statement() { 319 @Override 320 public void evaluate() throws Throwable { 321 base.evaluate(); 322 } 323 }; 324 } 325 } 326 327 Statement statement = mTestRule.apply(base, description); 328 mTestRule = null; 329 return statement; 330 } 331 } 332 333 /** 334 * Forces a full reboot at the end of the test class to restore any device state. 335 */ 336 private static class TearDownRule extends ExternalResource { 337 338 private DeviceProvider mDeviceProvider; 339 private boolean mInitialized; 340 private boolean mWasVerityEnabled; 341 private boolean mWasAdbRoot; 342 private boolean mIsVerityEnabled; 343 344 TearDownRule(DeviceProvider deviceProvider) { 345 mDeviceProvider = deviceProvider; 346 } 347 348 @Override 349 protected void before() { 350 // This method will never be run 351 } 352 353 @Override 354 protected void after() { 355 try { 356 initialize(); 357 ITestDevice device = mDeviceProvider.getDevice(); 358 if (mWasVerityEnabled != mIsVerityEnabled) { 359 device.executeShellCommand( 360 mWasVerityEnabled ? "enable-verity" : "disable-verity"); 361 } 362 device.reboot(); 363 if (!mWasAdbRoot) { 364 device.disableAdbRoot(); 365 } 366 } catch (DeviceNotAvailableException e) { 367 Assert.fail(e.toString()); 368 } 369 } 370 371 /** 372 * Remount is done inside this class so that the verity state can be tracked. 373 */ 374 public void remount() throws DeviceNotAvailableException { 375 initialize(); 376 ITestDevice device = mDeviceProvider.getDevice(); 377 device.enableAdbRoot(); 378 if (mIsVerityEnabled) { 379 mIsVerityEnabled = false; 380 device.executeShellCommand("disable-verity"); 381 device.reboot(); 382 } 383 device.executeShellCommand("remount"); 384 device.waitForDeviceAvailable(); 385 } 386 387 private void initialize() throws DeviceNotAvailableException { 388 if (mInitialized) { 389 return; 390 } 391 mInitialized = true; 392 ITestDevice device = mDeviceProvider.getDevice(); 393 mWasAdbRoot = device.isAdbRoot(); 394 device.enableAdbRoot(); 395 String veritySystem = device.getProperty("partition.system.verified"); 396 String verityVendor = device.getProperty("partition.vendor.verified"); 397 mWasVerityEnabled = (veritySystem != null && !veritySystem.isEmpty()) 398 || (verityVendor != null && !verityVendor.isEmpty()); 399 mIsVerityEnabled = mWasVerityEnabled; 400 } 401 } 402 403 public interface DeviceProvider { 404 ITestDevice getDevice(); 405 } 406 407 /** 408 * How to reboot the device. Ordered from slowest to fastest. 409 */ 410 @SuppressWarnings("DanglingJavadoc") 411 public enum RebootStrategy { 412 /** @see ITestDevice#reboot() */ 413 FULL, 414 415 /** @see ITestDevice#rebootUntilOnline() () */ 416 UNTIL_ONLINE, 417 418 /** @see ITestDevice#rebootUserspace() */ 419 USERSPACE, 420 421 /** @see ITestDevice#rebootUserspaceUntilOnline() () */ 422 USERSPACE_UNTIL_ONLINE, 423 424 /** 425 * Uses shell stop && start to "reboot" the device. May leave invalid state after each test. 426 * Whether this matters or not depends on what's being tested. 427 * 428 * TODO(b/159540015): There's a bug with this causing unnecessary disk space usage, which 429 * can eventually lead to an insufficient storage space error. 430 * 431 * This can be uncommented for local development, but should be left out when merging. 432 * It is done this way to hopefully be caught by code review, since merging this will 433 * break all of postsubmit. But the nearly 50% reduction in test runtime is worth having 434 * this option exist. 435 * 436 * @deprecated do not use this in merged code until bug is resolved 437 */ 438 // @Deprecated 439 // START_STOP 440 } 441 } 442