Create VirtualizationTestCaseBase
The base case comes with reusable test methods, extracted from
MicrodroidTestCase.
Bug: 191056545
Bug: 182478337
Test: atest MicrodroidTestCase
Change-Id: I7506c757978cc6d7146e9ade35dd734f5157d235
diff --git a/tests/hostside/helper/Android.bp b/tests/hostside/helper/Android.bp
new file mode 100644
index 0000000..05742a0
--- /dev/null
+++ b/tests/hostside/helper/Android.bp
@@ -0,0 +1,11 @@
+java_test_helper_library {
+ name: "VirtualizationTestHelper",
+ host_supported: true,
+ device_supported: false,
+ srcs: ["java/**/*.java"],
+ test_suites: ["device-tests"],
+ libs: [
+ "tradefed",
+ "compatibility-tradefed",
+ ],
+}
diff --git a/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java b/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
new file mode 100644
index 0000000..dccdca8
--- /dev/null
+++ b/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.virt.test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeThat;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.RunUtil;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public abstract class VirtualizationTestCaseBase extends BaseHostJUnit4Test {
+ private static final String TEST_ROOT = "/data/local/tmp/virt/";
+ private static final String VIRT_APEX = "/apex/com.android.virt/";
+ private static final int TEST_VM_ADB_PORT = 8000;
+ private static final String MICRODROID_SERIAL = "localhost:" + TEST_VM_ADB_PORT;
+
+ // This is really slow on GCE (2m 40s) but fast on localhost or actual Android phones (< 10s)
+ // Set the maximum timeout value big enough.
+ private static final long MICRODROID_BOOT_TIMEOUT_MINUTES = 5;
+
+ public void prepareVirtualizationTestSetup() throws Exception {
+ // kill stale crosvm processes
+ tryRunOnAndroid("killall", "crosvm");
+
+ // Prepare the test root
+ tryRunOnAndroid("rm", "-rf", TEST_ROOT);
+ tryRunOnAndroid("mkdir", "-p", TEST_ROOT);
+
+ // disconnect from microdroid
+ tryRunOnHost("adb", "disconnect", MICRODROID_SERIAL);
+ }
+
+ public void cleanUpVirtualizationTestSetup() throws Exception {
+ // disconnect from microdroid
+ tryRunOnHost("adb", "disconnect", MICRODROID_SERIAL);
+
+ // kill stale VMs and directories
+ tryRunOnAndroid("killall", "crosvm");
+ tryRunOnAndroid("rm", "-rf", "/data/misc/virtualizationservice/*");
+ tryRunOnAndroid("stop", "virtualizationservice");
+ }
+
+ public void testIfDeviceIsCapable() throws Exception {
+ // Checks the preconditions to run microdroid. If the condition is not satisfied
+ // don't run the test (instead of failing)
+ skipIfFail("ls /dev/kvm");
+ skipIfFail("ls /dev/vhost-vsock");
+ skipIfFail("ls /apex/com.android.virt/bin/crosvm");
+ }
+
+ // Run an arbitrary command in the host side and returns the result
+ private String runOnHost(String... cmd) {
+ return runOnHostWithTimeout(10000, cmd);
+ }
+
+ // Same as runOnHost, but failure is not an error
+ private String tryRunOnHost(String... cmd) {
+ final long timeout = 10000;
+ CommandResult result = RunUtil.getDefault().runTimedCmd(timeout, cmd);
+ return result.getStdout().trim();
+ }
+
+ // Same as runOnHost, but with custom timeout
+ private String runOnHostWithTimeout(long timeoutMillis, String... cmd) {
+ assertTrue(timeoutMillis >= 0);
+ CommandResult result = RunUtil.getDefault().runTimedCmd(timeoutMillis, cmd);
+ assertThat(result.getStatus(), is(CommandStatus.SUCCESS));
+ return result.getStdout().trim();
+ }
+
+ // Run a shell command on Android. the default timeout is 2 min by tradefed
+ private String runOnAndroid(String... cmd) throws Exception {
+ CommandResult result = getDevice().executeShellV2Command(join(cmd));
+ if (result.getStatus() != CommandStatus.SUCCESS) {
+ fail(join(cmd) + " has failed: " + result);
+ }
+ return result.getStdout().trim();
+ }
+
+ // Same as runOnAndroid, but failure is not an error
+ private String tryRunOnAndroid(String... cmd) throws Exception {
+ CommandResult result = getDevice().executeShellV2Command(join(cmd));
+ return result.getStdout().trim();
+ }
+
+ private String runOnAndroidWithTimeout(long timeoutMillis, String... cmd) throws Exception {
+ CommandResult result =
+ getDevice()
+ .executeShellV2Command(
+ join(cmd),
+ timeoutMillis,
+ java.util.concurrent.TimeUnit.MILLISECONDS);
+ if (result.getStatus() != CommandStatus.SUCCESS) {
+ fail(join(cmd) + " has failed: " + result);
+ }
+ return result.getStdout().trim();
+ }
+
+ // Run a shell command on Microdroid
+ public String runOnMicrodroid(String... cmd) {
+ final long timeout = 30000; // 30 sec. Microdroid is extremely slow on GCE-on-CF.
+ CommandResult result =
+ RunUtil.getDefault()
+ .runTimedCmd(timeout, "adb", "-s", MICRODROID_SERIAL, "shell", join(cmd));
+ if (result.getStatus() != CommandStatus.SUCCESS) {
+ fail(join(cmd) + " has failed: " + result);
+ }
+ return result.getStdout().trim();
+ }
+
+ private String join(String... strs) {
+ return String.join(" ", Arrays.asList(strs));
+ }
+
+ private File findTestFile(String name) throws Exception {
+ return (new CompatibilityBuildHelper(getBuild())).getTestFile(name);
+ }
+
+ public String startMicrodroid(String apkName, String packageName, String configPath)
+ throws Exception {
+ // Install APK
+ File apkFile = findTestFile(apkName);
+ getDevice().installPackage(apkFile, /* reinstall */ true);
+
+ // Get the path to the installed apk. Note that
+ // getDevice().getAppPackageInfo(...).getCodePath() doesn't work due to the incorrect
+ // parsing of the "=" character. (b/190975227). So we use the `pm path` command directly.
+ String apkPath = runOnAndroid("pm", "path", packageName);
+ assertTrue(apkPath.startsWith("package:"));
+ apkPath = apkPath.substring("package:".length());
+
+ // Push the idsig file to the device
+ File idsigOnHost = findTestFile(apkName + ".idsig");
+ final String apkIdsigPath = TEST_ROOT + apkName + ".idsig";
+ getDevice().pushFile(idsigOnHost, apkIdsigPath);
+
+ final String logPath = TEST_ROOT + "log.txt";
+
+ // Run the VM
+ runOnAndroid("start", "virtualizationservice");
+ String ret =
+ runOnAndroid(
+ VIRT_APEX + "bin/vm",
+ "run-app",
+ "--daemonize",
+ "--log " + logPath,
+ apkPath,
+ apkIdsigPath,
+ configPath);
+
+ // Redirect log.txt to logd using logwrapper
+ ExecutorService executor = Executors.newFixedThreadPool(1);
+ executor.execute(
+ () -> {
+ try {
+ // Keep redirecting sufficiently long enough
+ runOnAndroidWithTimeout(
+ MICRODROID_BOOT_TIMEOUT_MINUTES * 60 * 1000,
+ "logwrapper",
+ "tail",
+ "-f",
+ "-n +0",
+ logPath);
+ } catch (Exception e) {
+ // Consume
+ }
+ });
+
+ // Retrieve the CID from the vm tool output
+ Pattern pattern = Pattern.compile("with CID (\\d+)");
+ Matcher matcher = pattern.matcher(ret);
+ assertTrue(matcher.find());
+ return matcher.group(1);
+ }
+
+ public void shutdownMicrodroid(String cid) throws Exception {
+ // Shutdown microdroid
+ runOnAndroid(VIRT_APEX + "bin/vm", "stop", cid);
+ }
+
+ // Establish an adb connection to microdroid by letting Android forward the connection to
+ // microdroid. Wait until the connection is established and microdroid is booted.
+ public void adbConnectToMicrodroid(String cid, long timeoutMinutes) throws Exception {
+ long start = System.currentTimeMillis();
+ long timeoutMillis = timeoutMinutes * 60 * 1000;
+ long elapsed = 0;
+
+ final String serial = getDevice().getSerialNumber();
+ final String from = "tcp:" + TEST_VM_ADB_PORT;
+ final String to = "vsock:" + cid + ":5555";
+ runOnHost("adb", "-s", serial, "forward", from, to);
+
+ boolean disconnected = true;
+ while (disconnected) {
+ elapsed = System.currentTimeMillis() - start;
+ timeoutMillis -= elapsed;
+ start = System.currentTimeMillis();
+ String ret = runOnHostWithTimeout(timeoutMillis, "adb", "connect", MICRODROID_SERIAL);
+ disconnected = ret.equals("failed to connect to " + MICRODROID_SERIAL);
+ if (disconnected) {
+ // adb demands us to disconnect if the prior connection was a failure.
+ runOnHost("adb", "disconnect", MICRODROID_SERIAL);
+ }
+ }
+
+ elapsed = System.currentTimeMillis() - start;
+ timeoutMillis -= elapsed;
+ runOnHostWithTimeout(timeoutMillis, "adb", "-s", MICRODROID_SERIAL, "wait-for-device");
+
+ boolean dataAvailable = false;
+ while (!dataAvailable && timeoutMillis >= 0) {
+ elapsed = System.currentTimeMillis() - start;
+ timeoutMillis -= elapsed;
+ start = System.currentTimeMillis();
+ final String checkCmd = "if [ -d /data/local/tmp ]; then echo 1; fi";
+ dataAvailable = runOnMicrodroid(checkCmd).equals("1");
+ }
+
+ // Check if it actually booted by reading a sysprop.
+ assertThat(runOnMicrodroid("getprop", "ro.hardware"), is("microdroid"));
+ }
+
+ private void skipIfFail(String command) throws Exception {
+ CommandResult result = getDevice().executeShellV2Command(command);
+ assumeThat(result.getStatus(), is(CommandStatus.SUCCESS));
+ }
+}