| // 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 <fuchsia/testing/modular/cpp/fidl.h> |
| #include <lib/fit/function.h> |
| #include <lib/syslog/cpp/macros.h> |
| |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| |
| #include "src/modular/bin/sessionmgr/annotations.h" |
| #include "src/modular/bin/sessionmgr/testing/annotations_matchers.h" |
| #include "src/modular/lib/modular_test_harness/cpp/fake_element.h" |
| #include "src/modular/lib/modular_test_harness/cpp/fake_module.h" |
| #include "src/modular/lib/modular_test_harness/cpp/fake_session_shell.h" |
| #include "src/modular/lib/modular_test_harness/cpp/fake_story_shell.h" |
| #include "src/modular/lib/modular_test_harness/cpp/test_harness_fixture.h" |
| #include "src/modular/lib/pseudo_dir/pseudo_dir_server.h" |
| |
| namespace { |
| |
| using ::testing::ByRef; |
| using ::testing::ElementsAre; |
| |
| class ElementManagerTest : public modular_testing::TestHarnessFixture { |
| protected: |
| ElementManagerTest() |
| : session_shell_(modular_testing::FakeSessionShell::CreateWithDefaultOptions()), |
| story_shell_(modular_testing::FakeStoryShell::CreateWithDefaultOptions()), |
| element_(modular_testing::FakeElement::CreateWithDefaultOptions()) {} |
| |
| fuchsia::modular::PuppetMasterPtr& puppet_master() { return puppet_master_; } |
| fuchsia::element::ManagerPtr& element_manager() { return element_manager_; } |
| modular_testing::FakeSessionShell* session_shell() { return session_shell_.get(); } |
| modular_testing::FakeStoryShell* story_shell() { return story_shell_.get(); } |
| modular_testing::FakeElement* element() { return element_.get(); } |
| |
| void StartSession() { |
| modular_testing::TestHarnessBuilder builder; |
| builder.InterceptSessionShell(session_shell_->BuildInterceptOptions()); |
| builder.InterceptStoryShell(story_shell_->BuildInterceptOptions()); |
| builder.InterceptComponent(element_->BuildInterceptOptions()); |
| builder.BuildAndRun(test_harness()); |
| |
| fuchsia::modular::testing::ModularService request; |
| request.set_puppet_master(puppet_master_.NewRequest()); |
| request.set_element_manager(element_manager_.NewRequest()); |
| test_harness()->ConnectToModularService(std::move(request)); |
| |
| // Wait for the session shell to start. |
| RunLoopUntil([this] { return session_shell_->is_running(); }); |
| } |
| |
| private: |
| fuchsia::modular::PuppetMasterPtr puppet_master_; |
| fuchsia::element::ManagerPtr element_manager_; |
| std::unique_ptr<modular_testing::FakeSessionShell> session_shell_; |
| std::unique_ptr<modular_testing::FakeStoryShell> story_shell_; |
| std::unique_ptr<modular_testing::FakeElement> element_; |
| }; |
| |
| // Tests that ElementManager.ProposeElement creates the element's component. |
| TEST_F(ElementManagerTest, ProposeCreatesElement) { |
| bool is_element_created{false}; |
| element()->set_on_create( |
| [&is_element_created](fuchsia::sys::StartupInfo /*unused*/) { is_element_created = true; }); |
| |
| StartSession(); |
| |
| // Propose the element. |
| bool is_proposed{false}; |
| element_manager()->ProposeElement(fidl::Clone(element()->spec()), |
| /*controller=*/nullptr, |
| [&](fuchsia::element::Manager_ProposeElement_Result result) { |
| EXPECT_FALSE(result.is_err()); |
| is_proposed = true; |
| }); |
| RunLoopUntil([&]() { return is_proposed; }); |
| |
| RunLoopUntil([&]() { return is_element_created; }); |
| EXPECT_TRUE(is_element_created); |
| } |
| |
| // Tests that ElementManager.ProposeElement starts a story. |
| TEST_F(ElementManagerTest, ProposeStartsStory) { |
| StartSession(); |
| |
| fuchsia::modular::StoryProvider* story_provider = session_shell()->story_provider(); |
| ASSERT_TRUE(story_provider != nullptr); |
| |
| // Proposing the element should create and start a story. |
| bool has_story_started{false}; |
| modular_testing::SimpleStoryProviderWatcher watcher; |
| watcher.set_on_change_2([&has_story_started](fuchsia::modular::StoryInfo2 /*unused*/, |
| fuchsia::modular::StoryState story_state, |
| fuchsia::modular::StoryVisibilityState /*unused*/) { |
| if (story_state == fuchsia::modular::StoryState::RUNNING) { |
| has_story_started = true; |
| } |
| }); |
| |
| bool is_watcher_added{false}; |
| fit::function<void(std::vector<fuchsia::modular::StoryInfo2>)> on_get_stories = |
| [&](const std::vector<fuchsia::modular::StoryInfo2>& story_infos) { |
| ASSERT_TRUE(story_infos.empty()); |
| is_watcher_added = true; |
| }; |
| watcher.Watch(story_provider, &on_get_stories); |
| RunLoopUntil([&]() { return is_watcher_added; }); |
| |
| // Propose the element. |
| bool is_proposed{false}; |
| element_manager()->ProposeElement(fidl::Clone(element()->spec()), |
| /*controller=*/nullptr, |
| [&](fuchsia::element::Manager_ProposeElement_Result result) { |
| EXPECT_FALSE(result.is_err()); |
| is_proposed = true; |
| }); |
| RunLoopUntil([&]() { return is_proposed; }); |
| |
| // The story should have started. |
| RunLoopUntil([&]() { return has_story_started; }); |
| EXPECT_TRUE(has_story_started); |
| } |
| |
| // Tests that closing the element Controller deletes the element story. |
| TEST_F(ElementManagerTest, ClosingElementControllerDeletesStory) { |
| StartSession(); |
| |
| fuchsia::modular::StoryProvider* story_provider = session_shell()->story_provider(); |
| ASSERT_TRUE(story_provider != nullptr); |
| |
| // Proposing the element should create and start a story. |
| bool has_story_started{false}; |
| bool has_story_stopped{false}; |
| modular_testing::SimpleStoryProviderWatcher watcher; |
| watcher.set_on_change_2( |
| [&has_story_started, &has_story_stopped](fuchsia::modular::StoryInfo2 /*unused*/, |
| fuchsia::modular::StoryState story_state, |
| fuchsia::modular::StoryVisibilityState /*unused*/) { |
| if (story_state == fuchsia::modular::StoryState::RUNNING) { |
| has_story_started = true; |
| } else if (has_story_started && story_state == fuchsia::modular::StoryState::STOPPED) { |
| has_story_stopped = true; |
| } |
| }); |
| |
| bool is_watcher_added{false}; |
| fit::function<void(std::vector<fuchsia::modular::StoryInfo2>)> on_get_stories = |
| [&](const std::vector<fuchsia::modular::StoryInfo2>& story_infos) { |
| ASSERT_TRUE(story_infos.empty()); |
| is_watcher_added = true; |
| }; |
| watcher.Watch(story_provider, &on_get_stories); |
| RunLoopUntil([&]() { return is_watcher_added; }); |
| |
| fuchsia::element::ControllerPtr element_controller; |
| |
| // Propose the element. |
| bool is_proposed{false}; |
| element_manager()->ProposeElement(fidl::Clone(element()->spec()), element_controller.NewRequest(), |
| [&](fuchsia::element::Manager_ProposeElement_Result result) { |
| EXPECT_FALSE(result.is_err()); |
| is_proposed = true; |
| }); |
| RunLoopUntil([&]() { return is_proposed; }); |
| |
| // The story should have started. |
| RunLoopUntil([&]() { return has_story_started; }); |
| EXPECT_TRUE(has_story_started); |
| |
| // Close the ElementController. |
| element_controller.Unbind().TakeChannel().reset(); |
| |
| // The story should have stopped. |
| RunLoopUntil([&]() { return has_story_stopped; }); |
| EXPECT_TRUE(has_story_stopped); |
| |
| // The story should have been deleted. |
| bool got_stories{false}; |
| story_provider->GetStories2( |
| /*watcher=*/nullptr, [&](std::vector<fuchsia::modular::StoryInfo2> story_infos) { |
| EXPECT_TRUE(story_infos.empty()); |
| got_stories = true; |
| }); |
| RunLoopUntil([&] { return got_stories; }); |
| } |
| |
| // Tests that ElementManager.ProposeElement adds the element's view as a surface in the story shell. |
| TEST_F(ElementManagerTest, ProposeAddsSurfaceToStoryShell) { |
| StartSession(); |
| |
| // The element module's surface will be added to the story shell. |
| bool is_surface_added{false}; |
| story_shell()->set_on_add_surface( |
| [&is_surface_added](fuchsia::modular::ViewConnection /*unused*/, |
| fuchsia::modular::SurfaceInfo2 /*unused*/) { is_surface_added = true; }); |
| |
| // Propose the element. |
| bool is_proposed{false}; |
| element_manager()->ProposeElement(fidl::Clone(element()->spec()), |
| /*controller=*/nullptr, |
| [&](fuchsia::element::Manager_ProposeElement_Result result) { |
| EXPECT_FALSE(result.is_err()); |
| is_proposed = true; |
| }); |
| RunLoopUntil([&]() { return is_proposed; }); |
| |
| // The story shell should receive the element's view. |
| RunLoopUntil([&]() { return is_surface_added; }); |
| } |
| |
| // Tests that ElementManager.ProposeElement creates a story containing the annotations from |
| // the Spec. |
| TEST_F(ElementManagerTest, ProposeAnnotatesStory) { |
| static constexpr auto kTestAnnotationKey = "test_annotation_key"; |
| static constexpr auto kTestAnnotationValue = "test_annotation_value"; |
| |
| StartSession(); |
| |
| fuchsia::modular::StoryProvider* story_provider = session_shell()->story_provider(); |
| ASSERT_TRUE(story_provider != nullptr); |
| |
| // Create a Spec with an annotation. |
| auto element_annotation = fuchsia::element::Annotation{ |
| .key = fuchsia::element::AnnotationKey{.namespace_ = "global", .value = kTestAnnotationKey}, |
| .value = fuchsia::element::AnnotationValue::WithText(kTestAnnotationValue)}; |
| |
| auto element_spec = fidl::Clone(element()->spec()); |
| element_spec.mutable_annotations()->push_back(std::move(element_annotation)); |
| |
| // Propose the element. |
| bool is_proposed{false}; |
| element_manager()->ProposeElement(std::move(element_spec), /*controller=*/nullptr, |
| [&](fuchsia::element::Manager_ProposeElement_Result result) { |
| EXPECT_FALSE(result.is_err()); |
| is_proposed = true; |
| }); |
| RunLoopUntil([&]() { return is_proposed; }); |
| |
| // The story should have the annotation. |
| bool got_annotations{false}; |
| story_provider->GetStories2( |
| /*watcher=*/nullptr, [&](std::vector<fuchsia::modular::StoryInfo2> story_infos) { |
| ASSERT_EQ(1u, story_infos.size()); |
| auto& story_info = story_infos.at(0); |
| |
| auto modular_annotation = fuchsia::modular::Annotation{ |
| .key = kTestAnnotationKey, |
| .value = std::make_unique<fuchsia::modular::AnnotationValue>( |
| fuchsia::modular::AnnotationValue::WithText(kTestAnnotationValue))}; |
| |
| ASSERT_TRUE(story_info.has_annotations()); |
| EXPECT_THAT(story_info.annotations(), |
| ElementsAre(modular::annotations::AnnotationEq(ByRef(modular_annotation)))); |
| |
| got_annotations = true; |
| }); |
| RunLoopUntil([&] { return got_annotations; }); |
| } |
| |
| // Tests that ElementController.GetAnnotations returns the annotations initially proposed |
| // on the element. |
| TEST_F(ElementManagerTest, ElementControllerGetAnnotations) { |
| static constexpr auto kTestAnnotationKey = "test_annotation_key"; |
| static constexpr auto kTestAnnotationValue = "test_annotation_value"; |
| |
| StartSession(); |
| |
| fuchsia::modular::StoryProvider* story_provider = session_shell()->story_provider(); |
| ASSERT_TRUE(story_provider != nullptr); |
| |
| fuchsia::element::ControllerPtr element_controller; |
| |
| // Create an ElementSpec with an annotation. |
| auto element_annotation = fuchsia::element::Annotation{ |
| .key = modular::annotations::ToElementAnnotationKey(kTestAnnotationKey), |
| .value = fuchsia::element::AnnotationValue::WithText(kTestAnnotationValue)}; |
| |
| auto element_spec = fidl::Clone(element()->spec()); |
| element_spec.mutable_annotations()->push_back(fidl::Clone(element_annotation)); |
| |
| // Propose the element. |
| bool is_proposed{false}; |
| element_manager()->ProposeElement(std::move(element_spec), element_controller.NewRequest(), |
| [&](fuchsia::element::Manager_ProposeElement_Result result) { |
| EXPECT_FALSE(result.is_err()); |
| is_proposed = true; |
| }); |
| RunLoopUntil([&]() { return is_proposed; }); |
| |
| // The story should have the annotation. |
| bool got_annotations{false}; |
| element_controller->GetAnnotations( |
| [&](fuchsia::element::AnnotationController_GetAnnotations_Result result) { |
| ASSERT_FALSE(result.is_err()); |
| |
| EXPECT_THAT(result.response().annotations, |
| ElementsAre(element::annotations::AnnotationEq(ByRef(element_annotation)))); |
| |
| got_annotations = true; |
| }); |
| RunLoopUntil([&] { return got_annotations; }); |
| } |
| |
| // Tests that ElementController.UpdateAnnotations sets annotations on the element story. |
| TEST_F(ElementManagerTest, ElementControllerSetAnnotations) { |
| static constexpr auto kTestAnnotationKey = "test_annotation_key"; |
| static constexpr auto kTestAnnotationValue = "test_annotation_value"; |
| |
| StartSession(); |
| |
| fuchsia::modular::StoryProvider* story_provider = session_shell()->story_provider(); |
| ASSERT_TRUE(story_provider != nullptr); |
| |
| fuchsia::element::ControllerPtr element_controller; |
| |
| // Propose the element. |
| bool is_proposed{false}; |
| element_manager()->ProposeElement(fidl::Clone(element()->spec()), element_controller.NewRequest(), |
| [&](fuchsia::element::Manager_ProposeElement_Result result) { |
| EXPECT_FALSE(result.is_err()); |
| is_proposed = true; |
| }); |
| RunLoopUntil([&]() { return is_proposed; }); |
| |
| // The story should initially have an empty list of annotations. |
| bool got_story_annotations_before{false}; |
| story_provider->GetStories2(/*watcher=*/nullptr, |
| [&](std::vector<fuchsia::modular::StoryInfo2> story_infos) { |
| ASSERT_EQ(1u, story_infos.size()); |
| auto& story_info = story_infos.at(0); |
| |
| ASSERT_TRUE(story_info.has_annotations()); |
| EXPECT_TRUE(story_info.annotations().empty()); |
| |
| got_story_annotations_before = true; |
| }); |
| RunLoopUntil([&] { return got_story_annotations_before; }); |
| |
| // Set the element's annotations. |
| auto element_annotation = fuchsia::element::Annotation{ |
| .key = modular::annotations::ToElementAnnotationKey(kTestAnnotationKey), |
| .value = fuchsia::element::AnnotationValue::WithText(kTestAnnotationValue)}; |
| |
| std::vector<fuchsia::element::Annotation> annotations_to_set; |
| annotations_to_set.push_back(fidl::Clone(element_annotation)); |
| |
| bool did_update_annotations{false}; |
| element_controller->UpdateAnnotations( |
| std::move(annotations_to_set), |
| /*annotations_to_delete=*/{}, |
| [&](fuchsia::element::AnnotationController_UpdateAnnotations_Result result) { |
| EXPECT_FALSE(result.is_err()); |
| did_update_annotations = true; |
| }); |
| RunLoopUntil([&] { return did_update_annotations; }); |
| |
| // The story should have the new annotation. |
| bool got_story_annotations_after{false}; |
| story_provider->GetStories2( |
| /*watcher=*/nullptr, [&](std::vector<fuchsia::modular::StoryInfo2> story_infos) { |
| ASSERT_EQ(1u, story_infos.size()); |
| auto& story_info = story_infos.at(0); |
| |
| auto modular_annotation = fuchsia::modular::Annotation{ |
| .key = kTestAnnotationKey, |
| .value = std::make_unique<fuchsia::modular::AnnotationValue>( |
| fuchsia::modular::AnnotationValue::WithText(kTestAnnotationValue))}; |
| |
| ASSERT_TRUE(story_info.has_annotations()); |
| EXPECT_THAT(story_info.annotations(), |
| ElementsAre(modular::annotations::AnnotationEq(ByRef(modular_annotation)))); |
| |
| got_story_annotations_after = true; |
| }); |
| RunLoopUntil([&] { return got_story_annotations_after; }); |
| |
| // The element should have the annotation. |
| bool got_element_annotations{false}; |
| element_controller->GetAnnotations( |
| [&](fuchsia::element::AnnotationController_GetAnnotations_Result result) { |
| ASSERT_FALSE(result.is_err()); |
| |
| EXPECT_THAT(result.response().annotations, |
| ElementsAre(element::annotations::AnnotationEq(ByRef(element_annotation)))); |
| |
| got_element_annotations = true; |
| }); |
| RunLoopUntil([&] { return got_element_annotations; }); |
| } |
| |
| // Tests that ElementManager.ProposeElement with an ElementSpec that contains |
| // |additional_services| offers them to the launched element. |
| TEST_F(ElementManagerTest, ProposeOffersServices) { |
| StartSession(); |
| |
| fuchsia::modular::StoryProvider* story_provider = session_shell()->story_provider(); |
| ASSERT_TRUE(story_provider != nullptr); |
| |
| // Build a directory to serve the ServiceList passed to the element. |
| int connect_count = 0; |
| auto dir = std::make_unique<vfs::PseudoDir>(); |
| dir->AddEntry( |
| fuchsia::testing::modular::TestProtocol::Name_, |
| std::make_unique<vfs::Service>( |
| [&](zx::channel /*unused*/, async_dispatcher_t* /*unused*/) { ++connect_count; })); |
| auto dir_server = std::make_unique<modular::PseudoDirServer>(std::move(dir)); |
| |
| // Construct a ServiceList with the above dir server. |
| fuchsia::sys::ServiceList service_list; |
| service_list.names.push_back(fuchsia::testing::modular::TestProtocol::Name_); |
| service_list.host_directory = dir_server->Serve().Unbind().TakeChannel(); |
| |
| // Create an ElementSpec with the ServiceList in |additional_services|. |
| auto element_spec = fidl::Clone(element()->spec()); |
| element_spec.set_additional_services(std::move(service_list)); |
| |
| // Propose the element. |
| bool is_proposed{false}; |
| element_manager()->ProposeElement(std::move(element_spec), /*controller=*/nullptr, |
| [&](fuchsia::element::Manager_ProposeElement_Result result) { |
| EXPECT_FALSE(result.is_err()); |
| is_proposed = true; |
| }); |
| RunLoopUntil([&]() { return is_proposed; }); |
| |
| // The element must be running to use its ComponentContext. |
| RunLoopUntil([&]() { return element()->is_running(); }); |
| |
| // Connect to the provided service from the element. |
| auto test_ptr = |
| element()->component_context()->svc()->Connect<fuchsia::testing::modular::TestProtocol>(); |
| RunLoopUntil([&] { return connect_count > 0; }); |
| EXPECT_EQ(1, connect_count); |
| } |
| |
| } // namespace |