blob: 0d9d648a47816587401dc51c76c23dd3b44bae24 [file] [log] [blame]
/*
* Copyright (C) 2021 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 com.android.sdkext.extensions;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import android.cts.install.lib.host.InstallUtilsHost;
import com.android.modules.utils.build.testing.DeviceSdkLevel;
import com.android.os.ext.testing.CurrentVersion;
import com.android.tests.rollback.host.AbandonSessionsRule;
import com.android.tradefed.device.ITestDevice.ApexInfo;
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
import com.android.tradefed.util.CommandResult;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
import java.time.Duration;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@RunWith(DeviceJUnit4ClassRunner.class)
public class SdkExtensionsHostTest extends BaseHostJUnit4Test {
private static final String APP_FILENAME = "sdkextensions_e2e_test_app.apk";
private static final String APP_PACKAGE = "com.android.sdkext.extensions.apps";
private static final String MEDIA_FILENAME = "test_com.android.media.apex";
private static final String SDKEXTENSIONS_FILENAME = "test_com.android.sdkext.apex";
private static String appFilename(String appName) {
return "sdkextensions_e2e_test_app_req_" + appName + ".apk";
}
private static String appPackage(String appName) {
return "com.android.sdkext.extensions.apps." + appName;
}
private static final Duration BOOT_COMPLETE_TIMEOUT = Duration.ofMinutes(2);
private final InstallUtilsHost mInstallUtils = new InstallUtilsHost(this);
private DeviceSdkLevel mDeviceSdkLevel;
private Boolean mIsAtLeastS = null;
private Boolean mIsAtLeastT = null;
private Boolean mIsAtLeastU = null;
@Rule public AbandonSessionsRule mHostTestRule = new AbandonSessionsRule(this);
@Before
public void setUp() throws Exception {
assumeTrue("Updating APEX is not supported", mInstallUtils.isApexUpdateSupported());
mDeviceSdkLevel = new DeviceSdkLevel(getDevice());
}
@Before
public void installTestApp() throws Exception {
File testAppFile = mInstallUtils.getTestFile(APP_FILENAME);
String installResult = getDevice().installPackage(testAppFile, true);
assertNull(installResult);
}
@Before // Generally not needed, but local test devices are sometimes in a "bad" start state.
@After
public void cleanup() throws Exception {
getDevice().uninstallPackage(APP_PACKAGE);
uninstallApexes(SDKEXTENSIONS_FILENAME, MEDIA_FILENAME);
}
@Test
public void testDefault() throws Exception {
assertVersionDefault();
}
@Test
public void upgradeOneApexWithBump() throws Exception {
assertVersionDefault();
mInstallUtils.installApexes(SDKEXTENSIONS_FILENAME);
reboot();
// Version 12 requires sdkext, which is fulfilled
// Version 45 requires sdkext + media, which isn't fulfilled
assertRVersionEquals(12);
assertSVersionEquals(12);
assertTVersionEquals(12);
assertTestMethodsPresent(); // 45 APIs are available on 12 too.
}
@Test
public void upgradeOneApex() throws Exception {
// Version 45 requires updated sdkext and media, so updating just media changes nothing.
assertVersionDefault();
mInstallUtils.installApexes(MEDIA_FILENAME);
reboot();
assertVersionDefault();
}
@Test
public void upgradeTwoApexes() throws Exception {
// Updating sdkext and media bumps the version to 45.
assertVersionDefault();
mInstallUtils.installApexes(MEDIA_FILENAME, SDKEXTENSIONS_FILENAME);
reboot();
assertVersion45();
}
private boolean canInstallApp(String appName) throws Exception {
File appFile = mInstallUtils.getTestFile(appFilename(appName));
String installResult = getDevice().installPackage(appFile, true);
if (installResult != null) {
return false;
}
assertNull(getDevice().uninstallPackage(appPackage(appName)));
return true;
}
private String getExtensionVersionFromSysprop(String v) throws Exception {
String command = "getprop build.version.extensions." + v;
CommandResult res = getDevice().executeShellV2Command(command);
checkExitCode(command, res);
return res.getStdout().replace("\n", "");
}
private String broadcast(String action, String extra) throws Exception {
String command = getBroadcastCommand(action, extra);
CommandResult res = getDevice().executeShellV2Command(command);
checkExitCode(command, res);
Matcher matcher = Pattern.compile("data=\"([^\"]+)\"").matcher(res.getStdout());
assertTrue("Unexpected output from am broadcast: " + res.getStdout(), matcher.find());
return matcher.group(1);
}
private static void checkExitCode(String command, CommandResult res) {
int exitCode = (int) res.getExitCode();
if (exitCode != 0) {
throw new IllegalStateException(
String.format(
"Unexpected result from `%s`\n"
+ " exitCode=%d\n"
+ " stderr=\n"
+ "%s\n"
+ " stdout=\n"
+ "%s\n",
command, exitCode, res.getStderr(), res.getStdout()));
}
}
private boolean broadcastForBoolean(String action, String extra) throws Exception {
String result = broadcast(action, extra);
if (result.equals("true") || result.equals("false")) {
return result.equals("true");
}
throw getAppParsingError(result);
}
private int broadcastForInt(String action, String extra) throws Exception {
String result = broadcast(action, extra);
try {
return Integer.parseInt(result);
} catch (NumberFormatException e) {
throw getAppParsingError(result);
}
}
private Error getAppParsingError(String result) {
String message = "App error! Full stack trace in logcat (grep for SdkExtensionsE2E): ";
return new AssertionError(message + result);
}
private void assertVersionDefault() throws Exception {
int expected =
isAtLeastU()
? CurrentVersion.CURRENT_TRAIN_VERSION
: isAtLeastT()
? CurrentVersion.T_BASE_VERSION
: isAtLeastS()
? CurrentVersion.S_BASE_VERSION
: CurrentVersion.R_BASE_VERSION;
assertRVersionEquals(expected);
assertSVersionEquals(expected);
assertTVersionEquals(expected);
assertTestMethodsNotPresent();
}
private void assertVersion45() throws Exception {
assertRVersionEquals(45);
assertSVersionEquals(45);
assertTVersionEquals(45);
assertTestMethodsPresent();
}
private void assertTestMethodsNotPresent() throws Exception {
assertTrue(broadcastForBoolean("MAKE_CALLS_DEFAULT", null));
}
private void assertTestMethodsPresent() throws Exception {
if (isAtLeastS()) {
assertTrue(broadcastForBoolean("MAKE_CALLS_45", null));
} else {
// The APIs in the test apex are not currently getting installed correctly
// on Android R devices because they rely on the dynamic classpath feature.
// TODO(b/234361913): fix this
assertTestMethodsNotPresent();
}
}
private void assertRVersionEquals(int version) throws Exception {
String[] apps =
version >= 45
? new String[] {"r12", "r45"}
: version >= 12 ? new String[] {"r12"} : new String[] {};
assertExtensionVersionEquals("r", version, apps, true);
}
private void assertSVersionEquals(int version) throws Exception {
// These APKs require the same R version as they do S version.
int minVersion = Math.min(version, broadcastForInt("GET_SDK_VERSION", "r"));
String[] apps =
minVersion >= 45
? new String[] {"s12", "s45"}
: minVersion >= 12 ? new String[] {"s12"} : new String[] {};
assertExtensionVersionEquals("s", version, apps, isAtLeastS());
}
private void assertTVersionEquals(int version) throws Exception {
assertExtensionVersionEquals("t", version, new String[] {}, isAtLeastT());
}
private void assertExtensionVersionEquals(
String extension, int version, String[] apps, boolean expected) throws Exception {
int appValue = broadcastForInt("GET_SDK_VERSION", extension);
String syspropValue = getExtensionVersionFromSysprop(extension);
if (expected) {
assertEquals(version, appValue);
assertEquals(String.valueOf(version), syspropValue);
for (String app : apps) {
assertTrue(canInstallApp(app));
}
} else {
assertEquals(0, appValue);
assertEquals("", syspropValue);
for (String app : apps) {
assertFalse(canInstallApp(app));
}
}
}
private static String getBroadcastCommand(String action, String extra) {
String cmd = "am broadcast";
cmd += " -a com.android.sdkext.extensions.apps." + action;
if (extra != null) {
cmd += " -e extra " + extra;
}
cmd += " -n com.android.sdkext.extensions.apps/.Receiver";
return cmd;
}
private boolean isAtLeastS() throws Exception {
if (mIsAtLeastS == null) {
mIsAtLeastS = mDeviceSdkLevel.isDeviceAtLeastS();
}
return mIsAtLeastS;
}
private boolean isAtLeastT() throws Exception {
if (mIsAtLeastT == null) {
mIsAtLeastT = mDeviceSdkLevel.isDeviceAtLeastT();
}
return mIsAtLeastT;
}
private boolean isAtLeastU() throws Exception {
if (mIsAtLeastU == null) {
mIsAtLeastU = mDeviceSdkLevel.isDeviceAtLeastU();
}
return mIsAtLeastU;
}
private boolean uninstallApexes(String... filenames) throws Exception {
boolean reboot = false;
for (String filename : filenames) {
ApexInfo apex = mInstallUtils.getApexInfo(mInstallUtils.getTestFile(filename));
String res = getDevice().uninstallPackage(apex.name);
// res is null for successful uninstalls (non-null likely implesfactory version).
reboot |= res == null;
}
if (reboot) {
reboot();
return true;
}
return false;
}
private void reboot() throws Exception {
getDevice().reboot();
boolean success = getDevice().waitForBootComplete(BOOT_COMPLETE_TIMEOUT.toMillis());
assertWithMessage("Device didn't boot in %s", BOOT_COMPLETE_TIMEOUT).that(success).isTrue();
}
}