/* * Copyright (C) 2022 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.dsu; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import com.android.tradefed.config.Option; import com.android.tradefed.device.DeviceNotAvailableException; import com.android.tradefed.log.LogUtil.CLog; import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; import com.android.tradefed.util.FileUtil; import com.android.tradefed.util.StreamUtil; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @RunWith(DeviceJUnit4ClassRunner.class) public class DsuGsiIntegrationTest extends DsuTestBase { private static final long DSU_MAX_WAIT_SEC = 10 * 60; private static final long DSU_DEFAULT_USERDATA_SIZE = 8L << 30; private static final String GSI_IMAGE_NAME = "system.img"; private static final String DSU_IMAGE_ZIP_PUSH_PATH = "/sdcard/gsi.zip"; private static final String REMOUNT_TEST_PATH = "/system/remount_test"; private static final String REMOUNT_TEST_FILE = REMOUNT_TEST_PATH + "/test_file"; @Option( name = "wipe-dsu-on-failure", description = "Wipe the DSU installation on test failure.") private boolean mWipeDsuOnFailure = true; @Option( name = "system-image-path", description = "Path to the GSI system.img or directory containing the system.img.", mandatory = true) private File mSystemImagePath; private File mSystemImageZip; private String getDsuInstallCommand() { return String.format( "am start-activity" + " -n com.android.dynsystem/com.android.dynsystem.VerificationActivity" + " -a android.os.image.action.START_INSTALL" + " -d file://%s" + " --el KEY_USERDATA_SIZE %d" + " --ez KEY_ENABLE_WHEN_COMPLETED true", DSU_IMAGE_ZIP_PUSH_PATH, getDsuUserdataSize(DSU_DEFAULT_USERDATA_SIZE)); } @Before public void setUp() throws IOException { mSystemImageZip = null; InputStream stream = null; try { assertNotNull("--system-image-path is invalid", mSystemImagePath); if (mSystemImagePath.isDirectory()) { File gsiImageFile = FileUtil.findFile(mSystemImagePath, GSI_IMAGE_NAME); assertNotNull("Cannot find " + GSI_IMAGE_NAME, gsiImageFile); stream = new FileInputStream(gsiImageFile); } else { stream = new FileInputStream(mSystemImagePath); } stream = new BufferedInputStream(stream); mSystemImageZip = FileUtil.createTempFile(this.getClass().getSimpleName(), "gsi.zip"); try (FileOutputStream foStream = new FileOutputStream(mSystemImageZip); BufferedOutputStream boStream = new BufferedOutputStream(foStream); ZipOutputStream out = new ZipOutputStream(boStream); ) { // Don't bother compressing it as we are going to uncompress it on device anyway. out.setLevel(0); out.putNextEntry(new ZipEntry(GSI_IMAGE_NAME)); StreamUtil.copyStreams(stream, out); out.closeEntry(); } } finally { StreamUtil.close(stream); } } @After public void tearDown() { try { FileUtil.deleteFile(mSystemImageZip); } catch (SecurityException e) { CLog.w("Failed to clean up '%s': %s", mSystemImageZip, e); } if (mWipeDsuOnFailure) { // If test case completed successfully, then the test body should have called `wipe` // already and calling `wipe` again would be a noop. // If test case failed, then this piece of code would clean up the DSU installation left // by the failed test case. try { getDevice().executeShellV2Command("gsi_tool wipe"); if (isDsuRunning()) { getDevice().reboot(); getDevice().disableAdbRoot(); } } catch (DeviceNotAvailableException e) { CLog.w("Failed to clean up DSU installation on device: %s", e); } } try { getDevice().deleteFile(DSU_IMAGE_ZIP_PUSH_PATH); } catch (DeviceNotAvailableException e) { CLog.w("Failed to clean up device '%s': %s", DSU_IMAGE_ZIP_PUSH_PATH, e); } } @Test public void testDsuGsi() throws DeviceNotAvailableException { if (isDsuRunning()) { CLog.i("Wipe existing DSU installation"); assertShellCommand("gsi_tool wipe"); getDevice().reboot(); getDevice().disableAdbRoot(); assertDsuNotRunning(); } CLog.i("Pushing '%s' -> '%s'", mSystemImageZip, DSU_IMAGE_ZIP_PUSH_PATH); getDevice().pushFile(mSystemImageZip, DSU_IMAGE_ZIP_PUSH_PATH, true); final long freeSpaceBeforeInstall = getDevice().getPartitionFreeSpace("/data") << 10; assertShellCommand(getDsuInstallCommand()); CLog.i("Wait for DSU installation complete and reboot"); assertTrue( "Timed out waiting for DSU installation complete", getDevice().waitForDeviceNotAvailable(DSU_MAX_WAIT_SEC * 1000)); CLog.i("DSU installation is complete and device is disconnected"); getDevice().waitForDeviceAvailable(); assertDsuRunning(); CLog.i("Successfully booted with DSU"); CLog.i("Test 'gsi_tool enable -s' and 'gsi_tool enable'"); getDevice().reboot(); getDevice().disableAdbRoot(); assertDsuNotRunning(); final long freeSpaceAfterInstall = getDevice().getPartitionFreeSpace("/data") << 10; final long estimatedDsuSize = freeSpaceBeforeInstall - freeSpaceAfterInstall; assertTrue( String.format( "Expected DSU installation to consume some storage space, free space before" + " install: %d, free space after install: %d, delta: %d", freeSpaceBeforeInstall, freeSpaceAfterInstall, estimatedDsuSize), estimatedDsuSize > 0); assertShellCommand("gsi_tool enable"); getDevice().reboot(); getDevice().disableAdbRoot(); assertDsuRunning(); CLog.i("Set up 'adb remount' for testing (requires reboot)"); assertAdbRoot(); assertShellCommand("remount"); getDevice().reboot(); getDevice().disableAdbRoot(); assertDsuRunning(); assertAdbRoot(); assertShellCommand("remount"); assertDevicePathNotExist(REMOUNT_TEST_PATH); assertShellCommand(String.format("mkdir -p '%s'", REMOUNT_TEST_PATH)); assertShellCommand(String.format("cp /system/bin/init '%s'", REMOUNT_TEST_FILE)); final String initContent = getDevice().pullFileContents("/system/bin/init"); CLog.i("DSU and original system have separate remount overlays"); assertShellCommand("gsi_tool disable"); getDevice().reboot(); getDevice().disableAdbRoot(); assertDsuNotRunning(); assertDevicePathNotExist(REMOUNT_TEST_PATH); CLog.i("Test that 'adb remount' is consistent after reboot"); assertShellCommand("gsi_tool enable"); getDevice().reboot(); getDevice().disableAdbRoot(); assertDsuRunning(); assertDevicePathExist(REMOUNT_TEST_FILE); assertEquals( String.format( "Expected contents of '%s' to persist after reboot", REMOUNT_TEST_FILE), initContent, getDevice().pullFileContents(REMOUNT_TEST_FILE)); CLog.i("'enable-verity' should teardown the remount overlay and restore the filesystem"); assertAdbRoot(); assertShellCommand("enable-verity"); getDevice().reboot(); getDevice().disableAdbRoot(); assertDsuRunning(); assertDevicePathNotExist(REMOUNT_TEST_PATH); CLog.i("Test 'gsi_tool wipe'"); assertShellCommand("gsi_tool wipe"); getDevice().reboot(); getDevice().disableAdbRoot(); assertDsuNotRunning(); final double dampeningCoefficient = 0.9; final long freeSpaceAfterWipe = getDevice().getPartitionFreeSpace("/data") << 10; final long freeSpaceReturnedByWipe = freeSpaceAfterWipe - freeSpaceAfterInstall; assertTrue( String.format( "Expected 'gsi_tool wipe' to return roughly %d of storage space, free space" + " before wipe: %d, free space after wipe: %d, delta: %d", estimatedDsuSize, freeSpaceAfterInstall, freeSpaceAfterWipe, freeSpaceReturnedByWipe), freeSpaceReturnedByWipe > (long) (estimatedDsuSize * dampeningCoefficient)); } }