Split userdebug only tests from MicrodroidHostTestCases
Host side pvmfw tests were in MicrodroidHostTestCases and requires
`adb root` for test setup. However, MicrodroidHostTestCases is CTS,
so it should be able to run on user build.
This CL creates CustomPvmfwHostTestCases by splitting the host side
tests requires custom pvmfw to reduce tests failure from unexpected
configurations, and also reduce confusion from manufacturers about
the test failure.
Bug: 297282463
Test: TH, and `atest MicrodroidHostTestCases CustomPvmfwHostTestCases`
Change-Id: I3e44e5f525763deca498458067b972e875cbf5e8
diff --git a/tests/pvmfw/Android.bp b/tests/pvmfw/Android.bp
new file mode 100644
index 0000000..61667f3
--- /dev/null
+++ b/tests/pvmfw/Android.bp
@@ -0,0 +1,46 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+genrule_defaults {
+ name: "test_avf_dts_to_dtb",
+ tools: ["dtc"],
+ cmd: "$(location dtc) -I dts -O dtb $(in) -o $(out)",
+}
+
+genrule {
+ name: "test_avf_debug_policy_with_adb",
+ defaults: ["test_avf_dts_to_dtb"],
+ srcs: ["assets/avf_debug_policy_with_adb.dts"],
+ out: ["avf_debug_policy_with_adb.dtbo"],
+}
+
+genrule {
+ name: "test_avf_debug_policy_without_adb",
+ defaults: ["test_avf_dts_to_dtb"],
+ srcs: ["assets/avf_debug_policy_without_adb.dts"],
+ out: ["avf_debug_policy_without_adb.dtbo"],
+}
+
+java_test_host {
+ name: "CustomPvmfwHostTestCases",
+ srcs: ["java/**/*.java"],
+ test_suites: ["general-tests"],
+ libs: [
+ "androidx.annotation_annotation",
+ "tradefed",
+ ],
+ static_libs: [
+ "MicrodroidHostTestHelper",
+ "PvmfwHostTestHelper",
+ "compatibility-host-util",
+ ],
+ per_testcase_directory: true,
+ data: [
+ ":MicrodroidTestApp",
+ ":pvmfw_test",
+ ":test_avf_debug_policy_with_adb",
+ ":test_avf_debug_policy_without_adb",
+ "assets/bcc.dat",
+ ],
+}
diff --git a/tests/pvmfw/AndroidTest.xml b/tests/pvmfw/AndroidTest.xml
new file mode 100644
index 0000000..6ff7b6f
--- /dev/null
+++ b/tests/pvmfw/AndroidTest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2023 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.
+-->
+<configuration description="Host driven tests for pvmfw with customization">
+ <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer">
+ <option name="force-root" value="true"/>
+ </target_preparer>
+
+ <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
+ <option name="jar" value="CustomPvmfwHostTestCases.jar" />
+ </test>
+</configuration>
diff --git a/tests/pvmfw/assets/avf_debug_policy_with_adb.dts b/tests/pvmfw/assets/avf_debug_policy_with_adb.dts
new file mode 100644
index 0000000..9ad15dd
--- /dev/null
+++ b/tests/pvmfw/assets/avf_debug_policy_with_adb.dts
@@ -0,0 +1,18 @@
+/dts-v1/;
+/plugin/;
+
+/ {
+ fragment@avf {
+ target-path = "/";
+
+ __overlay__ {
+ avf {
+ guest {
+ microdroid {
+ adb = <1>;
+ };
+ };
+ };
+ };
+ };
+};
\ No newline at end of file
diff --git a/tests/pvmfw/assets/avf_debug_policy_without_adb.dts b/tests/pvmfw/assets/avf_debug_policy_without_adb.dts
new file mode 100644
index 0000000..992e0ff
--- /dev/null
+++ b/tests/pvmfw/assets/avf_debug_policy_without_adb.dts
@@ -0,0 +1,18 @@
+/dts-v1/;
+/plugin/;
+
+/ {
+ fragment@avf {
+ target-path = "/";
+
+ __overlay__ {
+ avf {
+ guest {
+ microdroid {
+ adb = <0>;
+ };
+ };
+ };
+ };
+ };
+};
\ No newline at end of file
diff --git a/tests/pvmfw/assets/bcc.dat b/tests/pvmfw/assets/bcc.dat
new file mode 100644
index 0000000..7ab71f1
--- /dev/null
+++ b/tests/pvmfw/assets/bcc.dat
Binary files differ
diff --git a/tests/pvmfw/helper/Android.bp b/tests/pvmfw/helper/Android.bp
new file mode 100644
index 0000000..1b96842
--- /dev/null
+++ b/tests/pvmfw/helper/Android.bp
@@ -0,0 +1,9 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library_host {
+ name: "PvmfwHostTestHelper",
+ srcs: ["java/**/*.java"],
+ libs: ["androidx.annotation_annotation"],
+}
diff --git a/tests/pvmfw/helper/java/com/android/pvmfw/test/host/Pvmfw.java b/tests/pvmfw/helper/java/com/android/pvmfw/test/host/Pvmfw.java
new file mode 100644
index 0000000..b0c1207
--- /dev/null
+++ b/tests/pvmfw/helper/java/com/android/pvmfw/test/host/Pvmfw.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2023 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 com.android.pvmfw.test.host;
+
+import static java.nio.ByteOrder.LITTLE_ENDIAN;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Objects;
+import java.nio.ByteBuffer;
+
+/** pvmfw.bin with custom config payloads on host. */
+public final class Pvmfw {
+ private static final int SIZE_8B = 8; // 8 bytes
+ private static final int SIZE_4K = 4 << 10; // 4 KiB, PAGE_SIZE
+ private static final int BUFFER_SIZE = 1024;
+ private static final int HEADER_MAGIC = 0x666d7670;
+ private static final int HEADER_DEFAULT_VERSION = getVersion(1, 0);
+ private static final int HEADER_FLAGS = 0;
+
+ @NonNull private final File mPvmfwBinFile;
+ @NonNull private final File mBccFile;
+ @Nullable private final File mDebugPolicyFile;
+ private final int mVersion;
+
+ private Pvmfw(
+ @NonNull File pvmfwBinFile,
+ @NonNull File bccFile,
+ @Nullable File debugPolicyFile,
+ int version) {
+ mPvmfwBinFile = Objects.requireNonNull(pvmfwBinFile);
+ mBccFile = Objects.requireNonNull(bccFile);
+ mDebugPolicyFile = debugPolicyFile;
+ mVersion = version;
+ }
+
+ /**
+ * Serializes pvmfw.bin and its config, as written in the <a
+ * href="https://android.googlesource.com/platform/packages/modules/Virtualization/+/master/pvmfw/README.md">README.md</a>
+ */
+ public void serialize(@NonNull File outFile) throws IOException {
+ Objects.requireNonNull(outFile);
+
+ int headerSize = alignTo(getHeaderSize(mVersion), SIZE_8B);
+ int bccOffset = headerSize;
+ int bccSize = (int) mBccFile.length();
+
+ int debugPolicyOffset = alignTo(bccOffset + bccSize, SIZE_8B);
+ int debugPolicySize = mDebugPolicyFile == null ? 0 : (int) mDebugPolicyFile.length();
+
+ int totalSize = debugPolicyOffset + debugPolicySize;
+ if (hasVmDtbo(mVersion)) {
+ // Add VM DTBO size as well.
+ totalSize += Integer.BYTES * 2;
+ }
+
+ ByteBuffer header = ByteBuffer.allocate(headerSize).order(LITTLE_ENDIAN);
+ header.putInt(HEADER_MAGIC);
+ header.putInt(mVersion);
+ header.putInt(totalSize);
+ header.putInt(HEADER_FLAGS);
+ header.putInt(bccOffset);
+ header.putInt(bccSize);
+ header.putInt(debugPolicyOffset);
+ header.putInt(debugPolicySize);
+
+ if (hasVmDtbo(mVersion)) {
+ // Add placeholder entry for VM DTBO.
+ // TODO(b/291191157): Add a real DTBO and test.
+ header.putInt(0);
+ header.putInt(0);
+ }
+
+ try (FileOutputStream pvmfw = new FileOutputStream(outFile)) {
+ appendFile(pvmfw, mPvmfwBinFile);
+ padTo(pvmfw, SIZE_4K);
+ pvmfw.write(header.array());
+ padTo(pvmfw, SIZE_8B);
+ appendFile(pvmfw, mBccFile);
+ if (mDebugPolicyFile != null) {
+ padTo(pvmfw, SIZE_8B);
+ appendFile(pvmfw, mDebugPolicyFile);
+ }
+ padTo(pvmfw, SIZE_4K);
+ }
+ }
+
+ private void appendFile(@NonNull FileOutputStream out, @NonNull File inFile)
+ throws IOException {
+ byte buffer[] = new byte[BUFFER_SIZE];
+ try (FileInputStream in = new FileInputStream(inFile)) {
+ int size;
+ while (true) {
+ size = in.read(buffer);
+ if (size < 0) {
+ return;
+ }
+ out.write(buffer, /* offset= */ 0, size);
+ }
+ }
+ }
+
+ private void padTo(@NonNull FileOutputStream out, int size) throws IOException {
+ int streamSize = (int) out.getChannel().size();
+ for (int i = streamSize; i < alignTo(streamSize, size); i++) {
+ out.write(0); // write byte.
+ }
+ }
+
+ private static int getHeaderSize(int version) {
+ if (version == getVersion(1, 0)) {
+ return Integer.BYTES * 8; // Header has 8 integers.
+ }
+ return Integer.BYTES * 10; // Default + VM DTBO (offset, size)
+ }
+
+ private static boolean hasVmDtbo(int version) {
+ int major = getMajorVersion(version);
+ int minor = getMinorVersion(version);
+ return major > 1 || (major == 1 && minor >= 1);
+ }
+
+ private static int alignTo(int x, int size) {
+ return (x + size - 1) & ~(size - 1);
+ }
+
+ private static int getVersion(int major, int minor) {
+ return ((major & 0xFFFF) << 16) | (minor & 0xFFFF);
+ }
+
+ private static int getMajorVersion(int version) {
+ return (version >> 16) & 0xFFFF;
+ }
+
+ private static int getMinorVersion(int version) {
+ return version & 0xFFFF;
+ }
+
+ /** Builder for {@link Pvmfw}. */
+ public static final class Builder {
+ @NonNull private final File mPvmfwBinFile;
+ @NonNull private final File mBccFile;
+ @Nullable private File mDebugPolicyFile;
+ private int mVersion;
+
+ public Builder(@NonNull File pvmfwBinFile, @NonNull File bccFile) {
+ mPvmfwBinFile = Objects.requireNonNull(pvmfwBinFile);
+ mBccFile = Objects.requireNonNull(bccFile);
+ mVersion = HEADER_DEFAULT_VERSION;
+ }
+
+ @NonNull
+ public Builder setDebugPolicyOverlay(@Nullable File debugPolicyFile) {
+ mDebugPolicyFile = debugPolicyFile;
+ return this;
+ }
+
+ @NonNull
+ public Builder setVersion(int major, int minor) {
+ mVersion = getVersion(major, minor);
+ return this;
+ }
+
+ @NonNull
+ public Pvmfw build() {
+ return new Pvmfw(mPvmfwBinFile, mBccFile, mDebugPolicyFile, mVersion);
+ }
+ }
+}
diff --git a/tests/pvmfw/java/com/android/pvmfw/test/DebugPolicyHostTests.java b/tests/pvmfw/java/com/android/pvmfw/test/DebugPolicyHostTests.java
new file mode 100644
index 0000000..410e6e0
--- /dev/null
+++ b/tests/pvmfw/java/com/android/pvmfw/test/DebugPolicyHostTests.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright 2023 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 com.android.pvmfw.test;
+
+import static com.android.tradefed.device.TestDevice.MicrodroidBuilder;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assume.assumeTrue;
+import static org.junit.Assert.assertThrows;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.microdroid.test.host.CommandRunner;
+import com.android.microdroid.test.host.MicrodroidHostTestCaseBase;
+import com.android.pvmfw.test.host.Pvmfw;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.DeviceRuntimeException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.TestDevice;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.FileUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/** Tests debug policy */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class DebugPolicyHostTests extends MicrodroidHostTestCaseBase {
+ @NonNull private static final String PVMFW_FILE_NAME = "pvmfw_test.bin";
+ @NonNull private static final String BCC_FILE_NAME = "bcc.dat";
+ @NonNull private static final String PACKAGE_FILE_NAME = "MicrodroidTestApp.apk";
+ @NonNull private static final String PACKAGE_NAME = "com.android.microdroid.test";
+ @NonNull private static final String MICRODROID_DEBUG_FULL = "full";
+ @NonNull private static final String MICRODROID_DEBUG_NONE = "none";
+ @NonNull private static final String MICRODROID_CONFIG_PATH = "assets/vm_config_apex.json";
+ @NonNull private static final String MICRODROID_LOG_PATH = TEST_ROOT + "log.txt";
+ private static final int BOOT_COMPLETE_TIMEOUT_MS = 30000; // 30 seconds
+ private static final int BOOT_FAILURE_WAIT_TIME_MS = 10000; // 10 seconds
+ private static final int CONSOLE_OUTPUT_WAIT_MS = 5000; // 5 seconds
+
+ @NonNull private static final String CUSTOM_PVMFW_FILE_PREFIX = "pvmfw";
+ @NonNull private static final String CUSTOM_PVMFW_FILE_SUFFIX = ".bin";
+ @NonNull private static final String CUSTOM_PVMFW_IMG_PATH = TEST_ROOT + PVMFW_FILE_NAME;
+ @NonNull private static final String CUSTOM_PVMFW_IMG_PATH_PROP = "hypervisor.pvmfw.path";
+
+ @NonNull private static final String CUSTOM_DEBUG_POLICY_FILE_NAME = "debug_policy.dtb";
+
+ @NonNull
+ private static final String CUSTOM_DEBUG_POLICY_PATH =
+ TEST_ROOT + CUSTOM_DEBUG_POLICY_FILE_NAME;
+
+ @NonNull
+ private static final String CUSTOM_DEBUG_POLICY_PATH_PROP =
+ "hypervisor.virtualizationmanager.debug_policy.path";
+
+ @NonNull
+ private static final String AVF_DEBUG_POLICY_ADB_DT_PROP_PATH = "/avf/guest/microdroid/adb";
+
+ @NonNull private static final String MICRODROID_CMDLINE_PATH = "/proc/cmdline";
+ @NonNull private static final String MICRODROID_DT_ROOT_PATH = "/proc/device-tree";
+
+ @NonNull
+ private static final String MICRODROID_DT_BOOTARGS_PATH =
+ MICRODROID_DT_ROOT_PATH + "/chosen/bootargs";
+
+ @NonNull
+ private static final String MICRODROID_DT_RAMDUMP_PATH =
+ MICRODROID_DT_ROOT_PATH + "/avf/guest/common/ramdump";
+
+ @NonNull private static final String HEX_STRING_ZERO = "00000000";
+ @NonNull private static final String HEX_STRING_ONE = "00000001";
+
+ @Nullable private static File mPvmfwBinFileOnHost;
+ @Nullable private static File mBccFileOnHost;
+
+ @Nullable private TestDevice mAndroidDevice;
+ @Nullable private ITestDevice mMicrodroidDevice;
+ @Nullable private File mCustomPvmfwBinFileOnHost;
+ @Nullable private File mCustomDebugPolicyFileOnHost;
+
+ @Before
+ public void setUp() throws Exception {
+ mAndroidDevice = (TestDevice) Objects.requireNonNull(getDevice());
+
+ // Check device capabilities
+ assumeDeviceIsCapable(mAndroidDevice);
+ assumeTrue(
+ "Skip if protected VMs are not supported",
+ mAndroidDevice.supportsMicrodroid(/* protectedVm= */ true));
+
+ // tradefed copies the test artfacts under /tmp when running tests,
+ // so we should *find* the artifacts with the file name.
+ mPvmfwBinFileOnHost =
+ getTestInformation().getDependencyFile(PVMFW_FILE_NAME, /* targetFirst= */ false);
+ mBccFileOnHost =
+ getTestInformation().getDependencyFile(BCC_FILE_NAME, /* targetFirst= */ false);
+
+ // Prepare for system properties for custom debug policy.
+ // File will be prepared later in individual test by setupCustomDebugPolicy()
+ // and then pushed to device when launching with launchProtectedVmAndWaitForBootCompleted()
+ // or tryLaunchProtectedNonDebuggableVm().
+ mCustomPvmfwBinFileOnHost =
+ FileUtil.createTempFile(CUSTOM_PVMFW_FILE_PREFIX, CUSTOM_PVMFW_FILE_SUFFIX);
+ setPropertyOrThrow(mAndroidDevice, CUSTOM_PVMFW_IMG_PATH_PROP, CUSTOM_PVMFW_IMG_PATH);
+ setPropertyOrThrow(mAndroidDevice, CUSTOM_DEBUG_POLICY_PATH_PROP, CUSTOM_DEBUG_POLICY_PATH);
+
+ // Prepare for launching microdroid
+ mAndroidDevice.installPackage(findTestFile(PACKAGE_FILE_NAME), /* reinstall */ false);
+ prepareVirtualizationTestSetup(mAndroidDevice);
+ mMicrodroidDevice = null;
+ }
+
+ @After
+ public void shutdown() throws Exception {
+ if (!mAndroidDevice.supportsMicrodroid(/* protectedVm= */ true)) {
+ return;
+ }
+ if (mMicrodroidDevice != null) {
+ mAndroidDevice.shutdownMicrodroid(mMicrodroidDevice);
+ mMicrodroidDevice = null;
+ }
+ mAndroidDevice.uninstallPackage(PACKAGE_NAME);
+
+ // Cleanup for custom debug policies
+ setPropertyOrThrow(mAndroidDevice, CUSTOM_DEBUG_POLICY_PATH_PROP, "");
+ setPropertyOrThrow(mAndroidDevice, CUSTOM_PVMFW_IMG_PATH_PROP, "");
+ FileUtil.deleteFile(mCustomPvmfwBinFileOnHost);
+
+ cleanUpVirtualizationTestSetup(mAndroidDevice);
+ }
+
+ @Test
+ public void testAdbInDebugPolicy_withDebugLevelNone_bootWithAdbConnection() throws Exception {
+ prepareCustomDebugPolicy("avf_debug_policy_with_adb.dtbo");
+
+ launchProtectedVmAndWaitForBootCompleted(MICRODROID_DEBUG_NONE);
+ }
+
+ @Test
+ public void testNoAdbInDebugPolicy_withDebugLevelNone_boots() throws Exception {
+ prepareCustomDebugPolicy("avf_debug_policy_without_adb.dtbo");
+
+ // VM would boot, but cannot verify directly because of no adbd in the VM.
+ CommandResult result = tryLaunchProtectedNonDebuggableVm();
+ assertThat(result.getStatus()).isEqualTo(CommandStatus.TIMED_OUT);
+ assertWithMessage("Microdroid should have booted")
+ .that(result.getStderr())
+ .contains("payload is ready");
+ }
+
+ @Test
+ public void testNoAdbInDebugPolicy_withDebugLevelNone_noConnection() throws Exception {
+ prepareCustomDebugPolicy("avf_debug_policy_without_adb.dtbo");
+
+ assertThrows(
+ "Microdroid shouldn't be recognized because of missing adb connection",
+ DeviceRuntimeException.class,
+ () ->
+ launchProtectedVmAndWaitForBootCompleted(
+ MICRODROID_DEBUG_NONE, BOOT_FAILURE_WAIT_TIME_MS));
+ }
+
+ @Test
+ public void testNoAdbInDebugPolicy_withDebugLevelFull_bootWithAdbConnection() throws Exception {
+ prepareCustomDebugPolicy("avf_debug_policy_without_adb.dtbo");
+
+ launchProtectedVmAndWaitForBootCompleted(MICRODROID_DEBUG_FULL);
+ }
+
+ private boolean isDebugPolicyEnabled(@NonNull String dtPropertyPath)
+ throws DeviceNotAvailableException {
+ CommandRunner runner = new CommandRunner(mAndroidDevice);
+ CommandResult result =
+ runner.runForResult("xxd", "-p", "/proc/device-tree" + dtPropertyPath);
+ if (result.getStatus() == CommandStatus.SUCCESS) {
+ return HEX_STRING_ONE.equals(result.getStdout().trim());
+ }
+ return false;
+ }
+
+ @NonNull
+ private String readMicrodroidFileAsString(@NonNull String path)
+ throws DeviceNotAvailableException {
+ return new CommandRunner(mMicrodroidDevice).run("cat", path);
+ }
+
+ @NonNull
+ private String readMicrodroidFileAsHexString(@NonNull String path)
+ throws DeviceNotAvailableException {
+ return new CommandRunner(mMicrodroidDevice).run("xxd", "-p", path);
+ }
+
+ private void prepareCustomDebugPolicy(@NonNull String debugPolicyFileName) throws Exception {
+ mCustomDebugPolicyFileOnHost =
+ getTestInformation()
+ .getDependencyFile(debugPolicyFileName, /* targetFirst= */ false);
+
+ Pvmfw pvmfw =
+ new Pvmfw.Builder(mPvmfwBinFileOnHost, mBccFileOnHost)
+ .setDebugPolicyOverlay(mCustomDebugPolicyFileOnHost)
+ .build();
+ pvmfw.serialize(mCustomPvmfwBinFileOnHost);
+ }
+
+ private boolean hasConsoleOutput(@NonNull CommandResult result)
+ throws DeviceNotAvailableException {
+ return result.getStdout().contains("Run /init as init process");
+ }
+
+ private boolean hasMicrodroidLogcatOutput() throws DeviceNotAvailableException {
+ CommandResult result =
+ new CommandRunner(mAndroidDevice).runForResult("test", "-s", MICRODROID_LOG_PATH);
+ return result.getExitCode() == 0;
+ }
+
+ private ITestDevice launchProtectedVmAndWaitForBootCompleted(String debugLevel)
+ throws DeviceNotAvailableException {
+ return launchProtectedVmAndWaitForBootCompleted(debugLevel, BOOT_COMPLETE_TIMEOUT_MS);
+ }
+
+ private ITestDevice launchProtectedVmAndWaitForBootCompleted(
+ String debugLevel, long adbTimeoutMs) throws DeviceNotAvailableException {
+ mMicrodroidDevice =
+ MicrodroidBuilder.fromDevicePath(
+ getPathForPackage(PACKAGE_NAME), MICRODROID_CONFIG_PATH)
+ .debugLevel(debugLevel)
+ .protectedVm(/* protectedVm= */ true)
+ .addBootFile(mCustomPvmfwBinFileOnHost, PVMFW_FILE_NAME)
+ .addBootFile(mCustomDebugPolicyFileOnHost, CUSTOM_DEBUG_POLICY_FILE_NAME)
+ .setAdbConnectTimeoutMs(adbTimeoutMs)
+ .build(mAndroidDevice);
+ assertThat(mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT_MS)).isTrue();
+ assertThat(mMicrodroidDevice.enableAdbRoot()).isTrue();
+ return mMicrodroidDevice;
+ }
+
+ // Try to launch protected non-debuggable VM for a while and quit.
+ // Non-debuggable VM might not enable adb, so there's no ITestDevice instance of it.
+ private CommandResult tryLaunchProtectedNonDebuggableVm() throws DeviceNotAvailableException {
+ // Can't use MicrodroidBuilder because it expects adb connection
+ // but non-debuggable VM may not enable adb.
+ CommandRunner runner = new CommandRunner(mAndroidDevice);
+ runner.run("mkdir", "-p", TEST_ROOT);
+ mAndroidDevice.pushFile(mCustomPvmfwBinFileOnHost, CUSTOM_PVMFW_IMG_PATH);
+ mAndroidDevice.pushFile(mCustomDebugPolicyFileOnHost, CUSTOM_DEBUG_POLICY_PATH);
+
+ // This will fail because app wouldn't finish itself.
+ // But let's run the app once and get logs.
+ String command =
+ String.join(
+ " ",
+ "/apex/com.android.virt/bin/vm",
+ "run-app",
+ "--log",
+ MICRODROID_LOG_PATH,
+ "--protected",
+ getPathForPackage(PACKAGE_NAME),
+ TEST_ROOT + "idsig",
+ TEST_ROOT + "instance.img",
+ "--config-path",
+ MICRODROID_CONFIG_PATH);
+ return mAndroidDevice.executeShellV2Command(
+ command, CONSOLE_OUTPUT_WAIT_MS, TimeUnit.MILLISECONDS, /* retryAttempts= */ 0);
+ }
+}
diff --git a/tests/pvmfw/java/com/android/pvmfw/test/PvmfwImgTest.java b/tests/pvmfw/java/com/android/pvmfw/test/PvmfwImgTest.java
new file mode 100644
index 0000000..95c1c4e
--- /dev/null
+++ b/tests/pvmfw/java/com/android/pvmfw/test/PvmfwImgTest.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2023 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 com.android.pvmfw.test;
+
+import static com.android.tradefed.device.TestDevice.MicrodroidBuilder;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeTrue;
+import static org.junit.Assert.assertThrows;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.microdroid.test.host.MicrodroidHostTestCaseBase;
+import com.android.pvmfw.test.host.Pvmfw;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.DeviceRuntimeException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.TestDevice;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.util.FileUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/** Tests pvmfw.img and pvmfw */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class PvmfwImgTest extends MicrodroidHostTestCaseBase {
+ @NonNull private static final String PVMFW_FILE_NAME = "pvmfw_test.bin";
+ @NonNull private static final String BCC_FILE_NAME = "bcc.dat";
+ @NonNull private static final String PACKAGE_FILE_NAME = "MicrodroidTestApp.apk";
+ @NonNull private static final String PACKAGE_NAME = "com.android.microdroid.test";
+ @NonNull private static final String MICRODROID_DEBUG_FULL = "full";
+ @NonNull private static final String MICRODROID_CONFIG_PATH = "assets/vm_config_apex.json";
+ private static final int BOOT_COMPLETE_TIMEOUT_MS = 30000; // 30 seconds
+ private static final int BOOT_FAILURE_WAIT_TIME_MS = 10000; // 10 seconds
+
+ @NonNull private static final String CUSTOM_PVMFW_FILE_PREFIX = "pvmfw";
+ @NonNull private static final String CUSTOM_PVMFW_FILE_SUFFIX = ".bin";
+ @NonNull private static final String CUSTOM_PVMFW_IMG_PATH = TEST_ROOT + PVMFW_FILE_NAME;
+ @NonNull private static final String CUSTOM_PVMFW_IMG_PATH_PROP = "hypervisor.pvmfw.path";
+
+ @Nullable private static File mPvmfwBinFileOnHost;
+ @Nullable private static File mBccFileOnHost;
+
+ @Nullable private TestDevice mAndroidDevice;
+ @Nullable private ITestDevice mMicrodroidDevice;
+ @Nullable private File mCustomPvmfwBinFileOnHost;
+
+ @Before
+ public void setUp() throws Exception {
+ mAndroidDevice = (TestDevice) Objects.requireNonNull(getDevice());
+
+ // Check device capabilities
+ assumeDeviceIsCapable(mAndroidDevice);
+ assumeTrue(
+ "Skip if protected VMs are not supported",
+ mAndroidDevice.supportsMicrodroid(/* protectedVm= */ true));
+
+ // tradefed copies the test artfacts under /tmp when running tests,
+ // so we should *find* the artifacts with the file name.
+ mPvmfwBinFileOnHost =
+ getTestInformation().getDependencyFile(PVMFW_FILE_NAME, /* targetFirst= */ false);
+ mBccFileOnHost =
+ getTestInformation().getDependencyFile(BCC_FILE_NAME, /* targetFirst= */ false);
+
+ // Prepare for system properties for custom pvmfw.img.
+ // File will be prepared later in individual test and then pushed to device
+ // when launching with launchProtectedVmAndWaitForBootCompleted().
+ mCustomPvmfwBinFileOnHost =
+ FileUtil.createTempFile(CUSTOM_PVMFW_FILE_PREFIX, CUSTOM_PVMFW_FILE_SUFFIX);
+ setPropertyOrThrow(mAndroidDevice, CUSTOM_PVMFW_IMG_PATH_PROP, CUSTOM_PVMFW_IMG_PATH);
+
+ // Prepare for launching microdroid
+ mAndroidDevice.installPackage(findTestFile(PACKAGE_FILE_NAME), /* reinstall */ false);
+ prepareVirtualizationTestSetup(mAndroidDevice);
+ mMicrodroidDevice = null;
+ }
+
+ @After
+ public void shutdown() throws Exception {
+ if (!mAndroidDevice.supportsMicrodroid(/* protectedVm= */ true)) {
+ return;
+ }
+ if (mMicrodroidDevice != null) {
+ mAndroidDevice.shutdownMicrodroid(mMicrodroidDevice);
+ mMicrodroidDevice = null;
+ }
+ mAndroidDevice.uninstallPackage(PACKAGE_NAME);
+
+ // Cleanup for custom pvmfw.img
+ setPropertyOrThrow(mAndroidDevice, CUSTOM_PVMFW_IMG_PATH_PROP, "");
+ FileUtil.deleteFile(mCustomPvmfwBinFileOnHost);
+
+ cleanUpVirtualizationTestSetup(mAndroidDevice);
+ }
+
+ @Test
+ public void testConfigVersion1_0_boots() throws Exception {
+ Pvmfw pvmfw =
+ new Pvmfw.Builder(mPvmfwBinFileOnHost, mBccFileOnHost).setVersion(1, 0).build();
+ pvmfw.serialize(mCustomPvmfwBinFileOnHost);
+
+ launchProtectedVmAndWaitForBootCompleted(BOOT_COMPLETE_TIMEOUT_MS);
+ }
+
+ @Test
+ public void testConfigVersion1_1_boots() throws Exception {
+ Pvmfw pvmfw =
+ new Pvmfw.Builder(mPvmfwBinFileOnHost, mBccFileOnHost).setVersion(1, 1).build();
+ pvmfw.serialize(mCustomPvmfwBinFileOnHost);
+
+ launchProtectedVmAndWaitForBootCompleted(BOOT_COMPLETE_TIMEOUT_MS);
+ }
+
+ @Test
+ public void testInvalidConfigVersion_doesNotBoot() throws Exception {
+ // Disclaimer: Update versions when they become valid
+ List<int[]> invalid_versions =
+ Arrays.asList(
+ new int[] {0, 0},
+ new int[] {0, 1},
+ new int[] {0, 0xFFFF},
+ new int[] {2, 0},
+ new int[] {2, 1},
+ new int[] {2, 0xFFFF},
+ new int[] {0xFFFF, 0},
+ new int[] {0xFFFF, 1},
+ new int[] {0xFFFF, 0xFFFF});
+
+ Pvmfw.Builder builder = new Pvmfw.Builder(mPvmfwBinFileOnHost, mBccFileOnHost);
+
+ for (int[] pair : invalid_versions) {
+ int major = pair[0];
+ int minor = pair[1];
+ String version = "v" + major + "." + minor;
+
+ Pvmfw pvmfw = builder.setVersion(major, minor).build();
+ pvmfw.serialize(mCustomPvmfwBinFileOnHost);
+
+ assertThrows(
+ "pvmfw shouldn't boot with invalid version " + version,
+ DeviceRuntimeException.class,
+ () -> launchProtectedVmAndWaitForBootCompleted(BOOT_FAILURE_WAIT_TIME_MS));
+ }
+ }
+
+ private ITestDevice launchProtectedVmAndWaitForBootCompleted(long adbTimeoutMs)
+ throws DeviceNotAvailableException {
+ mMicrodroidDevice =
+ MicrodroidBuilder.fromDevicePath(
+ getPathForPackage(PACKAGE_NAME), MICRODROID_CONFIG_PATH)
+ .debugLevel(MICRODROID_DEBUG_FULL)
+ .protectedVm(true)
+ .addBootFile(mCustomPvmfwBinFileOnHost, PVMFW_FILE_NAME)
+ .setAdbConnectTimeoutMs(adbTimeoutMs)
+ .build(mAndroidDevice);
+ assertThat(mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT_MS)).isTrue();
+ return mMicrodroidDevice;
+ }
+}
diff --git a/tests/pvmfw/tools/Android.bp b/tests/pvmfw/tools/Android.bp
new file mode 100644
index 0000000..7bd3ef5
--- /dev/null
+++ b/tests/pvmfw/tools/Android.bp
@@ -0,0 +1,10 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_binary_host {
+ name: "pvmfw-tool",
+ manifest: "pvmfw-tool-manifest.txt",
+ srcs: ["PvmfwTool.java"],
+ static_libs: ["PvmfwHostTestHelper"],
+}
diff --git a/tests/pvmfw/tools/PvmfwTool.java b/tests/pvmfw/tools/PvmfwTool.java
new file mode 100644
index 0000000..e4b6020
--- /dev/null
+++ b/tests/pvmfw/tools/PvmfwTool.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2023 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 com.android.pvmfw;
+
+import com.android.pvmfw.test.host.Pvmfw;
+
+import java.io.File;
+import java.io.IOException;
+
+/** CLI for {@link com.android.microdroid.test.host.Pvmfw}. */
+public class PvmfwTool {
+ public static void printUsage() {
+ System.out.println("pvmfw-tool: Appends pvmfw.bin and config payloads.");
+ System.out.println("Requires BCC and debug policy dtbo files");
+ System.out.println("");
+ System.out.println("Usage: pvmfw-tool <pvmfw_with_config> <pvmfw_bin> <bcc.dat> <dp.dtbo>");
+ }
+
+ public static void main(String[] args) {
+ if (args.length != 4) {
+ printUsage();
+ System.exit(1);
+ }
+
+ File out = new File(args[0]);
+ File pvmfw_bin = new File(args[1]);
+ File bcc_dat = new File(args[2]);
+ File dtbo = new File(args[3]);
+
+ try {
+ Pvmfw pvmfw = new Pvmfw.Builder(pvmfw_bin, bcc_dat).setDebugPolicyOverlay(dtbo).build();
+ pvmfw.serialize(out);
+ } catch (IOException e) {
+ e.printStackTrace();
+ printUsage();
+ System.exit(1);
+ }
+ }
+}
diff --git a/tests/pvmfw/tools/pvmfw-tool-manifest.txt b/tests/pvmfw/tools/pvmfw-tool-manifest.txt
new file mode 100644
index 0000000..dc71fd2
--- /dev/null
+++ b/tests/pvmfw/tools/pvmfw-tool-manifest.txt
@@ -0,0 +1,2 @@
+Manifest-Version: 1.0
+Main-Class: com.android.microdroid.PvmfwTool