blob: 258afe89864413ff9bea0c4ed02447d65f99ec67 [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.
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 BlackBoxTest to start Component Manager v2.
/// A fuchsia system is expected to have Component Manager v2 at this URL for
/// BlackBoxTest::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 BlackBoxTest 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
/// BlackBoxTest 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" flag is passed in.
///
/// Usage: /src/sys/component_manager/tests contains multiple tests such as
/// routing, base_resolver and shutdown which use this class.
pub struct BlackBoxTest {
/// 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 BlackBoxTest {
/// 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.
pub async fn default(root_component_url: &str) -> Result<Self, Error> {
Self::custom(COMPONENT_MANAGER_URL, root_component_url, vec![]).await
}
/// Creates a black box test from the given component manager and
/// root component URL. The supplied directory handles are given to the
/// started component manager. If an output file descriptor is supplied it
/// is attached to the component manager's output.
///
/// At the end of this function call, a component manager has been created
/// in a hermetic environment and its execution is halted.
pub async fn custom(
component_manager_url: &str,
root_component_url: &str,
dir_handles: Vec<(String, zx::Handle)>,
) -> Result<Self, 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_app = launch_component_manager(
launcher,
component_manager_url,
root_component_url,
dir_handles,
)
.await?;
let test = Self {
env,
component_manager_app,
component_manager_url: component_manager_url.to_string(),
root_component_url: root_component_url.to_string(),
label,
};
Ok(test)
}
/// 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
}
}
/// 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 debug flag. Blocks until the out directory of component
/// manager is set up.
async fn launch_component_manager(
launcher: LauncherProxy,
component_manager_url: &str,
root_component_url: &str,
dir_handles: Vec<(String, zx::Handle)>,
) -> 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);
}
// Start component manager, giving the debug flag and the root component URL.
let component_manager_app = launch_with_options(
&launcher,
component_manager_url.to_string(),
Some(vec![root_component_url.to_string(), "--debug".to_string()]),
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)
}