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 com.android.microdroid.test.host; 18 19 import static com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData; 20 21 import static com.google.common.truth.Truth.assertThat; 22 import static com.google.common.truth.Truth.assertWithMessage; 23 24 import static org.junit.Assume.assumeFalse; 25 import static org.junit.Assume.assumeTrue; 26 27 import com.android.microdroid.test.common.DeviceProperties; 28 import com.android.microdroid.test.common.MetricsProcessor; 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.testtype.junit4.BaseHostJUnit4Test; 33 import com.android.tradefed.util.CommandResult; 34 import com.android.tradefed.util.CommandStatus; 35 import com.android.tradefed.util.RunUtil; 36 import com.android.tradefed.util.SearchArtifactUtil; 37 38 import org.json.JSONArray; 39 import org.json.JSONObject; 40 41 import java.io.File; 42 import java.util.ArrayList; 43 import java.util.Arrays; 44 import java.util.List; 45 import java.util.Map; 46 47 public abstract class MicrodroidHostTestCaseBase extends BaseHostJUnit4Test { 48 protected static final String TEST_ROOT = "/data/local/tmp/virt/"; 49 protected static final String TRADEFED_TEST_ROOT = "/data/local/tmp/virt/tradefed/"; 50 protected static final String LOG_PATH = TEST_ROOT + "log.txt"; 51 protected static final String CONSOLE_PATH = TEST_ROOT + "console.txt"; 52 protected static final String TRADEFED_CONSOLE_PATH = TRADEFED_TEST_ROOT + "console.txt"; 53 protected static final String TRADEFED_LOG_PATH = TRADEFED_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 protected static final String VIRT_APEX = "/apex/com.android.virt/"; 58 protected static final String SECRETKEEPER_AIDL = 59 "android.hardware.security.secretkeeper.ISecretkeeper/default"; 60 61 private static final long MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES = 5; 62 protected static final long MICRODROID_COMMAND_TIMEOUT_MILLIS = 30000; 63 private static final long MICRODROID_COMMAND_RETRY_INTERVAL_MILLIS = 500; 64 protected static final int MICRODROID_ADB_CONNECT_MAX_ATTEMPTS = 65 (int) 66 (MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES 67 * 60 68 * 1000 69 / MICRODROID_COMMAND_RETRY_INTERVAL_MILLIS); 70 71 // We use a map here because the parameterizer `DeviceParameterizedRunner` doesn't support "-" 72 // in test names. 73 // The key of the map is the name of the parameter while the value is the actual OS variant. 74 protected static final Map<String, String> SUPPORTED_OSES = 75 Map.ofEntries( 76 Map.entry("microdroid", "microdroid"), 77 Map.entry("microdroid_16k", "microdroid_16k"), 78 Map.entry("android15_66", "microdroid_gki-android15-6.6")); 79 80 /* Keep this sync with AssignableDevice.aidl */ 81 public static final class AssignableDevice { 82 public final String node; 83 public final String dtbo_label; 84 AssignableDevice(String node, String dtbo_label)85 public AssignableDevice(String node, String dtbo_label) { 86 this.node = node; 87 this.dtbo_label = dtbo_label; 88 } 89 } 90 prepareVirtualizationTestSetup(ITestDevice androidDevice)91 public static void prepareVirtualizationTestSetup(ITestDevice androidDevice) 92 throws DeviceNotAvailableException { 93 CommandRunner android = new CommandRunner(androidDevice); 94 95 // kill stale crosvm processes 96 android.tryRun("killall", "crosvm"); 97 98 // disconnect from microdroid 99 tryRunOnHost("adb", "disconnect", MICRODROID_SERIAL); 100 101 // remove any leftover files under test root 102 android.tryRun("rm", "-rf", TEST_ROOT + "*"); 103 104 android.tryRun("mkdir " + TEST_ROOT); 105 } 106 cleanUpVirtualizationTestSetup(ITestDevice androidDevice)107 public static void cleanUpVirtualizationTestSetup(ITestDevice androidDevice) 108 throws DeviceNotAvailableException { 109 CommandRunner android = new CommandRunner(androidDevice); 110 111 // disconnect from microdroid 112 tryRunOnHost("adb", "disconnect", MICRODROID_SERIAL); 113 114 // kill stale VMs and directories 115 android.tryRun("killall", "crosvm"); 116 android.tryRun("stop", "virtualizationservice"); 117 android.tryRun("rm", "-rf", "/data/misc/virtualizationservice/*"); 118 } 119 isUserBuild()120 public boolean isUserBuild() { 121 return DeviceProperties.create(getDevice()::getProperty).isUserBuild(); 122 } 123 isCuttlefish()124 protected boolean isCuttlefish() { 125 return DeviceProperties.create(getDevice()::getProperty).isCuttlefish(); 126 } 127 isHwasan()128 protected boolean isHwasan() { 129 return DeviceProperties.create(getDevice()::getProperty).isHwasan(); 130 } 131 getMetricPrefix()132 protected String getMetricPrefix() { 133 return MetricsProcessor.getMetricPrefix( 134 DeviceProperties.create(getDevice()::getProperty).getMetricsTag()); 135 } 136 assumeDeviceIsCapable(ITestDevice androidDevice)137 public static void assumeDeviceIsCapable(ITestDevice androidDevice) throws Exception { 138 assumeTrue("Need an actual TestDevice", androidDevice instanceof TestDevice); 139 TestDevice testDevice = (TestDevice) androidDevice; 140 assumeTrue( 141 "Requires VM support", 142 testDevice.hasFeature("android.software.virtualization_framework")); 143 assumeTrue("Requires VM support", testDevice.supportsMicrodroid()); 144 145 CommandRunner android = new CommandRunner(androidDevice); 146 long vendorApiLevel = androidDevice.getIntProperty("ro.board.api_level", 0); 147 boolean isGsi = 148 android.runForResult("[ -e /system/system_ext/etc/init/init.gsi.rc ]").getStatus() 149 == CommandStatus.SUCCESS; 150 assumeFalse( 151 "GSI with vendor API level < 202404 may not support AVF", 152 isGsi && vendorApiLevel < 202404); 153 } 154 archiveLogThenDelete( TestLogData logs, ITestDevice device, String remotePath, String localName)155 public static void archiveLogThenDelete( 156 TestLogData logs, ITestDevice device, String remotePath, String localName) 157 throws DeviceNotAvailableException { 158 LogArchiver.archiveLogThenDelete(logs, device, remotePath, localName); 159 } 160 setPropertyOrThrow(ITestDevice device, String propertyName, String value)161 public static void setPropertyOrThrow(ITestDevice device, String propertyName, String value) 162 throws DeviceNotAvailableException { 163 if (!device.setProperty(propertyName, value)) { 164 throw new RuntimeException("Failed to set sysprop " + propertyName + " to " + value); 165 } 166 } 167 168 // Run an arbitrary command in the host side and returns the result. 169 // Note failure is not an error. tryRunOnHost(String... cmd)170 public static String tryRunOnHost(String... cmd) { 171 final long timeout = 10000; 172 CommandResult result = RunUtil.getDefault().runTimedCmd(timeout, cmd); 173 return result.getStdout().trim(); 174 } 175 join(String... strs)176 private static String join(String... strs) { 177 return String.join(" ", Arrays.asList(strs)); 178 } 179 findTestFile(String name)180 public File findTestFile(String name) { 181 String moduleName = getInvocationContext().getConfigurationDescriptor().getModuleName(); 182 File testFile = SearchArtifactUtil.searchFile(name, false); 183 if (testFile == null) { 184 throw new AssertionError( 185 "Failed to find test file " + name + " for module " + moduleName); 186 } 187 return testFile; 188 } 189 getPathForPackage(String packageName)190 public String getPathForPackage(String packageName) throws DeviceNotAvailableException { 191 return getPathForPackage(getDevice(), packageName); 192 } 193 194 // Get the path to the installed apk. Note that 195 // getDevice().getAppPackageInfo(...).getCodePath() doesn't work due to the incorrect 196 // parsing of the "=" character. (b/190975227). So we use the `pm path` command directly. getPathForPackage(ITestDevice device, String packageName)197 private static String getPathForPackage(ITestDevice device, String packageName) 198 throws DeviceNotAvailableException { 199 CommandRunner android = new CommandRunner(device); 200 String pathLine = android.run("pm", "path", packageName); 201 assertWithMessage("Package " + packageName + " not found") 202 .that(pathLine) 203 .startsWith("package:"); 204 return pathLine.substring("package:".length()); 205 } 206 parseFieldFromVmInfo(String header)207 public String parseFieldFromVmInfo(String header) throws Exception { 208 CommandRunner android = new CommandRunner(getDevice()); 209 String result = android.run("/apex/com.android.virt/bin/vm", "info"); 210 for (String line : result.split("\n")) { 211 if (!line.startsWith(header)) continue; 212 213 return line.substring(header.length()); 214 } 215 return ""; 216 } 217 parseStringArrayFieldsFromVmInfo(String header)218 public List<String> parseStringArrayFieldsFromVmInfo(String header) throws Exception { 219 String field = parseFieldFromVmInfo(header); 220 221 List<String> ret = new ArrayList<>(); 222 if (!field.isEmpty()) { 223 JSONArray jsonArray = new JSONArray(field); 224 for (int i = 0; i < jsonArray.length(); i++) { 225 ret.add(jsonArray.getString(i)); 226 } 227 } 228 return ret; 229 } 230 isFeatureEnabled(String feature)231 public boolean isFeatureEnabled(String feature) throws Exception { 232 CommandRunner android = new CommandRunner(getDevice()); 233 String cmd = VIRT_APEX + "bin/vm check-feature-enabled " + feature; 234 CommandResult result = android.runForResult(cmd); 235 assumeTrue( 236 "Failed to run" + cmd + " " + result, 237 result.getStatus() == CommandStatus.SUCCESS && result.getExitCode() == 0); 238 return result.getStdout().trim().contains("Feature " + feature + " is enabled"); 239 } 240 getAssignableDevices()241 public List<AssignableDevice> getAssignableDevices() throws Exception { 242 String field = parseFieldFromVmInfo("Assignable devices: "); 243 244 List<AssignableDevice> ret = new ArrayList<>(); 245 if (!field.isEmpty()) { 246 JSONArray jsonArray = new JSONArray(field); 247 for (int i = 0; i < jsonArray.length(); i++) { 248 JSONObject jsonObject = jsonArray.getJSONObject(i); 249 ret.add( 250 new AssignableDevice( 251 jsonObject.getString("node"), jsonObject.getString("dtbo_label"))); 252 } 253 } 254 return ret; 255 } 256 isUpdatableVmSupported()257 public boolean isUpdatableVmSupported() throws DeviceNotAvailableException { 258 // Updatable VMs are possible iff device supports Secretkeeper. 259 CommandRunner android = new CommandRunner(getDevice()); 260 CommandResult result = android.runForResult("service check", SECRETKEEPER_AIDL); 261 assertWithMessage("Failed to run service check. Result= " + result) 262 .that(result.getStatus() == CommandStatus.SUCCESS && result.getExitCode() == 0) 263 .isTrue(); 264 boolean is_sk_supported = !result.getStdout().trim().contains("not found"); 265 return is_sk_supported; 266 } 267 getSupportedOSList()268 public List<String> getSupportedOSList() throws Exception { 269 // The --os flag was introduced in SDK level 36. When running tests on earlier dessert 270 // releases only use "microdroid" OS. 271 if (getAndroidDevice().getApiLevel() < 36) { 272 return Arrays.asList("microdroid"); 273 } 274 return parseStringArrayFieldsFromVmInfo("Available OS list: "); 275 } 276 isPkvmHypervisor()277 protected boolean isPkvmHypervisor() throws DeviceNotAvailableException { 278 return "kvm.arm-protected".equals(getDevice().getProperty("ro.boot.hypervisor.version")); 279 } 280 getAndroidDevice()281 protected TestDevice getAndroidDevice() { 282 TestDevice androidDevice = (TestDevice) getDevice(); 283 assertThat(androidDevice).isNotNull(); 284 return androidDevice; 285 } 286 assumeKernelSupported(String osKey)287 protected void assumeKernelSupported(String osKey) throws Exception { 288 String os = SUPPORTED_OSES.get(osKey); 289 assumeTrue( 290 "Skipping test as OS \"" + os + "\" is not supported", 291 getSupportedOSList().contains(os)); 292 } 293 assumeVmTypeSupported(String os, boolean protectedVm)294 protected void assumeVmTypeSupported(String os, boolean protectedVm) throws Exception { 295 // TODO(b/376870129): remove this check 296 if (protectedVm) { 297 assumeFalse("pVMs with 16k kernel are not supported yet :(", os.endsWith("_16k")); 298 } 299 assumeTrue( 300 "Microdroid is not supported for specific VM protection type", 301 getAndroidDevice().supportsMicrodroid(protectedVm)); 302 } 303 } 304