| // Copyright 2016 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/modular/testing/cpp/fidl.h> |
| #include <fuchsia/testing/modular/cpp/fidl.h> |
| #include <lib/fidl/cpp/optional.h> |
| #include <lib/modular/testing/cpp/fake_agent.h> |
| |
| #include <gmock/gmock.h> |
| #include <src/modular/lib/modular_config/modular_config_constants.h> |
| |
| #include "src/lib/fsl/vmo/strings.h" |
| #include "src/modular/bin/sessionmgr/testing/annotations_matchers.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/test_harness_fixture.h" |
| |
| #define TEST_NAME(SUFFIX) \ |
| std::string(::testing::UnitTest::GetInstance()->current_test_info()->name()) + "_" #SUFFIX; |
| |
| using fuchsia::modular::StoryInfo2; |
| using fuchsia::modular::StoryState; |
| using fuchsia::modular::StoryVisibilityState; |
| using fuchsia::modular::ViewIdentifier; |
| using testing::ByRef; |
| using testing::Pointee; |
| using testing::UnorderedElementsAre; |
| |
| namespace { |
| |
| class SessionShellTest : public modular_testing::TestHarnessFixture { |
| protected: |
| SessionShellTest() |
| : fake_session_shell_(modular_testing::FakeSessionShell::CreateWithDefaultOptions()), |
| fake_module_(modular_testing::FakeModule::CreateWithDefaultOptions()) {} |
| // Shared boilerplate for configuring the test harness to intercept the |
| // session shell, setting up the session shell mock object, running the test |
| // harness, and waiting for the session shell to be successfully intercepted. |
| // Note that this method blocks the thread until the session shell has started |
| // up. |
| // |
| // Not done in SetUp() or the constructor to let the test reader know that |
| // this is happening. Also, certain tests may want to change this flow. |
| void RunHarnessAndInterceptSessionShell() { |
| modular_testing::TestHarnessBuilder builder; |
| builder.InterceptSessionShell(fake_session_shell_->BuildInterceptOptions()); |
| builder.InterceptComponent(fake_module_->BuildInterceptOptions()); |
| builder.BuildAndRun(test_harness()); |
| |
| // Wait for our session shell to start. |
| RunLoopUntil([&] { return fake_session_shell_->is_running(); }); |
| } |
| |
| void RunFakeModule(const std::string& story_name) { |
| // Connect to PuppetMaster. |
| fuchsia::modular::testing::ModularService svc; |
| fuchsia::modular::PuppetMasterPtr puppet_master; |
| svc.set_puppet_master(puppet_master.NewRequest()); |
| test_harness()->ConnectToModularService(std::move(svc)); |
| |
| // Add the module to the story. |
| fuchsia::modular::Intent intent; |
| intent.handler = fake_module_->url(); |
| intent.action = "action"; |
| |
| modular_testing::AddModToStory(test_harness(), story_name, "modname", std::move(intent)); |
| |
| // Wait for the story and mod to start. |
| RunLoopUntil([&] { return fake_module_->is_running(); }); |
| } |
| |
| std::unique_ptr<modular_testing::FakeSessionShell> fake_session_shell_; |
| std::unique_ptr<modular_testing::FakeModule> fake_module_; |
| }; |
| |
| class TestComponent : public modular_testing::FakeComponent { |
| public: |
| // |on_created| is called when the component is launched. |on_destroyed| is called when the |
| // component is terminated. |
| TestComponent(fit::function<void()> on_created, fit::function<void()> on_destroyed) |
| : FakeComponent({.url = modular_testing::TestHarnessBuilder::GenerateFakeUrl(), |
| .sandbox_services = {"fuchsia.modular.SessionShellContext"}}), |
| on_created_(std::move(on_created)), |
| on_destroyed_(std::move(on_destroyed)) {} |
| |
| protected: |
| void OnCreate(fuchsia::sys::StartupInfo startup_info) override { on_created_(); } |
| |
| void OnDestroy() override { on_destroyed_(); } |
| |
| fit::function<void()> on_created_; |
| fit::function<void()> on_destroyed_; |
| }; |
| |
| TEST_F(SessionShellTest, RestartShell) { |
| modular_testing::TestHarnessBuilder builder; |
| |
| // Overriding OnDestroy() and OnCreate() to ensure that there isn't a race condition when |
| // verifying that the session_shell restarts properly. |
| bool stopped = false; |
| bool started = false; |
| |
| TestComponent session_shell([&] { started = true; }, [&] { stopped = true; }); |
| builder.InterceptSessionShell(session_shell.BuildInterceptOptions()); |
| builder.BuildAndRun(test_harness()); |
| |
| EXPECT_FALSE(session_shell.is_running()); |
| RunLoopUntil([&] { return session_shell.is_running(); }); |
| |
| started = false; |
| |
| fuchsia::modular::SessionShellContextPtr session_shell_context; |
| session_shell.component_context()->svc()->Connect(session_shell_context.NewRequest()); |
| session_shell_context->Restart(); |
| |
| RunLoopUntil([&] { return stopped; }); |
| |
| // Tests that the session shell is restarted after a call to SessionShellContext::Restart. |
| RunLoopUntil([&] { return started; }); |
| } |
| |
| TEST_F(SessionShellTest, GetStoryInfoNonexistentStory) { |
| RunHarnessAndInterceptSessionShell(); |
| |
| fuchsia::modular::StoryProvider* story_provider = fake_session_shell_->story_provider(); |
| ASSERT_TRUE(story_provider != nullptr); |
| |
| bool tried_get_story_info = false; |
| story_provider->GetStoryInfo2("X", |
| [&tried_get_story_info](fuchsia::modular::StoryInfo2 story_info) { |
| EXPECT_TRUE(story_info.IsEmpty()); |
| tried_get_story_info = true; |
| }); |
| |
| RunLoopUntil([&] { return tried_get_story_info; }); |
| } |
| |
| TEST_F(SessionShellTest, GetStoriesEmpty) { |
| RunHarnessAndInterceptSessionShell(); |
| |
| fuchsia::modular::StoryProvider* story_provider = fake_session_shell_->story_provider(); |
| ASSERT_TRUE(story_provider != nullptr); |
| |
| bool called_get_stories = false; |
| story_provider->GetStories2( |
| nullptr, [&called_get_stories](const std::vector<fuchsia::modular::StoryInfo2>& stories) { |
| EXPECT_THAT(stories, testing::IsEmpty()); |
| called_get_stories = true; |
| }); |
| |
| RunLoopUntil([&] { return called_get_stories; }); |
| } |
| |
| TEST_F(SessionShellTest, StartAndStopStoryWithExtraInfoMod) { |
| static constexpr char kStoryId[] = "my_story"; |
| |
| RunHarnessAndInterceptSessionShell(); |
| |
| fuchsia::modular::StoryProvider* story_provider = fake_session_shell_->story_provider(); |
| ASSERT_TRUE(story_provider != nullptr); |
| |
| // Have the mock session_shell record the sequence of story states it sees, |
| // and confirm that it only sees the correct story id. |
| std::vector<StoryState> sequence_of_story_states; |
| modular_testing::SimpleStoryProviderWatcher watcher; |
| watcher.set_on_change_2([&sequence_of_story_states](StoryInfo2 story_info, StoryState story_state, |
| StoryVisibilityState _) { |
| ASSERT_TRUE(story_info.has_id()); |
| EXPECT_EQ(story_info.id(), kStoryId); |
| sequence_of_story_states.push_back(story_state); |
| }); |
| watcher.Watch(story_provider, /*on_get_stories=*/nullptr); |
| |
| // Start the story. |
| RunFakeModule(kStoryId); |
| |
| // Stop the story. Check that the story went through the correct sequence |
| // of states (see StoryState FIDL file for valid state transitions). Since we |
| // started it, ran it, and stopped it, the sequence is STOPPED -> RUNNING -> |
| // STOPPING -> STOPPED. |
| fuchsia::modular::StoryControllerPtr story_controller; |
| story_provider->GetController(kStoryId, story_controller.NewRequest()); |
| |
| bool stop_called = false; |
| story_controller->Stop([&stop_called] { stop_called = true; }); |
| RunLoopUntil([&] { return stop_called; }); |
| |
| // Run the loop until there are the expected number of state changes; |
| // having called Stop() is not enough to guarantee seeing all updates. |
| RunLoopUntil([&] { return sequence_of_story_states.size() == 4; }); |
| EXPECT_THAT(sequence_of_story_states, |
| testing::ElementsAre(StoryState::STOPPED, StoryState::RUNNING, StoryState::STOPPING, |
| StoryState::STOPPED)); |
| } |
| |
| TEST_F(SessionShellTest, StoryInfoBeforeAndAfterDelete) { |
| static constexpr char kStoryId[] = "my_story"; |
| |
| RunHarnessAndInterceptSessionShell(); |
| |
| // Connect to PuppetMaster. |
| fuchsia::modular::PuppetMasterPtr puppet_master; |
| fuchsia::modular::testing::ModularService svc; |
| svc.set_puppet_master(puppet_master.NewRequest()); |
| test_harness()->ConnectToModularService(std::move(svc)); |
| |
| fuchsia::modular::StoryProvider* story_provider = fake_session_shell_->story_provider(); |
| ASSERT_TRUE(story_provider != nullptr); |
| |
| // Start the story. |
| RunFakeModule(kStoryId); |
| |
| // Verify that the newly created story returns something for |
| // GetStoryInfo(). |
| bool get_story_info_called = false; |
| story_provider->GetStoryInfo2(kStoryId, |
| [&get_story_info_called](fuchsia::modular::StoryInfo2 story_info) { |
| ASSERT_TRUE(story_info.has_id()); |
| EXPECT_EQ(story_info.id(), kStoryId); |
| get_story_info_called = true; |
| }); |
| RunLoopUntil([&] { return get_story_info_called; }); |
| |
| // Delete the story and confirm that the story info is null now. |
| bool delete_called = false; |
| puppet_master->DeleteStory(kStoryId, [&delete_called, story_provider] { |
| story_provider->GetStoryInfo2(kStoryId, [](fuchsia::modular::StoryInfo2 story_info) { |
| EXPECT_TRUE(story_info.IsEmpty()); |
| }); |
| delete_called = true; |
| }); |
| RunLoopUntil([&] { return delete_called; }); |
| } |
| |
| TEST_F(SessionShellTest, DISABLED_AttachesAndDetachesView) { |
| static constexpr char kStoryId[] = "my_story"; |
| |
| RunHarnessAndInterceptSessionShell(); |
| |
| fuchsia::modular::StoryProvider* story_provider = fake_session_shell_->story_provider(); |
| ASSERT_TRUE(story_provider != nullptr); |
| |
| // Have the mock session_shell record the sequence of story states it sees, |
| // and confirm that it only sees the correct story id. |
| std::vector<StoryState> sequence_of_story_states; |
| modular_testing::SimpleStoryProviderWatcher watcher; |
| watcher.set_on_change_2([&sequence_of_story_states](StoryInfo2 story_info, StoryState story_state, |
| StoryVisibilityState _) { |
| EXPECT_TRUE(story_info.has_id()); |
| EXPECT_EQ(story_info.id(), kStoryId); |
| sequence_of_story_states.push_back(story_state); |
| }); |
| watcher.Watch(story_provider, /*on_get_stories=*/nullptr); |
| |
| bool called_attach_view = false; |
| fake_session_shell_->set_on_attach_view( |
| [&called_attach_view](const ViewIdentifier& /*unused*/) { called_attach_view = true; }); |
| |
| bool called_detach_view = false; |
| fake_session_shell_->set_on_detach_view( |
| [&called_detach_view](const ViewIdentifier& /*unused*/) { called_detach_view = true; }); |
| |
| // Start the story and confirm that AttachView() was called. |
| RunFakeModule(kStoryId); |
| |
| RunLoopUntil([&] { return called_attach_view; }); |
| |
| // Stop the story. Confirm that: |
| // a. DetachView() was called. |
| // b. The story went through the correct sequence |
| // of states (see StoryState FIDL file for valid state transitions). Since we |
| // started it, ran it, and stopped it, the sequence is STOPPED -> RUNNING -> |
| // STOPPING -> STOPPED. |
| fuchsia::modular::StoryControllerPtr story_controller; |
| story_provider->GetController(kStoryId, story_controller.NewRequest()); |
| |
| bool stop_called = false; |
| story_controller->Stop([&stop_called] { stop_called = true; }); |
| RunLoopUntil([&] { return stop_called; }); |
| |
| // Run the loop until there are the expected number of state changes; |
| // having called Stop() is not enough to guarantee seeing all updates. |
| RunLoopUntil([&] { return sequence_of_story_states.size() == 4; }); |
| EXPECT_TRUE(called_detach_view); |
| EXPECT_THAT(sequence_of_story_states, |
| testing::ElementsAre(StoryState::STOPPED, StoryState::RUNNING, StoryState::STOPPING, |
| StoryState::STOPPED)); |
| } |
| |
| TEST_F(SessionShellTest, DISABLED_StoryStopDoesntWaitOnDetachView) { |
| static constexpr char kStoryId[] = "my_story"; |
| |
| RunHarnessAndInterceptSessionShell(); |
| |
| fuchsia::modular::StoryProvider* story_provider = fake_session_shell_->story_provider(); |
| ASSERT_TRUE(story_provider != nullptr); |
| |
| // Have the mock session_shell record the sequence of story states it sees, |
| // and confirm that it only sees the correct story id. |
| std::vector<StoryState> sequence_of_story_states; |
| modular_testing::SimpleStoryProviderWatcher watcher; |
| watcher.set_on_change_2([&sequence_of_story_states](StoryInfo2 story_info, StoryState story_state, |
| StoryVisibilityState _) { |
| EXPECT_TRUE(story_info.has_id()); |
| EXPECT_EQ(story_info.id(), kStoryId); |
| sequence_of_story_states.push_back(story_state); |
| }); |
| watcher.Watch(story_provider, /*on_get_stories=*/nullptr); |
| |
| bool called_attach_view = false; |
| fake_session_shell_->set_on_attach_view( |
| [&called_attach_view](const ViewIdentifier& /*unused*/) { called_attach_view = true; }); |
| |
| // Start the story and confirm that AttachView() was called. |
| RunFakeModule(kStoryId); |
| |
| RunLoopUntil([&] { return called_attach_view; }); |
| |
| // Stop the story. Confirm that: |
| // a. The story stopped, even though it didn't see the DetachView() response |
| // (it was artificially delayed for 1hr). |
| // b. The story went through the correct sequence of states (see StoryState |
| // FIDL file for valid state transitions). Since we started it, ran it, and |
| // stopped it, the sequence is STOPPED -> RUNNING -> STOPPING -> STOPPED. |
| fake_session_shell_->set_detach_delay(zx::sec(60 * 60)); |
| |
| fuchsia::modular::StoryControllerPtr story_controller; |
| story_provider->GetController(kStoryId, story_controller.NewRequest()); |
| |
| bool stop_called = false; |
| story_controller->Stop([&stop_called] { stop_called = true; }); |
| RunLoopUntil([&] { return stop_called; }); |
| |
| // Run the loop until there are the expected number of state changes; |
| // having called Stop() is not enough to guarantee seeing all updates. |
| RunLoopUntil([&] { return sequence_of_story_states.size() == 4; }); |
| EXPECT_THAT(sequence_of_story_states, |
| testing::ElementsAre(StoryState::STOPPED, StoryState::RUNNING, StoryState::STOPPING, |
| StoryState::STOPPED)); |
| } |
| |
| TEST_F(SessionShellTest, GetStoryInfo2HasId) { |
| static constexpr char kStoryId[] = "my_story"; |
| |
| RunHarnessAndInterceptSessionShell(); |
| |
| fuchsia::modular::StoryProvider* story_provider = fake_session_shell_->story_provider(); |
| ASSERT_TRUE(story_provider != nullptr); |
| |
| // Start the story. |
| RunFakeModule(kStoryId); |
| |
| // Verify that the newly created story returns something for |
| // GetStoryInfo2(). |
| bool get_story_info_called = false; |
| story_provider->GetStoryInfo2(kStoryId, |
| [&get_story_info_called](fuchsia::modular::StoryInfo2 story_info) { |
| ASSERT_FALSE(story_info.IsEmpty()); |
| EXPECT_TRUE(story_info.has_id()); |
| EXPECT_EQ(story_info.id(), kStoryId); |
| get_story_info_called = true; |
| }); |
| RunLoopUntil([&] { return get_story_info_called; }); |
| } |
| |
| TEST_F(SessionShellTest, GetStories2ReturnsStoryInfo) { |
| static constexpr char kStoryId[] = "my_story"; |
| |
| RunHarnessAndInterceptSessionShell(); |
| |
| fuchsia::modular::StoryProvider* story_provider = fake_session_shell_->story_provider(); |
| ASSERT_TRUE(story_provider != nullptr); |
| |
| // Start the story. |
| RunFakeModule(kStoryId); |
| |
| // Verify that GetStories2 returns the StoryInfo2 for the newly created story |
| bool get_stories_called = false; |
| story_provider->GetStories2( |
| /*watcher=*/nullptr, |
| [&get_stories_called](std::vector<fuchsia::modular::StoryInfo2> story_infos) { |
| ASSERT_FALSE(story_infos.empty()); |
| const auto& story_info = story_infos.at(0); |
| ASSERT_FALSE(story_info.IsEmpty()); |
| EXPECT_TRUE(story_info.has_id()); |
| EXPECT_EQ(story_info.id(), kStoryId); |
| get_stories_called = true; |
| }); |
| RunLoopUntil([&] { return get_stories_called; }); |
| } |
| |
| TEST_F(SessionShellTest, StoryProviderWatcher) { |
| static constexpr auto kStoryId = "my_story"; |
| |
| RunHarnessAndInterceptSessionShell(); |
| |
| // Connect to PuppetMaster. |
| fuchsia::modular::PuppetMasterPtr puppet_master; |
| fuchsia::modular::testing::ModularService svc; |
| svc.set_puppet_master(puppet_master.NewRequest()); |
| test_harness()->ConnectToModularService(std::move(svc)); |
| |
| fuchsia::modular::StoryProvider* story_provider = fake_session_shell_->story_provider(); |
| ASSERT_NE(nullptr, story_provider); |
| |
| // Once the story is created, OnChange2 should be called with a StoryInfo2 that has the story ID. |
| modular_testing::SimpleStoryProviderWatcher watcher; |
| std::vector<StoryInfo2> on_change_calls; |
| watcher.set_on_change_2( |
| [&](StoryInfo2 story_info, StoryState /* unused */, StoryVisibilityState /* unused */) { |
| on_change_calls.push_back(std::move(story_info)); |
| }); |
| std::vector<std::string> on_delete_calls; |
| watcher.set_on_delete([&](const std::string& story_id) { on_delete_calls.push_back(story_id); }); |
| watcher.Watch(story_provider, /*on_get_stories=*/nullptr); |
| |
| // Start the story. |
| RunFakeModule(kStoryId); |
| |
| RunLoopUntil([&] { return !on_change_calls.empty(); }); |
| EXPECT_TRUE(on_change_calls.at(0).has_id()); |
| EXPECT_EQ(kStoryId, on_change_calls.at(0).id()); |
| |
| // Delete the story twice. Expect that we are notified only once. |
| EXPECT_TRUE(on_delete_calls.empty()); |
| int delete_story_count = 0; |
| puppet_master->DeleteStory(kStoryId, [&delete_story_count] { delete_story_count++; }); |
| puppet_master->DeleteStory(kStoryId, [&delete_story_count] { delete_story_count++; }); |
| RunLoopUntil([&] { return delete_story_count == 2; }); |
| EXPECT_EQ(2, delete_story_count); |
| |
| // In order to ensure that both DeleteStory() operations have completed, |
| // perform another operation that is enqueued after them and wait for it |
| // to return. |
| bool get_stories_done = false; |
| story_provider->GetStories2(nullptr, [&](auto /*ignored*/) { get_stories_done = true; }); |
| RunLoopUntil([&] { return get_stories_done; }); |
| |
| RunLoopUntil([&] { return !on_delete_calls.empty(); }); |
| EXPECT_EQ(1u, on_delete_calls.size()); |
| } |
| |
| TEST_F(SessionShellTest, StoryControllerAnnotate) { |
| const auto story_name = TEST_NAME(story); |
| |
| RunHarnessAndInterceptSessionShell(); |
| RunFakeModule(story_name); |
| |
| fuchsia::modular::StoryProvider* story_provider = fake_session_shell_->story_provider(); |
| |
| fuchsia::modular::StoryControllerPtr story_controller; |
| story_provider->GetController(story_name, story_controller.NewRequest()); |
| |
| // Create some annotations, one for each variant of AnnotationValue. |
| auto text_annotation_value = fuchsia::modular::AnnotationValue{}; |
| text_annotation_value.set_text("text_value"); |
| auto text_annotation = fuchsia::modular::Annotation{ |
| .key = "text_key", .value = fidl::MakeOptional(fidl::Clone(text_annotation_value))}; |
| |
| auto bytes_annotation_value = fuchsia::modular::AnnotationValue{}; |
| bytes_annotation_value.set_bytes({0x01, 0x02, 0x03, 0x04}); |
| auto bytes_annotation = fuchsia::modular::Annotation{ |
| .key = "bytes_key", .value = fidl::MakeOptional(fidl::Clone(bytes_annotation_value))}; |
| |
| fuchsia::mem::Buffer buffer{}; |
| std::string buffer_value = "buffer_value"; |
| ASSERT_TRUE(fsl::VmoFromString(buffer_value, &buffer)); |
| auto buffer_annotation_value = fuchsia::modular::AnnotationValue{}; |
| buffer_annotation_value.set_buffer(std::move(buffer)); |
| auto buffer_annotation = fuchsia::modular::Annotation{ |
| .key = "buffer_key", .value = fidl::MakeOptional(fidl::Clone(buffer_annotation_value))}; |
| |
| std::vector<fuchsia::modular::Annotation> annotations; |
| annotations.push_back(fidl::Clone(text_annotation)); |
| annotations.push_back(fidl::Clone(bytes_annotation)); |
| annotations.push_back(fidl::Clone(buffer_annotation)); |
| |
| // Annotate the story. |
| bool done_annotating{false}; |
| story_controller->Annotate(std::move(annotations), |
| [&](fuchsia::modular::StoryController_Annotate_Result result) { |
| EXPECT_FALSE(result.is_err()); |
| done_annotating = true; |
| }); |
| RunLoopUntil([&] { return done_annotating; }); |
| |
| // GetStoryInfo should contain the annotations. |
| fuchsia::modular::StoryInfo2 story_info; |
| bool done_getting_story_info = false; |
| |
| story_provider->GetStoryInfo2(story_name, [&](fuchsia::modular::StoryInfo2 data) { |
| done_getting_story_info = true; |
| story_info = std::move(data); |
| }); |
| RunLoopUntil([&] { return done_getting_story_info; }); |
| |
| EXPECT_FALSE(story_info.IsEmpty()); |
| EXPECT_TRUE(story_info.has_annotations()); |
| |
| EXPECT_EQ(3u, story_info.annotations().size()); |
| |
| EXPECT_THAT( |
| story_info.mutable_annotations(), |
| Pointee(UnorderedElementsAre(modular::annotations::AnnotationEq(ByRef(text_annotation)), |
| modular::annotations::AnnotationEq(ByRef(bytes_annotation)), |
| modular::annotations::AnnotationEq(ByRef(buffer_annotation))))); |
| } |
| |
| // Verifies that Annotate merges new annotations, preserving existing ones. |
| TEST_F(SessionShellTest, StoryControllerAnnotateMerge) { |
| const auto story_name = TEST_NAME(story); |
| |
| RunHarnessAndInterceptSessionShell(); |
| RunFakeModule(story_name); |
| |
| fuchsia::modular::StoryProvider* story_provider = fake_session_shell_->story_provider(); |
| |
| fuchsia::modular::StoryControllerPtr story_controller; |
| story_provider->GetController(story_name, story_controller.NewRequest()); |
| |
| // Create the initial set of annotations. |
| auto first_annotation_value = fuchsia::modular::AnnotationValue{}; |
| first_annotation_value.set_text("first_value"); |
| auto first_annotation = fuchsia::modular::Annotation{ |
| .key = "first_key", .value = fidl::MakeOptional(fidl::Clone(first_annotation_value))}; |
| |
| std::vector<fuchsia::modular::Annotation> annotations; |
| annotations.push_back(fidl::Clone(first_annotation)); |
| |
| // Annotate the story. |
| bool done{false}; |
| story_controller->Annotate(std::move(annotations), |
| [&](fuchsia::modular::StoryController_Annotate_Result result) { |
| EXPECT_FALSE(result.is_err()); |
| done = true; |
| }); |
| RunLoopUntil([&] { return done; }); |
| |
| // GetStoryData should contain the first annotation. |
| done = false; |
| story_provider->GetStoryInfo2(story_name, [&](fuchsia::modular::StoryInfo2 story_info) { |
| EXPECT_FALSE(story_info.IsEmpty()); |
| EXPECT_TRUE(story_info.has_annotations()); |
| |
| EXPECT_EQ(1u, story_info.annotations().size()); |
| |
| EXPECT_EQ(story_info.annotations().at(0).key, first_annotation.key); |
| EXPECT_EQ(story_info.annotations().at(0).value->text(), first_annotation_value.text()); |
| |
| done = true; |
| }); |
| RunLoopUntil([&] { return done; }); |
| |
| // Create another set of annotations that should be merged into the initial one. |
| auto second_annotation_value = fuchsia::modular::AnnotationValue{}; |
| second_annotation_value.set_text("second_value"); |
| auto second_annotation = fuchsia::modular::Annotation{ |
| .key = "second_key", .value = fidl::MakeOptional(fidl::Clone(second_annotation_value))}; |
| |
| std::vector<fuchsia::modular::Annotation> annotations_2; |
| annotations_2.push_back(fidl::Clone(second_annotation)); |
| |
| // Annotate the story with the second set of annotations. |
| done = false; |
| story_controller->Annotate(std::move(annotations_2), |
| [&](fuchsia::modular::StoryController_Annotate_Result result) { |
| EXPECT_FALSE(result.is_err()); |
| done = true; |
| }); |
| RunLoopUntil([&] { return done; }); |
| |
| // GetStoryData should now return annotations from both the first and second set. |
| done = false; |
| story_provider->GetStoryInfo2(story_name, [&](fuchsia::modular::StoryInfo2 story_info) { |
| EXPECT_FALSE(story_info.IsEmpty()); |
| |
| EXPECT_EQ(2u, story_info.annotations().size()); |
| |
| EXPECT_THAT(story_info.mutable_annotations(), |
| Pointee(UnorderedElementsAre( |
| modular::annotations::AnnotationEq(ByRef(first_annotation)), |
| modular::annotations::AnnotationEq(ByRef(second_annotation))))); |
| |
| done = true; |
| }); |
| RunLoopUntil([&] { return done; }); |
| } |
| |
| // Verifies that StoryController.Annotate returns an error when one of the annotations |
| // has a buffer value that exceeds MAX_ANNOTATION_VALUE_BUFFER_LENGTH_BYTES. |
| TEST_F(SessionShellTest, StoryControllerAnnotateBufferValueTooBig) { |
| const auto story_name = TEST_NAME(story); |
| |
| RunHarnessAndInterceptSessionShell(); |
| RunFakeModule(story_name); |
| |
| fuchsia::modular::StoryProvider* story_provider = fake_session_shell_->story_provider(); |
| |
| fuchsia::modular::StoryControllerPtr story_controller; |
| story_provider->GetController(story_name, story_controller.NewRequest()); |
| |
| // Create an annotation with a large buffer value. |
| fuchsia::mem::Buffer buffer{}; |
| std::string buffer_value(fuchsia::modular::MAX_ANNOTATION_VALUE_BUFFER_LENGTH_BYTES + 1, 'x'); |
| ASSERT_TRUE(fsl::VmoFromString(buffer_value, &buffer)); |
| |
| auto annotation_value = fuchsia::modular::AnnotationValue{}; |
| annotation_value.set_buffer(std::move(buffer)); |
| auto annotation = fuchsia::modular::Annotation{ |
| .key = "buffer_key", .value = fidl::MakeOptional(std::move(annotation_value))}; |
| |
| std::vector<fuchsia::modular::Annotation> annotations; |
| annotations.push_back(std::move(annotation)); |
| |
| // Annotate the story. |
| bool done{false}; |
| story_controller->Annotate( |
| std::move(annotations), [&](fuchsia::modular::StoryController_Annotate_Result result) { |
| EXPECT_TRUE(result.is_err()); |
| EXPECT_EQ(fuchsia::modular::AnnotationError::VALUE_TOO_BIG, result.err()); |
| done = true; |
| }); |
| RunLoopUntil([&] { return done; }); |
| } |
| |
| // Verifies that StoryPuppetMaster.Annotate returns an error when one of the annotations |
| // has a buffer value that exceeds MAX_ANNOTATION_VALUE_BUFFER_LENGTH_BYTES. |
| TEST_F(SessionShellTest, StoryPuppetMasterAnnotateBufferValueTooBig) { |
| const auto story_name = TEST_NAME(story); |
| |
| RunHarnessAndInterceptSessionShell(); |
| RunFakeModule(story_name); |
| |
| // Connect to StoryPuppetMaster. |
| fuchsia::modular::testing::ModularService svc; |
| fuchsia::modular::PuppetMasterPtr puppet_master; |
| svc.set_puppet_master(puppet_master.NewRequest()); |
| test_harness()->ConnectToModularService(std::move(svc)); |
| |
| fuchsia::modular::StoryPuppetMasterPtr story_puppet_master; |
| puppet_master->ControlStory(story_name, story_puppet_master.NewRequest()); |
| |
| // Create an annotation with a large buffer value. |
| fuchsia::mem::Buffer buffer{}; |
| std::string buffer_value(fuchsia::modular::MAX_ANNOTATION_VALUE_BUFFER_LENGTH_BYTES + 1, 'x'); |
| ASSERT_TRUE(fsl::VmoFromString(buffer_value, &buffer)); |
| |
| auto annotation_value = fuchsia::modular::AnnotationValue{}; |
| annotation_value.set_buffer(std::move(buffer)); |
| auto annotation = fuchsia::modular::Annotation{ |
| .key = "buffer_key", .value = fidl::MakeOptional(std::move(annotation_value))}; |
| |
| std::vector<fuchsia::modular::Annotation> annotations; |
| annotations.push_back(std::move(annotation)); |
| |
| // Annotate the story. |
| bool done{false}; |
| story_puppet_master->Annotate( |
| std::move(annotations), [&](fuchsia::modular::StoryPuppetMaster_Annotate_Result result) { |
| EXPECT_TRUE(result.is_err()); |
| EXPECT_EQ(fuchsia::modular::AnnotationError::VALUE_TOO_BIG, result.err()); |
| done = true; |
| }); |
| RunLoopUntil([&] { return done; }); |
| } |
| |
| // Verifies that Annotate returns an error when adding new annotations to exceeds |
| // MAX_ANNOTATIONS_PER_STORY. |
| TEST_F(SessionShellTest, StoryControllerAnnotateTooMany) { |
| const auto story_name = TEST_NAME(story); |
| |
| RunHarnessAndInterceptSessionShell(); |
| RunFakeModule(story_name); |
| |
| fuchsia::modular::StoryProvider* story_provider = fake_session_shell_->story_provider(); |
| |
| fuchsia::modular::StoryControllerPtr story_controller; |
| story_provider->GetController(story_name, story_controller.NewRequest()); |
| |
| // A single Annotate call should not accept more annotations than allowed on a single story. |
| ASSERT_GE(fuchsia::modular::MAX_ANNOTATIONS_PER_STORY, |
| fuchsia::modular::MAX_ANNOTATIONS_PER_UPDATE); |
| |
| // Annotate the story repeatedly, in batches of MAX_ANNOTATIONS_PER_UPDATE items, in order |
| // to reach, but not exceed the MAX_ANNOTATIONS_PER_STORY limit. |
| for (unsigned int num_annotate_calls = 0; |
| num_annotate_calls < |
| fuchsia::modular::MAX_ANNOTATIONS_PER_STORY / fuchsia::modular::MAX_ANNOTATIONS_PER_UPDATE; |
| ++num_annotate_calls) { |
| std::vector<fuchsia::modular::Annotation> annotations; |
| |
| // Create MAX_ANNOTATIONS_PER_UPDATE annotations for each call to Annotate. |
| for (unsigned int num_annotations = 0; |
| num_annotations < fuchsia::modular::MAX_ANNOTATIONS_PER_UPDATE; ++num_annotations) { |
| auto annotation_value = fuchsia::modular::AnnotationValue{}; |
| annotation_value.set_text("test_annotation_value"); |
| auto annotation = |
| fuchsia::modular::Annotation{.key = "annotation_" + std::to_string(num_annotate_calls) + |
| "_" + std::to_string(num_annotations), |
| .value = fidl::MakeOptional(std::move(annotation_value))}; |
| annotations.push_back(std::move(annotation)); |
| } |
| |
| // Annotate the story. |
| bool done{false}; |
| story_controller->Annotate( |
| std::move(annotations), [&](fuchsia::modular::StoryController_Annotate_Result result) { |
| EXPECT_FALSE(result.is_err()) |
| << "Annotate call #" << num_annotate_calls << " returned an error when trying to add " |
| << std::to_string(fuchsia::modular::MAX_ANNOTATIONS_PER_UPDATE) |
| << " annotations to the story."; |
| done = true; |
| }); |
| RunLoopUntil([&] { return done; }); |
| } |
| |
| // Create some more annotations for a total of (MAX_ANNOTATIONS_PER_STORY + 1) on the story. |
| std::vector<fuchsia::modular::Annotation> annotations; |
| |
| for (unsigned int num_annotations = 0; |
| num_annotations < (fuchsia::modular::MAX_ANNOTATIONS_PER_STORY % |
| fuchsia::modular::MAX_ANNOTATIONS_PER_UPDATE) + |
| 1; |
| ++num_annotations) { |
| auto annotation_value = fuchsia::modular::AnnotationValue{}; |
| annotation_value.set_text("test_annotation_value"); |
| auto annotation = |
| fuchsia::modular::Annotation{.key = "excess_annotation_" + std::to_string(num_annotations), |
| .value = fidl::MakeOptional(std::move(annotation_value))}; |
| annotations.push_back(std::move(annotation)); |
| } |
| |
| // Annotate the story. |
| bool done{false}; |
| story_controller->Annotate( |
| std::move(annotations), [&](fuchsia::modular::StoryController_Annotate_Result result) { |
| EXPECT_TRUE(result.is_err()); |
| EXPECT_EQ(fuchsia::modular::AnnotationError::TOO_MANY_ANNOTATIONS, result.err()); |
| done = true; |
| }); |
| RunLoopUntil([&] { return done; }); |
| } |
| |
| // Verifies that a call to StoryController.Annotate results in a StoryProviderWatcher.OnChange2 |
| // being called with the updated annotations. |
| TEST_F(SessionShellTest, StoryControllerAnnotateNotifiesWatcher) { |
| const auto story_name = TEST_NAME(story); |
| |
| RunHarnessAndInterceptSessionShell(); |
| RunFakeModule(story_name); |
| |
| fuchsia::modular::StoryProvider* story_provider = fake_session_shell_->story_provider(); |
| |
| fuchsia::modular::StoryControllerPtr story_controller; |
| story_provider->GetController(story_name, story_controller.NewRequest()); |
| |
| // Watch the story for new annotations. |
| auto num_on_change_2_calls = 0; |
| auto num_annotations = 0; |
| modular_testing::SimpleStoryProviderWatcher watcher; |
| watcher.set_on_change_2( |
| [&num_on_change_2_calls, &num_annotations](StoryInfo2 story_info, StoryState /*unused*/, |
| StoryVisibilityState /*unused*/) { |
| num_on_change_2_calls++; |
| num_annotations = story_info.mutable_annotations()->size(); |
| }); |
| watcher.Watch(story_provider, /*on_get_stories=*/nullptr); |
| |
| // Create a set of annotations, containing a single annotation. |
| auto first_annotation_value = fuchsia::modular::AnnotationValue{}; |
| first_annotation_value.set_text("first_value"); |
| auto first_annotation = fuchsia::modular::Annotation{ |
| .key = "first_key", .value = fidl::MakeOptional(fidl::Clone(first_annotation_value))}; |
| |
| std::vector<fuchsia::modular::Annotation> annotations; |
| annotations.push_back(fidl::Clone(first_annotation)); |
| |
| // Annotate the story. |
| bool done{false}; |
| story_controller->Annotate(std::move(annotations), |
| [&](fuchsia::modular::StoryController_Annotate_Result result) { |
| EXPECT_FALSE(result.is_err()); |
| done = true; |
| }); |
| RunLoopUntil([&] { return done; }); |
| |
| RunLoopUntil([&] { return num_on_change_2_calls > 0; }); |
| EXPECT_EQ(1, num_annotations); |
| } |
| |
| // Verifies that a call to StoryPuppetMaster.Annotate results in a StoryProviderWatcher.OnChange2 |
| // being called with the updated annotations. |
| TEST_F(SessionShellTest, StoryPuppetMasterAnnotateNotifiesWatcher) { |
| const auto story_name = TEST_NAME(story); |
| |
| RunHarnessAndInterceptSessionShell(); |
| RunFakeModule(story_name); |
| |
| fuchsia::modular::StoryProvider* story_provider = fake_session_shell_->story_provider(); |
| |
| // Connect to StoryPuppetMaster. |
| fuchsia::modular::testing::ModularService svc; |
| fuchsia::modular::PuppetMasterPtr puppet_master; |
| svc.set_puppet_master(puppet_master.NewRequest()); |
| test_harness()->ConnectToModularService(std::move(svc)); |
| |
| fuchsia::modular::StoryPuppetMasterPtr story_puppet_master; |
| puppet_master->ControlStory(story_name, story_puppet_master.NewRequest()); |
| |
| // Watch the story for new annotations. |
| auto num_on_change_2_calls = 0; |
| auto num_annotations = 0; |
| modular_testing::SimpleStoryProviderWatcher watcher; |
| watcher.set_on_change_2( |
| [&num_on_change_2_calls, &num_annotations](StoryInfo2 story_info, StoryState /*unused*/, |
| StoryVisibilityState /*unused*/) { |
| num_on_change_2_calls++; |
| num_annotations = story_info.mutable_annotations()->size(); |
| }); |
| watcher.Watch(story_provider, /*on_get_stories=*/nullptr); |
| |
| // Create a set of annotations, containing a single annotation. |
| auto first_annotation_value = fuchsia::modular::AnnotationValue{}; |
| first_annotation_value.set_text("first_value"); |
| auto first_annotation = fuchsia::modular::Annotation{ |
| .key = "first_key", .value = fidl::MakeOptional(fidl::Clone(first_annotation_value))}; |
| |
| std::vector<fuchsia::modular::Annotation> annotations; |
| annotations.push_back(fidl::Clone(first_annotation)); |
| |
| // Annotate the story. |
| bool done{false}; |
| story_puppet_master->Annotate(std::move(annotations), |
| [&](fuchsia::modular::StoryPuppetMaster_Annotate_Result result) { |
| EXPECT_FALSE(result.is_err()); |
| done = true; |
| }); |
| RunLoopUntil([&] { return done; }); |
| |
| RunLoopUntil([&] { return num_on_change_2_calls > 0; }); |
| EXPECT_EQ(1, num_annotations); |
| } |
| |
| class ServicePublishingSessionShell : public modular_testing::FakeSessionShell { |
| public: |
| explicit ServicePublishingSessionShell(modular_testing::FakeComponent::Args args) |
| : FakeSessionShell(std::move(args)) {} |
| |
| static std::unique_ptr<ServicePublishingSessionShell> CreateWithDefaultOptions() { |
| return std::make_unique<ServicePublishingSessionShell>(modular_testing::FakeComponent::Args{ |
| .url = modular_testing::TestHarnessBuilder::GenerateFakeUrl(), |
| .sandbox_services = FakeSessionShell::GetDefaultSandboxServices()}); |
| } |
| |
| int fake_service_connect_count() const { return fake_service_connect_count_; } |
| |
| protected: |
| void OnCreate(fuchsia::sys::StartupInfo startup_info) override { |
| FakeSessionShell::OnCreate(std::move(startup_info)); |
| component_context()->outgoing()->AddPublicService( |
| fit::function<void(fidl::InterfaceRequest<fuchsia::testing::modular::TestProtocol>)>( |
| [this](fidl::InterfaceRequest<fuchsia::testing::modular::TestProtocol> request) { |
| ++fake_service_connect_count_; |
| service_channels_.push_back(request.TakeChannel()); |
| })); |
| } |
| |
| std::vector<zx::channel> service_channels_; |
| int fake_service_connect_count_ = 0; |
| }; |
| |
| // Show that the session shell can publish a service that is accessible to agents |
| // via the agent_service_index. |
| TEST_F(SessionShellTest, SessionShellCanPublishServicesToAgents) { |
| auto session_shell = ServicePublishingSessionShell::CreateWithDefaultOptions(); |
| auto agent = modular_testing::FakeAgent::CreateWithDefaultOptions(); |
| |
| fuchsia::modular::testing::TestHarnessSpec spec; |
| std::vector<fuchsia::modular::session::AgentServiceIndexEntry> agent_service_index; |
| fuchsia::modular::session::AgentServiceIndexEntry agent_service; |
| agent_service.set_service_name(fuchsia::testing::modular::TestProtocol::Name_); |
| agent_service.set_agent_url(session_shell->url()); |
| agent_service_index.emplace_back(std::move(agent_service)); |
| |
| spec.mutable_sessionmgr_config()->set_agent_service_index(std::move(agent_service_index)); |
| spec.mutable_sessionmgr_config()->set_session_agents({agent->url()}); |
| |
| modular_testing::TestHarnessBuilder builder(std::move(spec)); |
| builder.InterceptSessionShell(session_shell->BuildInterceptOptions()); |
| auto agent_intercept_options = agent->BuildInterceptOptions(); |
| agent_intercept_options.sandbox_services.push_back( |
| fuchsia::testing::modular::TestProtocol::Name_); |
| builder.InterceptComponent(std::move(agent_intercept_options)); |
| builder.BuildAndRun(test_harness()); |
| |
| RunLoopUntil([&] { return session_shell->is_running() && agent->is_running(); }); |
| |
| auto service_ptr = |
| agent->component_context()->svc()->Connect<fuchsia::testing::modular::TestProtocol>(); |
| service_ptr.set_error_handler( |
| [](zx_status_t status) { FX_PLOGS(FATAL, status) << "channel should not have closed"; }); |
| |
| RunLoopUntil([&] { return session_shell->fake_service_connect_count() > 0; }); |
| EXPECT_EQ(1, session_shell->fake_service_connect_count()); |
| service_ptr.set_error_handler(nullptr); |
| } |
| |
| } // namespace |