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