blob: 08461fbda06108abf1862477d6f5e76ee1a8448f [file] [log] [blame]
/*
* 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.media.cts;
import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
import com.android.ddmlib.testrunner.TestResult.TestStatus;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.CollectingTestListener;
import com.android.tradefed.result.TestDescription;
import com.android.tradefed.result.TestResult;
import com.android.tradefed.result.TestRunResult;
import com.android.tradefed.testtype.DeviceTestCase;
import com.android.tradefed.testtype.IBuildReceiver;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Base class for host-side tests for multi-user aware media APIs.
*/
public class BaseMultiUserTest extends DeviceTestCase implements IBuildReceiver {
private static final String RUNNER = "androidx.test.runner.AndroidJUnitRunner";
/**
* The defined timeout (in milliseconds) is used as a maximum waiting time when expecting the
* command output from the device. At any time, if the shell command does not output anything
* for a period longer than the defined timeout the Tradefed run terminates.
*/
private static final long DEFAULT_SHELL_TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(5);
/**
* Instrumentation test runner argument key used for individual test timeout
**/
protected static final String TEST_TIMEOUT_INST_ARGS_KEY = "timeout_msec";
/**
* Sets timeout (in milliseconds) that will be applied to each test. In the
* event of a test timeout it will log the results and proceed with executing the next test.
*/
private static final long DEFAULT_TEST_TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(5);
private static final String SETTINGS_PACKAGE_VERIFIER_NAMESPACE = "global";
private static final String SETTINGS_PACKAGE_VERIFIER_NAME = "package_verifier_enable";
/**
* User ID for all users.
* The value is from the UserHandle class.
*/
protected static final int USER_ALL = -1;
/**
* User ID for the system user.
* The value is from the UserHandle class.
*/
protected static final int USER_SYSTEM = 0;
private IBuildInfo mCtsBuild;
private String mPackageVerifier;
private Set<String> mExistingPackages;
private List<Integer> mExistingUsers;
private HashSet<String> mAvailableFeatures;
@Override
protected void setUp() throws Exception {
super.setUp();
// Ensure that build has been set before test is run.
assertNotNull(mCtsBuild);
mExistingPackages = getDevice().getInstalledPackageNames();
// Disable the package verifier to avoid the dialog when installing an app
mPackageVerifier =
getSettings(
SETTINGS_PACKAGE_VERIFIER_NAMESPACE,
SETTINGS_PACKAGE_VERIFIER_NAME,
USER_ALL);
putSettings(
SETTINGS_PACKAGE_VERIFIER_NAMESPACE,
SETTINGS_PACKAGE_VERIFIER_NAME,
"0",
USER_ALL);
mExistingUsers = new ArrayList();
int primaryUserId = getDevice().getPrimaryUserId();
mExistingUsers.add(primaryUserId);
mExistingUsers.add(USER_SYSTEM);
executeShellCommand("am switch-user " + primaryUserId);
executeShellCommand("wm dismiss-keyguard");
}
@Override
protected void tearDown() throws Exception {
// Reset the package verifier setting to its original value.
putSettings(
SETTINGS_PACKAGE_VERIFIER_NAMESPACE,
SETTINGS_PACKAGE_VERIFIER_NAME,
mPackageVerifier,
USER_ALL);
// Remove users created during the test.
for (int userId : getDevice().listUsers()) {
if (!mExistingUsers.contains(userId)) {
removeUser(userId);
}
}
// Remove packages installed during the test.
for (String packageName : getDevice().getUninstallablePackageNames()) {
if (mExistingPackages.contains(packageName)) {
continue;
}
CLog.d("Removing leftover package: " + packageName);
getDevice().uninstallPackage(packageName);
}
super.tearDown();
}
@Override
public void setBuild(IBuildInfo buildInfo) {
mCtsBuild = buildInfo;
}
/**
* Installs the app as if the user of the ID {@param userId} has installed the app.
*
* @param appFileName file name of the app.
* @param userId user ID to install the app against.
*/
protected void installAppAsUser(String appFileName, int userId, boolean asInstantApp)
throws FileNotFoundException, DeviceNotAvailableException {
CLog.d("Installing app " + appFileName + " for user " + userId);
CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mCtsBuild);
String result = getDevice().installPackageForUser(
buildHelper.getTestFile(appFileName),
true,
true,
userId,
"-t",
asInstantApp ? "--instant" : "");
assertNull("Failed to install " + appFileName + " for user " + userId + ": " + result,
result);
}
/**
* Excutes shell command and returns the result.
*
* @param command command to run.
* @return result from the command. If the result was {@code null}, empty string ("") will be
* returned instead. Otherwise, trimmed result will be returned.
*/
protected @Nonnull String executeShellCommand(final String command) throws Exception {
CLog.d("Starting command " + command);
String commandOutput = getDevice().executeShellCommand(command);
CLog.d("Output for command " + command + ": " + commandOutput);
return commandOutput != null ? commandOutput.trim() : "";
}
private int createAndStartUser(String extraParam) throws Exception {
String command = "pm create-user" + extraParam + " TestUser_" + System.currentTimeMillis();
String commandOutput = executeShellCommand(command);
String[] tokens = commandOutput.split("\\s+");
assertTrue(tokens.length > 0);
assertEquals("Success:", tokens[0]);
int userId = Integer.parseInt(tokens[tokens.length-1]);
// Start user for MediaSessionService to notice the created user.
getDevice().startUser(userId);
return userId;
}
/**
* Creates and starts a new user.
*/
protected int createAndStartUser() throws Exception {
return createAndStartUser("");
}
/**
* Creates and starts a restricted profile for the {@param parentUserId}.
*
* @param parentUserId parent user id.
*/
protected int createAndStartRestrictedProfile(int parentUserId) throws Exception {
return createAndStartUser(" --profileOf " + parentUserId + " --restricted");
}
/**
* Creates and starts a managed profile for the {@param parentUserId}.
*
* @param parentUserId parent user id.
*/
protected int createAndStartManagedProfile(int parentUserId) throws Exception {
return createAndStartUser(" --profileOf " + parentUserId + " --managed");
}
/**
* Removes the user that is created during the test.
* <p>It will be no-op if the user cannot be removed or doesn't exist.
*
* @param userId user ID to remove.
*/
protected void removeUser(int userId) throws Exception {
if (getDevice().listUsers().contains(userId) && userId != USER_SYSTEM
&& !mExistingUsers.contains(userId)) {
getDevice().executeShellCommand("am wait-for-broadcast-idle");
// Don't log output, as tests sometimes set no debug user restriction, which
// causes this to fail, we should still continue and remove the user.
String stopUserCommand = "am stop-user -w -f " + userId;
CLog.d("Stopping and removing user " + userId);
getDevice().executeShellCommand(stopUserCommand);
assertTrue("Couldn't remove user", getDevice().removeUser(userId));
}
}
/**
* Runs tests on the device as if it's {@param userId}.
*
* @param pkgName test package file name that contains the {@link AndroidTestCase}
* @param testClassName Class name to test within the test package. Can be {@code null} if you
* want to run all test classes in the package.
* @param testMethodName Method name to test within the test class. Can be {@code null} if you
* want to run all test methods in the class. Will be ignored if {@param testClassName} is
* {@code null}.
* @param userId user ID to run the tests as.
*/
protected void runDeviceTestsAsUser(
String pkgName, @Nullable String testClassName,
@Nullable String testMethodName, int userId) throws DeviceNotAvailableException {
if (testClassName != null && testClassName.startsWith(".")) {
testClassName = pkgName + testClassName;
}
RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner(
pkgName, RUNNER, getDevice().getIDevice());
testRunner.setMaxTimeToOutputResponse(DEFAULT_SHELL_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
testRunner.addInstrumentationArg(
TEST_TIMEOUT_INST_ARGS_KEY, Long.toString(DEFAULT_TEST_TIMEOUT_MILLIS));
if (testClassName != null && testMethodName != null) {
testRunner.setMethodName(testClassName, testMethodName);
} else if (testClassName != null) {
testRunner.setClassName(testClassName);
}
CollectingTestListener listener = new CollectingTestListener();
assertTrue(getDevice().runInstrumentationTestsAsUser(testRunner, userId, listener));
final TestRunResult result = listener.getCurrentRunResults();
if (result.isRunFailure()) {
throw new AssertionError("Failed to successfully run device tests for "
+ result.getName() + ": " + result.getRunFailureMessage());
}
if (result.getNumTests() == 0) {
throw new AssertionError("No tests were run on the device");
}
if (result.hasFailedTests()) {
// Build a meaningful error message
StringBuilder errorBuilder = new StringBuilder("On-device tests failed:\n");
for (Map.Entry<TestDescription, TestResult> resultEntry :
result.getTestResults().entrySet()) {
if (!resultEntry.getValue().getStatus().equals(TestStatus.PASSED)) {
errorBuilder.append(resultEntry.getKey().toString());
errorBuilder.append(":\n");
errorBuilder.append(resultEntry.getValue().getStackTrace());
}
}
throw new AssertionError(errorBuilder.toString());
}
}
/**
* Checks whether it is possible to create the desired number of users.
*/
protected boolean canCreateAdditionalUsers(int numberOfUsers)
throws DeviceNotAvailableException {
return getDevice().listUsers().size() + numberOfUsers <=
getDevice().getMaxNumberOfUsersSupported();
}
/**
* Gets the system setting as a string from the system settings provider for the user.
*
* @param namespace namespace of the setting.
* @param name name of the setting.
* @param userId user ID to query the setting. Can be {@link #USER_ALL}.
* @return value of the system setting provider with the given namespace and name.
* {@code null}, empty string, or "null" will be returned to the empty string ("") instead.
*/
protected @Nonnull String getSettings(@Nonnull String namespace, @Nonnull String name,
int userId) throws Exception {
String userFlag = (userId == USER_ALL) ? "" : " --user " + userId;
String commandOutput = executeShellCommand(
"settings" + userFlag + " get " + namespace + " " + name);
if (commandOutput == null || commandOutput.isEmpty() || commandOutput.equals("null")) {
commandOutput = "";
}
return commandOutput;
}
/**
* Puts the string to the system settings provider for the user.
* <p>This deletes the setting for an empty {@param value} as 'settings put' doesn't allow
* putting empty value.
*
* @param namespace namespace of the setting.
* @param name name of the setting.
* @param value value of the system setting provider with the given namespace and name.
* @param userId user ID to set the setting. Can be {@link #USER_ALL}.
*/
protected void putSettings(@Nonnull String namespace, @Nonnull String name,
@Nullable String value, int userId) throws Exception {
if (value == null || value.isEmpty()) {
// Delete the setting if the value is null or empty as 'settings put' doesn't accept
// them.
// Ignore userId here because 'settings delete' doesn't support it.
executeShellCommand("settings delete " + namespace + " " + name);
} else {
String userFlag = (userId == USER_ALL) ? "" : " --user " + userId;
executeShellCommand("settings" + userFlag + " put " + namespace + " " + name
+ " " + value);
}
}
protected boolean hasDeviceFeature(String requiredFeature) throws DeviceNotAvailableException {
if (mAvailableFeatures == null) {
// TODO: Move this logic to ITestDevice.
String command = "pm list features";
String commandOutput = getDevice().executeShellCommand(command);
CLog.i("Output for command " + command + ": " + commandOutput);
// Extract the id of the new user.
mAvailableFeatures = new HashSet<>();
for (String feature : commandOutput.split("\\s+")) {
// Each line in the output of the command has the format "feature:{FEATURE_VALUE}".
String[] tokens = feature.split(":");
assertTrue(
"\"" + feature + "\" expected to have format feature:{FEATURE_VALUE}",
tokens.length > 1);
assertEquals(feature, "feature", tokens[0]);
mAvailableFeatures.add(tokens[1]);
}
}
boolean result = mAvailableFeatures.contains(requiredFeature);
return result;
}
}