blob: 977145f5d3dbe859ae0e6a3ba296811d1adc9ccd [file] [log] [blame]
// Copyright 2019 The Fuchsia 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 "src/developer/forensics/crash_reports/queue.h"
#include <lib/inspect/cpp/inspect.h>
#include <lib/inspect/testing/cpp/inspect.h>
#include <lib/syslog/cpp/macros.h>
#include <memory>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "src/developer/forensics/crash_reports/constants.h"
#include "src/developer/forensics/crash_reports/info/info_context.h"
#include "src/developer/forensics/crash_reports/settings.h"
#include "src/developer/forensics/crash_reports/tests/stub_crash_server.h"
#include "src/developer/forensics/testing/cobalt_test_fixture.h"
#include "src/developer/forensics/testing/stubs/cobalt_logger_factory.h"
#include "src/developer/forensics/testing/stubs/network_reachability_provider.h"
#include "src/developer/forensics/testing/unit_test_fixture.h"
#include "src/lib/files/directory.h"
#include "src/lib/files/file.h"
#include "src/lib/files/path.h"
#include "src/lib/files/scoped_temp_dir.h"
#include "src/lib/fsl/vmo/strings.h"
#include "src/lib/fxl/strings/string_printf.h"
#include "src/lib/timekeeper/test_clock.h"
#include "third_party/crashpad/client/crash_report_database.h"
namespace forensics {
namespace crash_reports {
namespace {
using crashpad::CrashReportDatabase;
using crashpad::UUID;
using inspect::testing::ChildrenMatch;
using inspect::testing::NameMatches;
using inspect::testing::NodeMatches;
using inspect::testing::PropertyList;
using inspect::testing::StringIs;
using inspect::testing::UintIs;
using testing::Contains;
using testing::ElementsAre;
using testing::IsEmpty;
using testing::IsSupersetOf;
using testing::Not;
using testing::UnorderedElementsAre;
using testing::UnorderedElementsAreArray;
using UploadPolicy = Settings::UploadPolicy;
constexpr bool kUploadSuccessful = true;
constexpr bool kUploadFailed = false;
constexpr char kCrashpadDatabasePath[] = "/tmp/crashes";
constexpr char kAttachmentKey[] = "attachment.key";
constexpr char kAttachmentValue[] = "attachment.value";
constexpr char kAnnotationKey[] = "annotation.key";
constexpr char kAnnotationValue[] = "annotation.value";
constexpr char kMinidumpKey[] = "uploadFileMinidump";
constexpr char kMinidumpValue[] = "minidump";
fuchsia::mem::Buffer BuildAttachment(const std::string& value) {
fuchsia::mem::Buffer attachment;
FX_CHECK(fsl::VmoFromString(value, &attachment));
return attachment;
}
std::map<std::string, fuchsia::mem::Buffer> MakeAttachments() {
std::map<std::string, fuchsia::mem::Buffer> attachments;
attachments[kAttachmentKey] = BuildAttachment(kAttachmentValue);
return attachments;
}
std::map<std::string, std::string> MakeAnnotations() {
return {{kAnnotationKey, kAnnotationValue}};
}
class QueueTest : public UnitTestFixture, CobaltTestFixture {
public:
QueueTest() : CobaltTestFixture(/*unit_test_fixture=*/this) {}
void SetUp() override {
settings_.set_upload_policy(UploadPolicy::LIMBO);
inspector_ = std::make_unique<inspect::Inspector>();
info_context_ =
std::make_shared<InfoContext>(&inspector_->GetRoot(), clock_, dispatcher(), services());
SetUpCobaltServer(std::make_unique<stubs::CobaltLoggerFactory>());
SetUpNetworkReachabilityProvider();
RunLoopUntilIdle();
}
void TearDown() override {
ASSERT_TRUE(files::DeletePath(kCrashpadDatabasePath, /*recursive=*/true));
}
protected:
void SetUpNetworkReachabilityProvider() {
network_reachability_provider_ = std::make_unique<stubs::NetworkReachabilityProvider>();
InjectServiceProvider(network_reachability_provider_.get());
}
void SetUpQueue(std::vector<bool> upload_attempt_results = std::vector<bool>{}) {
program_id_ = 1;
state_ = QueueOps::SetStateToLeaveAsPending;
expected_queue_contents_.clear();
upload_attempt_results_ = upload_attempt_results;
next_upload_attempt_result_ = upload_attempt_results_.cbegin();
crash_server_ = std::make_unique<StubCrashServer>(upload_attempt_results_);
queue_ = Queue::TryCreate(dispatcher(), services(), info_context_, crash_server_.get());
ASSERT_TRUE(queue_);
queue_->WatchSettings(&settings_);
}
enum class QueueOps {
AddNewReport,
DeleteOneReport,
SetStateToArchive,
SetStateToUpload,
SetStateToLeaveAsPending,
ProcessAll,
};
void ApplyQueueOps(const std::vector<QueueOps>& ops) {
for (auto const& op : ops) {
switch (op) {
case QueueOps::AddNewReport:
ASSERT_TRUE(queue_->Add(fxl::StringPrintf("program_%ld", program_id_),
/*attachments=*/MakeAttachments(),
/*minidump=*/BuildAttachment(kMinidumpValue),
/*annotations=*/MakeAnnotations()));
ASSERT_TRUE(RunLoopUntilIdle());
++program_id_;
if (!queue_->IsEmpty()) {
AddExpectedReport(queue_->LatestReport());
}
SetExpectedQueueContents();
break;
case QueueOps::DeleteOneReport:
if (!expected_queue_contents_.empty()) {
// Create a database in order to delete a report.
auto database_ = CrashReportDatabase::InitializeWithoutCreating(
base::FilePath(kCrashpadDatabasePath));
ASSERT_TRUE(database_);
database_->DeleteReport(expected_queue_contents_.back());
expected_queue_contents_.pop_back();
}
SetExpectedQueueContents();
break;
case QueueOps::SetStateToArchive:
state_ = QueueOps::SetStateToArchive;
settings_.set_upload_policy(UploadPolicy::DISABLED);
SetExpectedQueueContents();
break;
case QueueOps::SetStateToUpload:
state_ = QueueOps::SetStateToUpload;
settings_.set_upload_policy(UploadPolicy::ENABLED);
SetExpectedQueueContents();
break;
case QueueOps::SetStateToLeaveAsPending:
state_ = QueueOps::SetStateToLeaveAsPending;
settings_.set_upload_policy(UploadPolicy::LIMBO);
SetExpectedQueueContents();
break;
case QueueOps::ProcessAll:
EXPECT_EQ(SetExpectedQueueContents(), queue_->ProcessAll());
break;
}
}
}
void CheckQueueContents() {
for (const auto& id : expected_queue_contents_) {
EXPECT_TRUE(queue_->Contains(id));
}
EXPECT_EQ(queue_->Size(), expected_queue_contents_.size());
}
void CheckAnnotationsOnServer() {
FX_CHECK(crash_server_);
EXPECT_THAT(crash_server_->latest_annotations(),
UnorderedElementsAre(testing::Pair(kAnnotationKey, kAnnotationValue)));
}
void CheckAttachmentKeysOnServer() {
FX_CHECK(crash_server_);
EXPECT_THAT(crash_server_->latest_attachment_keys(),
UnorderedElementsAre(kAttachmentKey, kMinidumpKey));
}
inspect::Hierarchy InspectTree() {
auto result = inspect::ReadFromVmo(inspector_->DuplicateVmo());
FX_CHECK(result.is_ok());
return result.take_value();
}
std::unique_ptr<Queue> queue_;
std::vector<UUID> expected_queue_contents_;
std::unique_ptr<stubs::NetworkReachabilityProvider> network_reachability_provider_;
private:
void AddExpectedReport(const UUID& uuid) {
// Add a report to the back of the expected queue contents if and only if it is expected
// to be in the queue after processing.
if (state_ != QueueOps::SetStateToUpload) {
expected_queue_contents_.push_back(uuid);
} else if (!*next_upload_attempt_result_) {
expected_queue_contents_.push_back(uuid);
++next_upload_attempt_result_;
}
}
size_t SetExpectedQueueContents() {
if (state_ == QueueOps::SetStateToArchive) {
const size_t old_size = expected_queue_contents_.size();
expected_queue_contents_.clear();
return old_size;
} else if (state_ == QueueOps::SetStateToUpload) {
std::vector<UUID> new_queue_contents;
for (const auto& uuid : expected_queue_contents_) {
// We expect the reports we failed to upload to still be pending.
if (!(*next_upload_attempt_result_)) {
new_queue_contents.push_back(uuid);
}
++next_upload_attempt_result_;
}
expected_queue_contents_.swap(new_queue_contents);
return new_queue_contents.size() - expected_queue_contents_.size();
}
return 0;
}
size_t program_id_ = 1;
QueueOps state_ = QueueOps::SetStateToLeaveAsPending;
std::vector<bool> upload_attempt_results_;
std::vector<bool>::const_iterator next_upload_attempt_result_;
Settings settings_;
timekeeper::TestClock clock_;
std::unique_ptr<StubCrashServer> crash_server_;
std::unique_ptr<inspect::Inspector> inspector_;
std::shared_ptr<InfoContext> info_context_;
std::shared_ptr<cobalt::Logger> cobalt_;
};
TEST_F(QueueTest, Check_EmptyQueue_OnZeroAdds) {
SetUpQueue();
CheckQueueContents();
EXPECT_TRUE(queue_->IsEmpty());
}
TEST_F(QueueTest, Check_NotIsEmptyQueue_OnStateSetToLeaveAsPending_MultipleReports) {
SetUpQueue();
ApplyQueueOps({
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::ProcessAll,
});
CheckQueueContents();
EXPECT_EQ(queue_->Size(), 5u);
}
TEST_F(QueueTest, Check_IsEmptyQueue_OnStateSetToArchive_MultipleReports) {
SetUpQueue();
ApplyQueueOps({
QueueOps::SetStateToArchive,
QueueOps::AddNewReport,
QueueOps::AddNewReport,
});
CheckQueueContents();
EXPECT_TRUE(queue_->IsEmpty());
}
TEST_F(QueueTest, Check_IsEmptyQueue_OnStateSetToArchive_MultipleReports_OnePruned) {
SetUpQueue();
ApplyQueueOps({
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::DeleteOneReport,
QueueOps::SetStateToArchive,
QueueOps::ProcessAll,
});
CheckQueueContents();
EXPECT_TRUE(queue_->IsEmpty());
}
TEST_F(QueueTest, Check_IsEmptyQueue_OnSuccessfulUpload) {
SetUpQueue({kUploadSuccessful});
ApplyQueueOps({
QueueOps::SetStateToUpload,
QueueOps::AddNewReport,
});
CheckQueueContents();
CheckAnnotationsOnServer();
CheckAttachmentKeysOnServer();
EXPECT_TRUE(queue_->IsEmpty());
}
TEST_F(QueueTest, Check_NotIsEmptyQueue_OnFailedUpload) {
SetUpQueue({kUploadFailed});
ApplyQueueOps({
QueueOps::SetStateToUpload,
QueueOps::AddNewReport,
});
CheckQueueContents();
EXPECT_EQ(queue_->Size(), 1u);
}
TEST_F(QueueTest, Check_IsEmptyQueue_OnSuccessfulUpload_MultipleReports) {
SetUpQueue(std::vector<bool>(5u, kUploadSuccessful));
ApplyQueueOps({
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::SetStateToUpload,
QueueOps::ProcessAll,
});
CheckQueueContents();
CheckAnnotationsOnServer();
CheckAttachmentKeysOnServer();
EXPECT_TRUE(queue_->IsEmpty());
}
TEST_F(QueueTest, Check_NotIsEmptyQueue_OnFailedUpload_MultipleReports) {
SetUpQueue(std::vector<bool>(5u, kUploadFailed));
ApplyQueueOps({
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::SetStateToUpload,
});
CheckQueueContents();
CheckAnnotationsOnServer();
CheckAttachmentKeysOnServer();
EXPECT_FALSE(queue_->IsEmpty());
}
TEST_F(QueueTest, Check_IsEmptyQueue_OnSuccessfulUpload_OnePruned) {
SetUpQueue({kUploadSuccessful});
ApplyQueueOps({
QueueOps::AddNewReport,
QueueOps::DeleteOneReport,
QueueOps::SetStateToUpload,
QueueOps::AddNewReport,
});
CheckQueueContents();
CheckAnnotationsOnServer();
CheckAttachmentKeysOnServer();
EXPECT_TRUE(queue_->IsEmpty());
}
TEST_F(QueueTest, Check_IsEmptyQueue_OnSuccessfulUpload_MultiplePruned_MultipleReports) {
SetUpQueue({kUploadSuccessful});
ApplyQueueOps({
QueueOps::AddNewReport,
QueueOps::DeleteOneReport,
QueueOps::AddNewReport,
QueueOps::DeleteOneReport,
QueueOps::AddNewReport,
QueueOps::DeleteOneReport,
QueueOps::AddNewReport,
QueueOps::DeleteOneReport,
QueueOps::AddNewReport,
QueueOps::DeleteOneReport,
QueueOps::AddNewReport,
QueueOps::SetStateToUpload,
});
CheckQueueContents();
CheckAnnotationsOnServer();
CheckAttachmentKeysOnServer();
EXPECT_TRUE(queue_->IsEmpty());
}
TEST_F(QueueTest, Check_NotIsEmptyQueue_OnMixedUploadResults_MultipleReports) {
SetUpQueue({
kUploadSuccessful,
kUploadSuccessful,
kUploadFailed,
kUploadFailed,
kUploadSuccessful,
});
ApplyQueueOps({
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::SetStateToUpload,
});
CheckQueueContents();
CheckAnnotationsOnServer();
CheckAttachmentKeysOnServer();
EXPECT_EQ(queue_->Size(), 2u);
}
TEST_F(QueueTest, Check_NotIsEmptyQueue_OnMixedUploadResults_MultiplePruned_MultipleReports) {
SetUpQueue({
kUploadSuccessful,
kUploadSuccessful,
kUploadFailed,
kUploadFailed,
kUploadSuccessful,
});
ApplyQueueOps({
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::DeleteOneReport,
QueueOps::DeleteOneReport,
QueueOps::SetStateToUpload,
});
CheckQueueContents();
CheckAnnotationsOnServer();
CheckAttachmentKeysOnServer();
EXPECT_EQ(queue_->Size(), 2u);
}
TEST_F(QueueTest, Check_ProcessAll_ScheduledTwice) {
SetUpQueue({
kUploadFailed,
kUploadSuccessful,
kUploadFailed,
kUploadSuccessful,
});
ApplyQueueOps({
QueueOps::AddNewReport,
QueueOps::SetStateToUpload,
});
ASSERT_FALSE(queue_->IsEmpty());
RunLoopFor(zx::hour(1));
EXPECT_TRUE(queue_->IsEmpty());
ApplyQueueOps({
QueueOps::SetStateToLeaveAsPending,
QueueOps::AddNewReport,
QueueOps::SetStateToUpload,
});
ASSERT_FALSE(queue_->IsEmpty());
RunLoopFor(zx::hour(1));
EXPECT_TRUE(queue_->IsEmpty());
}
TEST_F(QueueTest, Check_ProcessAllTwice_OnNetworkReachable) {
// Setup crash report upload outcome
SetUpQueue({
// First crash report: automatic upload fails, succeed when the network becomes reachable.
kUploadFailed,
kUploadSuccessful,
// Second crash report: automatic upload fails, succeed when then network becomes reachable.
kUploadFailed,
kUploadSuccessful,
});
// First crash report: automatic upload fails. Succeed on the second upload attempt when the
// network becomes reachable.
ApplyQueueOps({
QueueOps::AddNewReport,
QueueOps::SetStateToUpload,
});
ASSERT_FALSE(queue_->IsEmpty());
network_reachability_provider_->TriggerOnNetworkReachable(true);
RunLoopUntilIdle();
EXPECT_TRUE(queue_->IsEmpty());
// Second crash report: Insert a new crash report that fails to upload at first,
// and then check that it gets uploaded when the network becomes reachable again.
ApplyQueueOps({
QueueOps::AddNewReport,
});
ASSERT_FALSE(queue_->IsEmpty());
network_reachability_provider_->TriggerOnNetworkReachable(false);
RunLoopUntilIdle();
EXPECT_FALSE(queue_->IsEmpty());
network_reachability_provider_->TriggerOnNetworkReachable(true);
RunLoopUntilIdle();
EXPECT_TRUE(queue_->IsEmpty());
}
TEST_F(QueueTest, Check_ProcessAll_OnReconnect_NetworkReachable) {
// Setup crash report upload outcome: Automatic upload fails,
// succeed when the network becomes reachable
SetUpQueue({
kUploadFailed,
kUploadSuccessful,
});
// Automatic crash report upload fails.
ApplyQueueOps({
QueueOps::AddNewReport,
QueueOps::SetStateToUpload,
});
ASSERT_FALSE(queue_->IsEmpty());
// Close the connection to the network reachability service.
network_reachability_provider_->CloseConnection();
// We run the loop longer than the delay to account for the nondeterminism of
// backoff::ExponentialBackoff.
RunLoopFor(zx::min(3));
// We should be re-connected to the network reachability service.
// Test upload on network reachable.
network_reachability_provider_->TriggerOnNetworkReachable(false);
RunLoopUntilIdle();
EXPECT_FALSE(queue_->IsEmpty());
network_reachability_provider_->TriggerOnNetworkReachable(true);
RunLoopUntilIdle();
EXPECT_TRUE(queue_->IsEmpty());
}
TEST_F(QueueTest, Check_InspectTree) {
SetUpQueue({
kUploadSuccessful,
kUploadSuccessful,
kUploadFailed,
kUploadFailed,
kUploadSuccessful,
});
ApplyQueueOps({
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::AddNewReport,
QueueOps::DeleteOneReport,
QueueOps::DeleteOneReport,
QueueOps::SetStateToUpload,
});
EXPECT_THAT(
InspectTree(),
ChildrenMatch(Contains(AllOf(
NodeMatches(NameMatches("crash_reporter")),
ChildrenMatch(IsSupersetOf({
AllOf(
NodeMatches(NameMatches("reports")),
ChildrenMatch(IsSupersetOf({
AllOf(NodeMatches(NameMatches("program_1")),
ChildrenMatch(ElementsAre(AllOf(
NodeMatches(AllOf(PropertyList(UnorderedElementsAreArray({
StringIs("creation_time", Not(IsEmpty())),
StringIs("final_state", "uploaded"),
UintIs("upload_attempts", 1u),
})))),
ChildrenMatch(ElementsAre(
NodeMatches(AllOf(NameMatches("crash_server"),
PropertyList(UnorderedElementsAreArray({
StringIs("creation_time", Not(IsEmpty())),
StringIs("id", kStubServerReportId),
})))))))))),
AllOf(NodeMatches(NameMatches("program_2")),
ChildrenMatch(ElementsAre(AllOf(
NodeMatches(AllOf(PropertyList(UnorderedElementsAreArray({
StringIs("creation_time", Not(IsEmpty())),
StringIs("final_state", "uploaded"),
UintIs("upload_attempts", 1u),
})))),
ChildrenMatch(ElementsAre(
NodeMatches(AllOf(NameMatches("crash_server"),
PropertyList(UnorderedElementsAreArray({
StringIs("creation_time", Not(IsEmpty())),
StringIs("id", kStubServerReportId),
})))))))))),
AllOf(
NodeMatches(NameMatches("program_3")),
ChildrenMatch(UnorderedElementsAreArray({
NodeMatches(AllOf(NameMatches(expected_queue_contents_[0].ToString()),
PropertyList(UnorderedElementsAreArray({
StringIs("creation_time", Not(IsEmpty())),
UintIs("upload_attempts", 1u),
})))),
}))),
AllOf(
NodeMatches(NameMatches("program_4")),
ChildrenMatch(UnorderedElementsAreArray({
NodeMatches(AllOf(NameMatches(expected_queue_contents_[1].ToString()),
PropertyList(UnorderedElementsAreArray({
StringIs("creation_time", Not(IsEmpty())),
UintIs("upload_attempts", 1u),
})))),
}))),
AllOf(NodeMatches(NameMatches("program_5")),
ChildrenMatch(ElementsAre(AllOf(
NodeMatches(AllOf(PropertyList(UnorderedElementsAreArray({
StringIs("creation_time", Not(IsEmpty())),
StringIs("final_state", "uploaded"),
UintIs("upload_attempts", 1u),
})))),
ChildrenMatch(ElementsAre(
NodeMatches(AllOf(NameMatches("crash_server"),
PropertyList(UnorderedElementsAreArray({
StringIs("creation_time", Not(IsEmpty())),
StringIs("id", kStubServerReportId),
})))))))))),
}))),
AllOf(NodeMatches(
AllOf(NameMatches("queue"), PropertyList(ElementsAre(UintIs("size", 2u))))),
ChildrenMatch(IsEmpty())),
}))))));
}
} // namespace
} // namespace crash_reports
} // namespace forensics