iorap_functional_test: Add iorap function test.

Changes:
* Check logcat instead of db to confirm each step.
* Include version invalidation.

Bug: 152443508
Bug: 148936893
Test: atest iorap-functional-tests on cuttlefish.
Change-Id: I27e8fc8705889a04cb6f38295b64a25c1aceb742
diff --git a/startop/iorap/functional_tests/Android.bp b/startop/iorap/functional_tests/Android.bp
index ce9dc32..ad85f14 100644
--- a/startop/iorap/functional_tests/Android.bp
+++ b/startop/iorap/functional_tests/Android.bp
@@ -15,6 +15,7 @@
 android_test {
     name: "iorap-functional-tests",
     srcs: ["src/**/*.java"],
+    data: ["test_data/*"],
     static_libs: [
         // Non-test dependencies
         // library under test
diff --git a/startop/iorap/functional_tests/AndroidTest.xml b/startop/iorap/functional_tests/AndroidTest.xml
index 3d5a229..31d4f6c 100644
--- a/startop/iorap/functional_tests/AndroidTest.xml
+++ b/startop/iorap/functional_tests/AndroidTest.xml
@@ -45,6 +45,20 @@
         <option name="run-command" value="sleep 1" />
     </target_preparer>
 
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="cleanup" value="true" />
+        <option name="abort-on-push-failure" value="true" />
+        <option name="push-file"
+          key="iorap_test_app_v1.apk"
+          value="/data/misc/iorapd/iorap_test_app_v1.apk" />
+        <option name="push-file"
+          key="iorap_test_app_v2.apk"
+          value="/data/misc/iorapd/iorap_test_app_v2.apk" />
+        <option name="push-file"
+          key="iorap_test_app_v3.apk"
+          value="/data/misc/iorapd/iorap_test_app_v3.apk" />
+    </target_preparer>
+
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="com.google.android.startop.iorap.tests" />
         <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
diff --git a/startop/iorap/functional_tests/src/com/google/android/startop/iorap/IorapWorkFlowTest.java b/startop/iorap/functional_tests/src/com/google/android/startop/iorap/IorapWorkFlowTest.java
index 9abbcd7..c35dd3b 100644
--- a/startop/iorap/functional_tests/src/com/google/android/startop/iorap/IorapWorkFlowTest.java
+++ b/startop/iorap/functional_tests/src/com/google/android/startop/iorap/IorapWorkFlowTest.java
@@ -37,45 +37,46 @@
 import androidx.test.uiautomator.UiDevice;
 import androidx.test.uiautomator.Until;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.concurrent.TimeUnit;
+import java.util.Date;
 import java.util.function.BooleanSupplier;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.List;
-
+import java.text.SimpleDateFormat;
 
 /**
  * Test for the work flow of iorap.
  *
- * <p> This test tests the function of iorap from perfetto collection -> compilation ->
- * prefetching.
- * </p>
+ * <p> This test tests the function of iorap from:
+ * perfetto collection -> compilation ->  prefetching -> version update -> perfetto collection.
  */
 @RunWith(AndroidJUnit4.class)
 public class IorapWorkFlowTest {
-
   private static final String TAG = "IorapWorkFlowTest";
 
-  private static final String TEST_PACKAGE_NAME = "com.android.settings";
-  private static final String TEST_ACTIVITY_NAME = "com.android.settings.Settings";
+  private static final String TEST_APP_VERSION_ONE_PATH = "/data/misc/iorapd/iorap_test_app_v1.apk";
+  private static final String TEST_APP_VERSION_TWO_PATH = "/data/misc/iorapd/iorap_test_app_v2.apk";
+  private static final String TEST_APP_VERSION_THREE_PATH = "/data/misc/iorapd/iorap_test_app_v3.apk";
 
   private static final String DB_PATH = "/data/misc/iorapd/sqlite.db";
   private static final Duration TIMEOUT = Duration.ofSeconds(300L);
 
-  private static final String READAHEAD_INDICATOR =
-      "Description = /data/misc/iorapd/com.android.settings/-?\\d+/com.android.settings.Settings/compiled_traces/compiled_trace.pb";
-
   private UiDevice mDevice;
 
   @Before
-  public void startMainActivityFromHomeScreen() throws Exception {
+  public void setUp() throws Exception {
     // Initialize UiDevice instance
     mDevice = UiDevice.getInstance(getInstrumentation());
 
@@ -88,21 +89,81 @@
     mDevice.wait(Until.hasObject(By.pkg(launcherPackage).depth(0)), TIMEOUT.getSeconds());
   }
 
+  @After
+  public void tearDown() throws Exception {
+    String packageName = "com.example.ioraptestapp";
+    uninstallApk(packageName);
+  }
+
   @Test (timeout = 300000)
-  public void testApp() throws Exception {
+  public void testNormalWorkFlow() throws Exception {
     assertThat(mDevice, notNullValue());
 
+    // Install test app version one
+    installApk(TEST_APP_VERSION_ONE_PATH);
+    String packageName = "com.example.ioraptestapp";
+    String activityName = "com.example.ioraptestapp.MainActivity";
+
     // Perfetto trace collection phase.
-    assertTrue(startAppForPerfettoTrace(/*expectPerfettoTraceCount=*/1));
-    assertTrue(startAppForPerfettoTrace(/*expectPerfettoTraceCount=*/2));
-    assertTrue(startAppForPerfettoTrace(/*expectPerfettoTraceCount=*/3));
-    assertTrue(checkPerfettoTracesExistence(TIMEOUT, 3));
+    assertTrue(startAppForPerfettoTrace(
+        packageName, activityName, /*version=*/1L));
+    assertTrue(startAppForPerfettoTrace(
+        packageName, activityName, /*version=*/1L));
+    assertTrue(startAppForPerfettoTrace(
+        packageName, activityName, /*version=*/1L));
 
     // Trigger maintenance service for compilation.
-    assertTrue(compile(TIMEOUT));
+    TimeUnit.SECONDS.sleep(5L);
+    assertTrue(compile(packageName, activityName, /*version=*/1L));
 
-    // Check if prefetching works.
-    assertTrue(waitForPrefetchingFromLogcat(/*expectPerfettoTraceCount=*/3));
+    // Run app with prefetching
+    assertTrue(startAppWithCompiledTrace(
+        packageName, activityName, /*version=*/1L));
+  }
+
+  @Test (timeout = 300000)
+  public void testUpdateApp() throws Exception {
+    assertThat(mDevice, notNullValue());
+
+    // Install test app version two,
+    String packageName = "com.example.ioraptestapp";
+    String activityName = "com.example.ioraptestapp.MainActivity";
+    installApk(TEST_APP_VERSION_TWO_PATH);
+
+    // Perfetto trace collection phase.
+    assertTrue(startAppForPerfettoTrace(
+        packageName, activityName, /*version=*/2L));
+    assertTrue(startAppForPerfettoTrace(
+        packageName, activityName, /*version=*/2L));
+    assertTrue(startAppForPerfettoTrace(
+        packageName, activityName, /*version=*/2L));
+
+    // Trigger maintenance service for compilation.
+    TimeUnit.SECONDS.sleep(5L);
+    assertTrue(compile(packageName, activityName, /*version=*/2L));
+
+    // Run app with prefetching
+    assertTrue(startAppWithCompiledTrace(
+        packageName, activityName, /*version=*/2L));
+
+    // Update test app to version 3
+    installApk(TEST_APP_VERSION_THREE_PATH);
+
+    // Rerun app, should do pefetto tracing.
+    assertTrue(startAppForPerfettoTrace(
+        packageName, activityName, /*version=*/3L));
+  }
+
+  private static void installApk(String apkPath) throws Exception {
+    // Disable the selinux to allow pm install apk in the dir.
+    executeShellCommand("setenforce 0");
+    executeShellCommand("pm install -r -d " + apkPath);
+    executeShellCommand("setenforce 1");
+
+  }
+
+  private static void uninstallApk(String apkPath) throws Exception {
+    executeShellCommand("pm uninstall " + apkPath);
   }
 
   /**
@@ -110,43 +171,81 @@
    *
    * @param expectPerfettoTraceCount is the expected count of perfetto traces.
    */
-  private boolean startAppForPerfettoTrace(long expectPerfettoTraceCount)
+  private boolean startAppForPerfettoTrace(
+      String packageName, String activityName, long version)
       throws Exception {
-    // Close the specified app if it's open
-    closeApp();
-    // Launch the specified app
-    startApp();
-    // Wait for the app to appear
-    mDevice.wait(Until.hasObject(By.pkg(TEST_PACKAGE_NAME).depth(0)), TIMEOUT.getSeconds());
+    LogcatTimestamp timestamp = runAppOnce(packageName, activityName);
+    return waitForPerfettoTraceSavedFromLogcat(
+        packageName, activityName, version, timestamp);
+  }
 
-    String sql = "SELECT COUNT(*) FROM activities "
-        + "JOIN app_launch_histories ON activities.id = app_launch_histories.activity_id "
-        + "JOIN raw_traces ON raw_traces.history_id = app_launch_histories.id "
-        + "WHERE activities.name = ?";
-    return checkAndWaitEntriesNum(sql, new String[]{TEST_ACTIVITY_NAME}, expectPerfettoTraceCount,
-        TIMEOUT);
+  private boolean startAppWithCompiledTrace(
+      String packageName, String activityName, long version)
+      throws Exception {
+    LogcatTimestamp timestamp = runAppOnce(packageName, activityName);
+    return waitForPrefetchingFromLogcat(
+        packageName, activityName, version, timestamp);
+  }
+
+  private LogcatTimestamp runAppOnce(String packageName, String activityName) throws Exception {
+    // Close the specified app if it's open
+    closeApp(packageName);
+    LogcatTimestamp timestamp = new LogcatTimestamp();
+    // Launch the specified app
+    startApp(packageName, activityName);
+    // Wait for the app to appear
+    mDevice.wait(Until.hasObject(By.pkg(packageName).depth(0)), TIMEOUT.getSeconds());
+    return timestamp;
   }
 
   // Invokes the maintenance to compile the perfetto traces to compiled trace.
-  private boolean compile(Duration timeout) throws Exception {
+  private boolean compile(
+      String packageName, String activityName, long version) throws Exception {
     // The job id (283673059) is defined in class IorapForwardingService.
-    executeShellCommand("cmd jobscheduler run -f android 283673059");
+    executeShellCommandViaTmpFile("cmd jobscheduler run -f android 283673059");
+    return waitForFileExistence(getCompiledTracePath(packageName, activityName, version));
+  }
 
-    // Wait for the compilation.
-    String sql = "SELECT COUNT(*) FROM activities JOIN prefetch_files ON "
-        + "activities.id = prefetch_files.activity_id "
-        + "WHERE activities.name = ?";
-    boolean result = checkAndWaitEntriesNum(sql, new String[]{TEST_ACTIVITY_NAME}, /*count=*/1,
-        timeout);
-    if (!result) {
-      return false;
+  private String getCompiledTracePath(
+      String packageName, String activityName, long version) {
+    return String.format(
+        "/data/misc/iorapd/%s/%d/%s/compiled_traces/compiled_trace.pb",
+        packageName, version, activityName);
+  }
+
+  /**
+   * Starts the testing app.
+   */
+  private void startApp(String packageName, String activityName) throws Exception {
+    executeShellCommandViaTmpFile(
+        String.format("am start %s/%s", packageName, activityName));
+  }
+
+  /**
+   * Closes the testing app.
+   * <p> Keep trying to kill the process of the app until no process of the app package
+   * appears.</p>
+   */
+  private void closeApp(String packageName) throws Exception {
+    while (true) {
+      String pid = executeShellCommand("pidof " + packageName);
+      if (pid.isEmpty()) {
+        Log.i(TAG, "Closed app " + packageName);
+        return;
+      }
+      executeShellCommand("kill -9 " + pid);
+      TimeUnit.SECONDS.sleep(1L);
     }
+  }
 
-    return retryWithTimeout(timeout, () -> {
+  /** Waits for a file to appear. */
+  private boolean waitForFileExistence(String fileName) throws Exception {
+    return retryWithTimeout(TIMEOUT, () -> {
       try {
-        String compiledTrace = getCompiledTraceFilePath();
-        File compiledTraceLocal = copyFileToLocal(compiledTrace, "compiled_trace.tmp");
-        return compiledTraceLocal.exists();
+        String fileExists = executeShellCommandViaTmpFile(
+            String.format("test -f %s; echo $?", fileName));
+        Log.i(TAG, fileName + " existence is " +  fileExists);
+        return fileExists.trim().equals("0");
       } catch (Exception e) {
         Log.i(TAG, e.getMessage());
         return false;
@@ -154,92 +253,96 @@
     });
   }
 
-  /**
-   * Check if all the perfetto traces in the db exist.
-   */
-  private boolean checkPerfettoTracesExistence(Duration timeout, int expectPerfettoTraceCount)
+  /** Waits for the perfetto trace saved message from logcat. */
+  private boolean waitForPerfettoTraceSavedFromLogcat(
+      String packageName, String activityName, long version, LogcatTimestamp timestamp)
       throws Exception {
-    return retryWithTimeout(timeout, () -> {
-      try {
-        File dbFile = getIorapDb();
-        List<String> traces = getPerfettoTracePaths(dbFile);
-        assertEquals(traces.size(), expectPerfettoTraceCount);
+    Pattern p = Pattern.compile(".*"
+        + getPerfettoTraceSavedIndicator(packageName, activityName, version)
+        + "(.*[.]perfetto_trace[.]pb)\n.*", Pattern.DOTALL);
 
-        int count = 0;
-        for (String trace : traces) {
-          File tmp = copyFileToLocal(trace, "perfetto_trace.tmp" + count);
-          ++count;
-          Log.i(TAG, "Check perfetto trace: " + trace);
-          if (!tmp.exists()) {
-            Log.i(TAG, "Perfetto trace does not exist: " + trace);
-            return false;
-          }
+    return retryWithTimeout(TIMEOUT, () -> {
+      try {
+        String log = timestamp.getLogcatAfter();
+        Matcher m = p.matcher(log);
+        Log.d(TAG, "Tries to find perfetto trace...");
+        if (!m.matches()) {
+          Log.i(TAG, "Cannot find perfetto trace saved in log.");
+          return false;
         }
+        String filePath = m.group(1);
+        Log.i(TAG, "Perfetto trace is saved to " + filePath);
         return true;
-      } catch (Exception e) {
-        Log.i(TAG, e.getMessage());
+      } catch(Exception e) {
+        Log.e(TAG, e.getMessage());
         return false;
       }
-    });
+   });
+  }
+
+  private String getPerfettoTraceSavedIndicator(
+      String packageName, String activityName, long version) {
+    return String.format(
+        "Perfetto TraceBuffer saved to file: /data/misc/iorapd/%s/%d/%s/raw_traces/",
+        packageName, version, activityName);
   }
 
   /**
-   * Gets the perfetto traces file path from the db.
-   */
-  private List<String> getPerfettoTracePaths(File dbFile) throws Exception {
-    String sql = "SELECT raw_traces.file_path FROM activities "
-        + "JOIN app_launch_histories ON activities.id = app_launch_histories.activity_id "
-        + "JOIN raw_traces ON raw_traces.history_id = app_launch_histories.id "
-        + "WHERE activities.name = ?";
-
-    List<String> perfettoTraces = new ArrayList<>();
-    try (SQLiteDatabase db = SQLiteDatabase
-        .openDatabase(dbFile.getPath(), null, SQLiteDatabase.OPEN_READONLY)) {
-      Cursor cursor = db.rawQuery(sql, new String[]{TEST_ACTIVITY_NAME});
-      while (cursor.moveToNext()) {
-        perfettoTraces.add(cursor.getString(0));
-      }
-    }
-    return perfettoTraces;
-  }
-
-  private String getCompiledTraceFilePath() throws Exception {
-    File dbFile = getIorapDb();
-    try (SQLiteDatabase db = SQLiteDatabase
-        .openDatabase(dbFile.getPath(), null, SQLiteDatabase.OPEN_READONLY)) {
-      String sql = "SELECT prefetch_files.file_path FROM activities JOIN prefetch_files ON "
-              + "activities.id = prefetch_files.activity_id "
-              + "WHERE activities.name = ?";
-      return DatabaseUtils.stringForQuery(db, sql, new String[]{TEST_ACTIVITY_NAME});
-    }
-  }
-
-  /**
-   * Checks the number of entries in the database table.
+   * Waits for the prefetching log in the logcat.
    *
-   * <p> Keep checking until the timeout.
+   * <p> When prefetching works, the perfetto traces should not be collected. </p>
    */
-  private boolean checkAndWaitEntriesNum(String sql, String[] selectionArgs, long count,
-      Duration timeout)
+  private boolean waitForPrefetchingFromLogcat(
+      String packageName, String activityName, long version, LogcatTimestamp timestamp)
       throws Exception {
-    return retryWithTimeout(timeout, () -> {
+    Pattern p = Pattern.compile(
+    ".*" + getReadaheadIndicator(packageName, activityName, version) +
+    ".*Total File Paths=(\\d+) \\(good: (\\d+[.]?\\d*)%\\)\n"
+        + ".*Total Entries=(\\d+) \\(good: (\\d+[.]?\\d*)%\\)\n"
+        + ".*Total Bytes=(\\d+) \\(good: (\\d+[.]?\\d*)%\\).*",
+    Pattern.DOTALL);
+
+    return retryWithTimeout(TIMEOUT, () -> {
       try {
-        File db = getIorapDb();
-        long curCount = getEntriesNum(db, selectionArgs, sql);
-        Log.i(TAG, String
-            .format("For %s, current count is %d, expected count is :%d.", sql, curCount,
-                count));
-        return curCount == count;
-      } catch (Exception e) {
-        Log.i(TAG, e.getMessage());
+        String log = timestamp.getLogcatAfter();
+        Matcher m = p.matcher(log);
+        if (!m.matches()) {
+          Log.i(TAG, "Cannot find readahead log.");
+          return false;
+        }
+
+        int totalFilePath = Integer.parseInt(m.group(1));
+        float totalFilePathGoodRate = Float.parseFloat(m.group(2)) / 100;
+        int totalEntries = Integer.parseInt(m.group(3));
+        float totalEntriesGoodRate = Float.parseFloat(m.group(4)) / 100;
+        int totalBytes = Integer.parseInt(m.group(5));
+        float totalBytesGoodRate = Float.parseFloat(m.group(6)) / 100;
+
+        Log.i(TAG, String.format(
+            "totalFilePath: %d (good %.2f) totalEntries: %d (good %.2f) totalBytes: %d (good %.2f)",
+            totalFilePath, totalFilePathGoodRate, totalEntries, totalEntriesGoodRate, totalBytes,
+            totalBytesGoodRate));
+
+        return totalFilePath > 0 &&
+          totalEntries > 0 &&
+          totalBytes > 0 &&
+          totalFilePathGoodRate > 0.5 &&
+          totalEntriesGoodRate > 0.5 &&
+          totalBytesGoodRate > 0.5;
+      } catch(Exception e) {
         return false;
       }
-    });
+   });
   }
 
-  /**
-   * Retry until timeout.
-   */
+  private static String getReadaheadIndicator(
+      String packageName, String activityName, long version) {
+    return String.format(
+        "Description = /data/misc/iorapd/%s/%d/%s/compiled_traces/compiled_trace.pb",
+        packageName, version, activityName);
+  }
+
+  /** Retry until timeout. */
   private boolean retryWithTimeout(Duration timeout, BooleanSupplier supplier) throws Exception {
     long totalSleepTimeSeconds = 0L;
     long sleepIntervalSeconds = 2L;
@@ -256,113 +359,28 @@
   }
 
   /**
-   * Gets the number of entries in the query of sql.
-   */
-  private long getEntriesNum(File dbFile, String[] selectionArgs, String sql) throws Exception {
-    try (SQLiteDatabase db = SQLiteDatabase
-        .openDatabase(dbFile.getPath(), null, SQLiteDatabase.OPEN_READONLY)) {
-      return DatabaseUtils.longForQuery(db, sql, selectionArgs);
-    }
-  }
-
-  /**
-   * Gets the iorapd sqlite db file.
+   * Executes command in adb shell via a tmp file.
    *
-   * <p> The test cannot access the db file directly under "/data/misc/iorapd".
-   * Copy it to the local directory and change the mode.
+   * <p> This should be run as root.</p>
    */
-  private File getIorapDb() throws Exception {
-    File tmpDb = copyFileToLocal("/data/misc/iorapd/sqlite.db", "tmp.db");
-    // Change the mode of the file to allow the access from test.
-    executeShellCommand("chmod 777 " + tmpDb.getPath());
-    return tmpDb;
-  }
-
-  /**
-   * Copys a file to local directory.
-   */
-  private File copyFileToLocal(String src, String tgtFileName) throws Exception {
-    File localDir = getApplicationContext().getDir(this.getClass().getName(), Context.MODE_PRIVATE);
-    File localFile = new File(localDir, tgtFileName);
-    executeShellCommand(String.format("cp %s %s", src, localFile.getPath()));
-    return localFile;
-  }
-
-  /**
-   * Starts the testing app.
-   */
-  private void startApp() throws Exception {
-    Context context = getApplicationContext();
-    final Intent intent = context.getPackageManager()
-        .getLaunchIntentForPackage(TEST_PACKAGE_NAME);
-    context.startActivity(intent);
-    Log.i(TAG, "Started app " + TEST_PACKAGE_NAME);
-  }
-
-  /**
-   * Closes the testing app.
-   * <p> Keep trying to kill the process of the app until no process of the app package
-   * appears.</p>
-   */
-  private void closeApp() throws Exception {
-    while (true) {
-      String pid = executeShellCommand("pidof " + TEST_PACKAGE_NAME);
-      if (pid.isEmpty()) {
-        Log.i(TAG, "Closed app " + TEST_PACKAGE_NAME);
-        return;
+  private static String executeShellCommandViaTmpFile(String cmd) throws Exception {
+    Log.i(TAG, "Execute via tmp file: " + cmd);
+    Path tmp = null;
+    try {
+      tmp = Files.createTempFile(/*prefix=*/null, /*suffix=*/".sh");
+      Files.write(tmp, cmd.getBytes(StandardCharsets.UTF_8));
+      tmp.toFile().setExecutable(true);
+      return UiDevice.getInstance(
+          InstrumentationRegistry.getInstrumentation()).
+          executeShellCommand(tmp.toString());
+    } finally {
+      if (tmp != null) {
+        Files.delete(tmp);
       }
-      executeShellCommand("kill -9 " + pid);
-      TimeUnit.SECONDS.sleep(1L);
     }
   }
 
   /**
-   * Waits for the prefetching log in the logcat.
-   *
-   * <p> When prefetching works, the perfetto traces should not be collected. </p>
-   */
-  private boolean waitForPrefetchingFromLogcat(long expectPerfettoTraceCount) throws Exception {
-    if (!startAppForPerfettoTrace(expectPerfettoTraceCount)) {
-      return false;
-    }
-
-    String log = executeShellCommand("logcat -d");
-
-    Pattern p = Pattern.compile(
-    ".*" + READAHEAD_INDICATOR
-        + ".*Total File Paths=(\\d+) \\(good: (\\d+[.]?\\d*)%\\)\n"
-        + ".*Total Entries=(\\d+) \\(good: (\\d+[.]?\\d*)%\\)\n"
-        + ".*Total Bytes=(\\d+) \\(good: (\\d+[.]?\\d*)%\\).*",
-    Pattern.DOTALL);
-    Matcher m = p.matcher(log);
-
-    if (!m.matches()) {
-      Log.i(TAG, "Cannot find readahead log.");
-      return false;
-    }
-
-    int totalFilePath = Integer.parseInt(m.group(1));
-    float totalFilePathGoodRate = Float.parseFloat(m.group(2)) / 100;
-    int totalEntries = Integer.parseInt(m.group(3));
-    float totalEntriesGoodRate = Float.parseFloat(m.group(4)) / 100;
-    int totalBytes = Integer.parseInt(m.group(5));
-    float totalBytesGoodRate = Float.parseFloat(m.group(6)) / 100;
-
-    Log.i(TAG, String.format(
-        "totalFilePath: %d (good %.2f) totalEntries: %d (good %.2f) totalBytes: %d (good %.2f)",
-        totalFilePath, totalFilePathGoodRate, totalEntries, totalEntriesGoodRate, totalBytes,
-        totalBytesGoodRate));
-
-    return totalFilePath > 0 &&
-        totalEntries > 0 &&
-        totalBytes > 100000 &&
-        totalFilePathGoodRate > 0.5 &&
-        totalEntriesGoodRate > 0.5 &&
-        totalBytesGoodRate > 0.5;
-  }
-
-
-  /**
    * Executes command in adb shell.
    *
    * <p> This should be run as root.</p>
@@ -372,6 +390,27 @@
     return UiDevice.getInstance(
         InstrumentationRegistry.getInstrumentation()).executeShellCommand(cmd);
   }
-}
 
+  static class LogcatTimestamp {
+    private String epochTime;
+
+    public LogcatTimestamp() throws Exception{
+      long currentTimeMillis = System.currentTimeMillis();
+      epochTime = String.format(
+          "%d.%d", currentTimeMillis/1000, currentTimeMillis%1000);
+      Log.i(TAG, "Current logcat timestamp is " + epochTime);
+    }
+
+    // For example, 1585264100.000
+    public String getEpochTime() {
+      return epochTime;
+    }
+
+    // Gets the logcat after this epoch time.
+    public String getLogcatAfter() throws Exception {
+      return executeShellCommandViaTmpFile(
+          "logcat -v epoch -t '" + epochTime + "'");
+    }
+  }
+}
 
diff --git a/startop/iorap/functional_tests/test_data/iorap_test_app_v1.apk b/startop/iorap/functional_tests/test_data/iorap_test_app_v1.apk
new file mode 120000
index 0000000..1c1a437
--- /dev/null
+++ b/startop/iorap/functional_tests/test_data/iorap_test_app_v1.apk
@@ -0,0 +1 @@
+../../../../../../packages/modules/ArtPrebuilt/iorap/test/iorap_test_app_v1.apk
\ No newline at end of file
diff --git a/startop/iorap/functional_tests/test_data/iorap_test_app_v2.apk b/startop/iorap/functional_tests/test_data/iorap_test_app_v2.apk
new file mode 120000
index 0000000..7cd41c4
--- /dev/null
+++ b/startop/iorap/functional_tests/test_data/iorap_test_app_v2.apk
@@ -0,0 +1 @@
+../../../../../../packages/modules/ArtPrebuilt/iorap/test/iorap_test_app_v2.apk
\ No newline at end of file
diff --git a/startop/iorap/functional_tests/test_data/iorap_test_app_v3.apk b/startop/iorap/functional_tests/test_data/iorap_test_app_v3.apk
new file mode 120000
index 0000000..7f4e996
--- /dev/null
+++ b/startop/iorap/functional_tests/test_data/iorap_test_app_v3.apk
@@ -0,0 +1 @@
+../../../../../../packages/modules/ArtPrebuilt/iorap/test/iorap_test_app_v3.apk
\ No newline at end of file