blob: 0059a29089be68735cbd46c5f5610a5e3229761e [file] [log] [blame]
// Copyright 2021 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::{anyhow, Context, Error},
fidl::endpoints::{create_proxy, ClientEnd},
fidl::HandleBased,
fidl_fuchsia_component as fcomponent, fidl_fuchsia_component_decl as fdecl,
fidl_fuchsia_component_runner as frunner, fidl_fuchsia_data as fdata, fidl_fuchsia_io as fio,
fidl_fuchsia_process as fprocess, fidl_fuchsia_test as ftest,
fuchsia_component::client as fclient,
fuchsia_runtime as fruntime, fuchsia_zircon as zx,
futures::{StreamExt, TryStreamExt},
rand::Rng,
runner::component::ComponentNamespace,
std::convert::TryInto,
url::Url,
};
/// The name of the collection in which the starnix runner is instantiated.
const RUNNERS_COLLECTION: &str = "runners";
/// Handles a single `ftest::SuiteRequestStream`.
///
/// # Parameters
/// - `test_url`: The URL for the test component to run.
/// - `program`: The program data associated with the runner request for the test component.
/// - `namespace`: The incoming namespace to provide to the test component.
/// - `stream`: The request stream to handle.
pub async fn handle_suite_requests(
test_url: &str,
program: Option<fdata::Dictionary>,
namespace: ComponentNamespace,
mut stream: ftest::SuiteRequestStream,
) -> Result<(), Error> {
while let Some(event) = stream.try_next().await? {
match event {
ftest::SuiteRequest::GetTests { iterator, .. } => {
let stream = iterator.into_stream()?;
handle_case_iterator(test_url, stream).await?;
}
ftest::SuiteRequest::Run { tests, options: _options, listener, .. } => {
let namespace = namespace.clone();
let program = program.clone();
let runner_name = format!("starnix-runner-{}", rand::thread_rng().gen::<u64>());
let (starnix_runner, realm) =
instantiate_runner_in_realm(&namespace, &runner_name, test_url).await?;
run_test_cases(
tests,
test_url,
program,
listener,
namespace,
&runner_name,
realm,
starnix_runner,
)
.await?;
}
}
}
Ok(())
}
fn get_realm(namespace: &ComponentNamespace) -> Result<fcomponent::RealmProxy, Error> {
namespace
.items()
.iter()
.flat_map(|(s, d)| {
if s == "/svc" {
Some(fuchsia_component::client::connect_to_protocol_at_dir_root::<
fcomponent::RealmMarker,
>(&d))
} else {
None
}
})
.next()
.ok_or_else(|| anyhow!("Unable to find /svc"))?
}
async fn open_exposed_directory(
realm: &fcomponent::RealmProxy,
child_name: &str,
collection_name: &str,
) -> Result<fio::DirectoryProxy, Error> {
let (directory_proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>()?;
realm
.open_exposed_dir(
&mut fdecl::ChildRef {
name: child_name.into(),
collection: Some(collection_name.into()),
},
server_end,
)
.await?
.map_err(|e| {
anyhow!(
"failed to bind to child {} in collection {:?}: {:?}",
child_name,
collection_name,
e
)
})?;
Ok(directory_proxy)
}
/// Instantiates a starnix runner in the realm of the given namespace.
///
/// # Parameters
/// - `namespace`: The namespace in which to fetch the realm to instantiate the runner in.
/// - `runner_name`: The name of the runner child.
/// - `runner_url`: The url of the runner component, excluding the `meta/starnix_runner.cm`.
///
/// Returns a proxy to the instantiated runner as well as to the realm in which the runner is
/// instantiated.
async fn instantiate_runner_in_realm(
namespace: &ComponentNamespace,
runner_name: &str,
runner_url: &str,
) -> Result<(frunner::ComponentRunnerProxy, fcomponent::RealmProxy), Error> {
let mut runner_url = Url::parse(runner_url)?;
runner_url.set_fragment(Some("meta/starnix_runner.cm"));
let realm = get_realm(namespace)?;
realm
.create_child(
&mut fdecl::CollectionRef { name: RUNNERS_COLLECTION.into() },
fdecl::Child {
name: Some(runner_name.to_string()),
url: Some(runner_url.to_string()),
startup: Some(fdecl::StartupMode::Lazy),
..fdecl::Child::EMPTY
},
fcomponent::CreateChildArgs::EMPTY,
)
.await?
.map_err(|e| anyhow::anyhow!("failed to create runner child: {:?}", e))?;
let runner_outgoing = open_exposed_directory(&realm, &runner_name, RUNNERS_COLLECTION).await?;
let starnix_runner = fclient::connect_to_protocol_at_dir_root::<frunner::ComponentRunnerMarker>(
&runner_outgoing,
)?;
Ok((starnix_runner, realm))
}
/// Runs the test cases associated with a single `ftest::SuiteRequest::Run` request.
///
/// Running the test component is delegated to an instance of the starnix runner.
///
/// # Parameters
/// - `tests`: The tests that are to be run. Each test executes an independent run of the test
/// component.
/// - `test_url`: The URL of the test component.
/// - `program`: The program data associated with the runner request for the test component.
/// - `listener`: The listener for the test run.
/// - `namespace`: The incoming namespace to provide to the test component.
async fn run_test_cases(
tests: Vec<ftest::Invocation>,
test_url: &str,
program: Option<fdata::Dictionary>,
listener: ClientEnd<ftest::RunListenerMarker>,
namespace: ComponentNamespace,
runner_name: &str,
realm: fcomponent::RealmProxy,
starnix_runner: frunner::ComponentRunnerProxy,
) -> Result<(), Error> {
let run_listener_proxy =
listener.into_proxy().context("Can't convert run listener channel to proxy")?;
for test in tests {
let (case_listener_proxy, case_listener) = create_proxy::<ftest::CaseListenerMarker>()?;
let (test_stdin, _) = zx::Socket::create(zx::SocketOpts::STREAM).unwrap();
let (test_stdout, stdout_client) = zx::Socket::create(zx::SocketOpts::STREAM).unwrap();
let (test_stderr, stderr_client) = zx::Socket::create(zx::SocketOpts::STREAM).unwrap();
let stdin_handle_info = fprocess::HandleInfo {
handle: test_stdin.into_handle(),
id: fruntime::HandleInfo::new(fruntime::HandleType::FileDescriptor, 0).as_raw(),
};
let stdout_handle_info = fprocess::HandleInfo {
handle: test_stdout.into_handle(),
id: fruntime::HandleInfo::new(fruntime::HandleType::FileDescriptor, 1).as_raw(),
};
let stderr_handle_info = fprocess::HandleInfo {
handle: test_stderr.into_handle(),
id: fruntime::HandleInfo::new(fruntime::HandleType::FileDescriptor, 2).as_raw(),
};
run_listener_proxy.on_test_case_started(
test,
ftest::StdHandles {
out: Some(stdout_client),
err: Some(stderr_client),
..ftest::StdHandles::EMPTY
},
case_listener,
)?;
let (component_controller, component_controller_server_end) =
create_proxy::<frunner::ComponentControllerMarker>()?;
let ns = Some(ComponentNamespace::try_into(namespace.clone())?);
let numbered_handles =
Some(vec![stdin_handle_info, stdout_handle_info, stderr_handle_info]);
let (outgoing_dir, _outgoing_dir) =
zx::Channel::create().expect("Failed to create channel.");
let start_info = frunner::ComponentStartInfo {
resolved_url: Some(test_url.to_string()),
program: program.clone(),
ns,
outgoing_dir: Some(outgoing_dir.into()),
runtime_dir: None,
numbered_handles,
..frunner::ComponentStartInfo::EMPTY
};
starnix_runner.start(start_info, component_controller_server_end)?;
let result = read_result(component_controller.take_event_stream()).await;
case_listener_proxy.finished(result)?;
}
realm
.destroy_child(&mut fdecl::ChildRef {
name: runner_name.to_string(),
collection: Some(RUNNERS_COLLECTION.into()),
})
.await?
.map_err(|e| anyhow::anyhow!("failed to destory runner child: {:?}", e))?;
run_listener_proxy.on_finished()?;
Ok(())
}
/// Reads the result of the test run from `event_stream`.
///
/// The result is determined by reading the epitaph from the provided `event_stream`.
async fn read_result(mut event_stream: frunner::ComponentControllerEventStream) -> ftest::Result_ {
let component_epitaph = match event_stream.next().await {
Some(Err(fidl::Error::ClientChannelClosed { status, .. })) => status,
result => {
tracing::error!(
"Didn't get epitaph from the component controller, instead got: {:?}",
result
);
// Fail the test case here, since the component controller's epitaph couldn't be
// read.
zx::Status::INTERNAL
}
};
match component_epitaph {
zx::Status::OK => {
ftest::Result_ { status: Some(ftest::Status::Passed), ..ftest::Result_::EMPTY }
}
_ => ftest::Result_ { status: Some(ftest::Status::Failed), ..ftest::Result_::EMPTY },
}
}
/// Lists all the available test cases and returns them in response to
/// `ftest::CaseIteratorRequest::GetNext`.
///
/// Currently only one "test case" is returned, named `test_name`.
async fn handle_case_iterator(
test_name: &str,
mut stream: ftest::CaseIteratorRequestStream,
) -> Result<(), Error> {
let mut cases_iter = vec![ftest::Case {
name: Some(test_name.to_string()),
enabled: Some(true),
..ftest::Case::EMPTY
}]
.into_iter();
while let Some(event) = stream.try_next().await? {
match event {
ftest::CaseIteratorRequest::GetNext { responder } => {
responder.send(&mut cases_iter)?;
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use {
super::*, fidl::endpoints::create_request_stream, fuchsia_async as fasync,
futures::TryStreamExt, std::convert::TryFrom,
};
/// Returns a `ftest::CaseIteratorProxy` that is served by `super::handle_case_iterator`.
///
/// # Parameters
/// - `test_name`: The name of the test case that is provided to `handle_case_iterator`.
fn set_up_iterator(test_name: &str) -> ftest::CaseIteratorProxy {
let test_name = test_name.to_string();
let (iterator_client_end, iterator_stream) =
create_request_stream::<ftest::CaseIteratorMarker>()
.expect("Couldn't create case iterator");
fasync::Task::local(async move {
let _ = handle_case_iterator(&test_name, iterator_stream).await;
})
.detach();
iterator_client_end.into_proxy().expect("Failed to create proxy")
}
/// Spawns a `ComponentRunnerRequestStream` server that immediately closes all incoming
/// component controllers with the epitaph specified in `component_controller_epitaph`.
///
/// This function can be used to mock the starnix runner in a way that simulates a component
/// exiting with or without error.
///
/// # Parameters
/// - `component_controller_epitaph`: The epitaph used to close the component controller.
///
/// # Returns
/// A `ComponentRunnerProxy` that serves each run request by closing the component with the
/// provided epitaph.
fn spawn_runner(component_controller_epitaph: zx::Status) -> frunner::ComponentRunnerProxy {
let (proxy, mut request_stream) =
fidl::endpoints::create_proxy_and_stream::<frunner::ComponentRunnerMarker>().unwrap();
fasync::Task::local(async move {
while let Some(event) =
request_stream.try_next().await.expect("Error in test runner request stream")
{
match event {
frunner::ComponentRunnerRequest::Start {
start_info: _start_info,
controller,
..
} => {
controller
.close_with_epitaph(component_controller_epitaph)
.expect("Could not close with epitaph");
}
}
}
})
.detach();
proxy
}
/// Returns the status from the first test case reported to `run_listener_stream`.
///
/// This is done by listening to the first `CaseListener` provided via `OnTestCaseStarted`.
///
/// # Parameters
/// - `run_listener_stream`: The run listener stream to extract the test status from.
///
/// # Returns
/// The status of the first test case that is run, or `None` if no such status is reported.
async fn listen_to_test_result(
mut run_listener_stream: ftest::RunListenerRequestStream,
) -> Option<ftest::Status> {
match run_listener_stream.try_next().await.expect("..") {
Some(ftest::RunListenerRequest::OnTestCaseStarted {
invocation: _,
std_handles: _,
listener,
..
}) => match listener
.into_stream()
.expect("Failed to get case listener stream")
.try_next()
.await
.expect("Failed to get case listener stream request")
{
Some(ftest::CaseListenerRequest::Finished { result, .. }) => result.status,
_ => None,
},
_ => None,
}
}
/// Spawns a task that calls `super::run_test_cases` with the provided `run_listener` and
/// `runner_proxy`. The call is made with a test cases vector consisting of one mock test case.
fn spawn_run_test_cases(
run_listener: ClientEnd<ftest::RunListenerMarker>,
starnix_runner: frunner::ComponentRunnerProxy,
) {
fasync::Task::local(async move {
let runner_name = format!("starnix-runner-{}", rand::thread_rng().gen::<u64>());
let (realm, _request_stream) =
fidl::endpoints::create_proxy_and_stream::<fcomponent::RealmMarker>().unwrap();
let _ = run_test_cases(
vec![ftest::Invocation {
name: Some("".to_string()),
tag: Some("".to_string()),
..ftest::Invocation::EMPTY
}],
"",
None,
run_listener,
ComponentNamespace::try_from(vec![]).expect(""),
&runner_name,
realm,
starnix_runner,
)
.await;
})
.detach();
}
#[fasync::run_singlethreaded(test)]
async fn test_number_of_test_cases() {
let iterator_proxy = set_up_iterator("test");
let first_result = iterator_proxy.get_next().await.expect("Didn't get first result");
let second_result = iterator_proxy.get_next().await.expect("Didn't get second result");
assert_eq!(first_result.len(), 1);
assert_eq!(second_result.len(), 0);
}
#[fasync::run_singlethreaded(test)]
async fn test_case_name() {
let test_name = "test_name";
let iterator_proxy = set_up_iterator(test_name);
let result = iterator_proxy.get_next().await.expect("Didn't get first result");
assert_eq!(result[0].name, Some(test_name.to_string()));
}
#[fasync::run_singlethreaded(test)]
async fn test_case_enabled() {
let iterator_proxy = set_up_iterator("test");
let result = iterator_proxy.get_next().await.expect("Didn't get first result");
assert_eq!(result[0].enabled, Some(true));
}
/// Tests that when starnix closes the component controller with an `OK` status, the test case
/// passes.
#[fasync::run_singlethreaded(test)]
async fn test_component_controller_epitaph_ok() {
let starnix_runner = spawn_runner(zx::Status::OK);
let (run_listener, run_listener_stream) =
create_request_stream::<ftest::RunListenerMarker>()
.expect("Couldn't create case listener");
spawn_run_test_cases(run_listener, starnix_runner);
assert_eq!(listen_to_test_result(run_listener_stream).await, Some(ftest::Status::Passed));
}
/// Tests that when starnix closes the component controller with an error status, the test case
/// fails.
#[fasync::run_singlethreaded(test)]
async fn test_component_controller_epitaph_not_ok() {
let starnix_runner = spawn_runner(zx::Status::INTERNAL);
let (run_listener, run_listener_stream) =
create_request_stream::<ftest::RunListenerMarker>()
.expect("Couldn't create case listener");
spawn_run_test_cases(run_listener, starnix_runner);
assert_eq!(listen_to_test_result(run_listener_stream).await, Some(ftest::Status::Failed));
}
}