| // 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/cpp/fidl.h> |
| #include <lib/async/cpp/task.h> |
| #include <lib/context/cpp/context_helper.h> |
| #include <lib/fidl/cpp/binding.h> |
| #include <lib/fidl/cpp/optional.h> |
| #include <src/lib/fxl/logging.h> |
| #include <lib/svc/cpp/services.h> |
| |
| #include "peridot/lib/rapidjson/rapidjson.h" |
| #include "peridot/lib/testing/story_provider_mock.h" |
| #include "peridot/lib/testing/wait_until_idle.h" |
| #include "peridot/tests/maxwell_integration/context_engine_test_base.h" |
| #include "peridot/tests/maxwell_integration/test_suggestion_listener.h" |
| #include "rapidjson/document.h" |
| #include "rapidjson/pointer.h" |
| |
| namespace maxwell { |
| namespace { |
| |
| // context agent that publishes an int n |
| class NWriter { |
| public: |
| NWriter(fuchsia::modular::ContextEngine* context_engine) { |
| fuchsia::modular::ComponentScope scope; |
| scope.set_global_scope(fuchsia::modular::GlobalScope()); |
| context_engine->GetWriter(std::move(scope), pub_.NewRequest()); |
| } |
| |
| void Publish(int n) { pub_->WriteEntityTopic("n", std::to_string(n)); } |
| |
| private: |
| fuchsia::modular::ContextWriterPtr pub_; |
| }; |
| |
| fuchsia::modular::Proposal CreateProposal( |
| const std::string& id, const std::string& headline, |
| std::vector<fuchsia::modular::StoryCommand> commands, |
| fuchsia::modular::AnnoyanceType annoyance) { |
| fuchsia::modular::Proposal p; |
| p.id = id; |
| p.on_selected = std::move(commands); |
| p.affinity.resize(0); |
| fuchsia::modular::SuggestionDisplay d; |
| |
| d.headline = headline; |
| d.color = 0x00aa00aa; // argb purple |
| d.annoyance = annoyance; |
| |
| p.display = std::move(d); |
| return p; |
| } |
| |
| class Proposinator { |
| public: |
| Proposinator(fuchsia::modular::SuggestionEngine* suggestion_engine, |
| fidl::StringPtr url = "Proposinator") { |
| suggestion_engine->RegisterProposalPublisher("Proposinator", |
| out_.NewRequest()); |
| } |
| |
| virtual ~Proposinator() = default; |
| |
| void Propose(const std::string& id, |
| std::vector<fuchsia::modular::StoryCommand> commands = {}) { |
| Propose(id, id, fuchsia::modular::AnnoyanceType::NONE, std::move(commands)); |
| } |
| |
| void Propose(const std::string& id, const std::string& headline, |
| fuchsia::modular::AnnoyanceType annoyance = |
| fuchsia::modular::AnnoyanceType::NONE, |
| std::vector<fuchsia::modular::StoryCommand> commands = {}) { |
| Propose(CreateProposal(id, headline, std::move(commands), annoyance)); |
| } |
| |
| void Propose(fuchsia::modular::Proposal proposal) { |
| out_->Propose(std::move(proposal)); |
| } |
| |
| void Remove(const std::string& id) { out_->Remove(id); } |
| |
| void KillPublisher() { out_.Unbind(); } |
| |
| protected: |
| fuchsia::modular::ProposalPublisherPtr out_; |
| }; |
| |
| class AskProposinator : public Proposinator, |
| public fuchsia::modular::QueryHandler { |
| public: |
| AskProposinator(fuchsia::modular::SuggestionEngine* suggestion_engine, |
| async::Loop* loop, fidl::StringPtr url = "AskProposinator") |
| : Proposinator(suggestion_engine, url), loop_(loop), ask_binding_(this) { |
| fidl::InterfaceHandle<fuchsia::modular::QueryHandler> query_handle; |
| ask_binding_.Bind(query_handle.NewRequest()); |
| suggestion_engine->RegisterQueryHandler(url, std::move(query_handle)); |
| } |
| |
| void OnQuery(fuchsia::modular::UserInput query, |
| OnQueryCallback callback) override { |
| query_ = fidl::MakeOptional(query); |
| query_callback_ = std::move(callback); |
| query_proposals_.resize(0); |
| |
| if (waiting_for_query_) { |
| waiting_for_query_ = false; |
| async::PostTask(loop_->dispatcher(), [this] { loop_->Quit(); }); |
| } |
| } |
| |
| void WaitForQuery() { |
| waiting_for_query_ = true; |
| loop_->Run(); |
| loop_->ResetQuit(); |
| } |
| |
| void Commit() { |
| fuchsia::modular::QueryResponse response; |
| response.proposals = std::move(query_proposals_); |
| query_callback_(std::move(response)); |
| } |
| |
| fidl::StringPtr query() const { return query_ ? query_->text : nullptr; } |
| |
| void ProposeForAsk(const std::string& id) { |
| std::vector<fuchsia::modular::StoryCommand> commands; |
| ProposeForAsk(id, id, fuchsia::modular::AnnoyanceType::NONE, |
| std::move(commands)); |
| } |
| |
| void ProposeForAsk( |
| const std::string& id, const std::string& headline, |
| fuchsia::modular::AnnoyanceType annoyance = |
| fuchsia::modular::AnnoyanceType::NONE, |
| std::vector<fuchsia::modular::StoryCommand> commands = {}) { |
| query_proposals_.push_back( |
| CreateProposal(id, headline, std::move(commands), annoyance)); |
| } |
| |
| private: |
| async::Loop* const loop_; |
| fidl::Binding<fuchsia::modular::QueryHandler> ask_binding_; |
| fuchsia::modular::UserInputPtr query_; |
| std::vector<fuchsia::modular::Proposal> query_proposals_; |
| OnQueryCallback query_callback_; |
| bool waiting_for_query_ = false; |
| }; |
| |
| // maintains the number of proposals specified by the context field "n" |
| class NProposals : public Proposinator, |
| public fuchsia::modular::ContextListener { |
| public: |
| NProposals(fuchsia::modular::ContextEngine* context_engine, |
| fuchsia::modular::SuggestionEngine* suggestion_engine) |
| : Proposinator(suggestion_engine, "NProposals"), listener_binding_(this) { |
| fuchsia::modular::ComponentScope scope; |
| scope.set_global_scope(fuchsia::modular::GlobalScope()); |
| context_engine->GetReader(std::move(scope), reader_.NewRequest()); |
| |
| fuchsia::modular::ContextSelector selector; |
| selector.type = fuchsia::modular::ContextValueType::ENTITY; |
| selector.meta = fuchsia::modular::ContextMetadata::New(); |
| selector.meta->entity = fuchsia::modular::EntityMetadata::New(); |
| selector.meta->entity->topic = "n"; |
| fuchsia::modular::ContextQuery query; |
| modular::AddToContextQuery(&query, "n", std::move(selector)); |
| reader_->Subscribe(std::move(query), listener_binding_.NewBinding()); |
| } |
| |
| void OnContextUpdate(fuchsia::modular::ContextUpdate update) override { |
| auto maybe_result = modular::TakeContextValue(&update, "n"); |
| if (!maybe_result) { |
| FXL_LOG(INFO) << "Expect an update key for every query key."; |
| } |
| auto& r = maybe_result.value(); |
| if (r.empty()) { |
| return; |
| } |
| int n = std::stoi(r.at(0).content); |
| |
| for (int i = n_; i < n; i++) { |
| Propose(std::to_string(i)); |
| } |
| for (int i = n; i < n_; i++) { |
| Remove(std::to_string(i)); |
| } |
| |
| n_ = n; |
| } |
| |
| private: |
| fuchsia::modular::ContextReaderPtr reader_; |
| fidl::Binding<fuchsia::modular::ContextListener> listener_binding_; |
| |
| int n_ = 0; |
| }; |
| |
| class SuggestionEngineTest : public ContextEngineTestBase, |
| fuchsia::modular::ProposalListener { |
| public: |
| SuggestionEngineTest() : story_provider_binding_(&story_provider_) {} |
| |
| void SetUp() override { |
| ContextEngineTestBase::SetUp(); |
| |
| component::Services suggestion_services = StartServices( |
| "fuchsia-pkg://fuchsia.com/suggestion_engine#meta/" |
| "suggestion_engine.cmx"); |
| suggestion_engine_ = |
| suggestion_services |
| .ConnectToService<fuchsia::modular::SuggestionEngine>(); |
| suggestion_provider_ = |
| suggestion_services |
| .ConnectToService<fuchsia::modular::SuggestionProvider>(); |
| suggestion_debug_ = |
| suggestion_services |
| .ConnectToService<fuchsia::modular::SuggestionDebug>(); |
| } |
| |
| protected: |
| fuchsia::modular::SuggestionEngine* suggestion_engine() { |
| return suggestion_engine_.get(); |
| } |
| |
| fuchsia::modular::SuggestionProvider* suggestion_provider() { |
| return suggestion_provider_.get(); |
| } |
| |
| fuchsia::modular::SuggestionDebug* suggestion_debug() { |
| return suggestion_debug_.get(); |
| } |
| |
| modular::StoryProviderMock* story_provider() { return &story_provider_; } |
| |
| void StartSuggestionAgent(const std::string& url) { |
| auto agent_bridge = |
| std::make_unique<MaxwellServiceProviderBridge>(root_environment()); |
| agent_bridge->AddService<fuchsia::modular::ContextReader>( |
| [this, |
| url](fidl::InterfaceRequest<fuchsia::modular::ContextReader> request) { |
| fuchsia::modular::ComponentScope scope; |
| fuchsia::modular::AgentScope agent_scope; |
| agent_scope.url = url; |
| scope.set_agent_scope(std::move(agent_scope)); |
| context_engine()->GetReader(std::move(scope), std::move(request)); |
| }); |
| agent_bridge->AddService<fuchsia::modular::ProposalPublisher>( |
| [this, url](fidl::InterfaceRequest<fuchsia::modular::ProposalPublisher> |
| request) { |
| suggestion_engine_->RegisterProposalPublisher(url, |
| std::move(request)); |
| }); |
| StartAgent(url, std::move(agent_bridge)); |
| } |
| |
| void AcceptSuggestion(const std::string& suggestion_id) { |
| Interact(suggestion_id, fuchsia::modular::InteractionType::SELECTED); |
| } |
| |
| void DismissSuggestion(const std::string& suggestion_id) { |
| Interact(suggestion_id, fuchsia::modular::InteractionType::DISMISSED); |
| } |
| |
| void WaitUntilIdle() { |
| ContextEngineTestBase::WaitUntilIdle(); |
| util::WaitUntilIdle(&suggestion_debug_, &loop_); |
| } |
| |
| void RunUntilIdle() { |
| suggestion_debug()->RunUntilIdle([this] { loop_.Quit(); }); |
| loop_.Run(); |
| loop_.ResetQuit(); |
| loop_.RunUntilIdle(); |
| } |
| |
| void AddProposalListenerBinding( |
| fidl::InterfaceRequest<fuchsia::modular::ProposalListener> request) { |
| proposal_listener_bindings_.AddBinding(this, std::move(request)); |
| } |
| |
| // The id of the most recently accepted proposal. |
| std::string accepted_proposal_id_; |
| |
| // The amount of proposals that have been accepted, as indicated by calls to |
| // |OnProposalAccepted|. |
| int accepted_proposal_count_ = 0; |
| |
| // Whether or not a successful create story action has been observed. |
| bool created_story_action_; |
| |
| // Story ID when a proposal is accepted. This is the ID of a story that was |
| // created as result of the execution of a proposal actions, either through |
| // AddModule or for a rich suggestion. |
| std::string accepted_proposal_story_id_; |
| |
| private: |
| void Interact(const std::string& suggestion_id, |
| fuchsia::modular::InteractionType interaction_type) { |
| fuchsia::modular::Interaction interaction; |
| interaction.type = interaction_type; |
| suggestion_provider_->NotifyInteraction(suggestion_id, |
| std::move(interaction)); |
| } |
| |
| // |fuchsia::modular::ProposalListener| |
| void OnProposalAccepted(std::string proposal_id, |
| fidl::StringPtr story_id) override { |
| accepted_proposal_id_ = proposal_id; |
| if (!story_id->empty()) { |
| created_story_action_ = true; |
| accepted_proposal_story_id_ = story_id; |
| } |
| accepted_proposal_count_++; |
| } |
| |
| fuchsia::modular::SuggestionEnginePtr suggestion_engine_; |
| fuchsia::modular::SuggestionDebugPtr suggestion_debug_; |
| fuchsia::modular::SuggestionProviderPtr suggestion_provider_; |
| |
| modular::StoryProviderMock story_provider_; |
| fidl::Binding<fuchsia::modular::StoryProvider> story_provider_binding_; |
| fidl::BindingSet<fuchsia::modular::ProposalListener> |
| proposal_listener_bindings_; |
| }; |
| |
| class AskTest : public virtual SuggestionEngineTest { |
| public: |
| AskTest() |
| : listener_binding_(&listener_), |
| debug_listener_binding_(&debug_listener_) {} |
| |
| void SetUp() override { |
| SuggestionEngineTest::SetUp(); |
| |
| suggestion_debug()->WatchAskProposals(debug_listener_binding_.NewBinding()); |
| } |
| |
| void CloseAndResetListener() { |
| if (listener_binding_.is_bound()) { |
| listener_binding_.Unbind(); |
| listener_.ClearSuggestions(); |
| } |
| } |
| |
| void Query(const std::string& query, int count = 10) { |
| CloseAndResetListener(); |
| fuchsia::modular::UserInput input; |
| input.type = fuchsia::modular::InputType::TEXT; |
| input.text = query; |
| suggestion_provider()->Query(listener_binding_.NewBinding(), |
| std::move(input), count); |
| } |
| |
| int suggestion_count() const { return listener_.suggestion_count(); } |
| |
| modular::TestSuggestionListener* listener() { return &listener_; } |
| |
| protected: |
| void EnsureDebugMatches() { |
| auto& subscriberAsks = listener_.GetSuggestions(); |
| auto& debugAsks = debug_listener_.GetProposals(); |
| EXPECT_GE(debugAsks.size(), subscriberAsks.size()); |
| for (size_t i = 0; i < subscriberAsks.size(); i++) { |
| auto& suggestion = subscriberAsks[i]; |
| auto& proposal = debugAsks[i]; |
| EXPECT_EQ(suggestion->display.headline, proposal.display.headline); |
| EXPECT_EQ(suggestion->display.subheadline, proposal.display.subheadline); |
| EXPECT_EQ(suggestion->display.details, proposal.display.details); |
| } |
| } |
| |
| private: |
| modular::TestSuggestionListener listener_; |
| modular::TestDebugAskListener debug_listener_; |
| fidl::Binding<fuchsia::modular::QueryListener> listener_binding_; |
| fidl::Binding<fuchsia::modular::AskProposalListener> debug_listener_binding_; |
| }; |
| |
| class InterruptionTest : public virtual SuggestionEngineTest { |
| public: |
| InterruptionTest() |
| : listener_binding_(&listener_), |
| debug_listener_binding_(&debug_listener_) {} |
| |
| void SetUp() override { |
| SuggestionEngineTest::SetUp(); |
| |
| suggestion_provider()->SubscribeToInterruptions( |
| listener_binding_.NewBinding()); |
| suggestion_debug()->WatchInterruptionProposals( |
| debug_listener_binding_.NewBinding()); |
| |
| // Make sure we're subscribed before we start the test. |
| WaitUntilIdle(); |
| } |
| |
| modular::TestDebugInterruptionListener* debugListener() { |
| return &debug_listener_; |
| } |
| modular::TestSuggestionListener* listener() { return &listener_; } |
| |
| protected: |
| int suggestion_count() const { return listener_.suggestion_count(); } |
| |
| void EnsureDebugMatches() { |
| auto& subscriberNexts = listener_.GetSuggestions(); |
| auto lastInterruption = debug_listener_.get_interrupt_proposal(); |
| ASSERT_GE(subscriberNexts.size(), 1u); |
| auto& suggestion = subscriberNexts[0]; |
| EXPECT_EQ(suggestion->display.headline, lastInterruption.display.headline); |
| EXPECT_EQ(suggestion->display.subheadline, |
| lastInterruption.display.subheadline); |
| EXPECT_EQ(suggestion->display.details, lastInterruption.display.details); |
| } |
| |
| private: |
| modular::TestSuggestionListener listener_; |
| modular::TestDebugInterruptionListener debug_listener_; |
| |
| fidl::Binding<fuchsia::modular::InterruptionListener> listener_binding_; |
| fidl::Binding<fuchsia::modular::InterruptionProposalListener> |
| debug_listener_binding_; |
| }; |
| |
| class NextTest : public virtual SuggestionEngineTest { |
| public: |
| NextTest() |
| : listener_binding_(&listener_), |
| debug_listener_binding_(&debug_listener_) {} |
| |
| void SetUp() override { |
| SuggestionEngineTest::SetUp(); |
| |
| suggestion_debug()->WatchNextProposals( |
| debug_listener_binding_.NewBinding()); |
| } |
| |
| modular::TestDebugNextListener* debugListener() { return &debug_listener_; } |
| modular::TestSuggestionListener* listener() { return &listener_; } |
| |
| protected: |
| void StartListening(int count) { |
| suggestion_provider()->SubscribeToNext(listener_binding_.NewBinding(), |
| count); |
| } |
| |
| void CloseAndResetListener() { |
| listener_binding_.Unbind(); |
| listener_.ClearSuggestions(); |
| } |
| |
| void SetResultCount(int count) { |
| CloseAndResetListener(); |
| suggestion_provider()->SubscribeToNext(listener_binding_.NewBinding(), |
| count); |
| } |
| |
| int suggestion_count() const { return listener_.suggestion_count(); } |
| |
| const fuchsia::modular::Suggestion* GetOnlySuggestion() const { |
| return listener_.GetOnlySuggestion(); |
| } |
| |
| void EnsureDebugMatches() { |
| auto& subscriberNexts = listener_.GetSuggestions(); |
| auto& debugNexts = debug_listener_.GetProposals(); |
| EXPECT_GE(debugNexts.size(), subscriberNexts.size()); |
| for (size_t i = 0; i < subscriberNexts.size(); i++) { |
| auto& suggestion = subscriberNexts[i]; |
| auto& proposal = debugNexts[i]; |
| EXPECT_EQ(suggestion->display.headline, proposal.display.headline); |
| EXPECT_EQ(suggestion->display.subheadline, proposal.display.subheadline); |
| EXPECT_EQ(suggestion->display.details, proposal.display.details); |
| } |
| } |
| |
| private: |
| modular::TestSuggestionListener listener_; |
| modular::TestDebugNextListener debug_listener_; |
| |
| fidl::Binding<fuchsia::modular::NextListener> listener_binding_; |
| fidl::Binding<fuchsia::modular::NextProposalListener> debug_listener_binding_; |
| }; |
| |
| class ResultCountTest : public NextTest { |
| public: |
| void SetUp() override { |
| NextTest::SetUp(); |
| |
| pub_.reset(new NWriter(context_engine())); |
| sub_.reset(new NProposals(context_engine(), suggestion_engine())); |
| } |
| |
| protected: |
| // Publishes signals for n new suggestions to context. |
| void PublishNewSignal(int n = 1) { pub_->Publish(n_ += n); } |
| |
| private: |
| std::unique_ptr<NWriter> pub_; |
| std::unique_ptr<NProposals> sub_; |
| int n_ = 0; |
| }; |
| |
| } // namespace |
| |
| // Macro rather than method to capture the expectation in the assertion message. |
| #define CHECK_RESULT_COUNT(expected) ASYNC_EQ(expected, suggestion_count()) |
| |
| TEST_F(ResultCountTest, InitiallyEmpty) { |
| StartListening(10); |
| WaitUntilIdle(); |
| EXPECT_EQ(0, suggestion_count()); |
| } |
| |
| TEST_F(ResultCountTest, OneByOne) { |
| StartListening(10); |
| PublishNewSignal(); |
| WaitUntilIdle(); |
| EXPECT_EQ(1, suggestion_count()); |
| |
| PublishNewSignal(); |
| WaitUntilIdle(); |
| EXPECT_EQ(2, suggestion_count()); |
| } |
| |
| TEST_F(ResultCountTest, AddOverLimit) { |
| StartListening(0); |
| PublishNewSignal(3); |
| WaitUntilIdle(); |
| EXPECT_EQ(0, suggestion_count()); |
| |
| SetResultCount(1); |
| WaitUntilIdle(); |
| EXPECT_EQ(1, suggestion_count()); |
| |
| SetResultCount(3); |
| WaitUntilIdle(); |
| EXPECT_EQ(3, suggestion_count()); |
| |
| SetResultCount(5); |
| WaitUntilIdle(); |
| EXPECT_EQ(3, suggestion_count()); |
| |
| PublishNewSignal(4); |
| WaitUntilIdle(); |
| EXPECT_EQ(5, suggestion_count()); |
| } |
| |
| TEST_F(ResultCountTest, Clear) { |
| StartListening(10); |
| PublishNewSignal(3); |
| WaitUntilIdle(); |
| EXPECT_EQ(3, suggestion_count()); |
| |
| SetResultCount(0); |
| WaitUntilIdle(); |
| EXPECT_EQ(0, suggestion_count()); |
| |
| SetResultCount(10); |
| WaitUntilIdle(); |
| EXPECT_EQ(3, suggestion_count()); |
| } |
| |
| TEST_F(ResultCountTest, MultiRemove) { |
| StartListening(10); |
| PublishNewSignal(3); |
| WaitUntilIdle(); |
| EXPECT_EQ(3, suggestion_count()); |
| |
| SetResultCount(1); |
| WaitUntilIdle(); |
| EXPECT_EQ(1, suggestion_count()); |
| |
| SetResultCount(10); |
| WaitUntilIdle(); |
| EXPECT_EQ(3, suggestion_count()); |
| } |
| |
| // Tests the removal of earlier suggestions, ensuring that suggestion engine can |
| // handle the case where an agent requests the removal of suggestions in a non- |
| // LIFO ordering. This exercises some internal shuffling, especially when |
| // rankings are likewise non-LIFO (where last = lowest-priority). |
| // |
| // TODO(rosswang): Currently this test also tests removing higher-ranked |
| // suggestions. After we have real ranking, add a test for that. |
| TEST_F(NextTest, Fifo) { |
| Proposinator fifo(suggestion_engine()); |
| |
| StartListening(10); |
| fifo.Propose("1"); |
| WaitUntilIdle(); |
| EXPECT_EQ(1, suggestion_count()); |
| auto uuid_1 = GetOnlySuggestion()->uuid; |
| |
| fifo.Propose("2"); |
| WaitUntilIdle(); |
| EXPECT_EQ(2, suggestion_count()); |
| fifo.Remove("1"); |
| WaitUntilIdle(); |
| EXPECT_EQ(1, suggestion_count()); |
| auto suggestion = GetOnlySuggestion(); |
| EXPECT_NE(uuid_1, suggestion->uuid); |
| EXPECT_EQ("2", suggestion->display.headline); |
| } |
| |
| // Tests the removal of earlier suggestions while capped. |
| // TODO(rosswang): see above TODO |
| TEST_F(NextTest, CappedFifo) { |
| Proposinator fifo(suggestion_engine()); |
| |
| StartListening(1); |
| fifo.Propose("1"); |
| WaitUntilIdle(); |
| EXPECT_EQ(1, suggestion_count()); |
| auto uuid1 = GetOnlySuggestion()->uuid; |
| |
| fifo.Propose("2"); |
| WaitUntilIdle(); |
| EXPECT_EQ(uuid1, GetOnlySuggestion()->uuid) |
| << "fuchsia::modular::Proposal 2 ranked over proposal 2; test invalid; " |
| "update to test " |
| "FIFO-ranked proposals."; |
| |
| fifo.Remove("1"); |
| WaitUntilIdle(); |
| ASSERT_EQ(1, suggestion_count()); |
| EXPECT_NE(uuid1, GetOnlySuggestion()->uuid); |
| |
| EXPECT_EQ("2", GetOnlySuggestion()->display.headline); |
| } |
| |
| TEST_F(NextTest, RemoveBeforeSubscribe) { |
| Proposinator zombinator(suggestion_engine()); |
| |
| zombinator.Propose("brains"); |
| zombinator.Remove("brains"); |
| WaitUntilIdle(); |
| |
| StartListening(10); |
| WaitUntilIdle(); |
| EXPECT_EQ(0, suggestion_count()); |
| } |
| |
| TEST_F(NextTest, SubscribeBeyondController) { |
| Proposinator p(suggestion_engine()); |
| |
| StartListening(10); |
| WaitUntilIdle(); |
| p.Propose("1"); |
| p.Propose("2"); |
| WaitUntilIdle(); |
| EXPECT_EQ(2, suggestion_count()); |
| } |
| |
| TEST_F(AskTest, DefaultAsk) { |
| AskProposinator p(suggestion_engine(), &loop_); |
| |
| Query("test query"); |
| p.WaitForQuery(); |
| p.ProposeForAsk("1"); |
| p.Commit(); |
| |
| WaitUntilIdle(); |
| EXPECT_EQ(1, suggestion_count()); |
| |
| Query("test query 2"); |
| p.WaitForQuery(); |
| p.ProposeForAsk("2"); |
| p.Commit(); |
| |
| WaitUntilIdle(); |
| EXPECT_EQ(1, suggestion_count()); |
| EnsureDebugMatches(); |
| } |
| |
| /* These tests assume that a string match between the proposal headline |
| and the query text factors into suggestion ranking. That ranking |
| feature is currently turned off and thus these tests fail, but they |
| will pass with it turned on. |
| |
| #define CHECK_TOP_HEADLINE(h) \ |
| ASYNC_CHECK(listener()->GetTopSuggestion()->display->headline == h) |
| |
| TEST_F(AskTest, AskDifferentQueries) { |
| AskProposinator p(suggestion_engine(), &loop_); |
| |
| Query("The Hottest Band on the Internet"); |
| p.WaitForQuery(); |
| p.ProposeForAsk("Mozart's Ghost"); |
| p.ProposeForAsk("The Hottest Band on the Internet"); |
| p.Commit(); |
| WaitUntilIdle(); |
| |
| CHECK_TOP_HEADLINE("The Hottest Band on the Internet"); |
| |
| Query("Mozart's Ghost"); |
| p.WaitForQuery(); |
| p.ProposeForAsk("Mozart's Ghost"); |
| p.ProposeForAsk("The Hottest Band on the Internet"); |
| p.Commit(); |
| WaitUntilIdle(); |
| |
| CHECK_TOP_HEADLINE("Mozart's Ghost"); |
| EnsureDebugMatches(); |
| } |
| |
| TEST_F(AskTest, ChangeHeadlineRank) { |
| AskProposinator p(suggestion_engine(), &loop_); |
| |
| Query("test query"); |
| p.WaitForQuery(); |
| p.ProposeForAsk("E-mail", "E-mail"); |
| p.ProposeForAsk("E-vite", "E-vite"); |
| p.ProposeForAsk("E-card", "E-card"); |
| p.ProposeForAsk("Music", "Music"); |
| p.Commit(); |
| |
| WaitUntilIdle(); |
| EXPECT_EQ(4, suggestion_count()); |
| |
| Query("Ca"); |
| p.WaitForQuery(); |
| p.ProposeForAsk("E-mail", "E-mail"); |
| p.ProposeForAsk("E-vite", "E-vite"); |
| p.ProposeForAsk("E-card", "E-card"); |
| p.ProposeForAsk("Music", "Music"); |
| p.Commit(); |
| WaitUntilIdle(); |
| |
| // E-card has a 'ca' in the 3rd position, so should be ranked highest. |
| CHECK_TOP_HEADLINE("E-card"); |
| |
| Query("Ca"); |
| p.WaitForQuery(); |
| p.ProposeForAsk("E-mail", "E-mail"); |
| p.ProposeForAsk("E-mail", "Cam"); |
| p.ProposeForAsk("E-vite", "E-vite"); |
| p.ProposeForAsk("E-card", "E-card"); |
| p.ProposeForAsk("Music", "Music"); |
| p.Commit(); |
| WaitUntilIdle(); |
| |
| CHECK_TOP_HEADLINE("Cam"); |
| EnsureDebugMatches(); |
| EXPECT_EQ(4, suggestion_count()); |
| } |
| */ |
| |
| /* These tests make an assumption that timestamp factors into ranking, which |
| it no longer does. It could be re-enabled if that factor is included again. |
| |
| #define HEADLINE_EQ(expected, index) \ |
| EXPECT_EQ(expected, (*listener())[index]->display->headline) |
| |
| TEST_F(AskTest, AskRanking) { |
| AskProposinator p(suggestion_engine(), &loop_); |
| |
| Query(""); |
| p.WaitForQuery(); |
| p.ProposeForAsk("View E-mail"); |
| p.ProposeForAsk("Compose E-mail"); |
| p.ProposeForAsk("Reply to E-mail"); |
| p.ProposeForAsk("Send E-vites"); |
| p.ProposeForAsk("E-mail Guests"); |
| p.Commit(); |
| |
| WaitUntilIdle(); |
| EXPECT_EQ(5, suggestion_count()); |
| // Results should be ranked by timestamp at this point. |
| HEADLINE_EQ("View E-mail", 0); |
| HEADLINE_EQ("Compose E-mail", 1); |
| HEADLINE_EQ("Reply to E-mail", 2); |
| HEADLINE_EQ("Send E-vites", 3); |
| HEADLINE_EQ("E-mail Guests", 4); |
| EnsureDebugMatches(); |
| |
| Query("e-mail"); |
| p.WaitForQuery(); |
| p.ProposeForAsk("View E-mail"); |
| p.ProposeForAsk("Compose E-mail"); |
| p.ProposeForAsk("Reply to E-mail"); |
| p.ProposeForAsk("Send E-vites"); |
| p.ProposeForAsk("E-mail Guests"); |
| p.Commit(); |
| |
| WaitUntilIdle(); |
| EXPECT_EQ(5, suggestion_count()); |
| HEADLINE_EQ("View E-mail", 0); |
| HEADLINE_EQ("E-mail Guests", 1); |
| HEADLINE_EQ("Compose E-mail", 2); |
| HEADLINE_EQ("Reply to E-mail", 3); |
| EnsureDebugMatches(); |
| |
| Query("e-mail", 2); |
| p.WaitForQuery(); |
| p.ProposeForAsk("View E-mail"); |
| p.ProposeForAsk("Compose E-mail"); |
| p.ProposeForAsk("Reply to E-mail"); |
| p.ProposeForAsk("Send E-vites"); |
| p.ProposeForAsk("E-mail Guests"); |
| p.Commit(); |
| |
| WaitUntilIdle(); |
| EXPECT_EQ(2, suggestion_count()); |
| HEADLINE_EQ("View E-mail", 0); |
| HEADLINE_EQ("E-mail Guests", 1); |
| |
| Query("Compose", 1); |
| p.WaitForQuery(); |
| p.ProposeForAsk("View E-mail"); |
| p.ProposeForAsk("Compose E-mail"); |
| p.ProposeForAsk("Reply to E-mail"); |
| p.ProposeForAsk("Send E-vites"); |
| p.ProposeForAsk("E-mail Guests"); |
| p.Commit(); |
| |
| WaitUntilIdle(); |
| EXPECT_EQ(1, suggestion_count()); |
| HEADLINE_EQ("Compose E-mail", 0); |
| EnsureDebugMatches(); |
| } |
| */ |
| |
| // This test ensures that Suggestion Engine doesn't keep querying dead handlers. |
| TEST_F(AskTest, DeadAgent) { |
| AskProposinator p(suggestion_engine(), &loop_); |
| |
| { AskProposinator m(suggestion_engine(), &loop_); } |
| |
| Query("test query"); |
| p.WaitForQuery(); |
| p.ProposeForAsk("1"); |
| p.Commit(); |
| |
| // RunUntilIdle rather than WaitUntilIdle to not wait for the query handler |
| // timeout. If we'd need to wait for the timeout, the test should fail. |
| RunUntilIdle(); |
| |
| EXPECT_EQ(1, suggestion_count()); |
| EXPECT_TRUE(listener()->query_complete()); |
| } |
| |
| // This test ensures that if a query handler dies while handling a query, |
| // Suggestion Engine stops waiting for it gracefully. |
| TEST_F(AskTest, AgentKilledInAction) { |
| AskProposinator p(suggestion_engine(), &loop_); |
| |
| { |
| AskProposinator coulson(suggestion_engine(), &loop_); |
| |
| Query("test query"); |
| p.WaitForQuery(); |
| p.ProposeForAsk("1"); |
| p.Commit(); |
| |
| RunUntilIdle(); |
| |
| EXPECT_FALSE(listener()->query_complete()); |
| } |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(1, suggestion_count()); |
| EXPECT_TRUE(listener()->query_complete()); |
| } |
| |
| class SuggestionFilteringTest : public NextTest {}; |
| |
| TEST_F(SuggestionFilteringTest, Baseline) { |
| // Show that without any existing Stories, we see Proposals to launch |
| // any story. |
| Proposinator p(suggestion_engine()); |
| StartListening(10); |
| |
| fuchsia::modular::Intent intent; |
| intent.handler = "foo://bar"; |
| fuchsia::modular::AddMod add_mod; |
| add_mod.mod_name_transitional = "foo"; |
| add_mod.intent = std::move(intent); |
| |
| fuchsia::modular::StoryCommand command; |
| command.set_add_mod(std::move(add_mod)); |
| std::vector<fuchsia::modular::StoryCommand> commands; |
| commands.push_back(std::move(command)); |
| p.Propose("1", std::move(commands)); |
| WaitUntilIdle(); |
| EXPECT_EQ(1, suggestion_count()); |
| } |
| |
| TEST_F(SuggestionFilteringTest, Baseline_FilterDoesntMatch) { |
| // Show that with an existing Story for a URL, we see Proposals to launch |
| // other URLs. |
| Proposinator p(suggestion_engine()); |
| StartListening(10); |
| |
| // First notify watchers of the fuchsia::modular::StoryProvider that a story |
| // already exists. |
| fuchsia::modular::StoryInfo story_info; |
| story_info.url = "foo://bazzle_dazzle"; |
| story_info.id = ""; |
| story_provider()->NotifyStoryChanged( |
| std::move(story_info), fuchsia::modular::StoryState::STOPPED, |
| fuchsia::modular::StoryVisibilityState::DEFAULT); |
| |
| fuchsia::modular::Intent intent; |
| intent.handler = "foo://bar"; |
| fuchsia::modular::AddMod add_mod; |
| add_mod.intent = std::move(intent); |
| add_mod.mod_name_transitional = "foo"; |
| |
| fuchsia::modular::StoryCommand command; |
| command.set_add_mod(std::move(add_mod)); |
| std::vector<fuchsia::modular::StoryCommand> commands; |
| commands.push_back(std::move(command)); |
| p.Propose("1", std::move(commands)); |
| WaitUntilIdle(); |
| EXPECT_EQ(1, suggestion_count()); |
| } |
| |
| TEST_F(InterruptionTest, SingleInterruption) { |
| Proposinator p(suggestion_engine()); |
| |
| p.Propose("1", "2", fuchsia::modular::AnnoyanceType::INTERRUPT); |
| |
| WaitUntilIdle(); |
| EXPECT_EQ(1, suggestion_count()); |
| EnsureDebugMatches(); |
| } |
| |
| TEST_F(InterruptionTest, RemovedInterruption) { |
| Proposinator p(suggestion_engine()); |
| |
| p.Propose("1", "2", fuchsia::modular::AnnoyanceType::INTERRUPT); |
| |
| WaitUntilIdle(); |
| EXPECT_EQ(1, suggestion_count()); |
| EnsureDebugMatches(); |
| |
| // Removing shouldn't do anything to an interruption |
| p.Remove("1"); |
| |
| WaitUntilIdle(); |
| EXPECT_EQ(1, suggestion_count()); |
| } |
| |
| } // namespace maxwell |