Add tests for reporter class

Test: Add test for Reporter class
Change-Id: Ic1d87a26dd4b8271bab7b03374c7a1d4d7b87f92
diff --git a/cmds/incidentd/Android.mk b/cmds/incidentd/Android.mk
index a8a5483..537c910 100644
--- a/cmds/incidentd/Android.mk
+++ b/cmds/incidentd/Android.mk
@@ -79,7 +79,10 @@
     src/Reporter.cpp \
     src/Section.cpp \
     src/protobuf.cpp \
+    src/report_directory.cpp \
+    src/section_list.cpp \
     tests/FdBuffer_test.cpp \
+    tests/Reporter_test.cpp \
     tests/Section_test.cpp \
 
 LOCAL_STATIC_LIBRARIES := \
diff --git a/cmds/incidentd/src/IncidentService.cpp b/cmds/incidentd/src/IncidentService.cpp
index 7c6789e..c4b54bb 100644
--- a/cmds/incidentd/src/IncidentService.cpp
+++ b/cmds/incidentd/src/IncidentService.cpp
@@ -153,7 +153,6 @@
             break;
         }
         reporter->batch.add(request);
-        reporter->args.merge(request->args);
     }
 
     // Take the report, which might take a while. More requests might queue
@@ -235,7 +234,7 @@
         return Status::fromExceptionCode(Status::EX_SECURITY,
                 "Only system uid can call systemRunning");
     }
-    
+
     // When system_server is up and running, schedule the dropbox task to run.
     mHandler->scheduleSendBacklogToDropbox();
 
diff --git a/cmds/incidentd/src/Reporter.cpp b/cmds/incidentd/src/Reporter.cpp
index 2a4f89e..ea73bcd 100644
--- a/cmds/incidentd/src/Reporter.cpp
+++ b/cmds/incidentd/src/Reporter.cpp
@@ -35,7 +35,7 @@
 /**
  * The directory where the incident reports are stored.
  */
-static const String8 INCIDENT_DIRECTORY("/data/misc/incidents");
+static const char* INCIDENT_DIRECTORY = "/data/misc/incidents/";
 
 // ================================================================================
 static status_t write_all(int fd, uint8_t const* buf, size_t size)
@@ -68,6 +68,7 @@
 // ================================================================================
 ReportRequestSet::ReportRequestSet()
     :mRequests(),
+     mSections(),
      mWritableCount(0),
      mMainFd(-1)
 {
@@ -77,10 +78,12 @@
 {
 }
 
+// TODO: dedup on exact same args and fd, report the status back to listener!
 void
 ReportRequestSet::add(const sp<ReportRequest>& request)
 {
     mRequests.push_back(request);
+    mSections.merge(request->args);
     mWritableCount++;
 }
 
@@ -122,11 +125,16 @@
     return mWritableCount > 0 ? NO_ERROR : err;
 }
 
+bool
+ReportRequestSet::containsSection(int id) {
+    return mSections.containsSection(id);
+}
 
 // ================================================================================
-Reporter::Reporter()
-    :args(),
-     batch()
+Reporter::Reporter() : Reporter(INCIDENT_DIRECTORY) { isTest = false; };
+
+Reporter::Reporter(const char* directory)
+    :batch()
 {
     char buf[100];
 
@@ -134,10 +142,15 @@
     mMaxSize = 100 * 1024 * 1024;
     mMaxCount = 100;
 
+    // string ends up with '/' is a directory
+    String8 dir = String8(directory);
+    if (directory[dir.size() - 1] != '/') dir += "/";
+    mIncidentDirectory = dir.string();
+
     // There can't be two at the same time because it's on one thread.
     mStartTime = time(NULL);
-    strftime(buf, sizeof(buf), "/incident-%Y%m%d-%H%M%S", localtime(&mStartTime));
-    mFilename = INCIDENT_DIRECTORY + buf;
+    strftime(buf, sizeof(buf), "incident-%Y%m%d-%H%M%S", localtime(&mStartTime));
+    mFilename = mIncidentDirectory + buf;
 }
 
 Reporter::~Reporter()
@@ -161,7 +174,7 @@
     }
     if (needMainFd) {
         // Create the directory
-        err = create_directory(INCIDENT_DIRECTORY);
+        if (!isTest) err = create_directory(mIncidentDirectory);
         if (err != NO_ERROR) {
             goto done;
         }
@@ -169,7 +182,7 @@
         // If there are too many files in the directory (for whatever reason),
         // delete the oldest ones until it's under the limit. Doing this first
         // does mean that we can go over, so the max size is not a hard limit.
-        clean_directory(INCIDENT_DIRECTORY, mMaxSize, mMaxCount);
+        if (!isTest) clean_directory(mIncidentDirectory, mMaxSize, mMaxCount);
 
         // Open the file.
         err = create_file(&mainFd);
@@ -214,7 +227,7 @@
         const int id = (*section)->id;
         ALOGD("Taking incident report section %d '%s'", id, (*section)->name.string());
 
-        if (this->args.containsSection(id)) {
+        if (this->batch.containsSection(id)) {
             // Notify listener of starting
             for (ReportRequestSet::iterator it=batch.begin(); it!=batch.end(); it++) {
                 if ((*it)->listener != NULL && (*it)->args.containsSection(id)) {
@@ -270,7 +283,7 @@
         // If the status was ok, delete the file. If not, leave it around until the next
         // boot or the next checkin. If the directory gets too big older files will
         // be rotated out.
-        unlink(mFilename.c_str());
+        if(!isTest) unlink(mFilename.c_str());
     }
 
     return REPORT_FINISHED;
@@ -284,7 +297,7 @@
 {
     const char* filename = mFilename.c_str();
 
-    *fd = open(filename, O_CREAT | O_TRUNC | O_RDWR, 0660);
+    *fd = open(filename, O_CREAT | O_TRUNC | O_RDWR | O_CLOEXEC, 0660);
     if (*fd < 0) {
         ALOGE("Couldn't open incident file: %s (%s)", filename, strerror(errno));
         return -errno;
@@ -303,20 +316,24 @@
     return NO_ERROR;
 }
 
-// ================================================================================
 Reporter::run_report_status_t
 Reporter::upload_backlog()
 {
     DIR* dir;
     struct dirent* entry;
     struct stat st;
+    status_t err;
 
-    if ((dir = opendir(INCIDENT_DIRECTORY.string())) == NULL) {
-        ALOGE("Couldn't open incident directory: %s", INCIDENT_DIRECTORY.string());
+    if ((err = create_directory(INCIDENT_DIRECTORY)) != NO_ERROR) {
+        ALOGE("directory doesn't exist: %s", strerror(-err));
+        return REPORT_FINISHED;
+    }
+
+    if ((dir = opendir(INCIDENT_DIRECTORY)) == NULL) {
+        ALOGE("Couldn't open incident directory: %s", INCIDENT_DIRECTORY);
         return REPORT_NEEDS_DROPBOX;
     }
 
-    String8 dirbase(INCIDENT_DIRECTORY + "/");
     sp<DropBoxManager> dropbox = new DropBoxManager();
 
     // Enumerate, count and add up size
@@ -324,7 +341,7 @@
         if (entry->d_name[0] == '.') {
             continue;
         }
-        String8 filename = dirbase + entry->d_name;
+        String8 filename = String8(INCIDENT_DIRECTORY) + entry->d_name;
         if (stat(filename.string(), &st) != 0) {
             ALOGE("Unable to stat file %s", filename.string());
             continue;
diff --git a/cmds/incidentd/src/Reporter.h b/cmds/incidentd/src/Reporter.h
index 5b86561..509611c 100644
--- a/cmds/incidentd/src/Reporter.h
+++ b/cmds/incidentd/src/Reporter.h
@@ -62,8 +62,10 @@
     iterator begin() { return mRequests.begin(); }
     iterator end() { return mRequests.end(); }
 
+    bool containsSection(int id);
 private:
     vector<sp<ReportRequest>> mRequests;
+    IncidentReportArgs mSections;
     int mWritableCount;
     int mMainFd;
 };
@@ -77,10 +79,10 @@
         REPORT_NEEDS_DROPBOX = 1
     };
 
-    IncidentReportArgs args;
     ReportRequestSet batch;
 
     Reporter();
+    Reporter(const char* directory);
     virtual ~Reporter();
 
     // Run the report as described in the batch and args parameters.
@@ -89,12 +91,16 @@
     static run_report_status_t upload_backlog();
 
 private:
+    String8 mIncidentDirectory;
+
     string mFilename;
     off_t mMaxSize;
     size_t mMaxCount;
     time_t mStartTime;
 
     status_t create_file(int* fd);
+
+    bool isTest = true; // default to true for testing
 };
 
 
diff --git a/cmds/incidentd/src/Section.cpp b/cmds/incidentd/src/Section.cpp
index 8ef6817..e3f2b36 100644
--- a/cmds/incidentd/src/Section.cpp
+++ b/cmds/incidentd/src/Section.cpp
@@ -424,7 +424,7 @@
     status_t cmdStatus = waitForChild(cmdPid);
     status_t ihStatus  = waitForChild(ihPid);
     if (cmdStatus != NO_ERROR || ihStatus != NO_ERROR) {
-        ALOGW("CommandSection '%s' abnormal child processes, return status: command: %s, incidnet helper: %s",
+        ALOGW("CommandSection '%s' abnormal child processes, return status: command: %s, incident helper: %s",
             this->name.string(), strerror(-cmdStatus), strerror(-ihStatus));
         return cmdStatus != NO_ERROR ? cmdStatus : ihStatus;
     }
diff --git a/cmds/incidentd/src/report_directory.cpp b/cmds/incidentd/src/report_directory.cpp
index f60b8ac..110902c 100644
--- a/cmds/incidentd/src/report_directory.cpp
+++ b/cmds/incidentd/src/report_directory.cpp
@@ -129,7 +129,8 @@
         return;
     }
 
-    String8 dirbase(String8(directory) + "/");
+    String8 dirbase(directory);
+    if (directory[dirbase.size() - 1] != '/') dirbase += "/";
 
     off_t totalSize = 0;
     size_t totalCount = 0;
diff --git a/cmds/incidentd/tests/Reporter_test.cpp b/cmds/incidentd/tests/Reporter_test.cpp
new file mode 100644
index 0000000..a774741
--- /dev/null
+++ b/cmds/incidentd/tests/Reporter_test.cpp
@@ -0,0 +1,197 @@
+// Copyright (C) 2017 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.
+
+#define LOG_TAG "incidentd"
+
+#include "Reporter.h"
+
+#include <android/os/BnIncidentReportStatusListener.h>
+
+#include <android-base/file.h>
+#include <android-base/test_utils.h>
+#include <dirent.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <string.h>
+
+
+using namespace android;
+using namespace android::base;
+using namespace android::binder;
+using namespace std;
+using ::testing::StrEq;
+using ::testing::Test;
+using ::testing::internal::CaptureStdout;
+using ::testing::internal::GetCapturedStdout;
+
+class TestListener : public IIncidentReportStatusListener
+{
+public:
+    int startInvoked;
+    int finishInvoked;
+    int failedInvoked;
+    map<int, int> startSections;
+    map<int, int> finishSections;
+
+    TestListener() : startInvoked(0), finishInvoked(0), failedInvoked(0) {};
+    virtual ~TestListener() {};
+
+    virtual Status onReportStarted() {
+        startInvoked++;
+        return Status::ok();
+    };
+    virtual Status onReportSectionStatus(int section, int status) {
+        switch (status) {
+        case IIncidentReportStatusListener::STATUS_STARTING:
+            if (startSections.count(section) == 0)
+                startSections[section] = 0;
+            startSections[section] = startSections[section] + 1;
+            break;
+        case IIncidentReportStatusListener::STATUS_FINISHED:
+            if (finishSections.count(section) == 0)
+                finishSections[section] = 0;
+            finishSections[section] = finishSections[section] + 1;
+            break;
+        }
+        return Status::ok();
+    };
+    virtual Status onReportFinished() {
+        finishInvoked++;
+        return Status::ok();
+    };
+    virtual Status onReportFailed() {
+        failedInvoked++;
+        return Status::ok();
+    };
+
+protected:
+    IBinder* onAsBinder() override { return nullptr; };
+
+};
+
+class ReporterTest : public Test {
+public:
+    virtual void SetUp() {
+        reporter = new Reporter(td.path);
+        l = new TestListener();
+    }
+
+    vector<string> InspectFiles() {
+        DIR* dir;
+        struct dirent* entry;
+        vector<string> results;
+
+        string dirbase = string(td.path) + "/";
+        dir = opendir(td.path);
+
+        while ((entry = readdir(dir)) != NULL) {
+            if (entry->d_name[0] == '.') {
+                continue;
+            }
+            string filename = dirbase + entry->d_name;
+            string content;
+            ReadFileToString(filename, &content);
+            results.push_back(content);
+        }
+        return results;
+    }
+
+protected:
+    TemporaryDir td;
+    ReportRequestSet requests;
+    sp<Reporter> reporter;
+    sp<TestListener> l;
+};
+
+TEST_F(ReporterTest, IncidentReportArgs) {
+    IncidentReportArgs args1, args2;
+    args1.addSection(1);
+    args2.addSection(3);
+
+    args1.merge(args2);
+    ASSERT_TRUE(args1.containsSection(1));
+    ASSERT_FALSE(args1.containsSection(2));
+    ASSERT_TRUE(args1.containsSection(3));
+}
+
+TEST_F(ReporterTest, ReportRequestSetEmpty) {
+    requests.setMainFd(STDOUT_FILENO);
+
+    CaptureStdout();
+    requests.write((uint8_t *) "abcdef", 6);
+    EXPECT_THAT(GetCapturedStdout(), StrEq("abcdef"));
+}
+
+TEST_F(ReporterTest, WriteToStreamFdAndMainFd) {
+    TemporaryFile tf;
+    IncidentReportArgs args;
+    sp<ReportRequest> r = new ReportRequest(args, l, tf.fd);
+
+    requests.add(r);
+    requests.setMainFd(STDOUT_FILENO);
+
+    const char* data = "abcdef";
+
+    CaptureStdout();
+    requests.write((uint8_t *) data, 6);
+    EXPECT_THAT(GetCapturedStdout(), StrEq(data));
+
+    string content;
+    ASSERT_TRUE(ReadFileToString(tf.path, &content));
+    EXPECT_THAT(content, StrEq(data));
+}
+
+TEST_F(ReporterTest, RunReportEmpty) {
+    ASSERT_EQ(Reporter::REPORT_FINISHED, reporter->runReport());
+    EXPECT_EQ(l->startInvoked, 0);
+    EXPECT_EQ(l->finishInvoked, 0);
+    EXPECT_TRUE(l->startSections.empty());
+    EXPECT_TRUE(l->finishSections.empty());
+    EXPECT_EQ(l->failedInvoked, 0);
+}
+
+TEST_F(ReporterTest, RunReportWithHeaders) {
+    IncidentReportArgs args1, args2;
+    args1.addSection(1);
+    args2.addSection(2);
+    std::vector<int8_t> header {'a', 'b', 'c', 'd', 'e'};
+    args2.addHeader(header);
+    sp<ReportRequest> r1 = new ReportRequest(args1, l, STDOUT_FILENO);
+    sp<ReportRequest> r2 = new ReportRequest(args2, l, STDOUT_FILENO);
+
+    reporter->batch.add(r1);
+    reporter->batch.add(r2);
+
+    CaptureStdout();
+    ASSERT_EQ(Reporter::REPORT_FINISHED, reporter->runReport());
+    EXPECT_THAT(GetCapturedStdout(), StrEq("\n\x5" "abcde"));
+    EXPECT_EQ(l->startInvoked, 2);
+    EXPECT_EQ(l->finishInvoked, 2);
+    EXPECT_TRUE(l->startSections.empty());
+    EXPECT_TRUE(l->finishSections.empty());
+    EXPECT_EQ(l->failedInvoked, 0);
+}
+
+TEST_F(ReporterTest, RunReportToGivenDirectory) {
+    IncidentReportArgs args;
+    args.addHeader({'1', '2', '3'});
+    args.addHeader({'a', 'b', 'c', 'd'});
+    sp<ReportRequest> r = new ReportRequest(args, l, -1);
+    reporter->batch.add(r);
+
+    ASSERT_EQ(Reporter::REPORT_FINISHED, reporter->runReport());
+    vector<string> results = InspectFiles();
+    ASSERT_EQ((int)results.size(), 1);
+    EXPECT_EQ(results[0], "\n\x3" "123\n\x4" "abcd");
+}