Move AuthFsHostTest into VM

This change also drops test coverage for local file. It was only used
for development purpose, and is not worth to keep it work with a VM.

Bug: 191056545
Test: atest AuthFsHostTest on CF
Test: atest MicrodroidHostTestCases on CF

Change-Id: Ie2e3d8ecca00aee3cf518edeb3a81a9f59d9671c
diff --git a/authfs/tests/Android.bp b/authfs/tests/Android.bp
index bacb890..8061c56 100644
--- a/authfs/tests/Android.bp
+++ b/authfs/tests/Android.bp
@@ -10,6 +10,12 @@
         "compatibility-tradefed",
         "compatibility-host-util",
     ],
+    static_libs: [
+        "VirtualizationTestHelper",
+    ],
     test_suites: ["general-tests"],
-    data: [":authfs_test_files"],
+    data: [
+        ":authfs_test_files",
+        ":MicrodroidTestApp.signed",
+    ],
 }
diff --git a/authfs/tests/AndroidTest.xml b/authfs/tests/AndroidTest.xml
index 485e392..8f940f6 100644
--- a/authfs/tests/AndroidTest.xml
+++ b/authfs/tests/AndroidTest.xml
@@ -15,10 +15,13 @@
 -->
 
 <configuration description="Config for authfs tests">
-    <!-- Since Android does not support user namespace, we need root to access /dev/fuse and also
-         to set up the mount. -->
+    <!-- Need root to start virtualizationservice -->
     <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
 
+    <!-- virtualizationservice doesn't have access to shell_data_file. Instead of giving it
+          a test-only permission, run it without selinux -->
+    <target_preparer class="com.android.tradefed.targetprep.DisableSELinuxTargetPreparer"/>
+
     <!-- Basic checks that the device has all the prerequisites. -->
     <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
         <option name="throw-if-cmd-fail" value="true" />
diff --git a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
index 43d1210..426b333 100644
--- a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
+++ b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
@@ -18,19 +18,16 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 
 import android.platform.test.annotations.RootPermissionTest;
+import android.virt.test.VirtualizationTestCaseBase;
 
 import com.android.compatibility.common.util.PollingCheck;
 import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
 import com.android.tradefed.util.CommandResult;
-import com.android.tradefed.util.CommandStatus;
 
 import org.junit.After;
 import org.junit.Before;
@@ -40,18 +37,22 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
+// TODO move to Virtualization/tests/hostside/
 @RootPermissionTest
 @RunWith(DeviceJUnit4ClassRunner.class)
-public final class AuthFsHostTest extends BaseHostJUnit4Test {
+public final class AuthFsHostTest extends VirtualizationTestCaseBase {
 
-    /** Test directory where data are located */
+    /** Test directory on Android where data are located */
     private static final String TEST_DIR = "/data/local/tmp/authfs";
 
-    /** Mount point of authfs during the test */
-    private static final String MOUNT_DIR = "/data/local/tmp/authfs/mnt";
+    /** 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";
-    private static final String AUTHFS_BIN = "/apex/com.android.virt/bin/authfs";
+
+    /** 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 = 1500;
@@ -59,68 +60,65 @@
     /** FUSE's magic from statfs(2) */
     private static final String FUSE_SUPER_MAGIC_HEX = "65735546";
 
-    private ITestDevice mDevice;
     private ExecutorService mThreadPool;
+    private String mCid;
 
     @Before
-    public void setUp() {
-        mDevice = getDevice();
+    public void setUp() throws DeviceNotAvailableException {
+        testIfDeviceIsCapable();
+
+        cleanUpTestFiles();
+
+        prepareVirtualizationTestSetup();
+
         mThreadPool = Executors.newCachedThreadPool();
+
+        // For each test case, boot and adb connect to a new Microdroid
+        final String apkName = "MicrodroidTestApp.apk";
+        final String packageName = "com.android.microdroid.test";
+        final String configPath = "assets/vm_config.json"; // path inside the APK
+        mCid = startMicrodroid(apkName, packageName, configPath, /* debug */ false);
+        adbConnectToMicrodroid(mCid);
+
+        // Root because authfs (started from shell in this test) currently require root to open
+        // /dev/fuse and mount the FUSE.
+        rootMicrodroid();
     }
 
     @After
     public void tearDown() throws DeviceNotAvailableException {
-        mDevice.executeShellV2Command("killall authfs fd_server");
-        mDevice.executeShellV2Command("umount " + MOUNT_DIR);
-        mDevice.executeShellV2Command("rm -f " + TEST_DIR + "/output");
+        if (mCid != null) {
+            shutdownMicrodroid(mCid);
+            mCid = null;
+        }
+
+        tryRunOnAndroid("killall fd_server");
+        cleanUpTestFiles();
+        cleanUpVirtualizationTestSetup();
     }
 
-    @Test
-    public void testReadWithFsverityVerification_LocalFile()
-            throws DeviceNotAvailableException, InterruptedException {
-        // Setup
-        runAuthFsInBackground(
-                "--local-ro-file-unverified 3:input.4m"
-                + " --local-ro-file 4:input.4m:input.4m.merkle_dump:input.4m.fsv_sig:cert.der"
-                + " --local-ro-file 5:input.4k1:input.4k1.merkle_dump:input.4k1.fsv_sig:cert.der"
-                + " --local-ro-file 6:input.4k:input.4k.merkle_dump:input.4k.fsv_sig:cert.der"
-        );
-
-        // Action
-        String actualHashUnverified4m = computeFileHashInGuest(MOUNT_DIR + "/3");
-        String actualHash4m = computeFileHashInGuest(MOUNT_DIR + "/4");
-        String actualHash4k1 = computeFileHashInGuest(MOUNT_DIR + "/5");
-        String actualHash4k = computeFileHashInGuest(MOUNT_DIR + "/6");
-
-        // Verify
-        String expectedHash4m = computeFileHash(TEST_DIR + "/input.4m");
-        String expectedHash4k1 = computeFileHash(TEST_DIR + "/input.4k1");
-        String expectedHash4k = computeFileHash(TEST_DIR + "/input.4k");
-
-        assertEquals("Inconsistent hash from /authfs/3: ", expectedHash4m, actualHashUnverified4m);
-        assertEquals("Inconsistent hash from /authfs/4: ", expectedHash4m, actualHash4m);
-        assertEquals("Inconsistent hash from /authfs/5: ", expectedHash4k1, actualHash4k1);
-        assertEquals("Inconsistent hash from /authfs/6: ", expectedHash4k, actualHash4k);
+    private void cleanUpTestFiles() throws DeviceNotAvailableException {
+        tryRunOnAndroid("rm -f " + TEST_DIR + "/output");
     }
 
     @Test
     public void testReadWithFsverityVerification_RemoteFile()
             throws DeviceNotAvailableException, InterruptedException {
         // Setup
-        runFdServerInBackground(
+        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"
-        );
-        runAuthFsInBackground(
+                "--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 = computeFileHashInGuest(MOUNT_DIR + "/10");
-        String actualHash4m = computeFileHashInGuest(MOUNT_DIR + "/11");
+        String actualHashUnverified4m = computeFileHashOnMicrodroid(MOUNT_DIR + "/10");
+        String actualHash4m = computeFileHashOnMicrodroid(MOUNT_DIR + "/11");
 
         // Verify
-        String expectedHash4m = computeFileHash(TEST_DIR + "/input.4m");
+        String expectedHash4m = computeFileHashOnAndroid(TEST_DIR + "/input.4m");
 
         assertEquals("Inconsistent hash from /authfs/10: ", expectedHash4m, actualHashUnverified4m);
         assertEquals("Inconsistent hash from /authfs/11: ", expectedHash4m, actualHash4m);
@@ -132,22 +130,20 @@
     public void testReadWithFsverityVerification_RemoteSmallerFile()
             throws DeviceNotAvailableException, InterruptedException {
         // Setup
-        runFdServerInBackground(
+        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"
-        );
-        runAuthFsInBackground(
-                "--remote-ro-file 10:3:4096:cert.der --remote-ro-file 11:6:4097:cert.der"
-        );
+                        + " 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 = computeFileHashInGuest(MOUNT_DIR + "/10");
-        String actualHash4k1 = computeFileHashInGuest(MOUNT_DIR + "/11");
+        String actualHash4k = computeFileHashOnMicrodroid(MOUNT_DIR + "/10");
+        String actualHash4k1 = computeFileHashOnMicrodroid(MOUNT_DIR + "/11");
 
         // Verify
-        String expectedHash4k = computeFileHash(TEST_DIR + "/input.4k");
-        String expectedHash4k1 = computeFileHash(TEST_DIR + "/input.4k1");
+        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);
@@ -157,31 +153,30 @@
     public void testReadWithFsverityVerification_TamperedMerkleTree()
             throws DeviceNotAvailableException, InterruptedException {
         // Setup
-        runFdServerInBackground(
+        runFdServerOnAndroid(
                 "3<input.4m 4<input.4m.merkle_dump.bad 5<input.4m.fsv_sig",
-                "--ro-fds 3:4:5"
-        );
-        runAuthFsInBackground("--remote-ro-file 10:3:4096:cert.der");
+                "--ro-fds 3:4:5 --rpc-binder");
+        runAuthFsOnMicrodroid("--remote-ro-file 10:3:4096:cert.der --cid 2");
 
         // Verify
-        assertFalse(copyFileInGuest(MOUNT_DIR + "/10", "/dev/null"));
+        assertFalse(copyFileOnMicrodroid(MOUNT_DIR + "/10", "/dev/null"));
     }
 
     @Test
     public void testWriteThroughCorrectly()
             throws DeviceNotAvailableException, InterruptedException {
         // Setup
-        runFdServerInBackground("3<>output", "--rw-fds 3");
-        runAuthFsInBackground("--remote-new-rw-file 20:3");
+        runFdServerOnAndroid("3<>output", "--rw-fds 3 --rpc-binder");
+        runAuthFsOnMicrodroid("--remote-new-rw-file 20:3 --cid 2");
 
         // Action
-        String srcPath = "/system/bin/linker";
+        String srcPath = "/system/bin/linker64";
         String destPath = MOUNT_DIR + "/20";
         String backendPath = TEST_DIR + "/output";
-        assertTrue(copyFileInGuest(srcPath, destPath));
+        assertTrue(copyFileOnMicrodroid(srcPath, destPath));
 
         // Verify
-        String expectedHash = computeFileHashInGuest(srcPath);
+        String expectedHash = computeFileHashOnMicrodroid(srcPath);
         expectBackingFileConsistency(destPath, backendPath, expectedHash);
     }
 
@@ -189,60 +184,59 @@
     public void testWriteFailedIfDetectsTampering()
             throws DeviceNotAvailableException, InterruptedException {
         // Setup
-        runFdServerInBackground("3<>output", "--rw-fds 3");
-        runAuthFsInBackground("--remote-new-rw-file 20:3");
+        runFdServerOnAndroid("3<>output", "--rw-fds 3 --rpc-binder");
+        runAuthFsOnMicrodroid("--remote-new-rw-file 20:3 --cid 2");
 
-        String srcPath = "/system/bin/linker";
+        String srcPath = "/system/bin/linker64";
         String destPath = MOUNT_DIR + "/20";
         String backendPath = TEST_DIR + "/output";
-        assertTrue(copyFileInGuest(srcPath, destPath));
+        assertTrue(copyFileOnMicrodroid(srcPath, destPath));
 
         // Action
         // Tampering with the first 2 4K block of the backing file.
-        expectRemoteCommandToSucceed("dd if=/dev/zero of=" + backendPath + " bs=1 count=8192");
+        runOnAndroid("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.
-        expectRemoteCommandToFail("dd if=/dev/zero of=" + destPath + " bs=1 count=1024 direct");
+        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.
-        expectRemoteCommandToSucceed(
-                "dd if=/dev/zero of=" + destPath + " bs=1 count=4096 skip=4096");
+        runOnMicrodroid("dd if=/dev/zero of=" + destPath + " bs=1 count=4096 skip=4096");
 
         // Otherwise, a partial write with correct backing file should still succeed.
-        expectRemoteCommandToSucceed(
-                "dd if=/dev/zero of=" + destPath + " bs=1 count=1024 skip=8192");
+        runOnMicrodroid("dd if=/dev/zero of=" + destPath + " bs=1 count=1024 skip=8192");
     }
 
     @Test
     public void testFileResize() throws DeviceNotAvailableException, InterruptedException {
         // Setup
-        runFdServerInBackground("3<>output", "--rw-fds 3");
-        runAuthFsInBackground("--remote-new-rw-file 20:3");
+        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
-        expectRemoteCommandToSucceed(
-                "yes $'\\x01' | tr -d '\\n' | dd bs=1 count=10000 of=" + outputPath);
-        assertEquals(getFileSizeInBytes(outputPath), 10000);
+        runOnMicrodroid("yes $'\\x01' | tr -d '\\n' | dd bs=1 count=10000 of=" + outputPath);
+        assertEquals(getFileSizeInBytesOnMicrodroid(outputPath), 10000);
         expectBackingFileConsistency(
                 outputPath,
                 backendPath,
                 "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353");
 
-        resizeFile(outputPath, 15000);
-        assertEquals(getFileSizeInBytes(outputPath), 15000);
+        resizeFileOnMicrodroid(outputPath, 15000);
+        assertEquals(getFileSizeInBytesOnMicrodroid(outputPath), 15000);
         expectBackingFileConsistency(
                 outputPath,
                 backendPath,
                 "567c89f62586e0d33369157afdfe99a2fa36cdffb01e91dcdc0b7355262d610d");
 
-        resizeFile(outputPath, 5000);
-        assertEquals(getFileSizeInBytes(outputPath), 5000);
+        resizeFileOnMicrodroid(outputPath, 5000);
+        assertEquals(getFileSizeInBytesOnMicrodroid(outputPath), 5000);
         expectBackingFileConsistency(
                 outputPath,
                 backendPath,
@@ -252,30 +246,16 @@
     private void expectBackingFileConsistency(
             String authFsPath, String backendPath, String expectedHash)
             throws DeviceNotAvailableException {
-        String hashOnAuthFs = computeFileHashInGuest(authFsPath);
+        String hashOnAuthFs = computeFileHashOnMicrodroid(authFsPath);
         assertEquals("File hash is different to expectation", expectedHash, hashOnAuthFs);
 
-        String hashOfBackingFile = computeFileHash(backendPath);
+        String hashOfBackingFile = computeFileHashOnAndroid(backendPath);
         assertEquals(
                 "Inconsistent file hash on the backend storage", hashOnAuthFs, hashOfBackingFile);
     }
 
-    // TODO(b/178874539): This does not really run in the guest VM.  Send the shell command to the
-    // guest VM when authfs works across VM boundary.
-    private String computeFileHashInGuest(String path) throws DeviceNotAvailableException {
-        return computeFileHash(path);
-    }
-
-    private boolean copyFileInGuest(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;
-        CommandResult result = mDevice.executeShellV2Command(cmd);
-        return result.getStatus() == CommandStatus.SUCCESS;
-    }
-
-    private String computeFileHash(String path) throws DeviceNotAvailableException {
-        String result = expectRemoteCommandToSucceed("sha256sum " + path);
+    private String computeFileHashOnMicrodroid(String path) {
+        String result = runOnMicrodroid("sha256sum " + path);
         String[] tokens = result.split("\\s");
         if (tokens.length > 0) {
             return tokens[0];
@@ -285,18 +265,46 @@
         }
     }
 
-    private void resizeFile(String path, long size) throws DeviceNotAvailableException {
-        expectRemoteCommandToSucceed("truncate -c -s " + size + " " + path);
+    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 long getFileSizeInBytes(String path) throws DeviceNotAvailableException {
-        return Long.parseLong(expectRemoteCommandToSucceed("stat -c '%s' " + path));
-    }
-
-    private void throwDowncastedException(Exception e) throws DeviceNotAvailableException {
-        if (e instanceof DeviceNotAvailableException) {
-            throw (DeviceNotAvailableException) e;
+    private String computeFileHashOnAndroid(String path) throws DeviceNotAvailableException {
+        String result = runOnAndroid("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.
@@ -304,56 +312,25 @@
         }
     }
 
-    private void runAuthFsInBackground(String flags) throws DeviceNotAvailableException {
-        String cmd = "cd " + TEST_DIR + " && " + AUTHFS_BIN + " " + MOUNT_DIR + " " + flags;
-
-        mThreadPool.submit(() -> {
-            try {
-                CLog.i("Starting authfs");
-                expectRemoteCommandToSucceed(cmd);
-            } catch (DeviceNotAvailableException e) {
-                CLog.e("Error running authfs", e);
-                throw new RuntimeException(e);
-            }
-        });
-        try {
-            PollingCheck.waitFor(AUTHFS_INIT_TIMEOUT_MS, () -> isRemoteDirectoryOnFuse(MOUNT_DIR));
-        } catch (Exception e) {
-            throwDowncastedException(e);
-        }
-    }
-
-    private void runFdServerInBackground(String execParamsForOpeningFds, String flags)
+    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");
-                expectRemoteCommandToSucceed(cmd);
-            } catch (DeviceNotAvailableException e) {
-                CLog.e("Error running fd_server", e);
-                throw new RuntimeException(e);
-            }
-        });
+        mThreadPool.submit(
+                () -> {
+                    try {
+                        CLog.i("Starting fd_server");
+                        CommandResult result = getDevice().executeShellV2Command(cmd);
+                        CLog.w("fd_server has stopped: " + result);
+                    } catch (DeviceNotAvailableException e) {
+                        CLog.e("Error running fd_server", e);
+                        throw new RuntimeException(e);
+                    }
+                });
     }
 
-    private boolean isRemoteDirectoryOnFuse(String path) throws DeviceNotAvailableException {
-        String fs_type = expectRemoteCommandToSucceed("stat -f -c '%t' " + path);
+    private boolean isMicrodroidDirectoryOnFuse(String path) {
+        String fs_type = tryRunOnMicrodroid("stat -f -c '%t' " + path);
         return FUSE_SUPER_MAGIC_HEX.equals(fs_type);
     }
-
-    private String expectRemoteCommandToSucceed(String cmd) throws DeviceNotAvailableException {
-        CommandResult result = mDevice.executeShellV2Command(cmd);
-        assertEquals("`" + cmd + "` failed: " + result.getStderr(), CommandStatus.SUCCESS,
-                result.getStatus());
-        CLog.d("Stdout: " + result.getStdout());
-        return result.getStdout().trim();
-    }
-
-    private void expectRemoteCommandToFail(String cmd) throws DeviceNotAvailableException {
-        CommandResult result = mDevice.executeShellV2Command(cmd);
-        assertNotEquals("Unexpected success from `" + cmd + "`: " + result.getStdout(),
-                result.getStatus(), CommandStatus.SUCCESS);
-    }
 }
diff --git a/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java b/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
index 851aa30..6d43760 100644
--- a/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
+++ b/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
@@ -23,12 +23,15 @@
 import static org.junit.Assume.assumeThat;
 
 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.log.LogUtil.CLog;
 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.io.FileNotFoundException;
 import java.util.Arrays;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -45,7 +48,9 @@
     // Set the maximum timeout value big enough.
     private static final long MICRODROID_BOOT_TIMEOUT_MINUTES = 5;
 
-    public void prepareVirtualizationTestSetup() throws Exception {
+    private static final long MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES = 5;
+
+    public void prepareVirtualizationTestSetup() throws DeviceNotAvailableException {
         // kill stale crosvm processes
         tryRunOnAndroid("killall", "crosvm");
 
@@ -57,7 +62,7 @@
         tryRunOnHost("adb", "disconnect", MICRODROID_SERIAL);
     }
 
-    public void cleanUpVirtualizationTestSetup() throws Exception {
+    public void cleanUpVirtualizationTestSetup() throws DeviceNotAvailableException {
         // disconnect from microdroid
         tryRunOnHost("adb", "disconnect", MICRODROID_SERIAL);
 
@@ -67,7 +72,7 @@
         tryRunOnAndroid("stop", "virtualizationservice");
     }
 
-    public void testIfDeviceIsCapable() throws Exception {
+    public void testIfDeviceIsCapable() throws DeviceNotAvailableException {
         // Checks the preconditions to run microdroid. If the condition is not satisfied
         // don't run the test (instead of failing)
         skipIfFail("ls /dev/kvm");
@@ -96,7 +101,7 @@
     }
 
     // Run a shell command on Android. the default timeout is 2 min by tradefed
-    private String runOnAndroid(String... cmd) throws Exception {
+    public String runOnAndroid(String... cmd) throws DeviceNotAvailableException {
         CommandResult result = getDevice().executeShellV2Command(join(cmd));
         if (result.getStatus() != CommandStatus.SUCCESS) {
             fail(join(cmd) + " has failed: " + result);
@@ -104,13 +109,19 @@
         return result.getStdout().trim();
     }
 
-    // Same as runOnAndroid, but failure is not an error
-    private String tryRunOnAndroid(String... cmd) throws Exception {
+    // Same as runOnAndroid, but returns null on error.
+    public String tryRunOnAndroid(String... cmd) throws DeviceNotAvailableException {
         CommandResult result = getDevice().executeShellV2Command(join(cmd));
-        return result.getStdout().trim();
+        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 Exception {
+    private String runOnAndroidWithTimeout(long timeoutMillis, String... cmd)
+            throws DeviceNotAvailableException {
         CommandResult result =
                 getDevice()
                         .executeShellV2Command(
@@ -125,26 +136,46 @@
 
     // 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));
+        CommandResult result = runOnMicrodroidForResult(cmd);
         if (result.getStatus() != CommandStatus.SUCCESS) {
             fail(join(cmd) + " has failed: " + result);
         }
         return result.getStdout().trim();
     }
 
+    // Same as runOnMicrodroid, but returns null on error.
+    public String tryRunOnMicrodroid(String... cmd) {
+        CommandResult result = runOnMicrodroidForResult(cmd);
+        if (result.getStatus() == CommandStatus.SUCCESS) {
+            return result.getStdout().trim();
+        } else {
+            CLog.d(join(cmd) + " has failed (but ok): " + result);
+            return null;
+        }
+    }
+
+    public 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) {
         return String.join(" ", Arrays.asList(strs));
     }
 
-    public File findTestFile(String name) throws Exception {
-        return (new CompatibilityBuildHelper(getBuild())).getTestFile(name);
+    public File findTestFile(String name) {
+        try {
+            return (new CompatibilityBuildHelper(getBuild())).getTestFile(name);
+        } catch (FileNotFoundException e) {
+            fail("Missing test file: " + name);
+            return null;
+        }
     }
 
     public String startMicrodroid(
-            String apkName, String packageName, String configPath, boolean debug) throws Exception {
+            String apkName, String packageName, String configPath, boolean debug)
+            throws DeviceNotAvailableException {
         // Install APK
         File apkFile = findTestFile(apkName);
         getDevice().installPackage(apkFile, /* reinstall */ true);
@@ -202,16 +233,38 @@
         return matcher.group(1);
     }
 
-    public void shutdownMicrodroid(String cid) throws Exception {
+    public void shutdownMicrodroid(String cid) throws DeviceNotAvailableException {
         // Shutdown microdroid
         runOnAndroid(VIRT_APEX + "bin/vm", "stop", cid);
+
+        // TODO(192660485): Figure out why shutting down the VM disconnects adb on cuttlefish
+        // temporarily. Without this wait, the rest of `runOnAndroid/skipIfFail` fails due to the
+        // connection loss, and results in assumption error exception for the rest of the tests.
+        try {
+            Thread.sleep(1000);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+    }
+
+    public void rootMicrodroid() throws DeviceNotAvailableException {
+        runOnHost("adb", "-s", MICRODROID_SERIAL, "root");
+
+        // TODO(192660959): Figure out the root cause and remove the sleep. For unknown reason,
+        // even though `adb root` actually wait-for-disconnect then wait-for-device, the next
+        // `adb -s $MICRODROID_SERIAL shell ...` often fails with "adb: device offline".
+        try {
+            Thread.sleep(1000);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
     }
 
     // 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 {
+    public void adbConnectToMicrodroid(String cid) throws DeviceNotAvailableException {
         long start = System.currentTimeMillis();
-        long timeoutMillis = timeoutMinutes * 60 * 1000;
+        long timeoutMillis = MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES * 60 * 1000;
         long elapsed = 0;
 
         final String serial = getDevice().getSerialNumber();
@@ -249,7 +302,7 @@
         assertThat(runOnMicrodroid("getprop", "ro.hardware"), is("microdroid"));
     }
 
-    private void skipIfFail(String command) throws Exception {
+    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/MicrodroidTestCase.java b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
index 0774503..02fb7e5 100644
--- a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
+++ b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
@@ -29,7 +29,6 @@
 
 @RunWith(DeviceJUnit4ClassRunner.class)
 public class MicrodroidTestCase extends VirtualizationTestCaseBase {
-    private static final long MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES = 5;
     private static final String APK_NAME = "MicrodroidTestApp.apk";
     private static final String PACKAGE_NAME = "com.android.microdroid.test";
 
@@ -37,7 +36,7 @@
     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, MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES);
+        adbConnectToMicrodroid(cid);
 
         // Test writing to /data partition
         runOnMicrodroid("echo MicrodroidTest > /data/local/tmp/test.txt");
@@ -82,7 +81,7 @@
         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, MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES);
+        adbConnectToMicrodroid(cid);
 
         assertThat(runOnMicrodroid("getenforce"), is("Permissive"));