1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.fs.common; 18 19 import static com.android.microdroid.test.host.LogArchiver.archiveLogThenDelete; 20 import static com.android.tradefed.device.TestDevice.MicrodroidBuilder; 21 import static com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData; 22 23 import static com.google.common.truth.Truth.assertThat; 24 25 import static org.junit.Assert.assertNotNull; 26 import static org.junit.Assert.fail; 27 28 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; 29 import com.android.compatibility.common.util.PollingCheck; 30 import com.android.microdroid.test.host.CommandRunner; 31 import com.android.tradefed.build.IBuildInfo; 32 import com.android.tradefed.device.DeviceNotAvailableException; 33 import com.android.tradefed.device.ITestDevice; 34 import com.android.tradefed.device.TestDevice; 35 import com.android.tradefed.invoker.TestInformation; 36 import com.android.tradefed.log.LogUtil.CLog; 37 import com.android.tradefed.util.CommandResult; 38 39 import org.junit.runner.Description; 40 import org.junit.runners.model.Statement; 41 42 import java.io.File; 43 import java.io.FileNotFoundException; 44 import java.util.concurrent.ExecutorService; 45 import java.util.concurrent.Executors; 46 import java.util.concurrent.Future; 47 import java.util.concurrent.atomic.AtomicBoolean; 48 49 /** Custom TestRule for AuthFs tests. */ 50 public class AuthFsTestRule extends TestLogData { 51 /** FUSE's magic from statfs(2) */ 52 public static final String FUSE_SUPER_MAGIC_HEX = "65735546"; 53 54 /** VM config entry path in the test APK */ 55 private static final String VM_CONFIG_PATH_IN_APK = "assets/vm_config.json"; 56 57 /** Test directory on Android where data are located */ 58 public static final String TEST_DIR = "/data/local/tmp/authfs"; 59 60 /** File name of the test APK */ 61 private static final String TEST_APK_NAME = "MicrodroidTestApp.apk"; 62 63 /** Output directory where the test can generate output on Android */ 64 public static final String TEST_OUTPUT_DIR = "/data/local/tmp/authfs/output_dir"; 65 66 /** Mount point of authfs on Microdroid during the test */ 67 public static final String MOUNT_DIR = "/data/local/tmp/mnt"; 68 69 /** VM's log file */ 70 private static final String LOG_PATH = TEST_OUTPUT_DIR + "/log.txt"; 71 72 /** Path to open_then_run on Android */ 73 private static final String OPEN_THEN_RUN_BIN = "/data/local/tmp/open_then_run"; 74 75 /** Path to fd_server on Android */ 76 private static final String FD_SERVER_BIN = "/apex/com.android.virt/bin/fd_server"; 77 78 /** Path to authfs on Microdroid */ 79 private static final String AUTHFS_BIN = "/system/bin/authfs"; 80 81 /** Plenty of time for authfs to get ready */ 82 private static final int AUTHFS_INIT_TIMEOUT_MS = 3000; 83 84 private static final int VMADDR_CID_HOST = 2; 85 86 private static TestInformation sTestInfo; 87 private static ITestDevice sMicrodroidDevice; 88 private static CommandRunner sAndroid; 89 private static CommandRunner sMicrodroid; 90 91 private ExecutorService mThreadPool; 92 setUpAndroid(TestInformation testInfo)93 public static void setUpAndroid(TestInformation testInfo) throws Exception { 94 assertNotNull(testInfo.getDevice()); 95 if (!(testInfo.getDevice() instanceof TestDevice)) { 96 CLog.w("Unexpected type of ITestDevice. Skipping."); 97 return; 98 } 99 sTestInfo = testInfo; 100 TestDevice androidDevice = getDevice(); 101 sAndroid = new CommandRunner(androidDevice); 102 } 103 tearDownAndroid()104 public static void tearDownAndroid() { 105 sAndroid = null; 106 } 107 108 /** This method is supposed to be called after {@link #setUpTest()}. */ getAndroid()109 public static CommandRunner getAndroid() { 110 assertThat(sAndroid).isNotNull(); 111 return sAndroid; 112 } 113 114 /** This method is supposed to be called after {@link #setUpTest()}. */ getMicrodroid()115 public static CommandRunner getMicrodroid() { 116 assertThat(sMicrodroid).isNotNull(); 117 return sMicrodroid; 118 } 119 getMicrodroidDevice()120 public static ITestDevice getMicrodroidDevice() { 121 assertThat(sMicrodroidDevice).isNotNull(); 122 return sMicrodroidDevice; 123 } 124 startMicrodroid(boolean protectedVm)125 public static void startMicrodroid(boolean protectedVm) throws DeviceNotAvailableException { 126 CLog.i("Starting the shared VM"); 127 assertThat(sMicrodroidDevice).isNull(); 128 sMicrodroidDevice = 129 MicrodroidBuilder.fromFile( 130 findTestFile(sTestInfo.getBuildInfo(), TEST_APK_NAME), 131 VM_CONFIG_PATH_IN_APK) 132 .debugLevel("full") 133 .protectedVm(protectedVm) 134 .build(getDevice()); 135 136 // From this point on, we need to tear down the Microdroid instance 137 sMicrodroid = new CommandRunner(sMicrodroidDevice); 138 139 sMicrodroid.runForResult("mkdir -p " + MOUNT_DIR); 140 141 // Root because authfs (started from shell in this test) currently require root to open 142 // /dev/fuse and mount the FUSE. 143 assertThat(sMicrodroidDevice.enableAdbRoot()).isTrue(); 144 } 145 shutdownMicrodroid()146 public static void shutdownMicrodroid() throws DeviceNotAvailableException { 147 if (sMicrodroidDevice != null) { 148 getDevice().shutdownMicrodroid(sMicrodroidDevice); 149 sMicrodroidDevice = null; 150 sMicrodroid = null; 151 } 152 } 153 154 @Override apply(final Statement base, Description description)155 public Statement apply(final Statement base, Description description) { 156 return super.apply( 157 new Statement() { 158 @Override 159 public void evaluate() throws Throwable { 160 setUpTest(); 161 base.evaluate(); 162 tearDownTest(description.getMethodName()); 163 } 164 }, 165 description); 166 } 167 168 public void runFdServerOnAndroid(String helperFlags, String fdServerFlags) 169 throws DeviceNotAvailableException { 170 String cmd = 171 "cd " 172 + TEST_DIR 173 + " && " 174 + OPEN_THEN_RUN_BIN 175 + " " 176 + helperFlags 177 + " -- " 178 + FD_SERVER_BIN 179 + " " 180 + fdServerFlags; 181 Future<?> unusedFuture = mThreadPool.submit(() -> runForResult(sAndroid, cmd, "fd_server")); 182 } 183 184 public void killFdServerOnAndroid() throws DeviceNotAvailableException { 185 sAndroid.tryRun("killall fd_server"); 186 } 187 188 public void runAuthFsOnMicrodroid(String flags) { 189 String cmd = AUTHFS_BIN + " " + MOUNT_DIR + " " + flags + " --cid " + VMADDR_CID_HOST; 190 191 AtomicBoolean starting = new AtomicBoolean(true); 192 Future<?> unusedFuture = 193 mThreadPool.submit( 194 () -> { 195 // authfs may fail to start if fd_server is not yet listening on the 196 // vsock 197 // ("Error: Invalid raw AIBinder"). Just restart if that happens. 198 while (starting.get()) { 199 runForResult(sMicrodroid, cmd, "authfs"); 200 } 201 }); 202 try { 203 PollingCheck.waitFor( 204 AUTHFS_INIT_TIMEOUT_MS, () -> isMicrodroidDirectoryOnFuse(MOUNT_DIR)); 205 } catch (Exception e) { 206 // Convert the broad Exception into an unchecked exception to avoid polluting all other 207 // methods. waitFor throws Exception because the callback, Callable#call(), has a 208 // signature to throw an Exception. 209 throw new RuntimeException(e); 210 } finally { 211 starting.set(false); 212 } 213 } 214 215 public static File findTestFile(IBuildInfo buildInfo, String fileName) { 216 try { 217 return (new CompatibilityBuildHelper(buildInfo)).getTestFile(fileName); 218 } catch (FileNotFoundException e) { 219 fail("Missing test file: " + fileName); 220 return null; 221 } 222 } 223 224 public static TestDevice getDevice() { 225 return (TestDevice) sTestInfo.getDevice(); 226 } 227 228 private void runForResult(CommandRunner cmdRunner, String cmd, String serviceName) { 229 try { 230 CLog.i("Starting " + serviceName); 231 CommandResult result = cmdRunner.runForResult(cmd); 232 CLog.w(serviceName + " has stopped: " + result); 233 } catch (DeviceNotAvailableException e) { 234 CLog.e("Error running " + serviceName, e); 235 throw new RuntimeException(e); 236 } 237 } 238 239 private boolean isMicrodroidDirectoryOnFuse(String path) throws DeviceNotAvailableException { 240 String fs_type = sMicrodroid.tryRun("stat -f -c '%t' " + path); 241 return FUSE_SUPER_MAGIC_HEX.equals(fs_type); 242 } 243 244 public void setUpTest() throws Exception { 245 mThreadPool = Executors.newCachedThreadPool(); 246 if (sAndroid != null) { 247 sAndroid.run("mkdir -p " + TEST_OUTPUT_DIR); 248 } 249 } 250 251 private void tearDownTest(String testName) throws Exception { 252 if (sMicrodroid != null) { 253 sMicrodroid.tryRun("killall authfs"); 254 sMicrodroid.tryRun("umount " + MOUNT_DIR); 255 } 256 257 assertNotNull(sAndroid); 258 killFdServerOnAndroid(); 259 260 // Even though we only run one VM for the whole class, and could have collect the VM log 261 // after all tests are done, TestLogData doesn't seem to work at class level. Hence, 262 // collect recent logs manually for each test method. 263 String vmRecentLog = TEST_OUTPUT_DIR + "/vm_recent.log"; 264 sAndroid.tryRun("tail -n 50 " + LOG_PATH + " > " + vmRecentLog); 265 archiveLogThenDelete(this, getDevice(), vmRecentLog, "vm_recent.log-" + testName); 266 267 sAndroid.run("rm -rf " + TEST_OUTPUT_DIR); 268 269 if (mThreadPool != null) { 270 mThreadPool.shutdownNow(); 271 mThreadPool = null; 272 } 273 } 274 } 275