1 /* 2 * Copyright (C) 2023 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.odsign; 18 19 import static com.google.common.truth.Truth.assertThat; 20 21 import com.android.tradefed.invoker.TestInformation; 22 23 import org.w3c.dom.Document; 24 import org.w3c.dom.Element; 25 import org.w3c.dom.Node; 26 import org.w3c.dom.NodeList; 27 28 import java.io.File; 29 import java.util.HashMap; 30 import java.util.HashSet; 31 import java.util.Map; 32 import java.util.Set; 33 import java.util.UUID; 34 import javax.xml.parsers.DocumentBuilder; 35 import javax.xml.parsers.DocumentBuilderFactory; 36 import javax.xml.transform.Transformer; 37 import javax.xml.transform.TransformerFactory; 38 import javax.xml.transform.dom.DOMSource; 39 import javax.xml.transform.stream.StreamResult; 40 41 /** A helper class that can mutate the device state and restore it afterwards. */ 42 public class DeviceState { 43 private static final String TEST_JAR_RESOURCE_NAME = "/art-gtest-jars-Main.jar"; 44 private static final String PHENOTYPE_FLAG_NAMESPACE = "runtime_native_boot"; 45 private static final String ART_APEX_DALVIK_CACHE_BACKUP_DIRNAME = 46 OdsignTestUtils.ART_APEX_DALVIK_CACHE_DIRNAME + ".bak"; 47 48 private final TestInformation mTestInfo; 49 private final OdsignTestUtils mTestUtils; 50 51 private Set<String> mTempFiles = new HashSet<>(); 52 private Set<String> mMountPoints = new HashSet<>(); 53 private Map<String, String> mMutatedProperties = new HashMap<>(); 54 private Set<String> mMutatedPhenotypeFlags = new HashSet<>(); 55 private Map<String, String> mDeletedFiles = new HashMap<>(); 56 private boolean mHasArtifactsBackup = false; 57 DeviceState(TestInformation testInfo)58 public DeviceState(TestInformation testInfo) throws Exception { 59 mTestInfo = testInfo; 60 mTestUtils = new OdsignTestUtils(testInfo); 61 } 62 63 /** Restores the device state. */ restore()64 public void restore() throws Exception { 65 for (String mountPoint : mMountPoints) { 66 mTestInfo.getDevice().executeShellV2Command(String.format("umount '%s'", mountPoint)); 67 } 68 69 for (String tempFile : mTempFiles) { 70 mTestInfo.getDevice().deleteFile(tempFile); 71 } 72 73 for (var entry : mMutatedProperties.entrySet()) { 74 mTestInfo.getDevice().setProperty( 75 entry.getKey(), entry.getValue() != null ? entry.getValue() : ""); 76 } 77 78 for (String flag : mMutatedPhenotypeFlags) { 79 mTestInfo.getDevice().executeShellV2Command(String.format( 80 "device_config delete '%s' '%s'", PHENOTYPE_FLAG_NAMESPACE, flag)); 81 } 82 83 if (!mMutatedPhenotypeFlags.isEmpty()) { 84 mTestInfo.getDevice().executeShellV2Command( 85 "device_config set_sync_disabled_for_tests none"); 86 } 87 88 for (var entry : mDeletedFiles.entrySet()) { 89 mTestInfo.getDevice().executeShellV2Command( 90 String.format("cp '%s' '%s'", entry.getValue(), entry.getKey())); 91 mTestInfo.getDevice().executeShellV2Command(String.format("rm '%s'", entry.getValue())); 92 mTestInfo.getDevice().executeShellV2Command( 93 String.format("restorecon '%s'", entry.getKey())); 94 } 95 96 if (mHasArtifactsBackup) { 97 mTestInfo.getDevice().executeShellV2Command( 98 String.format("rm -rf '%s'", OdsignTestUtils.ART_APEX_DALVIK_CACHE_DIRNAME)); 99 mTestInfo.getDevice().executeShellV2Command( 100 String.format("mv '%s' '%s'", ART_APEX_DALVIK_CACHE_BACKUP_DIRNAME, 101 OdsignTestUtils.ART_APEX_DALVIK_CACHE_DIRNAME)); 102 } 103 } 104 105 /** Simulates that the ART APEX has been upgraded. */ simulateArtApexUpgrade()106 public void simulateArtApexUpgrade() throws Exception { 107 updateApexInfo("com.android.art", false /* isFactory */); 108 } 109 110 /** 111 * Simulates that the new ART APEX has been uninstalled (i.e., the ART module goes back to the 112 * factory version). 113 */ simulateArtApexUninstall()114 public void simulateArtApexUninstall() throws Exception { 115 updateApexInfo("com.android.art", true /* isFactory */); 116 } 117 118 /** 119 * Simulates that an APEX has been upgraded. We could install a real APEX, but that would 120 * introduce an extra dependency to this test, which we want to avoid. 121 */ simulateApexUpgrade()122 public void simulateApexUpgrade() throws Exception { 123 updateApexInfo("com.android.wifi", false /* isFactory */); 124 } 125 126 /** 127 * Simulates that the new APEX has been uninstalled (i.e., the module goes back to the factory 128 * version). 129 */ simulateApexUninstall()130 public void simulateApexUninstall() throws Exception { 131 updateApexInfo("com.android.wifi", true /* isFactory */); 132 } 133 updateApexInfo(String moduleName, boolean isFactory)134 private void updateApexInfo(String moduleName, boolean isFactory) throws Exception { 135 try (var xmlMutator = new XmlMutator(OdsignTestUtils.APEX_INFO_FILE)) { 136 NodeList list = xmlMutator.getDocument().getElementsByTagName("apex-info"); 137 for (int i = 0; i < list.getLength(); i++) { 138 Element node = (Element) list.item(i); 139 if (node.getAttribute("moduleName").equals(moduleName) 140 && node.getAttribute("isActive").equals("true")) { 141 node.setAttribute("isFactory", String.valueOf(isFactory)); 142 node.setAttribute( 143 "lastUpdateMillis", String.valueOf(System.currentTimeMillis())); 144 } 145 } 146 } 147 } 148 149 /** Simulates that there is an OTA that updates a boot classpath jar. */ simulateBootClasspathOta()150 public void simulateBootClasspathOta() throws Exception { 151 File localFile = mTestUtils.copyResourceToFile(TEST_JAR_RESOURCE_NAME); 152 pushAndBindMount(localFile, "/system/framework/framework.jar"); 153 } 154 155 /** Simulates that there is an OTA that updates a system server jar. */ simulateSystemServerOta()156 public void simulateSystemServerOta() throws Exception { 157 File localFile = mTestUtils.copyResourceToFile(TEST_JAR_RESOURCE_NAME); 158 pushAndBindMount(localFile, "/system/framework/services.jar"); 159 } 160 makeDex2oatFail()161 public void makeDex2oatFail() throws Exception { 162 setProperty("dalvik.vm.boot-dex2oat-threads", "-1"); 163 } 164 165 /** Sets a system property. */ setProperty(String key, String value)166 public void setProperty(String key, String value) throws Exception { 167 if (!mMutatedProperties.containsKey(key)) { 168 // Backup the original value. 169 mMutatedProperties.put(key, mTestInfo.getDevice().getProperty(key)); 170 } 171 172 mTestInfo.getDevice().setProperty(key, value); 173 } 174 175 /** Sets a phenotype flag. */ setPhenotypeFlag(String key, String value)176 public void setPhenotypeFlag(String key, String value) throws Exception { 177 if (!mMutatedPhenotypeFlags.contains(key)) { 178 // Tests assume that phenotype flags are initially not set. Check if the assumption is 179 // true. 180 assertThat(mTestUtils.assertCommandSucceeds(String.format( 181 "device_config get '%s' '%s'", PHENOTYPE_FLAG_NAMESPACE, key))) 182 .isEqualTo("null"); 183 mMutatedPhenotypeFlags.add(key); 184 } 185 186 // Disable phenotype flag syncing. Potentially, we can set `set_sync_disabled_for_tests` to 187 // `until_reboot`, but setting it to `persistent` prevents unrelated system crashes/restarts 188 // from affecting the test. `set_sync_disabled_for_tests` is reset in `restore` anyway. 189 mTestUtils.assertCommandSucceeds("device_config set_sync_disabled_for_tests persistent"); 190 191 if (value != null) { 192 mTestUtils.assertCommandSucceeds(String.format( 193 "device_config put '%s' '%s' '%s'", PHENOTYPE_FLAG_NAMESPACE, key, value)); 194 } else { 195 mTestUtils.assertCommandSucceeds( 196 String.format("device_config delete '%s' '%s'", PHENOTYPE_FLAG_NAMESPACE, key)); 197 } 198 } 199 backupAndDeleteFile(String remotePath)200 public void backupAndDeleteFile(String remotePath) throws Exception { 201 String tempFile = "/data/local/tmp/odsign_e2e_tests_" + UUID.randomUUID() + ".tmp"; 202 // Backup the file before deleting it. 203 mTestUtils.assertCommandSucceeds(String.format("cp '%s' '%s'", remotePath, tempFile)); 204 mTestUtils.assertCommandSucceeds(String.format("rm '%s'", remotePath)); 205 mDeletedFiles.put(remotePath, tempFile); 206 } 207 backupArtifacts()208 public void backupArtifacts() throws Exception { 209 mTestInfo.getDevice().executeShellV2Command( 210 String.format("rm -rf '%s'", ART_APEX_DALVIK_CACHE_BACKUP_DIRNAME)); 211 mTestUtils.assertCommandSucceeds( 212 String.format("cp -r '%s' '%s'", OdsignTestUtils.ART_APEX_DALVIK_CACHE_DIRNAME, 213 ART_APEX_DALVIK_CACHE_BACKUP_DIRNAME)); 214 mHasArtifactsBackup = true; 215 } 216 217 /** 218 * Pushes the file to a temporary location and bind-mount it at the given path. This is useful 219 * when the path is readonly. 220 */ pushAndBindMount(File localFile, String remotePath)221 private void pushAndBindMount(File localFile, String remotePath) throws Exception { 222 String tempFile = "/data/local/tmp/odsign_e2e_tests_" + UUID.randomUUID() + ".tmp"; 223 assertThat(mTestInfo.getDevice().pushFile(localFile, tempFile)).isTrue(); 224 mTempFiles.add(tempFile); 225 226 // If the path has already been bind-mounted by this method before, unmount it first. 227 if (mMountPoints.contains(remotePath)) { 228 mTestUtils.assertCommandSucceeds(String.format("umount '%s'", remotePath)); 229 mMountPoints.remove(remotePath); 230 } 231 232 mTestUtils.assertCommandSucceeds( 233 String.format("mount --bind '%s' '%s'", tempFile, remotePath)); 234 mMountPoints.add(remotePath); 235 mTestUtils.assertCommandSucceeds(String.format("restorecon '%s'", remotePath)); 236 } 237 238 /** A helper class for mutating an XML file. */ 239 private class XmlMutator implements AutoCloseable { 240 private final Document mDocument; 241 private final String mRemoteXmlFile; 242 private final File mLocalFile; 243 XmlMutator(String remoteXmlFile)244 public XmlMutator(String remoteXmlFile) throws Exception { 245 // Load the XML file. 246 mRemoteXmlFile = remoteXmlFile; 247 mLocalFile = mTestInfo.getDevice().pullFile(remoteXmlFile); 248 assertThat(mLocalFile).isNotNull(); 249 DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); 250 mDocument = builder.parse(mLocalFile); 251 } 252 253 @Override close()254 public void close() throws Exception { 255 // Save the XML file. 256 Transformer transformer = TransformerFactory.newInstance().newTransformer(); 257 transformer.transform(new DOMSource(mDocument), new StreamResult(mLocalFile)); 258 pushAndBindMount(mLocalFile, mRemoteXmlFile); 259 } 260 261 /** Returns a mutable XML document. */ getDocument()262 public Document getDocument() { 263 return mDocument; 264 } 265 } 266 } 267