blob: b41478056d947e0fcac5a8a9235afe6db724c77e [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 {
async_trait::async_trait,
fidl::endpoints::{ServerEnd, ServiceMarker},
fidl_fuchsia_component_runner as fcrunner,
fidl_fuchsia_io::DirectoryMarker,
fuchsia_async as fasync,
fuchsia_component::server::ServiceFs,
fuchsia_runtime::job_default,
fuchsia_syslog::fx_log_err,
fuchsia_zircon as zx,
futures::future::abortable,
futures::future::AbortHandle,
futures::{future::BoxFuture, prelude::*},
runner::component::ComponentNamespace,
std::{
convert::TryFrom,
mem,
ops::Deref,
path::Path,
sync::{Arc, Mutex, Weak},
},
thiserror::Error,
zx::{HandleBased, Task},
};
/// Error encountered running test component
#[derive(Debug, Error)]
pub enum ComponentError {
#[error("invalid start info: {:?}", _0)]
InvalidStartInfo(runner::StartInfoError),
#[error("error for test {}: {:?}", _0, _1)]
InvalidArgs(String, anyhow::Error),
#[error("Cannot run test {}, no namespace was supplied.", _0)]
MissingNamespace(String),
#[error("Cannot run test {}, as no outgoing directory was supplied.", _0)]
MissingOutDir(String),
#[error("Cannot run suite server: {:?}", _0)]
ServeSuite(anyhow::Error),
#[error("{}: {:?}", _0, _1)]
Fidl(String, fidl::Error),
#[error("cannot create job: {:?}", _0)]
CreateJob(zx::Status),
#[error("cannot duplicate job: {:?}", _0)]
DuplicateJob(zx::Status),
#[error("invalid url")]
InvalidUrl,
}
impl ComponentError {
/// Convert this error into its approximate `fuchsia.component.Error` equivalent.
pub fn as_zx_status(&self) -> zx::Status {
match self {
Self::InvalidStartInfo(_) => zx::Status::INVALID_ARGS,
Self::InvalidArgs(_, _) => zx::Status::INVALID_ARGS,
Self::MissingNamespace(_) => zx::Status::INVALID_ARGS,
Self::MissingOutDir(_) => zx::Status::INVALID_ARGS,
Self::ServeSuite(_) => zx::Status::INTERNAL,
Self::Fidl(_, _) => zx::Status::INTERNAL,
Self::CreateJob(_) => zx::Status::INTERNAL,
Self::DuplicateJob(_) => zx::Status::INTERNAL,
Self::InvalidUrl => zx::Status::INVALID_ARGS,
}
}
}
/// All information about this test component.
pub struct Component {
/// Component URL
pub url: String,
/// Component name
pub name: String,
/// Binary path for this component relative to /pkg in 'ns'
pub binary: String,
/// Arguments for this test.
pub args: Vec<String>,
/// Namespace to pass to test process.
pub ns: ComponentNamespace,
/// Parent job in which all test processes should be executed.
pub job: zx::Job,
}
impl Component {
/// Create new object using `ComponentStartInfo`.
/// On sucess returns self and outgoing_dir from `ComponentStartInfo`.
pub fn new(
start_info: fcrunner::ComponentStartInfo,
) -> Result<(Self, ServerEnd<DirectoryMarker>), ComponentError> {
let url =
runner::get_resolved_url(&start_info).map_err(ComponentError::InvalidStartInfo)?;
let name = Path::new(&url)
.file_name()
.ok_or_else(|| ComponentError::InvalidUrl)?
.to_str()
.ok_or_else(|| ComponentError::InvalidUrl)?
.to_string();
let args = runner::get_program_args(&start_info)
.map_err(|e| ComponentError::InvalidArgs(url.clone(), e.into()))?;
// TODO validate args
let binary = runner::get_program_binary(&start_info)
.map_err(|e| ComponentError::InvalidArgs(url.clone(), e.into()))?;
let ns = start_info.ns.ok_or_else(|| ComponentError::MissingNamespace(url.clone()))?;
let ns = ComponentNamespace::try_from(ns)
.map_err(|e| ComponentError::InvalidArgs(url.clone(), e.into()))?;
let outgoing_dir =
start_info.outgoing_dir.ok_or_else(|| ComponentError::MissingOutDir(url.clone()))?;
Ok((
Self {
url: url,
name: name,
binary: binary,
args: args,
ns: ns,
job: job_default().create_child_job().map_err(ComponentError::CreateJob)?,
},
outgoing_dir,
))
}
}
#[async_trait]
impl runner::component::Controllable for ComponentRuntime {
async fn kill(mut self) {
self.kill_self();
}
fn stop<'a>(&mut self) -> BoxFuture<'a, ()> {
self.kill_self();
async move {}.boxed()
}
}
impl Drop for ComponentRuntime {
fn drop(&mut self) {
self.kill_self();
}
}
/// Information about all the test instances running for this component.
struct ComponentRuntime {
/// handle to abort component's outgoing services.
outgoing_abortable_handle: Option<futures::future::AbortHandle>,
/// handle to abort running test suite servers.
suite_service_abortable_handles: Option<Arc<Mutex<Vec<futures::future::AbortHandle>>>>,
/// job containing all processes in this component.
job: Option<zx::Job>,
/// component object which is stored here for safe keeping. It would be dropped when test is
/// stopped/killed.
component: Option<Arc<Component>>,
}
impl ComponentRuntime {
fn new(
outgoing_abortable_handle: futures::future::AbortHandle,
suite_service_abortable_handles: Arc<Mutex<Vec<futures::future::AbortHandle>>>,
job: zx::Job,
component: Arc<Component>,
) -> Self {
Self {
outgoing_abortable_handle: Some(outgoing_abortable_handle),
suite_service_abortable_handles: Some(suite_service_abortable_handles),
job: Some(job),
component: Some(component),
}
}
fn kill_self(&mut self) {
// drop component.
self.component.take();
// kill outgoing server.
if let Some(h) = self.outgoing_abortable_handle.take() {
h.abort();
}
// kill all suite servers.
if let Some(handles) = self.suite_service_abortable_handles.take() {
let handles = handles.lock().unwrap();
for h in handles.deref() {
h.abort();
}
}
// kill all test processes if running.
if let Some(job) = self.job.take() {
let _ = job.kill();
}
}
}
/// Setup and run test component in background.
/// |F|: Funciton which returns new instance of `SuitServer`.
pub fn start_component<F, S>(
start_info: fcrunner::ComponentStartInfo,
mut server_end: ServerEnd<fcrunner::ComponentControllerMarker>,
get_test_server: F,
) -> Result<(), ComponentError>
where
F: 'static + Fn() -> S,
S: SuiteServer,
{
let resolved_url = runner::get_resolved_url(&start_info).unwrap_or(String::new());
if let Err(e) = start_component_inner(start_info, &mut server_end, get_test_server) {
// Take ownership of `server_end`.
let server_end = take_server_end(&mut server_end);
runner::component::report_start_error(
e.as_zx_status(),
format!("{}", e),
&resolved_url,
server_end,
);
return Err(e);
}
Ok(())
}
fn start_component_inner<F, S>(
start_info: fcrunner::ComponentStartInfo,
server_end: &mut ServerEnd<fcrunner::ComponentControllerMarker>,
get_test_server: F,
) -> Result<(), ComponentError>
where
F: 'static + Fn() -> S,
S: SuiteServer,
{
let (component, outgoing_dir) = Component::new(start_info)?;
let component = Arc::new(component);
let job_dup = component
.job
.duplicate_handle(zx::Rights::SAME_RIGHTS)
.map_err(ComponentError::DuplicateJob)?;
let mut fs = ServiceFs::new_local();
let suite_server_abortable_handles = Arc::new(Mutex::new(vec![]));
let weak_test_suite_abortable_handles = Arc::downgrade(&suite_server_abortable_handles);
let weak_component = Arc::downgrade(&component);
let url = component.url.clone();
fs.dir("svc").add_fidl_service(move |stream| {
let abortable_handles = weak_test_suite_abortable_handles.upgrade();
if abortable_handles.is_none() {
return;
}
let abortable_handles = abortable_handles.unwrap();
let mut abortable_handles = abortable_handles.lock().unwrap();
let abortable_handle = get_test_server().run(weak_component.clone(), &url, stream);
abortable_handles.push(abortable_handle);
});
fs.serve_connection(outgoing_dir.into_channel()).map_err(ComponentError::ServeSuite)?;
let (fut, abortable_handle) = abortable(fs.collect::<()>());
let url = component.url.clone();
let component_runtime =
ComponentRuntime::new(abortable_handle, suite_server_abortable_handles, job_dup, component);
let resolved_url = url.clone();
fasync::spawn_local(async move {
// as error on abortable will always return Aborted,
// no need to check that, as it is a valid usecase.
fut.await.ok();
});
let server_end = take_server_end(server_end);
let controller_stream = server_end.into_stream().map_err(|e| {
ComponentError::Fidl("failed to convert server end to controller".to_owned(), e)
})?;
let controller = runner::component::Controller::new(component_runtime, controller_stream);
fasync::spawn_local(async move {
if let Err(e) = controller.serve().await {
fx_log_err!("test '{}' controller ended with error: {:?}", resolved_url, e);
}
});
Ok(())
}
fn take_server_end<S: ServiceMarker>(end: &mut ServerEnd<S>) -> ServerEnd<S> {
let invalid_end: ServerEnd<S> = zx::Handle::invalid().into();
mem::replace(end, invalid_end)
}
/// Trait implemented by suite server for elf component test.
pub trait SuiteServer {
/// Run this server.
/// |component|: Test component instance.
/// |test_utl|: Url of test component.
/// |stream|: Stream to serve Suite protocol on.
/// Returns abortable handle for suite server future.
fn run(
self,
component: Weak<Component>,
test_url: &str,
stream: fidl_fuchsia_test::SuiteRequestStream,
) -> AbortHandle;
}
#[cfg(test)]
mod tests {
use {
super::*,
anyhow::Error,
fidl::endpoints::{self, ClientEnd},
fidl_fuchsia_io::OPEN_RIGHT_READABLE,
fuchsia_runtime::job_default,
futures::future::Aborted,
matches::assert_matches,
runner::component::{ComponentNamespace, ComponentNamespaceError},
};
fn create_ns_from_current_ns(
dir_paths: Vec<(&str, u32)>,
) -> Result<ComponentNamespace, ComponentNamespaceError> {
let mut ns = vec![];
for (path, permission) in dir_paths {
let chan = io_util::open_directory_in_namespace(path, permission)
.unwrap()
.into_channel()
.unwrap()
.into_zx_channel();
let handle = ClientEnd::new(chan);
ns.push(fcrunner::ComponentNamespaceEntry {
path: Some(path.to_string()),
directory: Some(handle),
});
}
ComponentNamespace::try_from(ns)
}
macro_rules! child_job {
() => {
job_default().create_child_job().unwrap()
};
}
fn sample_test_component() -> Result<Arc<Component>, Error> {
let ns = create_ns_from_current_ns(vec![("/pkg", OPEN_RIGHT_READABLE)])?;
Ok(Arc::new(Component {
url: "fuchsia-pkg://fuchsia.com/sample_test#test.cm".to_owned(),
name: "test.cm".to_owned(),
binary: "bin/sample_tests".to_owned(),
args: vec![],
ns: ns,
job: child_job!(),
}))
}
async fn dummy_func() -> u32 {
2
}
struct DummyServer {}
impl SuiteServer for DummyServer {
fn run(
self,
_component: Weak<Component>,
_test_url: &str,
_stream: fidl_fuchsia_test::SuiteRequestStream,
) -> AbortHandle {
let (_, handle) = abortable(async {});
handle
}
}
#[fuchsia_async::run_singlethreaded(test)]
async fn start_component_error() {
let start_info = fcrunner::ComponentStartInfo {
resolved_url: None,
program: None,
ns: None,
outgoing_dir: None,
runtime_dir: None,
};
let (client_controller, server_controller) = endpoints::create_proxy().unwrap();
let get_test_server = || DummyServer {};
let err = start_component(start_info, server_controller, get_test_server);
assert_matches!(err, Err(ComponentError::InvalidStartInfo(_)));
assert_matches!(
client_controller.take_event_stream().next().await,
Some(Err(fidl::Error::ClientChannelClosed(zx::Status::INVALID_ARGS)))
);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn component_runtime_kill_job_works() {
let component = sample_test_component().unwrap();
let mut futs = vec![];
let mut handles = vec![];
for _i in 0..10 {
let (fut, handle) = abortable(dummy_func());
futs.push(fut);
handles.push(handle);
}
let (out_fut, out_handle) = abortable(dummy_func());
let mut runtime = ComponentRuntime::new(
out_handle,
Arc::new(Mutex::new(handles)),
child_job!(),
component.clone(),
);
assert_eq!(Arc::strong_count(&component), 2);
runtime.kill_self();
for fut in futs {
assert_eq!(fut.await, Err(Aborted));
}
assert_eq!(out_fut.await, Err(Aborted));
assert_eq!(Arc::strong_count(&component), 1);
}
}