blob: f385ddad56ae9ce0b460c492c0ade3f872e6bcb8 [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::{
builtin::runner::BuiltinRunnerFactory,
builtin_environment::{BuiltinEnvironment, BuiltinEnvironmentBuilder},
model::{
component::{ComponentInstance, InstanceState, StartReason},
error::ModelError,
hooks::HooksRegistration,
model::Model,
starter::Starter,
testing::{echo_service::*, mocks::*, out_dir::OutDir, test_helpers::*},
},
},
::routing::{
component_id_index::ComponentInstanceId,
component_instance::ComponentInstanceInterface,
config::{
AllowlistEntry, CapabilityAllowlistKey, ChildPolicyAllowlists, RuntimeConfig,
SecurityPolicy,
},
},
::routing_test_helpers::{generate_storage_path, RoutingTestModel, RoutingTestModelBuilder},
anyhow::anyhow,
async_trait::async_trait,
cm_moniker::InstancedRelativeMoniker,
cm_rust::*,
cm_types::Url,
fidl::{
self,
endpoints::{self, create_proxy, ClientEnd, Proxy, ServerEnd},
},
fidl_fidl_examples_routing_echo::{self as echo},
fidl_fuchsia_component as fcomponent, fidl_fuchsia_component_decl as fdecl,
fidl_fuchsia_component_runner as fcrunner, fidl_fuchsia_io as fio, fidl_fuchsia_sys2 as fsys,
fuchsia_inspect as inspect, fuchsia_zircon as zx,
futures::lock::Mutex,
futures::prelude::*,
moniker::{AbsoluteMoniker, AbsoluteMonikerBase, ChildMoniker, ChildMonikerBase},
std::{
collections::{HashMap, HashSet},
convert::{TryFrom, TryInto},
default::Default,
fs,
path::{Path, PathBuf},
sync::Arc,
},
tempfile::TempDir,
vfs::directory::entry::DirectoryEntry,
};
// TODO(https://fxbug.dev/61861): remove type aliases once the routing_test_helpers lib has a stable
// API.
pub type ExpectedResult = ::routing_test_helpers::ExpectedResult;
pub type CheckUse = ::routing_test_helpers::CheckUse;
/// Builder for setting up a new `RoutingTest` instance with a non-standard setup.
///
/// Use as follows:
///
/// ```
/// let universe = RoutingTestBuilder::new("root", components)
/// .add_hooks(...)
/// .add_outgoing_path(...)
/// .build();
/// ```
#[derive(Default)]
pub struct RoutingTestBuilder {
root_component: String,
components: Vec<(&'static str, ComponentDecl)>,
additional_hooks: Vec<HooksRegistration>,
outgoing_paths: HashMap<String, HashMap<CapabilityPath, Arc<dyn DirectoryEntry>>>,
builtin_runners: HashMap<CapabilityName, Arc<dyn BuiltinRunnerFactory>>,
mock_builtin_runners: HashSet<CapabilityName>,
namespace_capabilities: Vec<CapabilityDecl>,
builtin_capabilities: Vec<CapabilityDecl>,
component_id_index_path: Option<String>,
// Map of components that have a custom function serving the "outgoing" directory.
// Other functions will receive a stock directory generated by `RoutingTest`.
custom_outgoing_host_fns: HashMap<String, HostFn>,
capability_policy: HashMap<CapabilityAllowlistKey, HashSet<AllowlistEntry>>,
debug_capability_policy: HashMap<CapabilityAllowlistKey, HashSet<(AbsoluteMoniker, String)>>,
child_policy: ChildPolicyAllowlists,
reboot_on_terminate_enabled: bool,
}
impl RoutingTestBuilder {
pub fn new(root_component: &str, components: Vec<(&'static str, ComponentDecl)>) -> Self {
RoutingTestBuilder {
root_component: root_component.to_string(),
components,
..Default::default()
}
}
pub fn add_hooks(mut self, mut hooks: Vec<HooksRegistration>) -> Self {
self.additional_hooks.append(&mut hooks);
self
}
/// Expose the given `DirectoryEntry` at the given path of the `component`'s outgoing
/// directory.
pub fn add_outgoing_path(
mut self,
component: &str,
path: CapabilityPath,
entry: Arc<dyn DirectoryEntry>,
) -> Self {
self.outgoing_paths
.entry(component.to_string())
.or_insert_with(|| HashMap::new())
.insert(path, entry);
self
}
/// Add the given runner as a "builtin runner", registered in the root's environment
/// under the given name.
pub fn add_builtin_runner(mut self, name: &str, runner: Arc<dyn BuiltinRunnerFactory>) -> Self {
self.builtin_runners.insert(name.into(), runner);
self
}
/// Request a custom outgoing directory host function.
pub fn set_component_outgoing_host_fn(mut self, component: &str, function: HostFn) -> Self {
self.custom_outgoing_host_fns.insert(component.to_string(), function);
self
}
pub fn set_namespace_capabilities(mut self, caps: Vec<CapabilityDecl>) -> Self {
self.namespace_capabilities = caps;
self
}
/// Set the list of built-in capabilities that are provided from component manager.
/// The `test_runner` capability is always provided.
pub fn set_builtin_capabilities(mut self, caps: Vec<CapabilityDecl>) -> Self {
self.builtin_capabilities = caps;
self
}
pub fn set_component_id_index_path(mut self, index_path: String) -> Self {
self.component_id_index_path = Some(index_path);
self
}
pub fn set_reboot_on_terminate_enabled(mut self, val: bool) -> Self {
self.reboot_on_terminate_enabled = val;
self
}
pub fn set_reboot_on_terminate_policy(mut self, allowlist: Vec<AllowlistEntry>) -> Self {
self.child_policy.reboot_on_terminate = allowlist;
self
}
/// Add a custom capability security policy to restrict routing of certain caps.
pub fn add_capability_policy(
mut self,
key: CapabilityAllowlistKey,
allowlist: HashSet<AllowlistEntry>,
) -> Self {
self.capability_policy.insert(key, allowlist);
self
}
/// Add a custom debug capability security policy to restrict routing of certain caps.
pub fn add_debug_capability_policy(
mut self,
key: CapabilityAllowlistKey,
allowlist: HashSet<(AbsoluteMoniker, String)>,
) -> Self {
self.debug_capability_policy.insert(key, allowlist);
self
}
pub async fn build(self) -> RoutingTest {
RoutingTest::from_builder(self).await
}
}
#[async_trait]
impl RoutingTestModelBuilder for RoutingTestBuilder {
type Model = RoutingTest;
fn new(root_component: &str, components: Vec<(&'static str, ComponentDecl)>) -> Self {
Self::new(root_component, components)
}
fn set_namespace_capabilities(&mut self, caps: Vec<CapabilityDecl>) {
self.namespace_capabilities = caps;
}
fn set_builtin_capabilities(&mut self, caps: Vec<CapabilityDecl>) {
self.builtin_capabilities = caps;
}
fn register_mock_builtin_runner(&mut self, runner: &str) {
self.mock_builtin_runners.insert(runner.into());
}
fn add_capability_policy(
&mut self,
key: CapabilityAllowlistKey,
allowlist: HashSet<AllowlistEntry>,
) {
self.capability_policy.insert(key, allowlist);
}
fn add_debug_capability_policy(
&mut self,
key: CapabilityAllowlistKey,
allowlist: HashSet<(AbsoluteMoniker, String)>,
) {
self.debug_capability_policy.insert(key, allowlist);
}
fn set_component_id_index_path(&mut self, index_path: String) {
self.component_id_index_path = Some(index_path);
}
async fn build(self) -> RoutingTest {
self.build().await
}
}
/// A test for capability routing.
///
/// All string arguments are referring to component names, not URLs, ex: "a", not "test:///a" or
/// "test:///a_resolved".
pub struct RoutingTest {
components: Vec<(&'static str, ComponentDecl)>,
pub model: Arc<Model>,
pub builtin_environment: BuiltinEnvironment,
_echo_service: Arc<EchoService>,
pub mock_runner: Arc<MockRunner>,
test_dir: TempDir,
pub test_dir_proxy: fio::DirectoryProxy,
root_component_name: String,
}
impl RoutingTest {
/// Initializes a new RoutingTest with a default setup.
pub async fn new(root_component: &str, components: Vec<(&'static str, ComponentDecl)>) -> Self {
RoutingTestBuilder::new(root_component, components).build().await
}
/// Construct a new `RoutingTest` from the given builder.
async fn from_builder(mut builder: RoutingTestBuilder) -> Self {
let mock_runner = Arc::new(MockRunner::new());
let test_dir = TempDir::new_in("/tmp").expect("failed to create temp directory");
// Create a directory for the components, starting with a single static file
// "foo/hippo" in it.
let test_dir_proxy = io_util::open_directory_in_namespace(
test_dir.path().to_str().unwrap(),
io_util::OPEN_RIGHT_READABLE | io_util::OPEN_RIGHT_WRITABLE,
)
.expect("failed to open temp directory");
capability_util::create_static_file(&test_dir_proxy, Path::new("foo/hippo"), "hello")
.await
.expect("could not create test file");
// Create and populate an outgoing directory for each component.
let mut mock_resolver = MockResolver::new();
for (name, decl) in &builder.components {
let host_fn = match builder.custom_outgoing_host_fns.remove(*name) {
// If a custom outgoing HostFn was provided, use that.
Some(host_fn) => host_fn,
// Otherwise, create a default HostFn filled with sample data.
None => Self::build_outgoing_dir(
decl,
&test_dir_proxy,
builder.outgoing_paths.remove(name as &str).unwrap_or_else(|| HashMap::new()),
)
.host_fn(),
};
mock_runner.add_host_fn(&format!("test:///{}_resolved", name), host_fn);
mock_resolver.add_component(name, decl.clone());
}
let echo_service = Arc::new(EchoService::new());
// Add the `test_runner` capability as a built-in.
builder.builtin_capabilities.push(CapabilityDecl::Runner(RunnerDecl {
name: TEST_RUNNER_NAME.into(),
source_path: None,
}));
let config = RuntimeConfig {
namespace_capabilities: builder.namespace_capabilities,
builtin_capabilities: builder.builtin_capabilities,
root_component_url: Some(
Url::new(format!("test:///{}", builder.root_component)).unwrap(),
),
security_policy: SecurityPolicy {
capability_policy: builder.capability_policy,
debug_capability_policy: builder.debug_capability_policy,
child_policy: builder.child_policy,
..Default::default()
},
component_id_index_path: builder.component_id_index_path,
reboot_on_terminate_enabled: builder.reboot_on_terminate_enabled,
..Default::default()
};
let inspector = inspect::Inspector::new();
let mut env_builder = BuiltinEnvironmentBuilder::new()
.set_inspector(inspector)
.set_runtime_config(config)
.add_resolver("test".to_string(), Box::new(mock_resolver))
.add_runner(TEST_RUNNER_NAME.into(), mock_runner.clone());
for name in builder.mock_builtin_runners.clone() {
env_builder = env_builder.add_runner(name, mock_runner.clone())
}
for (name, runner) in builder.builtin_runners.clone() {
env_builder = env_builder.add_runner(name, runner);
}
let builtin_environment =
env_builder.build().await.expect("builtin environment setup failed");
let model = builtin_environment.model.clone();
model.root().hooks.install(builder.additional_hooks.clone()).await;
model.root().hooks.install(echo_service.hooks()).await;
Self {
components: builder.components,
model,
builtin_environment,
_echo_service: echo_service,
mock_runner,
test_dir,
test_dir_proxy,
root_component_name: builder.root_component.clone(),
}
}
pub fn test_dir_path(&self) -> &Path {
self.test_dir.path()
}
/// Set up the given OutDir, installing a set of files assumed to exist by
/// many tests:
/// - A file `/svc/foo` implementing `fidl.examples.routing.echo.Echo`.
/// - A static file `/svc/file`, containing the string "hippos" encoded as UTF-8.
pub fn install_default_out_files(dir: &mut OutDir) {
// Add "/svc/foo", providing an echo server.
dir.add_echo_service(CapabilityPath::try_from("/svc/foo").unwrap());
// Add "/svc/file", providing a read-only file.
dir.add_static_file(CapabilityPath::try_from("/svc/file").unwrap(), "hippos");
}
/// Creates a dynamic child `child_decl` in `moniker`'s `collection`.
pub async fn create_dynamic_child<'a>(
&'a self,
moniker: AbsoluteMoniker,
collection: &'a str,
decl: impl Into<ChildDecl>,
) {
self.create_dynamic_child_with_args(
moniker,
collection,
decl,
fcomponent::CreateChildArgs::EMPTY,
)
.await
}
/// Creates a dynamic child `child_decl` in `moniker`'s `collection`.
pub async fn create_dynamic_child_with_args<'a>(
&'a self,
moniker: AbsoluteMoniker,
collection: &'a str,
decl: impl Into<ChildDecl>,
args: fcomponent::CreateChildArgs,
) {
let component_name =
self.start_instance_and_wait_start(&moniker).await.expect("start instance failed");
let component_resolved_url = Self::resolved_url(&component_name);
Self::check_namespace(component_name, &self.mock_runner, self.components.clone()).await;
let namespace = self
.mock_runner
.get_namespace(&component_resolved_url)
.expect("could not find child namespace");
capability_util::call_create_child(&namespace, collection, decl.into(), args).await;
}
/// Deletes a dynamic child `child_decl` in `moniker`'s `collection`, waiting for destruction
/// to complete.
pub async fn destroy_dynamic_child<'a>(
&'a self,
moniker: AbsoluteMoniker,
collection: &'a str,
name: &'a str,
) {
let component = self.model.look_up(&moniker).await.expect("failed to look up component");
self.model
.start_instance(&component.abs_moniker, &StartReason::Eager)
.await
.expect("start instance failed");
let child_moniker = ChildMoniker::new(name.to_string(), Some(collection.to_string()));
let nf =
component.remove_dynamic_child(&child_moniker).await.expect("failed to remove child");
// Wait for destruction to fully complete.
nf.await.expect("failed to destroy child");
}
pub async fn bind_and_get_namespace(&self, moniker: AbsoluteMoniker) -> Arc<ManagedNamespace> {
let component_name = self.start_instance_and_wait_start(&moniker).await.unwrap();
let component_resolved_url = Self::resolved_url(&component_name);
let namespace = self
.mock_runner
.get_namespace(&component_resolved_url)
.expect("could not find namespace");
Self::check_namespace(component_name, &self.mock_runner, self.components.clone()).await;
namespace
}
/// Lists the contents of a storage directory.
pub async fn list_directory_in_storage(
&self,
subdir: Option<&str>,
relation: InstancedRelativeMoniker,
instance_id: Option<&ComponentInstanceId>,
relative_path: &str,
) -> Vec<String> {
let dir_path = generate_storage_path(subdir.map(|s| s.to_string()), &relation, instance_id);
let mut dir_path = dir_path.parent().unwrap().to_path_buf();
if !relative_path.is_empty() {
dir_path.push(relative_path);
}
if !dir_path.parent().is_none() {
let dir_proxy = io_util::open_directory(
&self.test_dir_proxy,
&dir_path,
io_util::OPEN_RIGHT_READABLE,
)
.expect("failed to open directory");
list_directory(&dir_proxy).await
} else {
list_directory(&self.test_dir_proxy).await
}
}
/// Lists the contents of a directory.
pub async fn list_directory(&self, path: &str) -> Vec<String> {
let dir_proxy = io_util::open_directory(
&self.test_dir_proxy,
Path::new(path),
io_util::OPEN_RIGHT_READABLE,
)
.expect("failed to open directory");
list_directory(&dir_proxy).await
}
/// check_namespace will ensure that the paths in `namespaces` for `component_name` match the use
/// declarations for the the component by the same name in `components`.
async fn check_namespace(
component_name: String,
runner: &MockRunner,
components: Vec<(&str, ComponentDecl)>,
) {
let (_, decl) = components
.into_iter()
.find(|(name, _)| name == &component_name)
.expect("component not in component decl list");
// Two services installed into the same dir will cause duplicates, so use a HashSet to remove
// them.
let expected_paths_hs: HashSet<String> = decl
.uses
.into_iter()
.filter_map(|u| match u {
UseDecl::Directory(d) => Some(d.target_path.to_string()),
UseDecl::Service(s) => Some(s.target_path.dirname),
UseDecl::Protocol(s) => Some(s.target_path.dirname),
UseDecl::Storage(s) => Some(s.target_path.to_string()),
UseDecl::Event(_) | UseDecl::EventStreamDeprecated(_) => None,
UseDecl::EventStream(_) => {
// TODO(fxbug.dev/81980): Route EventStream path
None
}
})
.collect();
let mut expected_paths = vec![];
expected_paths.extend(expected_paths_hs.into_iter());
// The "pkg" directory has been added to the mocked ResolvedComponent.
// We need to test that it exists.
expected_paths.push("/pkg".to_string());
// Get the paths in the component's namespace.
let mut actual_paths: Vec<String> = runner
.get_namespace(&format!("test:///{}_resolved", component_name))
.expect("component not in namespace")
.lock()
.await
.iter()
.map(|entry| entry.path.as_ref().unwrap().clone())
.collect();
expected_paths.sort_unstable();
actual_paths.sort_unstable();
assert_eq!(expected_paths, actual_paths);
}
/// Checks a `use /svc/fuchsia.component.Realm` declaration at `moniker` by calling
/// `BindChild`.
pub async fn check_use_realm(
&self,
moniker: AbsoluteMoniker,
bind_calls: Arc<Mutex<Vec<String>>>,
) {
let component_name =
self.start_instance_and_wait_start(&moniker).await.expect("bind instance failed");
let component_resolved_url = Self::resolved_url(&component_name);
let path = "/svc/fuchsia.component.Realm".try_into().unwrap();
Self::check_namespace(component_name, &self.mock_runner, self.components.clone()).await;
capability_util::call_realm_svc(
path,
&component_resolved_url,
&self.mock_runner.get_namespace(&component_resolved_url).unwrap(),
bind_calls.clone(),
)
.await;
}
/// Build an outgoing directory for the given component.
fn build_outgoing_dir(
decl: &ComponentDecl,
test_dir_proxy: &fio::DirectoryProxy,
mut outgoing_paths: HashMap<CapabilityPath, Arc<dyn DirectoryEntry>>,
) -> OutDir {
// if this decl is offering/exposing something from `Self`, let's host it
let mut out_dir = OutDir::new();
for capability in decl.capabilities.iter() {
let path = match capability {
CapabilityDecl::Protocol(ProtocolDecl { source_path, .. }) => source_path.as_ref(),
CapabilityDecl::Directory(DirectoryDecl { source_path, .. }) => {
source_path.as_ref()
}
_ => None,
};
if let Some(path) = path {
if outgoing_paths
.contains_key(&CapabilityPath::try_from(&format!("{}", path) as &str).unwrap())
{
// Client installed a custom DirectoryEntry at this path, don't serve the
// default.
continue;
}
}
match capability {
CapabilityDecl::Protocol(_) => {
Self::install_default_out_files(&mut out_dir);
}
CapabilityDecl::Directory(_) => out_dir.add_directory_proxy(test_dir_proxy),
CapabilityDecl::Storage(storage) => {
// Storage capabilities can have a source of "self", so there are situations we
// want to test where a storage capability is offered and used and there's no
// directory capability in the manifest, so we must host the directory structure
// for this case in addition to directory offers.
if storage.source == StorageDirectorySource::Self_ {
out_dir.add_directory_proxy(test_dir_proxy)
}
}
_ => {}
}
}
// Add any user-specific DirectoryEntry objects into the outgoing namespace.
for (path, entry) in outgoing_paths.drain() {
out_dir.add_entry(path, entry);
}
out_dir
}
/// Attempt to start the instance associated with the given moniker with the
/// default reason of StartReason::Eager.
///
/// On success, returns the short name of the component.
pub async fn start_instance(&self, moniker: &AbsoluteMoniker) -> Result<String, ModelError> {
self.start_instance_with(moniker, StartReason::Eager, false).await
}
/// Attempt to start the instance associated with the given moniker with the default reason of
/// StartReason::Eager, and waits for the runner to start the component. This method will only
/// work if the component in question is using the default mock runner (otherwise, it will
/// hang).
///
/// On success, returns the short name of the component.
pub async fn start_instance_and_wait_start(
&self,
moniker: &AbsoluteMoniker,
) -> Result<String, ModelError> {
self.start_instance_with(moniker, StartReason::Eager, true).await
}
async fn start_instance_with(
&self,
moniker: &AbsoluteMoniker,
reason: StartReason,
wait_for_start: bool,
) -> Result<String, ModelError> {
self.model.start_instance(moniker, &reason).await?;
let component_name = match moniker.path().last() {
Some(part) => part.name().to_string(),
None => self.root_component_name.to_string(),
};
if wait_for_start {
let resolved_url = Self::resolved_url(&component_name);
self.mock_runner.wait_for_url(&resolved_url).await;
}
Ok(component_name)
}
pub async fn start_and_get_instance<'a>(
&self,
moniker: &AbsoluteMoniker,
reason: StartReason,
wait_for_start: bool,
) -> Result<Arc<ComponentInstance>, ModelError> {
let instance = self.model.start_instance(moniker, &reason).await?;
let component_name = match moniker.path().last() {
Some(part) => part.name().to_string(),
None => self.root_component_name.to_string(),
};
if wait_for_start {
let resolved_url = Self::resolved_url(&component_name);
self.mock_runner.wait_for_url(&resolved_url).await;
}
Ok(instance)
}
/// Wait for the given component to start running.
///
/// We define "running" as "the components outgoing directory has responded to a simple
/// request", which the MockRunner supports.
pub async fn wait_for_component_start(&self, moniker: &AbsoluteMoniker) {
// Lookup, start, and open a connection to the component's outgoing directory.
let (dir_proxy, server_end) =
fidl::endpoints::create_proxy::<fio::DirectoryMarker>().unwrap();
self.model.look_up(moniker).await.expect("lookup component failed");
let mut server_end = server_end.into_channel();
self.model
.start_instance(moniker, &StartReason::Eager)
.await
.expect("failed to start component")
.open_outgoing(
fio::OPEN_RIGHT_READABLE,
fio::MODE_TYPE_DIRECTORY,
PathBuf::from("/."),
&mut server_end,
)
.await
.expect("failed to open component's outgoing directory");
// Ensure we can successfully talk to the directory.
dir_proxy
.sync()
.await
.expect("could not communicate with directory")
.map_err(zx::Status::from_raw)
.expect("failed to sync directory");
}
pub fn resolved_url(component_name: &str) -> String {
format!("test:///{}_resolved", component_name)
}
}
#[async_trait]
impl RoutingTestModel for RoutingTest {
type C = ComponentInstance;
async fn check_use(&self, moniker: AbsoluteMoniker, check: CheckUse) {
let component_name = self
.start_instance_and_wait_start(&moniker)
.await
.expect(&format!("start instance failed for `{}`", moniker));
let component_resolved_url = Self::resolved_url(&component_name);
let namespace = self
.mock_runner
.get_namespace(&component_resolved_url)
.expect("could not find child namespace");
Self::check_namespace(component_name, &self.mock_runner, self.components.clone()).await;
match check {
CheckUse::Protocol { path, expected_res } => {
capability_util::call_echo_svc_from_namespace(&namespace, path, expected_res).await;
}
CheckUse::Service { path, instance, member, expected_res } => {
capability_util::call_service_instance_echo_svc_from_namespace(
&namespace,
path,
instance,
member,
expected_res,
)
.await;
}
CheckUse::Directory { path, file, expected_res } => {
capability_util::read_data_from_namespace(&namespace, path, &file, expected_res)
.await
}
CheckUse::Storage {
path,
storage_relation,
from_cm_namespace,
storage_subdir,
expected_res,
} => {
if let ExpectedResult::Ok = &expected_res {
assert!(
storage_relation.is_some(),
"relative moniker required if expected result is ok"
);
}
capability_util::write_file_to_storage(&namespace, path, expected_res.clone())
.await;
let instance_id = self
.model
.root()
.try_get_component_id_index()
.unwrap()
.look_up_moniker(&moniker)
.cloned();
if let Some(relative_moniker) = storage_relation {
if from_cm_namespace {
// Check for the file in the /tmp in the test's namespace
let tmp_proxy = io_util::open_directory_in_namespace(
"/tmp",
io_util::OPEN_RIGHT_READABLE,
)
.expect("failed to open /tmp");
let res = capability_util::check_file_in_storage(
storage_subdir,
relative_moniker,
instance_id.as_ref(),
&tmp_proxy,
)
.await;
if let ExpectedResult::Ok = &expected_res {
res.expect("failed to read file");
}
} else {
// Check for the file in the test's isolated test directory
let res = capability_util::check_file_in_storage(
storage_subdir,
relative_moniker,
instance_id.as_ref(),
&self.test_dir_proxy,
)
.await;
if let ExpectedResult::Ok = &expected_res {
res.expect("failed to read file");
}
}
}
}
CheckUse::StorageAdmin {
storage_relation,
from_cm_namespace,
storage_subdir,
expected_res,
} => {
let storage_admin_proxy =
capability_util::connect_to_svc_in_namespace::<fsys::StorageAdminMarker>(
&namespace,
&"/svc/fuchsia.sys2.StorageAdmin".try_into().unwrap(),
)
.await;
let (storage_proxy, server_end) = create_proxy().unwrap();
let flags = fio::OPEN_RIGHT_WRITABLE | fio::OPEN_FLAG_CREATE;
let relative_moniker_string = format!("{}", storage_relation);
let component_abs_moniker = AbsoluteMoniker::from_relative(
&moniker,
&storage_relation.to_relative_moniker(),
)
.unwrap();
let component_instance_id = self
.model
.root()
.try_get_component_id_index()
.unwrap()
.look_up_moniker(&component_abs_moniker)
.cloned();
storage_admin_proxy
.open_component_storage(
relative_moniker_string.as_str(),
flags,
fio::MODE_TYPE_DIRECTORY,
server_end,
)
.expect("failed to open component storage");
let storage_proxy =
fio::DirectoryProxy::from_channel(storage_proxy.into_channel().unwrap());
capability_util::write_hippo_file_to_directory(
&storage_proxy,
expected_res.clone(),
)
.await;
if expected_res == ExpectedResult::Ok {
let storage_dir = if from_cm_namespace {
io_util::open_directory_in_namespace("/tmp", io_util::OPEN_RIGHT_READABLE)
.expect("failed to open /tmp")
} else {
io_util::clone_directory(&self.test_dir_proxy, fio::CLONE_FLAG_SAME_RIGHTS)
.expect("failed to clone test_dir_proxy")
};
capability_util::check_file_in_storage(
storage_subdir.clone(),
storage_relation.clone(),
component_instance_id.as_ref(),
&storage_dir,
)
.await
.expect("failed to read file");
storage_admin_proxy
.delete_component_storage(relative_moniker_string.as_str())
.await
.expect("failed to send fidl message")
.expect("error encountered while deleting component storage");
capability_util::confirm_storage_is_deleted_for_component(
storage_subdir,
storage_relation,
component_instance_id.as_ref(),
&storage_dir,
)
.await;
}
}
CheckUse::Event { request, expected_res } => {
// Fails if the component did not use the protocol EventSource or if the event is
// not allowed.
capability_util::subscribe_to_event_stream(&namespace, expected_res, request).await;
}
}
}
async fn check_use_exposed_dir(&self, moniker: AbsoluteMoniker, check: CheckUse) {
match check {
CheckUse::Protocol { path, expected_res } => {
capability_util::call_echo_svc_from_exposed_dir(
path,
&moniker,
&self.model,
expected_res,
)
.await;
}
CheckUse::Service { path, instance, member, expected_res } => {
capability_util::call_service_instance_echo_svc_from_exposed_dir(
path,
instance,
member,
&moniker,
&self.model,
expected_res,
)
.await;
}
CheckUse::Directory { path, file, expected_res } => {
capability_util::read_data_from_exposed_dir(
path,
&file,
&moniker,
&self.model,
expected_res,
)
.await;
}
CheckUse::Storage { .. } => {
panic!("storage capabilities can't be exposed");
}
CheckUse::StorageAdmin { .. } => {
panic!("unimplemented");
}
CheckUse::Event { .. } => {
panic!("event capabilities can't be exposed");
}
}
}
async fn look_up_instance(
&self,
moniker: &AbsoluteMoniker,
) -> Result<Arc<ComponentInstance>, anyhow::Error> {
self.model.look_up(&moniker).await.map_err(|err| anyhow!(err))
}
async fn check_open_file(&self, moniker: AbsoluteMoniker, path: CapabilityPath) {
let component_name =
self.start_instance_and_wait_start(&moniker).await.expect("start instance failed");
let component_resolved_url = Self::resolved_url(&component_name);
Self::check_namespace(component_name, &self.mock_runner, self.components.clone()).await;
let namespace = self.mock_runner.get_namespace(&component_resolved_url).unwrap();
capability_util::call_file_svc_from_namespace(&namespace, path).await;
}
async fn create_static_file(&self, path: &Path, contents: &str) -> Result<(), anyhow::Error> {
capability_util::create_static_file(&self.test_dir_proxy, path, contents).await
}
fn install_namespace_directory(&self, path: &str) {
let (client_chan, server_chan) = zx::Channel::create().unwrap();
let ns = fdio::Namespace::installed().expect("Failed to get installed namespace");
ns.bind(path, client_chan).expect(&format!("Failed to bind dir {}", path));
let mut out_dir = OutDir::new();
Self::install_default_out_files(&mut out_dir);
out_dir.add_directory_proxy(&self.test_dir_proxy);
out_dir.host_fn()(ServerEnd::new(server_chan));
}
fn add_subdir_to_data_directory(&self, subdir: &str) {
fs::create_dir_all(self.test_dir.path().join(subdir)).unwrap()
}
async fn check_test_subdir_contents(&self, path: &str, expected: Vec<String>) {
assert_eq!(self.list_directory(path).await, expected)
}
async fn check_namespace_subdir_contents(&self, path: &str, expected: Vec<String>) {
let dir_proxy = io_util::open_directory_in_namespace(path, io_util::OPEN_RIGHT_READABLE)
.expect("failed to open directory");
assert_eq!(list_directory(&dir_proxy).await, expected)
}
async fn check_test_subdir_contains(&self, path: &str, expected: String) {
assert!(self.list_directory(path).await.contains(&expected))
}
async fn check_test_dir_tree_contains(&self, expected: String) {
assert!(list_directory_recursive(&self.test_dir_proxy)
.await
.iter()
.find(|&name| name.starts_with(&expected))
.is_some());
}
}
/// Installs a new directory at `path` in the test's namespace, removing it when this object
/// goes out of scope.
pub struct ScopedNamespaceDir<'a> {
path: &'a str,
}
impl<'a> ScopedNamespaceDir<'a> {
pub fn new(test: &RoutingTest, path: &'a str) -> Self {
test.install_namespace_directory(path);
Self { path }
}
}
impl Drop for ScopedNamespaceDir<'_> {
fn drop(&mut self) {
let ns = fdio::Namespace::installed().expect("Failed to get installed namespace");
ns.unbind(self.path).expect(&format!("Failed to unbind dir {}", self.path));
}
}
/// Contains functions to use capabilities in routing tests.
pub mod capability_util {
use {
super::*, anyhow::format_err, assert_matches::assert_matches, cm_rust::NativeIntoFidl,
fidl::endpoints::ProtocolMarker, fidl_fuchsia_sys2::EventSourceMarker, std::path::PathBuf,
};
/// Looks up `resolved_url` in the namespace, and attempts to read ${path}/hippo. The file
/// should contain the string "hello".
pub async fn read_data_from_namespace(
namespace: &ManagedNamespace,
path: CapabilityPath,
file: &Path,
expected_res: ExpectedResult,
) {
let path = path.to_string();
let dir_proxy = take_dir_from_namespace(namespace, &path).await;
let file_proxy = io_util::open_file(&dir_proxy, file, fio::OPEN_RIGHT_READABLE)
.expect("failed to open file");
let res = io_util::read_file(&file_proxy).await;
match expected_res {
ExpectedResult::Ok => assert_eq!(
"hello",
res.expect(&format!("failed to read file {}", path.to_string()))
),
ExpectedResult::Err(s) => {
assert!(res.is_err(), "read file successfully when it should fail");
let epitaph = dir_proxy.take_event_stream().next().await.expect("no epitaph");
assert_matches!(
epitaph,
Err(fidl::Error::ClientChannelClosed {
status, ..
}) if status == s
);
}
ExpectedResult::ErrWithNoEpitaph => {
assert!(res.is_err(), "read file successfully when it should fail");
assert_matches!(dir_proxy.take_event_stream().next().await, None);
}
}
// We took ownership of `dir_proxy`, add it back to the namespace.
add_dir_to_namespace(namespace, &path, dir_proxy).await;
}
pub async fn write_file_to_storage(
namespace: &ManagedNamespace,
path: CapabilityPath,
expected_res: ExpectedResult,
) {
let dir_path = path.to_string();
let dir_proxy = take_dir_from_namespace(namespace, dir_path.as_str()).await;
write_hippo_file_to_directory(&dir_proxy, expected_res).await;
add_dir_to_namespace(namespace, dir_path.as_str(), dir_proxy).await;
}
pub async fn write_hippo_file_to_directory(
dir_proxy: &fio::DirectoryProxy,
expected_res: ExpectedResult,
) {
let (file_proxy, server_end) = create_proxy::<fio::FileMarker>().unwrap();
let flags = fio::OPEN_RIGHT_WRITABLE | fio::OPEN_FLAG_CREATE;
let res = async {
dir_proxy.open(
flags,
fio::MODE_TYPE_FILE,
"hippos",
ServerEnd::new(server_end.into_channel()),
)?;
file_proxy.write(b"hippos can be stored here").await
}
.await;
match expected_res {
ExpectedResult::Ok => {
let _: u64 = res
.expect("failed to write file")
.map_err(zx::Status::from_raw)
.expect("write error");
}
ExpectedResult::Err(s) => {
res.expect_err("unexpectedly succeeded writing file");
let epitaph = dir_proxy.take_event_stream().next().await.expect("no epitaph");
assert_matches!(
epitaph,
Err(fidl::Error::ClientChannelClosed {
status, ..
}) if status == s
);
}
ExpectedResult::ErrWithNoEpitaph => {
res.expect_err("unexpectedly succeeded writing file");
assert_matches!(dir_proxy.take_event_stream().next().await, None);
}
}
}
/// Create a file with the given contents in the test dir, along with any subdirectories
/// required.
pub(super) async fn create_static_file(
root: &fio::DirectoryProxy,
path: &Path,
contents: &str,
) -> Result<(), anyhow::Error> {
// Open file, and create subdirectories if required.
let file_proxy = if let Some(directory) = path.parent() {
let subdir = io_util::create_sub_directories(root, directory)
.map_err(|e| e.context(format!("failed to create subdirs for {:?}", path)))?;
io_util::open_file(
&subdir,
&PathBuf::from(path.file_name().unwrap()),
fio::OPEN_RIGHT_WRITABLE | fio::OPEN_FLAG_CREATE,
)?
} else {
io_util::open_file(root, path, fio::OPEN_RIGHT_WRITABLE | fio::OPEN_FLAG_CREATE)?
};
// Write contents.
io_util::write_file(&file_proxy, contents).await
}
pub async fn check_file_in_storage(
storage_subdir: Option<String>,
relation: InstancedRelativeMoniker,
instance_id: Option<&ComponentInstanceId>,
test_dir_proxy: &fio::DirectoryProxy,
) -> Result<(), anyhow::Error> {
let mut dir_path = generate_storage_path(storage_subdir, &relation, instance_id);
dir_path.push("hippos");
let file_proxy =
io_util::open_file(&test_dir_proxy, &dir_path, io_util::OPEN_RIGHT_READABLE)?;
let res = io_util::read_file(&file_proxy).await;
if let Ok(contents) = res {
assert_eq!("hippos can be stored here".to_string(), contents);
Ok(())
} else {
Err(res.expect_err("failed to read file"))
}
}
pub async fn confirm_storage_is_deleted_for_component(
storage_subdir: Option<String>,
relation: InstancedRelativeMoniker,
instance_id: Option<&ComponentInstanceId>,
test_dir_proxy: &fio::DirectoryProxy,
) {
let dir_path = generate_storage_path(storage_subdir, &relation, instance_id);
let res = io_util::directory::open_directory(
&test_dir_proxy,
dir_path.to_str().unwrap(),
io_util::OPEN_RIGHT_READABLE,
)
.await
.expect_err("open_directory shouldnt have succeeded");
assert_eq!(
format!("{:?}", res),
format!("{:?}", io_util::node::OpenError::OpenError(zx::Status::NOT_FOUND))
);
}
pub async fn connect_to_svc_in_namespace<T: ProtocolMarker>(
namespace: &ManagedNamespace,
path: &CapabilityPath,
) -> T::Proxy {
let dir_proxy = take_dir_from_namespace(namespace, &path.dirname).await;
let node_proxy = io_util::open_node(
&dir_proxy,
&Path::new(&path.basename),
fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_WRITABLE,
fio::MODE_TYPE_SERVICE,
)
.expect("failed to open echo service");
add_dir_to_namespace(namespace, &path.dirname, dir_proxy).await;
let client_end = ClientEnd::<T>::new(node_proxy.into_channel().unwrap().into_zx_channel());
client_end.into_proxy().unwrap()
}
pub async fn connect_to_instance_svc_in_namespace<T: ProtocolMarker>(
namespace: &ManagedNamespace,
path: &CapabilityPath,
instance: &str,
member: &str,
) -> T::Proxy {
let dir_proxy = take_dir_from_namespace(namespace, &path.dirname).await;
let service_dir = io_util::directory::open_directory(
&dir_proxy,
&path.basename,
fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_WRITABLE,
)
.await
.expect("failed to open service dir");
let instance_dir = io_util::directory::open_directory(
&service_dir,
instance,
fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_WRITABLE,
)
.await
.expect("failed to open instance dir");
let member_proxy = io_util::directory::open_node_no_describe(
&instance_dir,
member,
fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_WRITABLE,
fio::MODE_TYPE_SERVICE,
)
.expect("failed to open member node");
add_dir_to_namespace(namespace, &path.dirname, dir_proxy).await;
let client_end =
ClientEnd::<T>::new(member_proxy.into_channel().unwrap().into_zx_channel());
client_end.into_proxy().unwrap()
}
/// Verifies that it's possible to subscribe to the given `event` by connecting to an
/// `EventSource` on the given `namespace`. Used to test event capability routing.
/// Testing of usage of the stream lives in the integration tests in:
/// //src/sys/component_manager/tests/events/integration_test.rs
pub async fn subscribe_to_event_stream(
namespace: &ManagedNamespace,
expected_res: ExpectedResult,
event: ::routing::event::EventSubscription,
) {
let res = subscribe_to_event(namespace, event).await;
match (res, expected_res) {
(Err(e), ExpectedResult::Ok) => {
panic!("unexpected failure {}", e);
}
(Ok(_), ExpectedResult::Err(_)) | (Ok(_), ExpectedResult::ErrWithNoEpitaph) => {
panic!("unexpected success");
}
_ => {}
}
}
pub async fn subscribe_to_event(
namespace: &ManagedNamespace,
event: ::routing::event::EventSubscription,
) -> Result<fsys::EventStreamRequestStream, anyhow::Error> {
let event_source_path = CapabilityPath::try_from("/svc/fuchsia.sys2.EventSource").unwrap();
let event_source_proxy =
connect_to_svc_in_namespace::<EventSourceMarker>(namespace, &event_source_path).await;
let (client_end, stream) =
fidl::endpoints::create_request_stream::<fsys::EventStreamMarker>()?;
// Bind the future to a variable in order to avoid using `event` across an await.
let subscribe_future = event_source_proxy.subscribe(
&mut vec![fsys::EventSubscription {
event_name: Some(event.event_name.to_string()),
..fsys::EventSubscription::EMPTY
}]
.into_iter(),
client_end,
);
subscribe_future
.await?
.map_err(|error| format_err!("Unable to subscribe to event stream: {:?}", error))?;
Ok(stream)
}
/// Looks up `resolved_url` in the namespace, and attempts to use `path`. Expects the service
/// to be fidl.examples.routing.echo.Echo.
pub async fn call_echo_svc_from_namespace(
namespace: &ManagedNamespace,
path: CapabilityPath,
expected_res: ExpectedResult,
) {
let echo_proxy = connect_to_svc_in_namespace::<echo::EchoMarker>(namespace, &path).await;
call_echo_and_validate_result(echo_proxy, expected_res).await;
}
/// Looks up `resolved_url` in the namespace, and attempts to use `instance` at `path`. Expects the service
/// to be fidl.examples.routing.echo.Echo.
pub async fn call_service_instance_echo_svc_from_namespace(
namespace: &ManagedNamespace,
path: CapabilityPath,
instance: String,
member: String,
expected_res: ExpectedResult,
) {
let echo_proxy = connect_to_instance_svc_in_namespace::<echo::EchoMarker>(
namespace, &path, &instance, &member,
)
.await;
call_echo_and_validate_result(echo_proxy, expected_res).await;
}
async fn call_echo_and_validate_result(
echo_proxy: echo::EchoProxy,
expected_res: ExpectedResult,
) {
let res = echo_proxy.echo_string(Some("hippos")).await;
match expected_res {
ExpectedResult::Ok => {
assert_eq!(res.expect("failed to use echo service"), Some("hippos".to_string()))
}
ExpectedResult::Err(s) => {
let err = res.expect_err("used echo service successfully when it should fail");
assert!(err.is_closed(), "expected file closed error, got: {:?}", err);
let epitaph = echo_proxy.take_event_stream().next().await.expect("no epitaph");
assert_matches!(
epitaph,
Err(fidl::Error::ClientChannelClosed {
status, ..
}) if status == s
);
}
ExpectedResult::ErrWithNoEpitaph => {
let err = res.expect_err("used echo service successfully when it should fail");
assert!(err.is_closed(), "expected file closed error, got: {:?}", err);
assert_matches!(echo_proxy.take_event_stream().next().await, None);
}
}
}
/// Looks up `resolved_url` in the namespace, and attempts to use `path`.
/// Expects the service to work like a fuchsia.io service, and respond with
/// an OnOpen event when opened with OPEN_FLAG_DESCRIBE.
pub async fn call_file_svc_from_namespace(namespace: &ManagedNamespace, path: CapabilityPath) {
let dir_proxy = take_dir_from_namespace(namespace, &path.dirname).await;
let node_proxy = io_util::open_node(
&dir_proxy,
&Path::new(&path.basename),
fio::OPEN_RIGHT_READABLE | fio::OPEN_FLAG_DESCRIBE,
// This should be MODE_TYPE_SERVICE, but we implement the underlying
// service as a file for convenience in testing.
fio::MODE_TYPE_FILE,
)
.expect("failed to open file service");
add_dir_to_namespace(namespace, &path.dirname, dir_proxy).await;
let file_proxy = fio::FileProxy::new(node_proxy.into_channel().unwrap());
let mut event_stream = file_proxy.take_event_stream();
let event = event_stream.try_next().await.unwrap();
match event.expect("failed to received file event") {
fio::FileEvent::OnOpen_ { s, info } => {
assert_eq!(s, zx::sys::ZX_OK);
assert!(matches!(
*info.expect("failed to receive node info"),
fio::NodeInfo::Vmofile(fio::Vmofile { .. })
));
}
fio::FileEvent::OnConnectionInfo { info } => {
assert!(matches!(
info.representation.expect("failed to receive node info"),
fio::Representation::Memory(fio::MemoryInfo { .. })
));
}
}
}
/// Attempts to read ${path}/hippo in `abs_moniker`'s exposed directory. The file should
/// contain the string "hello".
pub async fn read_data_from_exposed_dir<'a>(
path: CapabilityPath,
file: &Path,
abs_moniker: &'a AbsoluteMoniker,
model: &'a Arc<Model>,
expected_res: ExpectedResult,
) {
let (node_proxy, server_end) = endpoints::create_proxy::<fio::NodeMarker>().unwrap();
open_exposed_dir(&path, abs_moniker, model, fio::MODE_TYPE_DIRECTORY, server_end).await;
let dir_proxy = fio::DirectoryProxy::new(node_proxy.into_channel().unwrap());
match expected_res {
ExpectedResult::Ok => {
let file_proxy = io_util::open_file(&dir_proxy, &file, fio::OPEN_RIGHT_READABLE)
.expect("failed to open file");
let res = io_util::read_file(&file_proxy).await;
assert_eq!("hello", res.expect("failed to read file"));
}
ExpectedResult::Err(s) => {
io_util::open_file(&dir_proxy, &file, fio::OPEN_RIGHT_READABLE)
.expect_err("opened file successfully when it should fail");
let epitaph = dir_proxy.take_event_stream().next().await.expect("no epitaph");
assert_matches!(
epitaph,
Err(fidl::Error::ClientChannelClosed {
status, ..
}) if status == s
);
}
ExpectedResult::ErrWithNoEpitaph => {
io_util::open_file(&dir_proxy, &file, fio::OPEN_RIGHT_READABLE)
.expect_err("opened file successfully when it should fail");
assert_matches!(dir_proxy.take_event_stream().next().await, None);
}
}
}
/// Attempts to use the fidl.examples.routing.echo.Echo service at `path` in `abs_moniker`'s exposed
/// directory.
pub async fn call_echo_svc_from_exposed_dir<'a>(
path: CapabilityPath,
abs_moniker: &'a AbsoluteMoniker,
model: &'a Arc<Model>,
expected_res: ExpectedResult,
) {
let (node_proxy, server_end) = endpoints::create_proxy::<fio::NodeMarker>().unwrap();
open_exposed_dir(&path, abs_moniker, model, fio::MODE_TYPE_SERVICE, server_end).await;
let echo_proxy = echo::EchoProxy::new(node_proxy.into_channel().unwrap());
call_echo_and_validate_result(echo_proxy, expected_res).await;
}
pub async fn call_service_instance_echo_svc_from_exposed_dir(
path: CapabilityPath,
instance: String,
member: String,
abs_moniker: &AbsoluteMoniker,
model: &Arc<Model>,
expected_res: ExpectedResult,
) {
let (node_proxy, server_end) = endpoints::create_proxy::<fio::NodeMarker>().unwrap();
open_exposed_dir(&path, abs_moniker, model, fio::MODE_TYPE_SERVICE, server_end).await;
let service_dir = fio::DirectoryProxy::from_channel(node_proxy.into_channel().unwrap());
let instance_dir = io_util::directory::open_directory(
&service_dir,
&instance,
io_util::OPEN_RIGHT_READABLE | io_util::OPEN_RIGHT_WRITABLE,
)
.await
.expect("failed to open instance");
let member_node = io_util::directory::open_node_no_describe(
&instance_dir,
&member,
io_util::OPEN_RIGHT_READABLE | io_util::OPEN_RIGHT_WRITABLE,
fio::MODE_TYPE_SERVICE,
)
.expect("failed to open member node");
let echo_proxy = echo::EchoProxy::new(member_node.into_channel().unwrap());
call_echo_and_validate_result(echo_proxy, expected_res).await;
}
/// Looks up `resolved_url` in the namespace, and attempts to use `path`. Expects the service
/// to be fuchsia.component.Realm.
pub async fn call_realm_svc(
path: CapabilityPath,
resolved_url: &str,
namespace: &ManagedNamespace,
bind_calls: Arc<Mutex<Vec<String>>>,
) {
let dir_proxy = take_dir_from_namespace(namespace, &path.dirname).await;
let node_proxy = io_util::open_node(
&dir_proxy,
&Path::new(&path.basename),
fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_WRITABLE,
fio::MODE_TYPE_SERVICE,
)
.expect("failed to open realm service");
let realm_proxy = fcomponent::RealmProxy::new(node_proxy.into_channel().unwrap());
let mut child_ref = fdecl::ChildRef { name: "my_child".to_string(), collection: None };
let (_, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>().unwrap();
let res = realm_proxy.open_exposed_dir(&mut child_ref, server_end).await;
// Check for side effects: realm service should have received the `open_exposed_dir` call.
res.expect("failed to send fidl message").expect("failed to use realm service");
let bind_url =
format!("test:///{}_resolved", bind_calls.lock().await.last().expect("no bind call"));
assert_eq!(bind_url, resolved_url);
}
/// Call `fuchsia.component.Realm.CreateChild` to create a dynamic child.
pub async fn call_create_child<'a>(
namespace: &ManagedNamespace,
collection: &'a str,
child_decl: ChildDecl,
args: fcomponent::CreateChildArgs,
) {
let path: CapabilityPath =
"/svc/fuchsia.component.Realm".try_into().expect("no realm service");
let dir_proxy = take_dir_from_namespace(namespace, &path.dirname).await;
let node_proxy = io_util::open_node(
&dir_proxy,
&Path::new(&path.basename),
fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_WRITABLE,
fio::MODE_TYPE_SERVICE,
)
.expect("failed to open realm service");
add_dir_to_namespace(namespace, &path.dirname, dir_proxy).await;
let realm_proxy = fcomponent::RealmProxy::new(node_proxy.into_channel().unwrap());
let mut collection_ref = fdecl::CollectionRef { name: collection.to_string() };
let child_decl = child_decl.native_into_fidl();
let res = realm_proxy.create_child(&mut collection_ref, child_decl, args).await;
res.expect("failed to send fidl message").expect("failed to create child");
}
/// Call `fuchsia.component.Realm.DestroyChild` to destroy a dynamic child, waiting for
/// destruction to complete.
pub async fn call_destroy_child<'a>(
namespace: &ManagedNamespace,
collection: &'a str,
name: &'a str,
) {
let path: CapabilityPath =
"/svc/fuchsia.component.Realm".try_into().expect("no realm service");
let dir_proxy = take_dir_from_namespace(namespace, &path.dirname).await;
let node_proxy = io_util::open_node(
&dir_proxy,
&Path::new(&path.basename),
fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_WRITABLE,
fio::MODE_TYPE_SERVICE,
)
.expect("failed to open realm service");
add_dir_to_namespace(namespace, &path.dirname, dir_proxy).await;
let realm_proxy = fcomponent::RealmProxy::new(node_proxy.into_channel().unwrap());
let mut child_ref =
fdecl::ChildRef { collection: Some(collection.to_string()), name: name.to_string() };
let res = realm_proxy.destroy_child(&mut child_ref).await;
res.expect("failed to send fidl message").expect("failed to destroy child");
}
pub async fn take_dir_from_namespace(
namespace: &ManagedNamespace,
dir_path: &str,
) -> fio::DirectoryProxy {
let mut ns = namespace.lock().await;
// Find the index of our directory in the namespace, and remove the directory and path. The
// path is removed so that the paths/dirs aren't shuffled in the namespace.
let index = ns
.iter()
.position(|entry| entry.path.as_ref().unwrap() == dir_path)
.expect(&format!("didn't find dir {}", dir_path));
let entry = ns.remove(index);
let dir_proxy = entry.directory.unwrap().into_proxy().unwrap();
dir_proxy
}
/// Adds `dir_proxy` back to the namespace. Useful for restoring the namespace after a call
/// to `take_dir_from_namespace`.
pub async fn add_dir_to_namespace(
namespace: &ManagedNamespace,
dir_path: &str,
dir_proxy: fio::DirectoryProxy,
) {
let mut ns = namespace.lock().await;
ns.push(fcrunner::ComponentNamespaceEntry {
path: Some(dir_path.to_string()),
directory: Some(ClientEnd::new(dir_proxy.into_channel().unwrap().into_zx_channel())),
..fcrunner::ComponentNamespaceEntry::EMPTY
});
}
/// Open the exposed dir for `abs_moniker`.
async fn open_exposed_dir<'a>(
path: &'a CapabilityPath,
abs_moniker: &'a AbsoluteMoniker,
model: &'a Arc<Model>,
open_mode: u32,
server_end: ServerEnd<fio::NodeMarker>,
) {
let component = model
.look_up(abs_moniker)
.await
.expect(&format!("component not found {}", abs_moniker));
model
.start_instance(abs_moniker, &StartReason::Eager)
.await
.expect("failed to start instance");
let state = component.lock_state().await;
match &*state {
InstanceState::Resolved(resolved_instance_state) => {
let flags = fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_WRITABLE;
let vns_path = to_fvfs_path(path);
resolved_instance_state
.get_exposed_dir()
.open(flags, open_mode, vns_path, server_end);
}
_ => {
panic!("Attempted to open exposed dir of unresolved component: {}", abs_moniker);
}
}
}
/// Function to convert a CapabilityPath to a pseudo_fs_mt::Path
fn to_fvfs_path(path: &CapabilityPath) -> vfs::path::Path {
let full_path = format!("{}/{}", path.dirname, path.basename);
let split_string = full_path.split('/').filter(|s| !s.is_empty()).collect::<Vec<_>>();
vfs::path::Path::validate_and_split(split_string.join("/"))
.expect("Failed to validate and split path")
}
}