blob: 1ee63ce2ad8a4497603896da490d379595e015fa [file] [log] [blame]
// Copyright 2020 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/feedback_data/datastore.h"
#include <fuchsia/hwinfo/cpp/fidl.h>
#include <fuchsia/intl/cpp/fidl.h>
#include <lib/async/cpp/executor.h>
#include <lib/fpromise/result.h>
#include <lib/inspect/cpp/vmo/types.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/syslog/logger.h>
#include <lib/zx/time.h>
#include <cstddef>
#include <memory>
#include <optional>
#include <string>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "src/developer/forensics/feedback/annotations/annotation_manager.h"
#include "src/developer/forensics/feedback_data/annotations/types.h"
#include "src/developer/forensics/feedback_data/archive_accessor_ptr.h"
#include "src/developer/forensics/feedback_data/attachments/types.h"
#include "src/developer/forensics/feedback_data/constants.h"
#include "src/developer/forensics/testing/gmatchers.h"
#include "src/developer/forensics/testing/gpretty_printers.h"
#include "src/developer/forensics/testing/log_message.h"
#include "src/developer/forensics/testing/stubs/channel_control.h"
#include "src/developer/forensics/testing/stubs/cobalt_logger_factory.h"
#include "src/developer/forensics/testing/stubs/device_id_provider.h"
#include "src/developer/forensics/testing/stubs/diagnostics_archive.h"
#include "src/developer/forensics/testing/stubs/diagnostics_batch_iterator.h"
#include "src/developer/forensics/testing/unit_test_fixture.h"
#include "src/developer/forensics/utils/cobalt/logger.h"
#include "src/developer/forensics/utils/cobalt/metrics.h"
#include "src/developer/forensics/utils/time.h"
#include "src/lib/files/directory.h"
#include "src/lib/files/file.h"
#include "src/lib/files/path.h"
#include "src/lib/fxl/strings/string_printf.h"
#include "src/lib/timekeeper/test_clock.h"
namespace forensics {
namespace feedback_data {
namespace {
using testing::BuildLogMessage;
using ::testing::Contains;
using ::testing::ElementsAreArray;
using ::testing::Eq;
using ::testing::IsEmpty;
using ::testing::Not;
using ::testing::Pair;
using ::testing::UnorderedElementsAreArray;
constexpr zx::duration kTimeout = zx::sec(30);
// Allowlist to use in test cases where the annotations don't matter, but where we want to avoid
// spurious logs due to empty annotation allowlist.
const AnnotationKeys kDefaultAnnotationsToAvoidSpuriousLogs = {
kAnnotationBuildIsDebug,
kAnnotationDeviceNumCPUs,
};
// Allowlist to use in test cases where the attachments don't matter, but where we want to avoid
// spurious logs due to empty attachment allowlist.
const AttachmentKeys kDefaultAttachmentsToAvoidSpuriousLogs = {
kAttachmentBuildSnapshot,
};
class DatastoreTest : public UnitTestFixture {
public:
DatastoreTest() : executor_(dispatcher()) {}
void SetUp() override {
device_id_provider_ =
std::make_unique<feedback::RemoteDeviceIdProvider>(dispatcher(), services());
SetUpCobaltServer(std::make_unique<stubs::CobaltLoggerFactory>());
cobalt_ = std::make_unique<cobalt::Logger>(dispatcher(), services(), &clock_);
inspect_node_manager_ = std::make_unique<InspectNodeManager>(&InspectRoot());
inspect_data_budget_ = std::make_unique<InspectDataBudget>(
"non-existent_path", inspect_node_manager_.get(), cobalt_.get());
}
void TearDown() override { FX_CHECK(files::DeletePath(kCurrentLogsDir, /*recursive=*/true)); }
protected:
void SetUpDatastore(const AnnotationKeys& annotation_allowlist,
const AttachmentKeys& attachment_allowlist,
const std::map<std::string, ErrorOr<std::string>>& startup_annotations = {}) {
std::set<std::string> allowlist;
for (const auto& [k, _] : startup_annotations) {
allowlist.insert(k);
}
annotation_manager_ =
std::make_unique<feedback::AnnotationManager>(dispatcher(), allowlist, startup_annotations);
datastore_ = std::make_unique<Datastore>(dispatcher(), services(), cobalt_.get(), &redactor_,
annotation_allowlist, attachment_allowlist,
annotation_manager_.get(), device_id_provider_.get(),
inspect_data_budget_.get());
}
void SetUpChannelProviderServer(std::unique_ptr<stubs::ChannelControlBase> server) {
channel_provider_server_ = std::move(server);
if (channel_provider_server_) {
InjectServiceProvider(channel_provider_server_.get());
}
}
void SetUpDeviceIdProviderServer(std::unique_ptr<stubs::DeviceIdProviderBase> server) {
device_id_provider_server_ = std::move(server);
if (device_id_provider_server_) {
InjectServiceProvider(device_id_provider_server_.get());
}
}
void SetUpDiagnosticsServer(const std::string& inspect_chunk) {
diagnostics_server_ = std::make_unique<stubs::DiagnosticsArchive>(
std::make_unique<stubs::DiagnosticsBatchIterator>(std::vector<std::vector<std::string>>({
{inspect_chunk},
{},
})));
InjectServiceProvider(diagnostics_server_.get(), kArchiveAccessorName);
}
void SetUpLogServer(const std::string& inspect_chunk) {
diagnostics_server_ = std::make_unique<stubs::DiagnosticsArchive>(
std::make_unique<stubs::DiagnosticsBatchIteratorNeverRespondsAfterOneBatch>(
std::vector<std::string>({
{inspect_chunk},
})));
InjectServiceProvider(diagnostics_server_.get(), kArchiveAccessorName);
}
void SetUpDiagnosticsServer(std::unique_ptr<stubs::DiagnosticsArchiveBase> server) {
diagnostics_server_ = std::move(server);
if (diagnostics_server_) {
InjectServiceProvider(diagnostics_server_.get(), kArchiveAccessorName);
}
}
void WriteFile(const std::string& filepath, const std::string& content) {
FX_CHECK(files::WriteFile(filepath, content.c_str(), content.size()));
}
::fpromise::result<Annotations> GetAnnotations() {
FX_CHECK(datastore_);
::fpromise::result<Annotations> result;
executor_.schedule_task(datastore_->GetAnnotations(kTimeout).then(
[&result](::fpromise::result<Annotations>& res) { result = std::move(res); }));
RunLoopFor(kTimeout);
return result;
}
::fpromise::result<Attachments> GetAttachments() {
FX_CHECK(datastore_);
::fpromise::result<Attachments> result;
executor_.schedule_task(datastore_->GetAttachments(kTimeout).then(
[&result](::fpromise::result<Attachments>& res) { result = std::move(res); }));
RunLoopFor(kTimeout);
return result;
}
Annotations GetImmediatelyAvailableAnnotations() {
return datastore_->GetImmediatelyAvailableAnnotations();
}
Attachments GetStaticAttachments() { return datastore_->GetStaticAttachments(); }
private:
async::Executor executor_;
timekeeper::TestClock clock_;
std::unique_ptr<feedback::AnnotationManager> annotation_manager_;
std::unique_ptr<feedback::DeviceIdProvider> device_id_provider_;
std::unique_ptr<cobalt::Logger> cobalt_;
IdentityRedactor redactor_{inspect::BoolProperty()};
protected:
std::unique_ptr<Datastore> datastore_;
private:
std::unique_ptr<InspectNodeManager> inspect_node_manager_;
std::unique_ptr<InspectDataBudget> inspect_data_budget_;
// Stubs servers.
std::unique_ptr<stubs::ChannelControlBase> channel_provider_server_;
std::unique_ptr<stubs::DeviceIdProviderBase> device_id_provider_server_;
std::unique_ptr<stubs::DiagnosticsArchiveBase> diagnostics_server_;
};
TEST_F(DatastoreTest, GetAnnotationsAndAttachments_SmokeTest) {
// We list the annotations and attachments that are likely on every build to minimize the logspam.
SetUpDatastore(
{
kAnnotationBuildBoard,
kAnnotationBuildProduct,
kAnnotationBuildLatestCommitDate,
kAnnotationBuildVersion,
kAnnotationBuildVersionPreviousBoot,
kAnnotationBuildIsDebug,
kAnnotationDeviceBoardName,
kAnnotationDeviceNumCPUs,
kAnnotationSystemBootIdCurrent,
kAnnotationSystemBootIdPrevious,
kAnnotationSystemLastRebootReason,
kAnnotationSystemLastRebootUptime,
},
{
kAttachmentBuildSnapshot,
},
{
{kAnnotationBuildBoard, "board"},
{kAnnotationBuildProduct, Error::kTimeout},
{kAnnotationBuildLatestCommitDate, "commit-date"},
{kAnnotationBuildVersion, "version"},
{kAnnotationBuildVersionPreviousBoot, Error::kMissingValue},
{kAnnotationBuildIsDebug, "true"},
{kAnnotationDeviceBoardName, "board-name"},
{kAnnotationDeviceNumCPUs, "4"},
{kAnnotationSystemBootIdCurrent, "boot-id"},
{kAnnotationSystemBootIdPrevious, "previous-boot-id"},
{kAnnotationSystemLastRebootReason, Error::kMissingValue},
{kAnnotationSystemLastRebootUptime, Error::kMissingValue},
});
// There is not much we can assert here as no missing annotation nor attachment is fatal and we
// cannot expect annotations or attachments to be present.
EXPECT_THAT(
GetImmediatelyAvailableAnnotations(),
UnorderedElementsAreArray({
Pair(kAnnotationBuildBoard, ErrorOr<std::string>("board")),
Pair(kAnnotationBuildProduct, ErrorOr<std::string>(Error::kTimeout)),
Pair(kAnnotationBuildLatestCommitDate, ErrorOr<std::string>("commit-date")),
Pair(kAnnotationBuildVersion, ErrorOr<std::string>("version")),
Pair(kAnnotationBuildVersionPreviousBoot, ErrorOr<std::string>(Error::kMissingValue)),
Pair(kAnnotationBuildIsDebug, ErrorOr<std::string>("true")),
Pair(kAnnotationDeviceBoardName, ErrorOr<std::string>("board-name")),
Pair(kAnnotationDeviceNumCPUs, ErrorOr<std::string>("4")),
Pair(kAnnotationSystemBootIdCurrent, ErrorOr<std::string>("boot-id")),
Pair(kAnnotationSystemBootIdPrevious, ErrorOr<std::string>("previous-boot-id")),
Pair(kAnnotationSystemLastRebootReason, ErrorOr<std::string>(Error::kMissingValue)),
Pair(kAnnotationSystemLastRebootUptime, ErrorOr<std::string>(Error::kMissingValue)),
}));
GetStaticAttachments();
GetAnnotations();
GetAttachments();
}
TEST_F(DatastoreTest, GetAnnotations_TargetChannel) {
SetUpChannelProviderServer(
std::make_unique<stubs::ChannelControl>(stubs::ChannelControlBase::Params({
.current = "current-channel",
.target = "target-channel",
})));
SetUpDatastore(
{
kAnnotationSystemUpdateChannelTarget,
},
kDefaultAttachmentsToAvoidSpuriousLogs);
::fpromise::result<Annotations> annotations = GetAnnotations();
ASSERT_TRUE(annotations.is_ok());
EXPECT_THAT(annotations.take_value(),
ElementsAreArray({
Pair(kAnnotationSystemUpdateChannelTarget, "target-channel"),
}));
EXPECT_THAT(GetImmediatelyAvailableAnnotations(), IsEmpty());
}
TEST_F(DatastoreTest, GetAnnotations_DeviceId) {
SetUpDeviceIdProviderServer(std::make_unique<stubs::DeviceIdProvider>("device-id"));
SetUpDatastore({kAnnotationDeviceFeedbackId}, kDefaultAttachmentsToAvoidSpuriousLogs);
::fpromise::result<Annotations> annotations = GetAnnotations();
ASSERT_TRUE(annotations.is_ok());
EXPECT_THAT(annotations.take_value(), ElementsAreArray({
Pair(kAnnotationDeviceFeedbackId, "device-id"),
}));
ASSERT_TRUE(files::DeletePath(kDeviceIdPath, /*recursive=*/false));
}
TEST_F(DatastoreTest, GetAnnotations_FailOn_EmptyAnnotationAllowlist) {
SetUpDatastore({}, kDefaultAttachmentsToAvoidSpuriousLogs);
::fpromise::result<Annotations> annotations = GetAnnotations();
ASSERT_TRUE(annotations.is_error());
EXPECT_THAT(GetImmediatelyAvailableAnnotations(), IsEmpty());
}
TEST_F(DatastoreTest, GetAnnotations_FailOn_OnlyUnknownAnnotationInAllowlist) {
SetUpDatastore({"unknown.annotation"}, kDefaultAttachmentsToAvoidSpuriousLogs);
::fpromise::result<Annotations> annotations = GetAnnotations();
ASSERT_TRUE(annotations.is_ok());
EXPECT_THAT(annotations.value(), ElementsAreArray({
Pair("unknown.annotation", Error::kMissingValue),
}));
EXPECT_THAT(GetImmediatelyAvailableAnnotations(), IsEmpty());
}
TEST_F(DatastoreTest, GetAttachments_Inspect) {
// CollectInspectData() has its own set of unit tests so we only cover one chunk of Inspect data
// here to check that we are attaching the Inspect data.
SetUpDiagnosticsServer("foo");
SetUpDatastore(kDefaultAnnotationsToAvoidSpuriousLogs, {kAttachmentInspect});
::fpromise::result<Attachments> attachments = GetAttachments();
ASSERT_TRUE(attachments.is_ok());
EXPECT_THAT(attachments.take_value(),
ElementsAreArray({Pair(kAttachmentInspect, AttachmentValue("[\nfoo\n]"))}));
EXPECT_THAT(GetStaticAttachments(), IsEmpty());
}
TEST_F(DatastoreTest, GetAttachments_PreviousSyslogAlreadyCached) {
const std::string previous_log_contents = "LAST SYSTEM LOG";
WriteFile(kPreviousLogsFilePath, previous_log_contents);
SetUpDatastore(kDefaultAnnotationsToAvoidSpuriousLogs, {kAttachmentLogSystemPrevious});
::fpromise::result<Attachments> attachments = GetAttachments();
ASSERT_TRUE(attachments.is_ok());
EXPECT_THAT(attachments.take_value(),
ElementsAreArray(
{Pair(kAttachmentLogSystemPrevious, AttachmentValue(previous_log_contents))}));
EXPECT_THAT(GetStaticAttachments(),
ElementsAreArray(
{Pair(kAttachmentLogSystemPrevious, AttachmentValue(previous_log_contents))}));
ASSERT_TRUE(files::DeletePath(kPreviousLogsFilePath, /*recursive=*/false));
}
TEST_F(DatastoreTest, GetAttachments_PreviousSyslogIsEmpty) {
const std::string previous_log_contents = "";
WriteFile(kPreviousLogsFilePath, previous_log_contents);
SetUpDatastore(kDefaultAnnotationsToAvoidSpuriousLogs, {kAttachmentLogSystemPrevious});
::fpromise::result<Attachments> attachments = GetAttachments();
ASSERT_TRUE(attachments.is_ok());
EXPECT_THAT(attachments.take_value(),
ElementsAreArray(
{Pair(kAttachmentLogSystemPrevious, AttachmentValue(Error::kMissingValue))}));
EXPECT_THAT(GetStaticAttachments(),
ElementsAreArray(
{Pair(kAttachmentLogSystemPrevious, AttachmentValue(Error::kMissingValue))}));
ASSERT_TRUE(files::DeletePath(kPreviousLogsFilePath, /*recursive=*/false));
}
TEST_F(DatastoreTest, GetAttachments_DropPreviousSyslog) {
const std::string previous_log_contents = "LAST SYSTEM LOG";
WriteFile(kPreviousLogsFilePath, previous_log_contents);
SetUpDatastore(kDefaultAnnotationsToAvoidSpuriousLogs, {kAttachmentLogSystemPrevious});
datastore_->DropStaticAttachment(kAttachmentLogSystemPrevious, Error::kCustom);
::fpromise::result<Attachments> attachments = GetAttachments();
ASSERT_TRUE(attachments.is_ok());
EXPECT_THAT(
GetStaticAttachments(),
ElementsAreArray({Pair(kAttachmentLogSystemPrevious, AttachmentValue(Error::kCustom))}));
ASSERT_TRUE(files::DeletePath(kPreviousLogsFilePath, /*recursive=*/false));
}
TEST_F(DatastoreTest, GetAttachments_SysLog) {
// CollectSystemLogs() has its own set of unit tests so we only cover one log message here to
// check that we are attaching the logs.
SetUpLogServer(R"JSON(
[
{
"metadata": {
"timestamp": 15604000000000,
"severity": "INFO",
"pid": 7559,
"tid": 7687,
"tags": ["foo"]
},
"payload": {
"root": {
"message": {
"value": "log message"
}
}
}
}
]
)JSON");
SetUpDatastore(kDefaultAnnotationsToAvoidSpuriousLogs, {kAttachmentLogSystem});
::fpromise::result<Attachments> attachments = GetAttachments();
ASSERT_TRUE(attachments.is_ok());
EXPECT_THAT(attachments.take_value(),
ElementsAreArray(
{Pair(kAttachmentLogSystem,
AttachmentValue("[15604.000][07559][07687][foo] INFO: log message\n"))}));
EXPECT_THAT(GetStaticAttachments(), IsEmpty());
}
TEST_F(DatastoreTest, GetAttachments_FailOn_EmptyAttachmentAllowlist) {
SetUpDatastore(kDefaultAnnotationsToAvoidSpuriousLogs, {});
::fpromise::result<Attachments> attachments = GetAttachments();
ASSERT_TRUE(attachments.is_error());
EXPECT_THAT(GetStaticAttachments(), IsEmpty());
}
TEST_F(DatastoreTest, GetAttachments_FailOn_OnlyUnknownAttachmentInAllowlist) {
SetUpDatastore(kDefaultAnnotationsToAvoidSpuriousLogs, {"unknown.attachment"});
::fpromise::result<Attachments> attachments = GetAttachments();
ASSERT_TRUE(attachments.is_error());
EXPECT_THAT(GetStaticAttachments(), IsEmpty());
}
TEST_F(DatastoreTest, GetAttachments_CobaltLogsTimeouts) {
// The timeout of the kernel log collection cannot be tested due to the fact that
// fuchsia::boot::ReadOnlyLog cannot be stubbed and we have no mechanism to set the timeout of
// the kernel log collection to 0 seconds.
//
// Inspect and system log share the same stub server so we only test one of the two (i.e.
// Inspect).
SetUpDatastore(kDefaultAnnotationsToAvoidSpuriousLogs, {
kAttachmentInspect,
});
SetUpDiagnosticsServer(std::make_unique<stubs::DiagnosticsArchive>(
std::make_unique<stubs::DiagnosticsBatchIteratorNeverResponds>()));
::fpromise::result<Attachments> attachments = GetAttachments();
ASSERT_TRUE(attachments.is_ok());
EXPECT_THAT(attachments.take_value(),
ElementsAreArray({
Pair(kAttachmentInspect, AttachmentValue(Error::kTimeout)),
}));
EXPECT_THAT(ReceivedCobaltEvents(), UnorderedElementsAreArray({
cobalt::Event(cobalt::TimedOutData::kInspect),
}));
}
} // namespace
} // namespace feedback_data
} // namespace forensics