1 /* 2 * Copyright (C) 2016 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 package android.host.multiuser; 17 18 import static com.google.common.truth.Truth.assertWithMessage; 19 20 import static org.junit.Assume.assumeTrue; 21 22 import com.android.ddmlib.Log; 23 import com.android.tradefed.device.DeviceNotAvailableException; 24 import com.android.tradefed.log.LogUtil.CLog; 25 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; 26 import com.android.tradefed.util.RunUtil; 27 28 import org.junit.After; 29 import org.junit.AssumptionViolatedException; 30 import org.junit.Before; 31 import org.junit.Rule; 32 import org.junit.rules.TestName; 33 import org.junit.rules.TestRule; 34 import org.junit.runner.Description; 35 import org.junit.runners.model.Statement; 36 37 import java.util.ArrayList; 38 import java.util.HashSet; 39 import java.util.Set; 40 import java.util.regex.Matcher; 41 import java.util.regex.Pattern; 42 43 /** 44 * Base class for multi user tests. 45 */ 46 // Must be public because of @Rule 47 public abstract class BaseMultiUserTest extends BaseHostJUnit4Test { 48 49 /** Guest flag value from android/content/pm/UserInfo.java */ 50 private static final int FLAG_GUEST = 0x00000004; 51 52 /** 53 * Feature flag for automotive devices 54 * https://source.android.com/compatibility/android-cdd#2_5_automotive_requirements 55 */ 56 private static final String FEATURE_AUTOMOTIVE = "feature:android.hardware.type.automotive"; 57 58 protected static final String TEST_APP_PKG_NAME = "com.android.cts.multiuser"; 59 protected static final String TEST_APP_PKG_APK = "CtsMultiuserApp.apk"; 60 61 protected static final long LOGCAT_POLL_INTERVAL_MS = 1000; // 1 second 62 protected static final long USER_REMOVAL_COMPLETE_TIMEOUT_MS = 8 * 60 * 1000; // 8 minutes 63 64 /** Whether multi-user is supported. */ 65 protected int mInitialUserId; 66 protected int mPrimaryUserId; 67 68 /** Users we shouldn't delete in the tests. */ 69 private ArrayList<Integer> mFixedUsers; 70 71 @Rule 72 public final TestName mTestNameRule = new TestName(); 73 74 @Before setUp()75 public void setUp() throws Exception { 76 mInitialUserId = getDevice().getCurrentUser(); 77 mPrimaryUserId = getDevice().getPrimaryUserId(); 78 79 // Test should not modify / remove any of the existing users. 80 mFixedUsers = getDevice().listUsers(); 81 } 82 83 @After tearDown()84 public void tearDown() throws Exception { 85 int currentUserId = getDevice().getCurrentUser(); 86 if (currentUserId != mInitialUserId) { 87 CLog.w("User changed during test (to %d). Switching back to %d", currentUserId, 88 mInitialUserId); 89 getDevice().switchUser(mInitialUserId); 90 } 91 // Remove the users created during this test. 92 removeTestUsers(); 93 } 94 getTestName()95 protected String getTestName() { 96 return mTestNameRule.getMethodName(); 97 } 98 assumeNotRoot()99 protected void assumeNotRoot() throws DeviceNotAvailableException { 100 if (!getDevice().isAdbRoot()) return; 101 102 String message = "Cannot test " + getTestName() + " on rooted devices"; 103 CLog.logAndDisplay(Log.LogLevel.WARN, message); 104 throw new AssumptionViolatedException(message); 105 } 106 createRestrictedProfile(int userId)107 protected int createRestrictedProfile(int userId) 108 throws DeviceNotAvailableException, IllegalStateException{ 109 final String command = "pm create-user --profileOf " + userId + " --restricted " 110 + "TestUser_" + System.currentTimeMillis(); 111 final String output = getDevice().executeShellCommand(command); 112 113 if (output.startsWith("Success")) { 114 try { 115 return Integer.parseInt(output.substring(output.lastIndexOf(" ")).trim()); 116 } catch (NumberFormatException e) { 117 CLog.e("Failed to parse result: %s", output); 118 } 119 } else { 120 CLog.e("Failed to create restricted profile: %s", output); 121 } 122 throw new IllegalStateException(); 123 } 124 createGuestUser()125 protected int createGuestUser() throws Exception { 126 return getDevice().createUser( 127 "TestUser_" + System.currentTimeMillis() /* name */, 128 true /* guest */, 129 false /* ephemeral */); 130 } 131 getGuestUser()132 protected int getGuestUser() throws Exception { 133 for (int userId : getDevice().listUsers()) { 134 if ((getDevice().getUserFlags(userId) & FLAG_GUEST) != 0) { 135 return userId; 136 } 137 } 138 return -1; 139 } 140 assumeGuestDoesNotExist()141 protected void assumeGuestDoesNotExist() throws Exception { 142 assumeTrue("Device already has a guest user", getGuestUser() == -1); 143 } 144 assumeIsAutomotive()145 protected void assumeIsAutomotive() throws Exception { 146 assumeTrue("Device does not have " + FEATURE_AUTOMOTIVE, 147 getDevice().hasFeature(FEATURE_AUTOMOTIVE)); 148 } 149 assertSwitchToUser(int toUserId)150 protected void assertSwitchToUser(int toUserId) throws Exception { 151 final boolean switchResult = getDevice().switchUser(toUserId); 152 assertWithMessage("Couldn't switch to user %s", toUserId) 153 .that(switchResult).isTrue(); 154 155 final int currentUserId = getDevice().getCurrentUser(); 156 assertWithMessage("Current user is %s, after switching to user %s", currentUserId, toUserId) 157 .that(currentUserId).isEqualTo(toUserId); 158 } 159 assertUserNotPresent(int userId)160 protected void assertUserNotPresent(int userId) throws Exception { 161 assertWithMessage("User ID %s should not be present", userId) 162 .that(getDevice().listUsers()).doesNotContain(userId); 163 } 164 assertUserPresent(int userId)165 protected void assertUserPresent(int userId) throws Exception { 166 assertWithMessage("User ID %s should be present", userId) 167 .that(getDevice().listUsers()).contains(userId); 168 } 169 170 /* 171 * Waits for userId to removed or at removing state. 172 * Returns true if user is removed or at removing state. 173 * False if user is not removed by USER_SWITCH_COMPLETE_TIMEOUT_MS. 174 */ waitForUserRemove(int userId)175 protected boolean waitForUserRemove(int userId) 176 throws DeviceNotAvailableException, InterruptedException { 177 // Example output from dumpsys when user is flagged for removal: 178 // UserInfo{11:Driver:154} serialNo=50 <removing> <partial> 179 final String userSerialPatter = "(.*\\{)(\\d+)(.*\\})(.*=)(\\d+)(.*)"; 180 final Pattern pattern = Pattern.compile(userSerialPatter); 181 long ti = System.currentTimeMillis(); 182 while (System.currentTimeMillis() - ti < USER_REMOVAL_COMPLETE_TIMEOUT_MS) { 183 if (!getDevice().listUsers().contains(userId)) { 184 return true; 185 } 186 String commandOutput = getDevice().executeShellCommand("dumpsys user"); 187 Matcher matcher = pattern.matcher(commandOutput); 188 while(matcher.find()) { 189 if (Integer.parseInt(matcher.group(2)) == userId 190 && matcher.group(6).contains("removing")) { 191 return true; 192 } 193 } 194 RunUtil.getDefault().sleep(LOGCAT_POLL_INTERVAL_MS); 195 } 196 return false; 197 } 198 removeTestUsers()199 private void removeTestUsers() throws Exception { 200 for (int userId : getDevice().listUsers()) { 201 if (!mFixedUsers.contains(userId)) { 202 getDevice().removeUser(userId); 203 } 204 } 205 } 206 207 static class AppCrashOnBootError extends AssertionError { 208 private static final Pattern PACKAGE_NAME_PATTERN = Pattern.compile("package ([^\\s]+)"); 209 private Set<String> errorPackages; 210 AppCrashOnBootError(Set<String> errorLogs)211 AppCrashOnBootError(Set<String> errorLogs) { 212 super("App error dialog(s) are present: " + errorLogs); 213 this.errorPackages = errorLogsToPackageNames(errorLogs); 214 } 215 errorLogsToPackageNames(Set<String> errorLogs)216 private static Set<String> errorLogsToPackageNames(Set<String> errorLogs) { 217 Set<String> result = new HashSet<>(); 218 for (String line : errorLogs) { 219 Matcher matcher = PACKAGE_NAME_PATTERN.matcher(line); 220 if (matcher.find()) { 221 result.add(matcher.group(1)); 222 } else { 223 throw new IllegalStateException("Unrecognized line " + line); 224 } 225 } 226 return result; 227 } 228 } 229 230 /** 231 * Rule that retries the test if it failed due to {@link AppCrashOnBootError} 232 */ 233 public static class AppCrashRetryRule implements TestRule { 234 235 @Override apply(Statement base, Description description)236 public Statement apply(Statement base, Description description) { 237 return new Statement() { 238 @Override 239 public void evaluate() throws Throwable { 240 Set<String> errors = evaluateAndReturnAppCrashes(base); 241 if (errors.isEmpty()) { 242 CLog.v("Good News, Everyone! No App crashes on %s", 243 description.getMethodName()); 244 return; 245 } 246 CLog.e("Retrying due to app crashes: %s", errors); 247 // Fail only if same apps are crashing in both runs 248 errors.retainAll(evaluateAndReturnAppCrashes(base)); 249 assertWithMessage("App error dialog(s) are present after 2 attempts") 250 .that(errors).isEmpty(); 251 } 252 }; 253 } 254 evaluateAndReturnAppCrashes(Statement base)255 private static Set<String> evaluateAndReturnAppCrashes(Statement base) throws Throwable { 256 try { 257 base.evaluate(); 258 } catch (AppCrashOnBootError e) { 259 return e.errorPackages; 260 } 261 return new HashSet<>(); 262 } 263 } 264 265 /** 266 * Rule that skips a test if device does not support more than 1 user 267 */ 268 protected static class SupportsMultiUserRule implements TestRule { 269 270 private final BaseHostJUnit4Test mDeviceTest; 271 272 public SupportsMultiUserRule(BaseHostJUnit4Test deviceTest) { 273 mDeviceTest = deviceTest; 274 } 275 276 @Override 277 public Statement apply(Statement base, Description description) { 278 return new Statement() { 279 @Override 280 public void evaluate() throws Throwable { 281 boolean supports = mDeviceTest.getDevice().getMaxNumberOfUsersSupported() > 1; 282 assumeTrue("device does not support multi users", supports); 283 284 base.evaluate(); 285 } 286 }; 287 } 288 } 289 } 290