[benchmarks][authfs] Run AuthFsBenchmarks in postsubmit

Test: atest AuthFsHostTest AuthFsBenchmarks
Bug: 254050475
Change-Id: I2ff801532c73a8b7dcde89c0d87bdf339c447ab4
diff --git a/authfs/tests/common/Android.bp b/authfs/tests/common/Android.bp
new file mode 100644
index 0000000..ec426c7
--- /dev/null
+++ b/authfs/tests/common/Android.bp
@@ -0,0 +1,33 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library_host {
+    name: "AuthFsHostTestCommon",
+    srcs: ["src/java/**/*.java"],
+    libs: [
+        "compatibility-host-util",
+        "compatibility-tradefed",
+        "tradefed",
+    ],
+    static_libs: [
+        "MicrodroidHostTestHelper",
+    ],
+}
+
+rust_test {
+    name: "open_then_run",
+    crate_name: "open_then_run",
+    srcs: ["src/open_then_run.rs"],
+    edition: "2021",
+    rustlibs: [
+        "libandroid_logger",
+        "libanyhow",
+        "libclap",
+        "libcommand_fds",
+        "liblibc",
+        "liblog_rust",
+    ],
+    test_suites: ["general-tests"],
+    test_harness: false,
+}
diff --git a/authfs/tests/common/src/java/com/android/fs/common/AuthFsTestRule.java b/authfs/tests/common/src/java/com/android/fs/common/AuthFsTestRule.java
new file mode 100644
index 0000000..994f23b
--- /dev/null
+++ b/authfs/tests/common/src/java/com/android/fs/common/AuthFsTestRule.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2022 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.fs.common;
+
+import static com.android.microdroid.test.host.LogArchiver.archiveLogThenDelete;
+import static com.android.tradefed.device.TestDevice.MicrodroidBuilder;
+import static com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.compatibility.common.util.PollingCheck;
+import com.android.microdroid.test.host.CommandRunner;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.TestDevice;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.CommandResult;
+
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/** Custom TestRule for AuthFs tests. */
+public class AuthFsTestRule extends TestLogData {
+    /** FUSE's magic from statfs(2) */
+    public static final String FUSE_SUPER_MAGIC_HEX = "65735546";
+
+    /** VM config entry path in the test APK */
+    private static final String VM_CONFIG_PATH_IN_APK = "assets/vm_config.json";
+
+    /** Test directory on Android where data are located */
+    public static final String TEST_DIR = "/data/local/tmp/authfs";
+
+    /** File name of the test APK */
+    private static final String TEST_APK_NAME = "MicrodroidTestApp.apk";
+
+    /** Output directory where the test can generate output on Android */
+    public static final String TEST_OUTPUT_DIR = "/data/local/tmp/authfs/output_dir";
+
+    /** Mount point of authfs on Microdroid during the test */
+    public static final String MOUNT_DIR = "/data/local/tmp/mnt";
+
+    /** VM's log file */
+    private static final String LOG_PATH = TEST_OUTPUT_DIR + "/log.txt";
+
+    /** Path to open_then_run on Android */
+    private static final String OPEN_THEN_RUN_BIN = "/data/local/tmp/open_then_run";
+
+    /** 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;
+
+    private static final int VMADDR_CID_HOST = 2;
+
+    private static TestInformation sTestInfo;
+    private static ITestDevice sMicrodroidDevice;
+    private static CommandRunner sAndroid;
+    private static CommandRunner sMicrodroid;
+
+    private final ExecutorService mThreadPool = Executors.newCachedThreadPool();
+
+    public static void setUpAndroid(TestInformation testInfo) throws Exception {
+        assertNotNull(testInfo.getDevice());
+        if (!(testInfo.getDevice() instanceof TestDevice)) {
+            CLog.w("Unexpected type of ITestDevice. Skipping.");
+            return;
+        }
+        sTestInfo = testInfo;
+        TestDevice androidDevice = getDevice();
+        sAndroid = new CommandRunner(androidDevice);
+
+        // NB: We can't use assumeTrue because the assumption exception is NOT handled by the test
+        // infra when it is thrown from a class method (see b/37502066). We need to skip both here
+        // and in setUp.
+        if (!androidDevice.supportsMicrodroid()) {
+            CLog.i("Microdroid not supported. Skipping.");
+            return;
+        }
+    }
+
+    public static void tearDownAndroid() {
+        sAndroid = null;
+    }
+
+    /** This method is supposed to be called after {@link #setUpTest()}. */
+    public static CommandRunner getAndroid() {
+        assertThat(sAndroid).isNotNull();
+        return sAndroid;
+    }
+
+    /** This method is supposed to be called after {@link #setUpTest()}. */
+    public static CommandRunner getMicrodroid() {
+        assertThat(sMicrodroid).isNotNull();
+        return sMicrodroid;
+    }
+
+    public static ITestDevice getMicrodroidDevice() {
+        assertThat(sMicrodroidDevice).isNotNull();
+        return sMicrodroidDevice;
+    }
+
+    public static void startMicrodroid() throws DeviceNotAvailableException {
+        CLog.i("Starting the shared VM");
+        assertThat(sMicrodroidDevice).isNull();
+        sMicrodroidDevice =
+                MicrodroidBuilder.fromFile(
+                                findTestFile(sTestInfo.getBuildInfo(), TEST_APK_NAME),
+                                VM_CONFIG_PATH_IN_APK)
+                        .debugLevel("full")
+                        .build(getDevice());
+
+        // From this point on, we need to tear down the Microdroid instance
+        sMicrodroid = new CommandRunner(sMicrodroidDevice);
+
+        sMicrodroid.runForResult("mkdir -p " + MOUNT_DIR);
+
+        // Root because authfs (started from shell in this test) currently require root to open
+        // /dev/fuse and mount the FUSE.
+        assertThat(sMicrodroidDevice.enableAdbRoot()).isTrue();
+    }
+
+    public static void shutdownMicrodroid() throws DeviceNotAvailableException {
+        assertNotNull(sMicrodroidDevice);
+        getDevice().shutdownMicrodroid(sMicrodroidDevice);
+        sMicrodroidDevice = null;
+        sMicrodroid = null;
+    }
+
+    @Override
+    public Statement apply(final Statement base, Description description) {
+        return super.apply(
+                new Statement() {
+                    @Override
+                    public void evaluate() throws Throwable {
+                        setUpTest();
+                        base.evaluate();
+                        tearDownTest(description.getMethodName());
+                    }
+                },
+                description);
+    }
+
+    public void runFdServerOnAndroid(String helperFlags, String fdServerFlags)
+            throws DeviceNotAvailableException {
+        String cmd =
+                "cd "
+                        + TEST_DIR
+                        + " && "
+                        + OPEN_THEN_RUN_BIN
+                        + " "
+                        + helperFlags
+                        + " -- "
+                        + FD_SERVER_BIN
+                        + " "
+                        + fdServerFlags;
+        Future<?> unusedFuture = mThreadPool.submit(() -> runForResult(sAndroid, cmd, "fd_server"));
+    }
+
+    public void runAuthFsOnMicrodroid(String flags) {
+        String cmd = AUTHFS_BIN + " " + MOUNT_DIR + " " + flags + " --cid " + VMADDR_CID_HOST;
+
+        AtomicBoolean starting = new AtomicBoolean(true);
+        Future<?> unusedFuture =
+                mThreadPool.submit(
+                        () -> {
+                            // authfs may fail to start if fd_server is not yet listening on the
+                            // vsock
+                            // ("Error: Invalid raw AIBinder"). Just restart if that happens.
+                            while (starting.get()) {
+                                runForResult(sMicrodroid, cmd, "authfs");
+                            }
+                        });
+        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);
+        } finally {
+            starting.set(false);
+        }
+    }
+
+    public static File findTestFile(IBuildInfo buildInfo, String fileName) {
+        try {
+            return (new CompatibilityBuildHelper(buildInfo)).getTestFile(fileName);
+        } catch (FileNotFoundException e) {
+            fail("Missing test file: " + fileName);
+            return null;
+        }
+    }
+
+    private static TestDevice getDevice() {
+        return (TestDevice) sTestInfo.getDevice();
+    }
+
+    private void runForResult(CommandRunner cmdRunner, String cmd, String serviceName) {
+        try {
+            CLog.i("Starting " + serviceName);
+            CommandResult result = cmdRunner.runForResult(cmd);
+            CLog.w(serviceName + " has stopped: " + result);
+        } catch (DeviceNotAvailableException e) {
+            CLog.e("Error running " + serviceName, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    private boolean isMicrodroidDirectoryOnFuse(String path) throws DeviceNotAvailableException {
+        String fs_type = sMicrodroid.tryRun("stat -f -c '%t' " + path);
+        return FUSE_SUPER_MAGIC_HEX.equals(fs_type);
+    }
+
+    private void setUpTest() throws Exception {
+        assumeTrue(getDevice().supportsMicrodroid());
+        sAndroid.run("mkdir -p " + TEST_OUTPUT_DIR);
+    }
+
+    private void tearDownTest(String testName) throws Exception {
+        if (sMicrodroid != null) {
+            sMicrodroid.tryRun("killall authfs");
+            sMicrodroid.tryRun("umount " + MOUNT_DIR);
+        }
+
+        assertNotNull(sAndroid);
+        sAndroid.tryRun("killall fd_server");
+
+        // Even though we only run one VM for the whole class, and could have collect the VM log
+        // after all tests are done, TestLogData doesn't seem to work at class level. Hence,
+        // collect recent logs manually for each test method.
+        String vmRecentLog = TEST_OUTPUT_DIR + "/vm_recent.log";
+        sAndroid.tryRun("tail -n 50 " + LOG_PATH + " > " + vmRecentLog);
+        archiveLogThenDelete(this, getDevice(), vmRecentLog, "vm_recent.log-" + testName);
+
+        sAndroid.run("rm -rf " + TEST_OUTPUT_DIR);
+    }
+}
diff --git a/authfs/tests/common/src/open_then_run.rs b/authfs/tests/common/src/open_then_run.rs
new file mode 100644
index 0000000..110d838
--- /dev/null
+++ b/authfs/tests/common/src/open_then_run.rs
@@ -0,0 +1,170 @@
+/*
+ * 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.
+ */
+
+//! This is a test helper program that opens files and/or directories, then passes the file
+//! descriptors to the specified command. When passing the file descriptors, they are mapped to the
+//! specified numbers in the child process.
+
+use anyhow::{bail, Context, Result};
+use clap::{App, Arg, Values};
+use command_fds::{CommandFdExt, FdMapping};
+use log::{debug, error};
+use std::fs::OpenOptions;
+use std::os::unix::fs::OpenOptionsExt;
+use std::os::unix::io::{AsRawFd, OwnedFd, RawFd};
+use std::process::Command;
+
+// `PseudoRawFd` is just an integer and not necessarily backed by a real FD. It is used to denote
+// the expecting FD number, when trying to set up FD mapping in the child process. The intention
+// with this alias is to improve readability by distinguishing from actual RawFd.
+type PseudoRawFd = RawFd;
+
+struct OwnedFdMapping {
+    owned_fd: OwnedFd,
+    target_fd: PseudoRawFd,
+}
+
+impl OwnedFdMapping {
+    fn as_fd_mapping(&self) -> FdMapping {
+        FdMapping { parent_fd: self.owned_fd.as_raw_fd(), child_fd: self.target_fd }
+    }
+}
+
+struct Args {
+    ro_file_fds: Vec<OwnedFdMapping>,
+    rw_file_fds: Vec<OwnedFdMapping>,
+    dir_fds: Vec<OwnedFdMapping>,
+    cmdline_args: Vec<String>,
+}
+
+fn parse_and_create_file_mapping<F>(
+    values: Option<Values<'_>>,
+    opener: F,
+) -> Result<Vec<OwnedFdMapping>>
+where
+    F: Fn(&str) -> Result<OwnedFd>,
+{
+    if let Some(options) = values {
+        options
+            .map(|option| {
+                // Example option: 10:/some/path
+                let strs: Vec<&str> = option.split(':').collect();
+                if strs.len() != 2 {
+                    bail!("Invalid option: {}", option);
+                }
+                let fd = strs[0].parse::<PseudoRawFd>().context("Invalid FD format")?;
+                let path = strs[1];
+                Ok(OwnedFdMapping { target_fd: fd, owned_fd: opener(path)? })
+            })
+            .collect::<Result<_>>()
+    } else {
+        Ok(Vec::new())
+    }
+}
+
+fn parse_args() -> Result<Args> {
+    #[rustfmt::skip]
+    let matches = App::new("open_then_run")
+        .arg(Arg::with_name("open-ro")
+             .long("open-ro")
+             .value_name("FD:PATH")
+             .help("Open <PATH> read-only to pass as fd <FD>")
+             .multiple(true)
+             .number_of_values(1))
+        .arg(Arg::with_name("open-rw")
+             .long("open-rw")
+             .value_name("FD:PATH")
+             .help("Open/create <PATH> read-write to pass as fd <FD>")
+             .multiple(true)
+             .number_of_values(1))
+        .arg(Arg::with_name("open-dir")
+             .long("open-dir")
+             .value_name("FD:DIR")
+             .help("Open <DIR> to pass as fd <FD>")
+             .multiple(true)
+             .number_of_values(1))
+        .arg(Arg::with_name("args")
+             .help("Command line to execute with pre-opened FD inherited")
+             .last(true)
+             .required(true)
+             .multiple(true))
+        .get_matches();
+
+    let ro_file_fds = parse_and_create_file_mapping(matches.values_of("open-ro"), |path| {
+        Ok(OwnedFd::from(
+            OpenOptions::new()
+                .read(true)
+                .open(path)
+                .with_context(|| format!("Open {} read-only", path))?,
+        ))
+    })?;
+
+    let rw_file_fds = parse_and_create_file_mapping(matches.values_of("open-rw"), |path| {
+        Ok(OwnedFd::from(
+            OpenOptions::new()
+                .read(true)
+                .write(true)
+                .create(true)
+                .open(path)
+                .with_context(|| format!("Open {} read-write", path))?,
+        ))
+    })?;
+
+    let dir_fds = parse_and_create_file_mapping(matches.values_of("open-dir"), |path| {
+        Ok(OwnedFd::from(
+            OpenOptions::new()
+                .custom_flags(libc::O_DIRECTORY)
+                .read(true) // O_DIRECTORY can only be opened with read
+                .open(path)
+                .with_context(|| format!("Open {} directory", path))?,
+        ))
+    })?;
+
+    let cmdline_args: Vec<_> = matches.values_of("args").unwrap().map(|s| s.to_string()).collect();
+
+    Ok(Args { ro_file_fds, rw_file_fds, dir_fds, cmdline_args })
+}
+
+fn try_main() -> Result<()> {
+    let args = parse_args()?;
+
+    let mut command = Command::new(&args.cmdline_args[0]);
+    command.args(&args.cmdline_args[1..]);
+
+    // Set up FD mappings in the child process.
+    let mut fd_mappings = Vec::new();
+    fd_mappings.extend(args.ro_file_fds.iter().map(OwnedFdMapping::as_fd_mapping));
+    fd_mappings.extend(args.rw_file_fds.iter().map(OwnedFdMapping::as_fd_mapping));
+    fd_mappings.extend(args.dir_fds.iter().map(OwnedFdMapping::as_fd_mapping));
+    command.fd_mappings(fd_mappings)?;
+
+    debug!("Spawning {:?}", command);
+    command.spawn()?;
+    Ok(())
+}
+
+fn main() {
+    android_logger::init_once(
+        android_logger::Config::default()
+            .with_tag("open_then_run")
+            .with_min_level(log::Level::Debug),
+    );
+
+    if let Err(e) = try_main() {
+        error!("Failed with {:?}", e);
+        std::process::exit(1);
+    }
+}