| // 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. | |
| use { | |
| crate::events::EventSource, | |
| anyhow::{Context as _, Error}, | |
| fidl_fuchsia_io::DirectoryProxy, | |
| fidl_fuchsia_sys::{ | |
| ComponentControllerEvent, EnvironmentControllerEvent, EnvironmentControllerProxy, | |
| EnvironmentMarker, EnvironmentOptions, LauncherProxy, | |
| }, | |
| fidl_fuchsia_sys2 as fsys, files_async, | |
| fuchsia_component::client::*, | |
| fuchsia_zircon as zx, | |
| futures::future, | |
| futures::stream::{StreamExt, TryStreamExt}, | |
| rand::random, | |
| std::fs::*, | |
| std::path::PathBuf, | |
| }; | |
| /// This static string is used by OpaqueTest to start Component Manager v2. | |
| /// A fuchsia system is expected to have Component Manager v2 at this URL for | |
| /// OpaqueTest::default to work correctly. | |
| pub static COMPONENT_MANAGER_URL: &str = | |
| "fuchsia-pkg://fuchsia.com/component-manager#meta/component_manager.cmx"; | |
| /// This structure contains all elements responsible for a black-box test | |
| /// of Component Manager v2. If this object is dropped, the component manager | |
| /// will be killed, ending the test. | |
| /// | |
| /// Any component using OpaqueTest must satisfy the following minimal requirements | |
| /// in its manifest: | |
| /// | |
| /// "sandbox": { | |
| /// "features": [ | |
| /// "hub" | |
| /// ], | |
| /// "services": [ | |
| /// "fuchsia.process.Launcher", | |
| /// "fuchsia.sys.Launcher", | |
| /// "fuchsia.sys.Environment", | |
| /// "fuchsia.logger.LogSink" | |
| /// ] | |
| /// } | |
| /// | |
| /// Component manager requires the process.Launcher and LogSink services, so | |
| /// OpaqueTest passes them into component manager from the parent component. | |
| /// | |
| /// The Environment service is required to ensure hermeticity. This is important | |
| /// for integration tests, where a single component runs multiple tests simultaneously. | |
| /// There will be multiple component managers and they must all run in isolated environments. | |
| /// | |
| /// The hub is required to access the Events FIDL service exposed by | |
| /// component manager when the debug mode is enabled in component manager config. | |
| /// | |
| /// Usage: /src/sys/component_manager/tests contains multiple tests such as | |
| /// routing, base_resolver and shutdown which use this class. | |
| pub struct OpaqueTest { | |
| /// The environment under which component manager runs | |
| pub env: EnvironmentControllerProxy, | |
| /// The app that handles component manager | |
| pub component_manager_app: App, | |
| /// URL for the component manager used for this test | |
| pub component_manager_url: String, | |
| /// URL of the root component given to this test's component manager | |
| pub root_component_url: String, | |
| /// Label give to this test's component manager in the hub | |
| pub label: String, | |
| } | |
| impl OpaqueTest { | |
| /// Creates a black box test with the default component manager URL and no | |
| /// additional directory handles that are passed in to component manager. | |
| /// The output of component manager is not redirected. | |
| /// | |
| /// At the end of this function call, a component manager has been created | |
| /// in a hermetic environment and its execution is halted. | |
| /// | |
| /// For greater control over test setup parameters, use [`OpaqueTestBuilder`]. | |
| pub async fn default(root_component_url: &str) -> Result<Self, Error> { | |
| OpaqueTestBuilder::new(root_component_url).build().await | |
| } | |
| /// The path from Hub v1 to component manager. | |
| pub fn get_component_manager_path(&self) -> PathBuf { | |
| find_component_manager_in_hub(&self.component_manager_url, &self.label) | |
| } | |
| /// The path from Hub v1 to Hub v2 | |
| /// To get this path, append "out/hub" to the component manager path. | |
| pub fn get_hub_v2_path(&self) -> PathBuf { | |
| let path = self.get_component_manager_path(); | |
| path.join("out/hub") | |
| } | |
| /// The events FIDL service connected to component manager. | |
| /// An integration test can use this service to halt tasks of component manager | |
| /// at various points during its execution. | |
| pub async fn connect_to_event_source(&self) -> Result<EventSource, Error> { | |
| let path = self.get_component_manager_path(); | |
| connect_to_event_source(&path).await | |
| } | |
| } | |
| /// Configures a [`OpaqueTest`]. | |
| pub struct OpaqueTestBuilder { | |
| config: String, | |
| root_component_url: String, | |
| component_manager_url: Option<String>, | |
| dir_handles: Vec<(String, zx::Handle)>, | |
| runtime_config: Option<String>, | |
| extra_args: Vec<String>, | |
| } | |
| impl OpaqueTestBuilder { | |
| /// Creates a new OpaqueTestBuilder instance configured to launch the root component | |
| /// identified by the URL `root_component_url`. | |
| pub fn new(root_component_url: impl Into<String>) -> Self { | |
| OpaqueTestBuilder { | |
| config: "/pkg/data/component_manager_config".to_string(), | |
| root_component_url: root_component_url.into(), | |
| component_manager_url: None, | |
| dir_handles: Vec::new(), | |
| runtime_config: None, | |
| extra_args: Vec::new(), | |
| } | |
| } | |
| /// Changes the URL of the component manager used for the black box test from the | |
| /// default [`COMPONENT_MANAGER_URL`] to `url`. | |
| pub fn component_manager_url(mut self, url: impl Into<String>) -> Self { | |
| self.component_manager_url = Some(url.into()); | |
| self | |
| } | |
| /// Adds a directory `handle` to be installed in the component manager's namespace at | |
| /// the given `path`. If an output file descriptor is supplied, it is attached to the | |
| /// component manager's output. | |
| pub fn add_dir_handle(mut self, path: impl Into<String>, handle: zx::Handle) -> Self { | |
| self.dir_handles.push((path.into(), handle)); | |
| self | |
| } | |
| pub fn config(mut self, config: impl Into<String>) -> Self { | |
| self.config = config.into(); | |
| self | |
| } | |
| /// Sets the path to the configuration file for component manager. | |
| pub fn runtime_config(mut self, runtime_config: impl Into<String>) -> Self { | |
| self.runtime_config = Some(runtime_config.into()); | |
| self | |
| } | |
| /// Extends the set of directory handles to include those in `handles`. If an output file | |
| /// descriptor is supplied, it is attached to the component manager's output. | |
| pub fn extend_dir_handles( | |
| mut self, | |
| handles: impl IntoIterator<Item = (String, zx::Handle)>, | |
| ) -> Self { | |
| self.dir_handles.extend(handles); | |
| self | |
| } | |
| /// Adds an extra parameter to pass to the started component manager. | |
| pub fn add_extra_arg(mut self, arg: impl Into<String>) -> Self { | |
| self.extra_args.push(arg.into()); | |
| self | |
| } | |
| /// Builds a OpaqueTest. Upon success, a component manager has been created | |
| /// in a hermetic environment and its execution is halted. | |
| pub async fn build(self) -> Result<OpaqueTest, Error> { | |
| // Use a random integer to identify this component manager | |
| let random_num = random::<u32>(); | |
| let label = format!("test_{}", random_num); | |
| let (env, launcher) = create_isolated_environment(&label).await?; | |
| let component_manager_url = | |
| self.component_manager_url.unwrap_or_else(|| COMPONENT_MANAGER_URL.to_string()); | |
| let component_manager_app = launch_component_manager( | |
| launcher, | |
| &component_manager_url, | |
| &self.config, | |
| &self.root_component_url, | |
| self.dir_handles, | |
| self.runtime_config, | |
| self.extra_args, | |
| ) | |
| .await?; | |
| Ok(OpaqueTest { | |
| env, | |
| component_manager_app, | |
| component_manager_url, | |
| root_component_url: self.root_component_url, | |
| label, | |
| }) | |
| } | |
| } | |
| /// Creates an isolated environment for component manager inside this component. | |
| /// This isolated environment will be given the provided label so as to identify | |
| /// it uniquely in the hub. | |
| async fn create_isolated_environment( | |
| label: &str, | |
| ) -> Result<(EnvironmentControllerProxy, LauncherProxy), Error> { | |
| let env = connect_to_service::<EnvironmentMarker>() | |
| .context("could not connect to current environment")?; | |
| let (new_env, new_env_server_end) = | |
| fidl::endpoints::create_proxy().context("could not create proxy")?; | |
| let (controller, controller_server_end) = | |
| fidl::endpoints::create_proxy().context("could not create proxy")?; | |
| let (launcher, launcher_server_end) = | |
| fidl::endpoints::create_proxy().context("could not create proxy")?; | |
| // Component manager will run with these environment options | |
| let mut env_options = EnvironmentOptions { | |
| // This flag ensures that component manager gets all its required | |
| // services from the parent component | |
| inherit_parent_services: true, | |
| use_parent_runners: true, | |
| kill_on_oom: false, | |
| delete_storage_on_death: true, | |
| }; | |
| env.create_nested_environment( | |
| new_env_server_end, | |
| controller_server_end, | |
| label, | |
| None, | |
| &mut env_options, | |
| ) | |
| .context("could not create isolated environment")?; | |
| // Wait for the environment to be setup. | |
| // There is only one type of event (OnCreated) in this protocol, so just get the next one. | |
| let EnvironmentControllerEvent::OnCreated {} = | |
| controller.take_event_stream().next().await.unwrap().unwrap(); | |
| // Get the launcher for this environment so it can be used to start component manager. | |
| new_env | |
| .get_launcher(launcher_server_end) | |
| .context("could not get isolated environment launcher")?; | |
| Ok((controller, launcher)) | |
| } | |
| /// Use the provided launcher from the isolated environment to startup component manager. | |
| /// Attach any provided directory handles. Component manager is provided the URL of the | |
| /// v2 component to start and the config. Blocks until the out directory of component | |
| /// manager is set up. | |
| async fn launch_component_manager( | |
| launcher: LauncherProxy, | |
| component_manager_url: &str, | |
| config: &str, | |
| root_component_url: &str, | |
| dir_handles: Vec<(String, zx::Handle)>, | |
| runtime_config: Option<String>, | |
| extra_args: Vec<String>, | |
| ) -> Result<App, Error> { | |
| let mut options = LaunchOptions::new(); | |
| // Add in any provided directory handles to component manager's namespace | |
| for dir in dir_handles { | |
| options.add_handle_to_namespace(dir.0, dir.1); | |
| } | |
| let mut args = vec!["--config".to_string(), config.to_string(), root_component_url.to_string()]; | |
| if let Some(runtime_config) = runtime_config { | |
| args.extend(vec!["--runtime-config".to_string(), runtime_config]); | |
| } | |
| args.extend(extra_args); | |
| let component_manager_app = | |
| launch_with_options(&launcher, component_manager_url.to_string(), Some(args), options) | |
| .context("could not launch component manager")?; | |
| // Wait for component manager to setup the out directory | |
| let event_stream = component_manager_app.controller().take_event_stream(); | |
| event_stream | |
| .try_filter_map(|event| { | |
| let event = match event { | |
| ComponentControllerEvent::OnDirectoryReady {} => Some(event), | |
| _ => None, | |
| }; | |
| future::ready(Ok(event)) | |
| }) | |
| .next() | |
| .await; | |
| Ok(component_manager_app) | |
| } | |
| /// Use the path to component manager's hub to find the events service | |
| /// and connect to it | |
| async fn connect_to_event_source(component_manager_path: &PathBuf) -> Result<EventSource, Error> { | |
| let path_to_svc = component_manager_path.join("out/svc"); | |
| let path_to_svc = path_to_svc.to_str().expect("found invalid chars"); | |
| let proxy = connect_to_service_at::<fsys::BlockingEventSourceMarker>(path_to_svc) | |
| .context("could not connect to BlockingEventSource service")?; | |
| Ok(EventSource::from_proxy(proxy)) | |
| } | |
| /// Explore the hub to find the component manager running in the environment | |
| /// marked by the provided label. | |
| fn find_component_manager_in_hub(component_manager_url: &str, label: &str) -> PathBuf { | |
| let path_to_env = format!("/hub/r/{}", label); | |
| // Get the id for the environment | |
| let dir: Vec<DirEntry> = read_dir(path_to_env) | |
| // TODO convert this to an error instead of causing a panic | |
| .expect("could not open nested environment in the hub") | |
| .map(|x| x.expect("entry unreadable")) | |
| .collect(); | |
| // There can only be one environment with this label. | |
| // So there should be only one id. | |
| assert_eq!(dir.len(), 1); | |
| // Since the component manager URL can be custom, we need to extract the | |
| // component name. | |
| let component_name = component_manager_url | |
| .split("/") | |
| .last() | |
| .expect("the URL for component manager must have at least one '/' character"); | |
| let path_to_cm = dir[0].path().join("c").join(component_name); | |
| // Get the id for component manager | |
| let dir: Vec<DirEntry> = read_dir(path_to_cm) | |
| // TOOD convert this into an error instead of causing a panic | |
| .expect("could not open component manager in the hub") | |
| .map(|x| x.expect("entry unreadable")) | |
| .collect(); | |
| // There can only be one component manager inside this environment. | |
| // So there should be only one id. | |
| assert_eq!(dir.len(), 1); | |
| dir[0].path() | |
| } | |
| /// Convenience method to lists the contents of a directory proxy as a sorted vector of strings. | |
| pub async fn list_directory(root_proxy: &DirectoryProxy) -> Result<Vec<String>, Error> { | |
| let entries = files_async::readdir(&root_proxy).await?; | |
| let mut items = entries.iter().map(|entry| entry.name.clone()).collect::<Vec<String>>(); | |
| items.sort(); | |
| Ok(items) | |
| } |