| // Copyright 2018 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/modular/bin/sessionmgr/puppet_master/puppet_master_impl.h" |
| |
| #include <fuchsia/modular/cpp/fidl.h> |
| #include <lib/fidl/cpp/optional.h> |
| #include <lib/syslog/cpp/macros.h> |
| |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| |
| #include "src/lib/fsl/vmo/strings.h" |
| #include "src/modular/bin/sessionmgr/testing/annotations_matchers.h" |
| #include "src/modular/lib/testing/test_story_command_executor.h" |
| #include "src/modular/lib/testing/test_with_session_storage.h" |
| |
| #define TEST_NAME(SUFFIX) \ |
| std::string(::testing::UnitTest::GetInstance()->current_test_info()->name()) + "_" #SUFFIX; |
| |
| namespace modular { |
| namespace { |
| |
| using ::testing::ByRef; |
| using ::testing::ElementsAre; |
| using ::testing::UnorderedElementsAre; |
| |
| fuchsia::modular::Intent CreateEmptyIntent(const std::string& action, |
| const std::string& handler = "") { |
| fuchsia::modular::Intent intent; |
| intent.action = "intent_action"; |
| if (!handler.empty()) { |
| intent.handler = "mod_url"; |
| } |
| return intent; |
| } |
| |
| fuchsia::modular::StoryCommand MakeAddModCommand(const std::string& mod_name) { |
| fuchsia::modular::AddMod add_mod; |
| add_mod.mod_name_transitional = mod_name; |
| auto intent = CreateEmptyIntent("intent_action", "mod_url"); |
| intent.parameters.emplace(); |
| intent.Clone(&add_mod.intent); |
| fuchsia::modular::StoryCommand command; |
| command.set_add_mod(std::move(add_mod)); |
| return command; |
| } |
| |
| fuchsia::modular::StoryCommand MakeRemoveModCommand(std::string mod_name) { |
| fuchsia::modular::StoryCommand command; |
| fuchsia::modular::RemoveMod remove_mod; |
| remove_mod.mod_name_transitional = mod_name; |
| command.set_remove_mod(std::move(remove_mod)); |
| return command; |
| } |
| |
| class PuppetMasterTest : public modular_testing::TestWithSessionStorage { |
| public: |
| void SetUp() override { |
| TestWithSessionStorage::SetUp(); |
| session_storage_ = MakeSessionStorage(); |
| impl_ = std::make_unique<PuppetMasterImpl>(session_storage_.get(), &executor_); |
| impl_->Connect(ptr_.NewRequest()); |
| } |
| |
| fuchsia::modular::StoryPuppetMasterPtr ControlStory(std::string story_name) { |
| story_name_ = story_name; |
| fuchsia::modular::StoryPuppetMasterPtr ptr; |
| ptr_->ControlStory(story_name, ptr.NewRequest()); |
| return ptr; |
| } |
| |
| void EnqueueAddModCommand(const fuchsia::modular::StoryPuppetMasterPtr& story, |
| const std::string& module_name) { |
| FX_CHECK(story_name_.has_value()); |
| |
| // Add the module. |
| std::vector<fuchsia::modular::StoryCommand> commands; |
| commands.push_back(MakeAddModCommand(module_name)); |
| story->Enqueue(std::move(commands)); |
| |
| // Instruct our test executor to return an OK status, and since we're going to |
| // AddMod, give the executor a StoryStorage. |
| executor_.SetExecuteReturnResult(fuchsia::modular::ExecuteStatus::OK, std::nullopt); |
| executor_.SetStoryStorage(GetStoryStorage(session_storage_.get(), story_name_.value())); |
| } |
| |
| protected: |
| std::optional<std::string> story_name_; |
| modular_testing::TestStoryCommandExecutor executor_; |
| std::unique_ptr<SessionStorage> session_storage_; |
| std::unique_ptr<PuppetMasterImpl> impl_; |
| fuchsia::modular::PuppetMasterPtr ptr_; |
| }; |
| |
| TEST_F(PuppetMasterTest, CommandsAreSentToExecutor) { |
| // This should create a new story in StoryStorage called "foo". |
| auto story = ControlStory("foo"); |
| |
| // Enqueue some commands. Do this twice and show that all the commands show |
| // up as one batch. |
| std::vector<fuchsia::modular::StoryCommand> commands; |
| commands.push_back(MakeRemoveModCommand("one")); |
| story->Enqueue(std::move(commands)); |
| commands.clear(); // restore from "unspecified state" |
| commands.push_back(MakeRemoveModCommand("two")); |
| commands.push_back(MakeRemoveModCommand("three")); |
| story->Enqueue(std::move(commands)); |
| |
| // Commands are not run until Execute() is called. |
| RunLoopUntilIdle(); |
| EXPECT_EQ(0, executor_.execute_count()); |
| |
| fuchsia::modular::ExecuteResult result; |
| bool done{false}; |
| // Instruct our test executor to return an OK status. |
| executor_.SetExecuteReturnResult(fuchsia::modular::ExecuteStatus::OK, std::nullopt); |
| story->Execute([&](fuchsia::modular::ExecuteResult r) { |
| result = std::move(r); |
| done = true; |
| }); |
| RunLoopUntil([&]() { return done; }); |
| EXPECT_EQ(1, executor_.execute_count()); |
| EXPECT_EQ(fuchsia::modular::ExecuteStatus::OK, result.status); |
| |
| EXPECT_EQ("foo", executor_.last_story_id()); |
| ASSERT_EQ(3u, executor_.last_commands().size()); |
| EXPECT_EQ("one", executor_.last_commands().at(0).remove_mod().mod_name_transitional); |
| EXPECT_EQ("two", executor_.last_commands().at(1).remove_mod().mod_name_transitional); |
| EXPECT_EQ("three", executor_.last_commands().at(2).remove_mod().mod_name_transitional); |
| } |
| |
| TEST_F(PuppetMasterTest, CommandsAreSentToExecutor_IfWeCloseStoryChannel) { |
| // We're going to call Execute(), and then immediately drop the |
| // StoryPuppetMaster connection. We won't get a callback, but we still |
| // expect that the commands are executed. |
| auto story = ControlStory("foo"); |
| |
| // Enqueue some commands. Do this twice and show that all the commands show |
| // up as one batch. |
| std::vector<fuchsia::modular::StoryCommand> commands; |
| commands.push_back(MakeRemoveModCommand("one")); |
| story->Enqueue(std::move(commands)); |
| |
| fuchsia::modular::ExecuteResult result; |
| bool callback_called{false}; |
| // Instruct our test executor to return an OK status. |
| executor_.SetExecuteReturnResult(fuchsia::modular::ExecuteStatus::OK, std::nullopt); |
| story->Execute([&](fuchsia::modular::ExecuteResult r) { callback_called = true; }); |
| story.Unbind(); |
| RunLoopUntil([&]() { return executor_.execute_count() > 0; }); |
| EXPECT_FALSE(callback_called); |
| EXPECT_EQ(1, executor_.execute_count()); |
| } |
| |
| TEST_F(PuppetMasterTest, MultipleExecuteCalls) { |
| // Create a new story, and then execute some new commands on the same |
| // connection. We should see that the StoryCommandExecutor receives the story |
| // id that it reported after successful creation of the story on the last |
| // execution. |
| auto story = ControlStory("foo"); |
| |
| std::vector<fuchsia::modular::StoryCommand> commands; |
| commands.push_back(MakeRemoveModCommand("one")); |
| executor_.SetExecuteReturnResult(fuchsia::modular::ExecuteStatus::OK, std::nullopt); |
| bool done{false}; |
| story->Execute([&](fuchsia::modular::ExecuteResult r) { done = true; }); |
| RunLoopUntil([&]() { return done; }); |
| auto story_id = executor_.last_story_id(); |
| |
| // Execute more commands. |
| commands.push_back(MakeRemoveModCommand("three")); |
| story->Enqueue(std::move(commands)); |
| done = false; |
| story->Execute([&](fuchsia::modular::ExecuteResult r) { done = true; }); |
| RunLoopUntil([&]() { return done; }); |
| EXPECT_EQ(story_id, executor_.last_story_id()); |
| } |
| |
| TEST_F(PuppetMasterTest, NewStoriesAreKeptSeparate) { |
| // Creating two new stories at the same time is OK and they are kept |
| // separate. |
| auto story1 = ControlStory("story1"); |
| auto story2 = ControlStory("story2"); |
| |
| std::vector<fuchsia::modular::StoryCommand> commands; |
| commands.push_back(MakeRemoveModCommand("one")); |
| story1->Enqueue(std::move(commands)); |
| // We must run the loop to ensure that our message is dispatched. |
| RunLoopUntilIdle(); |
| |
| commands.push_back(MakeRemoveModCommand("two")); |
| story2->Enqueue(std::move(commands)); |
| RunLoopUntilIdle(); |
| |
| fuchsia::modular::ExecuteResult result; |
| executor_.SetExecuteReturnResult(fuchsia::modular::ExecuteStatus::OK, std::nullopt); |
| bool done{false}; |
| story1->Execute([&](fuchsia::modular::ExecuteResult r) { |
| result = std::move(r); |
| done = true; |
| }); |
| RunLoopUntil([&]() { return done; }); |
| EXPECT_EQ(1, executor_.execute_count()); |
| auto story1_id = executor_.last_story_id(); |
| ASSERT_EQ(1u, executor_.last_commands().size()); |
| EXPECT_EQ("one", executor_.last_commands().at(0).remove_mod().mod_name_transitional); |
| |
| executor_.SetExecuteReturnResult(fuchsia::modular::ExecuteStatus::OK, std::nullopt); |
| done = false; |
| story2->Execute([&](fuchsia::modular::ExecuteResult r) { |
| result = std::move(r); |
| done = true; |
| }); |
| RunLoopUntil([&]() { return done; }); |
| EXPECT_EQ(2, executor_.execute_count()); |
| auto story2_id = executor_.last_story_id(); |
| ASSERT_EQ(1u, executor_.last_commands().size()); |
| EXPECT_EQ("two", executor_.last_commands().at(0).remove_mod().mod_name_transitional); |
| |
| // The two IDs should be different, because we gave the two stories different |
| // names. |
| EXPECT_NE(story1_id, story2_id); |
| } |
| |
| TEST_F(PuppetMasterTest, ControlExistingStory) { |
| // Controlling the same story from two connections is OK. The first call to |
| // Execute() will create the story, and the second will re-use the same story |
| // record. |
| auto story1 = ControlStory("foo"); |
| auto story2 = ControlStory("foo"); |
| |
| std::vector<fuchsia::modular::StoryCommand> commands; |
| commands.push_back(MakeRemoveModCommand("one")); |
| story1->Enqueue(std::move(commands)); |
| // We must run the loop to ensure that our message is dispatched. |
| RunLoopUntilIdle(); |
| |
| commands.push_back(MakeRemoveModCommand("two")); |
| story2->Enqueue(std::move(commands)); |
| RunLoopUntilIdle(); |
| |
| fuchsia::modular::ExecuteResult result; |
| executor_.SetExecuteReturnResult(fuchsia::modular::ExecuteStatus::OK, std::nullopt); |
| bool done{false}; |
| story1->Execute([&](fuchsia::modular::ExecuteResult r) { |
| result = std::move(r); |
| done = true; |
| }); |
| RunLoopUntil([&]() { return done; }); |
| EXPECT_EQ(1, executor_.execute_count()); |
| auto story_id = executor_.last_story_id(); |
| ASSERT_EQ(1u, executor_.last_commands().size()); |
| EXPECT_EQ("one", executor_.last_commands().at(0).remove_mod().mod_name_transitional); |
| |
| executor_.SetExecuteReturnResult(fuchsia::modular::ExecuteStatus::OK, std::nullopt); |
| done = false; |
| story2->Execute([&](fuchsia::modular::ExecuteResult r) { |
| result = std::move(r); |
| done = true; |
| }); |
| RunLoopUntil([&]() { return done; }); |
| EXPECT_EQ(2, executor_.execute_count()); |
| EXPECT_EQ(story_id, executor_.last_story_id()); |
| ASSERT_EQ(1u, executor_.last_commands().size()); |
| EXPECT_EQ("two", executor_.last_commands().at(0).remove_mod().mod_name_transitional); |
| } |
| |
| TEST_F(PuppetMasterTest, DeleteStory) { |
| // Create a story. |
| auto story_id = session_storage_->CreateStory("foo", /*annotations=*/{}); |
| |
| // Delete it |
| bool done{}; |
| ptr_->DeleteStory("foo", [&] { done = true; }); |
| RunLoopUntil([&] { return done; }); |
| |
| EXPECT_EQ(session_storage_->GetStoryData(story_id), nullptr); |
| } |
| |
| TEST_F(PuppetMasterTest, DeleteStoryWithQueuedCommands) { |
| const char* kStoryName = "DeleteWithQueuedCommandsStory"; |
| const char* kModuleName = "DeleteWithQueuedCommandsModule"; |
| |
| // Call PuppetMaster directly to create & control a new Story. |
| fuchsia::modular::StoryPuppetMasterPtr story_puppet_master; |
| impl_->ControlStory(kStoryName, story_puppet_master.NewRequest()); |
| |
| // Push an AddMod command to the StoryPuppetMaster. |
| bool is_story_puppet_master_closed = false; |
| story_puppet_master.set_error_handler([&](zx_status_t status) { |
| EXPECT_EQ(status, ZX_ERR_PEER_CLOSED); |
| is_story_puppet_master_closed = true; |
| }); |
| std::vector<fuchsia::modular::StoryCommand> commands; |
| commands.push_back(MakeAddModCommand(kModuleName)); |
| story_puppet_master->Enqueue(std::move(commands)); |
| story_puppet_master->Execute([](fuchsia::modular::ExecuteResult) { |
| // Execute() should never be processed |
| ADD_FAILURE(); |
| }); |
| |
| // Call PuppetMaster directly (i.e. without requiring the loop to be spun) |
| // to delete the Story before the commands can be executed. |
| impl_->DeleteStory(kStoryName, []() {}); |
| |
| // Spin the loop and expect that the StoryPuppetMaster be disconnected. |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(is_story_puppet_master_closed); |
| } |
| |
| TEST_F(PuppetMasterTest, GetStories) { |
| // Zero stories to should exist. |
| bool done{}; |
| ptr_->GetStories([&](std::vector<std::string> story_names) { |
| EXPECT_EQ(0u, story_names.size()); |
| done = true; |
| }); |
| RunLoopUntil([&] { return done; }); |
| |
| // Create a story. |
| session_storage_->CreateStory("foo", /*annotations=*/{}); |
| |
| // "foo" should be listed. |
| done = false; |
| ptr_->GetStories([&](std::vector<std::string> story_names) { |
| ASSERT_EQ(1u, story_names.size()); |
| EXPECT_EQ("foo", story_names.at(0)); |
| done = true; |
| }); |
| RunLoopUntil([&] { return done; }); |
| } |
| |
| // Verifies that a call to Annotate create a story. |
| TEST_F(PuppetMasterTest, AnnotateCreatesStory) { |
| const auto story_name = "annotate_creates_story"; |
| |
| auto story = ControlStory(story_name); |
| |
| // Create some annotations. |
| auto annotation_value = fuchsia::modular::AnnotationValue{}; |
| annotation_value.set_text("test_value"); |
| |
| auto annotation = fuchsia::modular::Annotation{ |
| .key = "test_key", .value = fidl::MakeOptional(std::move(annotation_value))}; |
| |
| std::vector<fuchsia::modular::Annotation> annotations; |
| annotations.push_back(std::move(annotation)); |
| |
| // Annotate the story, which should implicitly create it. |
| bool done{false}; |
| story->Annotate(std::move(annotations), |
| [&](fuchsia::modular::StoryPuppetMaster_Annotate_Result result) { |
| EXPECT_FALSE(result.is_err()); |
| done = true; |
| }); |
| RunLoopUntil([&] { return done; }); |
| |
| // GetStories should return the newly-created story. |
| done = false; |
| ptr_->GetStories([&](std::vector<std::string> story_names) { |
| ASSERT_EQ(1u, story_names.size()); |
| EXPECT_EQ(story_name, story_names.at(0)); |
| done = true; |
| }); |
| RunLoopUntil([&] { return done; }); |
| } |
| |
| // Verifies that annotations are saved to StoryData. |
| TEST_F(PuppetMasterTest, AnnotateInStoryData) { |
| const auto story_name = "annotate_in_storydata"; |
| |
| auto story = ControlStory(story_name); |
| |
| // 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{false}; |
| story->Annotate(std::move(annotations), |
| [&](fuchsia::modular::StoryPuppetMaster_Annotate_Result result) { |
| EXPECT_FALSE(result.is_err()); |
| done = true; |
| }); |
| RunLoopUntil([&] { return done; }); |
| |
| // GetStoryData should contain the annotations. |
| auto story_data = session_storage_->GetStoryData(story_name); |
| ASSERT_NE(nullptr, story_data); |
| ASSERT_TRUE(story_data->has_story_info()); |
| EXPECT_TRUE(story_data->story_info().has_annotations()); |
| |
| EXPECT_EQ(3u, story_data->story_info().annotations().size()); |
| |
| EXPECT_THAT(story_data->story_info().annotations(), |
| UnorderedElementsAre(annotations::AnnotationEq(ByRef(text_annotation)), |
| annotations::AnnotationEq(ByRef(bytes_annotation)), |
| annotations::AnnotationEq(ByRef(buffer_annotation)))); |
| } |
| |
| // Verifies that Annotate merges new annotations, preserving existing ones. |
| TEST_F(PuppetMasterTest, AnnotateMerge) { |
| const auto story_name = "annotate_merge"; |
| |
| auto story = ControlStory(story_name); |
| |
| // 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->Annotate(std::move(annotations), |
| [&](fuchsia::modular::StoryPuppetMaster_Annotate_Result result) { |
| EXPECT_FALSE(result.is_err()); |
| done = true; |
| }); |
| RunLoopUntil([&] { return done; }); |
| |
| // GetStoryData should contain the first annotation. |
| auto story_data = session_storage_->GetStoryData(story_name); |
| ASSERT_NE(nullptr, story_data); |
| ASSERT_TRUE(story_data->has_story_info()); |
| EXPECT_TRUE(story_data->story_info().has_annotations()); |
| |
| { |
| const auto annotations = story_data->mutable_story_info()->mutable_annotations(); |
| EXPECT_EQ(1u, annotations->size()); |
| |
| EXPECT_EQ(annotations->at(0).key, first_annotation.key); |
| EXPECT_EQ(annotations->at(0).value->text(), first_annotation_value.text()); |
| } |
| |
| // 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->Annotate(std::move(annotations_2), |
| [&](fuchsia::modular::StoryPuppetMaster_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. |
| story_data = session_storage_->GetStoryData(story_name); |
| ASSERT_NE(nullptr, story_data); |
| ASSERT_TRUE(story_data->has_story_info()); |
| EXPECT_TRUE(story_data->story_info().has_annotations()); |
| EXPECT_EQ(2u, story_data->story_info().annotations().size()); |
| EXPECT_THAT(story_data->story_info().annotations(), |
| UnorderedElementsAre(annotations::AnnotationEq(ByRef(first_annotation)), |
| annotations::AnnotationEq(ByRef(second_annotation)))); |
| } |
| |
| // Verifies that Annotate returns an error when one of the annotations has a buffer value that |
| // exceeds MAX_ANNOTATION_VALUE_BUFFER_LENGTH_BYTES. |
| TEST_F(PuppetMasterTest, AnnotateBufferValueTooBig) { |
| const auto story_name = "annotate_buffer_value_too_big"; |
| |
| auto story = ControlStory(story_name); |
| |
| // 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->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(PuppetMasterTest, AnnotateTooMany) { |
| // 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); |
| |
| const auto story_name = "annotate_too_many"; |
| |
| auto story = ControlStory(story_name); |
| |
| // 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->Annotate( |
| std::move(annotations), [&](fuchsia::modular::StoryPuppetMaster_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->Annotate( |
| std::move(annotations), [&](fuchsia::modular::StoryPuppetMaster_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 WatchAnnotations returns a NOT_FOUND error if the story does not exist. |
| TEST_F(PuppetMasterTest, WatchAnnotationsNotFound) { |
| constexpr auto story_name = "story_watch_annotations_not_found"; |
| |
| auto story = ControlStory(story_name); |
| |
| bool done{false}; |
| story->WatchAnnotations([&](fuchsia::modular::StoryPuppetMaster_WatchAnnotations_Result result) { |
| EXPECT_TRUE(result.is_err()); |
| EXPECT_EQ(fuchsia::modular::AnnotationError::NOT_FOUND, result.err()); |
| done = true; |
| }); |
| |
| RunLoopUntil([&] { return done; }); |
| } |
| |
| // Verifies that WatchAnnotations returns existing annotations on first call. |
| TEST_F(PuppetMasterTest, WatchAnnotationsExisting) { |
| constexpr auto story_name = "story_watch_annotations_existing"; |
| |
| auto story = ControlStory(story_name); |
| |
| // Create a story with some annotations. |
| auto annotation_value = fuchsia::modular::AnnotationValue{}; |
| annotation_value.set_text("test_value"); |
| auto annotation = fuchsia::modular::Annotation{ |
| .key = "test_key", .value = fidl::MakeOptional(std::move(annotation_value))}; |
| std::vector<fuchsia::modular::Annotation> annotations; |
| annotations.push_back(std::move(annotation)); |
| |
| session_storage_->CreateStory(story_name, std::move(annotations)); |
| |
| // Get the annotations. |
| bool done{false}; |
| int annotations_count = 0; |
| story->WatchAnnotations([&](fuchsia::modular::StoryPuppetMaster_WatchAnnotations_Result result) { |
| ASSERT_TRUE(result.is_response()); |
| annotations_count = result.response().annotations.size(); |
| done = true; |
| }); |
| |
| RunLoopUntil([&] { return done; }); |
| EXPECT_EQ(1, annotations_count); |
| } |
| |
| // Verifies that WatchAnnotations on two different StoryPuppetMasters both return existing |
| // annotations on first call. |
| TEST_F(PuppetMasterTest, WatchAnnotationsExistingMultipleClients) { |
| constexpr auto story_name = "story_watch_annotations_existing_multiple_clients"; |
| |
| auto story = ControlStory(story_name); |
| |
| // Create a story with some annotations. |
| auto annotation_value = fuchsia::modular::AnnotationValue{}; |
| annotation_value.set_text("test_value"); |
| auto annotation = fuchsia::modular::Annotation{ |
| .key = "test_key", .value = fidl::MakeOptional(std::move(annotation_value))}; |
| std::vector<fuchsia::modular::Annotation> annotations; |
| annotations.push_back(std::move(annotation)); |
| |
| session_storage_->CreateStory(story_name, std::move(annotations)); |
| |
| // Get the annotations. |
| bool done{false}; |
| int annotations_count = 0; |
| story->WatchAnnotations([&](fuchsia::modular::StoryPuppetMaster_WatchAnnotations_Result result) { |
| ASSERT_TRUE(result.is_response()); |
| annotations_count = result.response().annotations.size(); |
| done = true; |
| }); |
| |
| RunLoopUntil([&] { return done; }); |
| EXPECT_EQ(1, annotations_count); |
| |
| // Get a new StoryPuppetMaster for the same story. |
| auto story_2 = ControlStory(story_name); |
| |
| // Get the annotations from the second StoryPuppetMaster. |
| done = false; |
| annotations_count = 0; |
| story_2->WatchAnnotations( |
| [&](fuchsia::modular::StoryPuppetMaster_WatchAnnotations_Result result) { |
| ASSERT_TRUE(result.is_response()); |
| annotations_count = result.response().annotations.size(); |
| done = true; |
| }); |
| |
| // This should also return the current set of annotations, and not hang for updates. |
| RunLoopUntil([&] { return done; }); |
| EXPECT_EQ(1, annotations_count); |
| } |
| |
| // Verifies that WatchAnnotations returns updated annotations on subsequent calls. |
| TEST_F(PuppetMasterTest, WatchAnnotationsUpdates) { |
| constexpr auto story_name = "story_watch_annotations_updates"; |
| |
| auto story = ControlStory(story_name); |
| |
| // Create a story with one annotation. |
| auto first_annotation_value = fuchsia::modular::AnnotationValue{}; |
| first_annotation_value.set_text("first_test_value"); |
| auto first_annotation = fuchsia::modular::Annotation{ |
| .key = "first_test_key", .value = fidl::MakeOptional(std::move(first_annotation_value))}; |
| std::vector<fuchsia::modular::Annotation> first_annotations; |
| first_annotations.push_back(fidl::Clone(first_annotation)); |
| |
| session_storage_->CreateStory(story_name, std::move(first_annotations)); |
| |
| // Get the annotations. |
| bool first_watch_called{false}; |
| std::vector<fuchsia::modular::Annotation> first_watch_annotations; |
| story->WatchAnnotations([&](fuchsia::modular::StoryPuppetMaster_WatchAnnotations_Result result) { |
| // Ensure this callback is only called once. |
| ASSERT_FALSE(first_watch_called); |
| first_watch_called = true; |
| |
| ASSERT_TRUE(result.is_response()); |
| first_watch_annotations = std::move(result.response().annotations); |
| }); |
| |
| RunLoopUntil([&] { return first_watch_called; }); |
| EXPECT_THAT(first_watch_annotations, |
| ElementsAre(annotations::AnnotationEq(ByRef(first_annotation)))); |
| |
| // Start watching for annotations. |
| bool second_watch_called{false}; |
| std::vector<fuchsia::modular::Annotation> second_watch_annotations; |
| story->WatchAnnotations([&](fuchsia::modular::StoryPuppetMaster_WatchAnnotations_Result result) { |
| // Ensure this callback is only called once. |
| ASSERT_FALSE(second_watch_called); |
| second_watch_called = true; |
| |
| ASSERT_TRUE(result.is_response()); |
| second_watch_annotations = std::move(result.response().annotations); |
| }); |
| |
| // Add another annotation. |
| auto second_annotation_value = fuchsia::modular::AnnotationValue{}; |
| second_annotation_value.set_text("second_test_value"); |
| auto second_annotation = fuchsia::modular::Annotation{ |
| .key = "second_test_key", .value = fidl::MakeOptional(std::move(second_annotation_value))}; |
| std::vector<fuchsia::modular::Annotation> second_annotations; |
| second_annotations.push_back(fidl::Clone(second_annotation)); |
| |
| // Annotate the story. |
| bool done{false}; |
| story->Annotate(std::move(second_annotations), |
| [&](fuchsia::modular::StoryPuppetMaster_Annotate_Result result) { |
| EXPECT_FALSE(result.is_err()); |
| done = true; |
| }); |
| RunLoopUntil([&] { return done; }); |
| |
| // WatchAnnotations should have received the fnew annotations. |
| RunLoopUntil([&] { return second_watch_called; }); |
| EXPECT_THAT(second_watch_annotations, |
| UnorderedElementsAre(annotations::AnnotationEq(ByRef(first_annotation)), |
| annotations::AnnotationEq(ByRef(second_annotation)))); |
| } |
| |
| } // namespace |
| } // namespace modular |