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.car.cts; 18 19 import static com.google.common.truth.Truth.assertWithMessage; 20 21 import static org.junit.Assert.fail; 22 import static org.junit.Assume.assumeTrue; 23 24 import android.service.pm.PackageProto; 25 import android.service.pm.PackageProto.UserPermissionsProto; 26 import android.service.pm.PackageServiceDumpProto; 27 28 import com.android.compatibility.common.util.CommonTestUtils; 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.CLog; 33 import com.android.tradefed.testtype.ITestInformationReceiver; 34 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; 35 36 import org.junit.After; 37 import org.junit.AssumptionViolatedException; 38 import org.junit.Before; 39 import org.junit.Rule; 40 import org.junit.rules.TestRule; 41 import org.junit.runner.Description; 42 import org.junit.runners.model.Statement; 43 44 import java.util.ArrayList; 45 import java.util.HashMap; 46 import java.util.HashSet; 47 import java.util.List; 48 import java.util.Map; 49 import java.util.function.Function; 50 import java.util.regex.Matcher; 51 import java.util.regex.Pattern; 52 53 /** 54 * Base class for all test cases. 55 */ 56 // NOTE: must be public because of @Rules 57 public abstract class CarHostJUnit4TestCase extends BaseHostJUnit4Test { 58 59 private static final int DEFAULT_TIMEOUT_SEC = 20; 60 61 private static final Pattern CREATE_USER_OUTPUT_PATTERN = Pattern.compile("id=(\\d+)"); 62 63 private static final String USER_PREFIX = "CtsCarHostTestCases"; 64 65 /** 66 * User pattern in the output of "cmd user list --all -v" 67 * TEXT id=<id> TEXT name=<name>, TEX flags=<flags> TEXT 68 * group 1: id group 2: name group 3: flags group 4: other state(like "(running)") 69 */ 70 private static final Pattern USER_PATTERN = Pattern.compile( 71 ".*id=(\\d+).*name=([^\\s,]+).*flags=(\\S+)(.*)"); 72 73 private static final int USER_PATTERN_GROUP_ID = 1; 74 private static final int USER_PATTERN_GROUP_NAME = 2; 75 private static final int USER_PATTERN_GROUP_FLAGS = 3; 76 private static final int USER_PATTERN_GROUP_OTHER_STATE = 4; 77 78 /** 79 * User's package permission pattern string format in the output of "dumpsys package PKG_NAME" 80 */ 81 protected static final String APP_APK = "CtsCarApp.apk"; 82 protected static final String APP_PKG = "android.car.cts.app"; 83 84 @Rule 85 public final RequiredFeatureRule mHasAutomotiveRule = new RequiredFeatureRule(this, 86 "android.hardware.type.automotive"); 87 88 private final HashSet<Integer> mUsersToBeRemoved = new HashSet<>(); 89 90 private int mInitialUserId; 91 private Integer mInitialMaximumNumberOfUsers; 92 93 /** 94 * Saves multi-user state so it can be restored after the test. 95 */ 96 @Before saveUserState()97 public void saveUserState() throws Exception { 98 removeUsers(USER_PREFIX); 99 100 mInitialUserId = getCurrentUserId(); 101 } 102 103 /** 104 * Restores multi-user state from before the test. 105 */ 106 @After restoreUsersState()107 public void restoreUsersState() throws Exception { 108 int currentUserId = getCurrentUserId(); 109 CLog.d("restoreUsersState(): initial user: %d, current user: %d, created users: %s " 110 + "max number of users: %d", 111 mInitialUserId, currentUserId, mUsersToBeRemoved, mInitialMaximumNumberOfUsers); 112 if (currentUserId != mInitialUserId) { 113 CLog.i("Switching back from %d to %d", currentUserId, mInitialUserId); 114 switchUser(mInitialUserId); 115 } 116 117 if (!mUsersToBeRemoved.isEmpty()) { 118 CLog.i("Removing users %s", mUsersToBeRemoved); 119 for (int userId : mUsersToBeRemoved) { 120 removeUser(userId); 121 } 122 } 123 124 // Should have been removed above, but as the saying goes, better safe than sorry... 125 removeUsers(USER_PREFIX); 126 127 if (mInitialMaximumNumberOfUsers != null) { 128 CLog.i("Restoring max number of users to %d", mInitialMaximumNumberOfUsers); 129 setMaxNumberUsers(mInitialMaximumNumberOfUsers); 130 } 131 } 132 133 /** 134 * Makes sure the device supports multiple users, throwing {@link AssumptionViolatedException} 135 * if it doesn't. 136 */ assumeSupportsMultipleUsers()137 protected final void assumeSupportsMultipleUsers() throws Exception { 138 assumeTrue("device does not support multi-user", 139 getDevice().getMaxNumberOfUsersSupported() > 1); 140 } 141 142 /** 143 * Makes sure the device can add {@code numberOfUsers} new users, increasing limit if needed or 144 * failing if not possible. 145 */ requiresExtraUsers(int numberOfUsers)146 protected final void requiresExtraUsers(int numberOfUsers) throws Exception { 147 assumeSupportsMultipleUsers(); 148 149 int maxNumber = getDevice().getMaxNumberOfUsersSupported(); 150 int currentNumber = getDevice().listUsers().size(); 151 152 if (currentNumber + numberOfUsers <= maxNumber) return; 153 154 if (!getDevice().isAdbRoot()) { 155 failCannotCreateUsers(numberOfUsers, currentNumber, maxNumber, /* isAdbRoot= */ false); 156 } 157 158 // Increase limit... 159 mInitialMaximumNumberOfUsers = maxNumber; 160 setMaxNumberUsers(maxNumber + numberOfUsers); 161 162 // ...and try again 163 maxNumber = getDevice().getMaxNumberOfUsersSupported(); 164 if (currentNumber + numberOfUsers > maxNumber) { 165 failCannotCreateUsers(numberOfUsers, currentNumber, maxNumber, /* isAdbRoot= */ true); 166 } 167 } 168 failCannotCreateUsers(int numberOfUsers, int currentNumber, int maxNumber, boolean isAdbRoot)169 private void failCannotCreateUsers(int numberOfUsers, int currentNumber, int maxNumber, 170 boolean isAdbRoot) { 171 String reason = isAdbRoot ? "failed to increase it" 172 : "cannot be increased without adb root"; 173 String existingUsers = ""; 174 try { 175 existingUsers = "Existing users: " + executeCommand("cmd user list --all -v"); 176 } catch (Exception e) { 177 // ignore 178 } 179 fail("Cannot create " + numberOfUsers + " users: current number is " + currentNumber 180 + ", limit is " + maxNumber + " and could not be increased (" + reason + "). " 181 + existingUsers); 182 } 183 184 /** 185 * Executes the shell command and returns the output. 186 */ executeCommand(String command, Object... args)187 protected String executeCommand(String command, Object... args) throws Exception { 188 String fullCommand = String.format(command, args); 189 return getDevice().executeShellCommand(fullCommand); 190 } 191 192 /** 193 * Executes the shell command and parses output with {@code resultParser}. 194 */ executeAndParseCommand(Function<String, T> resultParser, String command, Object... args)195 protected <T> T executeAndParseCommand(Function<String, T> resultParser, 196 String command, Object... args) throws Exception { 197 String output = executeCommand(command, args); 198 return resultParser.apply(output); 199 } 200 201 /** 202 * Executes the shell command and parses the Matcher output with {@code resultParser}, failing 203 * with {@code matchNotFoundErrorMessage} if it didn't match the {@code regex}. 204 */ executeAndParseCommand(Pattern regex, String matchNotFoundErrorMessage, Function<Matcher, T> resultParser, String command, Object... args)205 protected <T> T executeAndParseCommand(Pattern regex, String matchNotFoundErrorMessage, 206 Function<Matcher, T> resultParser, 207 String command, Object... args) throws Exception { 208 String output = executeCommand(command, args); 209 Matcher matcher = regex.matcher(output); 210 if (!matcher.find()) { 211 fail(matchNotFoundErrorMessage + ". Shell command: '" + String.format(command, args) 212 + "'. Output: " + output.trim() + ". Regex: " + regex); 213 } 214 return resultParser.apply(matcher); 215 } 216 217 /** 218 * Executes the shell command and parses the Matcher output with {@code resultParser}. 219 */ executeAndParseCommand(Pattern regex, Function<Matcher, T> resultParser, String command, Object... args)220 protected <T> T executeAndParseCommand(Pattern regex, Function<Matcher, T> resultParser, 221 String command, Object... args) throws Exception { 222 String output = executeCommand(command, args); 223 return resultParser.apply(regex.matcher(output)); 224 } 225 226 /** 227 * Executes the shell command that returns all users and returns {@code function} applied to 228 * them. 229 */ onAllUsers(Function<List<UserInfo>, T> function)230 public <T> T onAllUsers(Function<List<UserInfo>, T> function) throws Exception { 231 ArrayList<UserInfo> allUsers = executeAndParseCommand(USER_PATTERN, (matcher) -> { 232 ArrayList<UserInfo> users = new ArrayList<>(); 233 while (matcher.find()) { 234 users.add(new UserInfo(matcher)); 235 } 236 return users; 237 }, "cmd user list --all -v"); 238 return function.apply(allUsers); 239 } 240 241 /** 242 * Gets the info for the given user. 243 */ getUserInfo(int userId)244 public UserInfo getUserInfo(int userId) throws Exception { 245 return onAllUsers((allUsers) -> allUsers.stream() 246 .filter((u) -> u.id == userId)) 247 .findFirst().get(); 248 } 249 250 /** 251 * Sets the maximum number of users that can be created for this car. 252 * 253 * @throws IllegalStateException if adb is not running as root 254 */ setMaxNumberUsers(int numUsers)255 protected void setMaxNumberUsers(int numUsers) throws Exception { 256 if (!getDevice().isAdbRoot()) { 257 throw new IllegalStateException("must be running adb root"); 258 } 259 executeCommand("setprop fw.max_users %d", numUsers); 260 } 261 262 /** 263 * Gets the current user's id. 264 */ getCurrentUserId()265 protected int getCurrentUserId() throws DeviceNotAvailableException { 266 return getDevice().getCurrentUser(); 267 } 268 269 /** 270 * Creates a full user with car service shell command. 271 */ createFullUser(String name)272 protected int createFullUser(String name) throws Exception { 273 return createUser(name, /* flags= */ 0, "android.os.usertype.full.SECONDARY"); 274 } 275 276 /** 277 * Creates a full guest with car service shell command. 278 */ createGuestUser(String name)279 protected int createGuestUser(String name) throws Exception { 280 return createUser(name, /* flags= */ 0, "android.os.usertype.full.GUEST"); 281 } 282 283 /** 284 * Creates a flexible user with car service shell command. 285 * 286 * <p><b>NOTE: </b>it uses User HAL flags, not core Android's. 287 */ createUser(String name, int flags, String type)288 protected int createUser(String name, int flags, String type) throws Exception { 289 name = USER_PREFIX + "." + name; 290 waitForCarServiceReady(); 291 int userId = executeAndParseCommand(CREATE_USER_OUTPUT_PATTERN, 292 "Could not create user with name " + name + ", flags " + flags + ", type" + type, 293 matcher -> Integer.parseInt(matcher.group(1)), 294 "cmd car_service create-user --flags %d --type %s %s", 295 flags, type, name); 296 markUserForRemovalAfterTest(userId); 297 return userId; 298 } 299 300 /** 301 * Marks a user to be removed at the end of the tests. 302 */ markUserForRemovalAfterTest(int userId)303 protected void markUserForRemovalAfterTest(int userId) { 304 mUsersToBeRemoved.add(userId); 305 } 306 307 /** 308 * Waits until the given user is initialized. 309 */ waitForUserInitialized(int userId)310 protected void waitForUserInitialized(int userId) throws Exception { 311 CommonTestUtils.waitUntil("timed out waiting for user " + userId + " initialization", 312 DEFAULT_TIMEOUT_SEC, () -> isUserInitialized(userId)); 313 } 314 315 /** 316 * Waits until the system server is ready. 317 */ waitForCarServiceReady()318 protected void waitForCarServiceReady() throws Exception { 319 CommonTestUtils.waitUntil("timed out waiting for system server ", 320 DEFAULT_TIMEOUT_SEC, () -> isCarServiceReady()); 321 } 322 isCarServiceReady()323 protected boolean isCarServiceReady() { 324 String cmd = "service check car_service"; 325 try { 326 String output = getDevice().executeShellCommand(cmd); 327 return !output.endsWith("not found"); 328 } catch (Exception e) { 329 CLog.i("%s failed: %s", cmd, e.getMessage()); 330 } 331 return false; 332 } 333 334 /** 335 * Asserts that the given user is initialized. 336 */ assertUserInitialized(int userId)337 protected void assertUserInitialized(int userId) throws Exception { 338 assertWithMessage("User %s not initialized", userId).that(isUserInitialized(userId)) 339 .isTrue(); 340 CLog.v("User %d is initialized", userId); 341 } 342 343 /** 344 * Checks if the given user is initialized. 345 */ isUserInitialized(int userId)346 protected boolean isUserInitialized(int userId) throws Exception { 347 UserInfo userInfo = getUserInfo(userId); 348 CLog.v("isUserInitialized(%d): %s", userId, userInfo); 349 return userInfo.flags.contains("INITIALIZED"); 350 } 351 352 /** 353 * Switches the current user. 354 */ switchUser(int userId)355 protected void switchUser(int userId) throws Exception { 356 waitForCarServiceReady(); 357 String output = executeCommand("cmd car_service switch-user %d", userId); 358 if (!output.contains("STATUS_SUCCESSFUL")) { 359 throw new IllegalStateException("Failed to switch to user " + userId + ": " + output); 360 } 361 waitUntilCurrentUser(userId); 362 } 363 364 /** 365 * Waits until the given user is the current foreground user. 366 */ waitUntilCurrentUser(int userId)367 protected void waitUntilCurrentUser(int userId) throws Exception { 368 CommonTestUtils.waitUntil("timed out (" + DEFAULT_TIMEOUT_SEC 369 + "s) waiting for current user to be " + userId 370 + " (it is " + getCurrentUserId() + ")", 371 DEFAULT_TIMEOUT_SEC, 372 () -> (getCurrentUserId() == userId)); 373 } 374 375 /** 376 * Removes a user by user ID and update the list of users to be removed. 377 */ removeUser(int userId)378 protected void removeUser(int userId) throws Exception { 379 executeCommand("cmd car_service remove-user %d", userId); 380 } 381 382 /** 383 * Removes users whose name start with the given prefix. 384 */ removeUsers(String prefix)385 protected void removeUsers(String prefix) throws Exception { 386 Pattern pattern = Pattern.compile("^.*id=(\\d+), name=(" + prefix + ".*),.*$"); 387 String output = executeCommand("cmd user list --all -v"); 388 for (String line : output.split("\\n")) { 389 Matcher matcher = pattern.matcher(line); 390 if (!matcher.find()) continue; 391 392 int userId = Integer.parseInt(matcher.group(1)); 393 String name = matcher.group(2); 394 CLog.e("Removing user with %s prefix (id=%d, name='%s')", prefix, userId, name); 395 removeUser(userId); 396 } 397 } 398 399 /** 400 * Checks if an app is installed for a given user. 401 */ isAppInstalledForUser(String packageName, int userId)402 protected boolean isAppInstalledForUser(String packageName, int userId) 403 throws DeviceNotAvailableException { 404 return getDevice().isPackageInstalled(packageName, Integer.toString(userId)); 405 } 406 407 /** 408 * Fails the test if the app is installed for the given user. 409 */ assertAppInstalledForUser(String packageName, int userId)410 protected void assertAppInstalledForUser(String packageName, int userId) 411 throws DeviceNotAvailableException { 412 assertWithMessage("%s should BE installed for user %s", packageName, userId).that( 413 isAppInstalledForUser(packageName, userId)).isTrue(); 414 } 415 416 /** 417 * Fails the test if the app is NOT installed for the given user. 418 */ assertAppNotInstalledForUser(String packageName, int userId)419 protected void assertAppNotInstalledForUser(String packageName, int userId) 420 throws DeviceNotAvailableException { 421 assertWithMessage("%s should NOT be installed for user %s", packageName, userId).that( 422 isAppInstalledForUser(packageName, userId)).isFalse(); 423 } 424 425 /** 426 * Restarts the system server process. 427 * 428 * <p>Useful for cases where the test case changes system properties, as 429 * {@link ITestDevice#reboot()} would reset them. 430 */ restartSystemServer()431 protected void restartSystemServer() throws Exception { 432 final ITestDevice device = getDevice(); 433 device.executeShellCommand("stop"); 434 device.executeShellCommand("start"); 435 device.waitForDeviceAvailable(); 436 waitForCarServiceReady(); 437 } 438 439 /** 440 * Gets mapping of package and permissions granted for requested user id. 441 * 442 * @return Map<String, List<String>> where key is the package name and 443 * the value is list of permissions granted for this user. 444 */ getPackagesAndPermissionsForUser(int userId)445 protected Map<String, List<String>> getPackagesAndPermissionsForUser(int userId) 446 throws Exception { 447 CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver(); 448 getDevice().executeShellCommand("dumpsys package --proto", receiver); 449 450 PackageServiceDumpProto dump = PackageServiceDumpProto.parser() 451 .parseFrom(receiver.getOutput()); 452 453 CLog.v("Device has %d packages while getPackagesAndPermissions", dump.getPackagesCount()); 454 Map<String, List<String>> pkgMap = new HashMap<>(); 455 for (PackageProto pkg : dump.getPackagesList()) { 456 String pkgName = pkg.getName(); 457 for (UserPermissionsProto userPermissions : pkg.getUserPermissionsList()) { 458 if (userPermissions.getId() == userId) { 459 pkgMap.put(pkg.getName(), userPermissions.getGrantedPermissionsList()); 460 break; 461 } 462 } 463 } 464 return pkgMap; 465 } 466 467 /** 468 * Sleeps for the given amount of milliseconds. 469 */ sleep(long ms)470 protected void sleep(long ms) throws InterruptedException { 471 CLog.v("Sleeping for %dms", ms); 472 Thread.sleep(ms); 473 CLog.v("Woke up; Little Susie woke up!"); 474 } 475 476 // TODO(b/169341308): move to common infra code 477 private static final class RequiredFeatureRule implements TestRule { 478 479 private final ITestInformationReceiver mReceiver; 480 private final String mFeature; 481 RequiredFeatureRule(ITestInformationReceiver receiver, String feature)482 RequiredFeatureRule(ITestInformationReceiver receiver, String feature) { 483 mReceiver = receiver; 484 mFeature = feature; 485 } 486 487 @Override apply(Statement base, Description description)488 public Statement apply(Statement base, Description description) { 489 return new Statement() { 490 491 @Override 492 public void evaluate() throws Throwable { 493 boolean hasFeature = false; 494 try { 495 hasFeature = mReceiver.getTestInformation().getDevice() 496 .hasFeature(mFeature); 497 } catch (DeviceNotAvailableException e) { 498 CLog.e("Could not check if device has feature %s: %e", mFeature, e); 499 return; 500 } 501 502 if (!hasFeature) { 503 CLog.d("skipping %s#%s" 504 + " because device does not have feature '%s'", 505 description.getClassName(), description.getMethodName(), mFeature); 506 throw new AssumptionViolatedException("Device does not have feature '" 507 + mFeature + "'"); 508 } 509 base.evaluate(); 510 } 511 }; 512 } 513 514 @Override toString()515 public String toString() { 516 return "RequiredFeatureRule[" + mFeature + "]"; 517 } 518 } 519 520 /** 521 * Represents a user as returned by {@code cmd user list -v}. 522 */ 523 public static final class UserInfo { 524 public final int id; 525 public final String flags; 526 public final String name; 527 public final String otherState; 528 529 private UserInfo(Matcher matcher) { 530 id = Integer.parseInt(matcher.group(USER_PATTERN_GROUP_ID)); 531 flags = matcher.group(USER_PATTERN_GROUP_FLAGS); 532 name = matcher.group(USER_PATTERN_GROUP_NAME); 533 otherState = matcher.group(USER_PATTERN_GROUP_OTHER_STATE); 534 } 535 536 @Override 537 public String toString() { 538 return "[UserInfo: id=" + id + ", flags=" + flags + ", name=" + name 539 + ", otherState=" + otherState + "]"; 540 } 541 } 542 } 543