blob: 9e844b8a2562fe427727cbd0544a631f3d832930 [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.
// Used because we use `futures::select!`.
//
// From https://docs.rs/futures/0.3.1/futures/macro.select.html:
// Note that select! relies on proc-macro-hack, and may require to set the compiler's
// recursion limit very high, e.g. #![recursion_limit="1024"].
#![recursion_limit = "512"]
use {
fidl_fuchsia_test::Invocation,
fidl_fuchsia_test_manager::HarnessProxy,
fuchsia_async as fasync,
futures::{channel::mpsc, prelude::*},
std::collections::HashSet,
std::fmt,
std::io::{self, Write},
test_executor::{TestEvent, TestRunOptions},
};
pub use test_executor::DisabledTestHandling;
#[derive(PartialEq, Debug)]
pub enum Outcome {
Passed,
Failed,
Inconclusive,
Timedout,
Error,
}
impl fmt::Display for Outcome {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Outcome::Passed => write!(f, "PASSED"),
Outcome::Failed => write!(f, "FAILED"),
Outcome::Inconclusive => write!(f, "INCONCLUSIVE"),
Outcome::Timedout => write!(f, "TIMED OUT"),
Outcome::Error => write!(f, "ERROR"),
}
}
}
#[derive(PartialEq, Debug)]
pub struct RunResult {
/// Test outcome.
pub outcome: Outcome,
/// All tests which were executed.
pub executed: Vec<String>,
/// All tests which passed.
pub passed: Vec<String>,
/// Suite protocol completed without error.
pub successful_completion: bool,
}
// Parameters for test.
pub struct TestParams {
/// Test URL.
pub test_url: String,
/// |timeout|: Test timeout.should be more than zero.
pub timeout: Option<std::num::NonZeroU32>,
/// Filter tests based on this glob pattern.
pub test_filter: Option<String>,
// Run disabled tests.
pub also_run_disabled_tests: bool,
/// Test concurrency count.
pub parallel: Option<u16>,
/// Arguments to pass to test using command line.
pub test_args: Option<Vec<String>>,
/// HarnessProxy that manages running the tests.
pub harness: HarnessProxy,
}
impl TestParams {
fn disabled_tests(&self) -> DisabledTestHandling {
return match self.also_run_disabled_tests {
true => DisabledTestHandling::Include,
false => DisabledTestHandling::Exclude,
};
}
}
async fn run_test_for_invocations<W: Write>(
suite_instance: &test_executor::SuiteInstance,
invocations: Vec<Invocation>,
run_options: TestRunOptions,
timeout: Option<std::num::NonZeroU32>,
writer: &mut W,
) -> Result<RunResult, anyhow::Error> {
let mut timeout = match timeout {
Some(timeout) => futures::future::Either::Left(
fasync::Timer::new(std::time::Duration::from_secs(timeout.get().into()))
.map(|()| Err(())),
),
None => futures::future::Either::Right(futures::future::ready(Ok(()))),
}
.fuse();
let (sender, mut recv) = mpsc::channel(1);
let mut outcome = Outcome::Passed;
let mut test_cases_in_progress = HashSet::new();
let mut test_cases_executed = HashSet::new();
let mut test_cases_passed = HashSet::new();
let mut successful_completion = false;
let test_fut = suite_instance
.run_and_collect_results_for_invocations(sender, invocations, run_options)
.fuse();
futures::pin_mut!(test_fut);
loop {
futures::select! {
timeout_res = timeout => {
match timeout_res {
Ok(()) => {}, // No timeout specified.
Err(()) => {
outcome = Outcome::Timedout;
break
},
}
},
test_res = test_fut => {
let () = test_res?;
},
test_event = recv.next() => {
if let Some(test_event) = test_event {
match test_event {
TestEvent::TestCaseStarted { test_case_name } => {
if test_cases_executed.contains(&test_case_name) {
return Err(anyhow::anyhow!("test case: '{}' started twice", test_case_name));
}
writeln!(writer, "[RUNNING]\t{}", test_case_name).expect("Cannot write logs");
test_cases_in_progress.insert(test_case_name.clone());
test_cases_executed.insert(test_case_name);
}
TestEvent::TestCaseFinished { test_case_name, result } => {
if !test_cases_in_progress.contains(&test_case_name) {
return Err(anyhow::anyhow!(
"test case: '{}' was never started, still got a finish event",
test_case_name
));
}
test_cases_in_progress.remove(&test_case_name);
let result_str = match result {
test_executor::TestResult::Passed => {
test_cases_passed.insert(test_case_name.clone());
"PASSED"
}
test_executor::TestResult::Failed => {
if outcome == Outcome::Passed {
outcome = Outcome::Failed;
}
"FAILED"
}
test_executor::TestResult::Skipped => "SKIPPED",
test_executor::TestResult::Error => {
outcome = Outcome::Error;
"ERROR"
}
};
writeln!(writer, "[{}]\t{}", result_str, test_case_name)
.expect("Cannot write logs");
}
TestEvent::LogMessage { test_case_name, mut msg } => {
if !test_cases_executed.contains(&test_case_name) {
return Err(anyhow::anyhow!(
"test case: '{}' was never started, still got a log",
test_case_name
));
}
// check if last byte is newline and remove it as we are already
// printing a newline.
if msg.ends_with("\n") {
msg.truncate(msg.len()-1)
}
// TODO(anmittal): buffer by newline or something else.
writeln!(writer, "[output - {}]:\n{}", test_case_name, msg).expect("Cannot write logs");
}
TestEvent::Finish => {
successful_completion = true;
break;
}
}
}
},
complete => { break },
}
}
let mut test_cases_in_progress: Vec<String> = test_cases_in_progress.into_iter().collect();
test_cases_in_progress.sort();
if test_cases_in_progress.len() != 0 {
match outcome {
Outcome::Passed | Outcome::Failed => {
outcome = Outcome::Inconclusive;
}
_ => {}
}
writeln!(writer, "\nThe following test(s) never completed:").expect("Cannot write logs");
for t in test_cases_in_progress {
writeln!(writer, "{}", t).expect("Cannot write logs");
}
}
let mut test_cases_executed: Vec<String> = test_cases_executed.into_iter().collect();
let mut test_cases_passed: Vec<String> = test_cases_passed.into_iter().collect();
test_cases_executed.sort();
test_cases_passed.sort();
Ok(RunResult {
outcome,
executed: test_cases_executed,
passed: test_cases_passed,
successful_completion,
})
}
/// Runs the test `count` number of times, and writes logs to writer.
pub async fn run_test<'a, W: Write>(
test_params: TestParams,
count: u16,
writer: &'a mut W,
) -> impl Stream<Item = Result<RunResult, anyhow::Error>> + 'a {
let run_options = TestRunOptions {
disabled_tests: test_params.disabled_tests(),
parallel: test_params.parallel,
arguments: test_params.test_args.clone(),
};
struct FoldArgs<'a, W: Write> {
current_count: u16,
count: u16,
suite_instance: Option<test_executor::SuiteInstance>,
invocations: Option<Vec<fidl_fuchsia_test::Invocation>>,
test_params: TestParams,
run_options: TestRunOptions,
writer: &'a mut W,
}
let args = FoldArgs {
current_count: 0,
count,
suite_instance: None,
invocations: None,
test_params,
run_options,
writer,
};
stream::try_unfold(args, |mut args| async move {
if args.current_count >= args.count {
return Ok(None);
}
let suite_instance = match args.suite_instance {
Some(s) => s,
None => {
test_executor::SuiteInstance::new(
&args.test_params.harness,
&args.test_params.test_url,
)
.await?
}
};
let invocations = match args.invocations {
Some(i) => i,
None => {
suite_instance
.enumerate_tests(&args.test_params.test_filter.as_ref().map(String::as_str))
.await?
}
};
let mut next_count = args.current_count + 1;
let result = run_test_for_invocations(
&suite_instance,
invocations.clone(),
args.run_options.clone(),
args.test_params.timeout,
args.writer,
)
.await?;
if result.outcome == Outcome::Timedout || result.outcome == Outcome::Error {
// don't run test again
next_count = args.count;
}
args.suite_instance = Some(suite_instance);
args.invocations = Some(invocations);
args.current_count = next_count;
Ok(Some((result, args)))
})
}
/// Runs the test and writes logs to stdout.
/// |count|: Number of times to run this test.
pub async fn run_tests_and_get_outcome(
test_params: TestParams,
count: std::num::NonZeroU16,
) -> Outcome {
let test_url = test_params.test_url.clone();
println!("\nRunning test '{}'", &test_url);
let mut stdout = io::stdout();
let mut final_outcome = Outcome::Passed;
let stream = run_test(test_params, count.get(), &mut stdout).await;
futures::pin_mut!(stream);
let mut i: u16 = 1;
loop {
match stream.try_next().await {
Err(e) => {
println!("Test suite encountered error trying to run tests: {:?}", e);
return Outcome::Error;
}
Ok(Some(RunResult { outcome, executed, passed, successful_completion })) => {
if count.get() > 1 {
println!("\nTest run count {}/{}", i, count);
}
println!("{} out of {} tests passed...", passed.len(), executed.len());
println!("{} completed with result: {}", &test_url, outcome);
if !successful_completion {
println!("{} did not complete successfully.", &test_url);
}
i = i + 1;
if count.get() > 1 {
if outcome != Outcome::Passed {
final_outcome = Outcome::Failed;
}
} else {
final_outcome = outcome;
}
}
Ok(None) => {
break;
}
}
}
if count.get() > 1 && final_outcome != Outcome::Passed {
println!("One or more test runs failed.");
}
final_outcome
}