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.gki.tests; 18 19 import static org.hamcrest.Matchers.anyOf; 20 import static org.hamcrest.Matchers.containsString; 21 import static org.hamcrest.Matchers.either; 22 import static org.hamcrest.Matchers.everyItem; 23 import static org.hamcrest.Matchers.greaterThan; 24 import static org.hamcrest.Matchers.is; 25 import static org.hamcrest.Matchers.isIn; 26 import static org.hamcrest.Matchers.notNullValue; 27 import static org.hamcrest.io.FileMatchers.aFileWithSize; 28 import static org.junit.Assert.assertNotNull; 29 import static org.junit.Assert.assertNull; 30 import static org.junit.Assert.assertThat; 31 import static org.junit.Assert.assertTrue; 32 import static org.junit.Assume.assumeThat; 33 import static org.junit.Assert.fail; 34 import static org.junit.Assume.assumeTrue; 35 36 import static java.util.stream.Collectors.toList; 37 38 import android.cts.host.utils.DeviceJUnit4ClassRunnerWithParameters; 39 import android.cts.host.utils.DeviceJUnit4Parameterized; 40 41 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; 42 import com.android.tradefed.device.ITestDevice; 43 import com.android.tradefed.device.ITestDevice.ApexInfo; 44 import com.android.tradefed.log.LogUtil.CLog; 45 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; 46 47 import org.junit.After; 48 import org.junit.Before; 49 import org.junit.Test; 50 import org.junit.runner.RunWith; 51 import org.junit.runners.Parameterized.Parameter; 52 import org.junit.runners.Parameterized.Parameters; 53 import org.junit.runners.Parameterized.UseParametersRunnerFactory; 54 55 import java.nio.file.Path; 56 import java.nio.file.Paths; 57 import java.io.File; 58 import java.util.ArrayList; 59 import java.util.HashSet; 60 import java.util.List; 61 import java.util.Set; 62 import java.util.Scanner; 63 import java.util.regex.Matcher; 64 import java.util.regex.Pattern; 65 66 @RunWith(DeviceJUnit4Parameterized.class) 67 @UseParametersRunnerFactory(DeviceJUnit4ClassRunnerWithParameters.RunnerFactory.class) 68 public class GkiInstallTest extends BaseHostJUnit4Test { 69 70 // Keep in sync with gki.go. 71 private static final String HIGH_SUFFIX = "_test_high.apex"; 72 private static final String LOW_SUFFIX = "_test_low.apex"; 73 private static final long TEST_HIGH_VERSION = 1000000000L; 74 75 // Timeout between device online for adb commands and boot completed flag is set. 76 private static final long DEVICE_AVAIL_TIMEOUT_MS = 180000; // 3mins 77 // Timeout for `adb install`. 78 private static final long INSTALL_TIMEOUT_MS = 600000; // 10mins 79 80 @Parameter 81 public String mFileName; 82 83 private String mPackageName; 84 private File mApexFile; 85 private boolean mExpectInstallSuccess; 86 private final Set<String> mOverlayfs = new HashSet(); 87 88 @Parameters(name = "{0}") getTestFileNames()89 public static Iterable<String> getTestFileNames() { 90 try (Scanner scanner = new Scanner( 91 GkiInstallTest.class.getClassLoader().getResourceAsStream( 92 "gki_install_test_file_list.txt"))) { 93 List<String> list = new ArrayList<>(); 94 scanner.forEachRemaining(list::add); 95 return list; 96 } 97 } 98 99 @Before setUp()100 public void setUp() throws Exception { 101 inferPackageName(); 102 skipTestIfPackageNotInstalled(); 103 skipTestIfWrongKernelVersion(); 104 findTestApexFile(); 105 prepareOverlayfs(); 106 } 107 108 /** Set mPackageName and mExpectInstallSuccess according to mFileName. */ inferPackageName()109 private void inferPackageName() throws Exception { 110 if (mFileName.endsWith(HIGH_SUFFIX)) { 111 mPackageName = mFileName.substring(0, mFileName.length() - HIGH_SUFFIX.length()); 112 mExpectInstallSuccess = true; 113 } else if (mFileName.endsWith(LOW_SUFFIX)) { 114 mPackageName = mFileName.substring(0, mFileName.length() - LOW_SUFFIX.length()); 115 mExpectInstallSuccess = false; 116 } else { 117 fail("Unrecognized test data file: " + mFileName); 118 } 119 } 120 121 /** Skip the test if mPackageName is not installed on the device. */ skipTestIfPackageNotInstalled()122 private void skipTestIfPackageNotInstalled() throws Exception { 123 CLog.i("Wait for device to be available for %d ms...", DEVICE_AVAIL_TIMEOUT_MS); 124 getDevice().waitForDeviceAvailable(DEVICE_AVAIL_TIMEOUT_MS); 125 CLog.i("Device is available after %d ms", DEVICE_AVAIL_TIMEOUT_MS); 126 127 // Skip if the device does not support this APEX package. 128 CLog.i("Checking if %s is installed on the device.", mPackageName); 129 ApexInfo oldApexInfo = getApexInfo(getDevice(), mPackageName); 130 assumeThat(oldApexInfo, is(notNullValue())); 131 assumeThat(oldApexInfo.name, is(mPackageName)); 132 } 133 134 /** 135 * Skip the test if APEX package name does not match kernel version. 136 * 137 * Due to b/186566367, on mixed builds, the wrong GKI APEX may be installed. In that case, just 138 * skip the test. 139 * 140 * As an exception, the package may contain "unstable" as the generation. When this is the 141 * case, any generation number in kernel release is considered a match. 142 * 143 * @throws Exception 144 */ skipTestIfWrongKernelVersion()145 private void skipTestIfWrongKernelVersion() throws Exception { 146 Pattern packagePattern = Pattern.compile( 147 "^com\\.android\\.gki\\.kmi_(?<w>\\d+)_(?<x>\\d+)_(?<z>android\\d+)_" + 148 "(?<k>\\d+|unstable)$"); 149 Matcher packageMatcher = packagePattern.matcher(mPackageName); 150 assertTrue(packageMatcher.matches()); 151 152 Pattern kernelPattern = Pattern.compile( 153 "^Linux version (?<fullrel>(?<w>\\d+)\\.(?<x>\\d+)\\.(?<y>\\d+)-(?<z>android\\d+)" 154 + "-(?<k>\\d+))"); 155 String kernel = getDevice().executeShellCommand("cat /proc/version"); 156 Matcher kernelMatcher = kernelPattern.matcher(kernel); 157 assumeTrue("Not GKI: " + kernel, kernelMatcher.find()); 158 159 String desc = String.format("package %s vs kernel release %s", mPackageName, 160 kernelMatcher.group("fullrel")); 161 162 CLog.i("Checking: %s", desc); 163 164 assumeThat(desc, packageMatcher.group("w"), is(kernelMatcher.group("w"))); 165 assumeThat(desc, packageMatcher.group("x"), is(kernelMatcher.group("x"))); 166 assumeThat(desc, packageMatcher.group("z"), is(kernelMatcher.group("z"))); 167 assumeThat(desc, packageMatcher.group("k"), 168 anyOf(is("unstable"), is(kernelMatcher.group("k")))); 169 } 170 171 /** Find the corresponding APEX test file with mFileName. */ findTestApexFile()172 private void findTestApexFile() throws Exception { 173 // Find the APEX file. 174 CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(getBuild()); 175 mApexFile = buildHelper.getTestFile(mFileName); 176 177 // There may be empty .apex files in the directory for disabled APEXes. But if the device 178 // is known to install the package, the test must be built with non-empty APEXes for this 179 // particular package. 180 assertThat("Test is not built properly. It does not contain a non-empty " + mFileName, 181 mApexFile, is(aFileWithSize(greaterThan(0L)))); 182 } 183 184 /** 185 * Record what partitions have overlayfs set up. Then, tear down overlayfs because it may 186 * make OTA fail. 187 * 188 * Usually, the test does not require root to run, but if the device has overlayfs set up, 189 * the test assumes that the device has root functionality, and attempts to tear down 190 * overlayfs before the test starts. 191 * Note that this function immediately reboots after enabling adb root to ensure the test runs 192 * with the same permission before it is called. 193 */ prepareOverlayfs()194 private void prepareOverlayfs() throws Exception { 195 mOverlayfs.addAll(getOverlayfsState(getDevice())); 196 197 if (!mOverlayfs.isEmpty()) { 198 getDevice().enableAdbRoot(); 199 getDevice().executeAdbCommand("enable-verity"); 200 rebootUntilAvailable(getDevice(), DEVICE_AVAIL_TIMEOUT_MS); 201 } 202 } 203 204 @Test testInstallAndReboot()205 public void testInstallAndReboot() throws Exception { 206 CLog.i("Installing %s with %d ms timeout", mApexFile, INSTALL_TIMEOUT_MS); 207 String result = getDevice().installPackage(mApexFile, false, 208 "--staged-ready-timeout", String.valueOf(INSTALL_TIMEOUT_MS)); 209 if (!mExpectInstallSuccess) { 210 assertNotNull("Should not be able to install downgrade package", result); 211 assertThat(result, either(containsString("Downgrade of APEX package " + mPackageName 212 + " is not allowed.")).or(containsString("INSTALL_FAILED_VERSION_DOWNGRADE"))); 213 return; 214 } 215 216 assertNull("Installation failed with " + result, result); 217 rebootUntilAvailable(getDevice(), DEVICE_AVAIL_TIMEOUT_MS); 218 219 ApexInfo newApexInfo = getApexInfo(getDevice(), mPackageName); 220 assertNotNull(newApexInfo); 221 assertThat(newApexInfo.versionCode, is(TEST_HIGH_VERSION)); 222 } 223 224 /** 225 * Restore overlayfs on partitions. 226 * 227 * Usually, tearDown() does not require root to run, but if the device had overlayfs set up 228 * before the test has started, 229 * the test assumes that the device has root functionality, and attempts to re-set up 230 * overlayfs after the test ends. 231 * Note that tearDown() immediately reboots after enabling adb root to ensure the test ends up 232 * with the same permission before the test has started. 233 */ 234 @After tearDown()235 public void tearDown() throws Exception { 236 // Restore overlayfs for partitions that the test knows of. 237 CLog.i("Test ends, now restoring overlayfs partitions %s.", mOverlayfs); 238 if (mOverlayfs.contains("system")) { 239 getDevice().enableAdbRoot(); 240 getDevice().remountSystemWritable(); 241 } 242 if (mOverlayfs.contains("vendor")) { 243 getDevice().enableAdbRoot(); 244 getDevice().remountVendorWritable(); 245 } 246 CLog.i("Restoring overlayfs partition ends, now rebooting."); 247 248 // Reboot device no matter what to avoid interference. 249 rebootUntilAvailable(getDevice(), DEVICE_AVAIL_TIMEOUT_MS); 250 251 // remount*Writable should have enabled overlayfs for all necessary partitions. If not, 252 // throw an error. 253 Set<String> newOverlayfsState = getOverlayfsState(getDevice()); 254 assertThat("Some partitions did not restore overlayfs properly. Before test: " + mOverlayfs 255 + ", after test: " + newOverlayfsState, mOverlayfs, 256 everyItem(isIn(newOverlayfsState))); 257 CLog.i("All overlayfs states are restored."); 258 } 259 260 /** 261 * @param device the device under test 262 * @param packageName the package name to look for 263 * @return The {@link ApexInfo} of the APEX named {@code packageName} on the 264 * {@code device}, or {@code null} if the device does not have the APEX installed. 265 * @throws Exception an error has occurred. 266 */ getApexInfo(ITestDevice device, String packageName)267 private static ApexInfo getApexInfo(ITestDevice device, String packageName) 268 throws Exception { 269 assertNotNull(packageName); 270 List<ApexInfo> list = device.getActiveApexes().stream().filter( 271 apexInfo -> packageName.equals(apexInfo.name)).collect(toList()); 272 if (list.isEmpty()) return null; 273 assertThat(list.size(), is(1)); 274 return list.get(0); 275 } 276 277 /** 278 * Similar to device.reboot(), but with a timeout on waitForDeviceAvailable. Note that 279 * the timeout does not include the rebootUntilOnline() call. 280 * 281 * @param device the device under test 282 * @param timeoutMs timeout for waitForDeviceAvailable() call 283 * @throws Exception an error has occurred. 284 */ rebootUntilAvailable(ITestDevice device, long timeoutMs)285 private static void rebootUntilAvailable(ITestDevice device, long timeoutMs) 286 throws Exception { 287 CLog.i("Reboot and waiting for device to be online"); 288 device.rebootUntilOnline(); 289 CLog.i("Device online, wait for device to be available for %d ms...", timeoutMs); 290 device.waitForDeviceAvailable(timeoutMs); 291 CLog.i("Device is available after %d ms", timeoutMs); 292 } 293 294 /** 295 * Get all partitions that have overlayfs setup. Parse /proc/mounts and if it finds lines like: 296 * {@code overlayfs /vendor ...}, then put {@code vendor} in the returned set. 297 * @param device the device under test 298 * @return a list of partitions like {@code system}, {@code vendor} that has overlayfs set up 299 * @throws Exception an error has occurred. 300 */ getOverlayfsState(ITestDevice device)301 private static Set<String> getOverlayfsState(ITestDevice device) throws Exception { 302 Set<String> ret = new HashSet(); 303 File mounts = device.pullFile("/proc/mounts"); 304 try (Scanner scanner = new Scanner(mounts)) { 305 while (scanner.hasNextLine()) { 306 String line = scanner.nextLine(); 307 String[] tokens = line.split("\\s"); 308 if (tokens.length < 2) continue; 309 if (!"overlay".equals(tokens[0])) continue; 310 Path path = Paths.get(tokens[1]); 311 if (path.getNameCount() == 0) continue; 312 ret.add(path.getName(0).toString()); 313 } 314 } 315 CLog.i("Device has overlayfs set up on partitions %s", ret); 316 return ret; 317 } 318 } 319