| // 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 "src/modular/lib/modular_test_harness/cpp/test_harness_impl.h" |
| |
| #include <dirent.h> |
| #include <fuchsia/cobalt/cpp/fidl.h> |
| #include <fuchsia/intl/cpp/fidl.h> |
| #include <fuchsia/stash/cpp/fidl.h> |
| #include <lib/fdio/directory.h> |
| #include <lib/syslog/cpp/macros.h> |
| #include <lib/vfs/cpp/pseudo_dir.h> |
| #include <lib/vfs/cpp/pseudo_file.h> |
| #include <zircon/status.h> |
| |
| #include <fbl/unique_fd.h> |
| #include <src/lib/files/path.h> |
| #include <src/modular/lib/modular_config/modular_config.h> |
| #include <src/modular/lib/modular_config/modular_config_constants.h> |
| #include <src/modular/lib/modular_config/modular_config_xdr.h> |
| #include <src/modular/lib/pseudo_dir/pseudo_dir_utils.h> |
| |
| #include "src/lib/fsl/io/fd.h" |
| #include "src/lib/fsl/vmo/strings.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 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 kSessionAgentFakeInterceptionCmx[] = R"( |
| { |
| "sandbox": { |
| "services": [ |
| "fuchsia.element.Manager", |
| "fuchsia.modular.ComponentContext", |
| "fuchsia.modular.PuppetMaster" |
| ] |
| } |
| } |
| )"; |
| |
| } // 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] { |
| if (!binding_.is_bound()) |
| return; |
| binding_.events().OnKill(); |
| }); |
| } |
| |
| 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(sys::ComponentContext* context) {} |
| |
| // Called by AgentDriver. |
| void Connect(fidl::InterfaceRequest<fuchsia::sys::ServiceProvider> outgoing_services) {} |
| |
| // Called by AgentDriver. |
| void Terminate(const fit::function<void()>& done) { done(); } |
| }; |
| |
| TestHarnessImpl::TestHarnessImpl(const fuchsia::sys::EnvironmentPtr& parent_env, |
| fit::function<void()> on_exit) |
| : parent_env_(parent_env), |
| binding_(this), |
| on_exit_(std::move(on_exit)), |
| interceptor_(sys::testing::ComponentInterceptor::CreateWithEnvironmentLoader(parent_env_)) {} |
| |
| void TestHarnessImpl::Bind(fidl::InterfaceRequest<fuchsia::modular::testing::TestHarness> request) { |
| binding_.Bind(std::move(request)); |
| } |
| |
| TestHarnessImpl::~TestHarnessImpl() = default; |
| |
| 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::kElementManager: { |
| BufferSessionAgentService(std::move(service.element_manager())); |
| break; |
| } |
| case fuchsia::modular::testing::ModularService::Tag::Invalid: |
| default: |
| assert(false && "should not have improperly constructed ModularService"); |
| return; |
| } |
| } |
| |
| void TestHarnessImpl::ConnectToEnvironmentService(std::string service_name, zx::channel request) { |
| enclosing_env_->ConnectToService(service_name, std::move(request)); |
| } |
| |
| void TestHarnessImpl::Terminate() { |
| // if basemgr is alive, send it a termination signal. |
| if (basemgr_lifecycle_) { |
| basemgr_lifecycle_->Terminate(); |
| // When basemgr exits, |basemgr_ctrl_| will be notified and will |on_exit_|. |
| } else { |
| on_exit_(); |
| } |
| } |
| |
| bool TestHarnessImpl::CloseBindingIfError(zx_status_t status) { |
| if (status != ZX_OK) { |
| FX_LOGS(ERROR) << "Destroying TestHarness because of error: " << zx_status_get_string(status); |
| binding_.Close(status); |
| // destory |enclosing_env_| should kill all processes. |
| enclosing_env_.reset(); |
| on_exit_(); |
| return true; |
| } |
| return false; |
| } |
| |
| std::string MakeTestHarnessEnvironmentName(std::string user_env_suffix) { |
| // Apply a random suffix to the environment name so that multiple hermetic |
| // test harness environments may coexist under the same parent env. |
| // If user_env_suffix is provided, the suffix is concatenated to 22 chars |
| // such that "mth_#####_{user_env_suffix}" is 32 chars or less. |
| uint32_t random_env_suffix = 0; |
| zx_cprng_draw(&random_env_suffix, sizeof(random_env_suffix)); |
| // Limit suffix to 5 digits because of 32 char max on the entire name. |
| random_env_suffix %= 100000; |
| std::string env_name = fxl::Substitute("mth_$0", std::to_string(random_env_suffix)); |
| if (!user_env_suffix.empty()) { |
| env_name.append("_" + user_env_suffix); |
| } |
| return env_name; |
| } |
| |
| zx_status_t TestHarnessImpl::PopulateEnvServices(sys::testing::EnvironmentServices* env_services) { |
| // The default set of component-provided services are all basemgr's hard |
| // dependencies. A map of service name => component URL providing the service. |
| std::map<std::string, std::string> default_svcs = { |
| {fuchsia::intl::PropertyProvider::Name_, |
| "fuchsia-pkg://fuchsia.com/intl-services-small#meta/intl_services.cmx"}, |
| {fuchsia::settings::Intl::Name_, |
| "fuchsia-pkg://fuchsia.com/setui_service#meta/setui_service.cmx"}, |
| {fuchsia::stash::Store::Name_, "fuchsia-pkg://fuchsia.com/stash#meta/stash.cmx"}, |
| {fuchsia::cobalt::LoggerFactory::Name_, |
| "fuchsia-pkg://fuchsia.com/mock_cobalt#meta/mock_cobalt.cmx"}, |
| {fuchsia::devicesettings::DeviceSettingsManager::Name_, |
| "fuchsia-pkg://fuchsia.com/device_settings_manager#meta/" |
| "device_settings_manager.cmx"}}; |
| |
| std::set<std::string> added_svcs; |
| |
| // 1. Allow services to be inherited from parent environment. |
| if (spec_.has_env_services_to_inherit()) { |
| for (auto& svc_name : spec_.env_services_to_inherit()) { |
| added_svcs.insert(svc_name); |
| env_services->AllowParentService(svc_name); |
| } |
| } |
| |
| // 2. Inject component-provided services. |
| if (auto retval = PopulateEnvServicesWithComponents(env_services, &added_svcs) != ZX_OK) { |
| return retval; |
| } |
| |
| // 3. Inject service_dir services. |
| if (auto retval = PopulateEnvServicesWithServiceDir(env_services, &added_svcs) != ZX_OK) { |
| return retval; |
| } |
| |
| // 4. Inject the remaining default component-provided services. |
| for (const auto& svc_component : default_svcs) { |
| if (added_svcs.find(svc_component.first) != added_svcs.end()) { |
| continue; |
| } |
| fuchsia::sys::LaunchInfo launch_info; |
| launch_info.url = svc_component.second; |
| env_services->AddServiceWithLaunchInfo(std::move(launch_info), svc_component.first); |
| } |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t TestHarnessImpl::PopulateEnvServicesWithComponents( |
| sys::testing::EnvironmentServices* env_services, std::set<std::string>* added_svcs) { |
| // Wire up client-specified injected services, and remove them from the |
| // default injected services. |
| if (!spec_.has_env_services() || !spec_.env_services().has_services_from_components()) { |
| return ZX_OK; |
| } |
| for (const auto& svc : spec_.env_services().services_from_components()) { |
| if (added_svcs->find(svc.name) != added_svcs->end()) { |
| FX_LOGS(ERROR) << svc.name |
| << " has already been injected into the environment, " |
| "cannot add twice."; |
| return ZX_ERR_ALREADY_EXISTS; |
| } |
| added_svcs->insert(svc.name); |
| |
| fuchsia::sys::LaunchInfo launch_info; |
| launch_info.url = svc.url; |
| env_services->AddServiceWithLaunchInfo(std::move(launch_info), svc.name); |
| } |
| |
| return ZX_OK; |
| } |
| |
| std::vector<std::string> GetDirListing(fuchsia::io::Directory* dir) { |
| // Make a clone of |dir| since translating to a POSIX fd is destructive. |
| fuchsia::io::NodePtr dir_copy; |
| dir->Clone(fuchsia::io::OpenFlags::RIGHT_READABLE, dir_copy.NewRequest()); |
| |
| std::vector<std::string> svcs; |
| DIR* fd = fdopendir(fsl::OpenChannelAsFileDescriptor(dir_copy.Unbind().TakeChannel()).release()); |
| FX_CHECK(fd != nullptr); |
| |
| struct dirent* dp = nullptr; |
| while ((dp = readdir(fd)) != nullptr) { |
| if (dp->d_name[0] != '.') { |
| svcs.push_back(dp->d_name); |
| } |
| } |
| |
| closedir(fd); |
| return svcs; |
| } |
| |
| zx_status_t TestHarnessImpl::PopulateEnvServicesWithServiceDir( |
| sys::testing::EnvironmentServices* env_services, std::set<std::string>* added_svcs) { |
| if (!spec_.has_env_services() || !spec_.env_services().has_service_dir() || |
| !spec_.env_services().service_dir()) { |
| return ZX_OK; |
| } |
| |
| fuchsia::io::DirectoryPtr dir; |
| dir.Bind(std::move(*spec_.mutable_env_services()->mutable_service_dir())); |
| for (auto& svc_name : GetDirListing(dir.get())) { |
| if (added_svcs->find(svc_name) != added_svcs->end()) { |
| FX_LOGS(ERROR) << svc_name << " is already injected into the environment, cannot add twice."; |
| return ZX_ERR_ALREADY_EXISTS; |
| } |
| env_services->AddService( |
| std::make_unique<vfs::Service>( |
| [this, svc_name](zx::channel request, async_dispatcher_t* dispatcher) { |
| FX_CHECK(env_service_dir_->Connect(svc_name, std::move(request)) == ZX_OK); |
| }), |
| svc_name); |
| added_svcs->insert(svc_name); |
| } |
| |
| env_service_dir_ = std::make_unique<sys::ServiceDirectory>(std::move(dir)); |
| return ZX_OK; |
| } |
| |
| void TestHarnessImpl::ParseConfig(std::string config, ParseConfigCallback callback) { |
| auto parsed = modular::ParseConfig(config).take_value(); |
| callback(std::move(*parsed.mutable_basemgr_config()), |
| std::move(*parsed.mutable_sessionmgr_config())); |
| } |
| |
| 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_); |
| |
| if (CloseBindingIfError(PopulateEnvServices(env_services.get()))) { |
| return; |
| } |
| |
| fuchsia::sys::EnvironmentOptions env_options; |
| env_options.delete_storage_on_death = true; |
| |
| std::string user_env_suffix = ""; |
| if (spec_.has_environment_suffix()) { |
| user_env_suffix = spec_.environment_suffix(); |
| } |
| |
| enclosing_env_ = |
| sys::testing::EnclosingEnvironment::Create(MakeTestHarnessEnvironmentName(user_env_suffix), |
| parent_env_, std::move(env_services), env_options); |
| |
| zx::channel client; |
| zx::channel request; |
| FX_CHECK(zx::channel::create(0u, &client, &request) == ZX_OK); |
| basemgr_config_dir_ = MakeBasemgrConfigDir(spec_); |
| basemgr_config_dir_->Serve(fuchsia::io::OpenFlags::RIGHT_READABLE, std::move(request)); |
| |
| fuchsia::io::DirectoryPtr basemgr_svc_dir; |
| fuchsia::sys::LaunchInfo launch_info; |
| launch_info.url = kBasemgrUrl; |
| launch_info.directory_request = basemgr_svc_dir.NewRequest().TakeChannel(); |
| launch_info.flat_namespace = std::make_unique<fuchsia::sys::FlatNamespace>(); |
| launch_info.flat_namespace->paths.push_back(modular_config::kOverriddenConfigDir); |
| launch_info.flat_namespace->directories.push_back(std::move(client)); |
| |
| sys::ServiceDirectory basemgr_svc(basemgr_svc_dir.Unbind().TakeChannel()); |
| basemgr_lifecycle_ = basemgr_svc.Connect<fuchsia::modular::Lifecycle>(); |
| |
| basemgr_ctrl_ = enclosing_env_->CreateComponent(std::move(launch_info)); |
| basemgr_ctrl_.set_error_handler([this](zx_status_t err) { on_exit_(); }); |
| } |
| |
| zx::channel TakeSvcFromFlatNamespace(fuchsia::sys::FlatNamespace* flat_namespace) { |
| for (size_t i = 0; i < flat_namespace->paths.size(); i++) { |
| if (flat_namespace->paths[i] == "/svc") { |
| return std::move(flat_namespace->directories[i]); |
| } |
| } |
| FX_CHECK(false) << "Could not find /svc in component namespace."; |
| return zx::channel(); |
| } |
| |
| 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 = std::make_unique<sys::ComponentContext>( |
| std::make_shared<sys::ServiceDirectory>( |
| TakeSvcFromFlatNamespace(&startup_info.flat_namespace)), |
| std::move(startup_info.launch_info.directory_request)); |
| intercepted_session_agent_info_.agent.reset(new ::modular::Agent( |
| intercepted_session_agent_info_.component_context->outgoing(), |
| [this] { intercepted_session_agent_info_.intercepted_component->Exit(0); })); |
| 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 story shell a default. |
| 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().empty()) { |
| 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, modular::XdrBasemgrConfig); |
| XdrWrite(&sessionmgr_json, sessionmgr_config, modular::XdrSessionmgrConfig); |
| |
| std::string modular_config_json = |
| fxl::Substitute(R"({ |
| "$0": $1, |
| "$2": $3 |
| })", |
| modular_config::kBasemgrConfigName, basemgr_json, |
| modular_config::kSessionmgrConfigName, sessionmgr_json); |
| |
| return modular::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->svc()->Connect( |
| req.service_name, std::move(req.service_request)); |
| } |
| intercepted_session_agent_info_.buffered_service_requests.clear(); |
| } |
| |
| } // namespace modular_testing |