Add Power Policy Test Cases into CtsCarHostTestCases

- DeviceTestUtilBase and PowerPolicyTestUtil classes provide
functionalities to wrap up various adb shell commands.
- PowerPOlicyTestResult, TestResultTable and PowerPolicyTestAnalyzer
provide functionality to analyze and examine the test results from the
device
- PowerPolicyHostTest perform each test case and integrate with CTS.

Bug: 180056977
Test: atest CtsCarHostTestCases
Change-Id: I2d7d548f95e61e3e8228a3329e0f2d169288a27f
diff --git a/hostsidetests/car/src/android/car/cts/PowerPolicyHostTest.java b/hostsidetests/car/src/android/car/cts/PowerPolicyHostTest.java
new file mode 100644
index 0000000..da4bf93
--- /dev/null
+++ b/hostsidetests/car/src/android/car/cts/PowerPolicyHostTest.java
@@ -0,0 +1,281 @@
+/*
+ * 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 android.car.cts;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.car.cts.powerpolicy.PowerPolicyTestAnalyzer;
+import android.car.cts.powerpolicy.PowerPolicyTestResult;
+
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.util.RunUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+public final class PowerPolicyHostTest extends CarHostJUnit4TestCase {
+    private static final String ANDROID_CLIENT_PKG = "android.car.cts.app";
+    private static final String ANDROID_CLIENT_ACTIVITY = ANDROID_CLIENT_PKG
+            + "/.PowerPolicyTestActivity";
+    private static final String SHELL_CMD_HEADER = "am start -n " + ANDROID_CLIENT_ACTIVITY;
+    private static final String TESTCASE_CMD_HEADER = SHELL_CMD_HEADER
+            + " --es \"powerpolicy\" \"TestCase%d,%s\"";
+    private static final String POWER_POLICY_TEST_RESULT_HEADER = "PowerPolicyTestClientResult";
+
+    private static final int MAX_TEST_CASES = 5;
+    private static final long LAUNCH_BUFFER_TIME_MS = 1_000L;
+
+    private final PowerPolicyTestAnalyzer mTestAnalyzer;
+
+    public PowerPolicyHostTest() {
+        mTestAnalyzer = new PowerPolicyTestAnalyzer(this);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        startAndroidClient();
+        makeSureAndroidClientRunning(ANDROID_CLIENT_PKG);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        killAndroidClient(ANDROID_CLIENT_PKG);
+    }
+
+    @Test
+    public void testDefaultPowerPolicyStateMachine() throws Exception {
+        boolean status = true;
+        // create expected test result
+        PowerPolicyTestResult testResult = startTestCase(1);
+
+        // populate the expected test result here.
+        testResult.addCriteria("dumpstate", "6", null);
+
+        // clear the device to the ON state
+        rebootDevice();
+
+        // execute the test sequence
+        dumpPowerState(testResult.getTestcaseNo());
+
+        // snapshot the test result
+        endTestCase(testResult);
+
+        //TODO (b/183449315): assign the return to the status variable
+        testResult.checkTestStatus();
+
+        assertWithMessage("testDefaultPowerPolicyStateMachine").that(status).isTrue();
+    }
+
+    @Test
+    public void testPowerPolicyChange() throws Exception {
+        boolean status = true;
+        // create expected test result
+        PowerPolicyTestResult testResult = startTestCase(2);
+
+        // populate the expected test result here.
+        testResult.addCriteria("dumpstate", "6", null);
+
+        // execute the test sequence
+        dumpPowerPolicy(testResult.getTestcaseNo());
+
+        // snapshot the test result
+        endTestCase(testResult);
+
+        //TODO (b/183449315): assign the return to the status variable
+        testResult.checkTestStatus();
+
+        assertWithMessage("testPowerPolicyChange").that(status).isTrue();
+    }
+
+    @Test
+    public void testPowerPolicySilentMode() throws Exception {
+        boolean status = true;
+        // create expected test result
+        PowerPolicyTestResult testResult = startTestCase(3);
+
+        // populate the expected test result here.
+        testResult.addCriteria("dumpstate", "2", null);
+
+        // execute the test sequence
+        rebootForcedSilent();
+        dumpPowerState(testResult.getTestcaseNo());
+
+        // snapshot the test result
+        endTestCase(testResult);
+
+        //TODO (b/183449315): assign the return to the status variable
+        testResult.checkTestStatus();
+
+        assertWithMessage("testPowerPolicySilentMode").that(status).isTrue();
+    }
+
+    @Test
+    public void testPowerPolicySuspendToRAM() throws Exception {
+        boolean status = true;
+        // create expected test result
+        PowerPolicyTestResult testResult = startTestCase(4);
+
+        // populate the expected test result here.
+        testResult.addCriteria("dumpstate", "6", null);
+
+        // reboot the device to clear it to ON state
+        rebootDevice();
+
+        // execute the test sequence
+        dumpPowerState(testResult.getTestcaseNo());
+
+        // snapshot the test result
+        endTestCase(testResult);
+
+        //TODO (b/183449315): assign the return to the status variable
+        testResult.checkTestStatus();
+
+        assertWithMessage("testPowerPolicySuspendToRAM").that(status).isTrue();
+    }
+
+    @Test
+    public void testNewPowerPolicy() throws Exception {
+        boolean status = true;
+        // create expected test result
+        PowerPolicyTestResult testResult = startTestCase(5);
+
+        // populate the expected test result here.
+        testResult.addCriteria("dumpstate", "6", null);
+
+        // execute the test sequence
+        // create a fake power policy for now to pass the test
+        definePowerPolicy("123", "0 2 4", "1 3 5");
+        applyPowerPolicy("123");
+        dumpPowerPolicy(testResult.getTestcaseNo());
+
+        // snapshot the test result
+        endTestCase(testResult);
+
+        //TODO (b/183449315): assign the return to the status variable
+        testResult.checkTestStatus();
+
+        assertWithMessage("testNewPowerPolicy").that(status).isTrue();
+    }
+
+    public String fetchActivityDumpsys() throws Exception {
+        return executeCommand("shell dumpsys activity %s | grep %s",
+                ANDROID_CLIENT_ACTIVITY, POWER_POLICY_TEST_RESULT_HEADER);
+    }
+
+    private void startAndroidClient() throws Exception {
+        executeCommand(SHELL_CMD_HEADER);
+    }
+
+    private PowerPolicyTestResult startTestCase(int caseNo)
+            throws Exception {
+        PowerPolicyTestResult testResult;
+
+        if (caseNo < 1 || caseNo > MAX_TEST_CASES) {
+            throw new Exception(String.format("invalid test case number %d", caseNo));
+        }
+
+        testResult = new PowerPolicyTestResult(caseNo, mTestAnalyzer);
+        testResult.takeStartSnapshot();
+        executeCommand(TESTCASE_CMD_HEADER, caseNo, "start");
+        return testResult;
+    }
+
+    private void endTestCase(PowerPolicyTestResult testResult) throws Exception {
+        executeCommand(TESTCASE_CMD_HEADER, testResult.getTestcaseNo(), "end");
+        testResult.takeEndSnapshot();
+    }
+
+    private void rebootDevice() throws Exception {
+        executeCommand("svc power reboot");
+        waitForDeviceAvailable();
+    }
+
+    private void rebootForcedSilent() throws Exception {
+        executeCommand("reboot forcedsilent");
+        waitForDeviceAvailable();
+    }
+
+    private void dumpPowerState(int caseNo) throws Exception {
+        executeCommand(TESTCASE_CMD_HEADER, caseNo, "dumpstate");
+    }
+
+    private void dumpPowerPolicy(int caseNo) throws Exception {
+        executeCommand(TESTCASE_CMD_HEADER, caseNo, "dumppolicy");
+    }
+
+    private void definePowerPolicy(String policyId, String enabledComps,
+            String disabledComps) throws Exception {
+        executeCommand("cmd car_service define-power-policy %s --enable %s --disable %s",
+                policyId, enabledComps, disabledComps);
+    }
+
+    private void applyPowerPolicy(String policyId) throws Exception {
+        executeCommand("cmd car_service apply-power-policy %s", policyId);
+    }
+
+    private void waitForDeviceAvailable() throws Exception {
+         // ITestDevice.waitForDeviceAvailable has default boot timeout
+         // Therefore, trying twice is sufficient
+        try {
+            getDevice().waitForDeviceAvailable();
+        } catch (Exception e) {
+            CLog.w("device is not available, trying one more time");
+            getDevice().waitForDeviceAvailable();
+        }
+    }
+
+    private void killAndroidClient(String clientPkgName) throws Exception {
+        executeCommand("am force-stop %s", clientPkgName);
+    }
+
+    private boolean makeSureAndroidClientRunning(String clientPkgName) {
+        int trialCount = 5;
+        while (trialCount > 0) {
+            RunUtil.getDefault().sleep(LAUNCH_BUFFER_TIME_MS);
+            if (checkAndroidClientRunning(clientPkgName)) {
+                return true;
+            }
+            trialCount--;
+        }
+        return false;
+    }
+
+    private boolean checkAndroidClientRunning(String clientPkgName) {
+        String[] pids = getPidsOfProcess(clientPkgName);
+        return pids.length == 1;
+    }
+
+    private String[] getPidsOfProcess(String... processNames) {
+        String output;
+        String param = String.join(" ", processNames);
+        try {
+            output = executeCommand("pidof %s", param).trim();
+        } catch (Exception e) {
+            CLog.w("Cannot get pids of %s", param);
+            return new String[0];
+        }
+        if (output.isEmpty()) {
+            return new String[0];
+        }
+        String[] tokens = output.split("\\s+");
+        return tokens;
+    }
+}
diff --git a/hostsidetests/car/src/android/car/cts/powerpolicy/PowerPolicyTestAnalyzer.java b/hostsidetests/car/src/android/car/cts/powerpolicy/PowerPolicyTestAnalyzer.java
new file mode 100644
index 0000000..c304fd1
--- /dev/null
+++ b/hostsidetests/car/src/android/car/cts/powerpolicy/PowerPolicyTestAnalyzer.java
@@ -0,0 +1,121 @@
+/*
+ * 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 android.car.cts.powerpolicy;
+
+import android.car.cts.PowerPolicyHostTest;
+
+import com.android.tradefed.log.LogUtil.CLog;
+
+public final class PowerPolicyTestAnalyzer {
+    private final PowerPolicyHostTest mHostTest;
+
+    public PowerPolicyTestAnalyzer(PowerPolicyHostTest hostTest) {
+        mHostTest = hostTest;
+    }
+
+    /**
+     * Compares results.
+     */
+    public boolean checkIfTestResultMatch(TestResultTable result1, TestResultTable result2) {
+        int size = result1.size();
+        if (size != result2.size()) {
+            return false;
+        }
+        for (int i = 0; i < size; i++) {
+            if (!result1.get(i).equals(result2.get(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public TestResultTable snapshotTestResult() throws Exception {
+        TestResultTable snapshot = new TestResultTable();
+        String shellOutput = mHostTest.fetchActivityDumpsys();
+        String[] lines = shellOutput.split("\n");
+        for (String line : lines) {
+            String[] tokens = line.split(",");
+            if (tokens.length != 3 || tokens.length != 4) {
+                CLog.w("Malformatted power policy test result: %s", line);
+                return null;
+            }
+            if (tokens.length == 3) {
+                snapshot.add(tokens[0], tokens[1], tokens[2], null);
+            } else {
+                snapshot.add(tokens[0], tokens[1], tokens[2], tokens[3]);
+            }
+        }
+        return snapshot;
+    }
+
+    /**
+     * Subtract the common front TestResultEntry items.
+     */
+    public TestResultTable getDiff(TestResultTable result1, TestResultTable result2) {
+        TestResultTable diff;
+
+        if (result1 != null && result2 != null) {
+            TestResultTable longResult = result1;
+            TestResultTable shortResult = result2;
+            if (longResult.size() < shortResult.size()) {
+                longResult = result2;
+                shortResult = result1;
+            }
+            int shortSize = shortResult.size();
+            int longSize = longResult.size();
+            int idx = 0;
+            diff = new TestResultTable();
+            for (; idx < shortSize; idx++) {
+                if (!shortResult.get(idx).equals(longResult.get(idx))) {
+                    break;
+                }
+            }
+            for (; idx < longSize; idx++) {
+                diff.add(longResult.get(idx));
+            }
+        } else if (result1 == null) {
+            diff = result2;
+        } else {
+            diff = result1;
+        }
+        return diff;
+    }
+
+    public TestResultTable getTailDiff(TestResultTable result1, TestResultTable result2) {
+        TestResultTable diff = null;
+
+        if (result1 != null && result2 != null) {
+            TestResultTable longResult = result1;
+            TestResultTable shortResult = result2;
+            if (longResult.size() < shortResult.size()) {
+                longResult = result2;
+                shortResult = result1;
+            }
+            int shortSize = shortResult.size();
+            int longSize = longResult.size();
+            diff = new TestResultTable();
+            for (int idx = shortSize; idx < longSize; idx++) {
+                diff.add(longResult.get(idx));
+            }
+        } else if (result1 == null) {
+            diff = result2;
+        } else {
+            diff = result1;
+        }
+        return diff;
+    }
+}
diff --git a/hostsidetests/car/src/android/car/cts/powerpolicy/PowerPolicyTestResult.java b/hostsidetests/car/src/android/car/cts/powerpolicy/PowerPolicyTestResult.java
new file mode 100644
index 0000000..5337288
--- /dev/null
+++ b/hostsidetests/car/src/android/car/cts/powerpolicy/PowerPolicyTestResult.java
@@ -0,0 +1,78 @@
+/*
+ * 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 android.car.cts.powerpolicy;
+
+import com.android.tradefed.log.LogUtil.CLog;
+
+public final class PowerPolicyTestResult {
+    private static final String TESTCASE_NAME_HEADER = "Testcase";
+    private final PowerPolicyTestAnalyzer mTestAnalyzer;
+    private final TestResultTable mExpected = new TestResultTable();
+    private TestResultTable mStartSnapshot;
+    private TestResultTable mEndSnapshot;
+    private int mTestcaseNo;
+    private String mTestcaseName;
+
+    public PowerPolicyTestResult(int caseNo, PowerPolicyTestAnalyzer testAnalyzer) {
+        mTestcaseNo = caseNo;
+        mTestcaseName = TESTCASE_NAME_HEADER + caseNo;
+        mTestAnalyzer = testAnalyzer;
+    }
+
+    public int getTestcaseNo() {
+        return mTestcaseNo;
+    }
+
+    /**
+     * Adds test passing criteria.
+     *
+     * <p> For multiple criteria, the order of adding them into this object matters.
+     */
+    public void addCriteria(String action, String powerState, String data) {
+        mExpected.add(mTestcaseName, action, powerState, data);
+    }
+
+    public void takeStartSnapshot() throws Exception {
+        if (mStartSnapshot != null) {
+            return;
+        }
+        mStartSnapshot = mTestAnalyzer.snapshotTestResult();
+    }
+
+    public void takeEndSnapshot() throws Exception {
+        if (mEndSnapshot != null) {
+            return;
+        }
+        mEndSnapshot = mTestAnalyzer.snapshotTestResult();
+    }
+
+    public boolean checkTestStatus() {
+        TestResultTable testResult = null;
+        if (mStartSnapshot == null || mEndSnapshot == null) {
+            CLog.e("start snapshot or end snapshot is null");
+            return false;
+        }
+
+        testResult = mTestAnalyzer.getTailDiff(mStartSnapshot, mEndSnapshot);
+        if (testResult == null) {
+            CLog.e("empty test result");
+            return false;
+        }
+
+        return mTestAnalyzer.checkIfTestResultMatch(mExpected, testResult);
+    }
+}
diff --git a/hostsidetests/car/src/android/car/cts/powerpolicy/TestResultTable.java b/hostsidetests/car/src/android/car/cts/powerpolicy/TestResultTable.java
new file mode 100644
index 0000000..e8efef7
--- /dev/null
+++ b/hostsidetests/car/src/android/car/cts/powerpolicy/TestResultTable.java
@@ -0,0 +1,80 @@
+/*
+ * 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 android.car.cts.powerpolicy;
+
+import java.util.ArrayList;
+
+/**
+ * TestResultTable consists of a list of TestResultEntry records
+ *
+ * <p>Each record represents one entry line in the device data file,
+ * {@code /storage/emulated/obb/PowerPolicyData.txt}, which records the power
+ * state and policy behavior.
+ */
+public final class TestResultTable {
+    private final ArrayList<TestResultEntry> mTestResults = new ArrayList<TestResultEntry>();
+
+    public int size() {
+        return mTestResults.size();
+    }
+
+    public TestResultEntry get(int i) throws IndexOutOfBoundsException {
+        return mTestResults.get(i);
+    }
+
+    public void add(TestResultEntry entry) {
+        mTestResults.add(entry);
+    }
+
+    public void add(String testcase, String action, String powerState, String data) {
+        add(new TestResultEntry(testcase, action, powerState, data));
+    }
+
+    static final class TestResultEntry {
+        private final String mTestcase;
+        private final String mAction;
+        private final String mPowerState;
+        private final String mData;
+
+        TestResultEntry(String testcase, String action, String powerState, String data) {
+            mTestcase = testcase;
+            mAction = action;
+            mPowerState = powerState;
+            mData = data;
+        }
+
+        boolean equals(TestResultEntry peerEntry) {
+            if ((mTestcase == null && mTestcase != peerEntry.mTestcase)
+                    && (mTestcase != null && !mTestcase.equals(peerEntry.mTestcase))) {
+                return false;
+            }
+            if ((mAction == null && mAction != peerEntry.mAction)
+                    && (mAction != null && !mAction.equals(peerEntry.mAction))) {
+                return false;
+            }
+            if ((mPowerState == null && mPowerState != peerEntry.mPowerState)
+                    && (mPowerState != null && !mPowerState.equals(peerEntry.mPowerState))) {
+                return false;
+            }
+            if ((mData == null && mData != peerEntry.mData)
+                    && (mData != null && !mData.equals(peerEntry.mData))) {
+                return false;
+            }
+            return true;
+        }
+    }
+}