1 /* 2 * Copyright (C) 2017 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.host.multiuser; 18 19 import android.platform.test.annotations.Presubmit; 20 21 import com.android.tradefed.device.DeviceNotAvailableException; 22 import com.android.tradefed.log.LogUtil.CLog; 23 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; 24 25 import org.junit.Before; 26 import org.junit.Rule; 27 import org.junit.Test; 28 import org.junit.rules.TestRule; 29 import org.junit.runner.Description; 30 import org.junit.runner.RunWith; 31 import org.junit.runners.model.Statement; 32 33 import java.util.HashSet; 34 import java.util.LinkedHashSet; 35 import java.util.Scanner; 36 import java.util.Set; 37 import java.util.regex.Matcher; 38 import java.util.regex.Pattern; 39 40 import static org.junit.Assert.assertTrue; 41 42 /** 43 * Test verifies that users can be created/switched to without error dialogs shown to the user 44 * Run: atest CreateUsersNoAppCrashesTest 45 */ 46 @RunWith(DeviceJUnit4ClassRunner.class) 47 public class CreateUsersNoAppCrashesTest extends BaseMultiUserTest { 48 private int mInitialUserId; 49 private static final long LOGCAT_POLL_INTERVAL_MS = 5000; 50 private static final long USER_SWITCH_COMPLETE_TIMEOUT_MS = 180000; 51 52 @Rule public AppCrashRetryRule appCrashRetryRule = new AppCrashRetryRule(); 53 54 @Before setUp()55 public void setUp() throws Exception { 56 CLog.e("setup_CreateUsersNoAppCrashesTest"); 57 super.setUp(); 58 mInitialUserId = getDevice().getCurrentUser(); 59 } 60 61 @Presubmit 62 @Test testCanCreateGuestUser()63 public void testCanCreateGuestUser() throws Exception { 64 if (!mSupportsMultiUser) { 65 return; 66 } 67 int userId = getDevice().createUser( 68 "TestUser_" + System.currentTimeMillis() /* name */, 69 true /* guest */, 70 false /* ephemeral */); 71 assertSwitchToNewUser(userId); 72 assertSwitchToUser(userId, mInitialUserId); 73 74 } 75 76 @Presubmit 77 @Test testCanCreateSecondaryUser()78 public void testCanCreateSecondaryUser() throws Exception { 79 if (!mSupportsMultiUser) { 80 return; 81 } 82 int userId = getDevice().createUser( 83 "TestUser_" + System.currentTimeMillis() /* name */, 84 false /* guest */, 85 false /* ephemeral */); 86 assertSwitchToNewUser(userId); 87 assertSwitchToUser(userId, mInitialUserId); 88 } 89 assertSwitchToNewUser(int toUserId)90 private void assertSwitchToNewUser(int toUserId) throws Exception { 91 final String exitString = "Finished processing BOOT_COMPLETED for u" + toUserId; 92 final Set<String> appErrors = new LinkedHashSet<>(); 93 getDevice().executeAdbCommand("logcat", "-c"); // Reset log 94 assertTrue("Couldn't switch to user " + toUserId, getDevice().switchUser(toUserId)); 95 final boolean result = waitForUserSwitchComplete(appErrors, toUserId, exitString); 96 assertTrue("Didn't receive BOOT_COMPLETED delivered notification. appErrors=" 97 + appErrors, result); 98 if (!appErrors.isEmpty()) { 99 throw new AppCrashOnBootError(appErrors); 100 } 101 } 102 assertSwitchToUser(int fromUserId, int toUserId)103 private void assertSwitchToUser(int fromUserId, int toUserId) throws Exception { 104 final String exitString = "Continue user switch oldUser #" + fromUserId + ", newUser #" 105 + toUserId; 106 final Set<String> appErrors = new LinkedHashSet<>(); 107 getDevice().executeAdbCommand("logcat", "-c"); // Reset log 108 assertTrue("Couldn't switch to user " + toUserId, getDevice().switchUser(toUserId)); 109 final boolean result = waitForUserSwitchComplete(appErrors, toUserId, exitString); 110 assertTrue("Didn't reach \"Continue user switch\" stage. appErrors=" + appErrors, result); 111 if (!appErrors.isEmpty()) { 112 throw new AppCrashOnBootError(appErrors); 113 } 114 } 115 waitForUserSwitchComplete(Set<String> appErrors, int targetUserId, String exitString)116 private boolean waitForUserSwitchComplete(Set<String> appErrors, int targetUserId, 117 String exitString) throws DeviceNotAvailableException, InterruptedException { 118 boolean mExitFound = false; 119 long ti = System.currentTimeMillis(); 120 while (System.currentTimeMillis() - ti < USER_SWITCH_COMPLETE_TIMEOUT_MS) { 121 String logs = getDevice().executeAdbCommand("logcat", "-v", "brief", "-d", 122 "ActivityManager:D", "AndroidRuntime:E", "*:S"); 123 Scanner in = new Scanner(logs); 124 while (in.hasNextLine()) { 125 String line = in.nextLine(); 126 if (line.contains("Showing crash dialog for package")) { 127 appErrors.add(line); 128 } else if (line.contains(exitString)) { 129 // Parse all logs in case crashes occur as a result of onUserChange callbacks 130 mExitFound = true; 131 } else if (line.contains("FATAL EXCEPTION IN SYSTEM PROCESS")) { 132 throw new IllegalStateException("System process crashed - " + line); 133 } 134 } 135 in.close(); 136 if (mExitFound) { 137 if (!appErrors.isEmpty()) { 138 CLog.w("App crash dialogs found: " + appErrors); 139 } 140 return true; 141 } 142 Thread.sleep(LOGCAT_POLL_INTERVAL_MS); 143 } 144 return false; 145 } 146 147 static class AppCrashOnBootError extends AssertionError { 148 private static final Pattern PACKAGE_NAME_PATTERN = Pattern.compile("package ([^\\s]+)"); 149 private Set<String> errorPackages; 150 AppCrashOnBootError(Set<String> errorLogs)151 AppCrashOnBootError(Set<String> errorLogs) { 152 super("App error dialog(s) are present: " + errorLogs); 153 this.errorPackages = errorLogsToPackageNames(errorLogs); 154 } 155 errorLogsToPackageNames(Set<String> errorLogs)156 private static Set<String> errorLogsToPackageNames(Set<String> errorLogs) { 157 Set<String> result = new HashSet<>(); 158 for (String line : errorLogs) { 159 Matcher matcher = PACKAGE_NAME_PATTERN.matcher(line); 160 if (matcher.find()) { 161 result.add(matcher.group(1)); 162 } else { 163 throw new IllegalStateException("Unrecognized line " + line); 164 } 165 } 166 return result; 167 } 168 } 169 170 /** 171 * Rule that retries the test if it failed due to {@link AppCrashOnBootError} 172 */ 173 public static class AppCrashRetryRule implements TestRule { 174 175 @Override apply(Statement base, Description description)176 public Statement apply(Statement base, Description description) { 177 return new Statement() { 178 @Override 179 public void evaluate() throws Throwable { 180 Set<String> errors = evaluateAndReturnAppCrashes(base); 181 if (errors.isEmpty()) { 182 return; 183 } 184 CLog.e("Retrying due to app crashes: " + errors); 185 // Fail only if same apps are crashing in both runs 186 errors.retainAll(evaluateAndReturnAppCrashes(base)); 187 assertTrue("App error dialog(s) are present after 2 attempts: " + errors, 188 errors.isEmpty()); 189 } 190 }; 191 } 192 evaluateAndReturnAppCrashes(Statement base)193 private static Set<String> evaluateAndReturnAppCrashes(Statement base) throws Throwable { 194 try { 195 base.evaluate(); 196 } catch (AppCrashOnBootError e) { 197 return e.errorPackages; 198 } 199 return new HashSet<>(); 200 } 201 } 202 } 203