blob: 0195baa26ca91bf8d2c4608d46f1830ec9ea16f0 [file] [log] [blame]
// Copyright 2020 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 {
anyhow::{format_err, Error},
fidl::endpoints::{DiscoverableProtocolMarker, Proxy, ServerEnd},
fidl_fuchsia_component_test as ftest, fidl_fuchsia_io as fio, fuchsia_async as fasync,
fuchsia_fs,
futures::lock::Mutex,
futures::{future::BoxFuture, TryStreamExt},
std::{collections::HashMap, sync::Arc},
tracing::*,
};
pub const MOCK_ID_KEY: &'static str = "local_component_id";
pub const RUNNER_NAME: &'static str = "realm_builder";
// [START mock_interface_rust]
/// The implementation for a mock component. The contained function is called when the framework
/// asks the mock to run, and the function is given the component's handles for its namespace and
/// outgoing directory. The mock component may then use this handles to run a ServiceFs, access
/// capabilities as the mock, or perform other such actions.
#[derive(Clone)]
pub struct Mock(
Arc<dyn Fn(MockHandles) -> BoxFuture<'static, Result<(), Error>> + Sync + Send + 'static>,
);
// [END mock_interface_rust]
impl Mock {
/// Creates a new `Mock`. The `mock_fn` must be a function which takes a `MockHandles` struct
/// and returnes a pinned and boxed future. This future will be polled so long as the
/// [`RealmInstance`] this mock is instantiated in is still alive.
///
/// The pinned and boxed future can be constructed using [`Box::pin`] with an asynchronous
/// function or block.
///
/// ```
/// let mock1 = Mock::new(move |m: MockHandles| { Box::pin(some_async_fn(m)) });
/// let mock2 = Mock::new(move |m: MockHandles| { Box::pin(async move {
/// let service_proxy = m.connect_to_service::<SomeServiceMarker>()?;
/// service_proxy.some_function()?;
/// Ok(())
/// })});
/// ```
pub fn new<M>(mock_fn: M) -> Self
where
M: Fn(MockHandles) -> BoxFuture<'static, Result<(), Error>> + Sync + Send + 'static,
{
Mock(Arc::new(mock_fn))
}
}
// [START mock_handles_rust]
/// The handles from the framework over which the mock should interact with other components.
pub struct MockHandles {
namespace: HashMap<String, fio::DirectoryProxy>,
/// The outgoing directory handle for a mock component. This can be used to run a ServiceFs for
/// the mock.
pub outgoing_dir: ServerEnd<fio::DirectoryMarker>,
}
// [END mock_handles_rust]
impl MockHandles {
/// Connects to a FIDL protocol and returns a proxy to that protocol.
pub fn connect_to_service<P: DiscoverableProtocolMarker>(&self) -> Result<P::Proxy, Error> {
let svc_dir_proxy = self
.namespace
.get(&"/svc".to_string())
.ok_or(format_err!("the mock's namespace doesn't have a /svc directory"))?;
fuchsia_component::client::connect_to_protocol_at::<P>(svc_dir_proxy)
}
/// Clones a directory from the mock's namespace.
///
/// Note that this function only works on exact matches from the namespace. For example if the
/// namespace had a `data` entry in it, and the caller wished to open the subdirectory at
/// `data/assets`, then this function should be called with the argument `data` and the
/// returned `DirectoryProxy` would then be used to open the subdirectory `assets`. In this
/// scenario, passing `data/assets` in its entirety to this function would fail.
///
/// ```
/// let data_dir = mock_handles.clone_from_namespace("data")?;
/// let assets_dir =
/// fuchsia_fs::directory::open_directory_no_describe(&data_dir, "assets", ...)?;
/// ```
pub fn clone_from_namespace(&self, directory_name: &str) -> Result<fio::DirectoryProxy, Error> {
let dir_proxy = self.namespace.get(&format!("/{}", directory_name)).ok_or(format_err!(
"the mock's namespace doesn't have a /{} directory",
directory_name
))?;
fuchsia_fs::directory::clone_no_describe(dir_proxy, None)
}
}
impl From<ftest::MockComponentStartInfo> for MockHandles {
fn from(fidl_mock_handles: ftest::MockComponentStartInfo) -> Self {
let namespace = fidl_mock_handles
.ns
.unwrap()
.into_iter()
.map(|namespace_entry| {
(
namespace_entry.path.unwrap(),
namespace_entry.directory.unwrap().into_proxy().unwrap(),
)
})
.collect::<HashMap<_, _>>();
Self { namespace, outgoing_dir: fidl_mock_handles.outgoing_dir.unwrap() }
}
}
#[derive(Debug)]
pub struct MocksRunner {
pub(crate) mocks: Arc<Mutex<HashMap<String, Mock>>>,
// We want the async task handling run requests from the realm builder server to run as long
// as this MocksRunner is alive, so hold on to the task for it in this struct.
//
// This is in an option because we want to be able to take this task during
// `RealmInstance::destroy` in order to keep mocks running during the realm shutdown.
pub(crate) event_stream_handling_task: Option<fasync::Task<()>>,
}
impl MocksRunner {
pub fn new(realm_builder_event_stream: ftest::RealmBuilderEventStream) -> Self {
let mocks = Arc::new(Mutex::new(HashMap::new()));
let event_stream_handling_task =
Some(Self::run_event_stream_handling_task(mocks.clone(), realm_builder_event_stream));
Self { mocks, event_stream_handling_task }
}
pub async fn register_mock(&self, mock_id: String, mock: Mock) {
let mut mocks_guard = self.mocks.lock().await;
mocks_guard.insert(mock_id, mock);
}
/// Takes ownership of the task containing the mocks runner and all running mock components.
/// Useful if the RealmInstance this is part of is being destroyed and we want to wait for
/// realm destruction to complete before stopping the mock components.
pub(crate) fn take_runner_task(&mut self) -> Option<fasync::Task<()>> {
self.event_stream_handling_task.take()
}
fn run_event_stream_handling_task(
mocks: Arc<Mutex<HashMap<String, Mock>>>,
event_stream: ftest::RealmBuilderEventStream,
) -> fasync::Task<()> {
fasync::Task::spawn(async move {
if let Err(e) = Self::handle_event_stream(mocks, event_stream).await {
error!(
"error encountered while handling realm builder server event stream: {:?}",
e
);
}
})
}
async fn handle_event_stream(
mocks: Arc<Mutex<HashMap<String, Mock>>>,
mut event_stream: ftest::RealmBuilderEventStream,
) -> Result<(), Error> {
let running_mocks = Arc::new(Mutex::new(HashMap::new()));
while let Some(req) = event_stream.try_next().await? {
match req {
ftest::RealmBuilderEvent::OnMockRunRequest { mock_id, start_info } => {
let mock = {
let mut mocks_guard = mocks.lock().await;
let mock = mocks_guard
.get_mut(&mock_id)
.ok_or(format_err!("no such mock: {:?}", mock_id))?;
mock.clone()
};
let mock_handles = start_info.into();
let mut running_mocks_guard = running_mocks.lock().await;
if running_mocks_guard.contains_key(&mock_id) {
return Err(format_err!(
"failed to start mock {:?} because it is already running",
mock_id
));
}
running_mocks_guard.insert(
mock_id.clone(),
fasync::Task::spawn(async move {
if let Err(e) = (*mock.0)(mock_handles).await {
error!("error running mock: {:?}", e);
}
}),
);
}
ftest::RealmBuilderEvent::OnMockStopRequest { mock_id } => {
if running_mocks.lock().await.remove(&mock_id).is_none() {
return Err(format_err!(
"failed to stop mock {:?} because it wasn't running"
));
}
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use {
super::*,
fidl::endpoints::{create_endpoints, create_proxy},
fidl_fidl_examples_routing_echo as fecho, fidl_fuchsia_component_runner as fcrunner,
fuchsia_component::server as fserver,
futures::{channel::oneshot, StreamExt},
maplit::hashmap,
vfs::{
directory::entry::DirectoryEntry, execution_scope::ExecutionScope,
file::vmo::read_only, path::Path as VfsPath, pseudo_directory,
},
};
#[fuchsia::test]
async fn mock_handles_clone_from_namespace() {
let dir_name = "data";
let file_name = "example_file";
let file_contents = "example contents";
let (_outgoing_dir_client_end, outgoing_dir_server_end) = create_endpoints();
let data_dir = pseudo_directory!(
file_name => read_only(file_contents),
);
let (data_dir_proxy, data_dir_server_end) = create_proxy::<fio::DirectoryMarker>().unwrap();
data_dir.open(
ExecutionScope::new(),
fio::OpenFlags::RIGHT_READABLE,
fio::MODE_TYPE_DIRECTORY,
VfsPath::dot(),
data_dir_server_end.into_channel().into(),
);
let mock_handles = MockHandles {
namespace: hashmap! {
format!("/{}", dir_name) => data_dir_proxy,
},
outgoing_dir: outgoing_dir_server_end,
};
let data_dir_clone =
mock_handles.clone_from_namespace("data").expect("failed to clone from namespace");
let file_proxy = fuchsia_fs::directory::open_file_no_describe(
&data_dir_clone,
file_name,
fio::OpenFlags::RIGHT_READABLE,
)
.expect("failed to open file");
assert_eq!(
file_contents,
&fuchsia_fs::file::read_to_string(&file_proxy).await.expect("failed to read file")
);
}
#[fuchsia::test]
async fn mocks_are_run() {
let (realm_builder_proxy, realm_builder_server_end) =
create_proxy::<ftest::RealmBuilderMarker>().unwrap();
let (_realm_builder_request_stream, realm_builder_server_control_handle) =
realm_builder_server_end.into_stream_and_control_handle().unwrap();
let mocks_runner = MocksRunner::new(realm_builder_proxy.take_event_stream());
// Register a mock
let mock_id_1 = "mocked mock 1".to_string();
let (signal_mock_1_called, receive_mock_1_called) = oneshot::channel();
let signal_mock_1_called = Arc::new(Mutex::new(Some(signal_mock_1_called)));
mocks_runner
.register_mock(
mock_id_1.clone(),
Mock::new(move |_mock_handles: MockHandles| {
let signal_mock_1_called = signal_mock_1_called.clone();
Box::pin(async move {
signal_mock_1_called
.lock()
.await
.take()
.expect("the mock was called twice")
.send(())
.expect("failed to signal that the mock was called");
Ok(())
})
}),
)
.await;
// Register a second mock
let mock_id_2 = "mocked mock 2".to_string();
let (signal_mock_2_called, receive_mock_2_called) = oneshot::channel();
let signal_mock_2_called = Arc::new(Mutex::new(Some(signal_mock_2_called)));
mocks_runner
.register_mock(
mock_id_2.clone(),
Mock::new(move |_mock_handles: MockHandles| {
let signal_mock_2_called = signal_mock_2_called.clone();
Box::pin(async move {
signal_mock_2_called
.lock()
.await
.take()
.expect("the mock was called twice")
.send(())
.expect("failed to signal that the mock was called");
Ok(())
})
}),
)
.await;
// Tell the mock runner to run the second mock, and observe that its called
let (_ignored, outgoing_dir) = create_proxy::<fio::DirectoryMarker>().unwrap();
realm_builder_server_control_handle
.send_on_mock_run_request(
&mock_id_2,
ftest::MockComponentStartInfo {
ns: Some(vec![]),
outgoing_dir: Some(outgoing_dir),
..Default::default()
},
)
.unwrap();
receive_mock_2_called.await.unwrap();
// Tell the mock runner to run the first mock, and observe that its called
let (_ignored, outgoing_dir) = create_proxy::<fio::DirectoryMarker>().unwrap();
realm_builder_server_control_handle
.send_on_mock_run_request(
&mock_id_1,
ftest::MockComponentStartInfo {
ns: Some(vec![]),
outgoing_dir: Some(outgoing_dir),
..Default::default()
},
)
.unwrap();
receive_mock_1_called.await.unwrap();
}
#[fuchsia::test]
async fn mock_handles_service_connection() {
let (svc_client_end, svc_server_end) = create_endpoints::<fio::DirectoryMarker>();
let (_ignored, outgoing_dir) = create_endpoints::<fio::DirectoryMarker>();
let fidl_mock_handles = ftest::MockComponentStartInfo {
ns: Some(vec![fcrunner::ComponentNamespaceEntry {
path: Some("/svc".to_string()),
directory: Some(svc_client_end),
..Default::default()
}]),
outgoing_dir: Some(outgoing_dir),
..Default::default()
};
let mock_handles = MockHandles::from(fidl_mock_handles);
// Run a ServiceFs on svc_server_end
let mut fs = fserver::ServiceFs::new_local();
fs.add_fidl_service(move |mut stream: fecho::EchoRequestStream| {
fasync::Task::spawn(async move {
while let Some(fecho::EchoRequest::EchoString { value, responder }) =
stream.try_next().await.expect("failed to serve echo service")
{
responder
.send(value.as_ref().map(|s| &**s))
.expect("failed to send echo response");
}
})
.detach();
});
fs.serve_connection(svc_server_end).unwrap();
let _service_fs_task = fasync::Task::local(fs.collect::<()>());
// Connect to the ServiceFs through our mock handles, and use the echo server
let echo_client = mock_handles.connect_to_service::<fecho::EchoMarker>().unwrap();
let string_to_echo = "this is an echo'd string".to_string();
assert_eq!(
Some(string_to_echo.clone()),
echo_client.echo_string(Some(&string_to_echo)).await.unwrap()
);
}
}