| // 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_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>>, |
| |
| /// |harness|: 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, |
| }; |
| } |
| } |
| |
| /// Runs test defined by `url`, and writes logs to writer. |
| /// |timeout|: Test timeout.should be more than zero. |
| /// |harness|: HarnessProxy that manages running the tests. |
| pub async fn run_test<W: Write>( |
| test_params: TestParams, |
| writer: &mut W, |
| ) -> Result<RunResult, anyhow::Error> { |
| let mut timeout = match test_params.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 run_options = TestRunOptions { |
| disabled_tests: test_params.disabled_tests(), |
| parallel: test_params.parallel, |
| arguments: test_params.test_args, |
| }; |
| |
| let test_fut = test_executor::run_v2_test_component( |
| test_params.harness, |
| test_params.test_url, |
| sender, |
| test_params.test_filter.as_ref().map(String::as_str), |
| 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 test defined by `test_url`, and writes logs to stdout. |
| /// |timeout|: Test timeout.should be more than zero. |
| /// |test_filter|: Glob filter for matching tests to run. |
| /// |harness|: HarnessProxy that manages running the tests. |
| pub async fn run_tests_and_get_outcome(test_params: TestParams) -> Outcome { |
| let test_url = test_params.test_url.clone(); |
| println!("\nRunning test '{}'", &test_url); |
| |
| let mut stdout = io::stdout(); |
| |
| let RunResult { outcome, executed, passed, successful_completion } = |
| match run_test(test_params, &mut stdout).await { |
| Ok(run_result) => run_result, |
| Err(err) => { |
| println!("Test suite encountered error trying to run tests: {:?}", err); |
| return Outcome::Error; |
| } |
| }; |
| |
| 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); |
| } |
| |
| outcome |
| } |