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