Add tests for cases where APEXes are factory-installed.

Bug: 272245228
Test: atest odsign_e2e_tests_full:OdrefreshFactoryWithCacheInfoHostTest
Test: atest odsign_e2e_tests_full:OdrefreshFactoryWithoutCacheInfoHostTest
Change-Id: I7444f5d2c2beac3c0467dd8eb18d27f757df6033
diff --git a/test/odsign/test-src/com/android/tests/odsign/DeviceState.java b/test/odsign/test-src/com/android/tests/odsign/DeviceState.java
index 8379cba..c6ca53c 100644
--- a/test/odsign/test-src/com/android/tests/odsign/DeviceState.java
+++ b/test/odsign/test-src/com/android/tests/odsign/DeviceState.java
@@ -40,7 +40,6 @@
 
 /** 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 static final String TEST_JAR_RESOURCE_NAME = "/art-gtest-jars-Main.jar";
     private static final String PHENOTYPE_FLAG_NAMESPACE = "runtime_native_boot";
     private static final String ART_APEX_DALVIK_CACHE_BACKUP_DIRNAME =
@@ -53,6 +52,7 @@
     private Set<String> mMountPoints = new HashSet<>();
     private Map<String, String> mMutatedProperties = new HashMap<>();
     private Set<String> mMutatedPhenotypeFlags = new HashSet<>();
+    private Map<String, String> mDeletedFiles = new HashMap<>();
     private boolean mHasArtifactsBackup = false;
 
     public DeviceState(TestInformation testInfo) throws Exception {
@@ -85,6 +85,14 @@
                     "device_config set_sync_disabled_for_tests none");
         }
 
+        for (var entry : mDeletedFiles.entrySet()) {
+            mTestInfo.getDevice().executeShellV2Command(
+                    String.format("cp '%s' '%s'", entry.getValue(), entry.getKey()));
+            mTestInfo.getDevice().executeShellV2Command(String.format("rm '%s'", entry.getValue()));
+            mTestInfo.getDevice().executeShellV2Command(
+                    String.format("restorecon '%s'", entry.getKey()));
+        }
+
         if (mHasArtifactsBackup) {
             mTestInfo.getDevice().executeShellV2Command(
                     String.format("rm -rf '%s'", OdsignTestUtils.ART_APEX_DALVIK_CACHE_DIRNAME));
@@ -96,17 +104,15 @@
 
     /** 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");
-                }
-            }
-        }
+        updateApexInfo("com.android.art", false /* isFactory */);
+    }
+
+    /**
+     * Simulates that the new ART APEX has been uninstalled (i.e., the ART module goes back to the
+     * factory version).
+     */
+    public void simulateArtApexUninstall() throws Exception {
+        updateApexInfo("com.android.art", true /* isFactory */);
     }
 
     /**
@@ -114,14 +120,27 @@
      * 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)) {
+        updateApexInfo("com.android.wifi", false /* isFactory */);
+    }
+
+    /**
+     * Simulates that the new APEX has been uninstalled (i.e., the module goes back to the factory
+     * version).
+     */
+    public void simulateApexUninstall() throws Exception {
+        updateApexInfo("com.android.wifi", true /* isFactory */);
+    }
+
+    private void updateApexInfo(String moduleName, boolean isFactory) throws Exception {
+        try (var xmlMutator = new XmlMutator(OdsignTestUtils.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")
+                if (node.getAttribute("moduleName").equals(moduleName)
                         && node.getAttribute("isActive").equals("true")) {
-                    node.setAttribute("isFactory", "false");
-                    node.setAttribute("lastUpdateMillis", "1");
+                    node.setAttribute("isFactory", String.valueOf(isFactory));
+                    node.setAttribute(
+                            "lastUpdateMillis", String.valueOf(System.currentTimeMillis()));
                 }
             }
         }
@@ -174,6 +193,14 @@
         }
     }
 
+    public void backupAndDeleteFile(String remotePath) throws Exception {
+        String tempFile = "/data/local/tmp/odsign_e2e_tests_" + UUID.randomUUID() + ".tmp";
+        // Backup the file before deleting it.
+        mTestUtils.assertCommandSucceeds(String.format("cp '%s' '%s'", remotePath, tempFile));
+        mTestUtils.assertCommandSucceeds(String.format("rm '%s'", remotePath));
+        mDeletedFiles.put(remotePath, tempFile);
+    }
+
     public void backupArtifacts() throws Exception {
         mTestInfo.getDevice().executeShellV2Command(
                 String.format("rm -rf '%s'", ART_APEX_DALVIK_CACHE_BACKUP_DIRNAME));
@@ -192,6 +219,12 @@
         assertThat(mTestInfo.getDevice().pushFile(localFile, tempFile)).isTrue();
         mTempFiles.add(tempFile);
 
+        // If the path has already been bind-mounted by this method before, unmount it first.
+        if (mMountPoints.contains(remotePath)) {
+            mTestUtils.assertCommandSucceeds(String.format("umount '%s'", remotePath));
+            mMountPoints.remove(remotePath);
+        }
+
         mTestUtils.assertCommandSucceeds(
                 String.format("mount --bind '%s' '%s'", tempFile, remotePath));
         mMountPoints.add(remotePath);
diff --git a/test/odsign/test-src/com/android/tests/odsign/OdrefreshFactoryHostTestBase.java b/test/odsign/test-src/com/android/tests/odsign/OdrefreshFactoryHostTestBase.java
new file mode 100644
index 0000000..7c7d97b
--- /dev/null
+++ b/test/odsign/test-src/com/android/tests/odsign/OdrefreshFactoryHostTestBase.java
@@ -0,0 +1,186 @@
+/*
+ * 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 org.junit.Assume.assumeTrue;
+
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.testtype.junit4.AfterClassWithInfo;
+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.Ignore;
+import org.junit.Test;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * This class tests odrefresh for the cases where all the APEXes are initially factory-installed.
+ * Similar to OdrefreshHostTest, it does not involve odsign, fs-verity, and ART runtime.
+ *
+ * The tests are run by derived classes with different conditions: with and without the cache info.
+ */
+@Ignore("See derived classes")
+abstract public class OdrefreshFactoryHostTestBase extends BaseHostJUnit4Test {
+    protected OdsignTestUtils mTestUtils;
+    protected DeviceState mDeviceState;
+
+    @BeforeClassWithInfo
+    public static void beforeClassWithDeviceBase(TestInformation testInfo) throws Exception {
+        OdsignTestUtils testUtils = new OdsignTestUtils(testInfo);
+        assumeTrue(testUtils.areAllApexesFactoryInstalled());
+        testUtils.assertCommandSucceeds("disable-verity");
+        testUtils.removeCompilationLogToAvoidBackoff();
+        testUtils.reboot();
+        testUtils.assertCommandSucceeds("remount");
+    }
+
+    @AfterClassWithInfo
+    public static void afterClassWithDeviceBase(TestInformation testInfo) throws Exception {
+        OdsignTestUtils testUtils = new OdsignTestUtils(testInfo);
+        testUtils.assertCommandSucceeds("enable-verity");
+        testUtils.removeCompilationLogToAvoidBackoff();
+        testUtils.reboot();
+    }
+
+    @Before
+    public void setUpBase() throws Exception {
+        mTestUtils = new OdsignTestUtils(getTestInformation());
+        mDeviceState = new DeviceState(getTestInformation());
+        mDeviceState.backupArtifacts();
+    }
+
+    @After
+    public void tearDownBase() throws Exception {
+        mDeviceState.restore();
+    }
+
+    @Test
+    public void verifyArtSamegradeUpdateTriggersCompilation() throws Exception {
+        mDeviceState.simulateArtApexUpgrade();
+        long timeMs = mTestUtils.getCurrentTimeMs();
+        mTestUtils.runOdrefresh();
+
+        // It should recompile everything.
+        mTestUtils.assertModifiedAfter(Set.of(OdsignTestUtils.CACHE_INFO_FILE), timeMs);
+        mTestUtils.assertModifiedAfter(mTestUtils.getZygotesExpectedArtifacts(), timeMs);
+        mTestUtils.assertModifiedAfter(mTestUtils.getSystemServerExpectedArtifacts(), timeMs);
+
+        mDeviceState.simulateArtApexUninstall();
+        mTestUtils.runOdrefresh();
+
+        // It should delete all compilation artifacts and update the cache info.
+        // TODO(b/272245228): The cache info should be updated.
+        mTestUtils.assertFilesNotExist(mTestUtils.getZygotesExpectedArtifacts());
+        mTestUtils.assertFilesNotExist(mTestUtils.getSystemServerExpectedArtifacts());
+    }
+
+    @Test
+    public void verifyOtherApexSamegradeUpdateTriggersCompilation() throws Exception {
+        mDeviceState.simulateApexUpgrade();
+        long timeMs = mTestUtils.getCurrentTimeMs();
+        mTestUtils.runOdrefresh();
+
+        // It should only recompile system server.
+        mTestUtils.assertModifiedAfter(Set.of(OdsignTestUtils.CACHE_INFO_FILE), timeMs);
+        mTestUtils.assertFilesNotExist(mTestUtils.getZygotesExpectedArtifacts());
+        mTestUtils.assertModifiedAfter(mTestUtils.getSystemServerExpectedArtifacts(), timeMs);
+
+        mDeviceState.simulateApexUninstall();
+        mTestUtils.runOdrefresh();
+
+        // It should delete all compilation artifacts and update the cache info.
+        // TODO(b/272245228): The cache info should be updated.
+        mTestUtils.assertFilesNotExist(mTestUtils.getZygotesExpectedArtifacts());
+        mTestUtils.assertFilesNotExist(mTestUtils.getSystemServerExpectedArtifacts());
+    }
+
+    @Test
+    @Ignore("This test cannot pass. The fix for b/272245228 will also fix this.")
+    public void verifyMissingArtifactTriggersCompilation() throws Exception {
+        // Simulate that an artifact is missing from /system.
+        mDeviceState.backupAndDeleteFile("/system/framework/oat/x86_64/services.odex");
+
+        long timeMs = mTestUtils.getCurrentTimeMs();
+        mTestUtils.runOdrefresh();
+
+        Set<String> expectedArtifacts = OdsignTestUtils.getApexDataDalvikCacheFilenames(
+                "/system/framework/services.jar", mTestUtils.getSystemServerIsa());
+
+        Set<String> nonExpectedArtifacts = new HashSet<>();
+        nonExpectedArtifacts.addAll(mTestUtils.getZygotesExpectedArtifacts());
+        nonExpectedArtifacts.addAll(mTestUtils.getSystemServerExpectedArtifacts());
+        nonExpectedArtifacts.removeAll(expectedArtifacts);
+
+        // It should only generate artifacts that are missing from /system.
+        mTestUtils.assertModifiedAfter(Set.of(OdsignTestUtils.CACHE_INFO_FILE), timeMs);
+        mTestUtils.assertFilesNotExist(nonExpectedArtifacts);
+        mTestUtils.assertModifiedAfter(expectedArtifacts, timeMs);
+
+        mDeviceState.simulateArtApexUpgrade();
+        timeMs = mTestUtils.getCurrentTimeMs();
+        mTestUtils.runOdrefresh();
+
+        // It should recompile everything.
+        mTestUtils.assertModifiedAfter(Set.of(OdsignTestUtils.CACHE_INFO_FILE), timeMs);
+        mTestUtils.assertModifiedAfter(mTestUtils.getZygotesExpectedArtifacts(), timeMs);
+        mTestUtils.assertModifiedAfter(mTestUtils.getSystemServerExpectedArtifacts(), timeMs);
+
+        mDeviceState.simulateArtApexUninstall();
+        timeMs = mTestUtils.getCurrentTimeMs();
+        mTestUtils.runOdrefresh();
+
+        // It should only re-generate artifacts that are missing from /system.
+        mTestUtils.assertModifiedAfter(Set.of(OdsignTestUtils.CACHE_INFO_FILE), timeMs);
+        mTestUtils.assertFilesNotExist(nonExpectedArtifacts);
+        mTestUtils.assertModifiedAfter(expectedArtifacts, timeMs);
+    }
+
+    @Test
+    public void verifyEnableUffdGcChangeTriggersCompilation() throws Exception {
+        mDeviceState.setPhenotypeFlag("enable_uffd_gc", "true");
+
+        long timeMs = mTestUtils.getCurrentTimeMs();
+        mTestUtils.runOdrefresh();
+
+        // It should recompile everything.
+        mTestUtils.assertModifiedAfter(Set.of(OdsignTestUtils.CACHE_INFO_FILE), timeMs);
+        mTestUtils.assertModifiedAfter(mTestUtils.getZygotesExpectedArtifacts(), timeMs);
+        mTestUtils.assertModifiedAfter(mTestUtils.getSystemServerExpectedArtifacts(), timeMs);
+
+        // Run odrefresh again with the flag unchanged.
+        timeMs = mTestUtils.getCurrentTimeMs();
+        mTestUtils.runOdrefresh();
+
+        // Nothing should change.
+        mTestUtils.assertNotModifiedAfter(Set.of(OdsignTestUtils.CACHE_INFO_FILE), timeMs);
+        mTestUtils.assertNotModifiedAfter(mTestUtils.getZygotesExpectedArtifacts(), timeMs);
+        mTestUtils.assertNotModifiedAfter(mTestUtils.getSystemServerExpectedArtifacts(), timeMs);
+
+        mDeviceState.setPhenotypeFlag("enable_uffd_gc", null);
+
+        mTestUtils.runOdrefresh();
+
+        // It should delete all compilation artifacts and update the cache info.
+        // TODO(b/272245228): The cache info should be updated.
+        mTestUtils.assertFilesNotExist(mTestUtils.getZygotesExpectedArtifacts());
+        mTestUtils.assertFilesNotExist(mTestUtils.getSystemServerExpectedArtifacts());
+    }
+}
diff --git a/test/odsign/test-src/com/android/tests/odsign/OdrefreshFactoryWithCacheInfoHostTest.java b/test/odsign/test-src/com/android/tests/odsign/OdrefreshFactoryWithCacheInfoHostTest.java
new file mode 100644
index 0000000..d270fab
--- /dev/null
+++ b/test/odsign/test-src/com/android/tests/odsign/OdrefreshFactoryWithCacheInfoHostTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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 com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Set;
+
+/**
+ * This class tests odrefresh for the cases where all the APEXes are initially factory-installed
+ * and the cache info exists, which is the normal case.
+ *
+ * Both the tests in the base class and the tests in this class are run with the setup of this
+ * class.
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class OdrefreshFactoryWithCacheInfoHostTest extends OdrefreshFactoryHostTestBase {
+    @Test
+    public void verifyNoCompilationWhenSystemIsGood() throws Exception {
+        // Only the cache info should exist.
+        mTestUtils.assertFilesExist(Set.of(OdsignTestUtils.CACHE_INFO_FILE));
+        mTestUtils.assertFilesNotExist(mTestUtils.getZygotesExpectedArtifacts());
+        mTestUtils.assertFilesNotExist(mTestUtils.getSystemServerExpectedArtifacts());
+
+        // Run again.
+        long timeMs = mTestUtils.getCurrentTimeMs();
+        mTestUtils.runOdrefresh();
+
+        // Nothing should change.
+        mTestUtils.assertNotModifiedAfter(Set.of(OdsignTestUtils.CACHE_INFO_FILE), timeMs);
+        mTestUtils.assertFilesNotExist(mTestUtils.getZygotesExpectedArtifacts());
+        mTestUtils.assertFilesNotExist(mTestUtils.getSystemServerExpectedArtifacts());
+    }
+}
diff --git a/test/odsign/test-src/com/android/tests/odsign/OdrefreshFactoryWithoutCacheInfoHostTest.java b/test/odsign/test-src/com/android/tests/odsign/OdrefreshFactoryWithoutCacheInfoHostTest.java
new file mode 100644
index 0000000..bec0dd8
--- /dev/null
+++ b/test/odsign/test-src/com/android/tests/odsign/OdrefreshFactoryWithoutCacheInfoHostTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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 com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Set;
+
+/**
+ * This class tests odrefresh for the cases where all the APEXes are initially factory-installed
+ * and the cache info does not exist.
+ *
+ * The cache info can be missing due to various reasons (corrupted files deleted by odsign, odsign
+ * failure, etc.), so this test makes sure that odrefresh doesn't rely on the cache info when
+ * checking artifacts on /system.
+ *
+ * Both the tests in the base class and the tests in this class are run with the setup of this
+ * class.
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class OdrefreshFactoryWithoutCacheInfoHostTest extends OdrefreshFactoryHostTestBase {
+    @Before
+    public void setUp() throws Exception {
+        getDevice().deleteFile(OdsignTestUtils.CACHE_INFO_FILE);
+    }
+
+    @Test
+    public void verifyNoCompilationWhenSystemIsGood() throws Exception {
+        long timeMs = mTestUtils.getCurrentTimeMs();
+        mTestUtils.runOdrefresh();
+
+        // It should only generate the missing cache info.
+        mTestUtils.assertModifiedAfter(Set.of(OdsignTestUtils.CACHE_INFO_FILE), timeMs);
+        mTestUtils.assertFilesNotExist(mTestUtils.getZygotesExpectedArtifacts());
+        mTestUtils.assertFilesNotExist(mTestUtils.getSystemServerExpectedArtifacts());
+    }
+}
diff --git a/test/odsign/test-src/com/android/tests/odsign/OdsignTestUtils.java b/test/odsign/test-src/com/android/tests/odsign/OdsignTestUtils.java
index 83cd881..c8d2516 100644
--- a/test/odsign/test-src/com/android/tests/odsign/OdsignTestUtils.java
+++ b/test/odsign/test-src/com/android/tests/odsign/OdsignTestUtils.java
@@ -36,6 +36,11 @@
 
 import com.google.common.io.ByteStreams;
 
+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.io.FileInputStream;
 import java.io.FileOutputStream;
@@ -53,11 +58,14 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Stream;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
 
 public class OdsignTestUtils {
     public static final String ART_APEX_DALVIK_CACHE_DIRNAME =
             "/data/misc/apexdata/com.android.art/dalvik-cache";
     public static final String CACHE_INFO_FILE = ART_APEX_DALVIK_CACHE_DIRNAME + "/cache-info.xml";
+    public static final String APEX_INFO_FILE = "/apex/apex-info-list.xml";
 
     private static final String ODREFRESH_BIN = "odrefresh";
 
@@ -413,6 +421,24 @@
         }
     }
 
+    public void assertFilesExist(Set<String> files) throws Exception {
+        assertThat(getExistingFiles(files)).containsExactlyElementsIn(files);
+    }
+
+    public void assertFilesNotExist(Set<String> files) throws Exception {
+        assertThat(getExistingFiles(files)).isEmpty();
+    }
+
+    private Set<String> getExistingFiles(Set<String> files) throws Exception {
+        Set<String> existingFiles = new HashSet<>();
+        for (String file : files) {
+            if (mTestInfo.getDevice().doesFileExist(file)) {
+                existingFiles.add(file);
+            }
+        }
+        return existingFiles;
+    }
+
     public static String replaceExtension(String filename, String extension) throws Exception {
         int index = filename.lastIndexOf(".");
         assertTrue("Extension not found in filename: " + filename, index != -1);
@@ -428,4 +454,24 @@
         mTestInfo.getDevice().executeShellV2Command(
                 ODREFRESH_BIN + " --partial-compilation --no-refresh " + extraArgs + " --compile");
     }
+
+    public boolean areAllApexesFactoryInstalled() throws Exception {
+        Document doc = loadXml(APEX_INFO_FILE);
+        NodeList list = doc.getElementsByTagName("apex-info");
+        for (int i = 0; i < list.getLength(); i++) {
+            Element node = (Element) list.item(i);
+            if (node.getAttribute("isActive").equals("true")
+                    && node.getAttribute("isFactory").equals("false")) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private Document loadXml(String remoteXmlFile) throws Exception {
+        File localFile = mTestInfo.getDevice().pullFile(remoteXmlFile);
+        assertThat(localFile).isNotNull();
+        DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+        return builder.parse(localFile);
+    }
 }