blob: 2d6f4e1c9192cbeb3fe604b160feb9a9b749f785 [file] [log] [blame]
// Copyright 2019 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 "lib/modular_test_harness/cpp/test_harness_impl.h"
#include <lib/fsl/vmo/strings.h>
#include <lib/vfs/cpp/pseudo_dir.h>
#include <lib/vfs/cpp/pseudo_file.h>
#include <peridot/lib/modular_config/modular_config_constants.h>
#include <peridot/lib/modular_config/modular_config_xdr.h>
#include <peridot/lib/util/pseudo_dir_utils.h>
#include <src/lib/files/path.h>
#include <src/lib/fxl/logging.h>
#include <src/lib/fxl/strings/join_strings.h>
#include <src/lib/fxl/strings/split_string.h>
#include <src/lib/fxl/strings/substitute.h>
namespace modular::testing {
namespace {
constexpr char kBasemgrUrl[] =
"fuchsia-pkg://fuchsia.com/basemgr#meta/basemgr.cmx";
// Defaut shell URLs which are used if not specified.
constexpr char kBaseShellDefaultUrl[] =
"fuchsia-pkg://fuchsia.com/modular_test_harness#meta/test_base_shell.cmx";
constexpr char kSessionShellDefaultUrl[] =
"fuchsia-pkg://fuchsia.com/modular_test_harness#meta/"
"test_session_shell.cmx";
constexpr char kStoryShellDefaultUrl[] =
"fuchsia-pkg://fuchsia.com/modular_test_harness#meta/test_story_shell.cmx";
constexpr char kSessionAgentFakeInterceptionUrl[] =
"fuchsia-pkg://example.com/FAKE_SESSION_AGENT_PKG/fake_session_agent.cmx";
constexpr char kSessionAgentFakeInterceptionCmx[] = R"(
{
"sandbox": {
"services": [
"fuchsia.modular.PuppetMaster",
"fuchsia.modular.AgentContext",
"fuchsia.modular.ComponentContext"
]
}
}
)";
}; // namespace
class TestHarnessImpl::InterceptedComponentImpl
: public fuchsia::modular::testing::InterceptedComponent {
public:
using RemoveHandler = fit::function<void()>;
InterceptedComponentImpl(
std::unique_ptr<sys::testing::InterceptedComponent> impl,
fidl::InterfaceRequest<fuchsia::modular::testing::InterceptedComponent>
request)
: impl_(std::move(impl)), binding_(this, std::move(request)) {
impl_->set_on_kill([this] {
binding_.events().OnKill();
remove_handler_();
});
}
virtual ~InterceptedComponentImpl() = default;
void set_remove_handler(RemoveHandler remove_handler) {
remove_handler_ = std::move(remove_handler);
}
private:
// |fuchsia::modular::testing::InterceptedComponent|
void Exit(int64_t exit_code, fuchsia::sys::TerminationReason reason) {
impl_->Exit(exit_code, reason);
remove_handler_();
}
std::unique_ptr<sys::testing::InterceptedComponent> impl_;
fidl::Binding<fuchsia::modular::testing::InterceptedComponent> binding_;
RemoveHandler remove_handler_;
};
// This class implements a session agent using AgentDriver.
class TestHarnessImpl::InterceptedSessionAgent final {
public:
InterceptedSessionAgent(::modular::AgentHost* host) {}
// Called by AgentDriver.
void Connect(
fidl::InterfaceRequest<fuchsia::sys::ServiceProvider> outgoing_services) {
}
// Called by AgentDriver.
void RunTask(const fidl::StringPtr& task_id,
const fit::function<void()>& done) {
FXL_DLOG(WARNING) << "This session agent does not run tasks";
done();
}
// Called by AgentDriver.
void Terminate(const fit::function<void()>& done) { done(); }
};
TestHarnessImpl::TestHarnessImpl(
const fuchsia::sys::EnvironmentPtr& parent_env,
fidl::InterfaceRequest<fuchsia::modular::testing::TestHarness> request,
fit::function<void()> on_disconnected)
: parent_env_(parent_env),
binding_(this, std::move(request)),
on_disconnected_(std::move(on_disconnected)),
interceptor_(
sys::testing::ComponentInterceptor::CreateWithEnvironmentLoader(
parent_env_)) {
binding_.set_error_handler(
[this](zx_status_t status) { CloseBindingIfError(status); });
}
TestHarnessImpl::~TestHarnessImpl() = default;
void TestHarnessImpl::GetService(
fuchsia::modular::testing::TestHarnessService service) {
switch (service.Which()) {
case fuchsia::modular::testing::TestHarnessService::Tag::kPuppetMaster: {
BufferSessionAgentService(std::move(service.puppet_master()));
} break;
case fuchsia::modular::testing::TestHarnessService::Tag::
kComponentContext: {
BufferSessionAgentService(std::move(service.component_context()));
} break;
case fuchsia::modular::testing::TestHarnessService::Tag::kAgentContext: {
BufferSessionAgentService(std::move(service.agent_context()));
} break;
case fuchsia::modular::testing::TestHarnessService::Tag::Empty: {
FXL_LOG(ERROR) << "The given TestHarnessService is empty.";
CloseBindingIfError(ZX_ERR_INVALID_ARGS);
return;
} break;
}
}
void TestHarnessImpl::ConnectToModularService(
fuchsia::modular::testing::ModularService service) {
switch (service.Which()) {
case fuchsia::modular::testing::ModularService::Tag::kPuppetMaster: {
BufferSessionAgentService(std::move(service.puppet_master()));
} break;
case fuchsia::modular::testing::ModularService::Tag::kComponentContext: {
BufferSessionAgentService(std::move(service.component_context()));
} break;
case fuchsia::modular::testing::ModularService::Tag::kAgentContext: {
BufferSessionAgentService(std::move(service.agent_context()));
} break;
case fuchsia::modular::testing::ModularService::Tag::Empty: {
FXL_LOG(ERROR) << "The given ModularService is empty.";
CloseBindingIfError(ZX_ERR_INVALID_ARGS);
return;
} break;
}
}
void TestHarnessImpl::ConnectToEnvironmentService(std::string service_name,
zx::channel request) {
enclosing_env_->ConnectToService(service_name, std::move(request));
}
bool TestHarnessImpl::CloseBindingIfError(zx_status_t status) {
if (status != ZX_OK) {
binding_.Close(status);
// destory |enclosing_env_| should kill all processes.
enclosing_env_.reset();
on_disconnected_();
return true;
}
return false;
}
void TestHarnessImpl::InjectServicesIntoEnvironment(
sys::testing::EnvironmentServices* env_services,
std::map<std::string, std::string>* default_injected_svcs) {
// Wire up client-specified injected services, and remove them from the
// default injected services.
if (spec_.has_env_services_to_inject()) {
for (const auto& injected_svc : spec_.env_services_to_inject()) {
default_injected_svcs->erase(injected_svc.name);
fuchsia::sys::LaunchInfo info;
info.url = injected_svc.url;
env_services->AddServiceWithLaunchInfo(std::move(info),
injected_svc.name);
}
}
// Wire up the remaining default injected services.
for (const auto& injected_svc : *default_injected_svcs) {
fuchsia::sys::LaunchInfo info;
info.url = injected_svc.second;
env_services->AddServiceWithLaunchInfo(std::move(info), injected_svc.first);
}
}
std::string MakeTestHarnessEnvironmentName() {
// Apply a random suffix to the environment name so that multiple hermetic
// test harness environments may coexist under the same parent env.
uint32_t random_env_suffix = 0;
zx_cprng_draw(&random_env_suffix, sizeof random_env_suffix);
return fxl::Substitute("modular_test_harness_$0",
std::to_string(random_env_suffix));
}
void TestHarnessImpl::Run(fuchsia::modular::testing::TestHarnessSpec spec) {
// Run() can only be called once.
if (enclosing_env_) {
CloseBindingIfError(ZX_ERR_ALREADY_BOUND);
return;
}
spec_ = std::move(spec);
if (CloseBindingIfError(SetupComponentInterception())) {
return;
}
if (CloseBindingIfError(SetupFakeSessionAgent())) {
return;
}
std::unique_ptr<sys::testing::EnvironmentServices> env_services =
interceptor_.MakeEnvironmentServices(parent_env_);
// The default injected services are all basemgr's hard dependencies.
// A map of service name => component URL serving it.
std::map<std::string, std::string> default_injected_svcs = {
{fuchsia::auth::account::AccountManager::Name_,
"fuchsia-pkg://fuchsia.com/account_manager#meta/account_manager.cmx"},
{fuchsia::devicesettings::DeviceSettingsManager::Name_,
"fuchsia-pkg://fuchsia.com/device_settings_manager#meta/"
"device_settings_manager.cmx"}};
// Allow services to be inherited from outside the test harness environment.
if (spec_.has_env_services_to_inherit()) {
for (auto& svc_name : spec_.env_services_to_inherit()) {
default_injected_svcs.erase(svc_name);
env_services->AllowParentService(svc_name);
}
}
InjectServicesIntoEnvironment(env_services.get(), &default_injected_svcs);
// Ledger configuration for tests by default:
// * use a memory-backed FS for ledger.
// * doesn't sync with a cloudprovider.
auto* sessionmgr_config =
spec_.mutable_sessionmgr_config(); // auto initialize.
if (!sessionmgr_config->has_use_memfs_for_ledger()) {
sessionmgr_config->set_use_memfs_for_ledger(true);
}
if (!sessionmgr_config->has_cloud_provider()) {
sessionmgr_config->set_cloud_provider(
fuchsia::modular::session::CloudProvider::NONE);
}
enclosing_env_ = sys::testing::EnclosingEnvironment::Create(
MakeTestHarnessEnvironmentName(), parent_env_, std::move(env_services));
zx::channel client;
zx::channel request;
FXL_CHECK(zx::channel::create(0u, &client, &request) == ZX_OK);
basemgr_config_dir_ = MakeBasemgrConfigDir(spec_);
basemgr_config_dir_->Serve(fuchsia::io::OPEN_RIGHT_READABLE,
std::move(request));
fuchsia::sys::LaunchInfo info;
info.url = kBasemgrUrl;
info.flat_namespace = fuchsia::sys::FlatNamespace::New();
info.flat_namespace->paths.push_back(modular_config::kOverriddenConfigDir);
info.flat_namespace->directories.push_back(std::move(client));
basemgr_ctrl_ = enclosing_env_->CreateComponent(std::move(info));
}
zx_status_t TestHarnessImpl::SetupFakeSessionAgent() {
auto interception_retval = interceptor_.InterceptURL(
kSessionAgentFakeInterceptionUrl, kSessionAgentFakeInterceptionCmx,
[this](fuchsia::sys::StartupInfo startup_info,
std::unique_ptr<sys::testing::InterceptedComponent>
intercepted_component) {
intercepted_session_agent_info_.component_context =
component::StartupContext::CreateFrom(std::move(startup_info));
intercepted_session_agent_info_.agent_driver.reset(
new ::modular::AgentDriver<InterceptedSessionAgent>(
intercepted_session_agent_info_.component_context.get(),
[] {}));
intercepted_session_agent_info_.intercepted_component =
std::move(intercepted_component);
FlushBufferedSessionAgentServices();
});
if (!interception_retval) {
return ZX_ERR_INVALID_ARGS;
}
return ZX_OK;
}
fuchsia::modular::session::AppConfig MakeAppConfigWithUrl(std::string url) {
fuchsia::modular::session::AppConfig app_config;
app_config.set_url(url);
return app_config;
}
fuchsia::modular::session::SessionShellMapEntry
MakeDefaultSessionShellMapEntry() {
fuchsia::modular::session::SessionShellConfig config;
config.mutable_app_config()->set_url(kSessionShellDefaultUrl);
fuchsia::modular::session::SessionShellMapEntry entry;
entry.set_name("");
entry.set_config(std::move(config));
return entry;
}
// static
std::unique_ptr<vfs::PseudoDir> TestHarnessImpl::MakeBasemgrConfigDir(
const fuchsia::modular::testing::TestHarnessSpec& const_spec) {
fuchsia::modular::testing::TestHarnessSpec spec;
const_spec.Clone(&spec);
auto* basemgr_config = spec.mutable_basemgr_config();
// 1. Give base & story shell a default.
if (!basemgr_config->has_base_shell() ||
!basemgr_config->mutable_base_shell()->has_app_config()) {
basemgr_config->mutable_base_shell()->set_app_config(
MakeAppConfigWithUrl(kBaseShellDefaultUrl));
}
if (!basemgr_config->has_story_shell() ||
!basemgr_config->mutable_story_shell()->has_app_config()) {
basemgr_config->mutable_story_shell()->set_app_config(
MakeAppConfigWithUrl(kStoryShellDefaultUrl));
}
// 1.1. Give session shell a default if not specified.
if (!basemgr_config->has_session_shell_map() ||
basemgr_config->session_shell_map().size() == 0) {
basemgr_config->mutable_session_shell_map()->push_back(
MakeDefaultSessionShellMapEntry());
}
auto* first_session_shell_entry =
&basemgr_config->mutable_session_shell_map()->at(0);
if (!first_session_shell_entry->has_config() ||
!first_session_shell_entry->config().has_app_config() ||
!first_session_shell_entry->config().app_config().has_url()) {
first_session_shell_entry->mutable_config()->mutable_app_config()->set_url(
kSessionShellDefaultUrl);
}
// 2. Configure a session agent and intercept/mock it for its capabilities.
std::vector<std::string> sessionmgr_args;
auto* sessionmgr_config =
spec.mutable_sessionmgr_config(); // initialize if empty.
auto* session_agents =
sessionmgr_config->mutable_session_agents(); // initialize if empty.
session_agents->push_back(kSessionAgentFakeInterceptionUrl);
// 3. Write sessionmgr and basemgr configs into a single modular config
// json object, as described in //peridot/docs/modular/guide/config.md
std::string basemgr_json;
std::string sessionmgr_json;
XdrWrite(&basemgr_json, basemgr_config, XdrBasemgrConfig);
XdrWrite(&sessionmgr_json, sessionmgr_config, XdrSessionmgrConfig);
std::string modular_config_json =
fxl::Substitute(R"({
"$0": $1,
"$2": $3
})",
modular_config::kBasemgrConfigName, basemgr_json,
modular_config::kSessionmgrConfigName, sessionmgr_json);
return MakeFilePathWithContents(modular_config::kStartupConfigFilePath,
modular_config_json);
}
fuchsia::modular::testing::InterceptedComponentPtr
TestHarnessImpl::AddInterceptedComponentBinding(
std::unique_ptr<sys::testing::InterceptedComponent> intercepted_component) {
fuchsia::modular::testing::InterceptedComponentPtr ptr;
auto impl = std::make_unique<InterceptedComponentImpl>(
std::move(intercepted_component), ptr.NewRequest());
// Hold on to |impl|.
// Automatically remove/destroy |impl| if its associated binding closes.
auto key = impl.get();
impl->set_remove_handler(
[this, key] { intercepted_component_impls_.erase(key); });
intercepted_component_impls_[key] = std::move(impl);
return ptr;
}
std::string GetCmxAsString(
const fuchsia::modular::testing::InterceptSpec& intercept_spec) {
std::string cmx_str = "";
if (intercept_spec.has_extra_cmx_contents()) {
if (!fsl::StringFromVmo(intercept_spec.extra_cmx_contents(), &cmx_str)) {
// Not returning |cmx_str| since fsl::StringFromVmo doesn't guarantee that
// |cmx_str| will be untouched on failure.
return "";
}
}
return cmx_str;
}
zx_status_t TestHarnessImpl::SetupComponentInterception() {
if (!spec_.has_components_to_intercept()) {
return ZX_OK;
}
for (const auto& intercept_spec : spec_.components_to_intercept()) {
if (!interceptor_.InterceptURL(
intercept_spec.component_url(), GetCmxAsString(intercept_spec),
[this](fuchsia::sys::StartupInfo startup_info,
std::unique_ptr<sys::testing::InterceptedComponent>
intercepted_component) {
binding_.events().OnNewComponent(
std::move(startup_info),
AddInterceptedComponentBinding(
std::move(intercepted_component)));
})) {
return ZX_ERR_INVALID_ARGS;
}
}
return ZX_OK;
}
void TestHarnessImpl::FlushBufferedSessionAgentServices() {
if (!intercepted_session_agent_info_.component_context) {
return;
}
for (auto&& req : intercepted_session_agent_info_.buffered_service_requests) {
intercepted_session_agent_info_.component_context
->ConnectToEnvironmentService(req.service_name,
std::move(req.service_request));
}
intercepted_session_agent_info_.buffered_service_requests.clear();
}
} // namespace modular::testing