blob: 41a30516ef2a283b8d092a571928a6b9c21300ff [file] [log] [blame]
// Copyright 2021 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 <fuchsia/feedback/cpp/fidl.h>
#include <fuchsia/hwinfo/cpp/fidl.h>
#include <fuchsia/intl/cpp/fidl.h>
#include <fuchsia/logger/cpp/fidl.h>
#include <fuchsia/mem/cpp/fidl.h>
#include <fuchsia/metrics/cpp/fidl.h>
#include <fuchsia/metrics/test/cpp/fidl.h>
#include <fuchsia/sys/cpp/fidl.h>
#include <fuchsia/update/channelcontrol/cpp/fidl.h>
#include <lib/async/cpp/executor.h>
#include <lib/fidl/cpp/binding.h>
#include <lib/fpromise/result.h>
#include <lib/inspect/contrib/cpp/archive_reader.h>
#include <lib/sys/cpp/service_directory.h>
#include <lib/sys/cpp/testing/test_with_environment_fixture.h>
#include <lib/syslog/cpp/macros.h>
#include <zircon/errors.h>
#include <zircon/types.h>
#include <memory>
#include <vector>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "src/developer/forensics/feedback/tests/zx_object_util.h"
#include "src/developer/forensics/feedback_data/constants.h"
#include "src/developer/forensics/testing/fakes/cobalt.h"
#include "src/developer/forensics/testing/gmatchers.h"
#include "src/developer/forensics/utils/archive.h"
#include "src/developer/forensics/utils/cobalt/metrics.h"
#include "src/developer/forensics/utils/cobalt/metrics_registry.cb.h"
#include "src/lib/files/file.h"
#include "src/lib/fostr/fidl/fuchsia/feedback/formatting.h"
#include "src/lib/fsl/vmo/strings.h"
#include "src/lib/uuid/uuid.h"
#include "src/ui/lib/escher/test/common/gtest_vulkan.h"
#include "third_party/rapidjson/include/rapidjson/document.h"
#include "third_party/rapidjson/include/rapidjson/schema.h"
namespace forensics::feedback {
namespace {
using fuchsia::feedback::Annotations;
using fuchsia::feedback::Attachment;
using fuchsia::feedback::ComponentDataRegisterSyncPtr;
using fuchsia::feedback::DataProviderSyncPtr;
using fuchsia::feedback::GetAnnotationsParameters;
using fuchsia::feedback::GetSnapshotParameters;
using fuchsia::feedback::ImageEncoding;
using fuchsia::feedback::Screenshot;
using fuchsia::feedback::Snapshot;
using fuchsia::hwinfo::BoardInfo;
using fuchsia::hwinfo::BoardPtr;
using fuchsia::hwinfo::ProductInfo;
using fuchsia::hwinfo::ProductPtr;
using fuchsia::intl::Profile;
using testing::Key;
using testing::UnorderedElementsAreArray;
class LogListener : public fuchsia::logger::LogListenerSafe {
public:
LogListener(std::shared_ptr<sys::ServiceDirectory> services) : binding_(this) {
binding_.Bind(log_listener_.NewRequest());
fuchsia::logger::LogPtr logger = services->Connect<fuchsia::logger::Log>();
logger->ListenSafe(std::move(log_listener_), /*options=*/nullptr);
}
bool HasLogs() { return has_logs_; }
private:
// |fuchsia::logger::LogListenerSafe|
void LogMany(::std::vector<fuchsia::logger::LogMessage> log, LogManyCallback done) {
has_logs_ = true;
done();
}
void Log(fuchsia::logger::LogMessage log, LogCallback done) {
has_logs_ = true;
done();
}
void Done() { FX_NOTIMPLEMENTED(); }
::fidl::Binding<fuchsia::logger::LogListenerSafe> binding_;
fuchsia::logger::LogListenerSafePtr log_listener_;
bool has_logs_ = false;
};
// Smoke-tests the real environment service for the fuchsia.feedback FIDL interfaces,
// connecting through FIDL.
class FeedbackIntegrationTest : public gtest::TestWithEnvironmentFixture {
public:
void SetUp() override {
environment_services_ = sys::ServiceDirectory::CreateFromNamespace();
environment_services_->Connect(crash_register_.NewRequest());
environment_services_->Connect(crash_reporter_.NewRequest());
fake_cobalt_ = std::make_unique<fakes::Cobalt>(environment_services_);
}
void TearDown() override {
if (inspect_test_app_controller_) {
inspect_test_app_controller_->Kill();
bool is_inspect_test_app_terminated = false;
inspect_test_app_controller_.events().OnTerminated =
[&is_inspect_test_app_terminated](int64_t code, fuchsia::sys::TerminationReason reason) {
FX_CHECK(reason == fuchsia::sys::TerminationReason::EXITED);
is_inspect_test_app_terminated = true;
};
RunLoopUntil([&is_inspect_test_app_terminated] { return is_inspect_test_app_terminated; });
}
}
protected:
// Makes sure the component serving fuchsia.logger.Log is up and running as the DumpLogs() request
// could time out on machines where the component is too slow to start.
//
// Syslog are generally handled by a single logger that implements two protocols:
// (1) fuchsia.logger.LogSink to write syslog messages
// (2) fuchsia.logger.Log to read syslog messages and kernel log messages.
// Returned syslog messages are restricted to the ones that were written using its LogSink while
// kernel log messages are the same for all loggers.
//
// In this integration test, we inject a "fresh copy" of archivist.cmx for fuchsia.logger.Log so
// we can retrieve the syslog messages. But we do _not_ inject that same archivist.cmx for
// fuchsia.logger.LogSink as it would swallow all the error and warning messages the other
// injected services could produce and make debugging really hard. Therefore, the injected
// archivist.cmx does not have any syslog messages and will only have the global kernel log
// messages.
//
// When archivist.cmx spawns, it will start collecting asynchronously kernel log messages. But if
// DumpLogs() is called "too soon", it will immediately return empty logs instead of waiting on
// the kernel log collection (fxbug.dev/4665), resulting in a flaky test (fxbug.dev/8303). We thus
// spawn archivist.cmx on advance and wait for it to have at least one message before running the
// actual test.
void WaitForLogger() {
LogListener log_listener(environment_services_);
RunLoopUntil([&log_listener] { return log_listener.HasLogs(); });
}
// Makes sure the component serving fuchsia.update.channelcontrol.ChannelControl is up and running
// as the GetCurrent() request could time out on machines where the component is too slow to
// start.
void WaitForChannelProvider() {
fuchsia::update::channelcontrol::ChannelControlSyncPtr channel_provider;
environment_services_->Connect(channel_provider.NewRequest());
std::string unused;
ASSERT_EQ(channel_provider->GetCurrent(&unused), ZX_OK);
}
// Makes sure there is at least one component in the test environment that exposes some Inspect
// data.
//
// This is useful as we are excluding system_objects paths from the Inspect discovery and the test
// component itself only has a system_objects Inspect node.
void WaitForInspect() {
fuchsia::sys::LaunchInfo launch_info;
launch_info.url = "fuchsia-pkg://fuchsia.com/feedback-tests#meta/inspect_test_app.cmx";
environment_ = CreateNewEnclosingEnvironment("inspect_test_app_environment", CreateServices());
environment_->CreateComponent(std::move(launch_info),
inspect_test_app_controller_.NewRequest());
bool ready = false;
inspect_test_app_controller_.events().OnDirectoryReady = [&ready] { ready = true; };
RunLoopUntil([&ready] { return ready; });
// Additionally wait for the component to appear in the observer's output.
async::Executor executor(dispatcher());
inspect::contrib::ArchiveReader reader(
environment_services_->Connect<fuchsia::diagnostics::ArchiveAccessor>(),
{"inspect_test_app_environment/inspect_test_app.cmx:root"});
bool done = false;
executor.schedule_task(
reader.SnapshotInspectUntilPresent({"inspect_test_app.cmx"})
.then([&](::fpromise::result<std::vector<inspect::contrib::DiagnosticsData>,
std::string>& unused) { done = true; }));
RunLoopUntil([&done] { return done; });
}
// Makes sure the component serving fuchsia.hwinfo.BoardInfo is up and running as the
// GetInfo() request could time out on machines where the component is too slow to start.
void WaitForBoardProvider() {
fuchsia::hwinfo::BoardPtr board_provider;
environment_services_->Connect(board_provider.NewRequest());
bool ready = false;
board_provider->GetInfo([&](BoardInfo board_info) { ready = true; });
RunLoopUntil([&ready] { return ready; });
}
// Makes sure the component serving fuchsia.hwinfo.ProductInfo is up and running as the
// GetInfo() request could time out on machines where the component is too slow to start.
void WaitForProductProvider() {
fuchsia::hwinfo::ProductPtr product_provider;
environment_services_->Connect(product_provider.NewRequest());
bool ready = false;
product_provider->GetInfo([&](ProductInfo product_info) { ready = true; });
RunLoopUntil([&ready] { return ready; });
}
// Makes sure the component serving fuchsia.intl.PropertyProvider is up and running as the
// GetProfile() request could time out on machines where the component is too slow to start.
void WaitForProfileProvider() {
fuchsia::intl::PropertyProviderPtr property_provider;
environment_services_->Connect(property_provider.NewRequest());
bool ready = false;
property_provider->GetProfile([&](Profile profile) { ready = true; });
RunLoopUntil([&ready] { return ready; });
}
void FileCrashReport() {
fuchsia::feedback::CrashReport report;
report.set_program_name("crashing_program");
fuchsia::feedback::CrashReporter_File_Result out_result;
ASSERT_EQ(crash_reporter_->File(std::move(report), &out_result), ZX_OK);
EXPECT_TRUE(out_result.is_response());
}
void RegisterProduct() {
fuchsia::feedback::CrashReportingProduct product;
product.set_name("some name");
product.set_version("some version");
product.set_channel("some channel");
EXPECT_EQ(crash_register_->Upsert("some/component/URL", std::move(product)), ZX_OK);
}
void RegisterProductWithAck() {
fuchsia::feedback::CrashReportingProduct product;
product.set_name("some name");
product.set_version("some version");
product.set_channel("some channel");
EXPECT_EQ(crash_register_->UpsertWithAck("some/component/URL", std::move(product)), ZX_OK);
}
protected:
std::shared_ptr<sys::ServiceDirectory> environment_services_;
private:
std::unique_ptr<sys::testing::EnclosingEnvironment> environment_;
fuchsia::feedback::CrashReportingProductRegisterSyncPtr crash_register_;
fuchsia::feedback::CrashReporterSyncPtr crash_reporter_;
fuchsia::sys::ComponentControllerPtr inspect_test_app_controller_;
protected:
std::unique_ptr<fakes::Cobalt> fake_cobalt_;
};
// Smoke-tests the actual service for fuchsia.feedback.CrashReportingProductRegister, connecting
// through FIDL.
TEST_F(FeedbackIntegrationTest, CrashRegister_SmokeTest) {
RegisterProduct();
RegisterProductWithAck();
}
// Smoke-tests the actual service for fuchsia.feedback.CrashReporter, connecting through FIDL.
TEST_F(FeedbackIntegrationTest, CrashReporter_SmokeTest) {
FileCrashReport();
fake_cobalt_->RegisterExpectedEvent(cobalt::CrashState::kFiled, 1);
fake_cobalt_->RegisterExpectedEvent(cobalt::CrashState::kArchived, 1);
EXPECT_TRUE(
fake_cobalt_->MeetsExpectedEvents(fuchsia::metrics::test::LogMethod::LOG_OCCURRENCE, true));
}
TEST_F(FeedbackIntegrationTest, ComponentDataRegister_Upsert_SmokeTest) {
ComponentDataRegisterSyncPtr data_register;
environment_services_->Connect(data_register.NewRequest());
ASSERT_EQ(data_register->Upsert({}), ZX_OK);
}
// We use VK_TEST instead of the regular TEST macro because Scenic needs Vulkan to operate properly
// and take a screenshot. Note that calls to Scenic hang indefinitely for headless devices so this
// test assumes the device has a display like the other Scenic tests, see fxbug.dev/24479.
VK_TEST_F(FeedbackIntegrationTest, DataProvider_GetScreenshot_SmokeTest) {
DataProviderSyncPtr data_provider;
environment_services_->Connect(data_provider.NewRequest());
std::unique_ptr<Screenshot> out_screenshot;
ASSERT_EQ(data_provider->GetScreenshot(ImageEncoding::PNG, &out_screenshot), ZX_OK);
// We cannot expect a particular payload in the response because Scenic might return a screenshot
// or not depending on which device the test runs.
}
constexpr char kInspectJsonSchema[] = R"({
"type": "array",
"items": {
"type": "object",
"properties": {
"moniker": {
"type": "string"
},
"payload": {
"type": "object"
}
},
"required": [
"moniker",
"payload"
],
"additionalProperties": true
},
"uniqueItems": true
})";
TEST_F(FeedbackIntegrationTest, DataProvider_GetSnapshot_CheckKeys) {
// We make sure the components serving the services GetSnapshot() connects to are up and running.
WaitForLogger();
WaitForChannelProvider();
WaitForInspect();
WaitForBoardProvider();
WaitForProductProvider();
WaitForProfileProvider();
DataProviderSyncPtr data_provider;
environment_services_->Connect(data_provider.NewRequest());
Snapshot snapshot;
ASSERT_EQ(data_provider->GetSnapshot(GetSnapshotParameters(), &snapshot), ZX_OK);
// We cannot expect a particular value for each annotation or attachment because values might
// depend on which device the test runs (e.g., board name) or what happened prior to running this
// test (e.g., logs). But we should expect the keys to be present.
ASSERT_TRUE(snapshot.has_annotations());
EXPECT_THAT(snapshot.annotations(),
testing::IsSupersetOf({
MatchesKey(feedback_data::kAnnotationBuildBoard),
MatchesKey(feedback_data::kAnnotationBuildIsDebug),
MatchesKey(feedback_data::kAnnotationBuildLatestCommitDate),
MatchesKey(feedback_data::kAnnotationBuildProduct),
MatchesKey(feedback_data::kAnnotationBuildVersion),
MatchesKey(feedback_data::kAnnotationDeviceBoardName),
MatchesKey(feedback_data::kAnnotationDeviceFeedbackId),
MatchesKey(feedback_data::kAnnotationDeviceUptime),
MatchesKey(feedback_data::kAnnotationDeviceUtcTime),
MatchesKey(feedback_data::kAnnotationHardwareBoardName),
MatchesKey(feedback_data::kAnnotationHardwareBoardRevision),
MatchesKey(feedback_data::kAnnotationHardwareProductLanguage),
MatchesKey(feedback_data::kAnnotationHardwareProductLocaleList),
MatchesKey(feedback_data::kAnnotationHardwareProductManufacturer),
MatchesKey(feedback_data::kAnnotationHardwareProductModel),
MatchesKey(feedback_data::kAnnotationHardwareProductName),
MatchesKey(feedback_data::kAnnotationHardwareProductRegulatoryDomain),
MatchesKey(feedback_data::kAnnotationHardwareProductSKU),
MatchesKey(feedback_data::kAnnotationSystemBootIdCurrent),
MatchesKey(feedback_data::kAnnotationSystemLastRebootReason),
MatchesKey(feedback_data::kAnnotationSystemTimezonePrimary),
MatchesKey(feedback_data::kAnnotationSystemUpdateChannelCurrent),
MatchesKey(feedback_data::kAnnotationSystemUpdateChannelTarget),
}));
ASSERT_TRUE(snapshot.has_archive());
EXPECT_STREQ(snapshot.archive().key.c_str(), feedback_data::kSnapshotFilename);
std::map<std::string, std::string> unpacked_attachments;
ASSERT_TRUE(Unpack(snapshot.archive().value, &unpacked_attachments));
ASSERT_THAT(unpacked_attachments, testing::UnorderedElementsAreArray({
Key(feedback_data::kAttachmentAnnotations),
Key(feedback_data::kAttachmentBuildSnapshot),
Key(feedback_data::kAttachmentInspect),
Key(feedback_data::kAttachmentLogKernel),
Key(feedback_data::kAttachmentLogSystem),
Key(feedback_data::kAttachmentMetadata),
}));
ASSERT_NE(unpacked_attachments.find(feedback_data::kAttachmentInspect),
unpacked_attachments.end());
const std::string inspect_json = unpacked_attachments[feedback_data::kAttachmentInspect];
ASSERT_FALSE(inspect_json.empty());
// JSON verification.
// We check that the output is a valid JSON and that it matches the schema.
rapidjson::Document json;
ASSERT_FALSE(json.Parse(inspect_json.c_str()).HasParseError());
rapidjson::Document schema_json;
ASSERT_FALSE(schema_json.Parse(kInspectJsonSchema).HasParseError());
rapidjson::SchemaDocument schema(schema_json);
rapidjson::SchemaValidator validator(schema);
EXPECT_TRUE(json.Accept(validator));
// We then check that we get the expected Inspect data for the injected test app.
bool has_entry_for_test_app = false;
for (const auto& obj : json.GetArray()) {
const std::string path = obj["moniker"].GetString();
if (path.find("inspect_test_app.cmx") != std::string::npos) {
has_entry_for_test_app = true;
const auto contents = obj["payload"].GetObject();
ASSERT_TRUE(contents.HasMember("root"));
const auto root = contents["root"].GetObject();
ASSERT_TRUE(root.HasMember("obj1"));
ASSERT_TRUE(root.HasMember("obj2"));
const auto obj1 = root["obj1"].GetObject();
const auto obj2 = root["obj2"].GetObject();
ASSERT_TRUE(obj1.HasMember("version"));
ASSERT_TRUE(obj2.HasMember("version"));
EXPECT_STREQ(obj1["version"].GetString(), "1.0");
EXPECT_STREQ(obj2["version"].GetString(), "1.0");
ASSERT_TRUE(obj1.HasMember("value"));
ASSERT_TRUE(obj2.HasMember("value"));
EXPECT_EQ(obj1["value"].GetUint(), 100u);
EXPECT_EQ(obj2["value"].GetUint(), 200u);
}
}
EXPECT_TRUE(has_entry_for_test_app);
}
TEST_F(FeedbackIntegrationTest, DataProvider_GetAnnotation_CheckKeys) {
// We make sure the components serving the services GetAnnotations() connects to are up and
// running.
WaitForChannelProvider();
WaitForBoardProvider();
WaitForProductProvider();
WaitForProfileProvider();
DataProviderSyncPtr data_provider;
environment_services_->Connect(data_provider.NewRequest());
Annotations annotations;
ASSERT_EQ(data_provider->GetAnnotations(GetAnnotationsParameters(), &annotations), ZX_OK);
// We cannot expect a particular value for each annotation because values might depend on which
// device the test runs (e.g., board name). But we should expect the keys to be present.
ASSERT_TRUE(annotations.has_annotations());
EXPECT_THAT(annotations.annotations(),
testing::IsSupersetOf({
MatchesKey(feedback_data::kAnnotationBuildBoard),
MatchesKey(feedback_data::kAnnotationBuildIsDebug),
MatchesKey(feedback_data::kAnnotationBuildLatestCommitDate),
MatchesKey(feedback_data::kAnnotationBuildProduct),
MatchesKey(feedback_data::kAnnotationBuildVersion),
MatchesKey(feedback_data::kAnnotationDeviceBoardName),
MatchesKey(feedback_data::kAnnotationDeviceFeedbackId),
MatchesKey(feedback_data::kAnnotationDeviceUptime),
MatchesKey(feedback_data::kAnnotationDeviceUtcTime),
MatchesKey(feedback_data::kAnnotationHardwareBoardName),
MatchesKey(feedback_data::kAnnotationHardwareBoardRevision),
MatchesKey(feedback_data::kAnnotationHardwareProductLanguage),
MatchesKey(feedback_data::kAnnotationHardwareProductLocaleList),
MatchesKey(feedback_data::kAnnotationHardwareProductManufacturer),
MatchesKey(feedback_data::kAnnotationHardwareProductModel),
MatchesKey(feedback_data::kAnnotationHardwareProductName),
MatchesKey(feedback_data::kAnnotationHardwareProductRegulatoryDomain),
MatchesKey(feedback_data::kAnnotationHardwareProductSKU),
MatchesKey(feedback_data::kAnnotationSystemBootIdCurrent),
MatchesKey(feedback_data::kAnnotationSystemLastRebootReason),
MatchesKey(feedback_data::kAnnotationSystemUpdateChannelCurrent),
MatchesKey(feedback_data::kAnnotationSystemUpdateChannelTarget),
}));
}
TEST_F(FeedbackIntegrationTest, DataProvider_GetSnapshot_CheckCobalt) {
// We make sure the components serving the services GetSnapshot() connects to are up and running.
WaitForLogger();
WaitForChannelProvider();
WaitForInspect();
WaitForBoardProvider();
WaitForProductProvider();
WaitForProfileProvider();
DataProviderSyncPtr data_provider;
environment_services_->Connect(data_provider.NewRequest());
Snapshot snapshot;
ASSERT_EQ(data_provider->GetSnapshot(GetSnapshotParameters(), &snapshot), ZX_OK);
ASSERT_FALSE(snapshot.IsEmpty());
fake_cobalt_->RegisterExpectedEvent(cobalt::SnapshotGenerationFlow::kSuccess, 1);
fake_cobalt_->RegisterExpectedEvent(cobalt::SnapshotVersion::kV_01, 1);
EXPECT_TRUE(
fake_cobalt_->MeetsExpectedEvents(fuchsia::metrics::test::LogMethod::LOG_INTEGER, false));
}
TEST_F(FeedbackIntegrationTest,
DataProvider_GetSnapshot_NonPlatformAnnotationsFromComponentDataRegister) {
// We make sure the components serving the services GetSnapshot() connects to are up and running.
WaitForLogger();
WaitForChannelProvider();
WaitForInspect();
WaitForBoardProvider();
WaitForProductProvider();
WaitForProfileProvider();
DataProviderSyncPtr data_provider;
environment_services_->Connect(data_provider.NewRequest());
ComponentDataRegisterSyncPtr data_register;
environment_services_->Connect(data_register.NewRequest());
fuchsia::feedback::ComponentData extra_data;
extra_data.set_namespace_("namespace");
extra_data.set_annotations({
{"k", "v"},
});
ASSERT_EQ(data_register->Upsert(std::move(extra_data)), ZX_OK);
Snapshot snapshot;
ASSERT_EQ(data_provider->GetSnapshot(GetSnapshotParameters(), &snapshot), ZX_OK);
ASSERT_TRUE(snapshot.has_annotations());
EXPECT_THAT(snapshot.annotations(), testing::Contains(MatchesAnnotation("namespace.k", "v")));
}
} // namespace
} // namespace forensics::feedback