Add BootTimeTest separate module

Test: make BootTimeTest && tradefed.sh run commandAndExit
/ssddrive/android/main/platform_testing/tests/automotive/health/boottime/AndroidTest.xml

Bug: 288323866

Change-Id: Ic89ee8852d4b47673f52a79a1086b6725dcf4865
Merged-In: Ic89ee8852d4b47673f52a79a1086b6725dcf4865
(cherry picked from commit 3b3b77f34a22a39df0418d840c1d03adc702e659)
diff --git a/tests/automotive/health/boottime/Android.bp b/tests/automotive/health/boottime/Android.bp
new file mode 100644
index 0000000..8ec7739
--- /dev/null
+++ b/tests/automotive/health/boottime/Android.bp
@@ -0,0 +1,26 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_host {
+    name: "BootTimeTest",
+    srcs: [ "src/**/*.java" ],
+    libs: [
+        "tradefed",
+        "loganalysis",
+    ],
+}
diff --git a/tests/automotive/health/boottime/AndroidTest.xml b/tests/automotive/health/boottime/AndroidTest.xml
new file mode 100644
index 0000000..d64ad4d
--- /dev/null
+++ b/tests/automotive/health/boottime/AndroidTest.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<configuration description="Boot Time Test">
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer" />
+    <!-- Needed to set perfetto trace property before fastboot commands -->
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer" />
+    <target_preparer class="com.android.tradefed.targetprep.FastbootCommandPreparer" />
+    <!-- Needed multiple run command target preparer for running commands before/after install. -->
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer" />
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup" />
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer" />
+    <target_preparer class="com.android.tradefed.targetprep.InstrumentationPreparer" >
+        <option name="disable" value="true" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.HostTest" >
+        <option name="class" value="android.boottime.BootTimeTest" />
+    </test>
+    <metrics_collector class="com.android.tradefed.device.metric.AtraceCollector"/>
+    <metrics_collector class="com.android.tradefed.device.metric.PerfettoPullerMetricCollector">
+        <option name="collect-on-run-ended-only" value="false" />
+    </metrics_collector>
+    <metric_post_processor class="com.android.tradefed.postprocessor.PerfettoGenericPostProcessor" />
+    <metric_post_processor class="com.android.tradefed.postprocessor.MetricFilePostProcessor">
+        <option name="aggregate-similar-tests" value="true" />
+    </metric_post_processor>
+</configuration>
diff --git a/tests/automotive/health/boottime/src/android/boottime/BootTimeTest.java b/tests/automotive/health/boottime/src/android/boottime/BootTimeTest.java
new file mode 100644
index 0000000..69beeff
--- /dev/null
+++ b/tests/automotive/health/boottime/src/android/boottime/BootTimeTest.java
@@ -0,0 +1,229 @@
+/*
+ * 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 android.boottime;
+
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.LogcatReceiver;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.FileInputStreamSource;
+import com.android.tradefed.result.InputStreamSource;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestMetrics;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.RunUtil;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Performs successive reboots */
+@RunWith(DeviceJUnit4ClassRunner.class)
+@OptionClass(alias = "boot-time-test")
+public class BootTimeTest extends BaseHostJUnit4Test {
+    private static final String LOGCAT_CMD = "logcat *:V";
+    private static final String LOGCAT_CMD_ALL = "logcat -b all *:V";
+    private static final String LOGCAT_CMD_CLEAR = "logcat -c";
+    private static final long LOGCAT_SIZE = 80 * 1024 * 1024;
+    private static final String DMESG_FILE = "/data/local/tmp/dmesglogs.txt";
+    private static final String DUMP_DMESG = String.format("dmesg > %s", DMESG_FILE);
+    private static final String LOGCAT_FILENAME = "Successive_reboots_logcat";
+    private static final String DMESG_FILENAME = "Successive_reboots_dmesg";
+    private static final String IMMEDIATE_DMESG_FILENAME = "Successive_reboots_immediate_dmesg";
+    private static final String F2FS_SHUTDOWN_COMMAND = "f2fs_io shutdown 4 /data";
+    private static final String F2FS_SHUTDOWN_SUCCESS_OUTPUT = "Shutdown /data with level=4";
+
+    @Option(
+            name = "boot-count",
+            description =
+                    "Number of times to boot the devices to calculate the successive boot delay."
+                            + " Second boot after the first boot will be skipped for correctness.")
+    private int mBootCount = 5;
+
+    @Option(
+            name = "boot-delay",
+            isTimeVal = true,
+            description = "Time to wait between the successive boots.")
+    private long mBootDelayTime = 2000;
+
+    @Option(
+            name = "after-boot-delay",
+            isTimeVal = true,
+            description = "Time to wait immediately after the successive boots.")
+    private long mAfterBootDelayTime = 0;
+
+    @Option(name = "device-boot-time", description = "Max time in ms to wait for device to boot.")
+    protected long mDeviceBootTime = 5 * 60 * 1000;
+
+    @Option(
+            name = "successive-boot-prepare-cmd",
+            description =
+                    "A list of adb commands to run after first boot to prepare for successive"
+                            + " boot tests")
+    private List<String> mDeviceSetupCommands = new ArrayList<>();
+
+    /**
+     * Use this flag not to dump the dmesg logs immediately after the device is online. Use it only
+     * if some of the boot dmesg logs are cleared when waiting until boot completed. By default this
+     * is set to true which might result in duplicate logging.
+     */
+    @Option(
+            name = "dump-dmesg-immediate",
+            description =
+                    "Whether to dump the dmesg logs" + "immediately after the device is online")
+    private boolean mDumpDmesgImmediate = true;
+
+    @Option(
+            name = "force-f2fs-shutdown",
+            description = "Force f2fs shutdown to trigger fsck check during the reboot.")
+    private boolean mForceF2FsShutdown = false;
+
+    @Option(
+            name = "boot-time-pattern",
+            description =
+                    "Named boot time regex patterns which are used to capture signals in logcat and"
+                            + " calculate duration between device boot to the signal being logged."
+                            + " Key: name of custom boot metric, Value: regex to match single"
+                            + " logcat line. Maybe repeated.")
+    private Map<String, String> mBootTimePatterns = new HashMap<>();
+
+    private LogcatReceiver mRebootLogcatReceiver = null;
+
+    @Rule public TestLogData testLog = new TestLogData();
+    @Rule public TestMetrics testMetrics = new TestMetrics();
+
+    /**
+     * Prepares the device for successive boots
+     *
+     * @param testInfo Test Information
+     * @throws DeviceNotAvailableException if connection with device is lost and cannot be
+     *     recovered.
+     */
+    @BeforeClassWithInfo
+    public static void beforeClassWithDevice(TestInformation testInfo)
+            throws DeviceNotAvailableException {
+        ITestDevice testDevice = testInfo.getDevice();
+
+        testDevice.enableAdbRoot();
+        testDevice.setDate(null);
+        testDevice.nonBlockingReboot();
+        testDevice.waitForDeviceOnline();
+        testDevice.waitForDeviceAvailable(0);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        ITestDevice testDevice = getDevice();
+        setUpDeviceForSuccessiveBoots();
+        String logcatCommand = mBootTimePatterns.isEmpty() ? LOGCAT_CMD_ALL : LOGCAT_CMD;
+        mRebootLogcatReceiver = new LogcatReceiver(testDevice, logcatCommand, LOGCAT_SIZE, 0);
+    }
+
+    @Test
+    public void testSuccessiveBoots() throws Exception {
+        CLog.v("Waiting for %d msecs before successive boots.", mBootDelayTime);
+        sleep(mBootDelayTime);
+        for (int count = 0; count < mBootCount; count++) {
+            testSuccessiveBoot(count);
+        }
+    }
+
+    public void testSuccessiveBoot(int iteration) throws Exception {
+        CLog.v("Successive boot iteration %d", iteration);
+        getDevice().enableAdbRoot();
+        // Property used for collecting the perfetto trace file on boot.
+        getDevice().executeShellCommand("setprop persist.debug.perfetto.boottrace 1");
+        if (mForceF2FsShutdown) {
+            forseF2FsShutdown();
+        }
+        clearLogcat();
+        sleep(5000);
+        getDevice().nonBlockingReboot();
+        getDevice().waitForDeviceOnline(mDeviceBootTime);
+        getDevice().enableAdbRoot();
+        if (mDumpDmesgImmediate) {
+            saveDmesgInfo(String.format("%s_%d", IMMEDIATE_DMESG_FILENAME, iteration));
+        }
+        CLog.v("Waiting for %d msecs immediately after successive boot.", mAfterBootDelayTime);
+        sleep(mAfterBootDelayTime);
+        getDevice().waitForBootComplete(mDeviceBootTime);
+        saveDmesgInfo(String.format("%s_%d", DMESG_FILENAME, iteration));
+        saveLogcatInfo(iteration);
+        // TODO(b/288323866): implement
+        // analyzeBootLoaderTimingInfo(iteration);
+    }
+
+    private void setUpDeviceForSuccessiveBoots() throws DeviceNotAvailableException {
+        for (String cmd : mDeviceSetupCommands) {
+            CommandResult result;
+            result = getDevice().executeShellV2Command(cmd);
+            if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
+                CLog.w(
+                        "Post boot setup cmd: '%s' failed, returned:\nstdout:%s\nstderr:%s",
+                        cmd, result.getStdout(), result.getStderr());
+            }
+        }
+    }
+
+    private void forseF2FsShutdown() throws DeviceNotAvailableException {
+        String output = getDevice().executeShellCommand(F2FS_SHUTDOWN_COMMAND).trim();
+        if (!F2FS_SHUTDOWN_SUCCESS_OUTPUT.equalsIgnoreCase(output)) {
+            CLog.e("Unable to shutdown the F2FS.");
+        } else {
+            CLog.i("F2FS shutdown successful.");
+        }
+    }
+
+    private void saveLogcatInfo(int iteration) {
+        try (InputStreamSource logcat = mRebootLogcatReceiver.getLogcatData()) {
+            testLog.addTestLog(
+                    String.format("%s_%d", LOGCAT_FILENAME, iteration), LogDataType.LOGCAT, logcat);
+        }
+    }
+
+    private void saveDmesgInfo(String filename) throws DeviceNotAvailableException {
+        getDevice().executeShellCommand(DUMP_DMESG);
+        File dmesgFile = getDevice().pullFile(DMESG_FILE);
+        testLog.addTestLog(
+                filename, LogDataType.HOST_LOG, new FileInputStreamSource(dmesgFile, false));
+    }
+
+    private void clearLogcat() throws DeviceNotAvailableException {
+        getDevice().executeShellCommand(LOGCAT_CMD_CLEAR);
+        getDevice().clearLogcat();
+        mRebootLogcatReceiver.clear();
+    }
+
+    private void sleep(long duration) {
+        RunUtil.getDefault().sleep(duration);
+    }
+}