blob: fb99f084f53d4787dd2f78046fa3dcd86ee2f632 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
crate::{
builtin::runner::BuiltinRunnerFactory,
builtin_environment::{BuiltinEnvironment, BuiltinEnvironmentBuilder},
model::{
component::{ComponentInstance, StartReason},
error::ModelError,
hooks::HooksRegistration,
model::Model,
testing::{
echo_service::{EchoProtocol, ECHO_CAPABILITY},
mocks::*,
out_dir::OutDir,
test_helpers::*,
},
},
},
::routing::component_instance::ComponentInstanceInterface,
::routing_test_helpers::{generate_storage_path, RoutingTestModel, RoutingTestModelBuilder},
anyhow::anyhow,
async_trait::async_trait,
camino::Utf8PathBuf,
cm_config::{
AllowlistEntry, CapabilityAllowlistKey, ChildPolicyAllowlists,
DebugCapabilityAllowlistEntry, DebugCapabilityKey, RuntimeConfig, SecurityPolicy,
},
cm_moniker::InstancedMoniker,
cm_rust::*,
cm_types::{Name, Url},
component_id_index::InstanceId,
fidl::{
self,
endpoints::{self, create_proxy, ClientEnd, Proxy, ServerEnd},
},
fidl_fidl_examples_routing_echo::{self as echo},
fidl_fuchsia_component as fcomponent, fidl_fuchsia_component_decl as fdecl,
fidl_fuchsia_io as fio, fidl_fuchsia_sys2 as fsys,
fuchsia_component::client::connect_to_named_protocol_at_dir_root,
fuchsia_inspect as inspect, fuchsia_zircon as zx,
futures::{channel::oneshot, prelude::*},
moniker::{ChildName, ChildNameBase, Moniker, MonikerBase},
std::{
collections::{HashMap, HashSet},
fs,
path::Path,
str::FromStr,
sync::Arc,
},
tempfile::TempDir,
vfs::{
directory::entry::{DirectoryEntry, OpenRequest},
ToObjectRequest,
},
};
// 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)>,
}
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(),
}
}
/// 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
}
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(),
fuchsia_fs::OpenFlags::RIGHT_READABLE | fuchsia_fs::OpenFlags::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());
}
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(), Box::new(mock_resolver))
.add_runner(TEST_RUNNER_NAME.parse().unwrap(), mock_runner.clone());
for name in builder.mock_builtin_runners.clone() {
env_builder = env_builder.add_runner(name, mock_runner.clone())
}
for (name, runner) in builder.builtin_runners.clone() {
env_builder = env_builder.add_runner(name, runner);
}
let mut builtin_environment =
env_builder.build().await.expect("builtin environment setup failed");
builtin_environment
.add_protocol_to_root_dict::<fidl_fidl_examples_routing_echo::EchoMarker>(
ECHO_CAPABILITY.clone(),
|s| EchoProtocol::serve(s).boxed(),
)
.await;
let model = builtin_environment.model.clone();
model.root().hooks.install(builder.additional_hooks.clone()).await;
builtin_environment.discover_root_component().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: InstancedMoniker,
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(),
fuchsia_fs::OpenFlags::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,
fuchsia_fs::OpenFlags::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)
}
}
#[async_trait]
impl RoutingTestModel for RoutingTest {
type C = ComponentInstance;
async fn check_use(&self, moniker: Moniker, check: CheckUse) {
let (component, component_name) = self
.start_and_get_instance(&moniker, StartReason::Eager, true)
.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",
fuchsia_fs::OpenFlags::RIGHT_READABLE,
)
.expect("failed to open /tmp");
let res = capability_util::check_file_in_storage(
storage_subdir,
component.persistent_storage,
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,
component.persistent_storage,
moniker,
instance_id.as_ref(),
&self.test_dir_proxy,
)
.await;
if let ExpectedResult::Ok = &expected_res {
res.expect("failed to read file");
}
}
}
}
CheckUse::StorageAdmin {
storage_relation,
from_cm_namespace,
storage_subdir,
expected_res,
} => {
let storage_admin_proxy =
capability_util::connect_to_svc_in_namespace::<fsys::StorageAdminMarker>(
&namespace,
&"/svc/fuchsia.sys2.StorageAdmin".parse().unwrap(),
)
.await;
let (storage_proxy, server_end) = create_proxy().unwrap();
let flags = fio::OpenFlags::RIGHT_WRITABLE
| fio::OpenFlags::CREATE
| fio::OpenFlags::DIRECTORY;
let moniker_string = format!("{}", storage_relation);
let component_moniker = moniker.concat(&storage_relation.without_instance_ids());
let instance_id =
self.model.root().component_id_index().id_for_moniker(&component_moniker);
storage_admin_proxy
.open_component_storage(
moniker_string.as_str(),
flags,
fio::ModeType::empty(),
server_end,
)
.expect("failed to open component storage");
let storage_proxy =
fio::DirectoryProxy::from_channel(storage_proxy.into_channel().unwrap());
capability_util::write_hippo_file_to_directory(
&storage_proxy,
expected_res.clone(),
)
.await;
if expected_res == ExpectedResult::Ok {
let storage_dir = if from_cm_namespace {
fuchsia_fs::directory::open_in_namespace(
"/tmp",
fuchsia_fs::OpenFlags::RIGHT_READABLE,
)
.expect("failed to open /tmp")
} else {
fuchsia_fs::directory::clone_no_describe(&self.test_dir_proxy, None)
.expect("failed to clone test_dir_proxy")
};
capability_util::check_file_in_storage(
storage_subdir.clone(),
component.persistent_storage,
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,
component.persistent_storage,
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");
}
}
}
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, fuchsia_fs::OpenFlags::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::*,
assert_matches::assert_matches,
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::OpenFlags::RIGHT_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_no_describe(
&dir_proxy,
file,
fio::OpenFlags::RIGHT_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_no_describe(
&dir_proxy,
file,
fio::OpenFlags::RIGHT_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>().unwrap();
let flags =
fio::OpenFlags::RIGHT_WRITABLE | fio::OpenFlags::CREATE | fio::OpenFlags::NOT_DIRECTORY;
let res = async {
dir_proxy.open(
flags,
fio::ModeType::empty(),
"hippos",
ServerEnd::new(server_end.into_channel()),
)?;
file_proxy.write(b"hippos can be stored here").await
}
.await;
match expected_res {
ExpectedResult::Ok => {
let _: u64 = res
.expect("failed to write file")
.map_err(zx::Status::from_raw)
.expect("write error");
}
ExpectedResult::Err(s) => {
res.expect_err("unexpectedly succeeded writing file");
let epitaph = dir_proxy.take_event_stream().next().await.expect("no epitaph");
assert_matches!(
epitaph,
Err(fidl::Error::ClientChannelClosed {
status, ..
}) if status == s
);
}
ExpectedResult::ErrWithNoEpitaph => {
res.expect_err("unexpectedly succeeded writing file");
assert_matches!(dir_proxy.take_event_stream().next().await, None);
}
}
}
/// Create a file with the given contents in the test dir, along with any subdirectories
/// required.
pub(super) async fn create_static_file(
root: &fio::DirectoryProxy,
path: &Path,
contents: &str,
) -> Result<(), anyhow::Error> {
// Open file, and create subdirectories if required.
let file_proxy = if let Some(directory) = path.parent() {
let subdir = fuchsia_fs::directory::create_directory_recursive(
root,
directory.to_str().ok_or(anyhow!("{:?} is not a valid UTF-8 string", path))?,
fio::OpenFlags::RIGHT_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::OpenFlags::RIGHT_WRITABLE | fio::OpenFlags::CREATE,
)
.await?
} else {
fuchsia_fs::directory::open_file(
root,
path.to_str().unwrap(),
fio::OpenFlags::RIGHT_WRITABLE | fio::OpenFlags::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>,
persistent_storage: bool,
relation: InstancedMoniker,
instance_id: Option<&InstanceId>,
test_dir_proxy: &fio::DirectoryProxy,
) -> Result<(), anyhow::Error> {
let relation =
if persistent_storage { relation.with_zero_value_instance_ids() } else { relation };
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(),
fuchsia_fs::OpenFlags::RIGHT_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>,
persistent_storage: bool,
relation: InstancedMoniker,
instance_id: Option<&InstanceId>,
test_dir_proxy: &fio::DirectoryProxy,
) {
let relation =
if persistent_storage { relation.with_zero_value_instance_ids() } else { relation };
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(),
fuchsia_fs::OpenFlags::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::OpenFlags::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: "" }
}
_ => panic!("Unexpected open error {:?}", e),
})?;
let instance_dir =
fuchsia_fs::directory::open_directory(&service_dir, instance, fio::OpenFlags::empty())
.await
.map_err(|e| match e {
OpenError::OpenError(status) => {
fidl::Error::ClientChannelClosed { status, protocol_name: "" }
}
_ => 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::OpenFlags::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;
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::OpenFlags::empty(),
)
.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>().unwrap();
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::OpenFlags::RIGHT_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_no_describe(
&dir_proxy,
&file,
fio::OpenFlags::RIGHT_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_no_describe(
&dir_proxy,
&file,
fio::OpenFlags::RIGHT_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>().unwrap();
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;
}
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>().unwrap();
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,
fuchsia_fs::OpenFlags::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>().unwrap();
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().unwrap()
}
/// 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")
}
}