1 /*
2  * Copyright (C) 2019 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.tests.util;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 
21 import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey;
22 import com.android.tradefed.build.IBuildInfo;
23 import com.android.tradefed.device.DeviceNotAvailableException;
24 import com.android.tradefed.device.ITestDevice;
25 import com.android.tradefed.device.ITestDevice.ApexInfo;
26 import com.android.tradefed.log.LogUtil.CLog;
27 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
28 import com.android.tradefed.util.CommandResult;
29 import com.android.tradefed.util.CommandStatus;
30 import com.android.tradefed.util.FileUtil;
31 import com.android.tradefed.util.IRunUtil;
32 import com.android.tradefed.util.RunUtil;
33 import com.android.tradefed.util.SystemUtil.EnvVariable;
34 
35 import com.google.common.base.Stopwatch;
36 
37 import org.junit.Assert;
38 
39 import java.io.File;
40 import java.io.IOException;
41 import java.time.Duration;
42 import java.util.regex.Matcher;
43 import java.util.regex.Pattern;
44 import java.util.stream.Stream;
45 
46 public class ModuleTestUtils {
47     private static final String SHIM = "com.android.apex.cts.shim";
48     private static final String APEX_INFO_EXTRACT_REGEX =
49             ".*package:\\sname='(\\S+)\\'\\sversionCode='(\\d+)'\\s.*";
50 
51     private static final Duration WAIT_FOR_SESSION_READY_TTL = Duration.ofSeconds(10);
52     private static final Duration SLEEP_FOR = Duration.ofMillis(200);
53 
54     protected final Pattern mIsSessionReadyPattern =
55             Pattern.compile("(isReady = true)|(isStagedSessionReady = true)");
56     protected final Pattern mIsSessionAppliedPattern =
57             Pattern.compile("(isApplied = true)|(isStagedSessionApplied = true)");
58 
59     private IRunUtil mRunUtil = new RunUtil();
60     private BaseHostJUnit4Test mTest;
61 
getBuild()62     private IBuildInfo getBuild() {
63         return mTest.getBuild();
64     }
65 
ModuleTestUtils(BaseHostJUnit4Test test)66     public ModuleTestUtils(BaseHostJUnit4Test test) {
67         mTest = test;
68     }
69 
70     /**
71      * Retrieve package name and version code from test apex file.
72      *
73      * @param apex input apex file to retrieve the info from
74      */
getApexInfo(File apex)75     public ApexInfo getApexInfo(File apex) {
76         String aaptOutput = runCmd(String.format(
77                 "aapt dump badging %s", apex.getAbsolutePath()));
78         String[] lines = aaptOutput.split("\n");
79         Pattern p = Pattern.compile(APEX_INFO_EXTRACT_REGEX);
80         for (String l : lines) {
81             Matcher m = p.matcher(l);
82             if (m.matches()) {
83                 ApexInfo apexInfo = new ApexInfo(m.group(1), Long.parseLong(m.group(2)));
84                 return apexInfo;
85             }
86         }
87         return null;
88     }
89 
90     /**
91      * Get the test file.
92      *
93      * @param testFileName name of the file
94      */
getTestFile(String testFileName)95     public File getTestFile(String testFileName) throws IOException {
96         File testFile = null;
97 
98         String testcasesPath = System.getenv(EnvVariable.ANDROID_HOST_OUT_TESTCASES.toString());
99         if (testcasesPath != null) {
100             testFile = searchTestFile(new File(testcasesPath), testFileName);
101         }
102         if (testFile != null) {
103             return testFile;
104         }
105 
106         File hostLinkedDir = getBuild().getFile(BuildInfoFileKey.HOST_LINKED_DIR);
107         if (hostLinkedDir != null) {
108             testFile = searchTestFile(hostLinkedDir, testFileName);
109         }
110         if (testFile != null) {
111             return testFile;
112         }
113 
114         // Find the file in the buildinfo.
115         File buildInfoFile = getBuild().getFile(testFileName);
116         if (buildInfoFile != null) {
117             return buildInfoFile;
118         }
119 
120         throw new IOException("Cannot find " + testFileName);
121     }
122 
runCmd(String cmd)123     private String runCmd(String cmd) {
124         CLog.d("About to run command: %s", cmd);
125         CommandResult result = mRunUtil.runTimedCmd(1000 * 60 * 5, cmd.split("\\s+"));
126         Assert.assertNotNull(result);
127         Assert.assertTrue(
128                 String.format("Command %s failed", cmd),
129                 result.getStatus().equals(CommandStatus.SUCCESS));
130         CLog.v("output:\n%s", result.getStdout());
131         return result.getStdout();
132     }
133 
134     /**
135      * Searches the file with the given name under the given directory, returns null if not found.
136      */
searchTestFile(File baseSearchFile, String testFileName)137     private File searchTestFile(File baseSearchFile, String testFileName) {
138         if (baseSearchFile != null && baseSearchFile.isDirectory()) {
139             File testFile = FileUtil.findFile(baseSearchFile, testFileName);
140             if (testFile != null && testFile.isFile()) {
141                 return testFile;
142             }
143         }
144         return null;
145     }
146 
waitForStagedSessionReady()147     public void waitForStagedSessionReady() throws DeviceNotAvailableException {
148         // TODO: implement wait for session ready logic inside PackageManagerShellCommand instead.
149         boolean sessionReady = false;
150         Duration spentWaiting = Duration.ZERO;
151         while (spentWaiting.compareTo(WAIT_FOR_SESSION_READY_TTL) < 0) {
152             CommandResult res = mTest.getDevice().executeShellV2Command("pm get-stagedsessions");
153             Assert.assertEquals("", res.getStderr());
154             sessionReady = Stream.of(res.getStdout().split("\n")).anyMatch(this::isReadyNotApplied);
155             if (sessionReady) {
156                 CLog.i("Done waiting after " + spentWaiting);
157                 break;
158             }
159             try {
160                 Thread.sleep(SLEEP_FOR.toMillis());
161                 spentWaiting = spentWaiting.plus(SLEEP_FOR);
162             } catch (InterruptedException e) {
163                 Thread.currentThread().interrupt();
164                 throw new RuntimeException(e);
165             }
166         }
167         Assert.assertTrue("Staged session wasn't ready in " + WAIT_FOR_SESSION_READY_TTL,
168                 sessionReady);
169     }
170 
171     /**
172      * Abandons any staged session that is marked {@code ready}
173      */
abandonActiveStagedSession()174     public void abandonActiveStagedSession() throws DeviceNotAvailableException {
175         CommandResult res = mTest.getDevice().executeShellV2Command("pm list staged-sessions "
176                 + "--only-ready --only-parent --only-sessionid");
177         assertThat(res.getStderr()).isEqualTo("");
178         String activeSessionId = res.getStdout();
179         if (activeSessionId != null && !activeSessionId.equals("")) {
180             res = mTest.getDevice().executeShellV2Command("pm install-abandon "
181                     + activeSessionId);
182             if (!res.getStderr().equals("") || res.getStatus() != CommandStatus.SUCCESS) {
183                 CLog.d("Failed to abandon session " + activeSessionId
184                         + " Error: " + res.getStderr());
185             }
186         }
187     }
188 
189     /**
190      * Uninstalls a shim apex only if its latest version is installed on /data partition
191      *
192      * <p>This is purely to optimize tests run time, since uninstalling an apex requires a reboot.
193      */
uninstallShimApexIfNecessary()194     public void uninstallShimApexIfNecessary() throws Exception {
195         if (!isApexUpdateSupported()) {
196             return;
197         }
198 
199         final ITestDevice.ApexInfo shim = getShimApex();
200         if (shim.sourceDir.startsWith("/system")) {
201             CLog.i("Skipping uninstall of " + shim.sourceDir + ". Reason: pre-installed version");
202             return;
203         }
204         CLog.i("Uninstalling shim apex");
205         final String errorMessage = mTest.getDevice().uninstallPackage(SHIM);
206         if (errorMessage == null) {
207             mTest.getDevice().reboot();
208         } else {
209             CLog.w("Failed to uninstall shim APEX: " + errorMessage);
210         }
211         assertThat(getShimApex().versionCode).isEqualTo(1L);
212     }
213 
getShimApex()214     private ITestDevice.ApexInfo getShimApex() throws DeviceNotAvailableException {
215         return mTest.getDevice().getActiveApexes().stream().filter(
216                 apex -> apex.name.equals(SHIM)).findAny().orElseThrow(
217                     () -> new AssertionError("Can't find " + SHIM));
218     }
219 
220     /**
221      * Return {@code true} if and only if device supports updating apex.
222      */
isApexUpdateSupported()223     public boolean isApexUpdateSupported() throws Exception {
224         return mTest.getDevice().getBooleanProperty("ro.apex.updatable", false);
225     }
226 
isReadyNotApplied(String sessionInfo)227     private boolean isReadyNotApplied(String sessionInfo) {
228         boolean isReady = mIsSessionReadyPattern.matcher(sessionInfo).find();
229         boolean isApplied = mIsSessionAppliedPattern.matcher(sessionInfo).find();
230         return isReady && !isApplied;
231     }
232 
233     /**
234      * Waits for given {@code timeout} for {@code filePath} to be deleted.
235      */
waitForFileDeleted(String filePath, Duration timeout)236     public void waitForFileDeleted(String filePath, Duration timeout) throws Exception {
237         Stopwatch stopwatch = Stopwatch.createStarted();
238         while (true) {
239             if (!mTest.getDevice().doesFileExist(filePath)) {
240                 return;
241             }
242             if (stopwatch.elapsed().compareTo(timeout) > 0) {
243                 break;
244             }
245             Thread.sleep(500);
246         }
247         throw new AssertionError("Timed out waiting for " + filePath + " to be deleted");
248     }
249 }
250