blob: 1cb36f2bedd28000a966d15c624b0dddb4922562 [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::Proxy,
fidl_fuchsia_io::{
self as fio, DirectoryProxy, CLONE_FLAG_SAME_RIGHTS, OPEN_RIGHT_READABLE,
OPEN_RIGHT_WRITABLE,
},
fidl_fuchsia_process as fproc,
fidl_fuchsia_test::{
self as ftest, Invocation, Result_ as TestResult, RunListenerProxy, Status,
},
fuchsia_async as fasync, fuchsia_zircon as zx,
futures::{
future::{abortable, AbortHandle, Future, FutureExt as _},
lock::Mutex,
prelude::*,
TryStreamExt,
},
lazy_static::lazy_static,
log::{debug, error, info},
serde::{Deserialize, Serialize},
std::{
num::NonZeroUsize,
path::Path,
str::from_utf8,
sync::{Arc, Weak},
},
test_runners_lib::{
cases::TestCaseInfo,
elf::{
Component, EnumeratedTestCases, FidlError, KernelError, MemoizedFutureContainer,
PinnedFuture, SuiteServer,
},
errors::*,
launch,
logs::{LogError, LogStreamReader, LoggerStream, SocketLogWriter},
},
};
/// In `gtest_list_test` output, provides info about individual test cases.
/// Example: For test FOO.Bar, this contains info about Bar.
/// Please refer to documentation of `ListTestResult` for details.
#[derive(Serialize, Deserialize, Debug)]
struct IndividualTestInfo {
pub name: String,
pub file: String,
pub line: u64,
}
/// In `gtest_list_test` output, provides info about individual test suites.
/// Example: For test FOO.Bar, this contains info about FOO.
/// Please refer to documentation of `ListTestResult` for details.
#[derive(Serialize, Deserialize, Debug)]
struct TestSuiteResult {
pub tests: usize,
pub name: String,
pub testsuite: Vec<IndividualTestInfo>,
}
/// Structure of the output of `<test binary> --gtest_list_test`.
///
/// Sample json will look like
/// ```
/// {
/// "tests": 6,
/// "name": "AllTests",
/// "testsuites": [
/// {
/// "name": "SampleTest1",
/// "tests": 2,
/// "testsuite": [
/// {
/// "name": "Test1",
/// "file": "../../src/sys/test_runners/gtest/test_data/sample_tests.cc",
/// "line": 7
/// },
/// {
/// "name": "Test2",
/// "file": "../../src/sys/test_runners/gtest/test_data/sample_tests.cc",
/// "line": 9
/// }
/// ]
/// },
/// ]
///}
///```
#[derive(Serialize, Deserialize, Debug)]
struct ListTestResult {
pub tests: usize,
pub name: String,
pub testsuites: Vec<TestSuiteResult>,
}
/// Provides info about test case failure if any.
#[derive(Serialize, Deserialize, Debug)]
struct Failure {
pub failure: String,
}
/// Provides info about individual test executions.
/// Example: For test FOO.Bar, this contains info about Bar.
/// Please refer to documentation of `TestOutput` for details.
#[derive(Serialize, Deserialize, Debug)]
struct IndividualTestOutput {
pub name: String,
pub status: IndividualTestOutputStatus,
pub time: String,
pub failures: Option<Vec<Failure>>,
}
/// Describes whether a test was run or skipped.
///
/// Refer to [`TestSuiteOutput`] documentation for schema details.
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "UPPERCASE")]
enum IndividualTestOutputStatus {
Run,
NotRun,
}
/// Provides info about individual test suites.
/// Refer to [gtest documentation] for output structure.
/// [gtest documentation]: https://github.com/google/googletest/blob/2002f267f05be6f41a3d458954414ba2bfa3ff1d/googletest/docs/advanced.md#generating-a-json-report
#[derive(Serialize, Deserialize, Debug)]
struct TestSuiteOutput {
pub name: String,
pub tests: usize,
pub failures: usize,
pub disabled: usize,
pub time: String,
pub testsuite: Vec<IndividualTestOutput>,
}
/// Provides info test and the its run result.
/// Example: For test FOO.Bar, this contains info about FOO.
/// Please refer to documentation of `TestSuiteOutput` for details.
#[derive(Serialize, Deserialize, Debug)]
struct TestOutput {
pub testsuites: Vec<TestSuiteOutput>,
}
/// Opens and reads file defined by `path` in `dir`.
async fn read_file(dir: &DirectoryProxy, path: &Path) -> Result<String, anyhow::Error> {
// Open the file in read-only mode.
let result_file_proxy = io_util::open_file(dir, path, OPEN_RIGHT_READABLE)?;
return io_util::read_file(&result_file_proxy).await;
}
/// Implements `fuchsia.test.Suite` and runs provided test.
pub struct TestServer {
/// Directory where test data(json) is written by gtest.
///
/// Note: Although `DirectoryProxy` is `Clone`able, it is not `Sync + Send`, so it has to be
/// wrapped in an `Arc`.
output_dir_proxy: Arc<fio::DirectoryProxy>,
/// Output directory name.
output_dir_name: String,
/// Output directory's parent path.
output_dir_parent_path: String,
/// Cache to store enumerated tests.
tests_future_container: MemoizedFutureContainer<EnumeratedTestCases, EnumerationError>,
}
static PARALLEL_DEFAULT: u16 = 1;
#[async_trait]
impl SuiteServer for TestServer {
/// Launches test process and gets test list out. Returns list of tests names in the format
/// defined by gtests, i.e FOO.Bar.
/// It only runs enumeration logic once, caches and returns the same result back on subsequent
/// calls.
async fn enumerate_tests(
&self,
test_component: Arc<Component>,
) -> Result<EnumeratedTestCases, EnumerationError> {
self.tests(test_component).await
}
async fn run_tests(
&self,
invocations: Vec<Invocation>,
run_options: ftest::RunOptions,
component: Arc<Component>,
run_listener: &RunListenerProxy,
) -> Result<(), RunTestError> {
let num_parallel =
Self::get_parallel_count(run_options.parallel.unwrap_or(PARALLEL_DEFAULT));
let invocations = stream::iter(invocations);
invocations
.map(Ok)
.try_for_each_concurrent(num_parallel, |invocation| {
self.run_test(invocation, &run_options, component.clone(), run_listener)
})
.await
}
/// Run this server.
fn run(
self,
weak_component: Weak<Component>,
test_url: &str,
stream: ftest::SuiteRequestStream,
) -> AbortHandle {
let test_data_name = self.output_dir_name.clone();
let test_data_parent = self.output_dir_parent_path.clone();
let test_url = test_url.clone().to_owned();
let (fut, test_suite_abortable_handle) =
abortable(self.serve_test_suite(stream, weak_component.clone()));
fasync::Task::local(async move {
match fut.await {
Ok(result) => {
if let Err(e) = result {
error!("server failed for test {}: {:?}", test_url, e);
}
}
Err(e) => debug!("server aborted for test {}: {:?}", test_url, e),
}
debug!("Done running server for {}.", test_url);
// Even if `serve_test_suite` failed, clean local data directory as these files are no
// longer needed and they are consuming space.
let test_data_dir = io_util::open_directory_in_namespace(
&test_data_parent,
OPEN_RIGHT_READABLE | OPEN_RIGHT_WRITABLE,
)
.expect("Cannot open data directory");
if let Err(e) = files_async::remove_dir_recursive(&test_data_dir, &test_data_name).await
{
debug!(
"cannot delete temp data dir '{}/{}': {:?}",
test_data_parent, test_data_name, e
);
}
})
.detach();
test_suite_abortable_handle
}
}
const NEWLINE: u8 = b'\n';
const PREFIXES_TO_EXCLUDE: [&[u8]; 10] = [
"Note: Google Test filter".as_bytes(),
" 1 FAILED TEST".as_bytes(),
" YOU HAVE 1 DISABLED TEST".as_bytes(),
"[==========]".as_bytes(),
"[----------]".as_bytes(),
"[ RUN ]".as_bytes(),
"[ PASSED ]".as_bytes(),
"[ FAILED ]".as_bytes(),
"[ SKIPPED ]".as_bytes(),
"[ OK ]".as_bytes(),
];
const SOCKET_BUFFER_SIZE: usize = 4096;
lazy_static! {
static ref RESTRICTED_FLAGS: Vec<&'static str> = vec![
"--gtest_filter",
"--gtest_output",
"--gtest_also_run_disabled_tests",
"--gtest_list_tests",
"--gtest_repeat"
];
}
impl TestServer {
/// Creates new test server.
pub fn new(
output_dir_proxy: fio::DirectoryProxy,
output_dir_name: String,
output_dir_parent_path: String,
) -> Self {
Self {
output_dir_proxy: Arc::new(output_dir_proxy),
output_dir_name: output_dir_name,
output_dir_parent_path: output_dir_parent_path,
tests_future_container: Arc::new(Mutex::new(None)),
}
}
/// Retrieves and memoizes the full list of tests from the test binary.
///
/// The entire `Future` is memoized, so repeated calls do not execute the test binary
/// repeatedly.
///
/// This outer method is _not_ `async`, to avoid capturing a reference to `&self` and fighting
/// the borrow checker until the end of time.
fn tests(
&self,
test_component: Arc<Component>,
) -> impl Future<Output = Result<EnumeratedTestCases, EnumerationError>> {
/// Fetches the full list of tests from the test binary.
async fn fetch(
test_component: Arc<Component>,
test_data_namespace: Result<fproc::NameInfo, IoError>,
output_dir_proxy: Arc<fio::DirectoryProxy>,
) -> Result<EnumeratedTestCases, EnumerationError> {
Ok(Arc::new(get_tests(test_component, test_data_namespace?, output_dir_proxy).await?))
}
/// Populates the given `tests_future_container` with a future, or returns a copy of that
/// future if already present.
async fn get_or_insert_tests_future(
test_component: Arc<Component>,
tests_future_container: MemoizedFutureContainer<EnumeratedTestCases, EnumerationError>,
test_data_namespace: Result<fproc::NameInfo, IoError>,
output_dir_proxy: Arc<fio::DirectoryProxy>,
) -> Result<EnumeratedTestCases, EnumerationError> {
tests_future_container
.lock()
.await
.get_or_insert_with(|| {
let fetched: PinnedFuture<EnumeratedTestCases, EnumerationError> =
Box::pin(fetch(test_component, test_data_namespace, output_dir_proxy));
fetched.shared()
})
.clone()
.await
}
let tests_future_container = self.tests_future_container.clone();
let test_data_namespace = self.test_data_namespace();
let output_dir_proxy = self.output_dir_proxy.clone();
get_or_insert_tests_future(
test_component,
tests_future_container,
test_data_namespace,
output_dir_proxy,
)
}
fn test_data_namespace(&self) -> Result<fproc::NameInfo, IoError> {
let client_channnel =
io_util::clone_directory(&self.output_dir_proxy, CLONE_FLAG_SAME_RIGHTS)
.map_err(IoError::CloneProxy)?
.into_channel()
.map_err(|_| FidlError::ProxyToChannel)
.unwrap()
.into_zx_channel();
Ok(fproc::NameInfo { path: "/test_data".to_owned(), directory: client_channnel.into() })
}
async fn run_test<'a>(
&'a self,
invocation: Invocation,
run_options: &ftest::RunOptions,
component: Arc<Component>,
run_listener: &RunListenerProxy,
) -> Result<(), RunTestError> {
let test = invocation.name.as_ref().ok_or(RunTestError::TestCaseName)?.to_string();
info!("Running test {}", test);
let names = vec![self.test_data_namespace()?];
let my_uuid = uuid::Uuid::new_v4();
let file_name = format!("test_result-{}.json", my_uuid);
let test_list_file = Path::new(&file_name);
let test_list_path = Path::new("/test_data").join(test_list_file);
let mut args = vec![
format!("--gtest_filter={}", test),
format!("--gtest_output=json:{}", test_list_path.display()),
];
if run_options.include_disabled_tests.unwrap_or(false) {
args.push("--gtest_also_run_disabled_tests".to_owned());
}
let (test_logger, log_client) =
zx::Socket::create(zx::SocketOpts::STREAM).map_err(KernelError::CreateSocket).unwrap();
let (case_listener_proxy, listener) =
fidl::endpoints::create_proxy::<fidl_fuchsia_test::CaseListenerMarker>()
.map_err(FidlError::CreateProxy)
.unwrap();
run_listener
.on_test_case_started(invocation, log_client, listener)
.map_err(RunTestError::SendStart)?;
let test_logger =
fasync::Socket::from_socket(test_logger).map_err(KernelError::SocketToAsync).unwrap();
let mut test_logger = SocketLogWriter::new(test_logger);
args.extend(component.args.clone());
if let Some(user_args) = &run_options.arguments {
if let Err(e) = TestServer::validate_args(user_args) {
test_logger.write_str(&format!("{}", e)).await?;
case_listener_proxy
.finished(TestResult { status: Some(Status::Failed), ..TestResult::EMPTY })
.map_err(RunTestError::SendFinish)?;
return Ok(());
}
args.extend(user_args.clone());
}
// run test.
// Load bearing to hold job guard.
let (process, _job, stdlogger) =
launch_component_process::<RunTestError>(&component, names, args).await?;
let mut last_line_excluded = false;
let mut socket_buf = vec![0u8; SOCKET_BUFFER_SIZE];
let mut socket = stdlogger.take_socket();
while let Some(bytes_read) =
NonZeroUsize::new(socket.read(&mut socket_buf[..]).await.map_err(LogError::Read)?)
{
let mut bytes = &socket_buf[..bytes_read.get()];
// Avoid printing trailing empty line
if *bytes.last().unwrap() == NEWLINE {
bytes = &bytes[..bytes.len() - 1];
}
let mut iter = bytes.split(|&x| x == NEWLINE);
while let Some(line) = iter.next() {
if line.len() == 0 && last_line_excluded {
// sometimes excluded lines print two newlines, we don't want to print blank
// output to user's screen.
continue;
}
last_line_excluded = PREFIXES_TO_EXCLUDE.iter().any(|p| line.starts_with(p));
if !last_line_excluded {
let line = [line, &[NEWLINE]].concat();
test_logger.write(&line).await?;
}
}
}
debug!("Waiting for test to finish: {}", test);
// wait for test to end.
fasync::OnSignals::new(&process, zx::Signals::PROCESS_TERMINATED)
.await
.map_err(KernelError::ProcessExit)
.unwrap();
let process_info = process.info().map_err(RunTestError::ProcessInfo)?;
// gtest returns 0 is test succeeds and 1 if test fails. This will test if test ended abnormally.
if process_info.return_code != 0 && process_info.return_code != 1 {
test_logger.write_str("Test exited abnormally\n").await?;
case_listener_proxy
.finished(TestResult { status: Some(Status::Failed), ..TestResult::EMPTY })
.map_err(RunTestError::SendFinish)?;
return Ok(());
}
debug!("Opening output file for {}", test);
// read test result file.
let result_str = match read_file(&self.output_dir_proxy, &test_list_file).await {
Ok(b) => b,
Err(e) => {
// TODO(fxbug.dev/45857): Introduce Status::InternalError.
test_logger
.write_str(&format!("Error reading test result:{:?}\n", IoError::File(e)))
.await?;
case_listener_proxy
.finished(TestResult { status: Some(Status::Failed), ..TestResult::EMPTY })
.map_err(RunTestError::SendFinish)?;
return Ok(());
}
};
debug!("parse output file for {}", test);
let test_list: TestOutput =
serde_json::from_str(&result_str).map_err(RunTestError::JsonParse)?;
debug!("parsed output file for {}", test);
// parse test results.
if test_list.testsuites.len() != 1 || test_list.testsuites[0].testsuite.len() != 1 {
// TODO(fxbug.dev/45857): Introduce Status::InternalError.
test_logger
.write_str("unexpected output, should have received exactly one test result.\n")
.await?;
case_listener_proxy
.finished(TestResult { status: Some(Status::Failed), ..TestResult::EMPTY })
.map_err(RunTestError::SendFinish)?;
return Ok(());
}
// as we only run one test per iteration result would be always at 0 index in the arrays.
let test_suite = &test_list.testsuites[0].testsuite[0];
let test_status = match &test_suite.status {
IndividualTestOutputStatus::NotRun => Status::Skipped,
IndividualTestOutputStatus::Run => {
match &test_suite.failures {
Some(_failures) => {
// TODO(fxbug.dev/53955): re-enable. currently we are getting these logs from test's
// stdout which we are printing above.
//for f in failures {
// test_logger.write_str(format!("failure: {}\n", f.failure)).await?;
// }
Status::Failed
}
None => Status::Passed,
}
}
};
case_listener_proxy
.finished(TestResult { status: Some(test_status), ..TestResult::EMPTY })
.map_err(RunTestError::SendFinish)?;
debug!("test finish {}", test);
Ok(())
}
pub fn validate_args(args: &Vec<String>) -> Result<(), ArgumentError> {
let restricted_flags = args
.iter()
.filter(|arg| {
for r_flag in RESTRICTED_FLAGS.iter() {
if arg.starts_with(r_flag) {
return true;
}
}
return false;
})
.map(|s| s.clone())
.collect::<Vec<_>>()
.join(", ");
if restricted_flags.len() > 0 {
return Err(ArgumentError::RestrictedArg(restricted_flags));
}
Ok(())
}
}
/// Internal, uncached implementation of `enumerate_tests`.
async fn get_tests(
component: Arc<Component>,
test_data_namespace: fproc::NameInfo,
output_dir_proxy: Arc<fio::DirectoryProxy>,
) -> Result<Vec<TestCaseInfo>, EnumerationError> {
let names = vec![test_data_namespace];
let test_list_file = Path::new("test_list.json");
let test_list_path = Path::new("/test_data").join(test_list_file);
let args = vec![
"--gtest_list_tests".to_owned(),
format!("--gtest_output=json:{}", test_list_path.display()),
];
// Load bearing to hold job guard.
let (process, _job, stdlogger) =
launch_component_process::<EnumerationError>(&component, names, args).await?;
// collect stdout in background before waiting for process termination.
let std_reader = LogStreamReader::new(stdlogger);
fasync::OnSignals::new(&process, zx::Signals::PROCESS_TERMINATED)
.await
.map_err(KernelError::ProcessExit)
.unwrap();
let process_info = process.info().map_err(KernelError::ProcessInfo).unwrap();
if process_info.return_code != 0 {
let logs = std_reader.get_logs().await?;
// TODO(fxbug.dev/4610): logs might not be utf8, fix the code.
let output = from_utf8(&logs)?;
// TODO(fxbug.dev/45858): Add a error logger to API so that we can display test stdout logs.
error!("Failed getting list of tests:\n{}", output);
return Err(EnumerationError::ListTest);
}
let result_str = match read_file(&output_dir_proxy, &test_list_file).await {
Ok(b) => b,
Err(e) => {
let logs = std_reader.get_logs().await?;
// TODO(fxbug.dev/4610): logs might not be utf8, fix the code.
let output = from_utf8(&logs)?;
error!("Failed getting list of tests from {}:\n{}", test_list_file.display(), output);
return Err(IoError::File(e).into());
}
};
let test_list: ListTestResult =
serde_json::from_str(&result_str).map_err(EnumerationError::from)?;
let mut tests = Vec::<TestCaseInfo>::with_capacity(test_list.tests);
for suite in &test_list.testsuites {
for test in &suite.testsuite {
let name = format!("{}.{}", suite.name, test.name);
let enabled = is_test_case_enabled(&name);
tests.push(TestCaseInfo { name, enabled })
}
}
Ok(tests)
}
/// Returns `true` if the test case is disabled, based on its name. (This is apparently the only
/// way that gtest tests can be disabled.)
/// See
/// https://github.com/google/googletest/blob/HEAD/googletest/docs/advanced.md#temporarily-disabling-tests
fn is_test_case_enabled(case_name: &str) -> bool {
!case_name.contains("DISABLED_")
}
/// Convenience wrapper around [`launch::launch_process`].
async fn launch_component_process<E>(
component: &Component,
names: Vec<fproc::NameInfo>,
args: Vec<String>,
) -> Result<(zx::Process, launch::ScopedJob, LoggerStream), E>
where
E: From<NamespaceError> + From<launch::LaunchError>,
{
Ok(launch::launch_process(launch::LaunchProcessArgs {
bin_path: &component.binary,
process_name: &component.name,
job: Some(component.job.create_child_job().map_err(KernelError::CreateJob).unwrap()),
ns: component.ns.clone().map_err(NamespaceError::Clone)?,
args: Some(args),
name_infos: Some(names),
environs: None,
handle_infos: None,
})
.await?)
}
#[cfg(test)]
mod tests {
use {
super::*,
anyhow::{Context as _, Error},
fidl::endpoints::ClientEnd,
fidl_fuchsia_component_runner as fcrunner,
fidl_fuchsia_test::{RunListenerMarker, RunOptions, SuiteMarker},
fio::OPEN_RIGHT_WRITABLE,
fuchsia_runtime::job_default,
pretty_assertions::assert_eq,
runner::component::ComponentNamespace,
runner::component::ComponentNamespaceError,
std::convert::TryFrom,
std::fs,
test_runners_test_lib::{
assert_event_ord, collect_listener_event, names_to_invocation, ListenerEvent,
},
uuid::Uuid,
};
struct TestDataDir {
dir_name: String,
}
impl TestDataDir {
fn new() -> Result<Self, Error> {
let dir = format!("/tmp/{}", Uuid::new_v4().to_simple());
fs::create_dir(&dir).context("cannot create test output directory")?;
Ok(Self { dir_name: dir })
}
fn proxy(&self) -> Result<fio::DirectoryProxy, Error> {
io_util::open_directory_in_namespace(
&self.dir_name,
OPEN_RIGHT_READABLE | OPEN_RIGHT_WRITABLE,
)
.context("Cannot open test data directory")
}
}
impl Drop for TestDataDir {
fn drop(&mut self) {
fs::remove_dir_all(&self.dir_name).expect("can't delete temp dir");
}
}
#[test]
fn validate_args_test() {
let restricted_flags = vec![
"--gtest_filter",
"--gtest_filter=mytest",
"--gtest_output",
"--gtest_output=json",
];
for flag in restricted_flags {
let args = vec![flag.to_string()];
let err = TestServer::validate_args(&args)
.expect_err(&format!("should error out for flag: {}", flag));
match err {
ArgumentError::RestrictedArg(f) => assert_eq!(f, flag),
}
}
let allowed_flags = vec!["--gtest_anyotherflag", "--anyflag", "--mycustomflag"];
for flag in allowed_flags {
let args = vec![flag.to_string()];
TestServer::validate_args(&args)
.expect(&format!("should not error out for flag: {}", flag));
}
}
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),
..fcrunner::ComponentNamespaceEntry::EMPTY
});
}
ComponentNamespace::try_from(ns)
}
macro_rules! current_job {
() => {
job_default().duplicate(zx::Rights::SAME_RIGHTS)?
};
}
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/gtest_runner_sample_tests".to_owned(),
args: vec![],
ns,
job: current_job!(),
}))
}
#[fuchsia_async::run_singlethreaded(test)]
async fn can_enumerate_sample_test() -> Result<(), Error> {
let test_data = TestDataDir::new()?;
let component = sample_test_component()?;
let server =
TestServer::new(test_data.proxy()?, "some_name".to_owned(), "some_path".to_owned());
assert_eq!(
*server.enumerate_tests(component.clone()).await?,
vec![
TestCaseInfo { name: "SampleTest1.SimpleFail".to_owned(), enabled: true },
TestCaseInfo { name: "SampleTest1.Crashing".to_owned(), enabled: true },
TestCaseInfo { name: "SampleTest2.SimplePass".to_owned(), enabled: true },
TestCaseInfo { name: "SampleFixture.Test1".to_owned(), enabled: true },
TestCaseInfo { name: "SampleFixture.Test2".to_owned(), enabled: true },
TestCaseInfo {
name: "SampleDisabled.DISABLED_TestPass".to_owned(),
enabled: false
},
TestCaseInfo {
name: "SampleDisabled.DISABLED_TestFail".to_owned(),
enabled: false
},
TestCaseInfo { name: "SampleDisabled.DynamicSkip".to_owned(), enabled: true },
TestCaseInfo { name: "WriteToStdout.TestPass".to_owned(), enabled: true },
TestCaseInfo { name: "WriteToStdout.TestFail".to_owned(), enabled: true },
TestCaseInfo {
name: "Tests/SampleParameterizedTestFixture.Test/0".to_owned(),
enabled: true,
},
TestCaseInfo {
name: "Tests/SampleParameterizedTestFixture.Test/1".to_owned(),
enabled: true,
},
TestCaseInfo {
name: "Tests/SampleParameterizedTestFixture.Test/2".to_owned(),
enabled: true,
},
TestCaseInfo {
name: "Tests/SampleParameterizedTestFixture.Test/3".to_owned(),
enabled: true,
},
]
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn can_enumerate_test_with_custom_args() -> Result<(), Error> {
let test_data = TestDataDir::new()?;
let ns = create_ns_from_current_ns(vec![("/pkg", OPEN_RIGHT_READABLE)])?;
let component = Arc::new(Component {
url: "fuchsia-pkg://fuchsia.com/test_with_custom_args#test.cm".to_owned(),
name: "test.cm".to_owned(),
binary: "bin/gtest_runner_test_with_custom_args".to_owned(),
args: vec!["--my_custom_arg".to_owned()],
ns,
job: current_job!(),
});
let server =
TestServer::new(test_data.proxy()?, "some_name".to_owned(), "some_path".to_owned());
assert_eq!(
*server.enumerate_tests(component.clone()).await?,
vec![TestCaseInfo { name: "TestArg.TestArg".to_owned(), enabled: true },]
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn can_enumerate_empty_test_file() -> Result<(), Error> {
let test_data = TestDataDir::new()?;
let ns = create_ns_from_current_ns(vec![("/pkg", OPEN_RIGHT_READABLE)])?;
let component = Arc::new(Component {
url: "fuchsia-pkg://fuchsia.com/sample_test#test.cm".to_owned(),
name: "test.cm".to_owned(),
binary: "bin/gtest_runner_no_tests".to_owned(),
args: vec![],
ns,
job: current_job!(),
});
let server =
TestServer::new(test_data.proxy()?, "some_name".to_owned(), "some_path".to_owned());
assert_eq!(*server.enumerate_tests(component.clone()).await?, Vec::<TestCaseInfo>::new());
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn enumerate_huge_tests() -> Result<(), Error> {
let test_data = TestDataDir::new()?;
let ns = create_ns_from_current_ns(vec![("/pkg", OPEN_RIGHT_READABLE)])?;
let component = Arc::new(Component {
url: "fuchsia-pkg://fuchsia.com/huge_test#test.cm".to_owned(),
name: "test.cm".to_owned(),
binary: "bin/huge_gtest_runner_example".to_owned(),
args: vec![],
ns,
job: current_job!(),
});
let server =
TestServer::new(test_data.proxy()?, "some_name".to_owned(), "some_path".to_owned());
let mut expected = vec![];
for i in 0..1000 {
let name = format!("HugeStress/HugeTest.Test/{}", i);
expected.push(TestCaseInfo { name, enabled: true });
}
assert_eq!(*server.enumerate_tests(component.clone()).await?, expected);
Ok(())
}
async fn run_tests(
invocations: Vec<Invocation>,
run_options: RunOptions,
component: Option<Arc<Component>>,
) -> Result<Vec<ListenerEvent>, anyhow::Error> {
let test_data = TestDataDir::new().context("Cannot create test data")?;
let component =
component.unwrap_or(sample_test_component().context("Cannot create test component")?);
let weak_component = Arc::downgrade(&component);
let server = TestServer::new(
test_data.proxy().context("Cannot create test server")?,
"some_name".to_owned(),
"some_path".to_owned(),
);
let (run_listener_client, run_listener) =
fidl::endpoints::create_request_stream::<RunListenerMarker>()
.context("Failed to create run_listener")?;
let (test_suite_client, test_suite) =
fidl::endpoints::create_request_stream::<SuiteMarker>()
.context("failed to create suite")?;
let suite_proxy =
test_suite_client.into_proxy().context("can't convert suite into proxy")?;
fasync::Task::spawn(async move {
server
.serve_test_suite(test_suite, weak_component)
.await
.expect("Failed to run test suite")
})
.detach();
suite_proxy
.run(&mut invocations.into_iter().map(|i| i.into()), run_options, run_listener_client)
.context("cannot call run")?;
collect_listener_event(run_listener).await.context("Failed to collect results")
}
#[fuchsia_async::run_singlethreaded(test)]
async fn run_multiple_tests() -> Result<(), Error> {
fuchsia_syslog::init_with_tags(&["gtest_runner_test"]).expect("cannot init logger");
let events = run_tests(
names_to_invocation(vec![
"SampleTest1.SimpleFail",
"SampleTest1.Crashing",
"SampleTest2.SimplePass",
"Tests/SampleParameterizedTestFixture.Test/2",
]),
RunOptions {
include_disabled_tests: Some(false),
parallel: None,
arguments: None,
..RunOptions::EMPTY
},
None,
)
.await
.unwrap();
let expected_events = vec![
ListenerEvent::start_test("SampleTest1.SimpleFail"),
ListenerEvent::finish_test(
"SampleTest1.SimpleFail",
TestResult { status: Some(Status::Failed), ..TestResult::EMPTY },
),
ListenerEvent::start_test("SampleTest1.Crashing"),
ListenerEvent::finish_test(
"SampleTest1.Crashing",
TestResult { status: Some(Status::Failed), ..TestResult::EMPTY },
),
ListenerEvent::start_test("SampleTest2.SimplePass"),
ListenerEvent::finish_test(
"SampleTest2.SimplePass",
TestResult { status: Some(Status::Passed), ..TestResult::EMPTY },
),
ListenerEvent::start_test("Tests/SampleParameterizedTestFixture.Test/2"),
ListenerEvent::finish_test(
"Tests/SampleParameterizedTestFixture.Test/2",
TestResult { status: Some(Status::Passed), ..TestResult::EMPTY },
),
ListenerEvent::finish_all_test(),
];
assert_eq!(expected_events, events);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn run_test_with_custom_arg() -> Result<(), Error> {
fuchsia_syslog::init_with_tags(&["gtest_runner_test"]).expect("cannot init logger");
let ns = create_ns_from_current_ns(vec![("/pkg", OPEN_RIGHT_READABLE)])?;
let component = Arc::new(Component {
url: "fuchsia-pkg://fuchsia.com/test_with_arg#test.cm".to_owned(),
name: "test.cm".to_owned(),
binary: "bin/gtest_runner_test_with_custom_args".to_owned(),
args: vec!["--my_custom_arg".to_owned()],
ns,
job: current_job!(),
});
let events = run_tests(
names_to_invocation(vec!["TestArg.TestArg"]),
RunOptions {
include_disabled_tests: Some(false),
parallel: None,
arguments: Some(vec!["--my_custom_arg2".to_owned()]),
..RunOptions::EMPTY
},
Some(component),
)
.await
.unwrap();
let expected_events = vec![
ListenerEvent::start_test("TestArg.TestArg"),
ListenerEvent::finish_test(
"TestArg.TestArg",
TestResult { status: Some(Status::Passed), ..TestResult::EMPTY },
),
ListenerEvent::finish_all_test(),
];
assert_eq!(expected_events, events);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn run_multiple_tests_parallel() -> Result<(), Error> {
fuchsia_syslog::init_with_tags(&["gtest_runner_test"]).expect("cannot init logger");
let mut events = run_tests(
names_to_invocation(vec![
"SampleTest1.SimpleFail",
"SampleTest1.Crashing",
"SampleTest2.SimplePass",
"Tests/SampleParameterizedTestFixture.Test/0",
"Tests/SampleParameterizedTestFixture.Test/1",
"Tests/SampleParameterizedTestFixture.Test/2",
]),
RunOptions {
include_disabled_tests: Some(false),
parallel: Some(4),
arguments: None,
..RunOptions::EMPTY
},
None,
)
.await
.unwrap();
let mut expected_events = vec![
ListenerEvent::start_test("SampleTest1.SimpleFail"),
ListenerEvent::finish_test(
"SampleTest1.SimpleFail",
TestResult { status: Some(Status::Failed), ..TestResult::EMPTY },
),
ListenerEvent::start_test("SampleTest1.Crashing"),
ListenerEvent::finish_test(
"SampleTest1.Crashing",
TestResult { status: Some(Status::Failed), ..TestResult::EMPTY },
),
ListenerEvent::start_test("SampleTest2.SimplePass"),
ListenerEvent::finish_test(
"SampleTest2.SimplePass",
TestResult { status: Some(Status::Passed), ..TestResult::EMPTY },
),
ListenerEvent::start_test("Tests/SampleParameterizedTestFixture.Test/0"),
ListenerEvent::finish_test(
"Tests/SampleParameterizedTestFixture.Test/0",
TestResult { status: Some(Status::Passed), ..TestResult::EMPTY },
),
ListenerEvent::start_test("Tests/SampleParameterizedTestFixture.Test/1"),
ListenerEvent::finish_test(
"Tests/SampleParameterizedTestFixture.Test/1",
TestResult { status: Some(Status::Passed), ..TestResult::EMPTY },
),
ListenerEvent::start_test("Tests/SampleParameterizedTestFixture.Test/2"),
ListenerEvent::finish_test(
"Tests/SampleParameterizedTestFixture.Test/2",
TestResult { status: Some(Status::Passed), ..TestResult::EMPTY },
),
ListenerEvent::finish_all_test(),
];
assert_event_ord(&events);
expected_events.sort();
events.sort();
assert_eq!(expected_events, events);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn run_disabled_tests_exclude() -> Result<(), Error> {
fuchsia_syslog::init_with_tags(&["gtest_runner_test"]).expect("cannot init logger");
let events = run_tests(
names_to_invocation(vec![
"SampleDisabled.DISABLED_TestPass",
"SampleDisabled.DISABLED_TestFail",
]),
RunOptions {
include_disabled_tests: Some(false),
parallel: None,
arguments: None,
..RunOptions::EMPTY
},
None,
)
.await
.unwrap();
let expected_events = vec![
ListenerEvent::start_test("SampleDisabled.DISABLED_TestPass"),
ListenerEvent::finish_test(
"SampleDisabled.DISABLED_TestPass",
TestResult { status: Some(Status::Skipped), ..TestResult::EMPTY },
),
ListenerEvent::start_test("SampleDisabled.DISABLED_TestFail"),
ListenerEvent::finish_test(
"SampleDisabled.DISABLED_TestFail",
TestResult { status: Some(Status::Skipped), ..TestResult::EMPTY },
),
ListenerEvent::finish_all_test(),
];
assert_eq!(expected_events, events);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn run_no_test() -> Result<(), Error> {
let events = run_tests(
vec![],
RunOptions {
include_disabled_tests: Some(false),
parallel: None,
arguments: None,
..RunOptions::EMPTY
},
None,
)
.await
.unwrap();
let expected_events = vec![ListenerEvent::finish_all_test()];
assert_eq!(expected_events, events);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn run_one_test() -> Result<(), Error> {
let events = run_tests(
names_to_invocation(vec!["SampleTest2.SimplePass"]),
RunOptions {
include_disabled_tests: Some(false),
parallel: None,
arguments: None,
..RunOptions::EMPTY
},
None,
)
.await
.unwrap();
let expected_events = vec![
ListenerEvent::start_test("SampleTest2.SimplePass"),
ListenerEvent::finish_test(
"SampleTest2.SimplePass",
TestResult { status: Some(Status::Passed), ..TestResult::EMPTY },
),
ListenerEvent::finish_all_test(),
];
assert_eq!(expected_events, events);
Ok(())
}
}