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