blob: 6bd6985f96750cfb77ea32dbef92fac67ba7f776 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.internal.util.test;
import static org.junit.Assert.assertTrue;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.rules.ExternalResource;
import org.junit.rules.TemporaryFolder;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import javax.annotation.Nullable;
/**
* Allows pushing files onto the device and various options for rebooting. Useful for installing
* APKs/files to system partitions which otherwise wouldn't be easily changed.
*
* It's strongly recommended to pass in a {@link ClassRule} annotated {@link TestRuleDelegate} to
* do a full reboot at the end of a test to ensure the device is in a valid state, assuming the
* default {@link RebootStrategy#FULL} isn't used.
*/
public class SystemPreparer extends ExternalResource {
private static final long OVERLAY_ENABLE_TIMEOUT_MS = 30000;
// The paths of the files pushed onto the device through this rule.
private ArrayList<String> mPushedFiles = new ArrayList<>();
// The package names of packages installed through this rule.
private ArrayList<String> mInstalledPackages = new ArrayList<>();
private final TemporaryFolder mHostTempFolder;
private final DeviceProvider mDeviceProvider;
private final RebootStrategy mRebootStrategy;
private final TearDownRule mTearDownRule;
public SystemPreparer(TemporaryFolder hostTempFolder, DeviceProvider deviceProvider) {
this(hostTempFolder, RebootStrategy.FULL, null, deviceProvider);
}
public SystemPreparer(TemporaryFolder hostTempFolder, RebootStrategy rebootStrategy,
@Nullable TestRuleDelegate testRuleDelegate, DeviceProvider deviceProvider) {
mHostTempFolder = hostTempFolder;
mDeviceProvider = deviceProvider;
mRebootStrategy = rebootStrategy;
mTearDownRule = new TearDownRule(mDeviceProvider);
if (testRuleDelegate != null) {
testRuleDelegate.setDelegate(mTearDownRule);
}
}
/** Copies a file within the host test jar to a path on device. */
public SystemPreparer pushResourceFile(String filePath, String outputPath)
throws DeviceNotAvailableException, IOException {
final ITestDevice device = mDeviceProvider.getDevice();
remount();
assertTrue(device.pushFile(copyResourceToTemp(filePath), outputPath));
mPushedFiles.add(outputPath);
return this;
}
/** Copies a file directly from the host file system to a path on device. */
public SystemPreparer pushFile(File file, String outputPath)
throws DeviceNotAvailableException {
final ITestDevice device = mDeviceProvider.getDevice();
remount();
assertTrue(device.pushFile(file, outputPath));
mPushedFiles.add(outputPath);
return this;
}
/** Deletes the given path from the device */
public SystemPreparer deleteFile(String file) throws DeviceNotAvailableException {
final ITestDevice device = mDeviceProvider.getDevice();
remount();
device.deleteFile(file);
return this;
}
/** Installs an APK within the host test jar onto the device. */
public SystemPreparer installResourceApk(String resourcePath, String packageName)
throws DeviceNotAvailableException, IOException {
final ITestDevice device = mDeviceProvider.getDevice();
final File tmpFile = copyResourceToTemp(resourcePath);
final String result = device.installPackage(tmpFile, true /* reinstall */);
Assert.assertNull(result);
mInstalledPackages.add(packageName);
return this;
}
/** Sets the enable state of an overlay package. */
public SystemPreparer setOverlayEnabled(String packageName, boolean enabled)
throws DeviceNotAvailableException {
final ITestDevice device = mDeviceProvider.getDevice();
final String enable = enabled ? "enable" : "disable";
// Wait for the overlay to change its enabled state.
final long endMillis = System.currentTimeMillis() + OVERLAY_ENABLE_TIMEOUT_MS;
String result;
while (System.currentTimeMillis() <= endMillis) {
device.executeShellCommand(String.format("cmd overlay %s %s", enable, packageName));
result = device.executeShellCommand("cmd overlay dump isenabled "
+ packageName);
if (((enabled) ? "true\n" : "false\n").equals(result)) {
return this;
}
try {
Thread.sleep(200);
} catch (InterruptedException ignore) {
}
}
throw new IllegalStateException(String.format("Failed to %s overlay %s:\n%s", enable,
packageName, device.executeShellCommand("cmd overlay list")));
}
/** Restarts the device and waits until after boot is completed. */
public SystemPreparer reboot() throws DeviceNotAvailableException {
ITestDevice device = mDeviceProvider.getDevice();
switch (mRebootStrategy) {
case FULL:
device.reboot();
break;
case UNTIL_ONLINE:
device.rebootUntilOnline();
break;
case USERSPACE:
device.rebootUserspace();
break;
case USERSPACE_UNTIL_ONLINE:
device.rebootUserspaceUntilOnline();
break;
case START_STOP:
device.executeShellCommand("stop");
device.executeShellCommand("start");
ITestDevice.RecoveryMode cachedRecoveryMode = device.getRecoveryMode();
device.setRecoveryMode(ITestDevice.RecoveryMode.ONLINE);
if (device.isEncryptionSupported()) {
if (device.isDeviceEncrypted()) {
LogUtil.CLog.e("Device is encrypted after userspace reboot!");
device.unlockDevice();
}
}
device.setRecoveryMode(cachedRecoveryMode);
device.waitForDeviceAvailable();
break;
}
return this;
}
public SystemPreparer remount() throws DeviceNotAvailableException {
mTearDownRule.remount();
return this;
}
/** Copies a file within the host test jar to a temporary file on the host machine. */
private File copyResourceToTemp(String resourcePath) throws IOException {
final File tempFile = mHostTempFolder.newFile();
final ClassLoader classLoader = getClass().getClassLoader();
try (InputStream assetIs = classLoader.getResource(resourcePath).openStream();
FileOutputStream assetOs = new FileOutputStream(tempFile)) {
if (assetIs == null) {
throw new IllegalStateException("Failed to find resource " + resourcePath);
}
int b;
while ((b = assetIs.read()) >= 0) {
assetOs.write(b);
}
}
return tempFile;
}
/** Removes installed packages and files that were pushed to the device. */
@Override
protected void after() {
final ITestDevice device = mDeviceProvider.getDevice();
try {
remount();
for (final String file : mPushedFiles) {
device.deleteFile(file);
}
for (final String packageName : mInstalledPackages) {
device.uninstallPackage(packageName);
}
reboot();
} catch (DeviceNotAvailableException e) {
Assert.fail(e.toString());
}
}
/**
* A hacky workaround since {@link org.junit.AfterClass} and {@link ClassRule} require static
* members. Will defer assignment of the actual {@link TestRule} to execute until after any
* test case has been run.
*
* In effect, this makes the {@link ITestDevice} to be accessible after all test cases have
* been executed, allowing {@link ITestDevice#reboot()} to be used to fully restore the device.
*/
public static class TestRuleDelegate implements TestRule {
private boolean mThrowOnNull;
@Nullable
private TestRule mTestRule;
public TestRuleDelegate(boolean throwOnNull) {
mThrowOnNull = throwOnNull;
}
public void setDelegate(TestRule testRule) {
mTestRule = testRule;
}
@Override
public Statement apply(Statement base, Description description) {
if (mTestRule == null) {
if (mThrowOnNull) {
throw new IllegalStateException("TestRule delegate was not set");
} else {
return new Statement() {
@Override
public void evaluate() throws Throwable {
base.evaluate();
}
};
}
}
Statement statement = mTestRule.apply(base, description);
mTestRule = null;
return statement;
}
}
/**
* Forces a full reboot at the end of the test class to restore any device state.
*/
private static class TearDownRule extends ExternalResource {
private DeviceProvider mDeviceProvider;
private boolean mInitialized;
private boolean mWasVerityEnabled;
private boolean mWasAdbRoot;
private boolean mIsVerityEnabled;
TearDownRule(DeviceProvider deviceProvider) {
mDeviceProvider = deviceProvider;
}
@Override
protected void before() {
// This method will never be run
}
@Override
protected void after() {
try {
initialize();
ITestDevice device = mDeviceProvider.getDevice();
if (mWasVerityEnabled != mIsVerityEnabled) {
device.executeShellCommand(
mWasVerityEnabled ? "enable-verity" : "disable-verity");
}
device.reboot();
if (!mWasAdbRoot) {
device.disableAdbRoot();
}
} catch (DeviceNotAvailableException e) {
Assert.fail(e.toString());
}
}
/**
* Remount is done inside this class so that the verity state can be tracked.
*/
public void remount() throws DeviceNotAvailableException {
initialize();
ITestDevice device = mDeviceProvider.getDevice();
device.enableAdbRoot();
if (mIsVerityEnabled) {
mIsVerityEnabled = false;
device.executeShellCommand("disable-verity");
device.reboot();
}
device.executeShellCommand("remount");
device.waitForDeviceAvailable();
}
private void initialize() throws DeviceNotAvailableException {
if (mInitialized) {
return;
}
mInitialized = true;
ITestDevice device = mDeviceProvider.getDevice();
mWasAdbRoot = device.isAdbRoot();
device.enableAdbRoot();
String veritySystem = device.getProperty("partition.system.verified");
String verityVendor = device.getProperty("partition.vendor.verified");
mWasVerityEnabled = (veritySystem != null && !veritySystem.isEmpty())
|| (verityVendor != null && !verityVendor.isEmpty());
mIsVerityEnabled = mWasVerityEnabled;
}
}
public interface DeviceProvider {
ITestDevice getDevice();
}
/**
* How to reboot the device. Ordered from slowest to fastest.
*/
public enum RebootStrategy {
/** @see ITestDevice#reboot() */
FULL,
/** @see ITestDevice#rebootUntilOnline() () */
UNTIL_ONLINE,
/** @see ITestDevice#rebootUserspace() */
USERSPACE,
/** @see ITestDevice#rebootUserspaceUntilOnline() () */
USERSPACE_UNTIL_ONLINE,
/**
* Uses shell stop && start to "reboot" the device. May leave invalid state after each test.
* Whether this matters or not depends on what's being tested.
*/
START_STOP
}
}