charger: Add tests for loading animation resources

Bug: 143907179
Test: run it

Change-Id: I1b1dfe4947210932c674657689e396ae909013c2
diff --git a/healthd/Android.bp b/healthd/Android.bp
index a4300f7..b3de9c4 100644
--- a/healthd/Android.bp
+++ b/healthd/Android.bp
@@ -243,19 +243,22 @@
 
 cc_test {
     name: "libhealthd_charger_test",
-    srcs: ["AnimationParser_test.cpp"],
-    shared_libs: [
-        "liblog",
-        "libbase",
-        "libcutils",
+    defaults: ["charger_defaults"],
+    srcs: [
+        "AnimationParser_test.cpp",
+        "healthd_mode_charger_test.cpp"
     ],
     static_libs: [
-        "libhealthd_charger",
+        "libgmock",
     ],
     test_suites: [
         "general-tests",
         "device-tests",
     ],
+    data: [
+        ":libhealthd_charger_test_data",
+    ],
+    require_root: true,
 }
 
 // /system/etc/res/images/charger/battery_fail.png
diff --git a/healthd/healthd_mode_charger_test.cpp b/healthd/healthd_mode_charger_test.cpp
new file mode 100644
index 0000000..f444f66
--- /dev/null
+++ b/healthd/healthd_mode_charger_test.cpp
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+#include <sysexits.h>
+#include <unistd.h>
+
+#include <iostream>
+#include <string>
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/strings.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <health/utils.h>
+
+#include "healthd_mode_charger.h"
+
+using android::hardware::Return;
+using android::hardware::health::InitHealthdConfig;
+using std::string_literals::operator""s;
+using testing::_;
+using testing::Invoke;
+using testing::NiceMock;
+using testing::StrEq;
+using testing::Test;
+
+namespace android {
+
+// A replacement to ASSERT_* to be used in a forked process. When the condition is not met,
+// print a gtest message, then exit abnormally.
+class ChildAssertHelper : public std::stringstream {
+  public:
+    ChildAssertHelper(bool res, const char* expr, const char* file, int line) : res_(res) {
+        (*this) << file << ":" << line << ": `" << expr << "` evaluates to false\n";
+    }
+    ~ChildAssertHelper() {
+        EXPECT_TRUE(res_) << str();
+        if (!res_) exit(EX_SOFTWARE);
+    }
+
+  private:
+    bool res_;
+    DISALLOW_COPY_AND_ASSIGN(ChildAssertHelper);
+};
+#define CHILD_ASSERT_TRUE(expr) ChildAssertHelper(expr, #expr, __FILE__, __LINE__)
+
+// Run |test_body| in a chroot jail in a forked process. |subdir| is a sub-directory in testdata.
+// Within |test_body|,
+// - non-fatal errors may be reported using EXPECT_* macro as usual.
+// - fatal errors must be reported using CHILD_ASSERT_TRUE macro. ASSERT_* must not be used.
+void ForkTest(const std::string& subdir, const std::function<void(void)>& test_body) {
+    pid_t pid = fork();
+    ASSERT_GE(pid, 0) << "Fork fails: " << strerror(errno);
+    if (pid == 0) {
+        // child
+        CHILD_ASSERT_TRUE(
+                chroot((android::base::GetExecutableDirectory() + "/" + subdir).c_str()) != -1)
+                << "Failed to chroot to " << subdir << ": " << strerror(errno);
+        test_body();
+        // EXPECT_* macros may set the HasFailure bit without calling exit(). Set exit status
+        // accordingly.
+        exit(::testing::Test::HasFailure() ? EX_SOFTWARE : EX_OK);
+    }
+    // parent
+    int status;
+    ASSERT_NE(-1, waitpid(pid, &status, 0)) << "waitpid() fails: " << strerror(errno);
+    ASSERT_TRUE(WIFEXITED(status)) << "Test fails, waitpid() returns " << status;
+    ASSERT_EQ(EX_OK, WEXITSTATUS(status)) << "Test fails, child process returns " << status;
+}
+
+class MockHealth : public android::hardware::health::V2_1::IHealth {
+    MOCK_METHOD(Return<::android::hardware::health::V2_0::Result>, registerCallback,
+                (const sp<::android::hardware::health::V2_0::IHealthInfoCallback>& callback));
+    MOCK_METHOD(Return<::android::hardware::health::V2_0::Result>, unregisterCallback,
+                (const sp<::android::hardware::health::V2_0::IHealthInfoCallback>& callback));
+    MOCK_METHOD(Return<::android::hardware::health::V2_0::Result>, update, ());
+    MOCK_METHOD(Return<void>, getChargeCounter, (getChargeCounter_cb _hidl_cb));
+    MOCK_METHOD(Return<void>, getCurrentNow, (getCurrentNow_cb _hidl_cb));
+    MOCK_METHOD(Return<void>, getCurrentAverage, (getCurrentAverage_cb _hidl_cb));
+    MOCK_METHOD(Return<void>, getCapacity, (getCapacity_cb _hidl_cb));
+    MOCK_METHOD(Return<void>, getEnergyCounter, (getEnergyCounter_cb _hidl_cb));
+    MOCK_METHOD(Return<void>, getChargeStatus, (getChargeStatus_cb _hidl_cb));
+    MOCK_METHOD(Return<void>, getStorageInfo, (getStorageInfo_cb _hidl_cb));
+    MOCK_METHOD(Return<void>, getDiskStats, (getDiskStats_cb _hidl_cb));
+    MOCK_METHOD(Return<void>, getHealthInfo, (getHealthInfo_cb _hidl_cb));
+    MOCK_METHOD(Return<void>, getHealthConfig, (getHealthConfig_cb _hidl_cb));
+    MOCK_METHOD(Return<void>, getHealthInfo_2_1, (getHealthInfo_2_1_cb _hidl_cb));
+    MOCK_METHOD(Return<void>, shouldKeepScreenOn, (shouldKeepScreenOn_cb _hidl_cb));
+};
+
+class TestCharger : public Charger {
+  public:
+    // Inherit constructor.
+    using Charger::Charger;
+    // Expose protected functions to be used in tests.
+    void Init(struct healthd_config* config) override { Charger::Init(config); }
+    MOCK_METHOD(int, CreateDisplaySurface, (const std::string& name, GRSurface** surface));
+    MOCK_METHOD(int, CreateMultiDisplaySurface,
+                (const std::string& name, int* frames, int* fps, GRSurface*** surface));
+};
+
+// Intentionally leak TestCharger instance to avoid calling ~HealthLoop() because ~HealthLoop()
+// should never be called. But still verify expected calls upon destruction.
+class VerifiedTestCharger {
+  public:
+    VerifiedTestCharger(TestCharger* charger) : charger_(charger) {
+        testing::Mock::AllowLeak(charger_);
+    }
+    TestCharger& operator*() { return *charger_; }
+    TestCharger* operator->() { return charger_; }
+    ~VerifiedTestCharger() { testing::Mock::VerifyAndClearExpectations(charger_); }
+
+  private:
+    TestCharger* charger_;
+};
+
+// Do not use SetUp and TearDown of a test suite, as they will be invoked in the parent process, not
+// the child process. In particular, if the test suite contains mocks, they will not be verified in
+// the child process. Instead, create mocks within closures in each tests.
+void ExpectChargerResAt(const std::string& root) {
+    sp<NiceMock<MockHealth>> health(new NiceMock<MockHealth>());
+    VerifiedTestCharger charger(new NiceMock<TestCharger>(health));
+
+    // Only one frame in all testdata/**/animation.txt
+    GRSurface* multi[] = {nullptr};
+
+    EXPECT_CALL(*charger, CreateDisplaySurface(StrEq(root + "charger/battery_fail.png"), _))
+            .WillRepeatedly(Invoke([](const auto&, GRSurface** surface) {
+                *surface = nullptr;
+                return 0;
+            }));
+    EXPECT_CALL(*charger,
+                CreateMultiDisplaySurface(StrEq(root + "charger/battery_scale.png"), _, _, _))
+            .WillRepeatedly(Invoke([&](const auto&, int* frames, int* fps, GRSurface*** surface) {
+                *frames = arraysize(multi);
+                *fps = 60;  // Unused fps value
+                *surface = multi;
+                return 0;
+            }));
+    struct healthd_config healthd_config;
+    InitHealthdConfig(&healthd_config);
+    charger->Init(&healthd_config);
+};
+
+// Test that if resources does not exist in /res or in /product/etc/res, load from /system.
+TEST(ChargerLoadAnimationRes, Empty) {
+    ForkTest("empty", std::bind(&ExpectChargerResAt, "/system/etc/res/images/"));
+}
+
+// Test loading everything from /res
+TEST(ChargerLoadAnimationRes, Legacy) {
+    ForkTest("legacy", std::bind(&ExpectChargerResAt, "/res/images/"));
+}
+
+// Test loading animation text from /res but images from /system if images does not exist under
+// /res.
+TEST(ChargerLoadAnimationRes, LegacyTextSystemImages) {
+    ForkTest("legacy_text_system_images",
+             std::bind(&ExpectChargerResAt, "/system/etc/res/images/"));
+}
+
+// Test loading everything from /product
+TEST(ChargerLoadAnimationRes, Product) {
+    ForkTest("product", std::bind(&ExpectChargerResAt, "/product/etc/res/images/"));
+}
+
+}  // namespace android
diff --git a/healthd/testdata/Android.bp b/healthd/testdata/Android.bp
new file mode 100644
index 0000000..110c79a
--- /dev/null
+++ b/healthd/testdata/Android.bp
@@ -0,0 +1,20 @@
+//
+// Copyright (C) 2020 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.
+//
+
+filegroup {
+    name: "libhealthd_charger_test_data",
+    srcs: ["**/*.*"],
+}
diff --git a/healthd/testdata/empty/ensure_directory_creation.txt b/healthd/testdata/empty/ensure_directory_creation.txt
new file mode 100644
index 0000000..36ceff4
--- /dev/null
+++ b/healthd/testdata/empty/ensure_directory_creation.txt
@@ -0,0 +1 @@
+File is placed to ensure directory is created on the device.
diff --git a/healthd/testdata/legacy/res/images/charger/battery_fail.png b/healthd/testdata/legacy/res/images/charger/battery_fail.png
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/healthd/testdata/legacy/res/images/charger/battery_fail.png
diff --git a/healthd/testdata/legacy/res/images/charger/battery_scale.png b/healthd/testdata/legacy/res/images/charger/battery_scale.png
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/healthd/testdata/legacy/res/images/charger/battery_scale.png
diff --git a/healthd/testdata/legacy/res/values/charger/animation.txt b/healthd/testdata/legacy/res/values/charger/animation.txt
new file mode 100644
index 0000000..0753336
--- /dev/null
+++ b/healthd/testdata/legacy/res/values/charger/animation.txt
@@ -0,0 +1,9 @@
+# Sample Animation file for testing.
+
+# animation: num_cycles, first_frame_repeats, animation_file
+animation: 2 1 charger/battery_scale
+
+fail: charger/battery_fail
+
+# frame: disp_time min_level max_level
+frame: 15 0 100
diff --git a/healthd/testdata/legacy_text_system_images/res/values/charger/animation.txt b/healthd/testdata/legacy_text_system_images/res/values/charger/animation.txt
new file mode 100644
index 0000000..0753336
--- /dev/null
+++ b/healthd/testdata/legacy_text_system_images/res/values/charger/animation.txt
@@ -0,0 +1,9 @@
+# Sample Animation file for testing.
+
+# animation: num_cycles, first_frame_repeats, animation_file
+animation: 2 1 charger/battery_scale
+
+fail: charger/battery_fail
+
+# frame: disp_time min_level max_level
+frame: 15 0 100
diff --git a/healthd/testdata/product/product/etc/res/images/charger/battery_fail.png b/healthd/testdata/product/product/etc/res/images/charger/battery_fail.png
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/healthd/testdata/product/product/etc/res/images/charger/battery_fail.png
diff --git a/healthd/testdata/product/product/etc/res/images/charger/battery_scale.png b/healthd/testdata/product/product/etc/res/images/charger/battery_scale.png
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/healthd/testdata/product/product/etc/res/images/charger/battery_scale.png
diff --git a/healthd/testdata/product/product/etc/res/values/charger/animation.txt b/healthd/testdata/product/product/etc/res/values/charger/animation.txt
new file mode 100644
index 0000000..0753336
--- /dev/null
+++ b/healthd/testdata/product/product/etc/res/values/charger/animation.txt
@@ -0,0 +1,9 @@
+# Sample Animation file for testing.
+
+# animation: num_cycles, first_frame_repeats, animation_file
+animation: 2 1 charger/battery_scale
+
+fail: charger/battery_fail
+
+# frame: disp_time min_level max_level
+frame: 15 0 100