| /* |
| * 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.gki.tests; |
| |
| import static org.hamcrest.Matchers.containsString; |
| import static org.hamcrest.Matchers.everyItem; |
| import static org.hamcrest.Matchers.greaterThan; |
| import static org.hamcrest.Matchers.hasItem; |
| import static org.hamcrest.Matchers.is; |
| import static org.hamcrest.Matchers.isIn; |
| import static org.hamcrest.Matchers.notNullValue; |
| import static org.hamcrest.io.FileMatchers.aFileWithSize; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertNull; |
| import static org.junit.Assert.assertThat; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assume.assumeThat; |
| import static org.junit.Assert.fail; |
| |
| import static java.util.stream.Collectors.toList; |
| |
| import android.cts.host.utils.DeviceJUnit4ClassRunnerWithParameters; |
| import android.cts.host.utils.DeviceJUnit4Parameterized; |
| |
| import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; |
| import com.android.tradefed.device.ITestDevice; |
| import com.android.tradefed.device.ITestDevice.ApexInfo; |
| import com.android.tradefed.log.LogUtil.CLog; |
| import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.Parameterized.Parameter; |
| import org.junit.runners.Parameterized.Parameters; |
| import org.junit.runners.Parameterized.UseParametersRunnerFactory; |
| |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.io.File; |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.Scanner; |
| import java.util.Set; |
| |
| @RunWith(DeviceJUnit4Parameterized.class) |
| @UseParametersRunnerFactory(DeviceJUnit4ClassRunnerWithParameters.RunnerFactory.class) |
| public class GkiInstallTest extends BaseHostJUnit4Test { |
| |
| // Keep in sync with gki.go. |
| private static final String HIGH_SUFFIX = "_test_high.apex"; |
| private static final String LOW_SUFFIX = "_test_low.apex"; |
| private static final long TEST_HIGH_VERSION = 1000000000L; |
| |
| // Timeout between device online for adb commands and boot completed flag is set. |
| private static final long DEVICE_AVAIL_TIMEOUT_MS = 180000; // 3mins |
| // Timeout for `adb install`. |
| private static final long INSTALL_TIMEOUT_MS = 600000; // 10mins |
| |
| @Parameter |
| public String mFileName; |
| |
| private String mPackageName; |
| private File mApexFile; |
| private boolean mExpectInstallSuccess; |
| private final Set<String> mOverlayfs = new HashSet(); |
| |
| @Parameters(name = "{0}") |
| public static Iterable<String> getTestFileNames() { |
| try (Scanner scanner = new Scanner( |
| GkiInstallTest.class.getClassLoader().getResourceAsStream( |
| "gki_install_test_file_list.txt"))) { |
| List<String> list = new ArrayList<>(); |
| scanner.forEachRemaining(list::add); |
| return list; |
| } |
| } |
| |
| @Before |
| public void setUp() throws Exception { |
| inferPackageName(); |
| skipTestIfPackageNotInstalled(); |
| findTestApexFile(); |
| prepareOverlayfs(); |
| } |
| |
| /** Set mPackageName and mExpectInstallSuccess according to mFileName. */ |
| private void inferPackageName() throws Exception { |
| if (mFileName.endsWith(HIGH_SUFFIX)) { |
| mPackageName = mFileName.substring(0, mFileName.length() - HIGH_SUFFIX.length()); |
| mExpectInstallSuccess = true; |
| } else if (mFileName.endsWith(LOW_SUFFIX)) { |
| mPackageName = mFileName.substring(0, mFileName.length() - LOW_SUFFIX.length()); |
| mExpectInstallSuccess = false; |
| } else { |
| fail("Unrecognized test data file: " + mFileName); |
| } |
| } |
| |
| /** Skip the test if mPackageName is not installed on the device. */ |
| private void skipTestIfPackageNotInstalled() throws Exception { |
| CLog.i("Wait for device to be available for %d ms...", DEVICE_AVAIL_TIMEOUT_MS); |
| getDevice().waitForDeviceAvailable(DEVICE_AVAIL_TIMEOUT_MS); |
| CLog.i("Device is available after %d ms", DEVICE_AVAIL_TIMEOUT_MS); |
| |
| // Skip if the device does not support this APEX package. |
| CLog.i("Checking if %s is installed on the device.", mPackageName); |
| ApexInfo oldApexInfo = getApexInfo(getDevice(), mPackageName); |
| assumeThat(oldApexInfo, is(notNullValue())); |
| assumeThat(oldApexInfo.name, is(mPackageName)); |
| } |
| |
| /** Find the corresponding APEX test file with mFileName. */ |
| private void findTestApexFile() throws Exception { |
| // Find the APEX file. |
| CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(getBuild()); |
| mApexFile = buildHelper.getTestFile(mFileName); |
| |
| // There may be empty .apex files in the directory for disabled APEXes. But if the device |
| // is known to install the package, the test must be built with non-empty APEXes for this |
| // particular package. |
| assertThat("Test is not built properly. It does not contain a non-empty " + mFileName, |
| mApexFile, is(aFileWithSize(greaterThan(0L)))); |
| } |
| |
| /** |
| * Record what partitions have overlayfs set up. Then, tear down overlayfs because it may |
| * make OTA fail. |
| * |
| * Usually, the test does not require root to run, but if the device has overlayfs set up, |
| * the test assumes that the device has root functionality, and attempts to tear down |
| * overlayfs before the test starts. |
| * Note that this function immediately reboots after enabling adb root to ensure the test runs |
| * with the same permission before it is called. |
| */ |
| private void prepareOverlayfs() throws Exception { |
| mOverlayfs.addAll(getOverlayfsState(getDevice())); |
| |
| if (!mOverlayfs.isEmpty()) { |
| getDevice().enableAdbRoot(); |
| getDevice().executeAdbCommand("enable-verity"); |
| rebootUntilAvailable(getDevice(), DEVICE_AVAIL_TIMEOUT_MS); |
| } |
| } |
| |
| @Test |
| public void testInstallAndReboot() throws Exception { |
| CLog.i("Installing %s with %d ms timeout", mApexFile, INSTALL_TIMEOUT_MS); |
| String result = getDevice().installPackage(mApexFile, false, |
| "--staged-ready-timeout", String.valueOf(INSTALL_TIMEOUT_MS)); |
| if (!mExpectInstallSuccess) { |
| assertNotNull("Should not be able to install downgrade package", result); |
| assertThat(result, containsString("Downgrade of APEX package " + mPackageName + |
| " is not allowed.")); |
| return; |
| } |
| |
| assertNull("Installation failed with " + result, result); |
| rebootUntilAvailable(getDevice(), DEVICE_AVAIL_TIMEOUT_MS); |
| |
| ApexInfo newApexInfo = getApexInfo(getDevice(), mPackageName); |
| assertNotNull(newApexInfo); |
| assertThat(newApexInfo.versionCode, is(TEST_HIGH_VERSION)); |
| } |
| |
| /** |
| * Restore overlayfs on partitions. |
| * |
| * Usually, tearDown() does not require root to run, but if the device had overlayfs set up |
| * before the test has started, |
| * the test assumes that the device has root functionality, and attempts to re-set up |
| * overlayfs after the test ends. |
| * Note that tearDown() immediately reboots after enabling adb root to ensure the test ends up |
| * with the same permission before the test has started. |
| */ |
| @After |
| public void tearDown() throws Exception { |
| // Restore overlayfs for partitions that the test knows of. |
| CLog.i("Test ends, now restoring overlayfs partitions %s.", mOverlayfs); |
| if (mOverlayfs.contains("system")) { |
| getDevice().enableAdbRoot(); |
| getDevice().remountSystemWritable(); |
| } |
| if (mOverlayfs.contains("vendor")) { |
| getDevice().enableAdbRoot(); |
| getDevice().remountVendorWritable(); |
| } |
| CLog.i("Restoring overlayfs partition ends, now rebooting."); |
| |
| // Reboot device no matter what to avoid interference. |
| rebootUntilAvailable(getDevice(), DEVICE_AVAIL_TIMEOUT_MS); |
| |
| // remount*Writable should have enabled overlayfs for all necessary partitions. If not, |
| // throw an error. |
| Set<String> newOverlayfsState = getOverlayfsState(getDevice()); |
| assertThat("Some partitions did not restore overlayfs properly. Before test: " + mOverlayfs |
| + ", after test: " + newOverlayfsState, mOverlayfs, |
| everyItem(isIn(newOverlayfsState))); |
| CLog.i("All overlayfs states are restored."); |
| } |
| |
| /** |
| * @param device the device under test |
| * @param packageName the package name to look for |
| * @return The {@link ApexInfo} of the APEX named {@code packageName} on the |
| * {@code device}, or {@code null} if the device does not have the APEX installed. |
| * @throws Exception an error has occurred. |
| */ |
| private static ApexInfo getApexInfo(ITestDevice device, String packageName) |
| throws Exception { |
| assertNotNull(packageName); |
| List<ApexInfo> list = device.getActiveApexes().stream().filter( |
| apexInfo -> packageName.equals(apexInfo.name)).collect(toList()); |
| if (list.isEmpty()) return null; |
| assertThat(list.size(), is(1)); |
| return list.get(0); |
| } |
| |
| /** |
| * Similar to device.reboot(), but with a timeout on waitForDeviceAvailable. Note that |
| * the timeout does not include the rebootUntilOnline() call. |
| * |
| * @param device the device under test |
| * @param timeoutMs timeout for waitForDeviceAvailable() call |
| * @throws Exception an error has occurred. |
| */ |
| private static void rebootUntilAvailable(ITestDevice device, long timeoutMs) |
| throws Exception { |
| CLog.i("Reboot and waiting for device to be online"); |
| device.rebootUntilOnline(); |
| CLog.i("Device online, wait for device to be available for %d ms...", timeoutMs); |
| device.waitForDeviceAvailable(timeoutMs); |
| CLog.i("Device is available after %d ms", timeoutMs); |
| } |
| |
| /** |
| * Get all partitions that have overlayfs setup. Parse /proc/mounts and if it finds lines like: |
| * {@code overlayfs /vendor ...}, then put {@code vendor} in the returned set. |
| * @param device the device under test |
| * @return a list of partitions like {@code system}, {@code vendor} that has overlayfs set up |
| * @throws Exception an error has occurred. |
| */ |
| private static Set<String> getOverlayfsState(ITestDevice device) throws Exception { |
| Set<String> ret = new HashSet(); |
| File mounts = device.pullFile("/proc/mounts"); |
| try (Scanner scanner = new Scanner(mounts)) { |
| while (scanner.hasNextLine()) { |
| String line = scanner.nextLine(); |
| String[] tokens = line.split("\\s"); |
| if (tokens.length < 2) continue; |
| if (!"overlay".equals(tokens[0])) continue; |
| Path path = Paths.get(tokens[1]); |
| if (path.getNameCount() == 0) continue; |
| ret.add(path.getName(0).toString()); |
| } |
| } |
| CLog.i("Device has overlayfs set up on partitions %s", ret); |
| return ret; |
| } |
| } |