Merge AuthFsHostTest into MicrodroidTestCase

In the meantime, in order to reduce the amount of testing time, boot the
VM once in AuthFsTestCase. This is possible through
@BeforeClassWithInfo.

The new DeviceRunner class intends to generalize shell command execution
to ITestDevice. Later when the VM can be represented as an ITestDevice,
we can easily run a command in the same way.

Bug: 191056545
Bug: 182478337
Test: atest MicrodroidHostTestCases  # succeeded in 7:50 on CF

Change-Id: I680a9ef00c345a2f4d7773412c7f0ebd767aab15
diff --git a/tests/hostside/Android.bp b/tests/hostside/Android.bp
index 968c991..4d70c70 100644
--- a/tests/hostside/Android.bp
+++ b/tests/hostside/Android.bp
@@ -8,9 +8,14 @@
     test_suites: ["device-tests"],
     libs: [
         "tradefed",
+        "compatibility-tradefed",
+        "compatibility-host-util",
     ],
     static_libs: [
         "VirtualizationTestHelper",
     ],
-    data: [":MicrodroidTestApp.signed"],
+    data: [
+        ":authfs_test_files",
+        ":MicrodroidTestApp.signed",
+    ],
 }
diff --git a/tests/hostside/AndroidTest.xml b/tests/hostside/AndroidTest.xml
index eda733a..889fc4e 100644
--- a/tests/hostside/AndroidTest.xml
+++ b/tests/hostside/AndroidTest.xml
@@ -22,6 +22,38 @@
       a test-only permission, run it without selinux -->
     <target_preparer class="com.android.tradefed.targetprep.DisableSELinuxTargetPreparer"/>
 
+    <!-- Prepare test directory for AuthFsTestCase. -->
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="throw-if-cmd-fail" value="true" />
+        <!-- Prepare test directory. -->
+        <option name="run-command" value="mkdir -p /data/local/tmp/authfs/mnt" />
+        <option name="teardown-command" value="rm -rf /data/local/tmp/authfs" />
+    </target_preparer>
+
+    <!-- Prepare files for AuthFsTestCase. -->
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="cleanup" value="true" />
+        <option name="abort-on-push-failure" value="true" />
+        <option name="push-file" key="cert.der" value="/data/local/tmp/authfs/cert.der" />
+        <option name="push-file" key="input.4m" value="/data/local/tmp/authfs/input.4m" />
+        <option name="push-file" key="input.4k1" value="/data/local/tmp/authfs/input.4k1" />
+        <option name="push-file" key="input.4k" value="/data/local/tmp/authfs/input.4k" />
+        <option name="push-file" key="input.4m.fsv_sig"
+            value="/data/local/tmp/authfs/input.4m.fsv_sig" />
+        <option name="push-file" key="input.4k1.fsv_sig"
+            value="/data/local/tmp/authfs/input.4k1.fsv_sig" />
+        <option name="push-file" key="input.4k.fsv_sig"
+            value="/data/local/tmp/authfs/input.4k.fsv_sig" />
+        <option name="push-file" key="input.4m.merkle_dump"
+            value="/data/local/tmp/authfs/input.4m.merkle_dump" />
+        <option name="push-file" key="input.4m.merkle_dump.bad"
+            value="/data/local/tmp/authfs/input.4m.merkle_dump.bad" />
+        <option name="push-file" key="input.4k1.merkle_dump"
+            value="/data/local/tmp/authfs/input.4k1.merkle_dump" />
+        <option name="push-file" key="input.4k.merkle_dump"
+            value="/data/local/tmp/authfs/input.4k.merkle_dump" />
+    </target_preparer>
+
     <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
         <option name="jar" value="MicrodroidHostTestCases.jar" />
     </test>
diff --git a/tests/hostside/helper/java/android/virt/test/CommandRunner.java b/tests/hostside/helper/java/android/virt/test/CommandRunner.java
new file mode 100644
index 0000000..696c89a
--- /dev/null
+++ b/tests/hostside/helper/java/android/virt/test/CommandRunner.java
@@ -0,0 +1,89 @@
+/*
+ * 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.fail;
+import static org.junit.Assume.assumeThat;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+
+import java.util.Arrays;
+
+import javax.annotation.Nonnull;
+
+/** A helper class to provide easy way to run commands on a test device. */
+public class CommandRunner {
+
+    /** Default timeout. 30 sec because Microdroid is extremely slow on GCE-on-CF. */
+    private static final long DEFAULT_TIMEOUT = 30000;
+
+    private ITestDevice mDevice;
+
+    public CommandRunner(@Nonnull ITestDevice device) {
+        mDevice = device;
+    }
+
+    public ITestDevice getDevice() {
+        return mDevice;
+    }
+
+    public String run(String... cmd) throws DeviceNotAvailableException {
+        CommandResult result = runForResult(cmd);
+        if (result.getStatus() != CommandStatus.SUCCESS) {
+            fail(join(cmd) + " has failed: " + result);
+        }
+        return result.getStdout().trim();
+    }
+
+    public String tryRun(String... cmd) throws DeviceNotAvailableException {
+        CommandResult result = runForResult(cmd);
+        if (result.getStatus() == CommandStatus.SUCCESS) {
+            return result.getStdout().trim();
+        } else {
+            CLog.d(join(cmd) + " has failed (but ok): " + result);
+            return null;
+        }
+    }
+
+    public String runWithTimeout(long timeoutMillis, String... cmd)
+            throws DeviceNotAvailableException {
+        CommandResult result =
+                mDevice.executeShellV2Command(
+                        join(cmd), timeoutMillis, java.util.concurrent.TimeUnit.MILLISECONDS);
+        if (result.getStatus() != CommandStatus.SUCCESS) {
+            fail(join(cmd) + " has failed: " + result);
+        }
+        return result.getStdout().trim();
+    }
+
+    public CommandResult runForResult(String... cmd) throws DeviceNotAvailableException {
+        return mDevice.executeShellV2Command(join(cmd));
+    }
+
+    public void assumeSuccess(String... cmd) throws DeviceNotAvailableException {
+        assumeThat(runForResult(cmd).getStatus(), is(CommandStatus.SUCCESS));
+    }
+
+    private static String join(String... strs) {
+        return String.join(" ", Arrays.asList(strs));
+    }
+}
diff --git a/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java b/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
index 451f9ba..7a17619 100644
--- a/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
+++ b/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
@@ -20,10 +20,11 @@
 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.build.IBuildInfo;
 import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
 import com.android.tradefed.util.CommandResult;
@@ -50,92 +51,63 @@
 
     private static final long MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES = 5;
 
-    public void prepareVirtualizationTestSetup() throws DeviceNotAvailableException {
-        // kill stale crosvm processes
-        tryRunOnAndroid("killall", "crosvm");
+    public static void prepareVirtualizationTestSetup(ITestDevice androidDevice)
+            throws DeviceNotAvailableException {
+        CommandRunner android = new CommandRunner(androidDevice);
 
-        // Prepare the test root
-        tryRunOnAndroid("rm", "-rf", TEST_ROOT);
-        tryRunOnAndroid("mkdir", "-p", TEST_ROOT);
+        // kill stale crosvm processes
+        android.tryRun("killall", "crosvm");
 
         // disconnect from microdroid
         tryRunOnHost("adb", "disconnect", MICRODROID_SERIAL);
     }
 
-    public void cleanUpVirtualizationTestSetup() throws DeviceNotAvailableException {
+    public static void cleanUpVirtualizationTestSetup(ITestDevice androidDevice)
+            throws DeviceNotAvailableException {
+        CommandRunner android = new CommandRunner(androidDevice);
+
         // 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");
+        android.tryRun("killall", "crosvm");
+        android.tryRun("rm", "-rf", "/data/misc/virtualizationservice/*");
+        android.tryRun("stop", "virtualizationservice");
     }
 
-    public void testIfDeviceIsCapable() throws DeviceNotAvailableException {
+    public static void testIfDeviceIsCapable(ITestDevice androidDevice)
+            throws DeviceNotAvailableException {
+        CommandRunner android = new CommandRunner(androidDevice);
+
         // 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");
+        android.assumeSuccess("ls /dev/kvm");
+        android.assumeSuccess("ls /dev/vhost-vsock");
+        android.assumeSuccess("ls /apex/com.android.virt/bin/crosvm");
     }
 
     // Run an arbitrary command in the host side and returns the result
-    private String runOnHost(String... cmd) {
+    private static String runOnHost(String... cmd) {
         return runOnHostWithTimeout(10000, cmd);
     }
 
     // Same as runOnHost, but failure is not an error
-    private String tryRunOnHost(String... cmd) {
+    private static 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) {
+    private static 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
-    public String runOnAndroid(String... cmd) throws DeviceNotAvailableException {
-        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 returns null on error.
-    public String tryRunOnAndroid(String... cmd) throws DeviceNotAvailableException {
-        CommandResult result = getDevice().executeShellV2Command(join(cmd));
-        if (result.getStatus() == CommandStatus.SUCCESS) {
-            return result.getStdout().trim();
-        } else {
-            CLog.d(join(cmd) + " has failed (but ok): " + result);
-            return null;
-        }
-    }
-
-    private String runOnAndroidWithTimeout(long timeoutMillis, String... cmd)
-            throws DeviceNotAvailableException {
-        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) {
+    public static String runOnMicrodroid(String... cmd) {
         CommandResult result = runOnMicrodroidForResult(cmd);
         if (result.getStatus() != CommandStatus.SUCCESS) {
             fail(join(cmd) + " has failed: " + result);
@@ -144,7 +116,7 @@
     }
 
     // Same as runOnMicrodroid, but returns null on error.
-    public String tryRunOnMicrodroid(String... cmd) {
+    public static String tryRunOnMicrodroid(String... cmd) {
         CommandResult result = runOnMicrodroidForResult(cmd);
         if (result.getStatus() == CommandStatus.SUCCESS) {
             return result.getStdout().trim();
@@ -154,51 +126,62 @@
         }
     }
 
-    public CommandResult runOnMicrodroidForResult(String... cmd) {
+    public static CommandResult runOnMicrodroidForResult(String... cmd) {
         final long timeout = 30000; // 30 sec. Microdroid is extremely slow on GCE-on-CF.
         return RunUtil.getDefault()
                 .runTimedCmd(timeout, "adb", "-s", MICRODROID_SERIAL, "shell", join(cmd));
     }
 
-    private String join(String... strs) {
+    private static String join(String... strs) {
         return String.join(" ", Arrays.asList(strs));
     }
 
     public File findTestFile(String name) {
+        return findTestFile(getBuild(), name);
+    }
+
+    private static File findTestFile(IBuildInfo buildInfo, String name) {
         try {
-            return (new CompatibilityBuildHelper(getBuild())).getTestFile(name);
+            return (new CompatibilityBuildHelper(buildInfo)).getTestFile(name);
         } catch (FileNotFoundException e) {
             fail("Missing test file: " + name);
             return null;
         }
     }
 
-    public String startMicrodroid(
-            String apkName, String packageName, String configPath, boolean debug)
+    public static String startMicrodroid(
+            ITestDevice androidDevice,
+            IBuildInfo buildInfo,
+            String apkName,
+            String packageName,
+            String configPath,
+            boolean debug)
             throws DeviceNotAvailableException {
+        CommandRunner android = new CommandRunner(androidDevice);
+
         // Install APK
-        File apkFile = findTestFile(apkName);
-        getDevice().installPackage(apkFile, /* reinstall */ true);
+        File apkFile = findTestFile(buildInfo, apkName);
+        androidDevice.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);
+        String apkPath = android.run("pm", "path", packageName);
         assertTrue(apkPath.startsWith("package:"));
         apkPath = apkPath.substring("package:".length());
 
         // Push the idsig file to the device
-        File idsigOnHost = findTestFile(apkName + ".idsig");
+        File idsigOnHost = findTestFile(buildInfo, apkName + ".idsig");
         final String apkIdsigPath = TEST_ROOT + apkName + ".idsig";
-        getDevice().pushFile(idsigOnHost, apkIdsigPath);
+        androidDevice.pushFile(idsigOnHost, apkIdsigPath);
 
         final String logPath = TEST_ROOT + "log.txt";
         final String debugFlag = debug ? "--debug " : "";
 
         // Run the VM
-        runOnAndroid("start", "virtualizationservice");
+        android.run("start", "virtualizationservice");
         String ret =
-                runOnAndroid(
+                android.run(
                         VIRT_APEX + "bin/vm",
                         "run-app",
                         "--daemonize",
@@ -214,7 +197,7 @@
                 () -> {
                     try {
                         // Keep redirecting sufficiently long enough
-                        runOnAndroidWithTimeout(
+                        android.runWithTimeout(
                                 MICRODROID_BOOT_TIMEOUT_MINUTES * 60 * 1000,
                                 "logwrapper",
                                 "tail",
@@ -233,17 +216,20 @@
         return matcher.group(1);
     }
 
-    public void shutdownMicrodroid(String cid) throws DeviceNotAvailableException {
+    public static void shutdownMicrodroid(ITestDevice androidDevice, String cid)
+            throws DeviceNotAvailableException {
+        CommandRunner android = new CommandRunner(androidDevice);
+
         // Close the connection before shutting the VM down. Otherwise, b/192660485.
         tryRunOnHost("adb", "disconnect", MICRODROID_SERIAL);
-        final String serial = getDevice().getSerialNumber();
+        final String serial = androidDevice.getSerialNumber();
         tryRunOnHost("adb", "-s", serial, "forward", "--remove", "tcp:" + TEST_VM_ADB_PORT);
 
         // Shutdown the VM
-        runOnAndroid(VIRT_APEX + "bin/vm", "stop", cid);
+        android.run(VIRT_APEX + "bin/vm", "stop", cid);
     }
 
-    public void rootMicrodroid() throws DeviceNotAvailableException {
+    public static void rootMicrodroid() throws DeviceNotAvailableException {
         runOnHost("adb", "-s", MICRODROID_SERIAL, "root");
 
         // TODO(192660959): Figure out the root cause and remove the sleep. For unknown reason,
@@ -251,6 +237,12 @@
         // `adb -s $MICRODROID_SERIAL shell ...` often fails with "adb: device offline".
         try {
             Thread.sleep(1000);
+            runOnHostWithTimeout(
+                    MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES * 60 * 1000,
+                    "adb",
+                    "-s",
+                    MICRODROID_SERIAL,
+                    "wait-for-device");
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
         }
@@ -258,12 +250,13 @@
 
     // 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) throws DeviceNotAvailableException {
+    public static void adbConnectToMicrodroid(ITestDevice androidDevice, String cid)
+            throws DeviceNotAvailableException {
         long start = System.currentTimeMillis();
         long timeoutMillis = MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES * 60 * 1000;
         long elapsed = 0;
 
-        final String serial = getDevice().getSerialNumber();
+        final String serial = androidDevice.getSerialNumber();
         final String from = "tcp:" + TEST_VM_ADB_PORT;
         final String to = "vsock:" + cid + ":5555";
         runOnHost("adb", "-s", serial, "forward", from, to);
@@ -297,9 +290,4 @@
         // Check if it actually booted by reading a sysprop.
         assertThat(runOnMicrodroid("getprop", "ro.hardware"), is("microdroid"));
     }
-
-    private void skipIfFail(String command) throws DeviceNotAvailableException {
-        CommandResult result = getDevice().executeShellV2Command(command);
-        assumeThat(result.getStatus(), is(CommandStatus.SUCCESS));
-    }
 }
diff --git a/tests/hostside/java/android/virt/test/AuthFsTestCase.java b/tests/hostside/java/android/virt/test/AuthFsTestCase.java
new file mode 100644
index 0000000..ae29a09
--- /dev/null
+++ b/tests/hostside/java/android/virt/test/AuthFsTestCase.java
@@ -0,0 +1,378 @@
+/*
+ * 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.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
+
+import android.platform.test.annotations.RootPermissionTest;
+
+import com.android.compatibility.common.util.PollingCheck;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.AfterClassWithInfo;
+import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
+import com.android.tradefed.util.CommandResult;
+
+import org.junit.After;
+import org.junit.AssumptionViolatedException;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+@RootPermissionTest
+@RunWith(DeviceJUnit4ClassRunner.class)
+public final class AuthFsTestCase extends VirtualizationTestCaseBase {
+
+    /** Test directory on Android where data are located */
+    private static final String TEST_DIR = "/data/local/tmp/authfs";
+
+    /** Mount point of authfs on Microdroid during the test */
+    private static final String MOUNT_DIR = "/data/local/tmp";
+
+    /** Path to fd_server on Android */
+    private static final String FD_SERVER_BIN = "/apex/com.android.virt/bin/fd_server";
+
+    /** Path to authfs on Microdroid */
+    private static final String AUTHFS_BIN = "/system/bin/authfs";
+
+    /** Plenty of time for authfs to get ready */
+    private static final int AUTHFS_INIT_TIMEOUT_MS = 3000;
+
+    /** FUSE's magic from statfs(2) */
+    private static final String FUSE_SUPER_MAGIC_HEX = "65735546";
+
+    private static CommandRunner sAndroid;
+    private static String sCid;
+    private static boolean sAssumptionFailed;
+
+    private ExecutorService mThreadPool = Executors.newCachedThreadPool();
+
+    @BeforeClassWithInfo
+    public static void beforeClassWithDevice(TestInformation testInfo)
+            throws DeviceNotAvailableException {
+        assertNotNull(testInfo.getDevice());
+        ITestDevice androidDevice = testInfo.getDevice();
+        sAndroid = new CommandRunner(androidDevice);
+
+        try {
+            testIfDeviceIsCapable(androidDevice);
+        } catch (AssumptionViolatedException e) {
+            // NB: The assumption exception is NOT handled by the test infra when it is thrown from
+            // a class method (see b/37502066). This has not only caused the loss of log, but also
+            // prevented the test cases to be reported at all and thus confused the test infra.
+            //
+            // Since we want to avoid the big overhead to start the VM repeatedly on CF, let's catch
+            // AssumptionViolatedException and emulate it artifitially.
+            CLog.e("Assumption failed: " + e);
+            sAssumptionFailed = true;
+            return;
+        }
+
+        prepareVirtualizationTestSetup(androidDevice);
+
+        // For each test case, boot and adb connect to a new Microdroid
+        CLog.i("Starting the shared VM");
+        final String apkName = "MicrodroidTestApp.apk";
+        final String packageName = "com.android.microdroid.test";
+        final String configPath = "assets/vm_config.json"; // path inside the APK
+        sCid =
+                startMicrodroid(
+                        androidDevice,
+                        testInfo.getBuildInfo(),
+                        apkName,
+                        packageName,
+                        configPath,
+                        /* debug */ false);
+        adbConnectToMicrodroid(androidDevice, sCid);
+
+        // Root because authfs (started from shell in this test) currently require root to open
+        // /dev/fuse and mount the FUSE.
+        rootMicrodroid();
+    }
+
+    @AfterClassWithInfo
+    public static void afterClassWithDevice(TestInformation testInfo)
+            throws DeviceNotAvailableException {
+        assertNotNull(sAndroid);
+
+        if (sCid != null) {
+            CLog.i("Shutting down shared VM");
+            shutdownMicrodroid(sAndroid.getDevice(), sCid);
+            sCid = null;
+        }
+
+        cleanUpVirtualizationTestSetup(sAndroid.getDevice());
+        sAndroid = null;
+    }
+
+    @Before
+    public void setUp() {
+        assumeFalse(sAssumptionFailed);
+    }
+
+    @After
+    public void tearDown() throws DeviceNotAvailableException {
+        sAndroid.tryRun("killall fd_server");
+        sAndroid.tryRun("rm -f " + TEST_DIR + "/output");
+
+        tryRunOnMicrodroid("killall authfs");
+        tryRunOnMicrodroid("umount " + MOUNT_DIR);
+    }
+
+    @Test
+    public void testReadWithFsverityVerification_RemoteFile()
+            throws DeviceNotAvailableException, InterruptedException {
+        // Setup
+        runFdServerOnAndroid(
+                "3<input.4m 4<input.4m.merkle_dump 5<input.4m.fsv_sig 6<input.4m",
+                "--ro-fds 3:4:5 --ro-fds 6 --rpc-binder");
+
+        runAuthFsOnMicrodroid(
+                "--remote-ro-file-unverified 10:6:4194304 --remote-ro-file 11:3:4194304:cert.der"
+                        + " --cid 2");
+
+        // Action
+        String actualHashUnverified4m = computeFileHashOnMicrodroid(MOUNT_DIR + "/10");
+        String actualHash4m = computeFileHashOnMicrodroid(MOUNT_DIR + "/11");
+
+        // Verify
+        String expectedHash4m = computeFileHashOnAndroid(TEST_DIR + "/input.4m");
+
+        assertEquals("Inconsistent hash from /authfs/10: ", expectedHash4m, actualHashUnverified4m);
+        assertEquals("Inconsistent hash from /authfs/11: ", expectedHash4m, actualHash4m);
+    }
+
+    // Separate the test from the above simply because exec in shell does not allow open too many
+    // files.
+    @Test
+    public void testReadWithFsverityVerification_RemoteSmallerFile()
+            throws DeviceNotAvailableException, InterruptedException {
+        // Setup
+        runFdServerOnAndroid(
+                "3<input.4k 4<input.4k.merkle_dump 5<input.4k.fsv_sig"
+                        + " 6<input.4k1 7<input.4k1.merkle_dump 8<input.4k1.fsv_sig",
+                "--ro-fds 3:4:5 --ro-fds 6:7:8 --rpc-binder");
+        runAuthFsOnMicrodroid(
+                "--remote-ro-file 10:3:4096:cert.der --remote-ro-file 11:6:4097:cert.der --cid 2");
+
+        // Action
+        String actualHash4k = computeFileHashOnMicrodroid(MOUNT_DIR + "/10");
+        String actualHash4k1 = computeFileHashOnMicrodroid(MOUNT_DIR + "/11");
+
+        // Verify
+        String expectedHash4k = computeFileHashOnAndroid(TEST_DIR + "/input.4k");
+        String expectedHash4k1 = computeFileHashOnAndroid(TEST_DIR + "/input.4k1");
+
+        assertEquals("Inconsistent hash from /authfs/10: ", expectedHash4k, actualHash4k);
+        assertEquals("Inconsistent hash from /authfs/11: ", expectedHash4k1, actualHash4k1);
+    }
+
+    @Test
+    public void testReadWithFsverityVerification_TamperedMerkleTree()
+            throws DeviceNotAvailableException, InterruptedException {
+        // Setup
+        runFdServerOnAndroid(
+                "3<input.4m 4<input.4m.merkle_dump.bad 5<input.4m.fsv_sig",
+                "--ro-fds 3:4:5 --rpc-binder");
+        runAuthFsOnMicrodroid("--remote-ro-file 10:3:4096:cert.der --cid 2");
+
+        // Verify
+        assertFalse(copyFileOnMicrodroid(MOUNT_DIR + "/10", "/dev/null"));
+    }
+
+    @Test
+    public void testWriteThroughCorrectly()
+            throws DeviceNotAvailableException, InterruptedException {
+        // Setup
+        runFdServerOnAndroid("3<>output", "--rw-fds 3 --rpc-binder");
+        runAuthFsOnMicrodroid("--remote-new-rw-file 20:3 --cid 2");
+
+        // Action
+        String srcPath = "/system/bin/linker64";
+        String destPath = MOUNT_DIR + "/20";
+        String backendPath = TEST_DIR + "/output";
+        assertTrue(copyFileOnMicrodroid(srcPath, destPath));
+
+        // Verify
+        String expectedHash = computeFileHashOnMicrodroid(srcPath);
+        expectBackingFileConsistency(destPath, backendPath, expectedHash);
+    }
+
+    @Test
+    public void testWriteFailedIfDetectsTampering()
+            throws DeviceNotAvailableException, InterruptedException {
+        // Setup
+        runFdServerOnAndroid("3<>output", "--rw-fds 3 --rpc-binder");
+        runAuthFsOnMicrodroid("--remote-new-rw-file 20:3 --cid 2");
+
+        String srcPath = "/system/bin/linker64";
+        String destPath = MOUNT_DIR + "/20";
+        String backendPath = TEST_DIR + "/output";
+        assertTrue(copyFileOnMicrodroid(srcPath, destPath));
+
+        // Action
+        // Tampering with the first 2 4K block of the backing file.
+        sAndroid.run("dd if=/dev/zero of=" + backendPath + " bs=1 count=8192");
+
+        // Verify
+        // Write to a block partially requires a read back to calculate the new hash. It should fail
+        // when the content is inconsistent to the known hash. Use direct I/O to avoid simply
+        // writing to the filesystem cache.
+        assertEquals(
+                tryRunOnMicrodroid("dd if=/dev/zero of=" + destPath + " bs=1 count=1024 direct"),
+                null);
+
+        // A full 4K write does not require to read back, so write can succeed even if the backing
+        // block has already been tampered.
+        runOnMicrodroid("dd if=/dev/zero of=" + destPath + " bs=1 count=4096 skip=4096");
+
+        // Otherwise, a partial write with correct backing file should still succeed.
+        runOnMicrodroid("dd if=/dev/zero of=" + destPath + " bs=1 count=1024 skip=8192");
+    }
+
+    @Test
+    public void testFileResize() throws DeviceNotAvailableException, InterruptedException {
+        // Setup
+        runFdServerOnAndroid("3<>output", "--rw-fds 3 --rpc-binder");
+        runAuthFsOnMicrodroid("--remote-new-rw-file 20:3 --cid 2");
+        String outputPath = MOUNT_DIR + "/20";
+        String backendPath = TEST_DIR + "/output";
+
+        // Action & Verify
+        runOnMicrodroid("yes $'\\x01' | tr -d '\\n' | dd bs=1 count=10000 of=" + outputPath);
+        assertEquals(getFileSizeInBytesOnMicrodroid(outputPath), 10000);
+        expectBackingFileConsistency(
+                outputPath,
+                backendPath,
+                "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353");
+
+        resizeFileOnMicrodroid(outputPath, 15000);
+        assertEquals(getFileSizeInBytesOnMicrodroid(outputPath), 15000);
+        expectBackingFileConsistency(
+                outputPath,
+                backendPath,
+                "567c89f62586e0d33369157afdfe99a2fa36cdffb01e91dcdc0b7355262d610d");
+
+        resizeFileOnMicrodroid(outputPath, 5000);
+        assertEquals(getFileSizeInBytesOnMicrodroid(outputPath), 5000);
+        expectBackingFileConsistency(
+                outputPath,
+                backendPath,
+                "e53130831c13dabff71d5d1797e3aaa467b4b7d32b3b8782c4ff03d76976f2aa");
+    }
+
+    private void expectBackingFileConsistency(
+            String authFsPath, String backendPath, String expectedHash)
+            throws DeviceNotAvailableException {
+        String hashOnAuthFs = computeFileHashOnMicrodroid(authFsPath);
+        assertEquals("File hash is different to expectation", expectedHash, hashOnAuthFs);
+
+        String hashOfBackingFile = computeFileHashOnAndroid(backendPath);
+        assertEquals(
+                "Inconsistent file hash on the backend storage", hashOnAuthFs, hashOfBackingFile);
+    }
+
+    private String computeFileHashOnMicrodroid(String path) {
+        String result = runOnMicrodroid("sha256sum " + path);
+        String[] tokens = result.split("\\s");
+        if (tokens.length > 0) {
+            return tokens[0];
+        } else {
+            CLog.e("Unrecognized output by sha256sum: " + result);
+            return "";
+        }
+    }
+
+    private boolean copyFileOnMicrodroid(String src, String dest)
+            throws DeviceNotAvailableException {
+        // TODO(b/182576497): cp returns error because close(2) returns ENOSYS in the current authfs
+        // implementation. We should probably fix that since programs can expect close(2) return 0.
+        String cmd = "cat " + src + " > " + dest;
+        return tryRunOnMicrodroid(cmd) != null;
+    }
+
+    private String computeFileHashOnAndroid(String path) throws DeviceNotAvailableException {
+        String result = sAndroid.run("sha256sum " + path);
+        String[] tokens = result.split("\\s");
+        if (tokens.length > 0) {
+            return tokens[0];
+        } else {
+            CLog.e("Unrecognized output by sha256sum: " + result);
+            return "";
+        }
+    }
+
+    private void resizeFileOnMicrodroid(String path, long size) {
+        runOnMicrodroid("truncate -c -s " + size + " " + path);
+    }
+
+    private long getFileSizeInBytesOnMicrodroid(String path) {
+        return Long.parseLong(runOnMicrodroid("stat -c '%s' " + path));
+    }
+
+    private void runAuthFsOnMicrodroid(String flags) {
+        String cmd = AUTHFS_BIN + " " + MOUNT_DIR + " " + flags;
+
+        mThreadPool.submit(
+                () -> {
+                    CLog.i("Starting authfs");
+                    CommandResult result = runOnMicrodroidForResult(cmd);
+                    CLog.w("authfs has stopped: " + result);
+                });
+        try {
+            PollingCheck.waitFor(
+                    AUTHFS_INIT_TIMEOUT_MS, () -> isMicrodroidDirectoryOnFuse(MOUNT_DIR));
+        } catch (Exception e) {
+            // Convert the broad Exception into an unchecked exception to avoid polluting all other
+            // methods. waitFor throws Exception because the callback, Callable#call(), has a
+            // signature to throw an Exception.
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void runFdServerOnAndroid(String execParamsForOpeningFds, String flags)
+            throws DeviceNotAvailableException {
+        String cmd = "cd " + TEST_DIR + " && exec " + execParamsForOpeningFds + " " + FD_SERVER_BIN
+                + " " + flags;
+        mThreadPool.submit(
+                () -> {
+                    try {
+                        CLog.i("Starting fd_server");
+                        CommandResult result = sAndroid.runForResult(cmd);
+                        CLog.w("fd_server has stopped: " + result);
+                    } catch (DeviceNotAvailableException e) {
+                        CLog.e("Error running fd_server", e);
+                        throw new RuntimeException(e);
+                    }
+                });
+    }
+
+    private boolean isMicrodroidDirectoryOnFuse(String path) {
+        String fs_type = tryRunOnMicrodroid("stat -f -c '%t' " + path);
+        return FUSE_SUPER_MAGIC_HEX.equals(fs_type);
+    }
+}
diff --git a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
index 02fb7e5..d48028e 100644
--- a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
+++ b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
@@ -35,8 +35,15 @@
     @Test
     public void testMicrodroidBoots() throws Exception {
         final String configPath = "assets/vm_config.json"; // path inside the APK
-        final String cid = startMicrodroid(APK_NAME, PACKAGE_NAME, configPath, /* debug */ false);
-        adbConnectToMicrodroid(cid);
+        final String cid =
+                startMicrodroid(
+                        getDevice(),
+                        getBuild(),
+                        APK_NAME,
+                        PACKAGE_NAME,
+                        configPath,
+                        /* debug */ false);
+        adbConnectToMicrodroid(getDevice(), cid);
 
         // Test writing to /data partition
         runOnMicrodroid("echo MicrodroidTest > /data/local/tmp/test.txt");
@@ -73,26 +80,27 @@
         // Check that keystore was found by the payload
         assertThat(runOnMicrodroid("getprop", "debug.microdroid.test.keystore"), is("PASS"));
 
-        shutdownMicrodroid(cid);
+        shutdownMicrodroid(getDevice(), cid);
     }
 
     @Test
     public void testDebugMode() throws Exception {
         final String configPath = "assets/vm_config.json"; // path inside the APK
         final boolean debug = true;
-        final String cid = startMicrodroid(APK_NAME, PACKAGE_NAME, configPath, debug);
-        adbConnectToMicrodroid(cid);
+        final String cid =
+                startMicrodroid(getDevice(), getBuild(), APK_NAME, PACKAGE_NAME, configPath, debug);
+        adbConnectToMicrodroid(getDevice(), cid);
 
         assertThat(runOnMicrodroid("getenforce"), is("Permissive"));
 
-        shutdownMicrodroid(cid);
+        shutdownMicrodroid(getDevice(), cid);
     }
 
     @Before
     public void setUp() throws Exception {
-        testIfDeviceIsCapable();
+        testIfDeviceIsCapable(getDevice());
 
-        prepareVirtualizationTestSetup();
+        prepareVirtualizationTestSetup(getDevice());
 
         getDevice().installPackage(findTestFile(APK_NAME), /* reinstall */ false);
 
@@ -102,7 +110,7 @@
 
     @After
     public void shutdown() throws Exception {
-        cleanUpVirtualizationTestSetup();
+        cleanUpVirtualizationTestSetup(getDevice());
 
         getDevice().uninstallPackage(PACKAGE_NAME);
     }