authfs: Integration test

This test currently only runs on Android, not VM, to verify existing
features. It needs to be moved into the VM when ready.

Bug: 178874539
Test: atest AuthFsHostTest

Change-Id: I7334b6ae0e684c36a9e350fe148c12a382ef076e
diff --git a/authfs/Android.bp b/authfs/Android.bp
index 4a20a0c..85f2abb 100644
--- a/authfs/Android.bp
+++ b/authfs/Android.bp
@@ -52,7 +52,13 @@
     name: "authfs_device_test_src_lib",
     defaults: ["authfs_defaults"],
     test_suites: ["device-tests"],
-    data: [
+    data: [":authfs_test_files"],
+}
+
+filegroup {
+    name: "authfs_test_files",
+    srcs: [
+        "testdata/cert.der",
         "testdata/input.4k",
         "testdata/input.4k.fsv_sig",
         "testdata/input.4k.merkle_dump",
diff --git a/authfs/src/fsverity/verifier.rs b/authfs/src/fsverity/verifier.rs
index 4021ce1..4af360f 100644
--- a/authfs/src/fsverity/verifier.rs
+++ b/authfs/src/fsverity/verifier.rs
@@ -178,7 +178,7 @@
     use crate::auth::FakeAuthenticator;
     use crate::file::{LocalFileReader, ReadOnlyDataByChunk};
     use anyhow::Result;
-    use std::fs::File;
+    use std::fs::{self, File};
     use std::io::Read;
 
     type LocalVerifiedFileReader = VerifiedFileReader<LocalFileReader, LocalFileReader>;
@@ -276,7 +276,7 @@
         let file_reader = LocalFileReader::new(File::open("testdata/input.4m")?)?;
         let file_size = file_reader.len();
         let merkle_tree = LocalFileReader::new(File::open("testdata/input.4m.merkle_dump")?)?;
-        let sig = include_bytes!("../../testdata/input.4m.fsv_sig").to_vec();
+        let sig = fs::read("testdata/input.4m.fsv_sig")?;
         assert!(VerifiedFileReader::new(&authenticator, file_reader, file_size, sig, merkle_tree)
             .is_err());
         Ok(())
diff --git a/authfs/tests/Android.bp b/authfs/tests/Android.bp
new file mode 100644
index 0000000..56e54f2
--- /dev/null
+++ b/authfs/tests/Android.bp
@@ -0,0 +1,11 @@
+java_test_host {
+    name: "AuthFsHostTest",
+    srcs: ["java/**/*.java"],
+    libs: [
+        "tradefed",
+        "compatibility-tradefed",
+        "compatibility-host-util",
+    ],
+    test_suites: ["general-tests"],
+    data: [":authfs_test_files"],
+}
diff --git a/authfs/tests/AndroidTest.xml b/authfs/tests/AndroidTest.xml
new file mode 100644
index 0000000..485e392
--- /dev/null
+++ b/authfs/tests/AndroidTest.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<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. -->
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+
+    <!-- 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" />
+        <!-- Make sure kernel has FUSE enabled. -->
+        <option name="run-command" value="ls /dev/fuse" />
+        <!-- Make sure necessary executables are installed. -->
+        <option name="run-command" value="ls /apex/com.android.virt/bin/fd_server" />
+        <option name="run-command" value="ls /apex/com.android.virt/bin/authfs" />
+        <!-- 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>
+
+    <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="AuthFsHostTest.jar" />
+    </test>
+</configuration>
diff --git a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
new file mode 100644
index 0000000..3837dd3
--- /dev/null
+++ b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
@@ -0,0 +1,294 @@
+/*
+ * 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 com.android.virt.fs;
+
+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 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;
+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 AuthFsHostTest extends BaseHostJUnit4Test {
+
+    /** Test directory 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";
+
+    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";
+
+    /** Plenty of time for authfs to get ready */
+    private static final int TIME_BUDGET_AUTHFS_SETUP = 1500;  // ms
+
+    private ITestDevice mDevice;
+    private ExecutorService mThreadPool;
+
+    @Before
+    public void setUp() {
+        mDevice = getDevice();
+        mThreadPool = Executors.newCachedThreadPool();
+    }
+
+    @After
+    public void tearDown() throws DeviceNotAvailableException {
+        mDevice.executeShellV2Command("killall authfs fd_server");
+        mDevice.executeShellV2Command("umount " + MOUNT_DIR);
+        mDevice.executeShellV2Command("rm -f " + TEST_DIR);
+    }
+
+    @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"
+        );
+        Thread.sleep(TIME_BUDGET_AUTHFS_SETUP);
+
+        // 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);
+    }
+
+    @Test
+    public void testReadWithFsverityVerification_RemoteFile()
+            throws DeviceNotAvailableException, InterruptedException {
+        // Setup
+        runFdServerInBackground(
+                "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(
+                "--remote-ro-file-unverified 10:6:4194304 --remote-ro-file 11:3:4194304:cert.der"
+        );
+        Thread.sleep(TIME_BUDGET_AUTHFS_SETUP);
+
+        // Action
+        String actualHashUnverified4m = computeFileHashInGuest(MOUNT_DIR + "/10");
+        String actualHash4m = computeFileHashInGuest(MOUNT_DIR + "/11");
+
+        // Verify
+        String expectedHash4m = computeFileHash(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
+        runFdServerInBackground(
+                "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"
+        );
+        Thread.sleep(TIME_BUDGET_AUTHFS_SETUP);
+
+        // Action
+        String actualHash4k = computeFileHashInGuest(MOUNT_DIR + "/10");
+        String actualHash4k1 = computeFileHashInGuest(MOUNT_DIR + "/11");
+
+        // Verify
+        String expectedHash4k = computeFileHash(TEST_DIR + "/input.4k");
+        String expectedHash4k1 = computeFileHash(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
+        runFdServerInBackground(
+                "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");
+        Thread.sleep(TIME_BUDGET_AUTHFS_SETUP);
+
+        // Verify
+        assertFalse(copyFileInGuest(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");
+        Thread.sleep(TIME_BUDGET_AUTHFS_SETUP);
+
+        // Action
+        String srcPath = "/system/bin/linker";
+        String destPath = MOUNT_DIR + "/20";
+        String backendPath = TEST_DIR + "/output";
+        assertTrue(copyFileInGuest(srcPath, destPath));
+
+        // Verify
+        String expectedHash = computeFileHashInGuest(srcPath);
+        String actualHash = computeFileHash(backendPath);
+        assertEquals("Inconsistent file hash on the backend storage", expectedHash, actualHash);
+
+        String actualHashFromAuthFs = computeFileHashInGuest(destPath);
+        assertEquals("Inconsistent file hash when reads from authfs", expectedHash,
+                actualHashFromAuthFs);
+    }
+
+    @Test
+    public void testWriteFailedIfDetectsTampering()
+            throws DeviceNotAvailableException, InterruptedException {
+        // Setup
+        runFdServerInBackground("3<>/output", "--rw-fds 3");
+        runAuthFsInBackground("--remote-new-rw-file 20:3");
+        Thread.sleep(TIME_BUDGET_AUTHFS_SETUP);
+
+        String srcPath = "/system/bin/linker";
+        String destPath = MOUNT_DIR + "/20";
+        String backendPath = TEST_DIR + "/output";
+        assertTrue(copyFileInGuest(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");
+
+        // 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");
+
+        // 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");
+
+        // Otherwise, a partial write with correct backing file should still succeed.
+        expectRemoteCommandToSucceed(
+                "dd if=/dev/zero of=" + destPath + " bs=1 count=1024 skip=8192");
+    }
+
+    // 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);
+        String[] tokens = result.split("\\s");
+        if (tokens.length > 0) {
+            return tokens[0];
+        } else {
+            CLog.e("Unrecognized output by sha256sum: " + result);
+            return "";
+        }
+    }
+
+    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);
+            }
+        });
+    }
+
+    private void runFdServerInBackground(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);
+            }
+        });
+    }
+
+    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/authfs/tools/device-test.sh b/authfs/tools/device-test.sh
deleted file mode 100755
index 82aa6bc..0000000
--- a/authfs/tools/device-test.sh
+++ /dev/null
@@ -1,72 +0,0 @@
-#!/system/bin/sh
-
-# TODO(victorhsieh): Create a standard Android test for continuous integration.
-#
-# How to run this test:
-#
-# Setup:
-# $ adb push testdata/input.4m* /data/local/tmp
-# $ adb push tools/device-test.sh /data/local/tmp/
-#
-# Shell 1:
-# $ adb shell /data/local/tmp/device-test.sh --run-fd-server
-#
-# Shell 2:
-# $ adb shell /data/local/tmp/device-test.sh
-
-cd /data/local/tmp
-cat /dev/null > output
-
-if [[ $1 == "--run-fd-server" ]]; then
-  exec 9</system/bin/sh 8<input.4m 7<input.4m.merkle_dump 6<input.4m \
-    5<input.4m.merkle_dump.bad 4<input.4m.fsv_sig 3<>output \
-    fd_server --ro-fds 9 --ro-fds 8:7:4 --ro-fds 6:5:4 --rw-fds 3
-fi
-
-# Run with -u to enter new namespace.
-if [[ $1 == "-u" ]]; then
-  exec unshare -mUr $0
-fi
-
-MOUNTPOINT=/data/local/tmp/authfs
-trap "umount ${MOUNTPOINT}" EXIT;
-mkdir -p ${MOUNTPOINT}
-
-size=$(du -b /system/bin/sh |awk '{print $1}')
-size2=$(du -b input.4m |awk '{print $1}')
-
-echo "Mounting authfs in background ..."
-
-# TODO(170494765): Replace /dev/null (currently not used) with a valid
-# certificate.
-authfs \
-  ${MOUNTPOINT} \
-  --local-ro-file 2:input.4m:input.4m.merkle_dump:input.4m.fsv_sig:/dev/null \
-  --local-ro-file 3:input.4k1:input.4k1.merkle_dump:input.4k1.fsv_sig:/dev/null \
-  --local-ro-file 4:input.4k:input.4k.merkle_dump:input.4k.fsv_sig:/dev/null \
-  --local-ro-file-unverified 5:/system/bin/sh \
-  --remote-ro-file-unverified 6:9:${size} \
-  --remote-ro-file 7:8:${size2}:/dev/null \
-  --remote-ro-file 8:6:${size2}:/dev/null \
-  --remote-new-rw-file 9:3 \
-  &
-sleep 0.1
-
-echo "Accessing files in authfs ..."
-md5sum ${MOUNTPOINT}/2 input.4m
-echo
-md5sum ${MOUNTPOINT}/3 input.4k1
-echo
-md5sum ${MOUNTPOINT}/4 input.4k
-echo
-md5sum ${MOUNTPOINT}/5 /system/bin/sh
-md5sum ${MOUNTPOINT}/6
-echo
-md5sum ${MOUNTPOINT}/7 input.4m
-echo
-cat input.4m > ${MOUNTPOINT}/9
-md5sum ${MOUNTPOINT}/9 output
-echo
-echo Checking error cases...
-cat /data/local/tmp/authfs/8 2>&1 |grep -q ": I/O error" || echo "Failed to catch the problem"
-echo "Done!"
diff --git a/authfs/tools/test.sh b/authfs/tools/test.sh
deleted file mode 100755
index 9ed3a99..0000000
--- a/authfs/tools/test.sh
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/bin/bash
-
-# Run with -u to enter new namespace.
-if [[ $1 == "-u" ]]; then
-  exec unshare -m -U -r $0
-fi
-
-trap "umount /tmp/mnt" EXIT;
-mkdir -p /tmp/mnt
-
-echo "Mounting authfs in background ..."
-strace -o authfs.strace target/debug/authfs \
-  /tmp/mnt \
-  --local-verified-file 2:testdata/input.4m:testdata/input.4m.merkle_dump:testdata/input.4m.fsv_sig \
-  --local-verified-file 3:testdata/input.4k1:testdata/input.4k1.merkle_dump:testdata/input.4k1.fsv_sig \
-  --local-verified-file 4:testdata/input.4k:testdata/input.4k.merkle_dump:testdata/input.4k.fsv_sig \
-  --local-unverified-file 5:testdata/input.4k \
-  &
-sleep 0.1
-
-echo "Accessing files in authfs ..."
-echo
-md5sum /tmp/mnt/2 testdata/input.4m
-echo
-md5sum /tmp/mnt/3 testdata/input.4k1
-echo
-md5sum /tmp/mnt/4 /tmp/mnt/5 testdata/input.4k
-echo
-dd if=/tmp/mnt/2 bs=1000 skip=100 count=50 status=none |md5sum
-dd if=testdata/input.4m bs=1000 skip=100 count=50 status=none |md5sum
-echo
-tac /tmp/mnt/4 |md5sum
-tac /tmp/mnt/5 |md5sum
-tac testdata/input.4k |md5sum
-echo
-test -f /tmp/mnt/2 || echo 'FAIL: an expected file is missing'
-test -f /tmp/mnt/0 && echo 'FAIL: unexpected file presents'
-test -f /tmp/mnt/1 && echo 'FAIL: unexpected file presents, 1 is root dir'
-test -f /tmp/mnt/100 && echo 'FAIL: unexpected file presents'
-test -f /tmp/mnt/foo && echo 'FAIL: unexpected file presents'
-test -f /tmp/mnt/dir/3 && echo 'FAIL: unexpected file presents'
-echo "Done!"