diff --git a/client/client.gyp b/client/client.gyp index c5602b26..3524be41 100644 --- a/client/client.gyp +++ b/client/client.gyp @@ -40,6 +40,8 @@ 'crashpad_client_win.cc', 'crashpad_info.cc', 'crashpad_info.h', + 'prune_crash_reports.cc', + 'prune_crash_reports.h', 'settings.cc', 'settings.h', 'simple_string_dictionary.cc', diff --git a/client/client_test.gyp b/client/client_test.gyp index cc6893f9..c534bebf 100644 --- a/client/client_test.gyp +++ b/client/client_test.gyp @@ -24,6 +24,7 @@ 'client.gyp:crashpad_client', '../compat/compat.gyp:crashpad_compat', '../test/test.gyp:crashpad_test', + '../third_party/gmock/gmock.gyp:gmock', '../third_party/gtest/gtest.gyp:gtest', '../third_party/gtest/gtest.gyp:gtest_main', '../third_party/mini_chromium/mini_chromium.gyp:base', @@ -35,6 +36,7 @@ 'sources': [ 'capture_context_mac_test.cc', 'crash_report_database_test.cc', + 'prune_crash_reports_test.cc', 'settings_test.cc', 'simple_string_dictionary_test.cc', 'simulate_crash_mac_test.cc', diff --git a/client/crash_report_database.h b/client/crash_report_database.h index baf95adc..1073d053 100644 --- a/client/crash_report_database.h +++ b/client/crash_report_database.h @@ -300,6 +300,13 @@ class CrashReportDatabase { //! \return The operation status code. virtual OperationStatus SkipReportUpload(const UUID& uuid) = 0; + //! \brief Deletes a crash report file and its associated metadata. + //! + //! \param[in] uuid The UUID of the report to delete. + //! + //! \return The operation status code. + virtual OperationStatus DeleteReport(const UUID& uuid) = 0; + protected: CrashReportDatabase() {} diff --git a/client/crash_report_database_mac.mm b/client/crash_report_database_mac.mm index f451efb0..621d8a66 100644 --- a/client/crash_report_database_mac.mm +++ b/client/crash_report_database_mac.mm @@ -125,6 +125,7 @@ class CrashReportDatabaseMac : public CrashReportDatabase { bool successful, const std::string& id) override; OperationStatus SkipReportUpload(const UUID& uuid) override; + OperationStatus DeleteReport(const UUID& uuid) override; private: //! \brief A private extension of the Report class that maintains bookkeeping @@ -476,6 +477,26 @@ CrashReportDatabase::OperationStatus CrashReportDatabaseMac::SkipReportUpload( return kNoError; } +CrashReportDatabase::OperationStatus CrashReportDatabaseMac::DeleteReport( + const UUID& uuid) { + INITIALIZATION_STATE_DCHECK_VALID(initialized_); + + base::FilePath report_path = LocateCrashReport(uuid); + if (report_path.empty()) + return kReportNotFound; + + base::ScopedFD lock(ObtainReportLock(report_path)); + if (!lock.is_valid()) + return kBusyError; + + if (unlink(report_path.value().c_str()) != 0) { + PLOG(ERROR) << "unlink " << report_path.value(); + return kFileSystemError; + } + + return kNoError; +} + base::FilePath CrashReportDatabaseMac::LocateCrashReport(const UUID& uuid) { const std::string target_uuid = uuid.ToString(); for (size_t i = 0; i < arraysize(kReportDirectories); ++i) { diff --git a/client/crash_report_database_test.cc b/client/crash_report_database_test.cc index 9ed7fe98..83b0de0f 100644 --- a/client/crash_report_database_test.cc +++ b/client/crash_report_database_test.cc @@ -497,6 +497,55 @@ TEST_F(CrashReportDatabaseTest, ReportRemoved) { db()->LookUpCrashReport(uuid, &report)); } +TEST_F(CrashReportDatabaseTest, DeleteReport) { + CrashReportDatabase::Report keep_pending; + CrashReportDatabase::Report delete_pending; + CrashReportDatabase::Report keep_completed; + CrashReportDatabase::Report delete_completed; + + CreateCrashReport(&keep_pending); + CreateCrashReport(&delete_pending); + CreateCrashReport(&keep_completed); + CreateCrashReport(&delete_completed); + + EXPECT_TRUE(FileExists(keep_pending.file_path)); + EXPECT_TRUE(FileExists(delete_pending.file_path)); + EXPECT_TRUE(FileExists(keep_completed.file_path)); + EXPECT_TRUE(FileExists(delete_completed.file_path)); + + UploadReport(keep_completed.uuid, true, "1"); + UploadReport(delete_completed.uuid, true, "2"); + + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->LookUpCrashReport(keep_completed.uuid, &keep_completed)); + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->LookUpCrashReport(delete_completed.uuid, &delete_completed)); + + EXPECT_TRUE(FileExists(keep_completed.file_path)); + EXPECT_TRUE(FileExists(delete_completed.file_path)); + + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->DeleteReport(delete_pending.uuid)); + EXPECT_FALSE(FileExists(delete_pending.file_path)); + EXPECT_EQ(CrashReportDatabase::kReportNotFound, + db()->LookUpCrashReport(delete_pending.uuid, &delete_pending)); + EXPECT_EQ(CrashReportDatabase::kReportNotFound, + db()->DeleteReport(delete_pending.uuid)); + + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->DeleteReport(delete_completed.uuid)); + EXPECT_FALSE(FileExists(delete_completed.file_path)); + EXPECT_EQ(CrashReportDatabase::kReportNotFound, + db()->LookUpCrashReport(delete_completed.uuid, &delete_completed)); + EXPECT_EQ(CrashReportDatabase::kReportNotFound, + db()->DeleteReport(delete_completed.uuid)); + + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->LookUpCrashReport(keep_pending.uuid, &keep_pending)); + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->LookUpCrashReport(keep_completed.uuid, &keep_completed)); +} + } // namespace } // namespace test } // namespace crashpad diff --git a/client/crash_report_database_win.cc b/client/crash_report_database_win.cc index f270530b..6d2bf1af 100644 --- a/client/crash_report_database_win.cc +++ b/client/crash_report_database_win.cc @@ -229,6 +229,19 @@ class Metadata { ReportState desired_state, ReportDisk** report_disk); + //! \brief Removes a report from the metadata database, without touching the + //! on-disk file. + //! + //! The returned report is only valid if CrashReportDatabase::kNoError is + //! returned. This will mark the database as dirty. Future metadata + //! operations for this report will not succeed. + //! + //! \param[in] uuid The report identifier to remove. + //! \param[out] report_path The found report's file_path, valid only if + //! CrashReportDatabase::kNoError is returned. + OperationStatus DeleteReport(const UUID& uuid, + base::FilePath* report_path); + private: Metadata(FileHandle handle, const base::FilePath& report_dir); @@ -351,6 +364,20 @@ OperationStatus Metadata::FindSingleReportAndMarkDirty( return os; } +OperationStatus Metadata::DeleteReport(const UUID& uuid, + base::FilePath* report_path) { + auto report_iter = std::find_if( + reports_.begin(), reports_.end(), [uuid](const ReportDisk& report) { + return report.uuid == uuid; + }); + if (report_iter == reports_.end()) + return CrashReportDatabase::kReportNotFound; + *report_path = report_iter->file_path; + reports_.erase(report_iter); + dirty_ = true; + return CrashReportDatabase::kNoError; +} + Metadata::Metadata(FileHandle handle, const base::FilePath& report_dir) : handle_(handle), report_dir_(report_dir), dirty_(false), reports_() { } @@ -531,6 +558,7 @@ class CrashReportDatabaseWin : public CrashReportDatabase { bool successful, const std::string& id) override; OperationStatus SkipReportUpload(const UUID& uuid) override; + OperationStatus DeleteReport(const UUID& uuid) override; private: scoped_ptr AcquireMetadata(); @@ -560,9 +588,6 @@ bool CrashReportDatabaseWin::Initialize() { !CreateDirectoryIfNecessary(base_dir_.Append(kReportsDirectory))) return false; - // TODO(scottmg): When are completed reports pruned from disk? Delete here or - // maybe on AcquireMetadata(). - if (!settings_.Initialize()) return false; @@ -732,6 +757,26 @@ OperationStatus CrashReportDatabaseWin::RecordUploadAttempt( return kNoError; } +OperationStatus CrashReportDatabaseWin::DeleteReport(const UUID& uuid) { + INITIALIZATION_STATE_DCHECK_VALID(initialized_); + + scoped_ptr metadata(AcquireMetadata()); + if (!metadata) + return kDatabaseError; + + base::FilePath report_path; + OperationStatus os = metadata->DeleteReport(uuid, &report_path); + if (os != kNoError) + return os; + + if (!DeleteFile(report_path.value().c_str())) { + PLOG(ERROR) << "DeleteFile " + << base::UTF16ToUTF8(report_path.value()); + return kFileSystemError; + } + return kNoError; +} + OperationStatus CrashReportDatabaseWin::SkipReportUpload(const UUID& uuid) { INITIALIZATION_STATE_DCHECK_VALID(initialized_); diff --git a/client/prune_crash_reports.cc b/client/prune_crash_reports.cc new file mode 100644 index 00000000..dadb93d7 --- /dev/null +++ b/client/prune_crash_reports.cc @@ -0,0 +1,132 @@ +// Copyright 2015 The Crashpad Authors. All rights reserved. +// +// 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 "client/prune_crash_reports.h" + +#include + +#include +#include + +#include "base/logging.h" + +namespace crashpad { + +void PruneCrashReportDatabase(CrashReportDatabase* database, + PruneCondition* condition) { + std::vector all_reports; + CrashReportDatabase::OperationStatus status; + + status = database->GetPendingReports(&all_reports); + if (status != CrashReportDatabase::kNoError) { + LOG(ERROR) << "PruneCrashReportDatabase: Failed to get pending reports"; + return; + } + + std::vector completed_reports; + status = database->GetCompletedReports(&completed_reports); + if (status != CrashReportDatabase::kNoError) { + LOG(ERROR) << "PruneCrashReportDatabase: Failed to get completed reports"; + return; + } + all_reports.insert(all_reports.end(), completed_reports.begin(), + completed_reports.end()); + + std::sort(all_reports.begin(), all_reports.end(), + [](const CrashReportDatabase::Report& lhs, + const CrashReportDatabase::Report& rhs) { + return lhs.creation_time > rhs.creation_time; + }); + + for (const auto& report : all_reports) { + if (condition->ShouldPruneReport(report)) { + status = database->DeleteReport(report.uuid); + if (status != CrashReportDatabase::kNoError) { + LOG(ERROR) << "Database Pruning: Failed to remove report " + << report.uuid.ToString(); + } + } + } + + // TODO(rsesek): For databases that do not use a directory structure, + // it is possible for the metadata sidecar to become corrupted and thus + // leave orphaned crash report files on-disk. + // https://code.google.com/p/crashpad/issues/detail?id=66 +} + +// static +scoped_ptr PruneCondition::GetDefault() { + // DatabaseSizePruneCondition must be the LHS so that it is always evaluated, + // due to the short-circuting behavior of BinaryPruneCondition. + return make_scoped_ptr(new BinaryPruneCondition(BinaryPruneCondition::OR, + new DatabaseSizePruneCondition(1024 * 128), new AgePruneCondition(365))); +} + +static const time_t kSecondsInDay = 60 * 60 * 24; + +AgePruneCondition::AgePruneCondition(int max_age_in_days) + : oldest_report_time_( + ((time(nullptr) - (max_age_in_days * kSecondsInDay)) + / kSecondsInDay) * kSecondsInDay) {} + +AgePruneCondition::~AgePruneCondition() {} + +bool AgePruneCondition::ShouldPruneReport( + const CrashReportDatabase::Report& report) { + return report.creation_time < oldest_report_time_; +} + +DatabaseSizePruneCondition::DatabaseSizePruneCondition(size_t max_size_in_kb) + : max_size_in_kb_(max_size_in_kb), measured_size_in_kb_(0) {} + +DatabaseSizePruneCondition::~DatabaseSizePruneCondition() {} + +bool DatabaseSizePruneCondition::ShouldPruneReport( + const CrashReportDatabase::Report& report) { +#if defined(OS_POSIX) + struct stat statbuf; + if (stat(report.file_path.value().c_str(), &statbuf) == 0) { +#elif defined(OS_WIN) + struct _stati64 statbuf; + if (_wstat64(report.file_path.value().c_str(), &statbuf) == 0) { +#else +#error "Not implemented" +#endif + // Round up fractional KB to the next 1-KB boundary. + measured_size_in_kb_ += + static_cast((statbuf.st_size + 1023) / 1024); + } + return measured_size_in_kb_ > max_size_in_kb_; +} + +BinaryPruneCondition::BinaryPruneCondition( + Operator op, PruneCondition* lhs, PruneCondition* rhs) + : op_(op), lhs_(lhs), rhs_(rhs) {} + +BinaryPruneCondition::~BinaryPruneCondition() {} + +bool BinaryPruneCondition::ShouldPruneReport( + const CrashReportDatabase::Report& report) { + switch (op_) { + case AND: + return lhs_->ShouldPruneReport(report) && rhs_->ShouldPruneReport(report); + case OR: + return lhs_->ShouldPruneReport(report) || rhs_->ShouldPruneReport(report); + default: + NOTREACHED(); + return false; + } +} + +} // namespace crashpad diff --git a/client/prune_crash_reports.h b/client/prune_crash_reports.h new file mode 100644 index 00000000..1c1ce9c3 --- /dev/null +++ b/client/prune_crash_reports.h @@ -0,0 +1,146 @@ +// Copyright 2015 The Crashpad Authors. All rights reserved. +// +// 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. + +#ifndef CRASHPAD_CLIENT_PRUNE_CRASH_REPORTS_H_ +#define CRASHPAD_CLIENT_PRUNE_CRASH_REPORTS_H_ + +#include +#include + +#include "base/memory/scoped_ptr.h" +#include "client/crash_report_database.h" + +namespace crashpad { + +class PruneCondition; + +//! \brief Deletes crash reports from \a database that match \a condition. +//! +//! This function can be used to remove old or large reports from the database. +//! The \a condition will be evaluated against each report in the \a database, +//! sorted in descending order by CrashReportDatabase::Report::creation_time. +//! This guarantee allows conditions to be stateful. +//! +//! \param[in] database The database from which crash reports will be deleted. +//! \param[in] condition The condition against which all reports in the database +//! will be evaluated. +void PruneCrashReportDatabase(CrashReportDatabase* database, + PruneCondition* condition); + +scoped_ptr GetDefaultDatabasePruneCondition(); + +//! \brief An abstract base class for evaluating crash reports for deletion. +//! +//! When passed to PruneCrashReportDatabase(), each crash report in the +//! database will be evaluated according to ShouldPruneReport(). The reports +//! are evaluated serially in descending sort order by +//! CrashReportDatabase::Report::creation_time. +class PruneCondition { + public: + //! \brief Returns a sensible default condition for removing obsolete crash + //! reports. + //! + //! The default is to keep reports for one year or a maximum database size + //! of 128 MB. + //! + //! \return A PruneCondition for use with PruneCrashReportDatabase(). + static scoped_ptr GetDefault(); + + virtual ~PruneCondition() {} + + //! \brief Evaluates a crash report for deletion. + //! + //! \param[in] report The crash report to evaluate. + //! + //! \return `true` if the crash report should be deleted, `false` if it + //! should be kept. + virtual bool ShouldPruneReport(const CrashReportDatabase::Report& report) = 0; +}; + +//! \brief A PruneCondition that deletes reports older than the specified number +//! days. +class AgePruneCondition final : public PruneCondition { + public: + //! \brief Creates a PruneCondition based on Report::creation_time. + //! + //! \param[in] max_age_in_days Reports created more than this many days ago + //! will be deleted. + explicit AgePruneCondition(int max_age_in_days); + ~AgePruneCondition(); + + bool ShouldPruneReport(const CrashReportDatabase::Report& report) override; + + private: + const time_t oldest_report_time_; + + DISALLOW_COPY_AND_ASSIGN(AgePruneCondition); +}; + +//! \brief A PruneCondition that deletes older reports to keep the total +//! Crashpad database size under the specified limit. +class DatabaseSizePruneCondition final : public PruneCondition { + public: + //! \brief Creates a PruneCondition that will keep newer reports, until the + //! sum of the size of all reports is not smaller than \a max_size_in_kb. + //! After the limit is reached, older reports will be pruned. + //! + //! \param[in] max_size_in_kb The maximum number of kilobytes that all crash + //! reports should consume. + explicit DatabaseSizePruneCondition(size_t max_size_in_kb); + ~DatabaseSizePruneCondition(); + + bool ShouldPruneReport(const CrashReportDatabase::Report& report) override; + + private: + const size_t max_size_in_kb_; + size_t measured_size_in_kb_; + + DISALLOW_COPY_AND_ASSIGN(DatabaseSizePruneCondition); +}; + +//! \breif A PruneCondition that conjoins two other PruneConditions. +class BinaryPruneCondition final : public PruneCondition { + public: + enum Operator { + AND, + OR, + }; + + //! \brief Evaluates two sub-conditions according to the specified logical + //! operator. + //! + //! This implements left-to-right evaluation. For Operator::AND, this means + //! if the \a lhs is `false`, the \a rhs will not be consulted. Similarly, + //! with Operator::OR, if the \a lhs is `true`, the \a rhs will not be + //! consulted. + //! + //! \param[in] op The logical operator to apply on \a lhs and \a rhs. + //! \param[in] lhs The left-hand side of \a op. This class takes ownership. + //! \param[in] rhs The right-hand side of \a op. This class takes ownership. + BinaryPruneCondition(Operator op, PruneCondition* lhs, PruneCondition* rhs); + ~BinaryPruneCondition(); + + bool ShouldPruneReport(const CrashReportDatabase::Report& report) override; + + private: + const Operator op_; + scoped_ptr lhs_; + scoped_ptr rhs_; + + DISALLOW_COPY_AND_ASSIGN(BinaryPruneCondition); +}; + +} // namespace crashpad + +#endif // CRASHPAD_CLIENT_PRUNE_CRASH_REPORTS_H_ diff --git a/client/prune_crash_reports_test.cc b/client/prune_crash_reports_test.cc new file mode 100644 index 00000000..6c825c81 --- /dev/null +++ b/client/prune_crash_reports_test.cc @@ -0,0 +1,236 @@ +// Copyright 2015 The Crashpad Authors. All rights reserved. +// +// 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 "client/prune_crash_reports.h" + +#include + +#include +#include +#include + +#include "base/rand_util.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "test/scoped_temp_dir.h" +#include "util/file/file_io.h" + +namespace crashpad { +namespace test { +namespace { + +class MockDatabase : public CrashReportDatabase { + public: + // CrashReportDatabase: + MOCK_METHOD0(GetSettings, Settings*()); + MOCK_METHOD1(PrepareNewCrashReport, OperationStatus(NewReport**)); + MOCK_METHOD2(FinishedWritingCrashReport, OperationStatus(NewReport*, UUID*)); + MOCK_METHOD1(ErrorWritingCrashReport, OperationStatus(NewReport*)); + MOCK_METHOD2(LookUpCrashReport, OperationStatus(const UUID&, Report*)); + MOCK_METHOD1(GetPendingReports, OperationStatus(std::vector*)); + MOCK_METHOD1(GetCompletedReports, OperationStatus(std::vector*)); + MOCK_METHOD2(GetReportForUploading, + OperationStatus(const UUID&, const Report**)); + MOCK_METHOD3(RecordUploadAttempt, + OperationStatus(const Report*, bool, const std::string&)); + MOCK_METHOD1(SkipReportUpload, OperationStatus(const UUID&)); + MOCK_METHOD1(DeleteReport, OperationStatus(const UUID&)); +}; + +time_t NDaysAgo(int num_days) { + return time(nullptr) - (num_days * 60 * 60 * 24); +} + +TEST(PruneCrashReports, AgeCondition) { + CrashReportDatabase::Report report_80_days; + report_80_days.creation_time = NDaysAgo(80); + + CrashReportDatabase::Report report_10_days; + report_10_days.creation_time = NDaysAgo(10); + + CrashReportDatabase::Report report_30_days; + report_30_days.creation_time = NDaysAgo(30); + + AgePruneCondition condition(30); + EXPECT_TRUE(condition.ShouldPruneReport(report_80_days)); + EXPECT_FALSE(condition.ShouldPruneReport(report_10_days)); + EXPECT_FALSE(condition.ShouldPruneReport(report_30_days)); +} + +TEST(PruneCrashReports, SizeCondition) { + ScopedTempDir temp_dir; + + CrashReportDatabase::Report report_1k; + report_1k.file_path = temp_dir.path().Append(FILE_PATH_LITERAL("file1024")); + CrashReportDatabase::Report report_3k; + report_3k.file_path = temp_dir.path().Append(FILE_PATH_LITERAL("file3072")); + + { + ScopedFileHandle scoped_file_1k( + LoggingOpenFileForWrite(report_1k.file_path, + FileWriteMode::kCreateOrFail, + FilePermissions::kOwnerOnly)); + ASSERT_TRUE(scoped_file_1k.is_valid()); + + std::string string; + for (int i = 0; i < 128; ++i) + string.push_back(static_cast(i)); + + for (int i = 0; i < 1024; i += string.size()) { + ASSERT_TRUE(LoggingWriteFile(scoped_file_1k.get(), + string.c_str(), string.length())); + } + + ScopedFileHandle scoped_file_3k( + LoggingOpenFileForWrite(report_3k.file_path, + FileWriteMode::kCreateOrFail, + FilePermissions::kOwnerOnly)); + ASSERT_TRUE(scoped_file_3k.is_valid()); + + for (int i = 0; i < 3072; i += string.size()) { + ASSERT_TRUE(LoggingWriteFile(scoped_file_3k.get(), + string.c_str(), string.length())); + } + } + + { + DatabaseSizePruneCondition condition(1); + EXPECT_FALSE(condition.ShouldPruneReport(report_1k)); + EXPECT_TRUE(condition.ShouldPruneReport(report_1k)); + } + + { + DatabaseSizePruneCondition condition(1); + EXPECT_TRUE(condition.ShouldPruneReport(report_3k)); + } + + { + DatabaseSizePruneCondition condition(6); + EXPECT_FALSE(condition.ShouldPruneReport(report_3k)); + EXPECT_FALSE(condition.ShouldPruneReport(report_3k)); + EXPECT_TRUE(condition.ShouldPruneReport(report_1k)); + } +} + +class StaticCondition final : public PruneCondition { + public: + explicit StaticCondition(bool value) : value_(value), did_execute_(false) {} + ~StaticCondition() {} + + bool ShouldPruneReport(const CrashReportDatabase::Report& report) override { + did_execute_ = true; + return value_; + } + + bool did_execute() const { return did_execute_; } + + private: + const bool value_; + bool did_execute_; + + DISALLOW_COPY_AND_ASSIGN(StaticCondition); +}; + +TEST(PruneCrashReports, BinaryCondition) { + const struct { + const char* name; + BinaryPruneCondition::Operator op; + bool lhs_value; + bool rhs_value; + bool cond_result; + bool lhs_executed; + bool rhs_executed; + } kTests[] = { + {"false && false", + BinaryPruneCondition::AND, false, false, + false, true, false}, + {"false && true", + BinaryPruneCondition::AND, false, true, + false, true, false}, + {"true && false", + BinaryPruneCondition::AND, true, false, + false, true, true}, + {"true && true", + BinaryPruneCondition::AND, true, true, + true, true, true}, + {"false || false", + BinaryPruneCondition::OR, false, false, + false, true, true}, + {"false || true", + BinaryPruneCondition::OR, false, true, + true, true, true}, + {"true || false", + BinaryPruneCondition::OR, true, false, + true, true, false}, + {"true || true", + BinaryPruneCondition::OR, true, true, + true, true, false}, + }; + for (const auto& test : kTests) { + SCOPED_TRACE(test.name); + auto lhs = new StaticCondition(test.lhs_value); + auto rhs = new StaticCondition(test.rhs_value); + BinaryPruneCondition condition(test.op, lhs, rhs); + CrashReportDatabase::Report report; + EXPECT_EQ(test.cond_result, condition.ShouldPruneReport(report)); + EXPECT_EQ(test.lhs_executed, lhs->did_execute()); + EXPECT_EQ(test.rhs_executed, rhs->did_execute()); + } +} + +MATCHER_P(TestUUID, data_1, "") { + return arg.data_1 == data_1; +} + +TEST(PruneCrashReports, PruneOrder) { + using ::testing::_; + using ::testing::DoAll; + using ::testing::Return; + using ::testing::SetArgPointee; + + std::vector reports; + for (int i = 0; i < 10; ++i) { + CrashReportDatabase::Report temp; + temp.uuid.data_1 = i; + temp.creation_time = NDaysAgo(i * 10); + reports.push_back(temp); + } + // The randomness from std::rand() is not, so use a better rand() instead. + std::random_shuffle(reports.begin(), reports.end(), [](int rand_max) { + return base::RandUint64() % rand_max; + }); + std::vector pending_reports( + reports.begin(), reports.begin() + 5); + std::vector completed_reports( + reports.begin() + 5, reports.end()); + + MockDatabase db; + EXPECT_CALL(db, GetPendingReports(_)).WillOnce(DoAll( + SetArgPointee<0>(pending_reports), + Return(CrashReportDatabase::kNoError))); + EXPECT_CALL(db, GetCompletedReports(_)).WillOnce(DoAll( + SetArgPointee<0>(completed_reports), + Return(CrashReportDatabase::kNoError))); + for (size_t i = 0; i < reports.size(); ++i) { + EXPECT_CALL(db, DeleteReport(TestUUID(i))) + .WillOnce(Return(CrashReportDatabase::kNoError)); + } + + StaticCondition delete_all(true); + PruneCrashReportDatabase(&db, &delete_all); +} + +} // namespace +} // namespace test +} // namespace crashpad