| // 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/feedback_data/data_provider.h" |
| |
| #include <fuchsia/feedback/cpp/fidl.h> |
| #include <fuchsia/math/cpp/fidl.h> |
| #include <fuchsia/sys/cpp/fidl.h> |
| #include <lib/fit/result.h> |
| #include <lib/syslog/cpp/macros.h> |
| #include <lib/zx/time.h> |
| #include <zircon/errors.h> |
| #include <zircon/types.h> |
| |
| #include <memory> |
| #include <set> |
| #include <string> |
| #include <vector> |
| |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| |
| #include "garnet/public/lib/fostr/fidl/fuchsia/math/formatting.h" |
| #include "src/developer/forensics/feedback_data/annotations/types.h" |
| #include "src/developer/forensics/feedback_data/attachments/types.h" |
| #include "src/developer/forensics/feedback_data/constants.h" |
| #include "src/developer/forensics/feedback_data/metadata.h" |
| #include "src/developer/forensics/testing/gmatchers.h" |
| #include "src/developer/forensics/testing/gpretty_printers.h" |
| #include "src/developer/forensics/testing/stubs/cobalt_logger_factory.h" |
| #include "src/developer/forensics/testing/stubs/scenic.h" |
| #include "src/developer/forensics/testing/unit_test_fixture.h" |
| #include "src/developer/forensics/utils/archive.h" |
| #include "src/lib/fsl/vmo/file.h" |
| #include "src/lib/fsl/vmo/sized_vmo.h" |
| #include "src/lib/fsl/vmo/vector.h" |
| #include "src/lib/fxl/strings/string_printf.h" |
| #include "src/lib/timekeeper/test_clock.h" |
| #include "third_party/rapidjson/include/rapidjson/document.h" |
| #include "third_party/rapidjson/include/rapidjson/schema.h" |
| |
| namespace forensics { |
| namespace feedback_data { |
| namespace { |
| |
| using fuchsia::feedback::Annotation; |
| using fuchsia::feedback::ImageEncoding; |
| using fuchsia::feedback::Screenshot; |
| using fuchsia::feedback::Snapshot; |
| using testing::UnorderedElementsAreArray; |
| |
| const AnnotationKeys kDefaultAnnotations = { |
| kAnnotationBuildBoard, kAnnotationBuildLatestCommitDate, kAnnotationBuildProduct, |
| kAnnotationBuildVersion, kAnnotationDeviceBoardName, kAnnotationDeviceUptime, |
| kAnnotationDeviceUtcTime, |
| }; |
| const AttachmentKeys kDefaultAttachments = { |
| kAttachmentBuildSnapshot, |
| }; |
| |
| constexpr bool kSuccess = true; |
| constexpr bool kFailure = false; |
| |
| constexpr zx::duration kDefaultSnapshotFlowDuration = zx::usec(5); |
| |
| // Returns a Screenshot with the right dimensions, no image. |
| std::unique_ptr<Screenshot> MakeUniqueScreenshot(const size_t image_dim_in_px) { |
| std::unique_ptr<Screenshot> screenshot = std::make_unique<Screenshot>(); |
| screenshot->dimensions_in_px.height = image_dim_in_px; |
| screenshot->dimensions_in_px.width = image_dim_in_px; |
| return screenshot; |
| } |
| |
| // Represents arguments for DataProvider::GetScreenshotCallback. |
| struct GetScreenshotResponse { |
| std::unique_ptr<Screenshot> screenshot; |
| |
| // This should be kept in sync with DoGetScreenshotResponseMatch() as we only want to display what |
| // we actually compare, for now the presence of a screenshot and its dimensions if present. |
| operator std::string() const { |
| if (!screenshot) { |
| return "no screenshot"; |
| } |
| const fuchsia::math::Size& dimensions_in_px = screenshot->dimensions_in_px; |
| return fxl::StringPrintf("a %d x %d screenshot", dimensions_in_px.width, |
| dimensions_in_px.height); |
| } |
| |
| // This is used by gTest to pretty-prints failed expectations instead of the default byte string. |
| friend std::ostream& operator<<(std::ostream& os, const GetScreenshotResponse& response) { |
| return os << std::string(response); |
| } |
| }; |
| |
| // Compares two GetScreenshotResponse objects. |
| // |
| // This should be kept in sync with std::string() as we only want to display what we actually |
| // compare, for now the presence of a screenshot and its dimensions. |
| template <typename ResultListenerT> |
| bool DoGetScreenshotResponseMatch(const GetScreenshotResponse& actual, |
| const GetScreenshotResponse& expected, |
| ResultListenerT* result_listener) { |
| if (actual.screenshot == nullptr && expected.screenshot == nullptr) { |
| return true; |
| } |
| if (actual.screenshot == nullptr && expected.screenshot != nullptr) { |
| *result_listener << "Got no screenshot, expected one"; |
| return false; |
| } |
| if (expected.screenshot == nullptr && actual.screenshot != nullptr) { |
| *result_listener << "Expected no screenshot, got one"; |
| return false; |
| } |
| // actual.screenshot and expected.screenshot are now valid. |
| |
| if (!::fidl::Equals(actual.screenshot->dimensions_in_px, expected.screenshot->dimensions_in_px)) { |
| *result_listener << "Expected screenshot dimensions " << expected.screenshot->dimensions_in_px |
| << ", got " << actual.screenshot->dimensions_in_px; |
| return false; |
| } |
| |
| // We do not compare the VMOs. |
| |
| return true; |
| } |
| |
| // Returns true if gMock |arg| matches |expected|, assuming two GetScreenshotResponse objects. |
| MATCHER_P(MatchesGetScreenshotResponse, expected, "matches " + std::string(expected.get())) { |
| return DoGetScreenshotResponseMatch(arg, expected, result_listener); |
| } |
| |
| // Unit-tests the implementation of the fuchsia.feedback.DataProvider FIDL interface. |
| // |
| // This does not test the environment service. It directly instantiates the class, without |
| // connecting through FIDL. |
| class DataProviderTest : public UnitTestFixture { |
| public: |
| DataProviderTest() : inspect_data_budget_("non-existent_path") {} |
| |
| void SetUp() override { |
| // |cobalt_| owns the test clock through a unique_ptr so we need to allocate |clock_| on the |
| // heap and then give |cobalt_| ownership of it. This allows us to control the time perceived by |
| // |cobalt_|. |
| clock_ = new timekeeper::TestClock(); |
| cobalt_ = std::make_unique<cobalt::Logger>(dispatcher(), services(), |
| std::unique_ptr<timekeeper::TestClock>(clock_)); |
| SetUpCobaltServer(std::make_unique<stubs::CobaltLoggerFactory>()); |
| } |
| |
| protected: |
| void SetUpDataProvider(const AnnotationKeys& annotation_allowlist = kDefaultAnnotations, |
| const AttachmentKeys& attachment_allowlist = kDefaultAttachments) { |
| datastore_ = std::make_unique<Datastore>( |
| dispatcher(), services(), cobalt_.get(), annotation_allowlist, attachment_allowlist, |
| PreviousBootFile::FromData(/*is_first_instance=*/true, "empty_boot_id.txt"), |
| &inspect_data_budget_); |
| data_provider_ = std::make_unique<DataProvider>( |
| dispatcher(), services(), clock_, /*is_first_instance=*/true, annotation_allowlist, |
| attachment_allowlist, cobalt_.get(), datastore_.get(), &inspect_data_budget_); |
| } |
| |
| void SetUpScenicServer(std::unique_ptr<stubs::ScenicBase> server) { |
| scenic_server_ = std::move(server); |
| if (scenic_server_) { |
| InjectServiceProvider(scenic_server_.get()); |
| } |
| } |
| |
| GetScreenshotResponse GetScreenshot() { |
| FX_CHECK(data_provider_); |
| |
| GetScreenshotResponse out_response; |
| data_provider_->GetScreenshot(ImageEncoding::PNG, |
| [&out_response](std::unique_ptr<Screenshot> screenshot) { |
| out_response.screenshot = std::move(screenshot); |
| }); |
| RunLoopUntilIdle(); |
| return out_response; |
| } |
| |
| Snapshot GetSnapshot(zx::duration snapshot_flow_duration = kDefaultSnapshotFlowDuration) { |
| FX_CHECK(data_provider_ && clock_); |
| |
| Snapshot snapshot; |
| |
| // We can set |clock_|'s start and end times because the call to start the timer happens |
| // independently of the loop while the call to end it happens in a task that is posted on the |
| // loop. So, as long the end time is set before the loop is run, a non-zero duration will be |
| // recorded. |
| clock_->Set(zx::time(0)); |
| data_provider_->GetSnapshot(fuchsia::feedback::GetSnapshotParameters(), |
| [&snapshot](Snapshot res) { snapshot = std::move(res); }); |
| clock_->Set(zx::time(0) + snapshot_flow_duration); |
| RunLoopUntilIdle(); |
| return snapshot; |
| } |
| |
| std::map<std::string, std::string> UnpackSnapshot(const Snapshot& snapshot) { |
| FX_CHECK(snapshot.has_archive()); |
| FX_CHECK(snapshot.archive().key == kSnapshotFilename); |
| std::map<std::string, std::string> unpacked_attachments; |
| FX_CHECK(Unpack(snapshot.archive().value, &unpacked_attachments)); |
| return unpacked_attachments; |
| } |
| |
| private: |
| // The lifetime of |clock_| is managed by |cobalt_|. |
| timekeeper::TestClock* clock_; |
| std::unique_ptr<cobalt::Logger> cobalt_; |
| std::unique_ptr<Datastore> datastore_; |
| |
| protected: |
| std::unique_ptr<DataProvider> data_provider_; |
| |
| private: |
| std::unique_ptr<stubs::ScenicBase> scenic_server_; |
| InspectDataBudget inspect_data_budget_; |
| }; |
| |
| TEST_F(DataProviderTest, GetScreenshot_SucceedOnScenicReturningSuccess) { |
| const size_t image_dim_in_px = 100; |
| std::vector<stubs::TakeScreenshotResponse> scenic_responses; |
| scenic_responses.emplace_back(stubs::CreateCheckerboardScreenshot(image_dim_in_px), kSuccess); |
| auto scenic = std::make_unique<stubs::Scenic>(); |
| scenic->set_take_screenshot_responses(std::move(scenic_responses)); |
| SetUpScenicServer(std::move(scenic)); |
| SetUpDataProvider(); |
| |
| GetScreenshotResponse feedback_response = GetScreenshot(); |
| |
| ASSERT_NE(feedback_response.screenshot, nullptr); |
| EXPECT_EQ(static_cast<size_t>(feedback_response.screenshot->dimensions_in_px.height), |
| image_dim_in_px); |
| EXPECT_EQ(static_cast<size_t>(feedback_response.screenshot->dimensions_in_px.width), |
| image_dim_in_px); |
| EXPECT_TRUE(feedback_response.screenshot->image.vmo.is_valid()); |
| |
| fsl::SizedVmo expected_sized_vmo; |
| ASSERT_TRUE(fsl::VmoFromFilename("/pkg/data/checkerboard_100.png", &expected_sized_vmo)); |
| std::vector<uint8_t> expected_pixels; |
| ASSERT_TRUE(fsl::VectorFromVmo(expected_sized_vmo, &expected_pixels)); |
| std::vector<uint8_t> actual_pixels; |
| ASSERT_TRUE(fsl::VectorFromVmo(feedback_response.screenshot->image, &actual_pixels)); |
| EXPECT_EQ(actual_pixels, expected_pixels); |
| } |
| |
| TEST_F(DataProviderTest, GetScreenshot_FailOnScenicNotAvailable) { |
| SetUpDataProvider(); |
| |
| GetScreenshotResponse feedback_response = GetScreenshot(); |
| EXPECT_EQ(feedback_response.screenshot, nullptr); |
| } |
| |
| TEST_F(DataProviderTest, GetScreenshot_FailOnScenicReturningFailure) { |
| std::vector<stubs::TakeScreenshotResponse> scenic_responses; |
| scenic_responses.emplace_back(stubs::CreateEmptyScreenshot(), kFailure); |
| auto scenic = std::make_unique<stubs::Scenic>(); |
| scenic->set_take_screenshot_responses(std::move(scenic_responses)); |
| SetUpScenicServer(std::move(scenic)); |
| SetUpDataProvider(); |
| |
| GetScreenshotResponse feedback_response = GetScreenshot(); |
| EXPECT_EQ(feedback_response.screenshot, nullptr); |
| } |
| |
| TEST_F(DataProviderTest, GetScreenshot_FailOnScenicReturningNonBGRA8Screenshot) { |
| std::vector<stubs::TakeScreenshotResponse> scenic_responses; |
| scenic_responses.emplace_back(stubs::CreateNonBGRA8Screenshot(), kSuccess); |
| auto scenic = std::make_unique<stubs::Scenic>(); |
| scenic->set_take_screenshot_responses(std::move(scenic_responses)); |
| SetUpScenicServer(std::move(scenic)); |
| SetUpDataProvider(); |
| |
| GetScreenshotResponse feedback_response = GetScreenshot(); |
| EXPECT_EQ(feedback_response.screenshot, nullptr); |
| } |
| |
| TEST_F(DataProviderTest, GetScreenshot_ParallelRequests) { |
| // We simulate three calls to DataProvider::GetScreenshot(): one for which the stub Scenic |
| // will return a checkerboard 10x10, one for a 20x20 and one failure. |
| const size_t num_calls = 3u; |
| const size_t image_dim_in_px_0 = 10u; |
| const size_t image_dim_in_px_1 = 20u; |
| std::vector<stubs::TakeScreenshotResponse> scenic_responses; |
| scenic_responses.emplace_back(stubs::CreateCheckerboardScreenshot(image_dim_in_px_0), kSuccess); |
| scenic_responses.emplace_back(stubs::CreateCheckerboardScreenshot(image_dim_in_px_1), kSuccess); |
| scenic_responses.emplace_back(stubs::CreateEmptyScreenshot(), kFailure); |
| ASSERT_EQ(scenic_responses.size(), num_calls); |
| auto scenic = std::make_unique<stubs::Scenic>(); |
| scenic->set_take_screenshot_responses(std::move(scenic_responses)); |
| SetUpScenicServer(std::move(scenic)); |
| SetUpDataProvider(); |
| |
| std::vector<GetScreenshotResponse> feedback_responses; |
| for (size_t i = 0; i < num_calls; i++) { |
| data_provider_->GetScreenshot(ImageEncoding::PNG, |
| [&feedback_responses](std::unique_ptr<Screenshot> screenshot) { |
| feedback_responses.push_back({std::move(screenshot)}); |
| }); |
| } |
| RunLoopUntilIdle(); |
| EXPECT_EQ(feedback_responses.size(), num_calls); |
| |
| // We cannot assume that the order of the DataProvider::GetScreenshot() calls match the order |
| // of the Scenic::TakeScreenshot() callbacks because of the async message loop. Thus we need to |
| // match them as sets. |
| // |
| // We set the expectations in advance and then pass a reference to the gMock matcher using |
| // testing::ByRef() because the underlying VMO is not copyable. |
| const GetScreenshotResponse expected_0 = {MakeUniqueScreenshot(image_dim_in_px_0)}; |
| const GetScreenshotResponse expected_1 = {MakeUniqueScreenshot(image_dim_in_px_1)}; |
| const GetScreenshotResponse expected_2 = {nullptr}; |
| EXPECT_THAT(feedback_responses, testing::UnorderedElementsAreArray({ |
| MatchesGetScreenshotResponse(testing::ByRef(expected_0)), |
| MatchesGetScreenshotResponse(testing::ByRef(expected_1)), |
| MatchesGetScreenshotResponse(testing::ByRef(expected_2)), |
| })); |
| |
| // Additionally, we check that in the non-empty responses, the VMO is valid. |
| for (const auto& response : feedback_responses) { |
| if (response.screenshot == nullptr) { |
| continue; |
| } |
| EXPECT_TRUE(response.screenshot->image.vmo.is_valid()); |
| EXPECT_GE(response.screenshot->image.size, 0u); |
| } |
| } |
| |
| TEST_F(DataProviderTest, GetSnapshot_SmokeTest) { |
| SetUpDataProvider(); |
| |
| Snapshot snapshot = GetSnapshot(); |
| |
| // There will always be a "manifest.json" so there will always be an archive. |
| |
| ASSERT_TRUE(snapshot.has_archive()); |
| |
| const auto attachment_size = snapshot.archive().value.size; |
| ASSERT_TRUE(attachment_size > 0); |
| |
| EXPECT_THAT(ReceivedCobaltEvents(), |
| UnorderedElementsAreArray({ |
| cobalt::Event(cobalt::SnapshotGenerationFlow::kSuccess, |
| kDefaultSnapshotFlowDuration.to_usecs()), |
| cobalt::Event(cobalt::SnapshotVersion::kV_01, attachment_size), |
| })); |
| } |
| |
| TEST_F(DataProviderTest, GetSnapshot_AnnotationsAsAttachment) { |
| SetUpDataProvider(); |
| |
| Snapshot snapshot = GetSnapshot(); |
| auto unpacked_attachments = UnpackSnapshot(snapshot); |
| |
| // There should be an "annotations.json" attachment present in the snapshot. |
| ASSERT_NE(unpacked_attachments.find(kAttachmentAnnotations), unpacked_attachments.end()); |
| const std::string annotations_json = unpacked_attachments[kAttachmentAnnotations]; |
| ASSERT_FALSE(annotations_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(annotations_json.c_str()).HasParseError()); |
| rapidjson::Document schema_json; |
| ASSERT_FALSE( |
| schema_json |
| .Parse(fxl::StringPrintf( |
| R"({ |
| "type": "object", |
| "properties": { |
| "%s": { |
| "type": "string" |
| }, |
| "%s": { |
| "type": "string" |
| }, |
| "%s": { |
| "type": "string" |
| }, |
| "%s": { |
| "type": "string" |
| }, |
| "%s": { |
| "type": "string" |
| }, |
| "%s": { |
| "type": "string" |
| }, |
| "%s": { |
| "type": "string" |
| }, |
| "%s": { |
| "type": "string" |
| } |
| }, |
| "additionalProperties": false |
| })", |
| kAnnotationBuildBoard, kAnnotationBuildIsDebug, kAnnotationBuildLatestCommitDate, |
| kAnnotationBuildProduct, kAnnotationBuildVersion, kAnnotationDeviceBoardName, |
| kAnnotationDeviceUptime, kAnnotationDeviceUtcTime)) |
| .HasParseError()); |
| rapidjson::SchemaDocument schema(schema_json); |
| rapidjson::SchemaValidator validator(schema); |
| EXPECT_TRUE(json.Accept(validator)); |
| } |
| |
| TEST_F(DataProviderTest, GetSnapshot_ManifestAsAttachment) { |
| SetUpDataProvider(); |
| |
| Snapshot snapshot = GetSnapshot(); |
| auto unpacked_attachments = UnpackSnapshot(snapshot); |
| |
| // There should be a "metadata.json" attachment present in the snapshot. |
| ASSERT_NE(unpacked_attachments.find(kAttachmentMetadata), unpacked_attachments.end()); |
| } |
| |
| TEST_F(DataProviderTest, GetSnapshot_SingleAttachmentOnEmptyAttachmentAllowlist) { |
| SetUpDataProvider(kDefaultAnnotations, /*attachment_allowlist=*/{}); |
| |
| Snapshot snapshot = GetSnapshot(); |
| auto unpacked_attachments = UnpackSnapshot(snapshot); |
| ASSERT_NE(unpacked_attachments.find(kAttachmentAnnotations), unpacked_attachments.end()); |
| } |
| |
| } // namespace |
| } // namespace feedback_data |
| } // namespace forensics |