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