Split odsign_e2e_tests. am: 59064ebccc

Original change: https://android-review.googlesource.com/c/platform/art/+/1833296

Change-Id: I97da3b5c01ed136096c0d618bdb4c3ccb88cc22f
diff --git a/test/odsign/test-src/com/android/tests/odsign/OdrefreshHostTest.java b/test/odsign/test-src/com/android/tests/odsign/OdrefreshHostTest.java
new file mode 100644
index 0000000..a6e9e73
--- /dev/null
+++ b/test/odsign/test-src/com/android/tests/odsign/OdrefreshHostTest.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2021 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.Assert.assertTrue;
+
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.AfterClassWithInfo;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+
+/**
+ * Test to check end-to-end odrefresh invocations, but without odsign, fs-verity, and ART runtime
+ * involved.
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class OdrefreshHostTest extends BaseHostJUnit4Test {
+    private static final String CACHE_INFO_FILE =
+            OdsignTestUtils.ART_APEX_DALVIK_CACHE_DIRNAME + "/cache-info.xml";
+
+    private static OdsignTestUtils sTestUtils;
+
+    private static Set<String> sZygoteArtifacts;
+    private static Set<String> sSystemServerArtifacts;
+
+    @BeforeClassWithInfo
+    public static void beforeClassWithDevice(TestInformation testInfo) throws Exception {
+        sTestUtils = new OdsignTestUtils(testInfo);
+        sTestUtils.installTestApex();
+
+        sZygoteArtifacts = new HashSet<>();
+        for (String zygoteName : sTestUtils.ZYGOTE_NAMES) {
+            sZygoteArtifacts.addAll(
+                    sTestUtils.getZygoteLoadedArtifacts(zygoteName).orElse(new HashSet<>()));
+        }
+        sSystemServerArtifacts = sTestUtils.getSystemServerLoadedArtifacts();
+    }
+
+    @AfterClassWithInfo
+    public static void afterClassWithDevice(TestInformation testInfo) throws Exception {
+        sTestUtils.uninstallTestApex();
+    }
+
+    @Test
+    public void verifyArtSamegradeUpdateTriggersCompilation() throws Exception {
+        simulateArtApexUpgrade();
+        sTestUtils.removeCompilationLogToAvoidBackoff();
+        long timeMs = getCurrentTimeMs();
+        getDevice().executeShellV2Command("odrefresh --compile");
+
+        assertArtifactsModifiedAfter(sZygoteArtifacts, timeMs);
+        assertArtifactsModifiedAfter(sSystemServerArtifacts, timeMs);
+    }
+
+    @Test
+    public void verifyOtherApexSamegradeUpdateTriggersCompilation() throws Exception {
+        simulateApexUpgrade();
+        sTestUtils.removeCompilationLogToAvoidBackoff();
+        long timeMs = getCurrentTimeMs();
+        getDevice().executeShellV2Command("odrefresh --compile");
+
+        assertArtifactsNotModifiedAfter(sZygoteArtifacts, timeMs);
+        assertArtifactsModifiedAfter(sSystemServerArtifacts, timeMs);
+    }
+
+    @Test
+    public void verifyBootClasspathOtaTriggersCompilation() throws Exception {
+        simulateBootClasspathOta();
+        sTestUtils.removeCompilationLogToAvoidBackoff();
+        long timeMs = getCurrentTimeMs();
+        getDevice().executeShellV2Command("odrefresh --compile");
+
+        assertArtifactsModifiedAfter(sZygoteArtifacts, timeMs);
+        assertArtifactsModifiedAfter(sSystemServerArtifacts, timeMs);
+    }
+
+    @Test
+    public void verifySystemServerOtaTriggersCompilation() throws Exception {
+        simulateSystemServerOta();
+        sTestUtils.removeCompilationLogToAvoidBackoff();
+        long timeMs = getCurrentTimeMs();
+        getDevice().executeShellV2Command("odrefresh --compile");
+
+        assertArtifactsNotModifiedAfter(sZygoteArtifacts, timeMs);
+        assertArtifactsModifiedAfter(sSystemServerArtifacts, timeMs);
+    }
+
+    @Test
+    public void verifyNoCompilationWhenCacheIsGood() throws Exception {
+        sTestUtils.removeCompilationLogToAvoidBackoff();
+        long timeMs = getCurrentTimeMs();
+        getDevice().executeShellV2Command("odrefresh --compile");
+
+        assertArtifactsNotModifiedAfter(sZygoteArtifacts, timeMs);
+        assertArtifactsNotModifiedAfter(sSystemServerArtifacts, timeMs);
+    }
+
+    /**
+     * Checks the input line by line and replaces all lines that match the regex with the given
+     * replacement.
+     */
+    private String replaceLine(String input, String regex, String replacement) {
+        StringBuffer output = new StringBuffer();
+        Pattern p = Pattern.compile(regex);
+        for (String line : input.split("\n")) {
+            Matcher m = p.matcher(line);
+            if (m.matches()) {
+                m.appendReplacement(output, replacement);
+                output.append("\n");
+            } else {
+                output.append(line + "\n");
+            }
+        }
+        return output.toString();
+    }
+
+    /**
+     * Simulates that there is an OTA that updates a boot classpath jar.
+     */
+    private void simulateBootClasspathOta() throws Exception {
+        String cacheInfo = getDevice().pullFileContents(CACHE_INFO_FILE);
+        // Replace the cached checksum of /system/framework/framework.jar with "aaaaaaaa".
+        cacheInfo = replaceLine(
+                cacheInfo,
+                "(.*/system/framework/framework\\.jar.*checksums=\").*?(\".*)",
+                "$1aaaaaaaa$2");
+        getDevice().pushString(cacheInfo, CACHE_INFO_FILE);
+    }
+
+    /**
+     * Simulates that there is an OTA that updates a system server jar.
+     */
+    private void simulateSystemServerOta() throws Exception {
+        String cacheInfo = getDevice().pullFileContents(CACHE_INFO_FILE);
+        // Replace the cached checksum of /system/framework/services.jar with "aaaaaaaa".
+        cacheInfo = replaceLine(
+                cacheInfo,
+                "(.*/system/framework/services\\.jar.*checksums=\").*?(\".*)",
+                "$1aaaaaaaa$2");
+        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 long parseFormattedDateTime(String dateTimeStr) throws Exception {
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(
+                "yyyy-MM-dd HH:mm:ss.nnnnnnnnn Z");
+        ZonedDateTime zonedDateTime = ZonedDateTime.parse(dateTimeStr, formatter);
+        return zonedDateTime.toInstant().toEpochMilli();
+    }
+
+    private long getModifiedTimeMs(String filename) throws Exception {
+        // We can't use the "-c '%.3Y'" flag when to get the timestamp because the Toybox's `stat`
+        // implementation truncates the timestamp to seconds, which is not accurate enough, so we
+        // use "-c '%%y'" and parse the time ourselves.
+        String dateTimeStr = getDevice()
+                .executeShellCommand(String.format("stat -c '%%y' '%s'", filename))
+                .trim();
+        return parseFormattedDateTime(dateTimeStr);
+    }
+
+    private long getCurrentTimeMs() throws Exception {
+        // We can't use getDevice().getDeviceDate() because it truncates the timestamp to seconds,
+        // which is not accurate enough.
+        String dateTimeStr = getDevice()
+                .executeShellCommand("date +'%Y-%m-%d %H:%M:%S.%N %z'")
+                .trim();
+        return parseFormattedDateTime(dateTimeStr);
+    }
+
+    private void assertArtifactsModifiedAfter(Set<String> artifacts, long timeMs) throws Exception {
+        for (String artifact : artifacts) {
+            long modifiedTime = getModifiedTimeMs(artifact);
+            assertTrue(
+                    String.format(
+                            "Artifact %s is not re-compiled. Modified time: %d, Reference time: %d",
+                            artifact,
+                            modifiedTime,
+                            timeMs),
+                    modifiedTime > timeMs);
+        }
+    }
+
+    private void assertArtifactsNotModifiedAfter(Set<String> artifacts, long timeMs)
+            throws Exception {
+        for (String artifact : artifacts) {
+            long modifiedTime = getModifiedTimeMs(artifact);
+            assertTrue(
+                    String.format(
+                            "Artifact %s is unexpectedly re-compiled. " +
+                                    "Modified time: %d, Reference time: %d",
+                            artifact,
+                            modifiedTime,
+                            timeMs),
+                    modifiedTime < timeMs);
+        }
+    }
+}
diff --git a/test/odsign/test-src/com/android/tests/odsign/OdsignTestUtils.java b/test/odsign/test-src/com/android/tests/odsign/OdsignTestUtils.java
new file mode 100644
index 0000000..c51b0c6
--- /dev/null
+++ b/test/odsign/test-src/com/android/tests/odsign/OdsignTestUtils.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2021 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.assertWithMessage;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import android.cts.install.lib.host.InstallUtilsHost;
+
+import com.android.tradefed.device.ITestDevice.ApexInfo;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.util.CommandResult;
+
+import java.time.Duration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+public class OdsignTestUtils {
+    public static final String ART_APEX_DALVIK_CACHE_DIRNAME =
+            "/data/misc/apexdata/com.android.art/dalvik-cache";
+
+    public static final List<String> ZYGOTE_NAMES = List.of("zygote", "zygote64");
+
+    private static final String APEX_FILENAME = "test_com.android.art.apex";
+
+    private static final String ODREFRESH_COMPILATION_LOG =
+            "/data/misc/odrefresh/compilation-log.txt";
+
+    private static final Duration BOOT_COMPLETE_TIMEOUT = Duration.ofMinutes(2);
+
+    private final InstallUtilsHost mInstallUtils;
+    private final TestInformation mTestInfo;
+
+    public OdsignTestUtils(TestInformation testInfo) throws Exception {
+        assertNotNull(testInfo.getDevice());
+        mInstallUtils = new InstallUtilsHost(testInfo);
+        mTestInfo = testInfo;
+    }
+
+    public void installTestApex() throws Exception {
+        assumeTrue("Updating APEX is not supported", mInstallUtils.isApexUpdateSupported());
+        mInstallUtils.installApexes(APEX_FILENAME);
+        removeCompilationLogToAvoidBackoff();
+        reboot();
+    }
+
+    public void uninstallTestApex() throws Exception {
+        ApexInfo apex = mInstallUtils.getApexInfo(mInstallUtils.getTestFile(APEX_FILENAME));
+        mTestInfo.getDevice().uninstallPackage(apex.name);
+        removeCompilationLogToAvoidBackoff();
+        reboot();
+    }
+
+    public Set<String> getMappedArtifacts(String pid, String grepPattern) throws Exception {
+        final String grepCommand = String.format("grep \"%s\" /proc/%s/maps", grepPattern, pid);
+        CommandResult result = mTestInfo.getDevice().executeShellV2Command(grepCommand);
+        assertTrue(result.toString(), result.getExitCode() == 0);
+        Set<String> mappedFiles = new HashSet<>();
+        for (String line : result.getStdout().split("\\R")) {
+            int start = line.indexOf(ART_APEX_DALVIK_CACHE_DIRNAME);
+            if (line.contains("[")) {
+                continue; // ignore anonymously mapped sections which are quoted in square braces.
+            }
+            mappedFiles.add(line.substring(start));
+        }
+        return mappedFiles;
+    }
+
+    /**
+     * Returns the mapped artifacts of the Zygote process, or {@code Optional.empty()} if the
+     * process does not exist.
+     */
+    public Optional<Set<String>> getZygoteLoadedArtifacts(String zygoteName) throws Exception {
+        final CommandResult result =
+                mTestInfo.getDevice().executeShellV2Command("pidof " + zygoteName);
+        if (result.getExitCode() != 0) {
+            return Optional.empty();
+        }
+        // There may be multiple Zygote processes when Zygote just forks and has not executed any
+        // app binary. We can take any of the pids.
+        // We can't use the "-s" flag when calling `pidof` because the Toybox's `pidof`
+        // implementation is wrong and it outputs multiple pids regardless of the "-s" flag, so we
+        // split the output and take the first pid ourselves.
+        final String zygotePid = result.getStdout().trim().split("\\s+")[0];
+        assertTrue(!zygotePid.isEmpty());
+
+        final String grepPattern = ART_APEX_DALVIK_CACHE_DIRNAME + ".*boot-framework";
+        return Optional.of(getMappedArtifacts(zygotePid, grepPattern));
+    }
+
+    public Set<String> getSystemServerLoadedArtifacts() throws Exception {
+        final CommandResult result =
+                mTestInfo.getDevice().executeShellV2Command("pidof system_server");
+        assertTrue(result.toString(), result.getExitCode() == 0);
+        final String systemServerPid = result.getStdout().trim();
+        assertTrue(!systemServerPid.isEmpty());
+        assertTrue(
+                "There should be exactly one `system_server` process",
+                systemServerPid.matches("\\d+"));
+
+        // system_server artifacts are in the APEX data dalvik cache and names all contain
+        // the word "@classes". Look for mapped files that match this pattern in the proc map for
+        // system_server.
+        final String grepPattern = ART_APEX_DALVIK_CACHE_DIRNAME + ".*@classes";
+        return getMappedArtifacts(systemServerPid, grepPattern);
+    }
+
+    public boolean haveCompilationLog() throws Exception {
+        CommandResult result =
+                mTestInfo.getDevice().executeShellV2Command("stat " + ODREFRESH_COMPILATION_LOG);
+        return result.getExitCode() == 0;
+    }
+
+    public void removeCompilationLogToAvoidBackoff() throws Exception {
+        mTestInfo.getDevice().executeShellCommand("rm -f " + ODREFRESH_COMPILATION_LOG);
+    }
+
+    public void reboot() throws Exception {
+        mTestInfo.getDevice().reboot();
+        boolean success =
+                mTestInfo.getDevice().waitForBootComplete(BOOT_COMPLETE_TIMEOUT.toMillis());
+        assertWithMessage("Device didn't boot in %s", BOOT_COMPLETE_TIMEOUT).that(success).isTrue();
+    }
+}
diff --git a/test/odsign/test-src/com/android/tests/odsign/OnDeviceSigningHostTest.java b/test/odsign/test-src/com/android/tests/odsign/OnDeviceSigningHostTest.java
index 2e20591..ca53188 100644
--- a/test/odsign/test-src/com/android/tests/odsign/OnDeviceSigningHostTest.java
+++ b/test/odsign/test-src/com/android/tests/odsign/OnDeviceSigningHostTest.java
@@ -16,101 +16,50 @@
 
 package com.android.tests.odsign;
 
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeTrue;
 
-import android.cts.install.lib.host.InstallUtilsHost;
-
-import com.android.tradefed.device.ITestDevice.ApexInfo;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 import com.android.tradefed.testtype.junit4.AfterClassWithInfo;
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
 import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
 import com.android.tradefed.testtype.junit4.DeviceTestRunOptions;
-import com.android.tradefed.util.CommandResult;
 
-import org.junit.FixMethodOrder;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.MethodSorters;
 
-import java.time.Duration;
-import java.time.ZonedDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.Arrays;
-import java.util.HashSet;
+import java.util.List;
 import java.util.Optional;
 import java.util.Set;
-import java.util.regex.Pattern;
-import java.util.regex.Matcher;
 import java.util.stream.Collectors;
 
+/**
+ * Test to check if odrefresh, odsign, fs-verity, and ART runtime work together properly.
+ */
 @RunWith(DeviceJUnit4ClassRunner.class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
 public class OnDeviceSigningHostTest extends BaseHostJUnit4Test {
+    private static final List<String> APP_ARTIFACT_EXTENSIONS = List.of(".art", ".odex", ".vdex");
 
-    private static final String APEX_FILENAME = "test_com.android.art.apex";
-
-    private static final String ART_APEX_DALVIK_CACHE_DIRNAME =
-            "/data/misc/apexdata/com.android.art/dalvik-cache";
-
-    private static final String ODREFRESH_COMPILATION_LOG =
-            "/data/misc/odrefresh/compilation-log.txt";
-
-    private static final String CACHE_INFO_FILE = ART_APEX_DALVIK_CACHE_DIRNAME + "/cache-info.xml";
-
-    private final String[] APP_ARTIFACT_EXTENSIONS = new String[] {".art", ".odex", ".vdex"};
-
-    private final String[] BCP_ARTIFACT_EXTENSIONS = new String[] {".art", ".oat", ".vdex"};
+    private static final List<String> BCP_ARTIFACT_EXTENSIONS = List.of(".art", ".oat", ".vdex");
 
     private static final String TEST_APP_PACKAGE_NAME = "com.android.tests.odsign";
     private static final String TEST_APP_APK = "odsign_e2e_test_app.apk";
 
-    private static final Duration BOOT_COMPLETE_TIMEOUT = Duration.ofMinutes(2);
-
-    private static final String[] ZYGOTE_NAMES = new String[] {"zygote", "zygote64"};
-
-    private static InstallUtilsHost sInstallUtils;
-    private static TestInformation sTestInfo;
-
-    private static Set<String> sZygoteArtifacts;
-    private static Set<String> sSystemServerArtifacts;
+    private static OdsignTestUtils sTestUtils;
 
     @BeforeClassWithInfo
     public static void beforeClassWithDevice(TestInformation testInfo) throws Exception {
-        assertNotNull(testInfo.getDevice());
-        sInstallUtils = new InstallUtilsHost(testInfo);
-        sTestInfo = testInfo;
-
-        assumeTrue("Updating APEX is not supported", sInstallUtils.isApexUpdateSupported());
-        sInstallUtils.installApexes(APEX_FILENAME);
-        removeCompilationLogToAvoidBackoff();
-        reboot();
-
-        sZygoteArtifacts = new HashSet<>();
-        for (String zygoteName : ZYGOTE_NAMES) {
-            sZygoteArtifacts.addAll(getZygoteLoadedArtifacts(zygoteName).orElse(new HashSet<>()));
-        }
-        sSystemServerArtifacts = getSystemServerLoadedArtifacts();
+        sTestUtils = new OdsignTestUtils(testInfo);
+        sTestUtils.installTestApex();;
     }
 
     @AfterClassWithInfo
     public static void afterClassWithDevice(TestInformation testInfo) throws Exception {
-        ApexInfo apex = sInstallUtils.getApexInfo(sInstallUtils.getTestFile(APEX_FILENAME));
-        testInfo.getDevice().uninstallPackage(apex.name);
-        removeCompilationLogToAvoidBackoff();
-        reboot();
+        sTestUtils.uninstallTestApex();
     }
 
-    // Test cases starts with `testA` check if odrefresh, odsign, fs-verity, and ART runtime work
-    // together properly.
-
     @Test
-    public void testAVerifyArtUpgradeSignsFiles() throws Exception {
+    public void verifyArtUpgradeSignsFiles() throws Exception {
         installPackage(TEST_APP_APK);
         DeviceTestRunOptions options = new DeviceTestRunOptions(TEST_APP_PACKAGE_NAME);
         options.setTestClassName(TEST_APP_PACKAGE_NAME + ".ArtifactsSignedTest");
@@ -119,7 +68,7 @@
     }
 
     @Test
-    public void testAVerifyArtUpgradeGeneratesRequiredArtifacts() throws Exception {
+    public void verifyArtUpgradeGeneratesRequiredArtifacts() throws Exception {
         installPackage(TEST_APP_APK);
         DeviceTestRunOptions options = new DeviceTestRunOptions(TEST_APP_PACKAGE_NAME);
         options.setTestClassName(TEST_APP_PACKAGE_NAME + ".ArtifactsSignedTest");
@@ -127,59 +76,27 @@
         runDeviceTests(options);
     }
 
-    private static Set<String> getMappedArtifacts(String pid, String grepPattern) throws Exception {
-        final String grepCommand = String.format("grep \"%s\" /proc/%s/maps", grepPattern, pid);
-        CommandResult result = sTestInfo.getDevice().executeShellV2Command(grepCommand);
-        assertTrue(result.toString(), result.getExitCode() == 0);
-        Set<String> mappedFiles = new HashSet<>();
-        for (String line : result.getStdout().split("\\R")) {
-            int start = line.indexOf(ART_APEX_DALVIK_CACHE_DIRNAME);
-            if (line.contains("[")) {
-                continue; // ignore anonymously mapped sections which are quoted in square braces.
-            }
-            mappedFiles.add(line.substring(start));
-        }
-        return mappedFiles;
+    @Test
+    public void verifyGeneratedArtifactsLoaded() throws Exception {
+        // Checking zygote and system_server need the device have adb root to walk process maps.
+        final boolean adbEnabled = getDevice().enableAdbRoot();
+        assertTrue("ADB root failed and required to get process maps", adbEnabled);
+
+        // Check there is a compilation log, we expect compilation to have occurred.
+        assertTrue("Compilation log not found", sTestUtils.haveCompilationLog());
+
+        // Check both zygote and system_server processes to see that they have loaded the
+        // artifacts compiled and signed by odrefresh and odsign. We check both here rather than
+        // having a separate test because the device reboots between each @Test method and
+        // that is an expensive use of time.
+        verifyZygotesLoadedArtifacts();
+        verifySystemServerLoadedArtifacts();
     }
 
-    /**
-     * Returns the mapped artifacts of the Zygote process, or {@code Optional.empty()} if the
-     * process does not exist.
-     */
-    private static Optional<Set<String>> getZygoteLoadedArtifacts(String zygoteName)
-            throws Exception {
-        final CommandResult result =
-                sTestInfo.getDevice().executeShellV2Command("pidof " + zygoteName);
-        if (result.getExitCode() != 0) {
-            return Optional.empty();
-        }
-        // There may be multiple Zygote processes when Zygote just forks and has not executed any
-        // app binary. We can take any of the pids.
-        // We can't use the "-s" flag when calling `pidof` because the Toybox's `pidof`
-        // implementation is wrong and it outputs multiple pids regardless of the "-s" flag, so we
-        // split the output and take the first pid ourselves.
-        final String zygotePid = result.getStdout().trim().split("\\s+")[0];
-        assertTrue(!zygotePid.isEmpty());
-
-        final String grepPattern = ART_APEX_DALVIK_CACHE_DIRNAME + ".*boot-framework";
-        return Optional.of(getMappedArtifacts(zygotePid, grepPattern));
-    }
-
-    private static Set<String> getSystemServerLoadedArtifacts() throws Exception {
-        final CommandResult result =
-                sTestInfo.getDevice().executeShellV2Command("pidof system_server");
-        assertTrue(result.toString(), result.getExitCode() == 0);
-        final String systemServerPid = result.getStdout().trim();
-        assertTrue(!systemServerPid.isEmpty());
-        assertTrue(
-                "There should be exactly one `system_server` process",
-                systemServerPid.matches("\\d+"));
-
-        // system_server artifacts are in the APEX data dalvik cache and names all contain
-        // the word "@classes". Look for mapped files that match this pattern in the proc map for
-        // system_server.
-        final String grepPattern = ART_APEX_DALVIK_CACHE_DIRNAME + ".*@classes";
-        return getMappedArtifacts(systemServerPid, grepPattern);
+    @Test
+    public void verifyGeneratedArtifactsLoadedAfterReboot() throws Exception {
+        sTestUtils.reboot();
+        verifyGeneratedArtifactsLoaded();
     }
 
     private String[] getSystemServerClasspath() throws Exception {
@@ -201,12 +118,13 @@
         String[] classpathElements = getSystemServerClasspath();
         assertTrue("SYSTEMSERVERCLASSPATH is empty", classpathElements.length > 0);
 
-        final Set<String> mappedArtifacts = getSystemServerLoadedArtifacts();
+        final Set<String> mappedArtifacts = sTestUtils.getSystemServerLoadedArtifacts();
         assertTrue(
-                "No mapped artifacts under " + ART_APEX_DALVIK_CACHE_DIRNAME,
+                "No mapped artifacts under " + OdsignTestUtils.ART_APEX_DALVIK_CACHE_DIRNAME,
                 mappedArtifacts.size() > 0);
         final String isa = getSystemServerIsa(mappedArtifacts.iterator().next());
-        final String isaCacheDirectory = String.format("%s/%s", ART_APEX_DALVIK_CACHE_DIRNAME, isa);
+        final String isaCacheDirectory =
+                String.format("%s/%s", OdsignTestUtils.ART_APEX_DALVIK_CACHE_DIRNAME, isa);
 
         // Check components in the system_server classpath have mapped artifacts.
         for (String element : classpathElements) {
@@ -221,7 +139,7 @@
         for (String mappedArtifact : mappedArtifacts) {
           // Check the mapped artifact has a .art, .odex or .vdex extension.
           final boolean knownArtifactKind =
-              Arrays.stream(APP_ARTIFACT_EXTENSIONS).anyMatch(e -> mappedArtifact.endsWith(e));
+              APP_ARTIFACT_EXTENSIONS.stream().anyMatch(e -> mappedArtifact.endsWith(e));
           assertTrue("Unknown artifact kind: " + mappedArtifact, knownArtifactKind);
         }
     }
@@ -245,8 +163,9 @@
         // instances 32-bit and 64-bit unspecialized app_process processes.
         // (frameworks/base/cmds/app_process).
         int zygoteCount = 0;
-        for (String zygoteName : ZYGOTE_NAMES) {
-            final Optional<Set<String>> mappedArtifacts = getZygoteLoadedArtifacts(zygoteName);
+        for (String zygoteName : OdsignTestUtils.ZYGOTE_NAMES) {
+            final Optional<Set<String>> mappedArtifacts =
+                    sTestUtils.getZygoteLoadedArtifacts(zygoteName);
             if (!mappedArtifacts.isPresent()) {
                 continue;
             }
@@ -255,227 +174,4 @@
         }
         assertTrue("No zygote processes found", zygoteCount > 0);
     }
-
-    @Test
-    public void testAVerifyGeneratedArtifactsLoaded() throws Exception {
-        // Checking zygote and system_server need the device have adb root to walk process maps.
-        final boolean adbEnabled = getDevice().enableAdbRoot();
-        assertTrue("ADB root failed and required to get process maps", adbEnabled);
-
-        // Check there is a compilation log, we expect compilation to have occurred.
-        assertTrue("Compilation log not found", haveCompilationLog());
-
-        // Check both zygote and system_server processes to see that they have loaded the
-        // artifacts compiled and signed by odrefresh and odsign. We check both here rather than
-        // having a separate test because the device reboots between each @Test method and
-        // that is an expensive use of time.
-        verifyZygotesLoadedArtifacts();
-        verifySystemServerLoadedArtifacts();
-    }
-
-    @Test
-    public void testAVerifyGeneratedArtifactsLoadedAfterReboot() throws Exception {
-        reboot();
-        testAVerifyGeneratedArtifactsLoaded();
-    }
-
-    /**
-     * Checks the input line by line and replaces all lines that match the regex with the given
-     * replacement.
-     */
-    String replaceLine(String input, String regex, String replacement) {
-        StringBuffer output = new StringBuffer();
-        Pattern p = Pattern.compile(regex);
-        for (String line : input.split("\n")) {
-            Matcher m = p.matcher(line);
-            if (m.matches()) {
-                m.appendReplacement(output, replacement);
-                output.append("\n");
-            } else {
-                output.append(line + "\n");
-            }
-        }
-        return output.toString();
-    }
-
-    /**
-     * Simulates that there is an OTA that updates a boot classpath jar.
-     */
-    void simulateBootClasspathOta() throws Exception {
-        String cacheInfo = getDevice().pullFileContents(CACHE_INFO_FILE);
-        // Replace the cached checksum of /system/framework/framework.jar with "aaaaaaaa".
-        cacheInfo = replaceLine(
-                cacheInfo,
-                "(.*/system/framework/framework\\.jar.*checksums=\").*?(\".*)",
-                "$1aaaaaaaa$2");
-        getDevice().pushString(cacheInfo, CACHE_INFO_FILE);
-    }
-
-    /**
-     * Simulates that there is an OTA that updates a system server jar.
-     */
-    void simulateSystemServerOta() throws Exception {
-        String cacheInfo = getDevice().pullFileContents(CACHE_INFO_FILE);
-        // Replace the cached checksum of /system/framework/services.jar with "aaaaaaaa".
-        cacheInfo = replaceLine(
-                cacheInfo,
-                "(.*/system/framework/services\\.jar.*checksums=\").*?(\".*)",
-                "$1aaaaaaaa$2");
-        getDevice().pushString(cacheInfo, CACHE_INFO_FILE);
-    }
-
-    /**
-     * Simulates that an ART APEX has been upgraded.
-     */
-    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.
-     */
-    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);
-    }
-
-    long parseFormattedDateTime(String dateTimeStr) throws Exception {
-        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(
-                "yyyy-MM-dd HH:mm:ss.nnnnnnnnn Z");
-        ZonedDateTime zonedDateTime = ZonedDateTime.parse(dateTimeStr, formatter);
-        return zonedDateTime.toInstant().toEpochMilli();
-    }
-
-    long getModifiedTimeMs(String filename) throws Exception {
-        // We can't use the "-c '%.3Y'" flag when to get the timestamp because the Toybox's `stat`
-        // implementation truncates the timestamp to seconds, which is not accurate enough, so we
-        // use "-c '%%y'" and parse the time ourselves.
-        String dateTimeStr = getDevice()
-                .executeShellCommand(String.format("stat -c '%%y' '%s'", filename))
-                .trim();
-        return parseFormattedDateTime(dateTimeStr);
-    }
-
-    long getCurrentTimeMs() throws Exception {
-        // We can't use getDevice().getDeviceDate() because it truncates the timestamp to seconds,
-        // which is not accurate enough.
-        String dateTimeStr = getDevice()
-                .executeShellCommand("date +'%Y-%m-%d %H:%M:%S.%N %z'")
-                .trim();
-        return parseFormattedDateTime(dateTimeStr);
-    }
-
-    void assertArtifactsModifiedAfter(Set<String> artifacts, long timeMs) throws Exception {
-        for (String artifact : artifacts) {
-            long modifiedTime = getModifiedTimeMs(artifact);
-            assertTrue(
-                    String.format(
-                            "Artifact %s is not re-compiled. Modified time: %d, Reference time: %d",
-                            artifact,
-                            modifiedTime,
-                            timeMs),
-                    modifiedTime > timeMs);
-        }
-    }
-
-    void assertArtifactsNotModifiedAfter(Set<String> artifacts, long timeMs) throws Exception {
-        for (String artifact : artifacts) {
-            long modifiedTime = getModifiedTimeMs(artifact);
-            assertTrue(
-                    String.format(
-                            "Artifact %s is unexpectedly re-compiled. " +
-                                    "Modified time: %d, Reference time: %d",
-                            artifact,
-                            modifiedTime,
-                            timeMs),
-                    modifiedTime < timeMs);
-        }
-    }
-
-    // Test cases starts with `testB` check end-to-end odrefresh invocations, but without odsign,
-    // fs-verity, and ART runtime involved. Do not add tests after `testB*` cases that check
-    // fs-verity or runtime behaviors.
-
-    @Test
-    public void testBVerifyArtSamegradeUpdateTriggersCompilation() throws Exception {
-        simulateArtApexUpgrade();
-        removeCompilationLogToAvoidBackoff();
-        long timeMs = getCurrentTimeMs();
-        getDevice().executeShellV2Command("odrefresh --compile");
-
-        assertArtifactsModifiedAfter(sZygoteArtifacts, timeMs);
-        assertArtifactsModifiedAfter(sSystemServerArtifacts, timeMs);
-    }
-
-    @Test
-    public void testBVerifyOtherApexSamegradeUpdateTriggersCompilation() throws Exception {
-        simulateApexUpgrade();
-        removeCompilationLogToAvoidBackoff();
-        long timeMs = getCurrentTimeMs();
-        getDevice().executeShellV2Command("odrefresh --compile");
-
-        assertArtifactsNotModifiedAfter(sZygoteArtifacts, timeMs);
-        assertArtifactsModifiedAfter(sSystemServerArtifacts, timeMs);
-    }
-
-    @Test
-    public void testBVerifyBootClasspathOtaTriggersCompilation() throws Exception {
-        simulateBootClasspathOta();
-        removeCompilationLogToAvoidBackoff();
-        long timeMs = getCurrentTimeMs();
-        getDevice().executeShellV2Command("odrefresh --compile");
-
-        assertArtifactsModifiedAfter(sZygoteArtifacts, timeMs);
-        assertArtifactsModifiedAfter(sSystemServerArtifacts, timeMs);
-    }
-
-    @Test
-    public void testBVerifySystemServerOtaTriggersCompilation() throws Exception {
-        simulateSystemServerOta();
-        removeCompilationLogToAvoidBackoff();
-        long timeMs = getCurrentTimeMs();
-        getDevice().executeShellV2Command("odrefresh --compile");
-
-        assertArtifactsNotModifiedAfter(sZygoteArtifacts, timeMs);
-        assertArtifactsModifiedAfter(sSystemServerArtifacts, timeMs);
-    }
-
-    @Test
-    public void testBVerifyNoCompilationWhenCacheIsGood() throws Exception {
-        removeCompilationLogToAvoidBackoff();
-        long timeMs = getCurrentTimeMs();
-        getDevice().executeShellV2Command("odrefresh --compile");
-
-        assertArtifactsNotModifiedAfter(sZygoteArtifacts, timeMs);
-        assertArtifactsNotModifiedAfter(sSystemServerArtifacts, timeMs);
-    }
-
-    private boolean haveCompilationLog() throws Exception {
-        CommandResult result =
-                getDevice().executeShellV2Command("stat " + ODREFRESH_COMPILATION_LOG);
-        return result.getExitCode() == 0;
-    }
-
-    private static void removeCompilationLogToAvoidBackoff() throws Exception {
-        sTestInfo.getDevice().executeShellCommand("rm -f " + ODREFRESH_COMPILATION_LOG);
-    }
-
-    private static void reboot() throws Exception {
-        sTestInfo.getDevice().reboot();
-        boolean success =
-                sTestInfo.getDevice().waitForBootComplete(BOOT_COMPLETE_TIMEOUT.toMillis());
-        assertWithMessage("Device didn't boot in %s", BOOT_COMPLETE_TIMEOUT).that(success).isTrue();
-    }
 }