| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| |
| package android.host.multiuser; |
| |
| import android.platform.test.annotations.Presubmit; |
| |
| import com.android.tradefed.device.DeviceNotAvailableException; |
| import com.android.tradefed.log.LogUtil.CLog; |
| import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; |
| |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.rules.TestRule; |
| import org.junit.runner.Description; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.model.Statement; |
| |
| import java.util.HashSet; |
| import java.util.LinkedHashSet; |
| import java.util.Scanner; |
| import java.util.Set; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import static org.junit.Assert.assertTrue; |
| |
| /** |
| * Test verifies that users can be created/switched to without error dialogs shown to the user |
| * Run: atest CreateUsersNoAppCrashesTest |
| */ |
| @RunWith(DeviceJUnit4ClassRunner.class) |
| public class CreateUsersNoAppCrashesTest extends BaseMultiUserTest { |
| private static final long LOGCAT_POLL_INTERVAL_MS = 1000; |
| private static final long USER_SWITCH_COMPLETE_TIMEOUT_MS = 180000; |
| |
| @Rule public AppCrashRetryRule appCrashRetryRule = new AppCrashRetryRule(); |
| |
| @Presubmit |
| @Test |
| public void testCanCreateGuestUser() throws Exception { |
| if (!mSupportsMultiUser) { |
| return; |
| } |
| int userId = getDevice().createUser( |
| "TestUser_" + System.currentTimeMillis() /* name */, |
| true /* guest */, |
| false /* ephemeral */); |
| assertSwitchToNewUser(userId); |
| assertSwitchToUser(userId, mInitialUserId); |
| } |
| |
| @Presubmit |
| @Test |
| public void testCanCreateSecondaryUser() throws Exception { |
| if (!mSupportsMultiUser) { |
| return; |
| } |
| int userId = getDevice().createUser( |
| "TestUser_" + System.currentTimeMillis() /* name */, |
| false /* guest */, |
| false /* ephemeral */); |
| assertSwitchToNewUser(userId); |
| assertSwitchToUser(userId, mInitialUserId); |
| } |
| |
| private void assertSwitchToNewUser(int toUserId) throws Exception { |
| final String exitString = "Finished processing BOOT_COMPLETED for u" + toUserId; |
| final Set<String> appErrors = new LinkedHashSet<>(); |
| getDevice().executeAdbCommand("logcat", "-c"); // Reset log |
| assertTrue("Couldn't switch to user " + toUserId, getDevice().switchUser(toUserId)); |
| final boolean result = waitForUserSwitchComplete(appErrors, toUserId, exitString); |
| assertTrue("Didn't receive BOOT_COMPLETED delivered notification. appErrors=" |
| + appErrors, result); |
| if (!appErrors.isEmpty()) { |
| throw new AppCrashOnBootError(appErrors); |
| } |
| } |
| |
| private void assertSwitchToUser(int fromUserId, int toUserId) throws Exception { |
| final String exitString = "Continue user switch oldUser #" + fromUserId + ", newUser #" |
| + toUserId; |
| final Set<String> appErrors = new LinkedHashSet<>(); |
| getDevice().executeAdbCommand("logcat", "-c"); // Reset log |
| assertTrue("Couldn't switch to user " + toUserId, getDevice().switchUser(toUserId)); |
| final boolean result = waitForUserSwitchComplete(appErrors, toUserId, exitString); |
| assertTrue("Didn't reach \"Continue user switch\" stage. appErrors=" + appErrors, result); |
| if (!appErrors.isEmpty()) { |
| throw new AppCrashOnBootError(appErrors); |
| } |
| } |
| |
| private boolean waitForUserSwitchComplete(Set<String> appErrors, int targetUserId, |
| String exitString) throws DeviceNotAvailableException, InterruptedException { |
| boolean mExitFound = false; |
| long ti = System.currentTimeMillis(); |
| while (System.currentTimeMillis() - ti < USER_SWITCH_COMPLETE_TIMEOUT_MS) { |
| String logs = getDevice().executeAdbCommand("logcat", "-v", "brief", "-d", |
| "ActivityManager:D", "AndroidRuntime:E", "*:S"); |
| Scanner in = new Scanner(logs); |
| while (in.hasNextLine()) { |
| String line = in.nextLine(); |
| if (line.contains("Showing crash dialog for package")) { |
| appErrors.add(line); |
| } else if (line.contains(exitString)) { |
| // Parse all logs in case crashes occur as a result of onUserChange callbacks |
| mExitFound = true; |
| } else if (line.contains("FATAL EXCEPTION IN SYSTEM PROCESS")) { |
| throw new IllegalStateException("System process crashed - " + line); |
| } |
| } |
| in.close(); |
| if (mExitFound) { |
| if (!appErrors.isEmpty()) { |
| CLog.w("App crash dialogs found: " + appErrors); |
| } |
| return true; |
| } |
| Thread.sleep(LOGCAT_POLL_INTERVAL_MS); |
| } |
| return false; |
| } |
| |
| static class AppCrashOnBootError extends AssertionError { |
| private static final Pattern PACKAGE_NAME_PATTERN = Pattern.compile("package ([^\\s]+)"); |
| private Set<String> errorPackages; |
| |
| AppCrashOnBootError(Set<String> errorLogs) { |
| super("App error dialog(s) are present: " + errorLogs); |
| this.errorPackages = errorLogsToPackageNames(errorLogs); |
| } |
| |
| private static Set<String> errorLogsToPackageNames(Set<String> errorLogs) { |
| Set<String> result = new HashSet<>(); |
| for (String line : errorLogs) { |
| Matcher matcher = PACKAGE_NAME_PATTERN.matcher(line); |
| if (matcher.find()) { |
| result.add(matcher.group(1)); |
| } else { |
| throw new IllegalStateException("Unrecognized line " + line); |
| } |
| } |
| return result; |
| } |
| } |
| |
| /** |
| * Rule that retries the test if it failed due to {@link AppCrashOnBootError} |
| */ |
| public static class AppCrashRetryRule implements TestRule { |
| |
| @Override |
| public Statement apply(Statement base, Description description) { |
| return new Statement() { |
| @Override |
| public void evaluate() throws Throwable { |
| Set<String> errors = evaluateAndReturnAppCrashes(base); |
| if (errors.isEmpty()) { |
| return; |
| } |
| CLog.e("Retrying due to app crashes: " + errors); |
| // Fail only if same apps are crashing in both runs |
| errors.retainAll(evaluateAndReturnAppCrashes(base)); |
| assertTrue("App error dialog(s) are present after 2 attempts: " + errors, |
| errors.isEmpty()); |
| } |
| }; |
| } |
| |
| private static Set<String> evaluateAndReturnAppCrashes(Statement base) throws Throwable { |
| try { |
| base.evaluate(); |
| } catch (AppCrashOnBootError e) { |
| return e.errorPackages; |
| } |
| return new HashSet<>(); |
| } |
| } |
| } |