blob: b23b2374de042e6adb97a2f15dea0c91abec4306 [file] [log] [blame]
// 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