| // 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; |
| use crate::builtin_environment::{BuiltinEnvironment, BuiltinEnvironmentBuilder}; |
| use crate::model::component::instance::InstanceState; |
| use crate::model::component::{ComponentInstance, StartReason}; |
| use crate::model::model::Model; |
| use crate::model::testing::echo_service::{EchoProtocol, ECHO_CAPABILITY}; |
| use crate::model::testing::mocks::*; |
| use crate::model::testing::out_dir::OutDir; |
| use crate::model::testing::test_helpers::*; |
| use crate::sandbox_util::LaunchTaskOnReceive; |
| use ::routing::capability_source::{BuiltinSource, CapabilitySource, InternalCapability}; |
| use ::routing::component_instance::ComponentInstanceInterface; |
| use ::routing_test_helpers::{generate_storage_path, RoutingTestModel, RoutingTestModelBuilder}; |
| use anyhow::anyhow; |
| use async_trait::async_trait; |
| use camino::Utf8PathBuf; |
| use cm_config::{ |
| AllowlistEntry, CapabilityAllowlistKey, ChildPolicyAllowlists, DebugCapabilityAllowlistEntry, |
| DebugCapabilityKey, RuntimeConfig, SecurityPolicy, |
| }; |
| use cm_rust::*; |
| use cm_types::{Name, Url}; |
| use component_id_index::InstanceId; |
| use errors::ModelError; |
| use fidl::endpoints::{self, create_proxy, ClientEnd, Proxy, ServerEnd}; |
| use fidl::{self}; |
| use fuchsia_component::client::connect_to_named_protocol_at_dir_root; |
| use futures::channel::oneshot; |
| use futures::prelude::*; |
| use hooks::{EventType, HooksRegistration}; |
| use moniker::{ChildName, Moniker}; |
| use sandbox::{Capability, Message, RouterResponse}; |
| use std::collections::{HashMap, HashSet}; |
| use std::fs; |
| use std::path::Path; |
| use std::str::FromStr; |
| use std::sync::Arc; |
| use tempfile::TempDir; |
| use vfs::directory::entry::{DirectoryEntry, OpenRequest}; |
| use vfs::execution_scope::ExecutionScope; |
| use vfs::ToObjectRequest; |
| use { |
| fidl_fidl_examples_routing_echo as echo, fidl_fuchsia_component as fcomponent, |
| fidl_fuchsia_component_decl as fdecl, fidl_fuchsia_io as fio, fidl_fuchsia_sys2 as fsys, |
| fuchsia_inspect as inspect, |
| }; |
| |
| // TODO(https://fxbug.dev/42140194): 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; |
| pub type ServiceInstance = ::routing_test_helpers::ServiceInstance; |
| |
| /// 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(); |
| /// ``` |
| pub struct RoutingTestBuilder { |
| root_component: String, |
| components: Vec<(&'static str, ComponentDecl)>, |
| blockers: Vec<(&'static str, (oneshot::Sender<()>, oneshot::Receiver<()>))>, |
| additional_hooks: Vec<HooksRegistration>, |
| outgoing_paths: HashMap<String, HashMap<cm_types::Path, Arc<dyn DirectoryEntry>>>, |
| builtin_runners: HashMap<Name, Arc<dyn BuiltinRunnerFactory>>, |
| mock_builtin_runners: HashSet<Name>, |
| namespace_capabilities: Vec<CapabilityDecl>, |
| builtin_capabilities: Vec<CapabilityDecl>, |
| component_id_index_path: Option<Utf8PathBuf>, |
| // 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<DebugCapabilityKey, HashSet<DebugCapabilityAllowlistEntry>>, |
| child_policy: ChildPolicyAllowlists, |
| configs: Vec<(String, ConfigValuesData)>, |
| scope_factory: Option<Box<dyn Fn() -> ExecutionScope + Send + Sync + 'static>>, |
| } |
| |
| impl RoutingTestBuilder { |
| pub fn new(root_component: &str, components: Vec<(&'static str, ComponentDecl)>) -> Self { |
| Self { |
| root_component: root_component.to_string(), |
| components, |
| blockers: vec![], |
| additional_hooks: vec![], |
| outgoing_paths: HashMap::new(), |
| builtin_runners: HashMap::new(), |
| mock_builtin_runners: HashSet::new(), |
| namespace_capabilities: vec![], |
| builtin_capabilities: vec![], |
| component_id_index_path: None, |
| custom_outgoing_host_fns: HashMap::new(), |
| capability_policy: HashMap::new(), |
| debug_capability_policy: HashMap::new(), |
| child_policy: ChildPolicyAllowlists::default(), |
| configs: Vec::new(), |
| scope_factory: None, |
| } |
| } |
| |
| /// Add a configuration file at a given `path`. |
| /// This will be added to the resolver for the created test and given to |
| /// a component that requests this file. |
| pub fn add_config(mut self, path: &str, values: ConfigValuesData) -> Self { |
| self.configs.push((path.to_string(), values)); |
| 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: cm_types::Path, |
| 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.parse().unwrap(), 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 |
| } |
| |
| /// 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, path: Utf8PathBuf) -> Self { |
| self.component_id_index_path = Some(path); |
| self |
| } |
| |
| pub fn set_reboot_on_terminate_policy(mut self, allowlist: Vec<AllowlistEntry>) -> Self { |
| self.child_policy.reboot_on_terminate = allowlist; |
| self |
| } |
| |
| /// Add a resolver blocker for `name`. |
| pub fn add_blocker( |
| mut self, |
| name: &'static str, |
| send: oneshot::Sender<()>, |
| recv: oneshot::Receiver<()>, |
| ) -> Self { |
| self.blockers.push((name, (send, recv))); |
| self |
| } |
| |
| /// Set a custom execution scope on components. This is useful for tests that wish |
| /// to directly control the execution of scoped tasks. |
| pub fn set_scope_factory( |
| mut self, |
| f: impl Fn() -> ExecutionScope + Send + Sync + 'static, |
| ) -> Self { |
| self.scope_factory = Some(Box::new(f)); |
| 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.parse().unwrap()); |
| } |
| |
| 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: DebugCapabilityKey, |
| allowlist: HashSet<DebugCapabilityAllowlistEntry>, |
| ) { |
| self.debug_capability_policy.insert(key, allowlist); |
| } |
| |
| fn set_component_id_index_path(&mut self, path: Utf8PathBuf) { |
| self.component_id_index_path = Some(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, |
| 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 = fuchsia_fs::directory::open_in_namespace( |
| test_dir.path().to_str().unwrap(), |
| fio::PERM_READABLE | fio::PERM_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 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()); |
| } |
| for (name, blocker) in builder.blockers { |
| let (send, recv) = blocker; |
| mock_resolver.add_blocker(name, send, recv).await; |
| } |
| for (path, values) in builder.configs { |
| mock_resolver.add_config_values(&path, values); |
| } |
| |
| // Add the `test_runner` capability as a built-in. |
| builder.builtin_capabilities.push(CapabilityDecl::Runner(RunnerDecl { |
| name: TEST_RUNNER_NAME.parse().unwrap(), |
| 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: Arc::new(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, |
| ..Default::default() |
| }; |
| let inspector = inspect::Inspector::default(); |
| let mut env_builder = BuiltinEnvironmentBuilder::new() |
| .set_inspector(inspector) |
| .set_runtime_config(config) |
| .add_resolver("test".to_string(), Arc::new(mock_resolver)) |
| .add_runner(TEST_RUNNER_NAME.parse().unwrap(), mock_runner.clone(), true); |
| for name in builder.mock_builtin_runners.clone() { |
| env_builder = env_builder.add_runner(name, mock_runner.clone(), true) |
| } |
| for (name, runner) in builder.builtin_runners.clone() { |
| env_builder = env_builder.add_runner(name, runner, true); |
| } |
| if let Some(f) = builder.scope_factory { |
| env_builder = env_builder.set_scope_factory(f); |
| } |
| let builtin_environment = |
| env_builder.build().await.expect("builtin environment setup failed"); |
| |
| let model = builtin_environment.model.clone(); |
| // Add `Echo` to the root input dict. |
| { |
| let name = ECHO_CAPABILITY.clone(); |
| let top_instance = model.top_instance(); |
| let root = top_instance.root(); |
| let state = root.lock_state().await; |
| let InstanceState::Unresolved(state) = &*state else { |
| unreachable!(); |
| }; |
| let capability_source = CapabilitySource::Builtin(BuiltinSource { |
| capability: InternalCapability::Protocol(name.clone()), |
| }); |
| |
| let launch = LaunchTaskOnReceive::new( |
| capability_source, |
| top_instance.task_group().as_weak(), |
| name.clone(), |
| Some(model.root().context.policy().clone()), |
| Arc::new(move |server_end, _| { |
| EchoProtocol::serve(crate::sandbox_util::take_handle_as_stream::< |
| echo::EchoMarker, |
| >(server_end)) |
| .boxed() |
| }), |
| ); |
| state.component_input.insert_capability(&name, launch.into_router().into()).unwrap(); |
| } |
| |
| model.root().hooks.install(builder.additional_hooks.clone()).await; |
| |
| Self { |
| components: builder.components, |
| model, |
| builtin_environment, |
| mock_runner, |
| test_dir, |
| test_dir_proxy, |
| root_component_name: builder.root_component.clone(), |
| } |
| } |
| |
| /// Set up the given OutDir, installing a set of files assumed to exist by |
| /// many tests: |
| /// - A file implementing `fidl.examples.routing.echo.Echo`, at `path`. |
| /// - A static file `/svc/file`, containing the string "hippos" encoded as UTF-8. |
| pub fn install_default_out_files(path: cm_types::Path, dir: &mut OutDir) { |
| // Add an echo server at `protocol`'s path. |
| dir.add_echo_protocol(path); |
| |
| // Add "/svc/file", providing a read-only file. |
| dir.add_static_file(cm_types::Path::from_str("/svc/file").unwrap(), "hippos"); |
| } |
| |
| /// Creates a dynamic child `child_decl` in `moniker`'s `collection`. |
| pub async fn create_dynamic_child<'a>( |
| &'a self, |
| moniker: &Moniker, |
| collection: &'a str, |
| decl: impl Into<ChildDecl>, |
| ) { |
| self.create_dynamic_child_with_args( |
| moniker, |
| collection, |
| decl, |
| fcomponent::CreateChildArgs::default(), |
| ) |
| .await |
| } |
| |
| /// Creates a dynamic child `child_decl` in `moniker`'s `collection`. |
| pub async fn create_dynamic_child_with_args<'a>( |
| &'a self, |
| moniker: &Moniker, |
| 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: Moniker, |
| collection: &'a str, |
| name: &'a str, |
| ) { |
| let root = self.model.root(); |
| let component = |
| root.find_and_maybe_resolve(&moniker).await.expect("failed to look up component"); |
| root.start_instance(&component.moniker, &StartReason::Eager) |
| .await |
| .expect("start instance failed"); |
| let child_moniker = ChildName::try_new(name, Some(collection)).expect("invalid moniker"); |
| component.remove_dynamic_child(&child_moniker).await.expect("failed to remove child"); |
| } |
| |
| pub async fn bind_and_get_namespace(&self, moniker: Moniker) -> 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: Moniker, |
| instance_id: Option<&InstanceId>, |
| 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 = fuchsia_fs::directory::open_directory( |
| &self.test_dir_proxy, |
| &dir_path.to_str().unwrap(), |
| fio::Flags::empty(), |
| ) |
| .await |
| .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 = |
| fuchsia_fs::directory::open_directory(&self.test_dir_proxy, path, fio::Flags::empty()) |
| .await |
| .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.parent().to_string()), |
| UseDecl::Protocol(s) => Some(s.target_path.parent().to_string()), |
| UseDecl::Storage(s) => Some(s.target_path.to_string()), |
| UseDecl::EventStream(s) => Some(s.target_path.parent().to_string()), |
| UseDecl::Runner(s) => Some(s.source_name.to_string().into()), |
| // Using config doesn't add a namespace path. |
| UseDecl::Config(_) => 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 |
| .paths() |
| .into_iter() |
| .map(Into::into) |
| .collect(); |
| |
| expected_paths.sort_unstable(); |
| actual_paths.sort_unstable(); |
| assert_eq!(expected_paths, actual_paths); |
| } |
| |
| /// Build an outgoing directory for the given component. |
| fn build_outgoing_dir( |
| decl: &ComponentDecl, |
| test_dir_proxy: &fio::DirectoryProxy, |
| mut outgoing_paths: HashMap<cm_types::Path, 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() |
| } |
| CapabilityDecl::Service(ServiceDecl { source_path, .. }) => source_path.as_ref(), |
| _ => None, |
| }; |
| if let Some(path) = path { |
| if outgoing_paths |
| .contains_key(&cm_types::Path::from_str(&format!("{}", path) as &str).unwrap()) |
| { |
| // Client installed a custom DirectoryEntry at this path, don't serve the |
| // default. |
| continue; |
| } |
| } |
| match capability { |
| CapabilityDecl::Protocol(p) => { |
| Self::install_default_out_files( |
| p.source_path.as_ref().unwrap().clone(), |
| &mut out_dir, |
| ); |
| } |
| CapabilityDecl::Service(_) => { |
| out_dir.add_echo_protocol( |
| cm_types::Path::from_str("/svc/foo.service/default/echo").unwrap(), |
| ); |
| } |
| 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: &Moniker) -> 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: &Moniker, |
| ) -> Result<String, ModelError> { |
| self.start_instance_with(moniker, StartReason::Eager, true).await |
| } |
| |
| async fn start_instance_with( |
| &self, |
| moniker: &Moniker, |
| reason: StartReason, |
| wait_for_start: bool, |
| ) -> Result<String, ModelError> { |
| self.start_and_get_instance(moniker, reason, wait_for_start).await.map(|v| v.1) |
| } |
| |
| pub async fn start_and_get_instance( |
| &self, |
| moniker: &Moniker, |
| reason: StartReason, |
| wait_for_start: bool, |
| ) -> Result<(Arc<ComponentInstance>, String), ModelError> { |
| let component_name = match moniker.path().last() { |
| Some(part) => part.name().to_string(), |
| None => self.root_component_name.to_string(), |
| }; |
| let resolved_url = Self::resolved_url(&component_name); |
| if wait_for_start { |
| let is_started = { |
| let component = self.model.root().find_and_maybe_resolve(moniker).await?; |
| component.is_started().await |
| }; |
| if !is_started { |
| self.mock_runner.reset_wait_for_url(&resolved_url); |
| } |
| } |
| let instance = self.model.root().start_instance(moniker, &reason).await?; |
| if wait_for_start { |
| self.mock_runner.wait_for_url(&resolved_url).await; |
| } |
| |
| Ok((instance, component_name)) |
| } |
| |
| pub fn resolved_url(component_name: &str) -> String { |
| format!("test:///{}_resolved", component_name) |
| } |
| |
| /// Create a new event stream for the test components. |
| pub async fn new_event_stream(&self, events: Vec<EventType>) -> fcomponent::EventStreamProxy { |
| new_event_stream(&self.builtin_environment, events).await |
| } |
| } |
| |
| #[async_trait] |
| impl RoutingTestModel for RoutingTest { |
| type C = ComponentInstance; |
| |
| async fn check_use(&self, moniker: Moniker, check: CheckUse) { |
| let component_name = self |
| .start_instance_and_wait_start(&moniker) |
| .await |
| .unwrap_or_else(|e| panic!("start instance failed for `{}`: {:?}", moniker, e)); |
| 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 } => match instance { |
| ServiceInstance::Named(instance) => { |
| capability_util::call_service_instance_echo_svc_from_namespace( |
| &namespace, |
| path, |
| instance, |
| member, |
| expected_res, |
| ) |
| .await; |
| } |
| ServiceInstance::Aggregated(count) => { |
| let entries = |
| capability_util::read_service_in_namespace(&namespace, path.clone()).await; |
| assert_eq!( |
| entries.len(), |
| count, |
| "service directory has wrong number of instances, \ |
| expected: {}, actual, {}", |
| count, |
| entries.len() |
| ); |
| for instance in entries { |
| capability_util::call_service_instance_echo_svc_from_namespace( |
| &namespace, |
| path.clone(), |
| instance.clone(), |
| member.clone(), |
| expected_res.clone(), |
| ) |
| .await; |
| } |
| } |
| }, |
| CheckUse::Directory { path, file, expected_res } => { |
| capability_util::read_data_from_namespace( |
| &namespace, |
| path, |
| file.to_str().unwrap(), |
| 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(), |
| "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().component_id_index().id_for_moniker(&moniker).cloned(); |
| |
| if let Some(moniker) = storage_relation { |
| if from_cm_namespace { |
| // Check for the file in the /tmp in the test's namespace |
| let tmp_proxy = |
| fuchsia_fs::directory::open_in_namespace("/tmp", fio::PERM_READABLE) |
| .expect("failed to open /tmp"); |
| let res = capability_util::check_file_in_storage( |
| storage_subdir, |
| 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, |
| 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, storage_subdir, expected_res } => { |
| let storage_admin_proxy = |
| capability_util::connect_to_svc_in_namespace::<fsys::StorageAdminMarker>( |
| &namespace, |
| &"/svc/fuchsia.sys2.StorageAdmin".parse().unwrap(), |
| ) |
| .await; |
| let (storage_proxy, server_end) = create_proxy(); |
| let moniker_string = format!("{}", storage_relation); |
| let component_moniker = moniker.concat(&storage_relation); |
| let instance_id = |
| self.model.root().component_id_index().id_for_moniker(&component_moniker); |
| match storage_admin_proxy.open_storage(moniker_string.as_str(), server_end).await { |
| Ok(response) => { |
| assert_eq!(expected_res, ExpectedResult::Ok); |
| response |
| } |
| Err(fidl::Error::ClientChannelClosed { status, .. }) => { |
| let ExpectedResult::Err(expected_status) = expected_res else { |
| panic!("StorageAdmin client channel closed: {:?}", status); |
| }; |
| assert_eq!(status, expected_status); |
| return; |
| } |
| Err(e) => panic!("Unexpected transport error: {:?}", e), |
| } |
| .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; |
| |
| let storage_dir = fuchsia_fs::directory::clone(&self.test_dir_proxy) |
| .expect("failed to clone test_dir_proxy"); |
| capability_util::check_file_in_storage( |
| storage_subdir.clone(), |
| storage_relation.clone(), |
| instance_id.clone(), |
| &storage_dir, |
| ) |
| .await |
| .expect("failed to read file"); |
| storage_admin_proxy |
| .delete_component_storage(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, |
| instance_id, |
| &storage_dir, |
| ) |
| .await; |
| } |
| CheckUse::EventStream { path, .. } => { |
| // Fails if the component did not use the protocol EventStream or if the event is |
| // not allowed. |
| capability_util::connect_to_svc_in_namespace::<fcomponent::EventStreamMarker>( |
| &namespace, &path, |
| ) |
| .await; |
| } |
| } |
| } |
| |
| async fn check_use_exposed_dir(&self, moniker: Moniker, 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 } => match instance { |
| ServiceInstance::Named(instance) => { |
| capability_util::call_service_instance_echo_svc_from_exposed_dir( |
| path, |
| instance, |
| member, |
| &moniker, |
| &self.model, |
| expected_res, |
| ) |
| .await; |
| } |
| ServiceInstance::Aggregated(count) => { |
| let entries = capability_util::read_service_from_exposed_dir( |
| path.clone(), |
| &moniker, |
| &self.model, |
| ) |
| .await; |
| assert_eq!( |
| entries.len(), |
| count, |
| "service directory has wrong number of instances, \ |
| expected: {}, actual, {}", |
| count, |
| entries.len() |
| ); |
| for instance in entries { |
| capability_util::call_service_instance_echo_svc_from_exposed_dir( |
| path.clone(), |
| instance.clone(), |
| member.clone(), |
| &moniker, |
| &self.model, |
| expected_res.clone(), |
| ) |
| .await; |
| } |
| } |
| }, |
| CheckUse::Directory { path, file, expected_res } => { |
| capability_util::read_data_from_exposed_dir( |
| path, |
| file.to_str().unwrap(), |
| &moniker, |
| &self.model, |
| expected_res, |
| ) |
| .await; |
| } |
| CheckUse::Storage { .. } => { |
| panic!("storage capabilities can't be exposed"); |
| } |
| CheckUse::StorageAdmin { .. } => { |
| panic!("unimplemented"); |
| } |
| CheckUse::EventStream { .. } => { |
| panic!("unimplemented"); |
| } |
| } |
| } |
| |
| /// Checks if the capability name referred to in the first element of the path in the |
| /// `CheckUse` can successfully be routed from the capabilities exposed to framework. Panics if |
| /// `path.split()` is longer than one element. Yes it's hacky to use the path to carry a name |
| /// here, but since this is such a small edge case it doesn't seem worth the refactor. |
| async fn check_exposed_to_framework(&self, moniker: Moniker, check: CheckUse) { |
| match check { |
| CheckUse::Protocol { path, expected_res } => { |
| capability_util::call_echo_svc_exposed_to_framework( |
| path, |
| &moniker, |
| &self.model, |
| expected_res, |
| ) |
| .await; |
| } |
| CheckUse::Service { .. } => { |
| panic!("unimplemented"); |
| } |
| CheckUse::Directory { .. } => { |
| panic!("unimplemented"); |
| } |
| CheckUse::Storage { .. } => { |
| panic!("storage capabilities can't be exposed"); |
| } |
| CheckUse::StorageAdmin { .. } => { |
| panic!("unimplemented"); |
| } |
| CheckUse::EventStream { .. } => { |
| panic!("unimplemented"); |
| } |
| } |
| } |
| |
| async fn look_up_instance( |
| &self, |
| moniker: &Moniker, |
| ) -> Result<Arc<ComponentInstance>, anyhow::Error> { |
| self.model.root().find_and_maybe_resolve(&moniker).await.map_err(|err| anyhow!(err)) |
| } |
| |
| async fn check_open_node(&self, moniker: Moniker, path: cm_types::Path) { |
| 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_node_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, server) = fidl::endpoints::create_endpoints(); |
| let ns = fdio::Namespace::installed().expect("Failed to get installed namespace"); |
| ns.bind(path, client).unwrap_or_else(|e| panic!("Failed to bind dir {}: {:?}", path, e)); |
| let mut out_dir = OutDir::new(); |
| Self::install_default_out_files("/svc/foo".parse().unwrap(), &mut out_dir); |
| out_dir.add_directory_proxy(&self.test_dir_proxy); |
| out_dir.host_fn()(server); |
| } |
| |
| 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 = fuchsia_fs::directory::open_in_namespace(path, fio::Flags::empty()) |
| .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()); |
| } |
| } |
| |
| /// Contains functions to use capabilities in routing tests. |
| pub mod capability_util { |
| use cm_types::NamespacePath; |
| use fuchsia_fs::node::OpenError; |
| |
| use super::*; |
| use assert_matches::assert_matches; |
| use fidl::endpoints::{DiscoverableProtocolMarker, ProtocolMarker}; |
| |
| /// 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: cm_types::Path, |
| file: &str, |
| expected_res: ExpectedResult, |
| ) { |
| let path = NamespacePath::from(path); |
| let dir_proxy = take_dir_from_namespace(namespace, &path).await; |
| match expected_res { |
| ExpectedResult::Ok => { |
| let file_proxy = |
| fuchsia_fs::directory::open_file(&dir_proxy, file, fio::PERM_READABLE) |
| .await |
| .expect("failed to open file"); |
| let res = fuchsia_fs::file::read_to_string(&file_proxy) |
| .await |
| .expect("failed to read file"); |
| assert_eq!("hello", res); |
| } |
| ExpectedResult::Err(s) => { |
| let file_proxy = |
| fuchsia_fs::directory::open_file_async(&dir_proxy, file, fio::PERM_READABLE) |
| .expect("failed to open file"); |
| let _ = fuchsia_fs::file::read_to_string(&file_proxy) |
| .await |
| .expect_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 => { |
| let file_proxy = |
| fuchsia_fs::directory::open_file_async(&dir_proxy, file, fio::PERM_READABLE) |
| .expect("failed to open file"); |
| let _ = fuchsia_fs::file::read_to_string(&file_proxy) |
| .await |
| .expect_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: cm_types::Path, |
| expected_res: ExpectedResult, |
| ) { |
| let path = path.into(); |
| let dir_proxy = take_dir_from_namespace(namespace, &path).await; |
| write_hippo_file_to_directory(&dir_proxy, expected_res).await; |
| add_dir_to_namespace(namespace, &path, 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>(); |
| let flags = fio::PERM_WRITABLE | fio::Flags::FLAG_MAYBE_CREATE | fio::Flags::PROTOCOL_FILE; |
| let res = async { |
| dir_proxy.open("hippos", flags, &Default::default(), 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 = fuchsia_fs::directory::create_directory_recursive( |
| root, |
| directory.to_str().ok_or(anyhow!("{:?} is not a valid UTF-8 string", path))?, |
| fio::PERM_WRITABLE, |
| ) |
| .await |
| .map_err(|e| anyhow!(e).context(format!("failed to create subdirs for {:?}", path)))?; |
| fuchsia_fs::directory::open_file( |
| &subdir, |
| path.file_name().unwrap().to_str().unwrap(), |
| fio::PERM_WRITABLE | fio::Flags::FLAG_MAYBE_CREATE, |
| ) |
| .await? |
| } else { |
| fuchsia_fs::directory::open_file( |
| root, |
| path.to_str().unwrap(), |
| fio::PERM_READABLE | fio::Flags::FLAG_MAYBE_CREATE, |
| ) |
| .await? |
| }; |
| |
| // Write contents. |
| fuchsia_fs::file::write(&file_proxy, contents).await.map_err(Into::into) |
| } |
| |
| pub async fn check_file_in_storage( |
| storage_subdir: Option<String>, |
| relation: Moniker, |
| instance_id: Option<&InstanceId>, |
| 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 = fuchsia_fs::directory::open_file( |
| &test_dir_proxy, |
| &dir_path.to_str().unwrap(), |
| fio::PERM_READABLE, |
| ) |
| .await?; |
| |
| let contents = fuchsia_fs::file::read_to_string(&file_proxy).await?; |
| assert_eq!(contents, "hippos can be stored here"); |
| Ok(()) |
| } |
| |
| pub async fn confirm_storage_is_deleted_for_component( |
| storage_subdir: Option<String>, |
| relation: Moniker, |
| instance_id: Option<&InstanceId>, |
| test_dir_proxy: &fio::DirectoryProxy, |
| ) { |
| let dir_path = generate_storage_path(storage_subdir, &relation, instance_id); |
| let res = fuchsia_fs::directory::open_directory( |
| &test_dir_proxy, |
| dir_path.to_str().unwrap(), |
| fio::Flags::empty(), |
| ) |
| .await |
| .expect_err("open_directory shouldn't have succeeded"); |
| assert_eq!( |
| format!("{:?}", res), |
| format!("{:?}", fuchsia_fs::node::OpenError::OpenError(zx::Status::NOT_FOUND)) |
| ); |
| } |
| |
| pub async fn connect_to_svc_in_namespace<T: ProtocolMarker>( |
| namespace: &ManagedNamespace, |
| path: &cm_types::Path, |
| ) -> T::Proxy { |
| let dirname = path.parent(); |
| let dir_proxy = take_dir_from_namespace(namespace, &dirname).await; |
| let proxy = |
| connect_to_named_protocol_at_dir_root::<T>(&dir_proxy, path.basename().as_str()) |
| .expect("failed to open service"); |
| add_dir_to_namespace(namespace, &dirname, dir_proxy).await; |
| proxy |
| } |
| |
| pub async fn connect_to_instance_svc_in_namespace<T: ProtocolMarker>( |
| namespace: &ManagedNamespace, |
| path: &cm_types::Path, |
| instance: &str, |
| member: &str, |
| ) -> Result<T::Proxy, fidl::Error> { |
| let dirname = path.parent(); |
| let dir_proxy = take_dir_from_namespace(namespace, &dirname).await; |
| // TODO(https://fxbug.dev/42069409): Utilize the new fuchsia_component::client method to connect to |
| // the service instance, passing in the service_dir, instance name, and member path. |
| let service_dir = fuchsia_fs::directory::open_directory( |
| &dir_proxy, |
| path.basename().as_str(), |
| fio::Flags::empty(), |
| ) |
| .await; |
| add_dir_to_namespace(namespace, &dirname, dir_proxy).await; |
| let service_dir = service_dir |
| // `open_directory` could fail if service capability routing fails. |
| .map_err(|e| match e { |
| OpenError::OpenError(status) => { |
| fidl::Error::ClientChannelClosed { status, protocol_name: "", epitaph: None } |
| } |
| _ => panic!("Unexpected open error {:?}", e), |
| })?; |
| |
| let instance_dir = |
| fuchsia_fs::directory::open_directory(&service_dir, instance, fio::Flags::empty()) |
| .await |
| .map_err(|e| match e { |
| OpenError::OpenError(status) => fidl::Error::ClientChannelClosed { |
| status, |
| protocol_name: "", |
| epitaph: None, |
| }, |
| _ => panic!("Unexpected open error {:?}", e), |
| })?; |
| Ok(connect_to_named_protocol_at_dir_root::<T>(&instance_dir, member) |
| .expect("failed to open member protocol")) |
| } |
| |
| pub async fn read_service_in_namespace( |
| namespace: &ManagedNamespace, |
| path: cm_types::Path, |
| ) -> Vec<String> { |
| let dirname = path.parent(); |
| let dir_proxy = take_dir_from_namespace(namespace, &dirname).await; |
| let service_dir = fuchsia_fs::directory::open_directory( |
| &dir_proxy, |
| path.basename().as_str(), |
| fio::Flags::empty(), |
| ) |
| .await |
| .expect("failed to open service dir"); |
| |
| let entries = fuchsia_fs::directory::readdir(&service_dir) |
| .await |
| .expect("failed to read directory entries") |
| .into_iter() |
| .map(|e| e.name) |
| .collect(); |
| |
| add_dir_to_namespace(namespace, &dirname, dir_proxy).await; |
| entries |
| } |
| |
| /// 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: cm_types::Path, |
| 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: cm_types::Path, |
| instance: String, |
| member: String, |
| expected_res: ExpectedResult, |
| ) { |
| let connect_result = connect_to_instance_svc_in_namespace::<echo::EchoMarker>( |
| namespace, &path, &instance, &member, |
| ) |
| .await; |
| let result = match connect_result { |
| Ok(p) => call_echo(p).await, |
| Err(e) => Err(Some(e)), |
| }; |
| validate_echo_result(result, expected_res).await; |
| } |
| |
| async fn call_echo(echo_proxy: echo::EchoProxy) -> Result<Option<String>, Option<fidl::Error>> { |
| match echo_proxy.echo_string(Some("hippos")).await { |
| Ok(value) => Ok(value), |
| Err(_) => { |
| let epitaph = echo_proxy.take_event_stream().next().await; |
| #[allow(unreachable_patterns)] // TODO(https://fxbug.dev/360336369) |
| match epitaph { |
| Some(Err(e)) => Err(Some(e)), |
| Some(Ok(v)) => panic!("unexpected ok event: {:?}", v), |
| None => Err(None), |
| } |
| } |
| } |
| } |
| |
| async fn validate_echo_result( |
| res: Result<Option<String>, Option<fidl::Error>>, |
| expected_res: ExpectedResult, |
| ) { |
| match expected_res { |
| ExpectedResult::Ok => { |
| assert_eq!(res.expect("failed to use echo"), Some("hippos".to_string())) |
| } |
| ExpectedResult::Err(s) => { |
| let err = res |
| .expect_err("used echo service successfully when it should fail") |
| .expect("expected fidl error"); |
| assert!(err.is_closed(), "expected closed error, got: {:?}", err); |
| assert_matches!( |
| err, |
| fidl::Error::ClientChannelClosed { |
| status, .. |
| } if status == s, |
| "Actual err {err}, Expected status {s}" |
| ); |
| } |
| ExpectedResult::ErrWithNoEpitaph => { |
| let err = res.expect_err("used echo successfully when it should fail"); |
| assert_matches!(err, None); |
| } |
| } |
| } |
| |
| pub async fn call_echo_and_validate_result( |
| echo_proxy: echo::EchoProxy, |
| expected_res: ExpectedResult, |
| ) { |
| validate_echo_result(call_echo(echo_proxy).await, expected_res).await; |
| } |
| |
| /// 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_node_svc_from_namespace(namespace: &ManagedNamespace, path: cm_types::Path) { |
| let dir_proxy = take_dir_from_namespace(namespace, &path.parent()).await; |
| let _node_proxy = fuchsia_fs::directory::open_node( |
| &dir_proxy, |
| path.basename().as_str(), |
| fio::Flags::PROTOCOL_NODE, |
| ) |
| .await |
| .expect("failed to open node"); |
| } |
| |
| /// Attempts to read ${path}/hippo in `moniker`'s exposed directory. The file should |
| /// contain the string "hello". |
| pub async fn read_data_from_exposed_dir<'a>( |
| path: cm_types::Path, |
| file: &str, |
| moniker: &'a Moniker, |
| model: &'a Arc<Model>, |
| expected_res: ExpectedResult, |
| ) { |
| let (node_proxy, server_end) = endpoints::create_proxy::<fio::NodeMarker>(); |
| open_exposed_dir(&path, moniker, model, true, server_end).await; |
| let dir_proxy = fio::DirectoryProxy::new(node_proxy.into_channel().unwrap()); |
| match expected_res { |
| ExpectedResult::Ok => { |
| let file_proxy = |
| fuchsia_fs::directory::open_file(&dir_proxy, &file, fio::PERM_READABLE) |
| .await |
| .expect("failed to open file"); |
| let res = fuchsia_fs::file::read_to_string(&file_proxy).await; |
| assert_eq!("hello", res.expect("failed to read file")); |
| } |
| ExpectedResult::Err(s) => { |
| fuchsia_fs::directory::open_file_async(&dir_proxy, &file, fio::PERM_READABLE) |
| .expect("failed to open 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 => { |
| fuchsia_fs::directory::open_file_async(&dir_proxy, &file, fio::PERM_READABLE) |
| .expect("failed to open file"); |
| assert_matches!(dir_proxy.take_event_stream().next().await, None); |
| } |
| } |
| } |
| |
| /// Attempts to use the fidl.examples.routing.echo.Echo service at `path` in `moniker`'s exposed |
| /// directory. |
| pub async fn call_echo_svc_from_exposed_dir<'a>( |
| path: cm_types::Path, |
| moniker: &'a Moniker, |
| model: &'a Arc<Model>, |
| expected_res: ExpectedResult, |
| ) { |
| let (node_proxy, server_end) = endpoints::create_proxy::<fio::NodeMarker>(); |
| open_exposed_dir(&path, moniker, model, false, server_end).await; |
| let echo_proxy = echo::EchoProxy::new(node_proxy.into_channel().unwrap()); |
| call_echo_and_validate_result(echo_proxy, expected_res).await; |
| } |
| |
| /// Attempts to use the fidl.examples.routing.echo.Echo service with name |
| /// `path.split().first()` in the component's set of capabilities exposed to framework. Panics |
| /// if `path.split()` is longer than one element. Yes it's hacky to use the path to carry a |
| /// name here, but since this is such a small edge case it doesn't seem worth the refactor. |
| pub async fn call_echo_svc_exposed_to_framework<'a>( |
| path: cm_types::Path, |
| moniker: &'a Moniker, |
| model: &'a Arc<Model>, |
| expected_res: ExpectedResult, |
| ) { |
| assert_eq!( |
| 1, |
| path.split().len(), |
| "path must have only 1 element, instead we have {}", |
| path |
| ); |
| let root = model.root(); |
| let component = root |
| .find_and_maybe_resolve(moniker) |
| .await |
| .unwrap_or_else(|e| panic!("component not found {}: {}", moniker, e)); |
| |
| let capability_name = path.split().first().cloned().unwrap(); |
| let router_capability = component |
| .lock_resolved_state() |
| .await |
| .unwrap() |
| .sandbox |
| .component_output |
| .framework() |
| .get(&capability_name) |
| .expect( |
| "component is missing capability in sandbox, does the expose to framework exist?", |
| ) |
| .unwrap(); |
| let router = match router_capability { |
| Capability::ConnectorRouter(r) => r, |
| _ => panic!("unexpected capability type"), |
| }; |
| let connector = match router.route(None, false).await.expect("failed to route") { |
| RouterResponse::Capability(c) => c, |
| _ => panic!("unexpected router response"), |
| }; |
| |
| let (echo_proxy, server_end) = endpoints::create_proxy::<echo::EchoMarker>(); |
| connector.send(Message { channel: server_end.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: cm_types::Path, |
| instance: String, |
| member: String, |
| moniker: &Moniker, |
| model: &Arc<Model>, |
| expected_res: ExpectedResult, |
| ) { |
| let (node_proxy, server_end) = endpoints::create_proxy::<fio::NodeMarker>(); |
| open_exposed_dir(&path, moniker, model, true, server_end).await; |
| // TODO(https://fxbug.dev/42069409): Utilize the new fuchsia_component::client method to connect to |
| // the service instance, passing in the service_dir, instance name, and member path. |
| let service_dir = fio::DirectoryProxy::from_channel(node_proxy.into_channel().unwrap()); |
| let instance_dir = |
| fuchsia_fs::directory::open_directory(&service_dir, &instance, fio::Flags::empty()) |
| .await |
| .expect("failed to open instance"); |
| let echo_proxy = |
| connect_to_named_protocol_at_dir_root::<echo::EchoMarker>(&instance_dir, &member) |
| .expect("failed to connect to Echo service"); |
| call_echo_and_validate_result(echo_proxy, expected_res).await; |
| } |
| |
| pub async fn read_service_from_exposed_dir( |
| path: cm_types::Path, |
| moniker: &Moniker, |
| model: &Arc<Model>, |
| ) -> Vec<String> { |
| let (node_proxy, server_end) = endpoints::create_proxy::<fio::NodeMarker>(); |
| open_exposed_dir(&path, moniker, model, true, server_end).await; |
| // TODO(https://fxbug.dev/42069409): Utilize the new fuchsia_component::client method to connect to |
| // the service instance, passing in the service_dir, instance name, and member path. |
| let service_dir = fio::DirectoryProxy::from_channel(node_proxy.into_channel().unwrap()); |
| let entries = fuchsia_fs::directory::readdir(&service_dir) |
| .await |
| .expect("failed to read directory entries") |
| .into_iter() |
| .map(|e| e.name) |
| .collect(); |
| entries |
| } |
| |
| /// 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 = cm_types::Path::from_str(&format!( |
| "/svc/{}", |
| fcomponent::RealmMarker::PROTOCOL_NAME |
| ) as &str) |
| .expect("no realm service"); |
| let realm_proxy = |
| connect_to_svc_in_namespace::<fcomponent::RealmMarker>(namespace, &path).await; |
| let collection_ref = fdecl::CollectionRef { name: collection.to_string() }; |
| let child_decl = child_decl.native_into_fidl(); |
| let res = realm_proxy.create_child(&collection_ref, &child_decl, args).await; |
| res.expect("failed to send fidl message").expect("failed to create child"); |
| } |
| |
| pub async fn take_dir_from_namespace( |
| namespace: &ManagedNamespace, |
| dir_path: &NamespacePath, |
| ) -> fio::DirectoryProxy { |
| let mut ns = namespace.lock().await; |
| ns.remove(dir_path).unwrap().into_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, |
| path: &NamespacePath, |
| dir_proxy: fio::DirectoryProxy, |
| ) { |
| let mut ns = namespace.lock().await; |
| // TODO(https://fxbug.dev/42060182): Use Proxy::into_client_end when available. |
| let client_end = ClientEnd::new(dir_proxy.into_channel().unwrap().into_zx_channel()); |
| ns.add(path, client_end).unwrap(); |
| } |
| |
| /// Open the exposed dir for `moniker`. |
| async fn open_exposed_dir<'a>( |
| path: &'a cm_types::Path, |
| moniker: &'a Moniker, |
| model: &'a Arc<Model>, |
| directory: bool, |
| server_end: ServerEnd<fio::NodeMarker>, |
| ) { |
| let root = model.root(); |
| let component = root |
| .find_and_maybe_resolve(moniker) |
| .await |
| .unwrap_or_else(|e| panic!("component not found {}: {}", moniker, e)); |
| root.start_instance(moniker, &StartReason::Eager).await.expect("failed to start instance"); |
| let flags = if directory { |
| fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::DIRECTORY |
| } else { |
| fio::OpenFlags::NOT_DIRECTORY |
| }; |
| let mut object_request = flags.to_object_request(server_end); |
| component |
| .open_exposed(OpenRequest::new( |
| component.execution_scope.clone(), |
| flags, |
| to_fvfs_path(path), |
| &mut object_request, |
| )) |
| .await |
| .unwrap(); |
| } |
| |
| /// Function to convert a [cm_types::Path] to a [vfs::path::Path] |
| fn to_fvfs_path(path: &cm_types::Path) -> vfs::path::Path { |
| vfs::path::Path::validate_and_split(path.to_string()) |
| .expect("Failed to validate and split path") |
| } |
| } |