• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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.virt.test;
18 
19 import static com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
20 
21 import static org.hamcrest.CoreMatchers.is;
22 import static org.junit.Assert.assertThat;
23 import static org.junit.Assert.assertTrue;
24 import static org.junit.Assert.fail;
25 import static org.junit.Assume.assumeTrue;
26 
27 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
28 import com.android.tradefed.build.IBuildInfo;
29 import com.android.tradefed.device.DeviceNotAvailableException;
30 import com.android.tradefed.device.ITestDevice;
31 import com.android.tradefed.device.TestDevice;
32 import com.android.tradefed.log.LogUtil.CLog;
33 import com.android.tradefed.result.FileInputStreamSource;
34 import com.android.tradefed.result.LogDataType;
35 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
36 import com.android.tradefed.util.CommandResult;
37 import com.android.tradefed.util.CommandStatus;
38 import com.android.tradefed.util.RunUtil;
39 
40 import java.io.File;
41 import java.io.FileNotFoundException;
42 import java.util.ArrayList;
43 import java.util.Arrays;
44 import java.util.Optional;
45 import java.util.concurrent.ExecutorService;
46 import java.util.concurrent.Executors;
47 import java.util.regex.Matcher;
48 import java.util.regex.Pattern;
49 
50 public abstract class VirtualizationTestCaseBase extends BaseHostJUnit4Test {
51     protected static final String TEST_ROOT = "/data/local/tmp/virt/";
52     protected static final String VIRT_APEX = "/apex/com.android.virt/";
53     protected static final String LOG_PATH = TEST_ROOT + "log.txt";
54     private static final int TEST_VM_ADB_PORT = 8000;
55     private static final String MICRODROID_SERIAL = "localhost:" + TEST_VM_ADB_PORT;
56     private static final String INSTANCE_IMG = "instance.img";
57 
58     // This is really slow on GCE (2m 40s) but fast on localhost or actual Android phones (< 10s).
59     // Then there is time to run the actual task. Set the maximum timeout value big enough.
60     private static final long MICRODROID_MAX_LIFETIME_MINUTES = 20;
61 
62     private static final long MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES = 5;
63 
prepareVirtualizationTestSetup(ITestDevice androidDevice)64     public static void prepareVirtualizationTestSetup(ITestDevice androidDevice)
65             throws DeviceNotAvailableException {
66         CommandRunner android = new CommandRunner(androidDevice);
67 
68         // kill stale crosvm processes
69         android.tryRun("killall", "crosvm");
70 
71         // disconnect from microdroid
72         tryRunOnHost("adb", "disconnect", MICRODROID_SERIAL);
73 
74         // remove any leftover files under test root
75         android.tryRun("rm", "-rf", TEST_ROOT + "*");
76     }
77 
cleanUpVirtualizationTestSetup(ITestDevice androidDevice)78     public static void cleanUpVirtualizationTestSetup(ITestDevice androidDevice)
79             throws DeviceNotAvailableException {
80         CommandRunner android = new CommandRunner(androidDevice);
81 
82         // disconnect from microdroid
83         tryRunOnHost("adb", "disconnect", MICRODROID_SERIAL);
84 
85         // kill stale VMs and directories
86         android.tryRun("killall", "crosvm");
87         android.tryRun("stop", "virtualizationservice");
88         android.tryRun("rm", "-rf", "/data/misc/virtualizationservice/*");
89     }
90 
testIfDeviceIsCapable(ITestDevice androidDevice)91     public static void testIfDeviceIsCapable(ITestDevice androidDevice) throws Exception {
92         assumeTrue("Need an actual TestDevice", androidDevice instanceof TestDevice);
93         TestDevice testDevice = (TestDevice) androidDevice;
94         assumeTrue("Requires VM support", testDevice.supportsMicrodroid());
95     }
96 
archiveLogThenDelete(TestLogData logs, ITestDevice device, String remotePath, String localName)97     public static void archiveLogThenDelete(TestLogData logs, ITestDevice device, String remotePath,
98             String localName) throws DeviceNotAvailableException {
99         File logFile = device.pullFile(remotePath);
100         if (logFile != null) {
101             logs.addTestLog(localName, LogDataType.TEXT, new FileInputStreamSource(logFile));
102             // Delete to avoid confusing logs from a previous run, just in case.
103             device.deleteFile(remotePath);
104         }
105     }
106 
107     // Run an arbitrary command in the host side and returns the result
runOnHost(String... cmd)108     public static String runOnHost(String... cmd) {
109         return runOnHostWithTimeout(10000, cmd);
110     }
111 
112     // Same as runOnHost, but failure is not an error
tryRunOnHost(String... cmd)113     private static String tryRunOnHost(String... cmd) {
114         final long timeout = 10000;
115         CommandResult result = RunUtil.getDefault().runTimedCmd(timeout, cmd);
116         return result.getStdout().trim();
117     }
118 
119     // Same as runOnHost, but with custom timeout
runOnHostWithTimeout(long timeoutMillis, String... cmd)120     private static String runOnHostWithTimeout(long timeoutMillis, String... cmd) {
121         assertTrue(timeoutMillis >= 0);
122         CommandResult result = RunUtil.getDefault().runTimedCmd(timeoutMillis, cmd);
123         assertThat(result.getStatus(), is(CommandStatus.SUCCESS));
124         return result.getStdout().trim();
125     }
126 
127     // Run a shell command on Microdroid
runOnMicrodroid(String... cmd)128     public static String runOnMicrodroid(String... cmd) {
129         CommandResult result = runOnMicrodroidForResult(cmd);
130         if (result.getStatus() != CommandStatus.SUCCESS) {
131             fail(join(cmd) + " has failed: " + result);
132         }
133         return result.getStdout().trim();
134     }
135 
136     // Same as runOnMicrodroid, but keeps retrying on error till timeout
runOnMicrodroidRetryingOnFailure(String... cmd)137     private static String runOnMicrodroidRetryingOnFailure(String... cmd) {
138         final long timeoutMs = 30000; // 30 sec. Microdroid is extremely slow on GCE-on-CF.
139         int attempts = (int) MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES * 60 * 1000 / 500;
140         CommandResult result = RunUtil.getDefault()
141                 .runTimedCmdRetry(timeoutMs, 500, attempts,
142                         "adb", "-s", MICRODROID_SERIAL, "shell", join(cmd));
143         if (result.getStatus() != CommandStatus.SUCCESS) {
144             fail(join(cmd) + " has failed: " + result);
145         }
146         return result.getStdout().trim();
147     }
148 
149     // Same as runOnMicrodroid, but returns null on error.
tryRunOnMicrodroid(String... cmd)150     public static String tryRunOnMicrodroid(String... cmd) {
151         CommandResult result = runOnMicrodroidForResult(cmd);
152         if (result.getStatus() == CommandStatus.SUCCESS) {
153             return result.getStdout().trim();
154         } else {
155             CLog.d(join(cmd) + " has failed (but ok): " + result);
156             return null;
157         }
158     }
159 
runOnMicrodroidForResult(String... cmd)160     public static CommandResult runOnMicrodroidForResult(String... cmd) {
161         final long timeoutMs = 30000; // 30 sec. Microdroid is extremely slow on GCE-on-CF.
162         return RunUtil.getDefault()
163                 .runTimedCmd(timeoutMs, "adb", "-s", MICRODROID_SERIAL, "shell", join(cmd));
164     }
165 
pullMicrodroidFile(String path, File target)166     public static void pullMicrodroidFile(String path, File target) {
167         final long timeoutMs = 30000; // 30 sec. Microdroid is extremely slow on GCE-on-CF.
168         CommandResult result =
169                 RunUtil.getDefault()
170                         .runTimedCmd(
171                                 timeoutMs,
172                                 "adb",
173                                 "-s",
174                                 MICRODROID_SERIAL,
175                                 "pull",
176                                 path,
177                                 target.getPath());
178         if (result.getStatus() != CommandStatus.SUCCESS) {
179             fail("pulling " + path + " has failed: " + result);
180         }
181     }
182 
183     // Asserts the command will fail on Microdroid.
assertFailedOnMicrodroid(String... cmd)184     public static void assertFailedOnMicrodroid(String... cmd) {
185         CommandResult result = runOnMicrodroidForResult(cmd);
186         assertThat(result.getStatus(), is(CommandStatus.FAILED));
187     }
188 
join(String... strs)189     private static String join(String... strs) {
190         return String.join(" ", Arrays.asList(strs));
191     }
192 
findTestFile(String name)193     public File findTestFile(String name) {
194         return findTestFile(getBuild(), name);
195     }
196 
findTestFile(IBuildInfo buildInfo, String name)197     private static File findTestFile(IBuildInfo buildInfo, String name) {
198         try {
199             return (new CompatibilityBuildHelper(buildInfo)).getTestFile(name);
200         } catch (FileNotFoundException e) {
201             fail("Missing test file: " + name);
202             return null;
203         }
204     }
205 
getPathForPackage(String packageName)206     public String getPathForPackage(String packageName)
207             throws DeviceNotAvailableException {
208         return getPathForPackage(getDevice(), packageName);
209     }
210 
211     // Get the path to the installed apk. Note that
212     // getDevice().getAppPackageInfo(...).getCodePath() doesn't work due to the incorrect
213     // parsing of the "=" character. (b/190975227). So we use the `pm path` command directly.
getPathForPackage(ITestDevice device, String packageName)214     private static String getPathForPackage(ITestDevice device, String packageName)
215             throws DeviceNotAvailableException {
216         CommandRunner android = new CommandRunner(device);
217         String pathLine = android.run("pm", "path", packageName);
218         assertTrue("package not found", pathLine.startsWith("package:"));
219         return pathLine.substring("package:".length());
220     }
221 
startMicrodroid( ITestDevice androidDevice, IBuildInfo buildInfo, String apkName, String packageName, String configPath, boolean debug, int memoryMib, Optional<Integer> numCpus, Optional<String> cpuAffinity)222     public static String startMicrodroid(
223             ITestDevice androidDevice,
224             IBuildInfo buildInfo,
225             String apkName,
226             String packageName,
227             String configPath,
228             boolean debug,
229             int memoryMib,
230             Optional<Integer> numCpus,
231             Optional<String> cpuAffinity)
232             throws DeviceNotAvailableException {
233         return startMicrodroid(androidDevice, buildInfo, apkName, packageName, null, configPath,
234                 debug, memoryMib, numCpus, cpuAffinity);
235     }
236 
startMicrodroid( ITestDevice androidDevice, IBuildInfo buildInfo, String apkName, String packageName, String[] extraIdsigPaths, String configPath, boolean debug, int memoryMib, Optional<Integer> numCpus, Optional<String> cpuAffinity)237     public static String startMicrodroid(
238             ITestDevice androidDevice,
239             IBuildInfo buildInfo,
240             String apkName,
241             String packageName,
242             String[] extraIdsigPaths,
243             String configPath,
244             boolean debug,
245             int memoryMib,
246             Optional<Integer> numCpus,
247             Optional<String> cpuAffinity)
248             throws DeviceNotAvailableException {
249         return startMicrodroid(androidDevice, buildInfo, apkName, null, packageName,
250                 extraIdsigPaths, configPath, debug,
251                 memoryMib, numCpus, cpuAffinity);
252     }
253 
startMicrodroid( ITestDevice androidDevice, IBuildInfo buildInfo, String apkName, String apkPath, String packageName, String[] extraIdsigPaths, String configPath, boolean debug, int memoryMib, Optional<Integer> numCpus, Optional<String> cpuAffinity)254     public static String startMicrodroid(
255             ITestDevice androidDevice,
256             IBuildInfo buildInfo,
257             String apkName,
258             String apkPath,
259             String packageName,
260             String[] extraIdsigPaths,
261             String configPath,
262             boolean debug,
263             int memoryMib,
264             Optional<Integer> numCpus,
265             Optional<String> cpuAffinity)
266             throws DeviceNotAvailableException {
267         CommandRunner android = new CommandRunner(androidDevice);
268 
269         // Install APK if necessary
270         if (apkName != null) {
271             File apkFile = findTestFile(buildInfo, apkName);
272             androidDevice.installPackage(apkFile, /* reinstall */ true);
273         }
274 
275         if (apkPath == null) {
276             apkPath = getPathForPackage(androidDevice, packageName);
277         }
278 
279         android.run("mkdir", "-p", TEST_ROOT);
280 
281         // This file is not what we provide. It will be created by the vm tool.
282         final String outApkIdsigPath = TEST_ROOT + apkName + ".idsig";
283 
284         final String instanceImg = TEST_ROOT + INSTANCE_IMG;
285         final String logPath = LOG_PATH;
286         final String debugFlag = debug ? "--debug full" : "";
287 
288         // Run the VM
289         ArrayList<String> args = new ArrayList<>(Arrays.asList(
290                 VIRT_APEX + "bin/vm",
291                 "run-app",
292                 "--daemonize",
293                 "--log " + logPath,
294                 "--mem " + memoryMib,
295                 numCpus.isPresent() ? "--cpus " + numCpus.get() : "",
296                 cpuAffinity.isPresent() ? "--cpu-affinity " + cpuAffinity.get() : "",
297                 debugFlag,
298                 apkPath,
299                 outApkIdsigPath,
300                 instanceImg,
301                 configPath));
302         if (extraIdsigPaths != null) {
303             for (String path : extraIdsigPaths) {
304                 args.add("--extra-idsig");
305                 args.add(path);
306             }
307         }
308         String ret = android.run(args.toArray(new String[0]));
309 
310         // Redirect log.txt to logd using logwrapper
311         ExecutorService executor = Executors.newFixedThreadPool(1);
312         executor.execute(
313                 () -> {
314                     try {
315                         // Keep redirecting as long as the expecting maximum test time. When an adb
316                         // command times out, it may trigger the device recovery process, which
317                         // disconnect adb, which terminates any live adb commands. See an example at
318                         // b/194974010#comment25.
319                         android.runWithTimeout(
320                                 MICRODROID_MAX_LIFETIME_MINUTES * 60 * 1000,
321                                 "logwrapper",
322                                 "tail",
323                                 "-f",
324                                 "-n +0",
325                                 logPath);
326                     } catch (Exception e) {
327                         // Consume
328                     }
329                 });
330 
331         // Retrieve the CID from the vm tool output
332         Pattern pattern = Pattern.compile("with CID (\\d+)");
333         Matcher matcher = pattern.matcher(ret);
334         assertTrue(matcher.find());
335         return matcher.group(1);
336     }
337 
shutdownMicrodroid(ITestDevice androidDevice, String cid)338     public static void shutdownMicrodroid(ITestDevice androidDevice, String cid)
339             throws DeviceNotAvailableException {
340         CommandRunner android = new CommandRunner(androidDevice);
341 
342         // Shutdown the VM
343         android.run(VIRT_APEX + "bin/vm", "stop", cid);
344     }
345 
rootMicrodroid()346     public static void rootMicrodroid() {
347         runOnHost("adb", "-s", MICRODROID_SERIAL, "root");
348         runOnHostWithTimeout(
349                 MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES * 60 * 1000,
350                 "adb",
351                 "-s",
352                 MICRODROID_SERIAL,
353                 "wait-for-device");
354         // There have been tests when adb wait-for-device succeeded but the following command
355         // fails with error: closed. Hence, we run adb shell true in microdroid with retries
356         // before returning.
357         runOnMicrodroidRetryingOnFailure("true");
358     }
359 
360     // Establish an adb connection to microdroid by letting Android forward the connection to
361     // microdroid. Wait until the connection is established and microdroid is booted.
adbConnectToMicrodroid(ITestDevice androidDevice, String cid)362     public static void adbConnectToMicrodroid(ITestDevice androidDevice, String cid) {
363         long start = System.currentTimeMillis();
364         long timeoutMillis = MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES * 60 * 1000;
365         long elapsed = 0;
366 
367         final String serial = androidDevice.getSerialNumber();
368         final String from = "tcp:" + TEST_VM_ADB_PORT;
369         final String to = "vsock:" + cid + ":5555";
370         runOnHost("adb", "-s", serial, "forward", from, to);
371 
372         boolean disconnected = true;
373         while (disconnected) {
374             elapsed = System.currentTimeMillis() - start;
375             timeoutMillis -= elapsed;
376             start = System.currentTimeMillis();
377             String ret = runOnHostWithTimeout(timeoutMillis, "adb", "connect", MICRODROID_SERIAL);
378             disconnected = ret.equals("failed to connect to " + MICRODROID_SERIAL);
379             if (disconnected) {
380                 // adb demands us to disconnect if the prior connection was a failure.
381                 // b/194375443: this somtimes fails, thus 'try*'.
382                 tryRunOnHost("adb", "disconnect", MICRODROID_SERIAL);
383             }
384         }
385 
386         elapsed = System.currentTimeMillis() - start;
387         timeoutMillis -= elapsed;
388         runOnHostWithTimeout(timeoutMillis, "adb", "-s", MICRODROID_SERIAL, "wait-for-device");
389 
390         boolean dataAvailable = false;
391         while (!dataAvailable && timeoutMillis >= 0) {
392             elapsed = System.currentTimeMillis() - start;
393             timeoutMillis -= elapsed;
394             start = System.currentTimeMillis();
395             final String checkCmd = "if [ -d /data/local/tmp ]; then echo 1; fi";
396             dataAvailable = runOnMicrodroid(checkCmd).equals("1");
397         }
398 
399         // Check if it actually booted by reading a sysprop.
400         assertThat(runOnMicrodroid("getprop", "ro.hardware"), is("microdroid"));
401     }
402 
isCuttlefish()403     protected boolean isCuttlefish() throws Exception {
404         return isCuttlefish(getDevice());
405     }
406 
isCuttlefish(ITestDevice device)407     protected static boolean isCuttlefish(ITestDevice device) throws Exception {
408         String productName = device.getProperty("ro.product.name");
409         return (null != productName)
410                 && (productName.startsWith("aosp_cf_x86")
411                         || productName.startsWith("aosp_cf_arm")
412                         || productName.startsWith("cf_x86")
413                         || productName.startsWith("cf_arm"));
414     }
415 }
416