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 Map<String, String> mMutatedPhenotypeFlags = new HashMap<>(); 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 (var entry : mMutatedPhenotypeFlags.entrySet()) { 79 if (entry.getValue() != null) { 80 mTestInfo.getDevice().executeShellV2Command( 81 String.format("device_config put '%s' '%s' '%s'", PHENOTYPE_FLAG_NAMESPACE, 82 entry.getKey(), entry.getValue())); 83 } else { 84 mTestInfo.getDevice().executeShellV2Command( 85 String.format("device_config delete '%s' '%s'", PHENOTYPE_FLAG_NAMESPACE, 86 entry.getKey())); 87 } 88 } 89 90 for (var entry : mDeletedFiles.entrySet()) { 91 mTestInfo.getDevice().executeShellV2Command( 92 String.format("cp '%s' '%s'", entry.getValue(), entry.getKey())); 93 mTestInfo.getDevice().executeShellV2Command(String.format("rm '%s'", entry.getValue())); 94 mTestInfo.getDevice().executeShellV2Command( 95 String.format("restorecon '%s'", entry.getKey())); 96 } 97 98 if (mHasArtifactsBackup) { 99 mTestInfo.getDevice().executeShellV2Command( 100 String.format("rm -rf '%s'", OdsignTestUtils.ART_APEX_DALVIK_CACHE_DIRNAME)); 101 mTestInfo.getDevice().executeShellV2Command( 102 String.format("mv '%s' '%s'", ART_APEX_DALVIK_CACHE_BACKUP_DIRNAME, 103 OdsignTestUtils.ART_APEX_DALVIK_CACHE_DIRNAME)); 104 } 105 } 106 107 /** Simulates that the ART APEX has been upgraded. */ simulateArtApexUpgrade()108 public void simulateArtApexUpgrade() throws Exception { 109 updateApexInfo("com.android.art", false /* isFactory */); 110 } 111 112 /** 113 * Simulates that the new ART APEX has been uninstalled (i.e., the ART module goes back to the 114 * factory version). 115 */ simulateArtApexUninstall()116 public void simulateArtApexUninstall() throws Exception { 117 updateApexInfo("com.android.art", true /* isFactory */); 118 } 119 120 /** 121 * Simulates that an APEX has been upgraded. We could install a real APEX, but that would 122 * introduce an extra dependency to this test, which we want to avoid. 123 */ simulateApexUpgrade()124 public void simulateApexUpgrade() throws Exception { 125 updateApexInfo("com.android.wifi", false /* isFactory */); 126 } 127 128 /** 129 * Simulates that the new APEX has been uninstalled (i.e., the module goes back to the factory 130 * version). 131 */ simulateApexUninstall()132 public void simulateApexUninstall() throws Exception { 133 updateApexInfo("com.android.wifi", true /* isFactory */); 134 } 135 updateApexInfo(String moduleName, boolean isFactory)136 private void updateApexInfo(String moduleName, boolean isFactory) throws Exception { 137 try (var xmlMutator = new XmlMutator(OdsignTestUtils.APEX_INFO_FILE)) { 138 NodeList list = xmlMutator.getDocument().getElementsByTagName("apex-info"); 139 for (int i = 0; i < list.getLength(); i++) { 140 Element node = (Element) list.item(i); 141 if (node.getAttribute("moduleName").equals(moduleName) 142 && node.getAttribute("isActive").equals("true")) { 143 node.setAttribute("isFactory", String.valueOf(isFactory)); 144 node.setAttribute( 145 "lastUpdateMillis", String.valueOf(System.currentTimeMillis())); 146 } 147 } 148 } 149 } 150 151 /** Simulates that there is an OTA that updates a boot classpath jar. */ simulateBootClasspathOta()152 public void simulateBootClasspathOta() throws Exception { 153 File localFile = mTestUtils.copyResourceToFile(TEST_JAR_RESOURCE_NAME); 154 pushAndBindMount(localFile, "/system/framework/framework-graphics.jar"); 155 } 156 157 /** Simulates that there is an OTA that updates a system server jar. */ simulateSystemServerOta()158 public void simulateSystemServerOta() throws Exception { 159 File localFile = mTestUtils.copyResourceToFile(TEST_JAR_RESOURCE_NAME); 160 pushAndBindMount(localFile, "/system/framework/services.jar"); 161 } 162 163 /** Simulates that a system server jar is bad. */ simulateBadSystemServerJar()164 public void simulateBadSystemServerJar() throws Exception { 165 File tempFile = File.createTempFile("empty", ".jar"); 166 tempFile.deleteOnExit(); 167 pushAndBindMount(tempFile, "/system/framework/services.jar"); 168 } 169 makeDex2oatFail()170 public void makeDex2oatFail() throws Exception { 171 setProperty("dalvik.vm.boot-dex2oat-threads", "-1"); 172 } 173 174 /** Sets a system property. */ setProperty(String key, String value)175 public void setProperty(String key, String value) throws Exception { 176 if (!mMutatedProperties.containsKey(key)) { 177 // Backup the original value. 178 mMutatedProperties.put(key, mTestInfo.getDevice().getProperty(key)); 179 } 180 181 mTestInfo.getDevice().setProperty(key, value); 182 } 183 184 /** Sets a phenotype flag. */ setPhenotypeFlag(String key, String value)185 public void setPhenotypeFlag(String key, String value) throws Exception { 186 if (!mMutatedPhenotypeFlags.containsKey(key)) { 187 String output = mTestUtils.assertCommandSucceeds( 188 String.format("device_config get '%s' '%s'", PHENOTYPE_FLAG_NAMESPACE, key)); 189 mMutatedPhenotypeFlags.put(key, output.equals("null") ? null : output); 190 } 191 192 if (value != null) { 193 mTestUtils.assertCommandSucceeds(String.format( 194 "device_config put '%s' '%s' '%s'", PHENOTYPE_FLAG_NAMESPACE, key, value)); 195 } else { 196 mTestUtils.assertCommandSucceeds( 197 String.format("device_config delete '%s' '%s'", PHENOTYPE_FLAG_NAMESPACE, key)); 198 } 199 } 200 backupAndDeleteFile(String remotePath)201 public void backupAndDeleteFile(String remotePath) throws Exception { 202 String tempFile = "/data/local/tmp/odsign_e2e_tests_" + UUID.randomUUID() + ".tmp"; 203 // Backup the file before deleting it. 204 mTestUtils.assertCommandSucceeds(String.format("cp '%s' '%s'", remotePath, tempFile)); 205 mTestUtils.assertCommandSucceeds(String.format("rm '%s'", remotePath)); 206 mDeletedFiles.put(remotePath, tempFile); 207 } 208 backupArtifacts()209 public void backupArtifacts() throws Exception { 210 mTestInfo.getDevice().executeShellV2Command( 211 String.format("rm -rf '%s'", ART_APEX_DALVIK_CACHE_BACKUP_DIRNAME)); 212 mTestUtils.assertCommandSucceeds( 213 String.format("cp -r '%s' '%s'", OdsignTestUtils.ART_APEX_DALVIK_CACHE_DIRNAME, 214 ART_APEX_DALVIK_CACHE_BACKUP_DIRNAME)); 215 mHasArtifactsBackup = true; 216 } 217 218 /** 219 * Pushes the file to a temporary location and bind-mount it at the given path. This is useful 220 * when the path is readonly. 221 */ pushAndBindMount(File localFile, String remotePath)222 private void pushAndBindMount(File localFile, String remotePath) throws Exception { 223 String tempFile = "/data/local/tmp/odsign_e2e_tests_" + UUID.randomUUID() + ".tmp"; 224 assertThat(mTestInfo.getDevice().pushFile(localFile, tempFile)).isTrue(); 225 mTempFiles.add(tempFile); 226 227 // If the path has already been bind-mounted by this method before, unmount it first. 228 if (mMountPoints.contains(remotePath)) { 229 mTestUtils.assertCommandSucceeds(String.format("umount '%s'", remotePath)); 230 mMountPoints.remove(remotePath); 231 } 232 233 mTestUtils.assertCommandSucceeds( 234 String.format("mount --bind '%s' '%s'", tempFile, remotePath)); 235 mMountPoints.add(remotePath); 236 mTestUtils.assertCommandSucceeds(String.format("restorecon '%s'", remotePath)); 237 } 238 239 /** A helper class for mutating an XML file. */ 240 private class XmlMutator implements AutoCloseable { 241 private final Document mDocument; 242 private final String mRemoteXmlFile; 243 private final File mLocalFile; 244 XmlMutator(String remoteXmlFile)245 public XmlMutator(String remoteXmlFile) throws Exception { 246 // Load the XML file. 247 mRemoteXmlFile = remoteXmlFile; 248 mLocalFile = mTestInfo.getDevice().pullFile(remoteXmlFile); 249 assertThat(mLocalFile).isNotNull(); 250 DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); 251 mDocument = builder.parse(mLocalFile); 252 } 253 254 @Override close()255 public void close() throws Exception { 256 // Save the XML file. 257 Transformer transformer = TransformerFactory.newInstance().newTransformer(); 258 transformer.transform(new DOMSource(mDocument), new StreamResult(mLocalFile)); 259 pushAndBindMount(mLocalFile, mRemoteXmlFile); 260 } 261 262 /** Returns a mutable XML document. */ getDocument()263 public Document getDocument() { 264 return mDocument; 265 } 266 } 267 } 268