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