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