// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/browser_watcher/postmortem_report_collector.h"

#include <stdint.h>

#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>

#include "base/debug/activity_analyzer.h"
#include "base/debug/activity_tracker.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/memory_mapped_file.h"
#include "base/files/scoped_file.h"
#include "base/files/scoped_temp_dir.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/persistent_memory_allocator.h"
#include "base/process/process_handle.h"
#include "base/stl_util.h"
#include "base/test/histogram_tester.h"
#include "base/threading/platform_thread.h"
#include "components/browser_watcher/stability_data_names.h"
#include "components/browser_watcher/stability_report_extractor.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/crashpad/crashpad/client/crash_report_database.h"

namespace browser_watcher {

using base::debug::ActivityData;
using base::debug::ActivityTrackerMemoryAllocator;
using base::debug::ActivityUserData;
using base::debug::GlobalActivityTracker;
using base::debug::ThreadActivityTracker;
using base::File;
using base::FilePath;
using base::FilePersistentMemoryAllocator;
using base::MemoryMappedFile;
using base::PersistentMemoryAllocator;
using base::WrapUnique;
using crashpad::CrashReportDatabase;
using crashpad::Settings;
using crashpad::UUID;
using testing::_;
using testing::DoAll;
using testing::Return;
using testing::SetArgPointee;

namespace {

TEST(PostmortemDeleterTest, BasicTest) {
  base::HistogramTester histogram_tester;
  base::ScopedTempDir temp_dir;
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());

  // Create three files.
  FilePath path_one = temp_dir.GetPath().AppendASCII("a.pma");
  FilePath path_two = temp_dir.GetPath().AppendASCII("b.pma");
  FilePath path_three = temp_dir.GetPath().AppendASCII("c.pma");

  std::vector<FilePath> stability_files = {path_one, path_two, path_three};

  for (const FilePath& path : stability_files) {
    {
      base::ScopedFILE file(base::OpenFile(path, "w"));
      ASSERT_NE(file.get(), nullptr);
    }
    ASSERT_TRUE(base::PathExists(path));
  }

  // Open one stability file to prevent its deletion.
  base::ScopedFILE file(base::OpenFile(path_two, "w"));
  ASSERT_NE(file.get(), nullptr);

  // Validate deletion and metrics.
  PostmortemDeleter deleter;
  deleter.Process(stability_files);

  ASSERT_FALSE(base::PathExists(path_one));
  ASSERT_FALSE(base::PathExists(path_three));
  histogram_tester.ExpectBucketCount("ActivityTracker.Collect.Status",
                                     UNCLEAN_SHUTDOWN, 2);

  ASSERT_TRUE(base::PathExists(path_two));
  histogram_tester.ExpectBucketCount("ActivityTracker.Collect.Status",
                                     DEBUG_FILE_DELETION_FAILED, 1);

  std::vector<CollectionStatus> unexpected_statuses = {
      NONE,
      SUCCESS,
      ANALYZER_CREATION_FAILED,
      DEBUG_FILE_NO_DATA,
      PREPARE_NEW_CRASH_REPORT_FAILED,
      WRITE_TO_MINIDUMP_FAILED,
      FINISHED_WRITING_CRASH_REPORT_FAILED,
      COLLECTION_ATTEMPT};
  for (CollectionStatus status : unexpected_statuses) {
    histogram_tester.ExpectBucketCount("ActivityTracker.Collect.Status", status,
                                       0);
  }
}

const char kProductName[] = "TestProduct";
const char kVersionNumber[] = "TestVersionNumber";
const char kChannelName[] = "TestChannel";

// Exposes a public constructor in order to create a dummy database.
class MockCrashReportDatabase : public CrashReportDatabase {
 public:
  MockCrashReportDatabase() {}
  MOCK_METHOD0(GetSettings, Settings*());
  MOCK_METHOD1(PrepareNewCrashReport,
               CrashReportDatabase::CrashReportDatabase::OperationStatus(
                   NewReport** report));
  MOCK_METHOD2(FinishedWritingCrashReport,
               CrashReportDatabase::CrashReportDatabase::OperationStatus(
                   CrashReportDatabase::NewReport* report,
                   crashpad::UUID* uuid));
  MOCK_METHOD1(ErrorWritingCrashReport,
               CrashReportDatabase::CrashReportDatabase::OperationStatus(
                   NewReport* report));
  MOCK_METHOD2(LookUpCrashReport,
               CrashReportDatabase::CrashReportDatabase::OperationStatus(
                   const UUID& uuid,
                   Report* report));
  MOCK_METHOD1(
      GetPendingReports,
      CrashReportDatabase::OperationStatus(std::vector<Report>* reports));
  MOCK_METHOD1(
      GetCompletedReports,
      CrashReportDatabase::OperationStatus(std::vector<Report>* reports));
  MOCK_METHOD2(GetReportForUploading,
               CrashReportDatabase::OperationStatus(const UUID& uuid,
                                                    const Report** report));
  MOCK_METHOD3(RecordUploadAttempt,
               CrashReportDatabase::OperationStatus(const Report* report,
                                                    bool successful,
                                                    const std::string& id));
  MOCK_METHOD2(SkipReportUpload,
               CrashReportDatabase::OperationStatus(
                   const UUID& uuid,
                   crashpad::Metrics::CrashSkippedReason reason));
  MOCK_METHOD1(DeleteReport,
               CrashReportDatabase::OperationStatus(const UUID& uuid));
  MOCK_METHOD1(RequestUpload,
               CrashReportDatabase::OperationStatus(const UUID& uuid));
};

class MockPostmortemReportCollector : public PostmortemReportCollector {
 public:
  explicit MockPostmortemReportCollector(CrashReportDatabase* crash_database)
      : PostmortemReportCollector(kProductName,
                                  kVersionNumber,
                                  kChannelName,
                                  crash_database,
                                  nullptr) {}

  MOCK_METHOD2(CollectOneReport,
               CollectionStatus(const FilePath&, StabilityReport* report));
  MOCK_METHOD4(WriteReportToMinidump,
               bool(StabilityReport* report,
                    const crashpad::UUID& client_id,
                    const crashpad::UUID& report_id,
                    base::PlatformFile minidump_file));
};

class MockSystemSessionAnalyzer : public SystemSessionAnalyzer {
 public:
  MockSystemSessionAnalyzer() : SystemSessionAnalyzer(10U) {}
  MOCK_METHOD1(IsSessionUnclean, Status(base::Time timestamp));
};

// Checks if two proto messages are the same based on their serializations. Note
// this only works if serialization is deterministic, which is not guaranteed.
// In practice, serialization is deterministic (even for protocol buffers with
// maps) and such matchers are common in the Chromium code base. Also note that
// in the context of this test, false positive matches are the problem and these
// are not possible (otherwise serialization would be ambiguous). False
// negatives would lead to test failure and developer action. Alternatives are:
// 1) a generic matcher (likely not possible without reflections, missing from
// lite runtime),  2) a specialized matcher or 3) implementing deterministic
// serialization.
// TODO(manzagop): switch a matcher with guarantees.
MATCHER_P(EqualsProto, message, "") {
  std::string expected_serialized;
  std::string actual_serialized;
  message.SerializeToString(&expected_serialized);
  arg.SerializeToString(&actual_serialized);
  return expected_serialized == actual_serialized;
}

}  // namespace

class PostmortemReportCollectorProcessTest : public testing::Test {
 public:
  void SetUpTest(bool system_session_clean, bool expect_write_dump) {
    collector_.reset(new MockPostmortemReportCollector(&database_));

    // Create a dummy debug file.
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
    debug_file_ = temp_dir_.GetPath().AppendASCII("foo-1.pma");
    {
      base::ScopedFILE file(base::OpenFile(debug_file_, "w"));
      ASSERT_NE(file.get(), nullptr);
    }
    ASSERT_TRUE(base::PathExists(debug_file_));

    EXPECT_CALL(database_, GetSettings()).Times(1).WillOnce(Return(nullptr));

    // Expect a single collection call.
    StabilityReport report;
    report.mutable_system_state()->set_session_state(
        system_session_clean ? SystemState::CLEAN : SystemState::UNCLEAN);
    EXPECT_CALL(*collector_, CollectOneReport(debug_file_, _))
        .Times(1)
        .WillOnce(DoAll(SetArgPointee<1>(report), Return(SUCCESS)));

    if (!expect_write_dump)
      return;

    // Expect the call to write the proto to a minidump. This involves
    // requesting a report from the crashpad database, writing the report, then
    // finalizing it with the database.
    FilePath minidump_path = temp_dir_.GetPath().AppendASCII("foo-1.dmp");
    base::File minidump_file(
        minidump_path, base::File::FLAG_CREATE | base::File::File::FLAG_WRITE);
    crashpad::UUID new_report_uuid;
    new_report_uuid.InitializeWithNew();
    crashpad_report_ = {minidump_file.GetPlatformFile(), new_report_uuid,
                        minidump_path};
    EXPECT_CALL(database_, PrepareNewCrashReport(_))
        .Times(1)
        .WillOnce(DoAll(SetArgPointee<0>(&crashpad_report_),
                        Return(CrashReportDatabase::kNoError)));

    EXPECT_CALL(*collector_,
                WriteReportToMinidump(_, _, _, minidump_file.GetPlatformFile()))
        .Times(1)
        .WillOnce(Return(true));
  }
  void ValidateHistograms(int unclean_cnt, int unclean_system_cnt) {
    histogram_tester_.ExpectBucketCount("ActivityTracker.Collect.Status",
                                        UNCLEAN_SHUTDOWN, unclean_cnt);
    histogram_tester_.ExpectBucketCount("ActivityTracker.Collect.Status",
                                        UNCLEAN_SESSION, unclean_system_cnt);
  }
  void CollectReports(bool is_session_clean) {
    SetUpTest(is_session_clean, true);

    EXPECT_CALL(database_, FinishedWritingCrashReport(&crashpad_report_, _))
        .Times(1)
        .WillOnce(Return(CrashReportDatabase::kNoError));

    // Run the test.
    std::vector<FilePath> debug_files{debug_file_};
    collector_->Process(debug_files);
    ASSERT_FALSE(base::PathExists(debug_file_));
  }

 protected:
  base::HistogramTester histogram_tester_;
  base::ScopedTempDir temp_dir_;
  FilePath debug_file_;
  MockCrashReportDatabase database_;
  std::unique_ptr<MockPostmortemReportCollector> collector_;
  CrashReportDatabase::NewReport crashpad_report_;
};

TEST_F(PostmortemReportCollectorProcessTest, ProcessCleanSession) {
  CollectReports(true);
  int expected_unclean = 1;
  int expected_system_unclean = 0;
  ValidateHistograms(expected_unclean, expected_system_unclean);
}

TEST_F(PostmortemReportCollectorProcessTest, ProcessUncleanSession) {
  CollectReports(false);
  int expected_unclean = 1;
  int expected_system_unclean = 1;
  ValidateHistograms(expected_unclean, expected_system_unclean);
}

TEST_F(PostmortemReportCollectorProcessTest, ProcessStuckFile) {
  bool system_session_clean = true;
  bool expect_write_dump = false;
  SetUpTest(system_session_clean, expect_write_dump);

  // Open the stability debug file to prevent its deletion.
  base::ScopedFILE file(base::OpenFile(debug_file_, "w"));
  ASSERT_NE(file.get(), nullptr);

  // Run the test.
  std::vector<FilePath> debug_files{debug_file_};
  collector_->Process(debug_files);
  ASSERT_TRUE(base::PathExists(debug_file_));

  histogram_tester_.ExpectBucketCount("ActivityTracker.Collect.Status",
                                      DEBUG_FILE_DELETION_FAILED, 1);
  int expected_unclean = 0;
  int expected_system_unclean = 0;
  ValidateHistograms(expected_unclean, expected_system_unclean);
}

TEST(PostmortemReportCollectorTest, CollectEmptyFile) {
  // Create an empty file.
  base::ScopedTempDir temp_dir;
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
  FilePath file_path = temp_dir.GetPath().AppendASCII("empty.pma");
  {
    base::ScopedFILE file(base::OpenFile(file_path, "w"));
    ASSERT_NE(file.get(), nullptr);
  }
  ASSERT_TRUE(PathExists(file_path));

  // Validate collection: an empty file cannot suppport an analyzer.
  MockCrashReportDatabase crash_db;
  PostmortemReportCollector collector(kProductName, kVersionNumber,
                                      kChannelName, &crash_db, nullptr);
  StabilityReport report;
  ASSERT_EQ(ANALYZER_CREATION_FAILED,
            collector.CollectOneReport(file_path, &report));
}

TEST(PostmortemReportCollectorTest, CollectRandomFile) {
  // Create a file with content we don't expect to be valid for a debug file.
  base::ScopedTempDir temp_dir;
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
  FilePath file_path = temp_dir.GetPath().AppendASCII("invalid_content.pma");
  {
    base::ScopedFILE file(base::OpenFile(file_path, "w"));
    ASSERT_NE(file.get(), nullptr);
    // Assuming this size is greater than the minimum size of a debug file.
    std::vector<uint8_t> data(1024);
    for (size_t i = 0; i < data.size(); ++i)
      data[i] = i % UINT8_MAX;
    ASSERT_EQ(data.size(),
              fwrite(&data.at(0), sizeof(uint8_t), data.size(), file.get()));
  }
  ASSERT_TRUE(PathExists(file_path));

  // Validate collection: random content appears as though there is not
  // stability data.
  MockCrashReportDatabase crash_db;
  PostmortemReportCollector collector(kProductName, kVersionNumber,
                                      kChannelName, &crash_db, nullptr);
  StabilityReport report;
  ASSERT_NE(SUCCESS, collector.CollectOneReport(file_path, &report));
}

class PostmortemReportCollectorCollectionFromGlobalTrackerTest
    : public testing::Test {
 public:
  const int kMemorySize = 1 << 20;  // 1MiB

  PostmortemReportCollectorCollectionFromGlobalTrackerTest() {}
  ~PostmortemReportCollectorCollectionFromGlobalTrackerTest() override {
    GlobalActivityTracker* global_tracker = GlobalActivityTracker::Get();
    if (global_tracker) {
      global_tracker->ReleaseTrackerForCurrentThreadForTesting();
      delete global_tracker;
    }
  }

  void SetUp() override {
    testing::Test::SetUp();

    // Set up a debug file path.
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
    debug_file_path_ = temp_dir_.GetPath().AppendASCII("debug.pma");
  }

  const FilePath& debug_file_path() { return debug_file_path_; }

 protected:
  base::ScopedTempDir temp_dir_;
  FilePath debug_file_path_;
};

TEST_F(PostmortemReportCollectorCollectionFromGlobalTrackerTest,
       SystemStateTest) {
  // Setup.
  GlobalActivityTracker::CreateWithFile(debug_file_path(), kMemorySize, 0ULL,
                                        "", 3);
  ActivityUserData& process_data = GlobalActivityTracker::Get()->process_data();
  process_data.SetInt(kStabilityStartTimestamp, 12345LL);

  // Collect.
  MockSystemSessionAnalyzer analyzer;
  EXPECT_CALL(analyzer,
              IsSessionUnclean(base::Time::FromInternalValue(12345LL)))
      .Times(1)
      .WillOnce(Return(SystemSessionAnalyzer::CLEAN));
  MockCrashReportDatabase crash_db;
  PostmortemReportCollector collector(kProductName, kVersionNumber,
                                      kChannelName, &crash_db, &analyzer);
  StabilityReport report;
  ASSERT_EQ(SUCCESS, collector.CollectOneReport(debug_file_path(), &report));

  // Validate the report.
  ASSERT_EQ(SystemState::CLEAN, report.system_state().session_state());
}

}  // namespace browser_watcher
