Use a better approach to simulate APEX updates.

Before this change, the tests mutates cache-info.xml to simulate APEX
updates. After this change, the tests bind-mounts a fake
apex-info-list.xml to simulate APEX updates.

This change allows us to add tests later to test the cases where
cache-info.xml doesn't exist. It also allows us to eventually remove
APEX info from cache-info.xml when we use OatFileAssistant in odrefresh.

Bug: 272245228
Test: atest odsign_e2e_tests_full:OdrefreshHostTest
Change-Id: I1f19aa4d50825c4052062732ca9b17bd82ab3ec9
diff --git a/test/odsign/test-src/com/android/tests/odsign/DeviceState.java b/test/odsign/test-src/com/android/tests/odsign/DeviceState.java
new file mode 100644
index 0000000..fb21656
--- /dev/null
+++ b/test/odsign/test-src/com/android/tests/odsign/DeviceState.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2023 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.odsign;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.tradefed.invoker.TestInformation;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.io.File;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+/** A helper class that can mutate the device state and restore it afterwards. */
+public class DeviceState {
+    private static final String APEX_INFO_FILE = "/apex/apex-info-list.xml";
+
+    private final TestInformation mTestInfo;
+    private final OdsignTestUtils mTestUtils;
+
+    private Set<String> mTempFiles = new HashSet<>();
+    private Set<String> mMountPoints = new HashSet<>();
+
+    public DeviceState(TestInformation testInfo) throws Exception {
+        mTestInfo = testInfo;
+        mTestUtils = new OdsignTestUtils(testInfo);
+    }
+
+    /** Restores the device state. */
+    public void restore() throws Exception {
+        for (String mountPoint : mMountPoints) {
+            mTestInfo.getDevice().executeShellV2Command(String.format("umount '%s'", mountPoint));
+        }
+
+        for (String tempFile : mTempFiles) {
+            mTestInfo.getDevice().deleteFile(tempFile);
+        }
+    }
+
+    /** Simulates that the ART APEX has been upgraded. */
+    public void simulateArtApexUpgrade() throws Exception {
+        try (var xmlMutator = new XmlMutator(APEX_INFO_FILE)) {
+            NodeList list = xmlMutator.getDocument().getElementsByTagName("apex-info");
+            for (int i = 0; i < list.getLength(); i++) {
+                Element node = (Element) list.item(i);
+                if (node.getAttribute("moduleName").equals("com.android.art")
+                        && node.getAttribute("isActive").equals("true")) {
+                    node.setAttribute("isFactory", "false");
+                    node.setAttribute("lastUpdateMillis", "1");
+                }
+            }
+        }
+    }
+
+    /**
+     * Simulates that an APEX has been upgraded. We could install a real APEX, but that would
+     * introduce an extra dependency to this test, which we want to avoid.
+     */
+    public void simulateApexUpgrade() throws Exception {
+        try (var xmlMutator = new XmlMutator(APEX_INFO_FILE)) {
+            NodeList list = xmlMutator.getDocument().getElementsByTagName("apex-info");
+            for (int i = 0; i < list.getLength(); i++) {
+                Element node = (Element) list.item(i);
+                if (node.getAttribute("moduleName").equals("com.android.wifi")
+                        && node.getAttribute("isActive").equals("true")) {
+                    node.setAttribute("isFactory", "false");
+                    node.setAttribute("lastUpdateMillis", "1");
+                }
+            }
+        }
+    }
+
+    /**
+     * Pushes the file to a temporary location and bind-mount it at the given path. This is useful
+     * when the path is readonly.
+     */
+    private void pushAndBindMount(File localFile, String remotePath) throws Exception {
+        String tempFile = "/data/local/tmp/odsign_e2e_tests_" + UUID.randomUUID() + ".tmp";
+        assertThat(mTestInfo.getDevice().pushFile(localFile, tempFile)).isTrue();
+        mTempFiles.add(tempFile);
+
+        mTestUtils.assertCommandSucceeds(
+                String.format("mount --bind '%s' '%s'", tempFile, remotePath));
+        mMountPoints.add(remotePath);
+        mTestUtils.assertCommandSucceeds(String.format("restorecon '%s'", remotePath));
+    }
+
+    /** A helper class for mutating an XML file. */
+    private class XmlMutator implements AutoCloseable {
+        private final Document mDocument;
+        private final String mRemoteXmlFile;
+        private final File mLocalFile;
+
+        public XmlMutator(String remoteXmlFile) throws Exception {
+            // Load the XML file.
+            mRemoteXmlFile = remoteXmlFile;
+            mLocalFile = mTestInfo.getDevice().pullFile(remoteXmlFile);
+            assertThat(mLocalFile).isNotNull();
+            DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+            mDocument = builder.parse(mLocalFile);
+        }
+
+        @Override
+        public void close() throws Exception {
+            // Save the XML file.
+            Transformer transformer = TransformerFactory.newInstance().newTransformer();
+            transformer.transform(new DOMSource(mDocument), new StreamResult(mLocalFile));
+            pushAndBindMount(mLocalFile, mRemoteXmlFile);
+        }
+
+        /** Returns a mutable XML document. */
+        public Document getDocument() {
+            return mDocument;
+        }
+    }
+}
diff --git a/test/odsign/test-src/com/android/tests/odsign/OdrefreshHostTest.java b/test/odsign/test-src/com/android/tests/odsign/OdrefreshHostTest.java
index 60709f1..cf2365e 100644
--- a/test/odsign/test-src/com/android/tests/odsign/OdrefreshHostTest.java
+++ b/test/odsign/test-src/com/android/tests/odsign/OdrefreshHostTest.java
@@ -27,6 +27,7 @@
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
 import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -54,6 +55,7 @@
     private static final String SYSTEM_SERVER_ARTIFACTS_KEY = TAG + ":SYSTEM_SERVER_ARTIFACTS";
 
     private OdsignTestUtils mTestUtils;
+    private DeviceState mDeviceState;
 
     @BeforeClassWithInfo
     public static void beforeClassWithDevice(TestInformation testInfo) throws Exception {
@@ -92,6 +94,7 @@
     @Before
     public void setUp() throws Exception {
         mTestUtils = new OdsignTestUtils(getTestInformation());
+        mDeviceState = new DeviceState(getTestInformation());
 
         // Restore the artifacts to ensure a clean initial state.
         getDevice().executeShellV2Command(
@@ -101,9 +104,14 @@
                         OdsignTestUtils.ART_APEX_DALVIK_CACHE_DIRNAME));
     }
 
+    @After
+    public void tearDown() throws Exception {
+        mDeviceState.restore();
+    }
+
     @Test
     public void verifyArtSamegradeUpdateTriggersCompilation() throws Exception {
-        simulateArtApexUpgrade();
+        mDeviceState.simulateArtApexUpgrade();
         long timeMs = mTestUtils.getCurrentTimeMs();
         runOdrefresh();
 
@@ -113,7 +121,7 @@
 
     @Test
     public void verifyOtherApexSamegradeUpdateTriggersCompilation() throws Exception {
-        simulateApexUpgrade();
+        mDeviceState.simulateApexUpgrade();
         long timeMs = mTestUtils.getCurrentTimeMs();
         runOdrefresh();
 
@@ -370,7 +378,7 @@
     @Test
     public void verifyCompilationOsMode() throws Exception {
         mTestUtils.removeCompilationLogToAvoidBackoff();
-        simulateApexUpgrade();
+        mDeviceState.simulateApexUpgrade();
         long timeMs = mTestUtils.getCurrentTimeMs();
         runOdrefresh("--compilation-os-mode");
 
@@ -472,33 +480,6 @@
         getDevice().pushString(cacheInfo, CACHE_INFO_FILE);
     }
 
-    /**
-     * Simulates that an ART APEX has been upgraded.
-     */
-    private void simulateArtApexUpgrade() throws Exception {
-        String apexInfo = getDevice().pullFileContents(CACHE_INFO_FILE);
-        // Replace the lastUpdateMillis of com.android.art with "1".
-        apexInfo = replaceLine(
-                apexInfo,
-                "(.*com\\.android\\.art.*lastUpdateMillis=\").*?(\".*)",
-                "$11$2");
-        getDevice().pushString(apexInfo, CACHE_INFO_FILE);
-    }
-
-    /**
-     * Simulates that an APEX has been upgraded. We could install a real APEX, but that would
-     * introduce an extra dependency to this test, which we want to avoid.
-     */
-    private void simulateApexUpgrade() throws Exception {
-        String apexInfo = getDevice().pullFileContents(CACHE_INFO_FILE);
-        // Replace the lastUpdateMillis of com.android.wifi with "1".
-        apexInfo = replaceLine(
-                apexInfo,
-                "(.*com\\.android\\.wifi.*lastUpdateMillis=\").*?(\".*)",
-                "$11$2");
-        getDevice().pushString(apexInfo, CACHE_INFO_FILE);
-    }
-
     private Set<String> simulateMissingArtifacts() throws Exception {
         Set<String> missingArtifacts = new HashSet<>();
         String sample = getSystemServerArtifacts().iterator().next();