1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tests.dsu; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.assertNotNull; 21 import static org.junit.Assert.assertTrue; 22 23 import com.android.tradefed.config.Option; 24 import com.android.tradefed.device.DeviceNotAvailableException; 25 import com.android.tradefed.log.LogUtil.CLog; 26 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; 27 import com.android.tradefed.util.FileUtil; 28 import com.android.tradefed.util.StreamUtil; 29 30 import org.junit.After; 31 import org.junit.Before; 32 import org.junit.Test; 33 import org.junit.runner.RunWith; 34 35 import java.io.BufferedInputStream; 36 import java.io.BufferedOutputStream; 37 import java.io.File; 38 import java.io.FileInputStream; 39 import java.io.FileOutputStream; 40 import java.io.IOException; 41 import java.io.InputStream; 42 import java.util.zip.ZipEntry; 43 import java.util.zip.ZipOutputStream; 44 45 @RunWith(DeviceJUnit4ClassRunner.class) 46 public class DsuGsiIntegrationTest extends DsuTestBase { 47 private static final long DSU_MAX_WAIT_SEC = 10 * 60; 48 private static final long DSU_DEFAULT_USERDATA_SIZE = 8L << 30; 49 50 private static final String GSI_IMAGE_NAME = "system.img"; 51 private static final String DSU_IMAGE_ZIP_PUSH_PATH = "/sdcard/gsi.zip"; 52 53 private static final String REMOUNT_TEST_PATH = "/system/remount_test"; 54 private static final String REMOUNT_TEST_FILE = REMOUNT_TEST_PATH + "/test_file"; 55 56 @Option( 57 name = "wipe-dsu-on-failure", 58 description = "Wipe the DSU installation on test failure.") 59 private boolean mWipeDsuOnFailure = true; 60 61 @Option( 62 name = "system-image-path", 63 description = "Path to the GSI system.img or directory containing the system.img.", 64 mandatory = true) 65 private File mSystemImagePath; 66 67 private File mSystemImageZip; 68 getDsuInstallCommand()69 private String getDsuInstallCommand() { 70 return String.format( 71 "am start-activity" 72 + " -n com.android.dynsystem/com.android.dynsystem.VerificationActivity" 73 + " -a android.os.image.action.START_INSTALL" 74 + " -d file://%s" 75 + " --el KEY_USERDATA_SIZE %d" 76 + " --ez KEY_ENABLE_WHEN_COMPLETED true", 77 DSU_IMAGE_ZIP_PUSH_PATH, getDsuUserdataSize(DSU_DEFAULT_USERDATA_SIZE)); 78 } 79 80 @Before setUp()81 public void setUp() throws IOException { 82 mSystemImageZip = null; 83 InputStream stream = null; 84 try { 85 assertNotNull("--system-image-path is invalid", mSystemImagePath); 86 if (mSystemImagePath.isDirectory()) { 87 File gsiImageFile = FileUtil.findFile(mSystemImagePath, GSI_IMAGE_NAME); 88 assertNotNull("Cannot find " + GSI_IMAGE_NAME, gsiImageFile); 89 stream = new FileInputStream(gsiImageFile); 90 } else { 91 stream = new FileInputStream(mSystemImagePath); 92 } 93 stream = new BufferedInputStream(stream); 94 mSystemImageZip = FileUtil.createTempFile(this.getClass().getSimpleName(), "gsi.zip"); 95 try (FileOutputStream foStream = new FileOutputStream(mSystemImageZip); 96 BufferedOutputStream boStream = new BufferedOutputStream(foStream); 97 ZipOutputStream out = new ZipOutputStream(boStream); ) { 98 // Don't bother compressing it as we are going to uncompress it on device anyway. 99 out.setLevel(0); 100 out.putNextEntry(new ZipEntry(GSI_IMAGE_NAME)); 101 StreamUtil.copyStreams(stream, out); 102 out.closeEntry(); 103 } 104 } finally { 105 StreamUtil.close(stream); 106 } 107 } 108 109 @After tearDown()110 public void tearDown() { 111 try { 112 FileUtil.deleteFile(mSystemImageZip); 113 } catch (SecurityException e) { 114 CLog.w("Failed to clean up '%s': %s", mSystemImageZip, e); 115 } 116 if (mWipeDsuOnFailure) { 117 // If test case completed successfully, then the test body should have called `wipe` 118 // already and calling `wipe` again would be a noop. 119 // If test case failed, then this piece of code would clean up the DSU installation left 120 // by the failed test case. 121 try { 122 getDevice().executeShellV2Command("gsi_tool wipe"); 123 if (isDsuRunning()) { 124 getDevice().reboot(); 125 getDevice().disableAdbRoot(); 126 } 127 } catch (DeviceNotAvailableException e) { 128 CLog.w("Failed to clean up DSU installation on device: %s", e); 129 } 130 } 131 try { 132 getDevice().deleteFile(DSU_IMAGE_ZIP_PUSH_PATH); 133 } catch (DeviceNotAvailableException e) { 134 CLog.w("Failed to clean up device '%s': %s", DSU_IMAGE_ZIP_PUSH_PATH, e); 135 } 136 } 137 138 @Test testDsuGsi()139 public void testDsuGsi() throws DeviceNotAvailableException { 140 if (isDsuRunning()) { 141 CLog.i("Wipe existing DSU installation"); 142 assertShellCommand("gsi_tool wipe"); 143 getDevice().reboot(); 144 getDevice().disableAdbRoot(); 145 assertDsuNotRunning(); 146 } 147 148 CLog.i("Pushing '%s' -> '%s'", mSystemImageZip, DSU_IMAGE_ZIP_PUSH_PATH); 149 getDevice().pushFile(mSystemImageZip, DSU_IMAGE_ZIP_PUSH_PATH, true); 150 151 final long freeSpaceBeforeInstall = getDevice().getPartitionFreeSpace("/data") << 10; 152 assertShellCommand(getDsuInstallCommand()); 153 CLog.i("Wait for DSU installation complete and reboot"); 154 assertTrue( 155 "Timed out waiting for DSU installation complete", 156 getDevice().waitForDeviceNotAvailable(DSU_MAX_WAIT_SEC * 1000)); 157 CLog.i("DSU installation is complete and device is disconnected"); 158 159 getDevice().waitForDeviceAvailable(); 160 assertDsuRunning(); 161 CLog.i("Successfully booted with DSU"); 162 163 CLog.i("Test 'gsi_tool enable -s' and 'gsi_tool enable'"); 164 getDevice().reboot(); 165 getDevice().disableAdbRoot(); 166 assertDsuNotRunning(); 167 168 final long freeSpaceAfterInstall = getDevice().getPartitionFreeSpace("/data") << 10; 169 final long estimatedDsuSize = freeSpaceBeforeInstall - freeSpaceAfterInstall; 170 assertTrue( 171 String.format( 172 "Expected DSU installation to consume some storage space, free space before" 173 + " install: %d, free space after install: %d, delta: %d", 174 freeSpaceBeforeInstall, freeSpaceAfterInstall, estimatedDsuSize), 175 estimatedDsuSize > 0); 176 177 assertShellCommand("gsi_tool enable"); 178 getDevice().reboot(); 179 getDevice().disableAdbRoot(); 180 assertDsuRunning(); 181 182 CLog.i("Set up 'adb remount' for testing (requires reboot)"); 183 assertAdbRoot(); 184 assertShellCommand("remount"); 185 getDevice().reboot(); 186 getDevice().disableAdbRoot(); 187 assertDsuRunning(); 188 assertAdbRoot(); 189 assertShellCommand("remount"); 190 assertDevicePathNotExist(REMOUNT_TEST_PATH); 191 assertShellCommand(String.format("mkdir -p '%s'", REMOUNT_TEST_PATH)); 192 assertShellCommand(String.format("cp /system/bin/init '%s'", REMOUNT_TEST_FILE)); 193 final String initContent = getDevice().pullFileContents("/system/bin/init"); 194 195 CLog.i("DSU and original system have separate remount overlays"); 196 assertShellCommand("gsi_tool disable"); 197 getDevice().reboot(); 198 getDevice().disableAdbRoot(); 199 assertDsuNotRunning(); 200 assertDevicePathNotExist(REMOUNT_TEST_PATH); 201 202 CLog.i("Test that 'adb remount' is consistent after reboot"); 203 assertShellCommand("gsi_tool enable"); 204 getDevice().reboot(); 205 getDevice().disableAdbRoot(); 206 assertDsuRunning(); 207 assertDevicePathExist(REMOUNT_TEST_FILE); 208 assertEquals( 209 String.format( 210 "Expected contents of '%s' to persist after reboot", REMOUNT_TEST_FILE), 211 initContent, 212 getDevice().pullFileContents(REMOUNT_TEST_FILE)); 213 214 CLog.i("'enable-verity' should teardown the remount overlay and restore the filesystem"); 215 assertAdbRoot(); 216 assertShellCommand("enable-verity"); 217 getDevice().reboot(); 218 getDevice().disableAdbRoot(); 219 assertDsuRunning(); 220 assertDevicePathNotExist(REMOUNT_TEST_PATH); 221 222 CLog.i("Test 'gsi_tool wipe'"); 223 assertShellCommand("gsi_tool wipe"); 224 getDevice().reboot(); 225 getDevice().disableAdbRoot(); 226 assertDsuNotRunning(); 227 228 final double dampeningCoefficient = 0.9; 229 final long freeSpaceAfterWipe = getDevice().getPartitionFreeSpace("/data") << 10; 230 final long freeSpaceReturnedByWipe = freeSpaceAfterWipe - freeSpaceAfterInstall; 231 assertTrue( 232 String.format( 233 "Expected 'gsi_tool wipe' to return roughly %d of storage space, free space" 234 + " before wipe: %d, free space after wipe: %d, delta: %d", 235 estimatedDsuSize, 236 freeSpaceAfterInstall, 237 freeSpaceAfterWipe, 238 freeSpaceReturnedByWipe), 239 freeSpaceReturnedByWipe > (long) (estimatedDsuSize * dampeningCoefficient)); 240 } 241 } 242