blob: e6081f79099dfb9e9a745935b74e2edd84649efd [file] [log] [blame]
/*
* 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.virt.fs;
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) */
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 */
private 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 */
private static final String TEST_OUTPUT_DIR = "/data/local/tmp/authfs/output_dir";
/** Mount point of authfs on Microdroid during the test */
private static final String MOUNT_DIR = "/data/local/tmp";
/** 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 TestInformation sTestInfo;
private static CommandRunner sAndroid;
private static CommandRunner sMicrodroid;
private final ExecutorService mThreadPool = Executors.newCachedThreadPool();
static void setUpClass(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;
}
// For each test case, boot and adb connect to a new Microdroid
CLog.i("Starting the shared VM");
ITestDevice microdroidDevice =
MicrodroidBuilder.fromFile(
findTestApk(testInfo.getBuildInfo()), VM_CONFIG_PATH_IN_APK)
.debugLevel("full")
.build((TestDevice) androidDevice);
// From this point on, we need to tear down the Microdroid instance
sMicrodroid = new CommandRunner(microdroidDevice);
// Root because authfs (started from shell in this test) currently require root to open
// /dev/fuse and mount the FUSE.
assertThat(microdroidDevice.enableAdbRoot()).isTrue();
}
static void tearDownClass(TestInformation testInfo) throws DeviceNotAvailableException {
assertNotNull(sAndroid);
if (sMicrodroid != null) {
CLog.i("Shutting down shared VM");
((TestDevice) testInfo.getDevice()).shutdownMicrodroid(sMicrodroid.getDevice());
sMicrodroid = null;
}
sAndroid = null;
}
/** This method is supposed to be called after {@link #setUpTest()}. */
static CommandRunner getAndroid() {
assertThat(sAndroid).isNotNull();
return sAndroid;
}
/** This method is supposed to be called after {@link #setUpTest()}. */
static CommandRunner getMicrodroid() {
assertThat(sMicrodroid).isNotNull();
return sMicrodroid;
}
@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);
}
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"));
}
void runAuthFsOnMicrodroid(String flags) {
String cmd = AUTHFS_BIN + " " + MOUNT_DIR + " " + flags;
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);
}
}
private static File findTestApk(IBuildInfo buildInfo) {
try {
return (new CompatibilityBuildHelper(buildInfo)).getTestFile(TEST_APK_NAME);
} catch (FileNotFoundException e) {
fail("Missing test file: " + TEST_APK_NAME);
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);
}
}