| // Copyright 2017 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 "peridot/bin/sessionmgr/agent_runner/agent_runner.h" |
| |
| #include <fs/service.h> |
| #include <fs/synchronous-vfs.h> |
| #include <fuchsia/modular/cpp/fidl.h> |
| #include <fuchsia/sys/cpp/fidl.h> |
| #include <lib/component/cpp/service_provider_impl.h> |
| #include <lib/component/cpp/testing/fake_launcher.h> |
| #include <lib/fidl/cpp/binding.h> |
| #include <lib/zx/object.h> |
| #include <src/lib/fxl/macros.h> |
| #include <zircon/errors.h> |
| #include <zircon/status.h> |
| |
| #include <memory> |
| |
| #include "gtest/gtest.h" |
| #include "peridot/bin/sessionmgr/agent_runner/map_agent_service_index.h" |
| #include "peridot/bin/sessionmgr/entity_provider_runner/entity_provider_runner.h" |
| #include "peridot/bin/sessionmgr/message_queue/message_queue_manager.h" |
| #include "peridot/lib/fidl/array_to_string.h" |
| #include "peridot/lib/ledger_client/page_id.h" |
| #include "peridot/lib/testing/fake_agent_runner_storage.h" |
| #include "peridot/lib/testing/mock_base.h" |
| #include "peridot/lib/testing/test_with_ledger.h" |
| #include "src/lib/files/scoped_temp_dir.h" |
| |
| namespace modular { |
| namespace testing { |
| namespace { |
| |
| using ::component::testing::FakeLauncher; |
| |
| // The choice of "Clipboard" as the test service is arbitrary, but the |
| // ConnectToAgentService() tests require an existing service type. |
| const char* kTestAgentService = fuchsia::modular::Clipboard::Name_; |
| constexpr char kTestAgentUrl[] = "file:///my_agent"; |
| |
| // Configuration for testing |ComponentContext| ConnectToAgentService(). |
| struct ConnectToAgentServiceTestConfig { |
| // The map of |service_name|->|agent_url| used to look up a service |
| // handler |agent_url| by name. |
| std::map<std::string, std::string> agent_service_index = {}; |
| |
| // If true, include the service_name in the |AgentServiceRequest|. |
| // This is required for a successful connection. |
| bool provide_service_name = false; |
| |
| // If true, include the specific handler (agent URL) in the |
| // |AgentServiceRequest|. This is *not* required for a successful connection. |
| bool provide_handler = false; |
| |
| // If true, include the service client-side channel in the |
| // |AgentServiceRequest|. This is required for a successful connection. |
| bool provide_channel = false; |
| |
| // If true, include the |AgentController| in the |AgentServiceRequest|. |
| // This is required for a successful connection. |
| bool provide_agent_controller = false; |
| }; |
| |
| // Expected test results. |
| struct ConnectToAgentServiceExpect { |
| // If true, the test should connect to the test agent and verify the |
| // agent-side channel has a |koid| that matches the client-side channel. |
| bool agent_got_service_request = false; |
| |
| // If set to an error code, the service channel should receive the given |
| // error. |
| zx_status_t service_status = ZX_OK; |
| }; |
| |
| static zx_koid_t get_object_koid(zx_handle_t handle) { |
| zx_info_handle_basic_t info; |
| if (zx_object_get_info(handle, ZX_INFO_HANDLE_BASIC, &info, sizeof(info), |
| NULL, NULL) != ZX_OK) { |
| return 0; |
| } |
| return info.koid; |
| } |
| |
| class TestAgent : fuchsia::modular::Agent, |
| public fuchsia::sys::ComponentController, |
| public testing::MockBase { |
| public: |
| TestAgent(zx::channel directory_request, |
| fidl::InterfaceRequest<fuchsia::sys::ComponentController> ctrl, |
| std::unique_ptr<component::ServiceNamespace> services_ptr = nullptr) |
| : vfs_(async_get_default_dispatcher()), |
| outgoing_directory_(fbl::AdoptRef(new fs::PseudoDir())), |
| controller_(this, std::move(ctrl)), |
| agent_binding_(this), |
| services_ptr_(std::move(services_ptr)) { |
| outgoing_directory_->AddEntry( |
| fuchsia::modular::Agent::Name_, |
| fbl::AdoptRef(new fs::Service([this](zx::channel channel) { |
| agent_binding_.Bind(std::move(channel)); |
| return ZX_OK; |
| }))); |
| vfs_.ServeDirectory(outgoing_directory_, std::move(directory_request)); |
| } |
| |
| void KillApplication() { controller_.Unbind(); } |
| |
| size_t GetCallCount(const std::string func) { return counts.count(func); } |
| |
| private: |
| // |ComponentController| |
| void Kill() override { ++counts["Kill"]; } |
| // |ComponentController| |
| void Detach() override { ++counts["Detach"]; } |
| |
| // |fuchsia::modular::Agent| |
| void Connect(std::string /*requestor_url*/, |
| fidl::InterfaceRequest<fuchsia::sys::ServiceProvider> |
| outgoing_services) override { |
| ++counts["Connect"]; |
| if (services_ptr_) { |
| services_ptr_->AddBinding(std::move(outgoing_services)); |
| } |
| } |
| |
| // |fuchsia::modular::Agent| |
| void RunTask(std::string /*task_id*/, RunTaskCallback /*callback*/) override { |
| ++counts["RunTask"]; |
| } |
| |
| private: |
| fs::SynchronousVfs vfs_; |
| fbl::RefPtr<fs::PseudoDir> outgoing_directory_; |
| fidl::Binding<fuchsia::sys::ComponentController> controller_; |
| fidl::Binding<fuchsia::modular::Agent> agent_binding_; |
| |
| std::unique_ptr<component::ServiceNamespace> services_ptr_; |
| |
| FXL_DISALLOW_COPY_AND_ASSIGN(TestAgent); |
| }; |
| |
| class AgentRunnerTest : public TestWithLedger { |
| public: |
| AgentRunnerTest() = default; |
| |
| void SetUp() override { |
| TestWithLedger::SetUp(); |
| |
| mqm_ = std::make_unique<MessageQueueManager>( |
| ledger_client(), MakePageId("0123456789123456"), mq_data_dir_.path()); |
| entity_provider_runner_ = std::make_unique<EntityProviderRunner>(nullptr); |
| // The |fuchsia::modular::UserIntelligenceProvider| below must be nullptr in |
| // order for agent creation to be synchronous, which these tests assume. |
| } |
| |
| void TearDown() override { |
| agent_runner_.reset(); |
| entity_provider_runner_.reset(); |
| mqm_.reset(); |
| |
| TestWithLedger::TearDown(); |
| } |
| |
| MessageQueueManager* message_queue_manager() { return mqm_.get(); } |
| |
| protected: |
| AgentRunner* agent_runner() { |
| if (agent_runner_ == nullptr) { |
| agent_runner_ = std::make_unique<AgentRunner>( |
| &launcher_, mqm_.get(), ledger_repository(), &agent_runner_storage_, |
| token_manager_.get(), nullptr, entity_provider_runner_.get(), |
| std::make_unique<MapAgentServiceIndex>( |
| std::move(agent_service_index_))); |
| } |
| return agent_runner_.get(); |
| } |
| |
| void set_agent_service_index( |
| std::map<std::string, std::string> agent_service_index) { |
| agent_service_index_ = std::move(agent_service_index); |
| } |
| |
| template <typename Interface> |
| void request_agent_service( |
| ConnectToAgentServiceTestConfig test_config, std::string service_name, |
| std::string agent_url, fidl::InterfaceRequest<Interface> service_request, |
| fuchsia::modular::AgentControllerPtr agent_controller) { |
| fuchsia::modular::AgentServiceRequest agent_service_request; |
| if (test_config.provide_service_name) { |
| agent_service_request.set_service_name(service_name); |
| } |
| if (test_config.provide_handler) { |
| agent_service_request.set_handler(agent_url); |
| } |
| if (test_config.provide_channel) { |
| agent_service_request.set_channel(service_request.TakeChannel()); |
| } |
| if (test_config.provide_agent_controller) { |
| agent_service_request.set_agent_controller(agent_controller.NewRequest()); |
| } |
| agent_runner()->ConnectToAgentService("requestor_url", |
| std::move(agent_service_request)); |
| } |
| |
| void execute_connect_to_agent_service_test( |
| ConnectToAgentServiceTestConfig test_config, |
| ConnectToAgentServiceExpect expect) { |
| // Client-side service pointer |
| fuchsia::modular::ClipboardPtr service_ptr; |
| auto service_name = service_ptr->Name_; |
| auto service_request = service_ptr.NewRequest(); |
| zx_status_t service_status = ZX_OK; |
| service_ptr.set_error_handler( |
| [&service_status](zx_status_t status) { service_status = status; }); |
| |
| // standard AgentController initialization |
| fuchsia::modular::AgentControllerPtr agent_controller; |
| zx_status_t agent_controller_status = ZX_OK; |
| agent_controller.set_error_handler( |
| [&agent_controller_status](zx_status_t status) { |
| agent_controller_status = status; |
| }); |
| |
| // register a service for the agent to serve, and expect the client's |
| // request |
| auto services_ptr = std::make_unique<component::ServiceNamespace>(); |
| bool agent_got_service_request = false; |
| services_ptr->AddService<fuchsia::modular::Clipboard>( |
| [&agent_got_service_request, client_request_koid = get_object_koid( |
| service_request.channel().get())]( |
| fidl::InterfaceRequest<fuchsia::modular::Clipboard> request) { |
| auto server_request_koid = get_object_koid(request.channel().get()); |
| EXPECT_EQ(server_request_koid, client_request_koid); |
| agent_got_service_request = true; |
| }); |
| |
| // register and launch the test agent, with services |
| std::unique_ptr<TestAgent> test_agent; |
| launcher()->RegisterComponent( |
| kTestAgentUrl, |
| [&test_agent, &services_ptr]( |
| fuchsia::sys::LaunchInfo launch_info, |
| fidl::InterfaceRequest<fuchsia::sys::ComponentController> ctrl) { |
| test_agent = std::make_unique<TestAgent>( |
| std::move(launch_info.directory_request), std::move(ctrl), |
| std::move(services_ptr)); |
| }); |
| |
| request_agent_service(test_config, service_name, kTestAgentUrl, |
| std::move(service_request), |
| std::move(agent_controller)); |
| |
| RunLoopWithTimeoutOrUntil([&] { |
| return agent_got_service_request || service_status != ZX_OK || |
| (expect.service_status == ZX_OK && |
| agent_controller_status != ZX_OK); |
| // The order of error callbacks is non-deterministic. If checking for a |
| // specific service error, wait for it. |
| }); |
| |
| EXPECT_EQ(agent_got_service_request, expect.agent_got_service_request); |
| if (!agent_got_service_request) { |
| // If the agent successfully got the expected service request, ignore |
| // service errors. This test does not actually complete the connection. |
| EXPECT_EQ(service_status, expect.service_status); |
| } |
| } |
| |
| FakeLauncher* launcher() { return &launcher_; } |
| |
| private: |
| FakeLauncher launcher_; |
| |
| files::ScopedTempDir mq_data_dir_; |
| std::unique_ptr<MessageQueueManager> mqm_; |
| FakeAgentRunnerStorage agent_runner_storage_; |
| std::unique_ptr<EntityProviderRunner> entity_provider_runner_; |
| std::unique_ptr<AgentRunner> agent_runner_; |
| std::map<std::string, std::string> agent_service_index_; |
| |
| fuchsia::auth::TokenManagerPtr token_manager_; |
| |
| FXL_DISALLOW_COPY_AND_ASSIGN(AgentRunnerTest); |
| }; |
| |
| } // namespace |
| |
| // Test that connecting to an agent will start it up. |
| // Then there should be an fuchsia::modular::Agent.Connect(). |
| TEST_F(AgentRunnerTest, ConnectToAgent) { |
| int agent_launch_count = 0; |
| std::unique_ptr<TestAgent> test_agent; |
| launcher()->RegisterComponent( |
| kTestAgentUrl, |
| [&test_agent, &agent_launch_count]( |
| fuchsia::sys::LaunchInfo launch_info, |
| fidl::InterfaceRequest<fuchsia::sys::ComponentController> ctrl) { |
| test_agent = std::make_unique<TestAgent>( |
| std::move(launch_info.directory_request), std::move(ctrl)); |
| ++agent_launch_count; |
| }); |
| |
| fuchsia::sys::ServiceProviderPtr incoming_services; |
| fuchsia::modular::AgentControllerPtr agent_controller; |
| agent_runner()->ConnectToAgent("requestor_url", kTestAgentUrl, |
| incoming_services.NewRequest(), |
| agent_controller.NewRequest()); |
| |
| RunLoopWithTimeoutOrUntil([&test_agent] { |
| return test_agent && test_agent->GetCallCount("Connect") > 0; |
| }); |
| EXPECT_EQ(1, agent_launch_count); |
| test_agent->ExpectCalledOnce("Connect"); |
| test_agent->ExpectNoOtherCalls(); |
| |
| // Connecting to the same agent again shouldn't launch a new instance and |
| // shouldn't re-initialize the existing instance of the agent application, |
| // but should call |Connect()|. |
| |
| fuchsia::modular::AgentControllerPtr agent_controller2; |
| fuchsia::sys::ServiceProviderPtr incoming_services2; |
| agent_runner()->ConnectToAgent("requestor_url2", kTestAgentUrl, |
| incoming_services2.NewRequest(), |
| agent_controller2.NewRequest()); |
| |
| RunLoopWithTimeoutOrUntil([&test_agent] { |
| return test_agent && test_agent->GetCallCount("Connect"); |
| }); |
| EXPECT_EQ(1, agent_launch_count); |
| test_agent->ExpectCalledOnce("Connect"); |
| test_agent->ExpectNoOtherCalls(); |
| } |
| |
| // Test that if an agent application dies, it is removed from agent runner |
| // (which means outstanding AgentControllers are closed). |
| TEST_F(AgentRunnerTest, AgentController) { |
| std::unique_ptr<TestAgent> test_agent; |
| launcher()->RegisterComponent( |
| kTestAgentUrl, |
| [&test_agent]( |
| fuchsia::sys::LaunchInfo launch_info, |
| fidl::InterfaceRequest<fuchsia::sys::ComponentController> ctrl) { |
| test_agent = std::make_unique<TestAgent>( |
| std::move(launch_info.directory_request), std::move(ctrl)); |
| }); |
| |
| fuchsia::sys::ServiceProviderPtr incoming_services; |
| fuchsia::modular::AgentControllerPtr agent_controller; |
| agent_runner()->ConnectToAgent("requestor_url", kTestAgentUrl, |
| incoming_services.NewRequest(), |
| agent_controller.NewRequest()); |
| |
| RunLoopWithTimeoutOrUntil([&test_agent] { return !!test_agent; }); |
| test_agent->KillApplication(); |
| |
| // fuchsia::modular::Agent application died, so check that |
| // fuchsia::modular::AgentController dies here. |
| agent_controller.set_error_handler( |
| [&agent_controller](zx_status_t status) { agent_controller.Unbind(); }); |
| RunLoopWithTimeoutOrUntil( |
| [&agent_controller] { return !agent_controller.is_bound(); }); |
| EXPECT_FALSE(agent_controller.is_bound()); |
| } |
| |
| TEST_F(AgentRunnerTest, NoServiceNameInAgentServiceRequest) { |
| ConnectToAgentServiceTestConfig test_config; |
| // test_config.provide_service_name = true; |
| test_config.provide_channel = true; |
| test_config.provide_agent_controller = true; |
| |
| ConnectToAgentServiceExpect expect; |
| expect.agent_got_service_request = false; |
| expect.service_status = ZX_ERR_PEER_CLOSED; |
| |
| execute_connect_to_agent_service_test(test_config, expect); |
| } |
| |
| TEST_F(AgentRunnerTest, NoChannelInAgentServiceRequest) { |
| ConnectToAgentServiceTestConfig test_config; |
| test_config.provide_service_name = true; |
| // test_config.provide_channel = true; |
| test_config.provide_agent_controller = true; |
| |
| ConnectToAgentServiceExpect expect; |
| expect.agent_got_service_request = false; |
| expect.service_status = ZX_ERR_PEER_CLOSED; |
| |
| execute_connect_to_agent_service_test(test_config, expect); |
| } |
| |
| TEST_F(AgentRunnerTest, NoAgentControllerInAgentServiceRequest) { |
| ConnectToAgentServiceTestConfig test_config; |
| test_config.provide_service_name = true; |
| test_config.provide_channel = true; |
| // test_config.provide_agent_controller = true; |
| |
| ConnectToAgentServiceExpect expect; |
| expect.agent_got_service_request = false; |
| expect.service_status = ZX_ERR_PEER_CLOSED; |
| |
| execute_connect_to_agent_service_test(test_config, expect); |
| } |
| |
| TEST_F(AgentRunnerTest, NoAgentForServiceName) { |
| ConnectToAgentServiceTestConfig test_config; |
| // Default agent_service_index is empty, so agent_url will not be found |
| test_config.provide_service_name = true; |
| test_config.provide_channel = true; |
| test_config.provide_agent_controller = true; |
| |
| ConnectToAgentServiceExpect expect; |
| expect.agent_got_service_request = false; |
| expect.service_status = ZX_ERR_NOT_FOUND; |
| |
| execute_connect_to_agent_service_test(test_config, expect); |
| } |
| |
| TEST_F(AgentRunnerTest, ConnectToServiceName) { |
| ConnectToAgentServiceTestConfig test_config; |
| // requested service will map to test agent |
| test_config.agent_service_index = { |
| {kTestAgentService, kTestAgentUrl}, |
| }; |
| test_config.provide_service_name = true; |
| test_config.provide_channel = true; |
| test_config.provide_agent_controller = true; |
| |
| ConnectToAgentServiceExpect expect; |
| expect.agent_got_service_request = true; |
| expect.service_status = ZX_ERR_PEER_CLOSED; |
| |
| set_agent_service_index(test_config.agent_service_index); |
| |
| execute_connect_to_agent_service_test(test_config, expect); |
| } |
| |
| } // namespace testing |
| } // namespace modular |