| /* |
| * Copyright (C) 2020 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.tests.apex; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.common.truth.Truth.assertWithMessage; |
| |
| import static org.junit.Assume.assumeTrue; |
| |
| import android.cts.install.lib.host.InstallUtilsHost; |
| |
| import com.android.apex.ApexInfo; |
| import com.android.apex.XmlParser; |
| import com.android.tests.rollback.host.AbandonSessionsRule; |
| import com.android.tradefed.device.ITestDevice; |
| import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; |
| import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; |
| |
| 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.io.FileInputStream; |
| import java.time.Duration; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.stream.Collectors; |
| |
| /** |
| * Host side integration tests for apexd. |
| */ |
| @RunWith(DeviceJUnit4ClassRunner.class) |
| public class ApexdHostTest extends BaseHostJUnit4Test { |
| |
| private static final String SHIM_APEX_PATH = "/system/apex/com.android.apex.cts.shim.apex"; |
| |
| private final InstallUtilsHost mHostUtils = new InstallUtilsHost(this); |
| |
| @Rule |
| public AbandonSessionsRule mHostTestRule = new AbandonSessionsRule(this); |
| |
| private boolean mWasAdbRoot = false; |
| |
| @Before |
| public void setUp() throws Exception { |
| mHostUtils.uninstallShimApexIfNecessary(); |
| mWasAdbRoot = getDevice().isAdbRoot(); |
| if (!mWasAdbRoot) { |
| assumeTrue("Device requires root", getDevice().enableAdbRoot()); |
| } |
| } |
| |
| @After |
| public void tearDown() throws Exception { |
| mHostUtils.uninstallShimApexIfNecessary(); |
| if (!mWasAdbRoot) { |
| getDevice().disableAdbRoot(); |
| } |
| } |
| |
| @Test |
| public void testOrphanedApexIsNotActivated() throws Exception { |
| assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); |
| assumeTrue("Device requires root", getDevice().isAdbRoot()); |
| try { |
| assertThat(getDevice().pushFile(mHostUtils.getTestFile("apex.apexd_test_v2.apex"), |
| "/data/apex/active/apexd_test_v2.apex")).isTrue(); |
| getDevice().reboot(); |
| assertWithMessage("Timed out waiting for device to boot").that( |
| getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue(); |
| final Set<ITestDevice.ApexInfo> activeApexes = getDevice().getActiveApexes(); |
| ITestDevice.ApexInfo testApex = new ITestDevice.ApexInfo( |
| "com.android.apex.test_package", 2L); |
| assertThat(activeApexes).doesNotContain(testApex); |
| mHostUtils.waitForFileDeleted("/data/apex/active/apexd_test_v2.apex", |
| Duration.ofMinutes(3)); |
| } finally { |
| getDevice().executeShellV2Command("rm /data/apex/active/apexd_test_v2.apex"); |
| } |
| } |
| @Test |
| public void testApexWithoutPbIsNotActivated() throws Exception { |
| assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); |
| assumeTrue("Device requires root", getDevice().isAdbRoot()); |
| final String testApexFile = "com.android.apex.cts.shim.v2_no_pb.apex"; |
| try { |
| assertThat(getDevice().pushFile(mHostUtils.getTestFile(testApexFile), |
| "/data/apex/active/" + testApexFile)).isTrue(); |
| getDevice().reboot(); |
| assertWithMessage("Timed out waiting for device to boot").that( |
| getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue(); |
| final Set<ITestDevice.ApexInfo> activeApexes = getDevice().getActiveApexes(); |
| ITestDevice.ApexInfo testApex = new ITestDevice.ApexInfo( |
| "com.android.apex.cts.shim", 2L); |
| assertThat(activeApexes).doesNotContain(testApex); |
| mHostUtils.waitForFileDeleted("/data/apex/active/" + testApexFile, |
| Duration.ofMinutes(3)); |
| } finally { |
| getDevice().executeShellV2Command("rm /data/apex/active/" + testApexFile); |
| } |
| } |
| |
| @Test |
| public void testRemountApex() throws Exception { |
| assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); |
| assumeTrue("Device requires root", getDevice().isAdbRoot()); |
| final File oldFile = getDevice().pullFile(SHIM_APEX_PATH); |
| try { |
| getDevice().remountSystemWritable(); |
| // In case remount requires a reboot, wait for boot to complete. |
| getDevice().waitForBootComplete(Duration.ofMinutes(3).toMillis()); |
| final File newFile = mHostUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); |
| // Stop framework |
| getDevice().executeShellV2Command("stop"); |
| // Push new shim APEX. This simulates adb sync. |
| getDevice().pushFile(newFile, SHIM_APEX_PATH); |
| // Ask apexd to remount packages |
| getDevice().executeShellV2Command("cmd -w apexservice remountPackages"); |
| // Start framework |
| getDevice().executeShellV2Command("start"); |
| // Give enough time for system_server to boot. |
| Thread.sleep(Duration.ofSeconds(15).toMillis()); |
| final Set<ITestDevice.ApexInfo> activeApexes = getDevice().getActiveApexes(); |
| ITestDevice.ApexInfo testApex = new ITestDevice.ApexInfo( |
| "com.android.apex.cts.shim", 2L); |
| assertThat(activeApexes).contains(testApex); |
| } finally { |
| getDevice().pushFile(oldFile, SHIM_APEX_PATH); |
| getDevice().reboot(); |
| } |
| } |
| |
| @Test |
| public void testApexWithoutPbIsNotActivated_ProductPartitionHasOlderVersion() |
| throws Exception { |
| assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); |
| assumeTrue("Device requires root", getDevice().isAdbRoot()); |
| |
| try { |
| getDevice().remountSystemWritable(); |
| // In case remount requires a reboot, wait for boot to complete. |
| assertWithMessage("Timed out waiting for device to boot").that( |
| getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue(); |
| |
| final File v1 = mHostUtils.getTestFile("apex.apexd_test.apex"); |
| getDevice().pushFile(v1, "/product/apex/apex.apexd_test.apex"); |
| |
| final File v2_no_pb = mHostUtils.getTestFile("apex.apexd_test_v2_no_pb.apex"); |
| getDevice().pushFile(v2_no_pb, "/data/apex/active/apex.apexd_test_v2_no_pb.apex"); |
| |
| getDevice().reboot(); |
| assertWithMessage("Timed out waiting for device to boot").that( |
| getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue(); |
| |
| final Set<ITestDevice.ApexInfo> activeApexes = getDevice().getActiveApexes(); |
| assertThat(activeApexes).contains(new ITestDevice.ApexInfo( |
| "com.android.apex.test_package", 1L)); |
| assertThat(activeApexes).doesNotContain(new ITestDevice.ApexInfo( |
| "com.android.apex.test_package", 2L)); |
| |
| // v2_no_pb should be deleted |
| mHostUtils.waitForFileDeleted("/data/apex/active/apex.apexd_test_v2_no_pb.apex", |
| Duration.ofMinutes(3)); |
| } finally { |
| getDevice().remountSystemWritable(); |
| assertWithMessage("Timed out waiting for device to boot").that( |
| getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue(); |
| |
| getDevice().executeShellV2Command("rm /product/apex/apex.apexd_test.apex"); |
| getDevice().executeShellV2Command("rm /data/apex/active/apex.apexd_test_v2_no_pb.apex"); |
| } |
| } |
| |
| @Test |
| public void testApexWithoutPbIsNotActivated_ProductPartitionHasNewerVersion() |
| throws Exception { |
| assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); |
| assumeTrue("Device requires root", getDevice().isAdbRoot()); |
| |
| try { |
| getDevice().remountSystemWritable(); |
| // In case remount requires a reboot, wait for boot to complete. |
| assertWithMessage("Timed out waiting for device to boot").that( |
| getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue(); |
| |
| final File v3 = mHostUtils.getTestFile("apex.apexd_test_v3.apex"); |
| getDevice().pushFile(v3, "/product/apex/apex.apexd_test_v3.apex"); |
| |
| final File v2_no_pb = mHostUtils.getTestFile("apex.apexd_test_v2_no_pb.apex"); |
| getDevice().pushFile(v2_no_pb, "/data/apex/active/apex.apexd_test_v2_no_pb.apex"); |
| |
| getDevice().reboot(); |
| assertWithMessage("Timed out waiting for device to boot").that( |
| getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue(); |
| |
| final Set<ITestDevice.ApexInfo> activeApexes = getDevice().getActiveApexes(); |
| assertThat(activeApexes).contains(new ITestDevice.ApexInfo( |
| "com.android.apex.test_package", 3L)); |
| assertThat(activeApexes).doesNotContain(new ITestDevice.ApexInfo( |
| "com.android.apex.test_package", 2L)); |
| |
| // v2_no_pb should be deleted |
| mHostUtils.waitForFileDeleted("/data/apex/active/apex.apexd_test_v2_no_pb.apex", |
| Duration.ofMinutes(3)); |
| } finally { |
| getDevice().remountSystemWritable(); |
| assertWithMessage("Timed out waiting for device to boot").that( |
| getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue(); |
| |
| getDevice().executeShellV2Command("rm /product/apex/apex.apexd_test_v3.apex"); |
| getDevice().executeShellV2Command("rm /data/apex/active/apex.apexd_test_v2_no_pb.apex"); |
| } |
| } |
| |
| @Test |
| public void testApexInfoListIsValid() throws Exception { |
| assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); |
| assumeTrue("Device requires root", getDevice().isAdbRoot()); |
| |
| try (FileInputStream fis = new FileInputStream( |
| getDevice().pullFile("/apex/apex-info-list.xml"))) { |
| // #1 Data got from apexd via binder |
| Set<ITestDevice.ApexInfo> fromApexd = getDevice().getActiveApexes(); |
| // #2 Data got from the xml file (key is the path) |
| Map<String, ApexInfo> fromXml = XmlParser.readApexInfoList(fis).getApexInfo().stream() |
| .collect(Collectors.toMap(ApexInfo::getModulePath, ai -> ai)); |
| |
| // Make sure that all items in #1 are also in #2 and they are identical |
| for (ITestDevice.ApexInfo ai : fromApexd) { |
| ApexInfo apexFromXml = fromXml.get(ai.sourceDir); |
| assertWithMessage("APEX (" + ai.toString() + ") is not found in the list") |
| .that(apexFromXml).isNotNull(); |
| assertWithMessage("Version mismatch for APEX (" + ai.toString() + ")") |
| .that(ai.versionCode).isEqualTo(apexFromXml.getVersionCode()); |
| assertWithMessage("APEX (" + ai.toString() + ") is not active") |
| .that(apexFromXml.getIsActive()).isTrue(); |
| } |
| } |
| } |
| |
| /** |
| * Test to verify that the state of a staged session does not change if apexd is stopped and |
| * restarted while a session is staged. |
| */ |
| @Test |
| public void testApexSessionStateUnchangedBeforeReboot() throws Exception { |
| assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); |
| assumeTrue("Device requires root", getDevice().isAdbRoot()); |
| |
| File apexFile = mHostUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); |
| String error = mHostUtils.installStagedPackage(apexFile); |
| assertThat(error).isNull(); |
| String sessionId = getDevice().executeShellCommand( |
| "pm get-stagedsessions --only-ready --only-parent --only-sessionid").trim(); |
| assertThat(sessionId).isNotEmpty(); |
| String sessionStateCmd = "cmd -w apexservice getStagedSessionInfo " + sessionId; |
| String initialState = getDevice().executeShellV2Command(sessionStateCmd).getStdout(); |
| assertThat(initialState).isNotEmpty(); |
| |
| // Kill apexd. This means apexd will perform its start logic when the second install |
| // is staged. |
| getDevice().executeShellV2Command("kill `pidof apexd`"); |
| |
| // Verify that the session state remains consistent after apexd has restarted. |
| String updatedState = getDevice().executeShellV2Command(sessionStateCmd).getStdout(); |
| assertThat(updatedState).isEqualTo(initialState); |
| } |
| |
| /** |
| * Verifies that content of {@code /data/apex/sessions/} is migrated to the {@code |
| * /metadata/apex/sessions}. |
| */ |
| @Test |
| public void testSessionsDirMigrationToMetadata() throws Exception { |
| assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); |
| assumeTrue("Device requires root", getDevice().isAdbRoot()); |
| |
| try { |
| getDevice().executeShellV2Command("mkdir -p /data/apex/sessions/1543"); |
| File file = File.createTempFile("foo", "bar"); |
| getDevice().pushFile(file, "/data/apex/sessions/1543/file"); |
| |
| // During boot sequence apexd will move /data/apex/sessions/1543/file to |
| // /metadata/apex/sessions/1543/file. |
| getDevice().reboot(); |
| assertWithMessage("Timed out waiting for device to boot").that( |
| getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue(); |
| |
| assertThat(getDevice().doesFileExist("/metadata/apex/sessions/1543/file")).isTrue(); |
| assertThat(getDevice().doesFileExist("/data/apex/sessions/1543/file")).isFalse(); |
| } finally { |
| getDevice().executeShellV2Command("rm -R /data/apex/sessions/1543"); |
| getDevice().executeShellV2Command("rm -R /metadata/apex/sessions/1543"); |
| } |
| } |
| } |