• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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