blob: 482331d509daa18e36f7a5f00559d4e051dc3954 [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},
config::{CapabilityAllowlistKey, RuntimeConfig, SecurityPolicy},
klog,
model::{
binding::Binder,
error::ModelError,
events::{event::EventMode, registry::EventSubscription},
hooks::HooksRegistration,
model::Model,
realm::BindReason,
testing::{echo_service::*, mocks::*, out_dir::OutDir, test_helpers::*},
},
startup::Arguments,
},
cm_rust::*,
cm_types::Url,
fidl::{
self,
endpoints::{self, create_proxy, ClientEnd, Proxy, ServerEnd},
},
fidl_fidl_examples_echo::{self as echo},
fidl_fuchsia_component_runner as fcrunner,
fidl_fuchsia_io::{
DirectoryProxy, FileEvent, FileMarker, FileObject, FileProxy, NodeInfo, NodeMarker,
CLONE_FLAG_SAME_RIGHTS, MODE_TYPE_DIRECTORY, MODE_TYPE_FILE, MODE_TYPE_SERVICE,
OPEN_FLAG_CREATE, OPEN_FLAG_DESCRIBE, OPEN_RIGHT_READABLE, OPEN_RIGHT_WRITABLE,
},
fidl_fuchsia_sys2 as fsys, fuchsia_zircon as zx,
futures::lock::Mutex,
futures::prelude::*,
matches::assert_matches,
moniker::{AbsoluteMoniker, PartialMoniker, RelativeMoniker},
std::{
collections::{HashMap, HashSet},
convert::{TryFrom, TryInto},
default::Default,
fs,
path::{Path, PathBuf},
sync::Arc,
},
tempfile::TempDir,
vfs::directory::entry::DirectoryEntry,
};
/// Construct a capability path for the hippo service.
pub fn default_service_capability() -> CapabilityPath {
"/svc/hippo".try_into().unwrap()
}
/// Construct a capability path for the hippo directory.
pub fn default_directory_capability() -> CapabilityPath {
"/data/hippo".try_into().unwrap()
}
#[derive(Debug, PartialEq, Clone)]
pub enum ExpectedResult {
Ok,
Err(zx::Status),
ErrWithNoEpitaph,
}
pub enum CheckUse {
Protocol {
path: CapabilityPath,
expected_res: ExpectedResult,
},
Directory {
path: CapabilityPath,
file: PathBuf,
expected_res: ExpectedResult,
},
Storage {
path: CapabilityPath,
// The relative moniker from the storage declaration to the use declaration. Only
// used if `expected_res` is Ok.
storage_relation: Option<RelativeMoniker>,
// The backing directory for this storage is in component manager's namsepace, not the
// test's isolated test directory.
from_cm_namespace: bool,
storage_subdir: Option<String>,
expected_res: ExpectedResult,
},
StorageAdmin {
// The relative moniker from the storage declaration to the use declaration.
storage_relation: RelativeMoniker,
// The backing directory for this storage is in component manager's namsepace, not the
// test's isolated test directory.
from_cm_namespace: bool,
storage_subdir: Option<String>,
expected_res: ExpectedResult,
},
Event {
requests: Vec<EventSubscription>,
expected_res: ExpectedResult,
},
}
impl CheckUse {
pub fn default_directory(expected_res: ExpectedResult) -> Self {
Self::Directory {
path: default_directory_capability(),
file: PathBuf::from("hippo"),
expected_res,
}
}
}
/// 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>>,
namespace_capabilities: Vec<CapabilityDecl>,
// 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<AbsoluteMoniker>>,
}
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 realm'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
}
/// Add a custom capability security policy to restrict routing of certain caps.
pub fn add_capability_policy(
mut self,
key: CapabilityAllowlistKey,
allowlist: HashSet<AbsoluteMoniker>,
) -> Self {
self.capability_policy.insert(key, allowlist);
self
}
pub async fn build(self) -> RoutingTest {
RoutingTest::from_builder(self).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: 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 {
// Ensure that kernel logging has been set up
klog::KernelLogger::init();
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());
// Set up runners for the system, including a default runner "test_runner"
// backed by mock_runner.
let args = Arguments {
root_component_url: Some(
Url::new(format!("test:///{}", builder.root_component)).unwrap(),
),
..Default::default()
};
let config = RuntimeConfig {
namespace_capabilities: builder.namespace_capabilities,
security_policy: SecurityPolicy {
capability_policy: builder.capability_policy,
..Default::default()
},
..Default::default()
};
let mut env_builder = BuiltinEnvironmentBuilder::new()
.set_args(args)
.set_runtime_config(config)
.add_resolver("test".to_string(), Box::new(mock_resolver))
.add_runner(TEST_RUNNER_NAME.into(), mock_runner.clone());
for (name, runner) in builder.builtin_runners {
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_realm.hooks.install(builder.additional_hooks).await;
model.root_realm.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,
}
}
/// Creates a static file at the given path in the temp directory.
pub 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
}
/// Set up the given OutDir, installing a set of files assumed to exist by
/// many tests:
/// - A file `/svc/foo` implementing `fidl.examples.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: ChildDecl,
) {
let component_name =
self.bind_instance_and_wait_start(&moniker).await.expect("bind 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).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 realm = self.model.look_up_realm(&moniker).await.expect("failed to look up realm");
self.model
.bind(&realm.abs_moniker, &BindReason::Eager)
.await
.expect("bind instance failed");
let partial_moniker = PartialMoniker::new(name.to_string(), Some(collection.to_string()));
let nf =
realm.remove_dynamic_child(&partial_moniker).await.expect("failed to remove child");
// Wait for destruction to fully complete.
nf.await.expect("failed to destroy child");
}
/// Creates a sub directory in the outgoing dir's /data directory
pub fn add_subdir_to_data_directory(&self, subdir: &str) {
fs::create_dir_all(self.test_dir.path().join(subdir)).unwrap()
}
/// Checks a `use` declaration at `moniker` by trying to use `capability`.
pub async fn check_use(&self, moniker: AbsoluteMoniker, check: CheckUse) {
let component_name =
self.bind_instance_and_wait_start(&moniker).await.expect("bind instance failed");
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::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;
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,
&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,
&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 = OPEN_RIGHT_WRITABLE | OPEN_FLAG_CREATE;
let relative_moniker_string = format!("{}", storage_relation);
storage_admin_proxy
.open_component_storage(
relative_moniker_string.as_str(),
flags,
MODE_TYPE_DIRECTORY,
server_end,
)
.expect("failed to open component storage");
let storage_proxy =
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, CLONE_FLAG_SAME_RIGHTS)
.expect("failed to clone test_dir_proxy")
};
capability_util::check_file_in_storage(
storage_subdir.clone(),
storage_relation.clone(),
&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,
&storage_dir,
)
.await;
}
}
CheckUse::Event { requests, 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, requests)
.await;
}
}
}
pub async fn bind_and_get_namespace(&self, moniker: AbsoluteMoniker) -> Arc<ManagedNamespace> {
let component_name = self.bind_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
}
/// Checks using a capability from a component's exposed directory.
pub 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::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");
}
}
}
/// Lists the contents of a storage directory.
pub async fn list_directory_in_storage(
&self,
subdir: Option<&str>,
relation: RelativeMoniker,
relative_path: &str,
) -> Vec<String> {
let dir_path =
capability_util::generate_storage_path(subdir.map(|s| s.to_string()), &relation);
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.to_string()),
UseDecl::Protocol(s) => Some(s.target_path.dirname),
UseDecl::Storage(s) => Some(s.target_path.to_string()),
UseDecl::Runner(_) | UseDecl::Event(_) | UseDecl::EventStream(_) => None,
})
.collect();
let mut expected_paths = vec![];
expected_paths.extend(expected_paths_hs.into_iter());
// 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.sys2.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.bind_instance_and_wait_start(&moniker).await.expect("bind instance failed");
let component_resolved_url = Self::resolved_url(&component_name);
let path = "/svc/fuchsia.sys2.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;
}
/// Checks that a use declaration of `path` at `moniker` can be opened with
/// Fuchsia file operations.
pub async fn check_open_file(&self, moniker: AbsoluteMoniker, path: CapabilityPath) {
let component_name =
self.bind_instance_and_wait_start(&moniker).await.expect("bind 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;
}
/// Build an outgoing directory for the given component.
fn build_outgoing_dir(
decl: &ComponentDecl,
test_dir_proxy: &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 expose in decl.exposes.iter() {
match expose {
ExposeDecl::Service(_) => panic!("service capability unsupported"),
_ => (),
}
}
for offer in decl.offers.iter() {
match offer {
OfferDecl::Service(_) => panic!("service capability unsupported"),
_ => (),
}
}
for capability in decl.capabilities.iter() {
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 bind the instance associated with the given moniker with the
/// default reason of BindReason::Eager.
///
/// On success, returns the short name of the component.
pub async fn bind_instance(&self, moniker: &AbsoluteMoniker) -> Result<String, ModelError> {
self.bind_instance_with(moniker, BindReason::Eager, false).await
}
/// Attempt to bind the instance associated with the given moniker with the
/// default reason of BindReason::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 bind_instance_and_wait_start(
&self,
moniker: &AbsoluteMoniker,
) -> Result<String, ModelError> {
self.bind_instance_with(moniker, BindReason::Eager, true).await
}
async fn bind_instance_with(
&self,
moniker: &AbsoluteMoniker,
reason: BindReason,
wait_for_start: bool,
) -> Result<String, ModelError> {
self.model.bind(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)
}
/// 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, component: &AbsoluteMoniker) {
// Lookup, bind, and open a connection to the realm's outgoing directory.
let (dir_proxy, server_end) =
fidl::endpoints::create_proxy::<fidl_fuchsia_io::DirectoryMarker>().unwrap();
let realm = self.model.look_up_realm(component).await.expect("lookup root realm failed");
let mut server_end = server_end.into_channel();
self.model
.bind(&realm.abs_moniker, &BindReason::Eager)
.await
.expect("failed to bind to realm")
.open_outgoing(
fidl_fuchsia_io::OPEN_RIGHT_READABLE,
fidl_fuchsia_io::MODE_TYPE_DIRECTORY,
PathBuf::from("/."),
&mut server_end,
)
.await
.expect("failed to open realm's outgoing directory");
// Ensure we can successfully talk to the directory.
dir_proxy.sync().await.expect("could not communicate with directory");
}
pub fn resolved_url(component_name: &str) -> String {
format!("test:///{}_resolved", component_name)
}
}
/// 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 {
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();
RoutingTest::install_default_out_files(&mut out_dir);
out_dir.add_directory_proxy(&test.test_dir_proxy);
out_dir.host_fn()(ServerEnd::new(server_chan));
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, cm_rust::NativeIntoFidl, fidl::endpoints::ServiceMarker,
fidl_fuchsia_sys2::BlockingEventSourceMarker, 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, 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: &DirectoryProxy,
expected_res: ExpectedResult,
) {
let (file_proxy, server_end) = create_proxy::<FileMarker>().unwrap();
let flags = OPEN_RIGHT_WRITABLE | OPEN_FLAG_CREATE;
let res = async {
dir_proxy.open(
flags,
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 (s, _) = res.expect("failed to write file");
assert_matches!(zx::Status::from_raw(s), zx::Status::OK);
}
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: &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()),
OPEN_RIGHT_WRITABLE | OPEN_FLAG_CREATE,
)?
} else {
io_util::open_file(root, path, OPEN_RIGHT_WRITABLE | 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: RelativeMoniker,
test_dir_proxy: &DirectoryProxy,
) -> Result<(), anyhow::Error> {
let mut dir_path = generate_storage_path(storage_subdir, &relation);
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: RelativeMoniker,
test_dir_proxy: &DirectoryProxy,
) {
let dir_path = generate_storage_path(storage_subdir, &relation);
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: ServiceMarker>(
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),
OPEN_RIGHT_READABLE | OPEN_RIGHT_WRITABLE,
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()
}
/// Verifies that it's possible to subscribe to the given `events` by connecting to an
/// `BlockingEventSource` on the given `namespace`. Used to test eventcapability 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,
events: Vec<EventSubscription>,
) {
let path: CapabilityPath = "/svc/fuchsia.sys2.BlockingEventSource".parse().unwrap();
let res = subscribe_to_events(namespace, &path, events).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_events(
namespace: &ManagedNamespace,
event_source_path: &CapabilityPath,
events: Vec<EventSubscription>,
) -> Result<fsys::EventStreamRequestStream, anyhow::Error> {
let event_source_proxy =
connect_to_svc_in_namespace::<BlockingEventSourceMarker>(namespace, event_source_path)
.await;
let (client_end, stream) =
fidl::endpoints::create_request_stream::<fsys::EventStreamMarker>()?;
event_source_proxy
.subscribe(
&mut events.into_iter().map(|request| fsys::EventSubscription {
event_name: Some(request.event_name.to_string()),
mode: Some(match request.mode {
EventMode::Sync => fsys::EventMode::Sync,
_ => fsys::EventMode::Async,
}),
..fsys::EventSubscription::EMPTY
}),
client_end,
)
.await?
.map_err(|error| format_err!("Unable to subscribe to event stream: {:?}", error))?;
event_source_proxy.start_component_tree().await?;
Ok(stream)
}
/// Looks up `resolved_url` in the namespace, and attempts to use `path`. Expects the service
/// to be fidl.examples.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;
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),
OPEN_RIGHT_READABLE | OPEN_FLAG_DESCRIBE,
// This should be MODE_TYPE_SERVICE, but we implement the underlying
// service as a file for convenience in testing.
MODE_TYPE_FILE,
)
.expect("failed to open file service");
add_dir_to_namespace(namespace, &path.dirname, dir_proxy).await;
let file_proxy = FileProxy::new(node_proxy.into_channel().unwrap());
let mut event_stream = file_proxy.take_event_stream();
let event = event_stream.try_next().await.unwrap();
let FileEvent::OnOpen_ { s, info } = event.expect("failed to received file event");
assert_eq!(s, zx::sys::ZX_OK);
assert_eq!(
*info.expect("failed to receive node info"),
NodeInfo::File(FileObject { event: None, stream: None })
);
}
/// 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::<NodeMarker>().unwrap();
open_exposed_dir(&path, abs_moniker, model, MODE_TYPE_DIRECTORY, server_end).await;
let dir_proxy = DirectoryProxy::new(node_proxy.into_channel().unwrap());
match expected_res {
ExpectedResult::Ok => {
let file_proxy = io_util::open_file(&dir_proxy, &file, 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, 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, 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.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::<NodeMarker>().unwrap();
open_exposed_dir(&path, abs_moniker, model, MODE_TYPE_SERVICE, server_end).await;
let echo_proxy = echo::EchoProxy::new(node_proxy.into_channel().unwrap());
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 be fuchsia.sys2.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),
OPEN_RIGHT_READABLE,
MODE_TYPE_SERVICE,
)
.expect("failed to open realm service");
let realm_proxy = fsys::RealmProxy::new(node_proxy.into_channel().unwrap());
let mut child_ref = fsys::ChildRef { name: "my_child".to_string(), collection: None };
let (_client_chan, server_chan) = zx::Channel::create().unwrap();
let exposed_capabilities = ServerEnd::new(server_chan);
let res = realm_proxy.bind_child(&mut child_ref, exposed_capabilities).await;
// Check for side effects: realm service should have received the `bind_child` call.
let _ = res.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.sys2.Realm.CreateChild` to create a dynamic child.
pub async fn call_create_child<'a>(
namespace: &ManagedNamespace,
collection: &'a str,
child_decl: ChildDecl,
) {
let path: CapabilityPath = "/svc/fuchsia.sys2.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),
OPEN_RIGHT_READABLE,
MODE_TYPE_SERVICE,
)
.expect("failed to open realm service");
add_dir_to_namespace(namespace, &path.dirname, dir_proxy).await;
let realm_proxy = fsys::RealmProxy::new(node_proxy.into_channel().unwrap());
let mut collection_ref = fsys::CollectionRef { name: collection.to_string() };
let child_decl = child_decl.native_into_fidl();
let res = realm_proxy.create_child(&mut collection_ref, child_decl).await;
let _ = res.expect("failed to create child");
}
/// Call `fuchsia.sys2.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.sys2.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),
OPEN_RIGHT_READABLE,
MODE_TYPE_SERVICE,
)
.expect("failed to open realm service");
add_dir_to_namespace(namespace, &path.dirname, dir_proxy).await;
let realm_proxy = fsys::RealmProxy::new(node_proxy.into_channel().unwrap());
let mut child_ref =
fsys::ChildRef { collection: Some(collection.to_string()), name: name.to_string() };
let res = realm_proxy.destroy_child(&mut child_ref).await;
let _ = res.expect("failed to destroy child");
}
pub async fn take_dir_from_namespace(
namespace: &ManagedNamespace,
dir_path: &str,
) -> 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: 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<NodeMarker>,
) {
let realm = model
.look_up_realm(abs_moniker)
.await
.expect(&format!("realm not found {}", abs_moniker));
model.bind(abs_moniker, &BindReason::Eager).await.expect("failed to bind instance");
let execution = realm.lock_execution().await;
let runtime = execution.runtime.as_ref().expect("not resolved");
let flags = OPEN_RIGHT_READABLE;
let vns_path = to_fvfs_path(path);
runtime.exposed_dir.open(flags, open_mode, vns_path, server_end);
}
// This function should reproduce the logic of `crate::storage::generate_storage_path`
pub fn generate_storage_path(
subdir: Option<String>,
relative_moniker: &RelativeMoniker,
) -> PathBuf {
assert!(relative_moniker.up_path().is_empty());
let mut down_path = relative_moniker.down_path().iter();
let mut dir_path = vec![];
if let Some(subdir) = subdir {
dir_path.push(subdir);
}
if let Some(p) = down_path.next() {
dir_path.push(p.as_str().to_string());
}
while let Some(p) = down_path.next() {
dir_path.push("children".to_string());
dir_path.push(p.as_str().to_string());
}
// Storage capabilities used to have a hardcoded set of types, which would be appended
// here. To maintain compatibility with the old paths (and thus not lose data when this was
// migrated) we append "data" here. This works because this is the only type of storage
// that was actually used in the wild.
//
// This is only temporary, until the storage instance id migration changes this layout.
dir_path.push("data".to_string());
dir_path.into_iter().collect()
}
/// 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")
}
}