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