| // 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/feedback/feedback_agent/data_provider.h" |
| |
| #include <fuchsia/feedback/cpp/fidl.h> |
| #include <fuchsia/hwinfo/cpp/fidl.h> |
| #include <fuchsia/intl/cpp/fidl.h> |
| #include <fuchsia/math/cpp/fidl.h> |
| #include <fuchsia/sys/cpp/fidl.h> |
| #include <lib/fit/result.h> |
| #include <lib/fostr/fidl/fuchsia/math/formatting.h> |
| #include <lib/sys/cpp/testing/test_with_environment.h> |
| #include <lib/syslog/logger.h> |
| #include <lib/zx/time.h> |
| #include <zircon/errors.h> |
| #include <zircon/types.h> |
| |
| #include <memory> |
| #include <set> |
| #include <string> |
| #include <vector> |
| |
| #include "src/developer/feedback/feedback_agent/config.h" |
| #include "src/developer/feedback/feedback_agent/constants.h" |
| #include "src/developer/feedback/feedback_agent/tests/stub_board.h" |
| #include "src/developer/feedback/feedback_agent/tests/stub_channel_provider.h" |
| #include "src/developer/feedback/feedback_agent/tests/stub_logger.h" |
| #include "src/developer/feedback/feedback_agent/tests/stub_product.h" |
| #include "src/developer/feedback/feedback_agent/tests/stub_scenic.h" |
| #include "src/developer/feedback/testing/gmatchers.h" |
| #include "src/developer/feedback/testing/gpretty_printers.h" |
| #include "src/developer/feedback/testing/unit_test_fixture.h" |
| #include "src/developer/feedback/utils/archive.h" |
| #include "src/lib/fsl/vmo/file.h" |
| #include "src/lib/fsl/vmo/sized_vmo.h" |
| #include "src/lib/fsl/vmo/strings.h" |
| #include "src/lib/fsl/vmo/vector.h" |
| #include "src/lib/fxl/logging.h" |
| #include "src/lib/fxl/strings/split_string.h" |
| #include "src/lib/fxl/strings/string_printf.h" |
| #include "src/lib/fxl/strings/string_view.h" |
| #include "src/lib/fxl/test/test_settings.h" |
| #include "src/lib/syslog/cpp/logger.h" |
| #include "third_party/googletest/googlemock/include/gmock/gmock.h" |
| #include "third_party/googletest/googletest/include/gtest/gtest.h" |
| #include "third_party/rapidjson/include/rapidjson/document.h" |
| #include "third_party/rapidjson/include/rapidjson/schema.h" |
| |
| namespace feedback { |
| namespace { |
| |
| using fuchsia::feedback::Attachment; |
| using fuchsia::feedback::Data; |
| using fuchsia::feedback::ImageEncoding; |
| using fuchsia::feedback::Screenshot; |
| using fuchsia::hwinfo::BoardInfo; |
| using fuchsia::hwinfo::ProductInfo; |
| using fuchsia::intl::LocaleId; |
| using fuchsia::intl::RegulatoryDomain; |
| using fxl::SplitResult::kSplitWantNonEmpty; |
| using fxl::WhiteSpaceHandling::kTrimWhitespace; |
| |
| const std::set<std::string> kDefaultAnnotations = { |
| kAnnotationBuildBoard, |
| kAnnotationBuildLatestCommitDate, |
| kAnnotationBuildProduct, |
| kAnnotationBuildVersion, |
| kAnnotationChannel, |
| kAnnotationDeviceBoardName, |
| kAnnotationDeviceUptime, |
| kAnnotationDeviceUTCTime, |
| kAnnotationHardwareBoardName, |
| kAnnotationHardwareBoardRevision, |
| kAnnotationHardwareProductSKU, |
| kAnnotationHardwareProductLanguage, |
| kAnnotationHardwareProductRegulatoryDomain, |
| kAnnotationHardwareProductLocaleList, |
| kAnnotationHardwareProductName, |
| kAnnotationHardwareProductModel, |
| kAnnotationHardwareProductManufacturer, |
| }; |
| |
| const std::set<std::string> kDefaultAttachments = { |
| kAttachmentBuildSnapshot, |
| // TODO(fxb/39804): re-enable once using Inspect service. |
| // kAttachmentInspect, |
| kAttachmentLogKernel, |
| kAttachmentLogSystem, |
| }; |
| const std::map<std::string, std::string> kBoardInfoValues = { |
| {kAnnotationHardwareBoardName, "board-name"}, |
| {kAnnotationHardwareBoardRevision, "revision"}, |
| }; |
| const std::map<std::string, std::string> kProductInfoValues = { |
| {kAnnotationHardwareProductSKU, "sku"}, |
| {kAnnotationHardwareProductLanguage, "language"}, |
| {kAnnotationHardwareProductRegulatoryDomain, "regulatory-domain"}, |
| {kAnnotationHardwareProductLocaleList, "locale1, locale2, locale3"}, |
| {kAnnotationHardwareProductName, "name"}, |
| {kAnnotationHardwareProductModel, "model"}, |
| {kAnnotationHardwareProductManufacturer, "manufacturer"}, |
| }; |
| const Config kDefaultConfig = Config{kDefaultAnnotations, kDefaultAttachments}; |
| |
| constexpr bool kSuccess = true; |
| constexpr bool kFailure = false; |
| |
| constexpr zx::duration kDataProviderIdleTimeout = zx::sec(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; |
| } |
| |
| BoardInfo CreateBoardInfo() { |
| BoardInfo info; |
| |
| info.set_name(kBoardInfoValues.at(kAnnotationHardwareBoardName)); |
| info.set_revision(kBoardInfoValues.at(kAnnotationHardwareBoardRevision)); |
| |
| return info; |
| } |
| |
| ProductInfo CreateProductInfo() { |
| ProductInfo info; |
| |
| info.set_sku(kProductInfoValues.at(kAnnotationHardwareProductSKU)); |
| info.set_language(kProductInfoValues.at(kAnnotationHardwareProductLanguage)); |
| info.set_name(kProductInfoValues.at(kAnnotationHardwareProductName)); |
| info.set_model(kProductInfoValues.at(kAnnotationHardwareProductModel)); |
| info.set_manufacturer(kProductInfoValues.at(kAnnotationHardwareProductManufacturer)); |
| |
| RegulatoryDomain domain; |
| domain.set_country_code(kProductInfoValues.at(kAnnotationHardwareProductRegulatoryDomain)); |
| info.set_regulatory_domain(std::move(domain)); |
| |
| auto locale_strings = |
| fxl::SplitStringCopy(kProductInfoValues.at(kAnnotationHardwareProductLocaleList), ",", |
| kTrimWhitespace, kSplitWantNonEmpty); |
| std::vector<LocaleId> locales; |
| for (const auto& locale : locale_strings) { |
| locales.emplace_back(); |
| locales.back().id = locale; |
| } |
| info.set_locale_list(locales); |
| |
| return info; |
| } |
| |
| // 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: |
| void SetUp() override { SetUpDataProvider(kDefaultConfig); } |
| |
| protected: |
| void SetUpDataProvider(const Config& config) { |
| data_provider_.reset(new DataProvider( |
| dispatcher(), services(), config, [this] { data_provider_timed_out_ = true; }, |
| kDataProviderIdleTimeout)); |
| } |
| |
| void SetUpDataProviderOnlyRequestingChannel(zx::duration timeout) { |
| data_provider_.reset(new DataProvider( |
| dispatcher(), services(), Config{{kAnnotationChannel}, {}}, |
| [this] { data_provider_timed_out_ = true; }, timeout)); |
| } |
| |
| void SetUpScenic(std::unique_ptr<StubScenic> scenic) { |
| scenic_ = std::move(scenic); |
| if (scenic_) { |
| InjectServiceProvider(scenic_.get()); |
| } |
| } |
| |
| void SetUpLogger(const std::vector<fuchsia::logger::LogMessage>& messages) { |
| logger_.reset(new StubLogger()); |
| logger_->set_messages(messages); |
| InjectServiceProvider(logger_.get()); |
| } |
| |
| void SetUpChannelProvider(std::unique_ptr<StubChannelProvider> channel_provider) { |
| channel_provider_ = std::move(channel_provider); |
| if (channel_provider_) { |
| InjectServiceProvider(channel_provider_.get()); |
| } |
| } |
| |
| void SetUpBoardProvider(std::unique_ptr<StubBoard> board_provider) { |
| board_provider_ = std::move(board_provider); |
| if (board_provider_) { |
| InjectServiceProvider(board_provider_.get()); |
| } |
| } |
| |
| void SetUpProductProvider(std::unique_ptr<StubProduct> product_provider) { |
| product_provider_ = std::move(product_provider); |
| if (product_provider_) { |
| InjectServiceProvider(product_provider_.get()); |
| } |
| } |
| |
| GetScreenshotResponse GetScreenshot() { |
| 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; |
| } |
| |
| fit::result<Data, zx_status_t> GetData() { |
| fit::result<Data, zx_status_t> out_result; |
| data_provider_->GetData( |
| [&out_result](fit::result<Data, zx_status_t> result) { out_result = std::move(result); }); |
| RunLoopUntilIdle(); |
| return out_result; |
| } |
| |
| void UnpackAttachmentBundle(const Data& data, std::vector<Attachment>* unpacked_attachments) { |
| ASSERT_TRUE(data.has_attachment_bundle()); |
| const auto& attachment_bundle = data.attachment_bundle(); |
| EXPECT_STREQ(attachment_bundle.key.c_str(), kAttachmentBundle); |
| ASSERT_TRUE(Unpack(attachment_bundle.value, unpacked_attachments)); |
| } |
| |
| uint64_t total_num_scenic_bindings() { return scenic_->total_num_bindings(); } |
| size_t current_num_scenic_bindings() { return scenic_->current_num_bindings(); } |
| const std::vector<TakeScreenshotResponse>& get_scenic_responses() const { |
| return scenic_->take_screenshot_responses(); |
| } |
| |
| std::unique_ptr<DataProvider> data_provider_; |
| bool data_provider_timed_out_ = false; |
| |
| private: |
| std::unique_ptr<StubChannelProvider> channel_provider_; |
| std::unique_ptr<StubScenic> scenic_; |
| std::unique_ptr<StubLogger> logger_; |
| std::unique_ptr<StubBoard> board_provider_; |
| std::unique_ptr<StubProduct> product_provider_; |
| }; |
| |
| TEST_F(DataProviderTest, GetScreenshot_SucceedOnScenicReturningSuccess) { |
| const size_t image_dim_in_px = 100; |
| std::vector<TakeScreenshotResponse> scenic_responses; |
| scenic_responses.emplace_back(CreateCheckerboardScreenshot(image_dim_in_px), kSuccess); |
| auto scenic = std::make_unique<StubScenic>(); |
| scenic->set_take_screenshot_responses(std::move(scenic_responses)); |
| SetUpScenic(std::move(scenic)); |
| |
| GetScreenshotResponse feedback_response = GetScreenshot(); |
| |
| EXPECT_TRUE(get_scenic_responses().empty()); |
| |
| 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) { |
| SetUpScenic(nullptr); |
| GetScreenshotResponse feedback_response = GetScreenshot(); |
| EXPECT_EQ(feedback_response.screenshot, nullptr); |
| } |
| |
| TEST_F(DataProviderTest, GetScreenshot_FailOnScenicReturningFailure) { |
| std::vector<TakeScreenshotResponse> scenic_responses; |
| scenic_responses.emplace_back(CreateEmptyScreenshot(), kFailure); |
| auto scenic = std::make_unique<StubScenic>(); |
| scenic->set_take_screenshot_responses(std::move(scenic_responses)); |
| SetUpScenic(std::move(scenic)); |
| |
| GetScreenshotResponse feedback_response = GetScreenshot(); |
| |
| EXPECT_TRUE(get_scenic_responses().empty()); |
| |
| EXPECT_EQ(feedback_response.screenshot, nullptr); |
| } |
| |
| TEST_F(DataProviderTest, GetScreenshot_FailOnScenicReturningNonBGRA8Screenshot) { |
| std::vector<TakeScreenshotResponse> scenic_responses; |
| scenic_responses.emplace_back(CreateNonBGRA8Screenshot(), kSuccess); |
| auto scenic = std::make_unique<StubScenic>(); |
| scenic->set_take_screenshot_responses(std::move(scenic_responses)); |
| SetUpScenic(std::move(scenic)); |
| |
| GetScreenshotResponse feedback_response = GetScreenshot(); |
| |
| EXPECT_TRUE(get_scenic_responses().empty()); |
| |
| 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<TakeScreenshotResponse> scenic_responses; |
| scenic_responses.emplace_back(CreateCheckerboardScreenshot(image_dim_in_px_0), kSuccess); |
| scenic_responses.emplace_back(CreateCheckerboardScreenshot(image_dim_in_px_1), kSuccess); |
| scenic_responses.emplace_back(CreateEmptyScreenshot(), kFailure); |
| ASSERT_EQ(scenic_responses.size(), num_calls); |
| auto scenic = std::make_unique<StubScenic>(); |
| scenic->set_take_screenshot_responses(std::move(scenic_responses)); |
| SetUpScenic(std::move(scenic)); |
| |
| 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); |
| EXPECT_TRUE(get_scenic_responses().empty()); |
| |
| // 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, GetScreenshot_OneScenicConnectionPerGetScreenshotCall) { |
| // We use a stub that always returns false as we are not interested in the responses. |
| SetUpScenic(std::make_unique<StubScenicAlwaysReturnsFalse>()); |
| |
| const size_t num_calls = 5u; |
| 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); |
| |
| EXPECT_EQ(total_num_scenic_bindings(), num_calls); |
| // The unbinding is asynchronous so we need to run the loop until all the outstanding connections |
| // are actually close in the stub. |
| RunLoopUntilIdle(); |
| EXPECT_EQ(current_num_scenic_bindings(), 0u); |
| } |
| |
| TEST_F(DataProviderTest, GetData_SmokeTest) { |
| fit::result<Data, zx_status_t> result = GetData(); |
| |
| ASSERT_TRUE(result.is_ok()); |
| |
| // 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. |
| |
| const Data& data = result.value(); |
| |
| // If there are annotations, there should also be the attachment bundle. |
| if (data.has_annotations()) { |
| ASSERT_TRUE(data.has_attachment_bundle()); |
| } |
| } |
| |
| TEST_F(DataProviderTest, GetData_AnnotationsAsAttachment) { |
| fit::result<Data, zx_status_t> result = GetData(); |
| |
| ASSERT_TRUE(result.is_ok()); |
| |
| const Data& data = result.value(); |
| |
| // There should be an "annotations.json" attachment present in the attachment bundle. |
| std::vector<Attachment> unpacked_attachments; |
| UnpackAttachmentBundle(data, &unpacked_attachments); |
| bool found_annotations_attachment = false; |
| std::string annotations_json; |
| for (const auto& attachment : unpacked_attachments) { |
| if (attachment.key != kAttachmentAnnotations) { |
| continue; |
| } |
| found_annotations_attachment = true; |
| |
| ASSERT_TRUE(fsl::StringFromVmo(attachment.value, &annotations_json)); |
| 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" |
| }, |
| "%s": { |
| "type": "string" |
| }, |
| "%s": { |
| "type": "string" |
| }, |
| "%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, kAnnotationChannel, |
| kAnnotationDeviceBoardName, kAnnotationDeviceUptime, kAnnotationDeviceUTCTime, |
| kAnnotationHardwareBoardName, kAnnotationHardwareBoardRevision, |
| kAnnotationHardwareProductLanguage, kAnnotationHardwareProductLocaleList, |
| kAnnotationHardwareProductManufacturer, kAnnotationHardwareProductModel, |
| kAnnotationHardwareProductName, kAnnotationHardwareProductRegulatoryDomain, |
| kAnnotationHardwareProductSKU)) |
| .HasParseError()); |
| rapidjson::SchemaDocument schema(schema_json); |
| rapidjson::SchemaValidator validator(schema); |
| EXPECT_TRUE(json.Accept(validator)); |
| } |
| EXPECT_TRUE(found_annotations_attachment); |
| } |
| |
| TEST_F(DataProviderTest, GetData_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. |
| SetUpLogger({ |
| BuildLogMessage(FX_LOG_INFO, "log message", |
| /*timestamp_offset=*/zx::duration(0), {"foo"}), |
| }); |
| const std::string expected_syslog = "[15604.000][07559][07687][foo] INFO: log message\n"; |
| |
| fit::result<Data, zx_status_t> result = GetData(); |
| |
| ASSERT_TRUE(result.is_ok()); |
| |
| const Data& data = result.value(); |
| |
| // There should be a "log.system.txt" attachment present in the attachment bundle. |
| std::vector<Attachment> unpacked_attachments; |
| UnpackAttachmentBundle(data, &unpacked_attachments); |
| EXPECT_THAT(unpacked_attachments, |
| testing::Contains(MatchesAttachment(kAttachmentLogSystem, expected_syslog))); |
| } |
| |
| TEST_F(DataProviderTest, GetData_Channel) { |
| auto channel_provider = std::make_unique<StubChannelProvider>(); |
| channel_provider->set_channel("my-channel"); |
| SetUpChannelProvider(std::move(channel_provider)); |
| |
| fit::result<Data, zx_status_t> result = GetData(); |
| |
| ASSERT_TRUE(result.is_ok()); |
| |
| const Data& data = result.value(); |
| ASSERT_TRUE(data.has_annotations()); |
| EXPECT_THAT(data.annotations(), |
| testing::Contains(MatchesAnnotation(kAnnotationChannel, "my-channel"))); |
| } |
| |
| TEST_F(DataProviderTest, GetData_BoardInfo) { |
| SetUpBoardProvider(std::make_unique<StubBoard>(CreateBoardInfo())); |
| |
| std::set<std::string> keys; |
| for (const auto& [key, _] : kBoardInfoValues) { |
| keys.insert(key); |
| } |
| |
| fit::result<Data, zx_status_t> result = GetData(); |
| ASSERT_TRUE(result.is_ok()); |
| |
| const Data& data = result.value(); |
| ASSERT_TRUE(data.has_annotations()); |
| EXPECT_THAT(data.annotations(), |
| testing::IsSupersetOf({ |
| MatchesAnnotation(kAnnotationHardwareBoardName, |
| kBoardInfoValues.at(kAnnotationHardwareBoardName)), |
| MatchesAnnotation(kAnnotationHardwareBoardRevision, |
| kBoardInfoValues.at(kAnnotationHardwareBoardRevision)), |
| })); |
| } |
| |
| TEST_F(DataProviderTest, GetData_ProductInfo) { |
| SetUpProductProvider(std::make_unique<StubProduct>(CreateProductInfo())); |
| |
| std::set<std::string> keys; |
| for (const auto& [key, _] : kProductInfoValues) { |
| keys.insert(key); |
| } |
| |
| fit::result<Data, zx_status_t> result = GetData(); |
| ASSERT_TRUE(result.is_ok()); |
| |
| const Data& data = result.value(); |
| ASSERT_TRUE(data.has_annotations()); |
| EXPECT_THAT( |
| data.annotations(), |
| testing::IsSupersetOf({ |
| MatchesAnnotation(kAnnotationHardwareProductSKU, |
| kProductInfoValues.at(kAnnotationHardwareProductSKU)), |
| MatchesAnnotation(kAnnotationHardwareProductLanguage, |
| kProductInfoValues.at(kAnnotationHardwareProductLanguage)), |
| MatchesAnnotation(kAnnotationHardwareProductRegulatoryDomain, |
| kProductInfoValues.at(kAnnotationHardwareProductRegulatoryDomain)), |
| MatchesAnnotation(kAnnotationHardwareProductLocaleList, |
| kProductInfoValues.at(kAnnotationHardwareProductLocaleList)), |
| MatchesAnnotation(kAnnotationHardwareProductName, |
| kProductInfoValues.at(kAnnotationHardwareProductName)), |
| MatchesAnnotation(kAnnotationHardwareProductModel, |
| kProductInfoValues.at(kAnnotationHardwareProductModel)), |
| MatchesAnnotation(kAnnotationHardwareProductManufacturer, |
| kProductInfoValues.at(kAnnotationHardwareProductManufacturer)), |
| })); |
| } |
| |
| TEST_F(DataProviderTest, GetData_Time) { |
| fit::result<Data, zx_status_t> result = GetData(); |
| |
| ASSERT_TRUE(result.is_ok()); |
| |
| const Data& data = result.value(); |
| ASSERT_TRUE(data.has_annotations()); |
| EXPECT_THAT(data.annotations(), testing::IsSupersetOf({ |
| MatchesKey(kAnnotationDeviceUptime), |
| MatchesKey(kAnnotationDeviceUTCTime), |
| })); |
| } |
| |
| TEST_F(DataProviderTest, GetData_EmptyAnnotationAllowlist) { |
| SetUpDataProvider(Config{/*annotation_allowlist=*/{}, kDefaultAttachments}); |
| |
| fit::result<Data, zx_status_t> result = GetData(); |
| ASSERT_TRUE(result.is_ok()); |
| |
| const Data& data = result.value(); |
| EXPECT_FALSE(data.has_annotations()); |
| } |
| |
| TEST_F(DataProviderTest, GetData_EmptyAttachmentAllowlist) { |
| SetUpDataProvider(Config{kDefaultAnnotations, /*attachment_allowlist=*/{}}); |
| |
| fit::result<Data, zx_status_t> result = GetData(); |
| ASSERT_TRUE(result.is_ok()); |
| |
| const Data& data = result.value(); |
| std::vector<Attachment> unpacked_attachments; |
| UnpackAttachmentBundle(data, &unpacked_attachments); |
| EXPECT_THAT(unpacked_attachments, testing::Contains(MatchesKey(kAttachmentAnnotations))); |
| } |
| |
| TEST_F(DataProviderTest, GetData_EmptyAllowlists) { |
| SetUpDataProvider(Config{/*annotation_allowlist=*/{}, /*attachment_allowlist=*/{}}); |
| |
| fit::result<Data, zx_status_t> result = GetData(); |
| ASSERT_TRUE(result.is_ok()); |
| |
| const Data& data = result.value(); |
| EXPECT_FALSE(data.has_annotations()); |
| EXPECT_FALSE(data.has_attachment_bundle()); |
| } |
| |
| TEST_F(DataProviderTest, GetData_UnknownAllowlistedAnnotation) { |
| SetUpDataProvider(Config{/*annotation_allowlist=*/{"unknown.annotation"}, kDefaultAttachments}); |
| |
| fit::result<Data, zx_status_t> result = GetData(); |
| ASSERT_TRUE(result.is_ok()); |
| |
| const Data& data = result.value(); |
| EXPECT_FALSE(data.has_annotations()); |
| } |
| |
| TEST_F(DataProviderTest, GetData_UnknownAllowlistedAttachment) { |
| SetUpDataProvider(Config{kDefaultAnnotations, |
| /*attachment_allowlist=*/{"unknown.attachment"}}); |
| |
| fit::result<Data, zx_status_t> result = GetData(); |
| ASSERT_TRUE(result.is_ok()); |
| |
| const Data& data = result.value(); |
| std::vector<Attachment> unpacked_attachments; |
| UnpackAttachmentBundle(data, &unpacked_attachments); |
| EXPECT_THAT(unpacked_attachments, testing::Contains(MatchesKey(kAttachmentAnnotations))); |
| } |
| |
| TEST_F(DataProviderTest, Check_IdleTimeout) { |
| // This test checks that requests to the data provider properly delay the idle timeout function |
| // that data provider executes and that said function runs after data provider is idle for a |
| // sufficient period of time. |
| // |
| // We setup the system such that requests for both data and screentshots hang, |
| // relying on their respective timeouts to ensure that an error is returned. Additionally, we |
| // set the idle timeout of the data provider to be half as long as the time it takes for a |
| // request to return in order to determine that neither is interruped by the idle timeout while |
| // completing. |
| // |
| // We test scenarios in which a single request is made, sequential requests are made, and |
| // concurrent requests are made, in that order. |
| |
| // Track if requests have completed. |
| bool got_data = false; |
| bool got_screenshot = false; |
| |
| const zx::duration kGetScreenshotTimeout = zx::sec(10); |
| const zx::duration kGetDataTimeout = zx::sec(30); |
| |
| ASSERT_GE(kGetScreenshotTimeout, kDataProviderIdleTimeout); |
| ASSERT_GE(kGetDataTimeout, kDataProviderIdleTimeout); |
| |
| SetUpDataProviderOnlyRequestingChannel(kDataProviderIdleTimeout); |
| |
| SetUpScenic(std::make_unique<StubScenicNeverReturns>()); |
| SetUpChannelProvider(std::make_unique<StubChannelProviderNeverReturns>()); |
| SetUpBoardProvider(std::make_unique<StubBoard>(CreateBoardInfo())); |
| SetUpProductProvider(std::make_unique<StubProduct>(CreateProductInfo())); |
| |
| // In the following tests we list the current time of a stopwatch that starts at 0 seconds and |
| // the point in time at which the idle timeout function is expected to run. In the circumstance |
| // the idle timeout function is blocked from running we denote the timeout as X. |
| |
| // Make a single request for a screenshot to check that the idle timeout happens after the |
| // screenshot has been returned. |
| |
| // TIME = 0; TIMEOUT @ X (unset) |
| data_provider_->GetScreenshot( |
| ImageEncoding::PNG, |
| [&got_screenshot](std::unique_ptr<Screenshot> _) { got_screenshot = true; }); |
| RunLoopFor(kGetScreenshotTimeout); |
| |
| // TIME = 10; TIMEOUT @ 15 (10 + 5, current time + kDataProviderIdleTimeout) |
| ASSERT_TRUE(got_screenshot); |
| ASSERT_FALSE(data_provider_timed_out_); |
| |
| RunLoopFor(kDataProviderIdleTimeout); |
| |
| // TIME = 15; TIMEOUT @ 15 (unchanged) |
| ASSERT_TRUE(data_provider_timed_out_); |
| |
| // Make a single request for data to check that the idle timeout happens after the data has been |
| // returned. |
| |
| // TIME = 15; TIMEOUT @ X (reset) |
| data_provider_timed_out_ = false; |
| data_provider_->GetData([&got_data](fit::result<Data, zx_status_t> _) { got_data = true; }); |
| RunLoopFor(kGetDataTimeout); |
| |
| // TIME = 25; TIMEOUT @ 30 (25 + 5, current time + kDataProviderIdleTimeout) |
| ASSERT_TRUE(got_data); |
| ASSERT_FALSE(data_provider_timed_out_); |
| |
| RunLoopFor(kDataProviderIdleTimeout); |
| |
| // TIME = 30; TIMEOUT @ 30 (unchanged) |
| ASSERT_TRUE(data_provider_timed_out_); |
| |
| got_screenshot = false; |
| got_data = false; |
| data_provider_timed_out_ = false; |
| |
| // Check that sequential requests for a screenshot and data properly block the idle timeout |
| // function and that it executes when expected. |
| |
| // TIME = 30; TIMEOUT @ X (reset) |
| data_provider_->GetScreenshot( |
| ImageEncoding::PNG, |
| [&got_screenshot](std::unique_ptr<Screenshot> _) { got_screenshot = true; }); |
| RunLoopFor(kGetScreenshotTimeout); |
| |
| // TIME = 40; TIMEOUT @ 45 (40 + 5, current time + kDataProviderIdleTimeout) |
| ASSERT_TRUE(got_screenshot); |
| ASSERT_FALSE(data_provider_timed_out_); |
| |
| data_provider_->GetData([&got_data](fit::result<Data, zx_status_t> _) { got_data = true; }); |
| RunLoopFor(kGetDataTimeout); |
| |
| // TIME = 50; TIMEOUT @ 55 (50 + 5, current time + kDataProviderIdleTimeout) |
| |
| ASSERT_TRUE(got_data); |
| ASSERT_FALSE(data_provider_timed_out_); |
| |
| RunLoopFor(kDataProviderIdleTimeout); |
| |
| // TIME = 55; TIMEOUT @ 55 (unchanged) |
| ASSERT_TRUE(data_provider_timed_out_); |
| |
| got_screenshot = false; |
| got_data = false; |
| data_provider_timed_out_ = false; |
| |
| // Check that concurrent requests for a screenshot and data properly block the idle timeout |
| // function and that it executes when expected. |
| |
| // TIME = 55; TIMEOUT @ X (reset) |
| data_provider_->GetScreenshot( |
| ImageEncoding::PNG, |
| [&got_screenshot](std::unique_ptr<Screenshot> _) { got_screenshot = true; }); |
| RunLoopFor(kDataProviderIdleTimeout); |
| |
| // TIME = 60; TIMEOUT @ X (reset) |
| data_provider_->GetData([&got_data](fit::result<Data, zx_status_t> _) { got_data = true; }); |
| RunLoopFor(kDataProviderIdleTimeout); |
| |
| // TIME = 65; TIMEOUT @ X (reset) |
| ASSERT_TRUE(got_screenshot); |
| ASSERT_FALSE(got_data); |
| ASSERT_FALSE(data_provider_timed_out_); |
| RunLoopFor(kGetDataTimeout - kDataProviderIdleTimeout); |
| |
| // TIME = 90; TIMEOUT @ 95 (90 + 5, current time + kDataProviderIdleTimeout) |
| |
| ASSERT_TRUE(got_data); |
| ASSERT_FALSE(data_provider_timed_out_); |
| RunLoopFor(kDataProviderIdleTimeout); |
| |
| // TIME = 95; TIMEOUT @ 95 (unchanged) |
| EXPECT_TRUE(data_provider_timed_out_); |
| } |
| |
| // Unit-tests the implementation of the fuchsia.feedback.DataProvider FIDL interface when we need |
| // to control the test environment, e.g. to inject additional components. |
| // |
| // This does not test the environment service. It directly instantiates the class, without |
| // connecting through FIDL. |
| class DataProviderTestWithEnv : public sys::testing::TestWithEnvironment { |
| public: |
| void SetUp() override { |
| SetUpDataProvider(Config{kDefaultAnnotations, |
| { |
| kAttachmentBuildSnapshot, |
| kAttachmentLogKernel, |
| kAttachmentInspect, |
| kAttachmentLogSystem, |
| }}); |
| } |
| |
| void TearDown() override { |
| if (inspect_test_app_controller_) { |
| TerminateInspectTestApp(); |
| } |
| } |
| |
| protected: |
| void SetUpDataProvider(const Config& config) { |
| data_provider_.reset(new DataProvider( |
| dispatcher(), service_directory_provider_.service_directory(), config, []() {}, |
| zx::duration::infinite())); |
| } |
| |
| // Injects a test app that exposes some Inspect data in the test environment. |
| // |
| // Useful to guarantee there is a component within the environment that exposes Inspect data as |
| // we are excluding system_objects paths from the Inspect discovery and the test component |
| // itself only has a system_objects Inspect node. |
| void InjectInspectTestApp() { |
| fuchsia::sys::LaunchInfo launch_info; |
| launch_info.url = "fuchsia-pkg://fuchsia.com/feedback_agent_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; }); |
| } |
| |
| fit::result<Data, zx_status_t> GetData() { |
| fit::result<Data, zx_status_t> out_result; |
| bool has_out_result = false; |
| data_provider_->GetData([&out_result, &has_out_result](fit::result<Data, zx_status_t> result) { |
| out_result = std::move(result); |
| has_out_result = true; |
| }); |
| RunLoopUntil([&has_out_result] { return has_out_result; }); |
| return out_result; |
| } |
| |
| void UnpackAttachmentBundle(const Data& data, std::vector<Attachment>* unpacked_attachments) { |
| ASSERT_TRUE(data.has_attachment_bundle()); |
| const auto& attachment_bundle = data.attachment_bundle(); |
| EXPECT_STREQ(attachment_bundle.key.c_str(), kAttachmentBundle); |
| ASSERT_TRUE(Unpack(attachment_bundle.value, unpacked_attachments)); |
| } |
| |
| private: |
| void TerminateInspectTestApp() { |
| 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) { |
| FXL_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: |
| std::unique_ptr<DataProvider> data_provider_; |
| |
| private: |
| sys::testing::ServiceDirectoryProvider service_directory_provider_; |
| std::unique_ptr<sys::testing::EnclosingEnvironment> environment_; |
| fuchsia::sys::ComponentControllerPtr inspect_test_app_controller_; |
| }; |
| |
| TEST_F(DataProviderTestWithEnv, GetData_Inspect) { |
| InjectInspectTestApp(); |
| |
| fit::result<Data, zx_status_t> result = GetData(); |
| |
| ASSERT_TRUE(result.is_ok()); |
| |
| const Data& data = result.value(); |
| |
| // There should be an "inspect.json" attachment present in the attachment bundle. |
| std::vector<Attachment> unpacked_attachments; |
| UnpackAttachmentBundle(data, &unpacked_attachments); |
| bool found_inspect_attachment = false; |
| std::string inspect_json; |
| for (const auto& attachment : unpacked_attachments) { |
| if (attachment.key != kAttachmentInspect) { |
| continue; |
| } |
| found_inspect_attachment = true; |
| |
| ASSERT_TRUE(fsl::StringFromVmo(attachment.value, &inspect_json)); |
| ASSERT_FALSE(inspect_json.empty()); |
| } |
| EXPECT_TRUE(found_inspect_attachment); |
| |
| EXPECT_THAT(unpacked_attachments, |
| testing::Contains(MatchesAttachment(kAttachmentInspect, inspect_json))); |
| } |
| |
| } // namespace |
| } // namespace feedback |