Added initial tests for dumpstate.

BUG: 31807540
Test: mmm -j32 frameworks/native/cmds/dumpstate/ && adb push ${ANDROID_PRODUCT_OUT}/data/nativetest/dumpstate_test* /data/nativetest && adb shell /data/nativetest/dumpstate_test/dumpstate_test

Change-Id: If5497784052b8d13d7c856f9400dbcd8c2015d05
diff --git a/cmds/dumpstate/Android.mk b/cmds/dumpstate/Android.mk
index 44a994c..2eb512f 100644
--- a/cmds/dumpstate/Android.mk
+++ b/cmds/dumpstate/Android.mk
@@ -1,30 +1,84 @@
 LOCAL_PATH:= $(call my-dir)
 
-include $(CLEAR_VARS)
+# ================#
+# Common settings #
+# ================#
+# ZipArchive support, the order matters here to get all symbols.
+COMMON_ZIP_LIBRARIES := libziparchive libz libcrypto_static
 
-ifdef BOARD_WLAN_DEVICE
-LOCAL_CFLAGS := -DFWDUMP_$(BOARD_WLAN_DEVICE)
-endif
-
-LOCAL_SRC_FILES := \
-        dumpstate.cpp \
+# TODO: ideally the tests should depend on a shared dumpstate library, but currently libdumpstate
+# is used to define the device-specific HAL library. Instead, both dumpstate and dumpstate_test
+# shares a lot of common settings
+COMMON_LOCAL_CFLAGS := \
+       -Wall -Werror -Wno-missing-field-initializers -Wno-unused-variable -Wunused-parameter
+COMMON_SRC_FILES := \
         utils.cpp
-
-LOCAL_MODULE := dumpstate
-
-LOCAL_SHARED_LIBRARIES := \
+COMMON_SHARED_LIBRARIES := \
         libbase \
         libcutils \
         libhardware_legacy \
         liblog \
         libselinux
 
-# ZipArchive support, the order matters here to get all symbols.
-ZIP_LIBRARIES := libziparchive libz libcrypto_static
+# ==========#
+# dumpstate #
+# ==========#
+include $(CLEAR_VARS)
 
-LOCAL_STATIC_LIBRARIES := $(ZIP_LIBRARIES)
+ifdef BOARD_WLAN_DEVICE
+LOCAL_CFLAGS := -DFWDUMP_$(BOARD_WLAN_DEVICE)
+endif
+
+LOCAL_SRC_FILES := $(COMMON_SRC_FILES) \
+        dumpstate.cpp
+
+LOCAL_MODULE := dumpstate
+
+LOCAL_SHARED_LIBRARIES := $(COMMON_SHARED_LIBRARIES)
+
+LOCAL_STATIC_LIBRARIES := $(COMMON_ZIP_LIBRARIES)
+
 LOCAL_HAL_STATIC_LIBRARIES := libdumpstate
-LOCAL_CFLAGS += -Wall -Werror -Wno-unused-parameter
+
+LOCAL_CFLAGS += $(COMMON_LOCAL_CFLAGS)
+
 LOCAL_INIT_RC := dumpstate.rc
 
 include $(BUILD_EXECUTABLE)
+
+# ===============#
+# dumpstate_test #
+# ===============#
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := dumpstate_test
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_CFLAGS := $(COMMON_LOCAL_CFLAGS)
+
+LOCAL_SRC_FILES := $(COMMON_SRC_FILES) \
+        tests/dumpstate_test.cpp
+
+LOCAL_STATIC_LIBRARIES := $(COMMON_ZIP_LIBRARIES) \
+        libgmock
+
+LOCAL_SHARED_LIBRARIES := $(COMMON_SHARED_LIBRARIES)
+
+include $(BUILD_NATIVE_TEST)
+
+# =======================#
+# dumpstate_test_fixture #
+# =======================#
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := dumpstate_test_fixture
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_CFLAGS := $(COMMON_LOCAL_CFLAGS)
+
+LOCAL_SRC_FILES := \
+        tests/dumpstate_test_fixture.cpp
+
+include $(BUILD_NATIVE_TEST)
diff --git a/cmds/dumpstate/dumpstate.cpp b/cmds/dumpstate/dumpstate.cpp
index f3e68b3..637b784 100644
--- a/cmds/dumpstate/dumpstate.cpp
+++ b/cmds/dumpstate/dumpstate.cpp
@@ -62,7 +62,6 @@
 static std::string args;
 
 // TODO: variables below should be part of dumpstate object
-static std::string buildType;
 static time_t now;
 static std::unique_ptr<ZipWriter> zip_writer;
 static std::set<std::string> mount_points;
@@ -127,14 +126,6 @@
 static constexpr char PROPERTY_EXTRA_OPTIONS[] = "dumpstate.options";
 static constexpr char PROPERTY_LAST_ID[] = "dumpstate.last_id";
 
-bool Dumpstate::IsUserBuild() {
-    return "user" == buildType;
-}
-
-bool Dumpstate::IsDryRun() {
-    return dryRun_;
-}
-
 /* gets the tombstone data, according to the bugreport type: if zipped, gets all tombstones;
  * otherwise, gets just those modified in the last half an hour. */
 static void get_tombstone_fds(tombstone_data_t data[NUM_TOMBSTONES]) {
@@ -155,7 +146,7 @@
 }
 
 // for_each_pid() callback to get mount info about a process.
-void do_mountinfo(int pid, const char *name) {
+void do_mountinfo(int pid, const char* name __attribute__((unused))) {
     char path[PATH_MAX];
 
     // Gets the the content of the /proc/PID/ns/mnt link, so only unique mount points
@@ -426,7 +417,7 @@
     return strcmp(path + len - sizeof(stat) + 1, stat); /* .../stat? */
 }
 
-static bool skip_none(const char *path) {
+static bool skip_none(const char* path __attribute__((unused))) {
     return false;
 }
 
@@ -704,7 +695,7 @@
 
     build = android::base::GetProperty("ro.build.display.id", "(unknown)");
     fingerprint = android::base::GetProperty("ro.build.fingerprint", "(unknown)");
-    buildType = android::base::GetProperty("ro.build.type", "(unknown)");
+    ds.buildType_ = android::base::GetProperty("ro.build.type", "(unknown)");
     radio = android::base::GetProperty("gsm.version.baseband", "(unknown)");
     bootloader = android::base::GetProperty("ro.bootloader", "(unknown)");
     network = android::base::GetProperty("gsm.operator.alpha", "(unknown)");
@@ -727,7 +718,7 @@
     printf("Command line: %s\n", strtok(cmdline_buf, "\n"));
     printf("Bugreport format version: %s\n", version.c_str());
     printf("Dumpstate info: id=%lu pid=%d dryRun=%d args=%s extraOptions=%s\n", ds.id_, getpid(),
-           ds.dryRun_, args.c_str(), extraOptions.c_str());
+           ds.IsDryRun(), args.c_str(), extraOptions.c_str());
     printf("\n");
 }
 
@@ -805,7 +796,7 @@
 }
 
 /* adds a file to the existing zipped bugreport */
-static int _add_file_from_fd(const char *title, const char *path, int fd) {
+static int _add_file_from_fd(const char* title __attribute__((unused)), const char* path, int fd) {
     return add_zip_entry_from_fd(ZIP_ROOT_DIR + path, fd) ? 0 : 1;
 }
 
@@ -861,7 +852,8 @@
     RunCommand("IP6TABLE RAW", {"ip6tables", "-t", "raw", "-L", "-nvx"});
 }
 
-static void dumpstate(const std::string& screenshot_path, const std::string& version) {
+static void dumpstate(const std::string& screenshot_path,
+                      const std::string& version __attribute__((unused))) {
     DurationReporter durationReporter("DUMPSTATE");
     unsigned long timeout;
 
@@ -1193,7 +1185,7 @@
     }
 }
 
-static void sig_handler(int signo) {
+static void sig_handler(int signo __attribute__((unused))) {
     wake_lock_releaser();
     _exit(EXIT_FAILURE);
 }
@@ -1319,8 +1311,7 @@
         register_sig_handler();
     }
 
-    ds.dryRun_ = android::base::GetBoolProperty("dumpstate.dry_run", false);
-    if (ds.dryRun_) {
+    if (ds.IsDryRun()) {
         MYLOGI("Running on dry-run mode (to disable it, call 'setprop dumpstate.dry_run false')\n");
     }
 
diff --git a/cmds/dumpstate/dumpstate.h b/cmds/dumpstate/dumpstate.h
index 70af270..1c570b0 100644
--- a/cmds/dumpstate/dumpstate.h
+++ b/cmds/dumpstate/dumpstate.h
@@ -202,6 +202,8 @@
  * that are spread accross utils.cpp and dumpstate.cpp will be moved to it.
  */
 class Dumpstate {
+    friend class DumpstateTest;
+
   public:
     static Dumpstate& GetInstance();
 
@@ -274,15 +276,18 @@
     // When set, defines a socket file-descriptor use to report progress to bugreportz.
     int controlSocketFd_ = -1;
 
-    // Whether this is a dry run.
-    bool dryRun_;
+    // Build type (such as 'user' or 'eng').
+    std::string buildType_;
 
     // Full path of the directory where the bugreport files will be written;
     std::string bugreportDir_;
 
   private:
+    // Whether this is a dry run.
+    bool dryRun_;
+
     // Used by GetInstance() only.
-    Dumpstate();
+    Dumpstate(bool dryRun = false);
 };
 
 // for_each_pid_func = void (*)(int, const char*);
diff --git a/cmds/dumpstate/tests/dumpstate_test.cpp b/cmds/dumpstate/tests/dumpstate_test.cpp
new file mode 100644
index 0000000..9e59080
--- /dev/null
+++ b/cmds/dumpstate/tests/dumpstate_test.cpp
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2016 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 "dumpstate.h"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <libgen.h>
+#include <unistd.h>
+
+#include <android-base/file.h>
+
+using ::testing::EndsWith;
+using ::testing::IsEmpty;
+using ::testing::StrEq;
+using ::testing::StartsWith;
+using ::testing::Test;
+using ::testing::internal::CaptureStderr;
+using ::testing::internal::CaptureStdout;
+using ::testing::internal::GetCapturedStderr;
+using ::testing::internal::GetCapturedStdout;
+
+// Not used on test cases yet...
+void dumpstate_board(void) {
+}
+
+class DumpstateTest : public Test {
+  public:
+    void SetUp() {
+        SetDryRun(false);
+    }
+
+    // Runs a command and capture `stdout` and `stderr`.
+    int RunCommand(const std::string& title, const std::vector<std::string>& fullCommand,
+                   const CommandOptions& options = CommandOptions::DEFAULT) {
+        CaptureStdout();
+        CaptureStderr();
+        int status = ds_.RunCommand(title, fullCommand, options);
+        out = GetCapturedStdout();
+        err = GetCapturedStderr();
+        return status;
+    }
+
+    // `stdout` and `stderr` from the last command ran.
+    std::string out, err;
+
+    std::string testPath = dirname(android::base::GetExecutablePath().c_str());
+    std::string simpleBin = testPath + "/../dumpstate_test_fixture/dumpstate_test_fixture";
+
+    void SetDryRun(bool dryRun) {
+        ds_.dryRun_ = dryRun;
+    }
+
+  private:
+    Dumpstate& ds_ = Dumpstate::GetInstance();
+};
+
+TEST_F(DumpstateTest, RunCommandNoArgs) {
+    EXPECT_EQ(-1, RunCommand("", {}));
+}
+
+TEST_F(DumpstateTest, RunCommandNoTitle) {
+    EXPECT_EQ(0, RunCommand("", {simpleBin}));
+    EXPECT_THAT(out, StrEq("stdout\n"));
+    EXPECT_THAT(err, StrEq("stderr\n"));
+}
+
+TEST_F(DumpstateTest, RunCommandWithTitle) {
+    EXPECT_EQ(0, RunCommand("I AM GROOT", {simpleBin}));
+    EXPECT_THAT(err, StrEq("stderr\n"));
+    // We don't know the exact duration, so we check the prefix and suffix
+    EXPECT_THAT(out, StartsWith("------ I AM GROOT (" + simpleBin + ") ------\nstdout\n------"));
+    EXPECT_THAT(out, EndsWith("s was the duration of 'I AM GROOT' ------\n"));
+}
+
+TEST_F(DumpstateTest, RunCommandRedirectStderr) {
+    EXPECT_EQ(
+        0, RunCommand("", {simpleBin}, CommandOptions::WithTimeout(10).RedirectStderr().Build()));
+    EXPECT_THAT(out, IsEmpty());
+    EXPECT_THAT(err, StrEq("stderr\nstdout\n"));
+}
+
+TEST_F(DumpstateTest, RunCommandWithOneArg) {
+    EXPECT_EQ(0, RunCommand("", {simpleBin, "one"}));
+    EXPECT_THAT(err, IsEmpty());
+    EXPECT_THAT(out, StrEq("one\n"));
+}
+
+TEST_F(DumpstateTest, RunCommandWithNoArgs) {
+    EXPECT_EQ(0, RunCommand("", {simpleBin, "one", "is", "the", "loniest", "number"}));
+    EXPECT_THAT(err, IsEmpty());
+    EXPECT_THAT(out, StrEq("one is the loniest number\n"));
+}
+
+TEST_F(DumpstateTest, RunCommandDryRun) {
+    SetDryRun(true);
+    EXPECT_EQ(0, RunCommand("I AM GROOT", {simpleBin}));
+    // We don't know the exact duration, so we check the prefix and suffix
+    EXPECT_THAT(out, StartsWith("------ I AM GROOT (" + simpleBin +
+                                ") ------\n\t(skipped on dry run)\n------"));
+    EXPECT_THAT(out, EndsWith("s was the duration of 'I AM GROOT' ------\n"));
+    EXPECT_THAT(err, IsEmpty());
+}
+
+TEST_F(DumpstateTest, RunCommandDryRunNoTitle) {
+    SetDryRun(true);
+    EXPECT_EQ(0, RunCommand("", {simpleBin}));
+    EXPECT_THAT(out, IsEmpty());
+    EXPECT_THAT(err, IsEmpty());
+}
+
+TEST_F(DumpstateTest, RunCommandDryRunAlways) {
+    SetDryRun(true);
+    EXPECT_EQ(0, RunCommand("", {simpleBin}, CommandOptions::WithTimeout(10).Always().Build()));
+    EXPECT_THAT(out, StrEq("stdout\n"));
+    EXPECT_THAT(err, StrEq("stderr\n"));
+}
+
+// TODO: add test for other scenarios:
+// - AsRoot()
+// - DropRoot
+// - WithLoggingMessage()
+// - command does not exist (invalid path)
+// - command times out
+// - command exits with a different value
+// - command is killed before timed out
+// - test progress
+
+// TODO: test DumpFile()
diff --git a/cmds/dumpstate/tests/dumpstate_test_fixture.cpp b/cmds/dumpstate/tests/dumpstate_test_fixture.cpp
new file mode 100644
index 0000000..5ac4cc7
--- /dev/null
+++ b/cmds/dumpstate/tests/dumpstate_test_fixture.cpp
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016 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 <stdio.h>
+
+/*
+ * Binary used to on RunCommand tests.
+ *
+ * Usage:
+ *
+ * - It returns 0 unless documented otherwise.
+ * - Without any arguments, prints `stdout\n` on `stdout` and `stderr\n` on `stderr`.
+ * - With arguments, prints all arguments on `stdout` separated by ` `.
+ */
+int main(int argc, char* const argv[]) {
+    if (argc > 1) {
+        for (int i = 1; i < argc; i++) {
+            fprintf(stdout, "%s", argv[i]);
+            if (i == argc - 1) {
+                fprintf(stdout, "\n");
+            } else {
+                fprintf(stdout, " ");
+            }
+        }
+        return 0;
+    }
+    fprintf(stdout, "stdout\n");
+    fprintf(stderr, "stderr\n");
+
+    return 0;
+}
diff --git a/cmds/dumpstate/utils.cpp b/cmds/dumpstate/utils.cpp
index e29d90d..55316c5 100644
--- a/cmds/dumpstate/utils.cpp
+++ b/cmds/dumpstate/utils.cpp
@@ -60,6 +60,9 @@
                       const CommandOptions& options = CommandOptions::DEFAULT) {
     return ds.RunCommand(title, fullCommand, options);
 }
+static bool IsDryRun() {
+    return Dumpstate::GetInstance().IsDryRun();
+}
 
 /* list of native processes to include in the native dumps */
 // This matches the /proc/pid/exe link instead of /proc/pid/cmdline.
@@ -154,11 +157,11 @@
     return CommandOptions::CommandOptionsBuilder(timeout);
 }
 
-Dumpstate::Dumpstate() {
+Dumpstate::Dumpstate(bool dryRun) : dryRun_(dryRun) {
 }
 
 Dumpstate& Dumpstate::GetInstance() {
-    static Dumpstate sSingleton;
+    static Dumpstate sSingleton(android::base::GetBoolProperty("dumpstate.dry_run", false));
     return sSingleton;
 }
 
@@ -190,13 +193,16 @@
     return (uint64_t) ts.tv_sec * NANOS_PER_SEC + ts.tv_nsec;
 }
 
-// TODO: temporary function used during the C++ refactoring
-static bool is_dry_run() {
-    return Dumpstate::GetInstance().IsDryRun();
+bool Dumpstate::IsDryRun() {
+    return dryRun_;
+}
+
+bool Dumpstate::IsUserBuild() {
+    return "user" == buildType_;
 }
 
 void for_each_userid(void (*func)(int), const char *header) {
-    if (is_dry_run()) return;
+    if (IsDryRun()) return;
 
     DIR *d;
     struct dirent *de;
@@ -279,7 +285,7 @@
 }
 
 void for_each_pid(for_each_pid_func func, const char *header) {
-    if (is_dry_run()) return;
+    if (IsDryRun()) return;
 
     __for_each_pid(for_each_pid_helper, header, (void *) func);
 }
@@ -333,13 +339,13 @@
 }
 
 void for_each_tid(for_each_tid_func func, const char *header) {
-    if (is_dry_run()) return;
+    if (IsDryRun()) return;
 
     __for_each_pid(for_each_tid_helper, header, (void *) func);
 }
 
 void show_wchan(int pid, int tid, const char *name) {
-    if (is_dry_run()) return;
+    if (IsDryRun()) return;
 
     char path[255];
     char buffer[255];
@@ -406,7 +412,7 @@
 }
 
 void show_showtime(int pid, const char *name) {
-    if (is_dry_run()) return;
+    if (IsDryRun()) return;
 
     char path[255];
     char buffer[1023];
@@ -474,7 +480,7 @@
     DurationReporter duration_reporter(title);
     printf("------ %s ------\n", title);
 
-    if (is_dry_run()) return;
+    if (IsDryRun()) return;
 
     /* Get size of kernel buffer */
     int size = klogctl(KLOG_SIZE_BUFFER, NULL, 0);
@@ -526,7 +532,7 @@
         }
         printf(") ------\n");
     }
-    if (is_dry_run()) {
+    if (IsDryRun()) {
         update_progress(WEIGHT_FILE);
         close(fd);
         return 0;
@@ -627,7 +633,7 @@
     if (!title.empty()) {
         printf("------ %s (%s) ------\n", title.c_str(), dir);
     }
-    if (is_dry_run()) return 0;
+    if (IsDryRun()) return 0;
 
     if (dir[strlen(dir) - 1] == '/') {
         ++slash;
@@ -684,7 +690,7 @@
  * stuck.
  */
 int dump_file_from_fd(const char *title, const char *path, int fd) {
-    if (is_dry_run()) return 0;
+    if (IsDryRun()) return 0;
 
     int flags = fcntl(fd, F_GETFL);
     if (flags == -1) {
@@ -796,7 +802,10 @@
         MYLOGI(loggingMessage.c_str(), commandString.c_str());
     }
 
-    if (is_dry_run() && !options.Always()) {
+    if (IsDryRun() && !options.Always()) {
+        if (!title.empty()) {
+            printf("\t(skipped on dry run)\n");
+        }
         update_progress(options.Timeout());
         return 0;
     }
@@ -988,7 +997,7 @@
     const char* title = "SYSTEM PROPERTIES";
     DurationReporter duration_reporter(title);
     printf("------ %s ------\n", title);
-    if (is_dry_run()) return;
+    if (IsDryRun()) return;
     size_t i;
     num_props = 0;
     property_list(print_prop, NULL);
@@ -1094,7 +1103,7 @@
 /* dump Dalvik and native stack traces, return the trace file location (NULL if none) */
 const char *dump_traces() {
     DurationReporter duration_reporter("DUMP TRACES", nullptr);
-    if (is_dry_run()) return nullptr;
+    if (IsDryRun()) return nullptr;
 
     const char* result = nullptr;
 
@@ -1246,7 +1255,7 @@
 
 void dump_route_tables() {
     DurationReporter duration_reporter("DUMP ROUTE TABLES");
-    if (is_dry_run()) return;
+    if (IsDryRun()) return;
     const char* const RT_TABLES_PATH = "/data/misc/net/rt_tables";
     ds.DumpFile("RT_TABLES", RT_TABLES_PATH);
     FILE* fp = fopen(RT_TABLES_PATH, "re");