• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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