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 android.cts.install.lib.host;
18 
19 import static com.android.cts.shim.lib.ShimPackage.SHIM_APEX_PACKAGE_NAME;
20 
21 import static com.google.common.truth.Truth.assertThat;
22 import static com.google.common.truth.Truth.assertWithMessage;
23 
24 import com.android.ddmlib.Log;
25 import com.android.tradefed.build.BuildInfoKey;
26 import com.android.tradefed.device.DeviceNotAvailableException;
27 import com.android.tradefed.device.ITestDevice;
28 import com.android.tradefed.invoker.TestInformation;
29 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
30 import com.android.tradefed.util.CommandResult;
31 import com.android.tradefed.util.CommandStatus;
32 import com.android.tradefed.util.FileUtil;
33 import com.android.tradefed.util.IRunUtil;
34 import com.android.tradefed.util.RunUtil;
35 import com.android.tradefed.util.SystemUtil;
36 
37 import com.google.common.base.Stopwatch;
38 
39 import java.io.File;
40 import java.io.IOException;
41 import java.time.Duration;
42 import java.util.List;
43 import java.util.Optional;
44 import java.util.regex.Matcher;
45 import java.util.regex.Pattern;
46 
47 /**
48  * Utilities to facilitate installation in tests on host side.
49  */
50 public class InstallUtilsHost {
51     private static final String TAG = InstallUtilsHost.class.getSimpleName();
52     private static final String APEX_INFO_EXTRACT_REGEX =
53             ".*package:\\sname='(\\S+)\\'\\sversionCode='(\\d+)'\\s.*";
54 
55     private final IRunUtil mRunUtil = new RunUtil();
56     private BaseHostJUnit4Test mTest = null;
57     private TestInformation mTestInfo = null;
58 
InstallUtilsHost(BaseHostJUnit4Test test)59     public InstallUtilsHost(BaseHostJUnit4Test test) {
60         mTest = test;
61     }
62 
InstallUtilsHost(TestInformation testInfo)63     public InstallUtilsHost(TestInformation testInfo) {
64         assertThat(testInfo).isNotNull();
65         mTestInfo = testInfo;
66     }
67 
68     /**
69      * Return {@code true} if and only if device supports updating apex.
70      */
isApexUpdateSupported()71     public boolean isApexUpdateSupported() throws Exception {
72         return getTestInfo().getDevice().getBooleanProperty("ro.apex.updatable", false);
73     }
74 
75     /**
76      * Return {@code true} if and only if device supports file system checkpoint.
77      */
isCheckpointSupported()78     public boolean isCheckpointSupported() throws Exception {
79         CommandResult result = getTestInfo().getDevice().executeShellV2Command(
80                 "sm supports-checkpoint");
81         assertWithMessage("Failed to check if file system checkpoint is supported : %s",
82                 result.getStderr()).that(result.getStatus()).isEqualTo(CommandStatus.SUCCESS);
83         return "true".equals(result.getStdout().trim());
84     }
85 
86     /**
87      * Uninstalls a shim apex only if it's latest version is installed on /data partition (i.e.
88      * it has a version higher than {@code 1}).
89      *
90      * <p>This is purely to optimize tests run time. Since uninstalling an apex requires a reboot,
91      * and only a small subset of tests successfully install an apex, this code avoids ~10
92      * unnecessary reboots.
93      */
uninstallShimApexIfNecessary()94     public void uninstallShimApexIfNecessary() throws Exception {
95         final ITestDevice.ApexInfo shimApex = getShimApex().orElseThrow(
96                 () -> new AssertionError("Can't find " + SHIM_APEX_PACKAGE_NAME));
97         if (shimApex.sourceDir.startsWith("/system")) {
98             // System version is active, nothing to uninstall.
99             return;
100         }
101         // Non system version is active, need to uninstall it and reboot the device.
102         Log.i(TAG, "Uninstalling shim apex");
103         final String errorMessage =
104                 getTestInfo().getDevice().uninstallPackage(SHIM_APEX_PACKAGE_NAME);
105         if (errorMessage != null) {
106             Log.e(TAG, "Failed to uninstall " + SHIM_APEX_PACKAGE_NAME + " : " + errorMessage);
107         } else {
108             getTestInfo().getDevice().reboot();
109             final ITestDevice.ApexInfo shim = getShimApex().orElseThrow(
110                     () -> new AssertionError("Can't find " + SHIM_APEX_PACKAGE_NAME));
111             assertThat(shim.versionCode).isEqualTo(1L);
112             assertThat(shim.sourceDir).startsWith("/system");
113         }
114     }
115 
116     /**
117      * Returns the active shim apex as optional.
118      */
getShimApex()119     public Optional<ITestDevice.ApexInfo> getShimApex() throws DeviceNotAvailableException {
120         return getTestInfo().getDevice().getActiveApexes().stream().filter(
121                 apex -> apex.name.equals(SHIM_APEX_PACKAGE_NAME)).findAny();
122     }
123 
124     /**
125      * Retrieve package name and version code from test apex file.
126      *
127      * @param apex input apex file to retrieve the info from
128      */
getApexInfo(File apex)129     public ITestDevice.ApexInfo getApexInfo(File apex) {
130         String aaptOutput = runCmd(String.format("aapt dump badging %s", apex.getAbsolutePath()));
131         String[] lines = aaptOutput.split("\n");
132         Pattern p = Pattern.compile(APEX_INFO_EXTRACT_REGEX);
133         for (String l : lines) {
134             Matcher m = p.matcher(l);
135             if (m.matches()) {
136                 return new ITestDevice.ApexInfo(m.group(1), Long.parseLong(m.group(2)));
137             }
138         }
139         return null;
140     }
141 
142     /**
143      * Installs packages using staged install flow and waits for pre-reboot verification to complete
144      */
installStagedPackage(File pkg)145     public String installStagedPackage(File pkg) throws Exception {
146         return getTestInfo().getDevice().installPackage(pkg, false, "--staged");
147     }
148 
149     /**
150      * Installs packages using rebootless (non-staged) install flow and waits
151      *     for verification to complete
152      */
installRebootlessPackage(File pkg)153     public String installRebootlessPackage(File pkg) throws Exception {
154         return getTestInfo().getDevice().installPackage(pkg, false, "--non-staged");
155     }
156 
157     /**
158      * Install multiple package at the same time
159      */
installApexes(String... filenames)160     public void installApexes(String... filenames) throws Exception {
161         String[] args = new String[filenames.length + 1];
162         args[0] = "install-multi-package";
163         for (int i = 0; i < filenames.length; i++) {
164             args[i + 1] = getTestFile(filenames[i]).getAbsolutePath();
165         }
166         String stdout = getTestInfo().getDevice().executeAdbCommand(args);
167         assertThat(stdout).isNotNull();
168     }
169 
170     /**
171      * Waits for given {@code timeout} for {@code filePath} to be deleted.
172      */
waitForFileDeleted(String filePath, Duration timeout)173     public void waitForFileDeleted(String filePath, Duration timeout) throws Exception {
174         Stopwatch stopwatch = Stopwatch.createStarted();
175         while (true) {
176             if (!getTestInfo().getDevice().doesFileExist(filePath)) {
177                 return;
178             }
179             if (stopwatch.elapsed().compareTo(timeout) > 0) {
180                 break;
181             }
182             Thread.sleep(500);
183         }
184         throw new AssertionError("Timed out waiting for " + filePath + " to be deleted");
185     }
186 
187     /**
188      * Get the test file.
189      *
190      * @param testFileName name of the file
191      */
getTestFile(String testFileName)192     public File getTestFile(String testFileName) throws IOException {
193         File testFile = null;
194 
195         final List<File> testCasesDirs = SystemUtil.getTestCasesDirs(getTestInfo().getBuildInfo());
196         for (File testCasesDir : testCasesDirs) {
197             testFile = searchTestFile(testCasesDir, testFileName);
198             if (testFile != null) {
199                 return testFile;
200             }
201         }
202 
203         File hostLinkedDir = getTestInfo().getBuildInfo().getFile(
204                 BuildInfoKey.BuildInfoFileKey.HOST_LINKED_DIR);
205         if (hostLinkedDir != null) {
206             testFile = searchTestFile(hostLinkedDir, testFileName);
207         }
208         if (testFile != null) {
209             return testFile;
210         }
211 
212         // Find the file in the buildinfo.
213         File buildInfoFile = getTestInfo().getBuildInfo().getFile(testFileName);
214         if (buildInfoFile != null) {
215             return buildInfoFile;
216         }
217 
218         throw new IOException("Cannot find " + testFileName);
219     }
220 
221     /**
222      * Searches the file with the given name under the given directory, returns null if not found.
223      */
searchTestFile(File baseSearchFile, String testFileName)224     private File searchTestFile(File baseSearchFile, String testFileName) {
225         if (baseSearchFile != null && baseSearchFile.isDirectory()) {
226             File testFile = FileUtil.findFile(baseSearchFile, testFileName);
227             if (testFile != null && testFile.isFile()) {
228                 return testFile;
229             }
230         }
231         return null;
232     }
233 
runCmd(String cmd)234     private String runCmd(String cmd) {
235         Log.d("About to run command: %s", cmd);
236         CommandResult result = mRunUtil.runTimedCmd(1000 * 60 * 5, cmd.split("\\s+"));
237         assertThat(result).isNotNull();
238         assertWithMessage(String.format("Command %s failed", cmd)).that(result.getStatus())
239                 .isEqualTo(CommandStatus.SUCCESS);
240         Log.d("output:\n%s", result.getStdout());
241         return result.getStdout();
242     }
243 
getTestInfo()244     private TestInformation getTestInfo() {
245         if (mTestInfo == null) {
246             mTestInfo = mTest.getTestInformation();
247             assertThat(mTestInfo).isNotNull();
248         }
249         return mTestInfo;
250     }
251 }
252