1 /* 2 * Copyright (C) 2020 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.cts.statsdatom.lib; 18 19 import static com.google.common.truth.Truth.assertThat; 20 import static com.google.common.truth.Truth.assertWithMessage; 21 22 import android.service.battery.BatteryServiceDumpProto; 23 24 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; 25 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner; 26 import com.android.ddmlib.testrunner.TestResult.TestStatus; 27 import com.android.internal.os.StatsdConfigProto.StatsdConfig; 28 import com.android.tradefed.build.IBuildInfo; 29 import com.android.tradefed.device.CollectingByteOutputReceiver; 30 import com.android.tradefed.device.DeviceNotAvailableException; 31 import com.android.tradefed.device.ITestDevice; 32 import com.android.tradefed.log.LogUtil; 33 import com.android.tradefed.log.LogUtil.CLog; 34 import com.android.tradefed.result.CollectingTestListener; 35 import com.android.tradefed.result.TestDescription; 36 import com.android.tradefed.result.TestResult; 37 import com.android.tradefed.result.TestRunResult; 38 import com.android.tradefed.util.Pair; 39 40 import com.google.protobuf.InvalidProtocolBufferException; 41 import com.google.protobuf.MessageLite; 42 import com.google.protobuf.Parser; 43 44 import java.io.FileNotFoundException; 45 import java.util.Map; 46 import javax.annotation.Nonnull; 47 import javax.annotation.Nullable; 48 49 /** 50 * Contains utility functions for interacting with the device. 51 * Largely copied from incident's ProtoDumpTestCase. 52 */ 53 public final class DeviceUtils { 54 public static final String STATSD_ATOM_TEST_APK = "CtsStatsdAtomApp.apk"; 55 public static final String STATSD_ATOM_TEST_PKG = "com.android.server.cts.device.statsdatom"; 56 57 private static final String TEST_RUNNER = "androidx.test.runner.AndroidJUnitRunner"; 58 59 private static final String KEY_ACTION = "action"; 60 61 // feature names 62 public static final String FEATURE_WATCH = "android.hardware.type.watch"; 63 64 public static final String DUMP_BATTERY_CMD = "dumpsys battery"; 65 66 /** 67 * Runs device side tests. 68 * 69 * @param device Can be retrieved by running getDevice() in a class that extends DeviceTestCase 70 * @param pkgName Test package name, such as "com.android.server.cts.statsdatom" 71 * @param testClassName Test class name which can either be a fully qualified name or "." + a 72 * class name; if null, all test in the package will be run 73 * @param testMethodName Test method name; if null, all tests in class or package will be run 74 * @return {@link TestRunResult} of this invocation 75 * @throws DeviceNotAvailableException 76 */ runDeviceTests(ITestDevice device, String pkgName, @Nullable String testClassName, @Nullable String testMethodName)77 public static @Nonnull TestRunResult runDeviceTests(ITestDevice device, String pkgName, 78 @Nullable String testClassName, @Nullable String testMethodName) 79 throws DeviceNotAvailableException { 80 if (testClassName != null && testClassName.startsWith(".")) { 81 testClassName = pkgName + testClassName; 82 } 83 84 RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner( 85 pkgName, TEST_RUNNER, device.getIDevice()); 86 if (testClassName != null && testMethodName != null) { 87 testRunner.setMethodName(testClassName, testMethodName); 88 } else if (testClassName != null) { 89 testRunner.setClassName(testClassName); 90 } 91 92 CollectingTestListener listener = new CollectingTestListener(); 93 assertThat(device.runInstrumentationTests(testRunner, listener)).isTrue(); 94 95 final TestRunResult result = listener.getCurrentRunResults(); 96 if (result.isRunFailure()) { 97 throw new Error("Failed to successfully run device tests for " 98 + result.getName() + ": " + result.getRunFailureMessage()); 99 } 100 if (result.getNumTests() == 0) { 101 throw new Error("No tests were run on the device"); 102 } 103 if (result.hasFailedTests()) { 104 StringBuilder errorBuilder = new StringBuilder("On-device tests failed:\n"); 105 for (Map.Entry<TestDescription, TestResult> resultEntry : 106 result.getTestResults().entrySet()) { 107 if (!resultEntry.getValue().getStatus().equals(TestStatus.PASSED)) { 108 errorBuilder.append(resultEntry.getKey().toString()); 109 errorBuilder.append(":\n"); 110 errorBuilder.append(resultEntry.getValue().getStackTrace()); 111 } 112 } 113 throw new AssertionError(errorBuilder.toString()); 114 } 115 return result; 116 } 117 118 /** 119 * Runs device side tests from the com.android.server.cts.device.statsdatom package. 120 */ runDeviceTestsOnStatsdApp(ITestDevice device, @Nullable String testClassName, @Nullable String testMethodName)121 public static @Nonnull TestRunResult runDeviceTestsOnStatsdApp(ITestDevice device, 122 @Nullable String testClassName, @Nullable String testMethodName) 123 throws DeviceNotAvailableException { 124 return runDeviceTests(device, STATSD_ATOM_TEST_PKG, testClassName, testMethodName); 125 } 126 127 /** 128 * Install the statsdatom CTS app to the device. 129 */ installStatsdTestApp(ITestDevice device, IBuildInfo ctsBuildInfo)130 public static void installStatsdTestApp(ITestDevice device, IBuildInfo ctsBuildInfo) 131 throws FileNotFoundException, DeviceNotAvailableException { 132 installTestApp(device, STATSD_ATOM_TEST_APK, STATSD_ATOM_TEST_PKG, ctsBuildInfo); 133 } 134 135 /** 136 * Install a test app to the device. 137 */ installTestApp(ITestDevice device, String apkName, String pkgName, IBuildInfo ctsBuildInfo)138 public static void installTestApp(ITestDevice device, String apkName, String pkgName, 139 IBuildInfo ctsBuildInfo) throws FileNotFoundException, DeviceNotAvailableException { 140 CLog.d("Installing app " + apkName); 141 CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(ctsBuildInfo); 142 final String result = device.installPackage( 143 buildHelper.getTestFile(apkName), /*reinstall=*/true, /*grantPermissions=*/true); 144 assertWithMessage("Failed to install " + apkName + ": " + result).that(result).isNull(); 145 allowBackgroundServices(device, pkgName); 146 } 147 148 /** 149 * Required to successfully start a background service from adb, starting in O. 150 */ allowBackgroundServices(ITestDevice device, String pkgName)151 private static void allowBackgroundServices(ITestDevice device, String pkgName) 152 throws DeviceNotAvailableException { 153 String cmd = "cmd deviceidle tempwhitelist " + pkgName; 154 device.executeShellCommand(cmd); 155 } 156 157 /** 158 * Uninstall the statsdatom CTS app from the device. 159 */ uninstallStatsdTestApp(ITestDevice device)160 public static void uninstallStatsdTestApp(ITestDevice device) throws Exception { 161 uninstallTestApp(device, STATSD_ATOM_TEST_PKG); 162 } 163 164 /** 165 * Uninstall the test app from the device. 166 */ uninstallTestApp(ITestDevice device, String pkgName)167 public static void uninstallTestApp(ITestDevice device, String pkgName) throws Exception { 168 device.uninstallPackage(pkgName); 169 } 170 171 /** 172 * Run an adb shell command on device and parse the results as a proto of a given type. 173 * 174 * @param device Device to run cmd on 175 * @param parser Protobuf parser object, which can be retrieved by running MyProto.parser() 176 * @param cmd The adb shell command to run (e.g. "cmd stats update config") 177 * 178 * @throws DeviceNotAvailableException 179 * @throws InvalidProtocolBufferException Occurs if there was an error parsing the proto. Note 180 * that a 0 length buffer is not necessarily an error. 181 * @return Proto of specified type 182 */ getShellCommandOutput(@onnull ITestDevice device, Parser<T> parser, String cmd)183 public static <T extends MessageLite> T getShellCommandOutput(@Nonnull ITestDevice device, 184 Parser<T> parser, String cmd) 185 throws DeviceNotAvailableException, InvalidProtocolBufferException { 186 final CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver(); 187 device.executeShellCommand(cmd, receiver); 188 try { 189 return parser.parseFrom(receiver.getOutput()); 190 } catch (Exception ex) { 191 CLog.d("Error parsing " + parser.getClass().getCanonicalName() + " for cmd " + cmd); 192 throw ex; 193 } 194 } 195 196 /** 197 * Returns the UID of the host, which should always either be AID_SHELL (2000) or AID_ROOT (0). 198 */ getHostUid(ITestDevice device)199 public static int getHostUid(ITestDevice device) throws DeviceNotAvailableException { 200 String uidString = ""; 201 try { 202 uidString = device.executeShellCommand("id -u"); 203 return Integer.parseInt(uidString.trim()); 204 } catch (NumberFormatException ex) { 205 CLog.e("Failed to get host's uid via shell command. Found " + uidString); 206 // Fall back to alternative method... 207 if (device.isAdbRoot()) { 208 return 0; 209 } else { 210 return 2000; // SHELL 211 } 212 } 213 } 214 215 /** 216 * Returns the UID of the statsdatom CTS test app. 217 */ getStatsdTestAppUid(ITestDevice device)218 public static int getStatsdTestAppUid(ITestDevice device) throws DeviceNotAvailableException { 219 return getAppUid(device, STATSD_ATOM_TEST_PKG); 220 } 221 222 /** 223 * Returns the UID of the test app. 224 */ getAppUid(ITestDevice device, String pkgName)225 public static int getAppUid(ITestDevice device, String pkgName) 226 throws DeviceNotAvailableException { 227 int currentUser = device.getCurrentUser(); 228 String uidLine = device.executeShellCommand("cmd package list packages -U --user " 229 + currentUser + " " + pkgName); 230 String[] uidLineArr = uidLine.split(":"); 231 232 // Package uid is located at index 2. 233 assertThat(uidLineArr.length).isGreaterThan(2); 234 int appUid = Integer.parseInt(uidLineArr[2].trim()); 235 assertThat(appUid).isGreaterThan(10000); 236 return appUid; 237 } 238 239 /** 240 * Determines if the device has the given features. 241 * 242 * @param feature name of the feature (e.g. "android.hardware.bluetooth") 243 */ hasFeature(ITestDevice device, String feature)244 public static boolean hasFeature(ITestDevice device, String feature) throws Exception { 245 final String features = device.executeShellCommand("pm list features"); 246 return features.contains(feature); 247 } 248 249 /** 250 * Runs an activity in a particular app. 251 */ runActivity(ITestDevice device, String pkgName, String activity, @Nullable String actionKey, @Nullable String actionValue)252 public static void runActivity(ITestDevice device, String pkgName, String activity, 253 @Nullable String actionKey, @Nullable String actionValue) throws Exception { 254 runActivity(device, pkgName, activity, actionKey, actionValue, 255 AtomTestUtils.WAIT_TIME_LONG); 256 } 257 258 /** 259 * Runs an activity in a particular app for a certain period of time. 260 * 261 * @param pkgName name of package that contains the Activity 262 * @param activity name of the Activity class 263 * @param actionKey key of extra data that is passed to the Activity via an Intent 264 * @param actionValue value of extra data that is passed to the Activity via an Intent 265 * @param waitTimeMs duration that the activity runs for 266 */ runActivity(ITestDevice device, String pkgName, String activity, @Nullable String actionKey, @Nullable String actionValue, long waitTimeMs)267 public static void runActivity(ITestDevice device, String pkgName, String activity, 268 @Nullable String actionKey, @Nullable String actionValue, long waitTimeMs) 269 throws Exception { 270 try (AutoCloseable a = withActivity(device, pkgName, activity, actionKey, actionValue)) { 271 Thread.sleep(waitTimeMs); 272 } 273 } 274 275 /** 276 * Starts the specified activity and returns an {@link AutoCloseable} that stops the activity 277 * when closed. 278 * 279 * <p>Example usage: 280 * <pre> 281 * try (AutoClosable a = withActivity("activity", "action", "action-value")) { 282 * doStuff(); 283 * } 284 * </pre> 285 */ withActivity(ITestDevice device, String pkgName, String activity, @Nullable String actionKey, @Nullable String actionValue)286 public static AutoCloseable withActivity(ITestDevice device, String pkgName, String activity, 287 @Nullable String actionKey, @Nullable String actionValue) throws Exception { 288 String intentString; 289 if (actionKey != null && actionValue != null) { 290 intentString = actionKey + " " + actionValue; 291 } else { 292 intentString = null; 293 } 294 295 String cmd = "am start -n " + pkgName + "/." + activity; 296 if (intentString != null) { 297 cmd += " -e " + intentString; 298 } 299 device.executeShellCommand(cmd); 300 301 return () -> { 302 device.executeShellCommand("am force-stop " + pkgName); 303 Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT); 304 }; 305 } 306 setChargingState(ITestDevice device, int state)307 public static void setChargingState(ITestDevice device, int state) throws Exception { 308 device.executeShellCommand("cmd battery set status " + state); 309 } 310 unplugDevice(ITestDevice device)311 public static void unplugDevice(ITestDevice device) throws Exception { 312 // On batteryless devices on Android P or above, the 'unplug' command 313 // alone does not simulate the really unplugged state. 314 // 315 // This is because charging state is left as "unknown". Unless a valid 316 // state like 3 = BatteryManager.BATTERY_STATUS_DISCHARGING is set, 317 // framework does not consider the device as running on battery. 318 setChargingState(device, 3); 319 device.executeShellCommand("cmd battery unplug"); 320 } 321 plugInAc(ITestDevice device)322 public static void plugInAc(ITestDevice device) throws Exception { 323 device.executeShellCommand("cmd battery set ac 1"); 324 } 325 turnScreenOn(ITestDevice device)326 public static void turnScreenOn(ITestDevice device) throws Exception { 327 device.executeShellCommand("input keyevent KEYCODE_WAKEUP"); 328 device.executeShellCommand("wm dismiss-keyguard"); 329 } 330 turnScreenOff(ITestDevice device)331 public static void turnScreenOff(ITestDevice device) throws Exception { 332 device.executeShellCommand("input keyevent KEYCODE_SLEEP"); 333 } 334 turnBatteryStatsAutoResetOn(ITestDevice device)335 public static void turnBatteryStatsAutoResetOn(ITestDevice device) throws Exception { 336 device.executeShellCommand("dumpsys batterystats enable no-auto-reset"); 337 } 338 turnBatteryStatsAutoResetOff(ITestDevice device)339 public static void turnBatteryStatsAutoResetOff(ITestDevice device) throws Exception { 340 device.executeShellCommand("dumpsys batterystats enable no-auto-reset"); 341 } 342 flushBatteryStatsHandlers(ITestDevice device)343 public static void flushBatteryStatsHandlers(ITestDevice device) throws Exception { 344 // Dumping batterystats will flush everything in the batterystats handler threads. 345 device.executeShellCommand("dumpsys batterystats"); 346 } 347 hasBattery(ITestDevice device)348 public static boolean hasBattery(ITestDevice device) throws Exception { 349 try { 350 BatteryServiceDumpProto batteryProto = getShellCommandOutput(device, BatteryServiceDumpProto.parser(), 351 String.join(" ", DUMP_BATTERY_CMD, "--proto")); 352 LogUtil.CLog.d("Got battery service dump:\n " + batteryProto.toString()); 353 return batteryProto.getIsPresent(); 354 } catch (com.google.protobuf.InvalidProtocolBufferException e) { 355 LogUtil.CLog.e("Failed to dump batteryservice proto"); 356 throw (e); 357 } 358 } 359 resetBatteryStatus(ITestDevice device)360 public static void resetBatteryStatus(ITestDevice device) throws Exception { 361 device.executeShellCommand("cmd battery reset"); 362 } 363 getProperty(ITestDevice device, String prop)364 public static String getProperty(ITestDevice device, String prop) throws Exception { 365 return device.executeShellCommand("getprop " + prop).replace("\n", ""); 366 } 367 isDebuggable(ITestDevice device)368 public static boolean isDebuggable(ITestDevice device) throws Exception { 369 return Integer.parseInt(getProperty(device, "ro.debuggable")) == 1; 370 } 371 checkDeviceFor(ITestDevice device, String methodName)372 public static boolean checkDeviceFor(ITestDevice device, String methodName) throws Exception { 373 try { 374 runDeviceTestsOnStatsdApp(device, ".Checkers", methodName); 375 // Test passes, meaning that the answer is true. 376 LogUtil.CLog.d(methodName + "() indicates true."); 377 return true; 378 } catch (AssertionError e) { 379 // Method is designed to fail if the answer is false. 380 LogUtil.CLog.d(methodName + "() indicates false."); 381 return false; 382 } 383 } 384 385 /** Make the test app standby-active so it can run syncs and jobs immediately. */ allowImmediateSyncs(ITestDevice device)386 public static void allowImmediateSyncs(ITestDevice device) throws Exception { 387 device.executeShellCommand("am set-standby-bucket " 388 + DeviceUtils.STATSD_ATOM_TEST_PKG + " active"); 389 } 390 391 /** 392 * Runs a (background) service to perform the given action. 393 * @param actionValue the action code constants indicating the desired action to perform. 394 */ executeBackgroundService(ITestDevice device, String actionValue)395 public static void executeBackgroundService(ITestDevice device, String actionValue) 396 throws Exception { 397 executeServiceAction(device, "StatsdCtsBackgroundService", actionValue); 398 } 399 400 /** 401 * Runs the specified statsd package service to perform the given action. 402 * @param actionValue the action code constants indicating the desired action to perform. 403 */ executeServiceAction(ITestDevice device, String service, String actionValue)404 public static void executeServiceAction(ITestDevice device, String service, String actionValue) 405 throws Exception { 406 allowBackgroundServices(device); 407 device.executeShellCommand(String.format( 408 "am startservice -n '%s/.%s' -e %s %s", 409 STATSD_ATOM_TEST_PKG, service, 410 KEY_ACTION, actionValue)); 411 } 412 413 /** 414 * Required to successfully start a background service from adb in Android O. 415 */ allowBackgroundServices(ITestDevice device)416 private static void allowBackgroundServices(ITestDevice device) throws Exception { 417 device.executeShellCommand(String.format( 418 "cmd deviceidle tempwhitelist %s", STATSD_ATOM_TEST_PKG)); 419 } 420 421 /** 422 * Returns the kernel major version as a pair of ints. 423 */ getKernelVersion(ITestDevice device)424 public static Pair<Integer, Integer> getKernelVersion(ITestDevice device) 425 throws Exception { 426 String[] version = device.executeShellCommand("uname -r").split("\\."); 427 if (version.length < 2) { 428 throw new RuntimeException("Could not parse kernel version"); 429 } 430 return Pair.create(Integer.parseInt(version[0]), Integer.parseInt(version[1])); 431 } 432 433 /** Returns if the device kernel version >= input kernel version. */ isKernelGreaterEqual(ITestDevice device, Pair<Integer, Integer> version)434 public static boolean isKernelGreaterEqual(ITestDevice device, Pair<Integer, Integer> version) 435 throws Exception { 436 Pair<Integer, Integer> kernelVersion = getKernelVersion(device); 437 return kernelVersion.first > version.first 438 || (kernelVersion.first == version.first && kernelVersion.second >= version.second); 439 } 440 DeviceUtils()441 private DeviceUtils() {} 442 } 443