1 /* 2 * Copyright (C) 2021 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 android.virt.test; 18 19 import static com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData; 20 21 import static org.hamcrest.CoreMatchers.is; 22 import static org.junit.Assert.assertThat; 23 import static org.junit.Assert.assertTrue; 24 import static org.junit.Assert.fail; 25 import static org.junit.Assume.assumeTrue; 26 27 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; 28 import com.android.tradefed.build.IBuildInfo; 29 import com.android.tradefed.device.DeviceNotAvailableException; 30 import com.android.tradefed.device.ITestDevice; 31 import com.android.tradefed.device.TestDevice; 32 import com.android.tradefed.log.LogUtil.CLog; 33 import com.android.tradefed.result.FileInputStreamSource; 34 import com.android.tradefed.result.LogDataType; 35 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; 36 import com.android.tradefed.util.CommandResult; 37 import com.android.tradefed.util.CommandStatus; 38 import com.android.tradefed.util.RunUtil; 39 40 import java.io.File; 41 import java.io.FileNotFoundException; 42 import java.util.ArrayList; 43 import java.util.Arrays; 44 import java.util.Optional; 45 import java.util.concurrent.ExecutorService; 46 import java.util.concurrent.Executors; 47 import java.util.regex.Matcher; 48 import java.util.regex.Pattern; 49 50 public abstract class VirtualizationTestCaseBase extends BaseHostJUnit4Test { 51 protected static final String TEST_ROOT = "/data/local/tmp/virt/"; 52 protected static final String VIRT_APEX = "/apex/com.android.virt/"; 53 protected static final String LOG_PATH = TEST_ROOT + "log.txt"; 54 private static final int TEST_VM_ADB_PORT = 8000; 55 private static final String MICRODROID_SERIAL = "localhost:" + TEST_VM_ADB_PORT; 56 private static final String INSTANCE_IMG = "instance.img"; 57 58 // This is really slow on GCE (2m 40s) but fast on localhost or actual Android phones (< 10s). 59 // Then there is time to run the actual task. Set the maximum timeout value big enough. 60 private static final long MICRODROID_MAX_LIFETIME_MINUTES = 20; 61 62 private static final long MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES = 5; 63 prepareVirtualizationTestSetup(ITestDevice androidDevice)64 public static void prepareVirtualizationTestSetup(ITestDevice androidDevice) 65 throws DeviceNotAvailableException { 66 CommandRunner android = new CommandRunner(androidDevice); 67 68 // kill stale crosvm processes 69 android.tryRun("killall", "crosvm"); 70 71 // disconnect from microdroid 72 tryRunOnHost("adb", "disconnect", MICRODROID_SERIAL); 73 74 // remove any leftover files under test root 75 android.tryRun("rm", "-rf", TEST_ROOT + "*"); 76 } 77 cleanUpVirtualizationTestSetup(ITestDevice androidDevice)78 public static void cleanUpVirtualizationTestSetup(ITestDevice androidDevice) 79 throws DeviceNotAvailableException { 80 CommandRunner android = new CommandRunner(androidDevice); 81 82 // disconnect from microdroid 83 tryRunOnHost("adb", "disconnect", MICRODROID_SERIAL); 84 85 // kill stale VMs and directories 86 android.tryRun("killall", "crosvm"); 87 android.tryRun("stop", "virtualizationservice"); 88 android.tryRun("rm", "-rf", "/data/misc/virtualizationservice/*"); 89 } 90 testIfDeviceIsCapable(ITestDevice androidDevice)91 public static void testIfDeviceIsCapable(ITestDevice androidDevice) throws Exception { 92 assumeTrue("Need an actual TestDevice", androidDevice instanceof TestDevice); 93 TestDevice testDevice = (TestDevice) androidDevice; 94 assumeTrue("Requires VM support", testDevice.supportsMicrodroid()); 95 } 96 archiveLogThenDelete(TestLogData logs, ITestDevice device, String remotePath, String localName)97 public static void archiveLogThenDelete(TestLogData logs, ITestDevice device, String remotePath, 98 String localName) throws DeviceNotAvailableException { 99 File logFile = device.pullFile(remotePath); 100 if (logFile != null) { 101 logs.addTestLog(localName, LogDataType.TEXT, new FileInputStreamSource(logFile)); 102 // Delete to avoid confusing logs from a previous run, just in case. 103 device.deleteFile(remotePath); 104 } 105 } 106 107 // Run an arbitrary command in the host side and returns the result runOnHost(String... cmd)108 public static String runOnHost(String... cmd) { 109 return runOnHostWithTimeout(10000, cmd); 110 } 111 112 // Same as runOnHost, but failure is not an error tryRunOnHost(String... cmd)113 private static String tryRunOnHost(String... cmd) { 114 final long timeout = 10000; 115 CommandResult result = RunUtil.getDefault().runTimedCmd(timeout, cmd); 116 return result.getStdout().trim(); 117 } 118 119 // Same as runOnHost, but with custom timeout runOnHostWithTimeout(long timeoutMillis, String... cmd)120 private static String runOnHostWithTimeout(long timeoutMillis, String... cmd) { 121 assertTrue(timeoutMillis >= 0); 122 CommandResult result = RunUtil.getDefault().runTimedCmd(timeoutMillis, cmd); 123 assertThat(result.getStatus(), is(CommandStatus.SUCCESS)); 124 return result.getStdout().trim(); 125 } 126 127 // Run a shell command on Microdroid runOnMicrodroid(String... cmd)128 public static String runOnMicrodroid(String... cmd) { 129 CommandResult result = runOnMicrodroidForResult(cmd); 130 if (result.getStatus() != CommandStatus.SUCCESS) { 131 fail(join(cmd) + " has failed: " + result); 132 } 133 return result.getStdout().trim(); 134 } 135 136 // Same as runOnMicrodroid, but keeps retrying on error till timeout runOnMicrodroidRetryingOnFailure(String... cmd)137 private static String runOnMicrodroidRetryingOnFailure(String... cmd) { 138 final long timeoutMs = 30000; // 30 sec. Microdroid is extremely slow on GCE-on-CF. 139 int attempts = (int) MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES * 60 * 1000 / 500; 140 CommandResult result = RunUtil.getDefault() 141 .runTimedCmdRetry(timeoutMs, 500, attempts, 142 "adb", "-s", MICRODROID_SERIAL, "shell", join(cmd)); 143 if (result.getStatus() != CommandStatus.SUCCESS) { 144 fail(join(cmd) + " has failed: " + result); 145 } 146 return result.getStdout().trim(); 147 } 148 149 // Same as runOnMicrodroid, but returns null on error. tryRunOnMicrodroid(String... cmd)150 public static String tryRunOnMicrodroid(String... cmd) { 151 CommandResult result = runOnMicrodroidForResult(cmd); 152 if (result.getStatus() == CommandStatus.SUCCESS) { 153 return result.getStdout().trim(); 154 } else { 155 CLog.d(join(cmd) + " has failed (but ok): " + result); 156 return null; 157 } 158 } 159 runOnMicrodroidForResult(String... cmd)160 public static CommandResult runOnMicrodroidForResult(String... cmd) { 161 final long timeoutMs = 30000; // 30 sec. Microdroid is extremely slow on GCE-on-CF. 162 return RunUtil.getDefault() 163 .runTimedCmd(timeoutMs, "adb", "-s", MICRODROID_SERIAL, "shell", join(cmd)); 164 } 165 pullMicrodroidFile(String path, File target)166 public static void pullMicrodroidFile(String path, File target) { 167 final long timeoutMs = 30000; // 30 sec. Microdroid is extremely slow on GCE-on-CF. 168 CommandResult result = 169 RunUtil.getDefault() 170 .runTimedCmd( 171 timeoutMs, 172 "adb", 173 "-s", 174 MICRODROID_SERIAL, 175 "pull", 176 path, 177 target.getPath()); 178 if (result.getStatus() != CommandStatus.SUCCESS) { 179 fail("pulling " + path + " has failed: " + result); 180 } 181 } 182 183 // Asserts the command will fail on Microdroid. assertFailedOnMicrodroid(String... cmd)184 public static void assertFailedOnMicrodroid(String... cmd) { 185 CommandResult result = runOnMicrodroidForResult(cmd); 186 assertThat(result.getStatus(), is(CommandStatus.FAILED)); 187 } 188 join(String... strs)189 private static String join(String... strs) { 190 return String.join(" ", Arrays.asList(strs)); 191 } 192 findTestFile(String name)193 public File findTestFile(String name) { 194 return findTestFile(getBuild(), name); 195 } 196 findTestFile(IBuildInfo buildInfo, String name)197 private static File findTestFile(IBuildInfo buildInfo, String name) { 198 try { 199 return (new CompatibilityBuildHelper(buildInfo)).getTestFile(name); 200 } catch (FileNotFoundException e) { 201 fail("Missing test file: " + name); 202 return null; 203 } 204 } 205 getPathForPackage(String packageName)206 public String getPathForPackage(String packageName) 207 throws DeviceNotAvailableException { 208 return getPathForPackage(getDevice(), packageName); 209 } 210 211 // Get the path to the installed apk. Note that 212 // getDevice().getAppPackageInfo(...).getCodePath() doesn't work due to the incorrect 213 // parsing of the "=" character. (b/190975227). So we use the `pm path` command directly. getPathForPackage(ITestDevice device, String packageName)214 private static String getPathForPackage(ITestDevice device, String packageName) 215 throws DeviceNotAvailableException { 216 CommandRunner android = new CommandRunner(device); 217 String pathLine = android.run("pm", "path", packageName); 218 assertTrue("package not found", pathLine.startsWith("package:")); 219 return pathLine.substring("package:".length()); 220 } 221 startMicrodroid( ITestDevice androidDevice, IBuildInfo buildInfo, String apkName, String packageName, String configPath, boolean debug, int memoryMib, Optional<Integer> numCpus, Optional<String> cpuAffinity)222 public static String startMicrodroid( 223 ITestDevice androidDevice, 224 IBuildInfo buildInfo, 225 String apkName, 226 String packageName, 227 String configPath, 228 boolean debug, 229 int memoryMib, 230 Optional<Integer> numCpus, 231 Optional<String> cpuAffinity) 232 throws DeviceNotAvailableException { 233 return startMicrodroid(androidDevice, buildInfo, apkName, packageName, null, configPath, 234 debug, memoryMib, numCpus, cpuAffinity); 235 } 236 startMicrodroid( ITestDevice androidDevice, IBuildInfo buildInfo, String apkName, String packageName, String[] extraIdsigPaths, String configPath, boolean debug, int memoryMib, Optional<Integer> numCpus, Optional<String> cpuAffinity)237 public static String startMicrodroid( 238 ITestDevice androidDevice, 239 IBuildInfo buildInfo, 240 String apkName, 241 String packageName, 242 String[] extraIdsigPaths, 243 String configPath, 244 boolean debug, 245 int memoryMib, 246 Optional<Integer> numCpus, 247 Optional<String> cpuAffinity) 248 throws DeviceNotAvailableException { 249 return startMicrodroid(androidDevice, buildInfo, apkName, null, packageName, 250 extraIdsigPaths, configPath, debug, 251 memoryMib, numCpus, cpuAffinity); 252 } 253 startMicrodroid( ITestDevice androidDevice, IBuildInfo buildInfo, String apkName, String apkPath, String packageName, String[] extraIdsigPaths, String configPath, boolean debug, int memoryMib, Optional<Integer> numCpus, Optional<String> cpuAffinity)254 public static String startMicrodroid( 255 ITestDevice androidDevice, 256 IBuildInfo buildInfo, 257 String apkName, 258 String apkPath, 259 String packageName, 260 String[] extraIdsigPaths, 261 String configPath, 262 boolean debug, 263 int memoryMib, 264 Optional<Integer> numCpus, 265 Optional<String> cpuAffinity) 266 throws DeviceNotAvailableException { 267 CommandRunner android = new CommandRunner(androidDevice); 268 269 // Install APK if necessary 270 if (apkName != null) { 271 File apkFile = findTestFile(buildInfo, apkName); 272 androidDevice.installPackage(apkFile, /* reinstall */ true); 273 } 274 275 if (apkPath == null) { 276 apkPath = getPathForPackage(androidDevice, packageName); 277 } 278 279 android.run("mkdir", "-p", TEST_ROOT); 280 281 // This file is not what we provide. It will be created by the vm tool. 282 final String outApkIdsigPath = TEST_ROOT + apkName + ".idsig"; 283 284 final String instanceImg = TEST_ROOT + INSTANCE_IMG; 285 final String logPath = LOG_PATH; 286 final String debugFlag = debug ? "--debug full" : ""; 287 288 // Run the VM 289 ArrayList<String> args = new ArrayList<>(Arrays.asList( 290 VIRT_APEX + "bin/vm", 291 "run-app", 292 "--daemonize", 293 "--log " + logPath, 294 "--mem " + memoryMib, 295 numCpus.isPresent() ? "--cpus " + numCpus.get() : "", 296 cpuAffinity.isPresent() ? "--cpu-affinity " + cpuAffinity.get() : "", 297 debugFlag, 298 apkPath, 299 outApkIdsigPath, 300 instanceImg, 301 configPath)); 302 if (extraIdsigPaths != null) { 303 for (String path : extraIdsigPaths) { 304 args.add("--extra-idsig"); 305 args.add(path); 306 } 307 } 308 String ret = android.run(args.toArray(new String[0])); 309 310 // Redirect log.txt to logd using logwrapper 311 ExecutorService executor = Executors.newFixedThreadPool(1); 312 executor.execute( 313 () -> { 314 try { 315 // Keep redirecting as long as the expecting maximum test time. When an adb 316 // command times out, it may trigger the device recovery process, which 317 // disconnect adb, which terminates any live adb commands. See an example at 318 // b/194974010#comment25. 319 android.runWithTimeout( 320 MICRODROID_MAX_LIFETIME_MINUTES * 60 * 1000, 321 "logwrapper", 322 "tail", 323 "-f", 324 "-n +0", 325 logPath); 326 } catch (Exception e) { 327 // Consume 328 } 329 }); 330 331 // Retrieve the CID from the vm tool output 332 Pattern pattern = Pattern.compile("with CID (\\d+)"); 333 Matcher matcher = pattern.matcher(ret); 334 assertTrue(matcher.find()); 335 return matcher.group(1); 336 } 337 shutdownMicrodroid(ITestDevice androidDevice, String cid)338 public static void shutdownMicrodroid(ITestDevice androidDevice, String cid) 339 throws DeviceNotAvailableException { 340 CommandRunner android = new CommandRunner(androidDevice); 341 342 // Shutdown the VM 343 android.run(VIRT_APEX + "bin/vm", "stop", cid); 344 } 345 rootMicrodroid()346 public static void rootMicrodroid() { 347 runOnHost("adb", "-s", MICRODROID_SERIAL, "root"); 348 runOnHostWithTimeout( 349 MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES * 60 * 1000, 350 "adb", 351 "-s", 352 MICRODROID_SERIAL, 353 "wait-for-device"); 354 // There have been tests when adb wait-for-device succeeded but the following command 355 // fails with error: closed. Hence, we run adb shell true in microdroid with retries 356 // before returning. 357 runOnMicrodroidRetryingOnFailure("true"); 358 } 359 360 // Establish an adb connection to microdroid by letting Android forward the connection to 361 // microdroid. Wait until the connection is established and microdroid is booted. adbConnectToMicrodroid(ITestDevice androidDevice, String cid)362 public static void adbConnectToMicrodroid(ITestDevice androidDevice, String cid) { 363 long start = System.currentTimeMillis(); 364 long timeoutMillis = MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES * 60 * 1000; 365 long elapsed = 0; 366 367 final String serial = androidDevice.getSerialNumber(); 368 final String from = "tcp:" + TEST_VM_ADB_PORT; 369 final String to = "vsock:" + cid + ":5555"; 370 runOnHost("adb", "-s", serial, "forward", from, to); 371 372 boolean disconnected = true; 373 while (disconnected) { 374 elapsed = System.currentTimeMillis() - start; 375 timeoutMillis -= elapsed; 376 start = System.currentTimeMillis(); 377 String ret = runOnHostWithTimeout(timeoutMillis, "adb", "connect", MICRODROID_SERIAL); 378 disconnected = ret.equals("failed to connect to " + MICRODROID_SERIAL); 379 if (disconnected) { 380 // adb demands us to disconnect if the prior connection was a failure. 381 // b/194375443: this somtimes fails, thus 'try*'. 382 tryRunOnHost("adb", "disconnect", MICRODROID_SERIAL); 383 } 384 } 385 386 elapsed = System.currentTimeMillis() - start; 387 timeoutMillis -= elapsed; 388 runOnHostWithTimeout(timeoutMillis, "adb", "-s", MICRODROID_SERIAL, "wait-for-device"); 389 390 boolean dataAvailable = false; 391 while (!dataAvailable && timeoutMillis >= 0) { 392 elapsed = System.currentTimeMillis() - start; 393 timeoutMillis -= elapsed; 394 start = System.currentTimeMillis(); 395 final String checkCmd = "if [ -d /data/local/tmp ]; then echo 1; fi"; 396 dataAvailable = runOnMicrodroid(checkCmd).equals("1"); 397 } 398 399 // Check if it actually booted by reading a sysprop. 400 assertThat(runOnMicrodroid("getprop", "ro.hardware"), is("microdroid")); 401 } 402 isCuttlefish()403 protected boolean isCuttlefish() throws Exception { 404 return isCuttlefish(getDevice()); 405 } 406 isCuttlefish(ITestDevice device)407 protected static boolean isCuttlefish(ITestDevice device) throws Exception { 408 String productName = device.getProperty("ro.product.name"); 409 return (null != productName) 410 && (productName.startsWith("aosp_cf_x86") 411 || productName.startsWith("aosp_cf_arm") 412 || productName.startsWith("cf_x86") 413 || productName.startsWith("cf_arm")); 414 } 415 } 416