| // 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 { |
| anyhow::{format_err, Context, Error}, |
| fidl::endpoints, |
| fidl::endpoints::ClientEnd, |
| fidl::endpoints::Proxy, |
| fidl_fuchsia_component as fcomponent, fidl_fuchsia_component_decl as fdecl, |
| fidl_fuchsia_component_runner as fcrunner, fidl_fuchsia_io as fio, |
| fidl_fuchsia_test::{ |
| CaseListenerRequest::Finished, |
| Invocation, Result_ as TestResult, |
| RunListenerRequest::{OnFinished, OnTestCaseStarted}, |
| RunListenerRequestStream, |
| }, |
| fidl_fuchsia_test_manager as ftest_manager, fuchsia_async as fasync, |
| fuchsia_component::client::{self, connect_to_protocol_at_dir_root}, |
| fuchsia_runtime::job_default, |
| fuchsia_zircon as zx, |
| futures::channel::mpsc, |
| futures::prelude::*, |
| runner::component::ComponentNamespace, |
| runner::component::ComponentNamespaceError, |
| std::collections::HashMap, |
| std::convert::TryFrom, |
| std::sync::Arc, |
| test_manager_test_lib::RunEvent, |
| test_runners_lib::elf::{BuilderArgs, Component}, |
| }; |
| |
| #[derive(PartialEq, Debug)] |
| pub enum ListenerEvent { |
| StartTest(String), |
| FinishTest(String, TestResult), |
| FinishAllTests, |
| } |
| |
| fn get_ord_index_and_name(event: &ListenerEvent) -> (usize, &str) { |
| match event { |
| ListenerEvent::StartTest(name) => (0, name), |
| ListenerEvent::FinishTest(name, _) => (1, name), |
| ListenerEvent::FinishAllTests => (2, ""), |
| } |
| } |
| |
| // Orders by test name and then event type. |
| impl Ord for ListenerEvent { |
| fn cmp(&self, other: &Self) -> core::cmp::Ordering { |
| let (s_index, s_test_name) = get_ord_index_and_name(self); |
| let (o_index, o_test_name) = get_ord_index_and_name(other); |
| if s_test_name == o_test_name || s_index == 2 || o_index == 2 { |
| return s_index.cmp(&o_index); |
| } |
| return s_test_name.cmp(&o_test_name); |
| } |
| } |
| |
| // Makes sure that FinishTest event never shows up before StartTest and FinishAllTests is always |
| // last. |
| pub fn assert_event_ord(events: &Vec<ListenerEvent>) { |
| let mut tests = HashMap::new(); |
| let mut all_finish = false; |
| for event in events { |
| assert!(!all_finish, "got FinishAllTests event twice: {:#?}", events); |
| match event { |
| ListenerEvent::StartTest(name) => { |
| assert!( |
| !tests.contains_key(&name), |
| "Multiple StartTest for test {}: {:#?}", |
| name, |
| events |
| ); |
| tests.insert(name, false); |
| } |
| ListenerEvent::FinishTest(name, _) => { |
| assert!( |
| tests.contains_key(&name), |
| "Got finish before start event for test {}: {:#?}", |
| name, |
| events |
| ); |
| assert!( |
| !tests.insert(name, true).unwrap(), |
| "Multiple FinishTest for test {}: {:#?}", |
| name, |
| events |
| ); |
| } |
| ListenerEvent::FinishAllTests => { |
| all_finish = true; |
| } |
| } |
| } |
| } |
| |
| impl PartialOrd for ListenerEvent { |
| fn partial_cmp(&self, other: &ListenerEvent) -> Option<core::cmp::Ordering> { |
| Some(self.cmp(other)) |
| } |
| } |
| |
| impl Eq for ListenerEvent {} |
| |
| impl ListenerEvent { |
| pub fn start_test(name: &str) -> ListenerEvent { |
| ListenerEvent::StartTest(name.to_string()) |
| } |
| pub fn finish_test(name: &str, test_result: TestResult) -> ListenerEvent { |
| ListenerEvent::FinishTest(name.to_string(), test_result) |
| } |
| pub fn finish_all_test() -> ListenerEvent { |
| ListenerEvent::FinishAllTests |
| } |
| } |
| |
| impl Clone for ListenerEvent { |
| fn clone(&self) -> Self { |
| match self { |
| ListenerEvent::StartTest(name) => ListenerEvent::start_test(name), |
| ListenerEvent::FinishTest(name, test_result) => ListenerEvent::finish_test( |
| name, |
| TestResult { status: test_result.status.clone(), ..TestResult::EMPTY }, |
| ), |
| ListenerEvent::FinishAllTests => ListenerEvent::finish_all_test(), |
| } |
| } |
| } |
| |
| /// Collects all the listener event as they come and return in a vector. |
| pub async fn collect_listener_event( |
| mut listener: RunListenerRequestStream, |
| ) -> Result<Vec<ListenerEvent>, Error> { |
| let mut ret = vec![]; |
| // collect loggers so that they do not die. |
| let mut loggers = vec![]; |
| while let Some(result_event) = listener.try_next().await? { |
| match result_event { |
| OnTestCaseStarted { invocation, std_handles, listener, .. } => { |
| let name = invocation.name.unwrap(); |
| ret.push(ListenerEvent::StartTest(name.clone())); |
| loggers.push(std_handles); |
| let mut listener = listener.into_stream()?; |
| // We want exhaustive match, and if we add more variants in the future we'd need to |
| // handle the requests in a loop, so allow this lint violation. |
| #[allow(clippy::never_loop)] |
| while let Some(result) = listener.try_next().await? { |
| match result { |
| Finished { result, .. } => { |
| ret.push(ListenerEvent::FinishTest(name, result)); |
| break; |
| } |
| } |
| } |
| } |
| OnFinished { .. } => { |
| ret.push(ListenerEvent::FinishAllTests); |
| break; |
| } |
| } |
| } |
| Ok(ret) |
| } |
| |
| /// Helper method to convert names to `Invocation`. |
| pub fn names_to_invocation(names: Vec<&str>) -> Vec<Invocation> { |
| names |
| .iter() |
| .map(|s| Invocation { name: Some(s.to_string()), tag: None, ..Invocation::EMPTY }) |
| .collect() |
| } |
| |
| // process events by parsing and normalizing logs. Returns `RunEvents` and collected logs. |
| pub async fn process_events( |
| suite_instance: test_manager_test_lib::SuiteRunInstance, |
| exclude_empty_logs: bool, |
| ) -> Result<(Vec<RunEvent>, Vec<String>), Error> { |
| let (sender, mut recv) = mpsc::channel(1); |
| let execution_task = |
| fasync::Task::spawn(async move { suite_instance.collect_events(sender).await }); |
| let mut events = vec![]; |
| let mut log_tasks = vec![]; |
| let mut buffered_stdout = HashMap::new(); |
| let mut buffered_stderr = HashMap::new(); |
| while let Some(event) = recv.next().await { |
| match event.payload { |
| test_manager_test_lib::SuiteEventPayload::RunEvent(RunEvent::CaseStdout { |
| name, |
| stdout_message, |
| }) => { |
| let strings = line_buffer_std_message( |
| &name, |
| stdout_message, |
| exclude_empty_logs, |
| &mut buffered_stdout, |
| ); |
| for s in strings { |
| events.push(RunEvent::case_stdout(name.clone(), s)); |
| } |
| } |
| test_manager_test_lib::SuiteEventPayload::RunEvent(RunEvent::CaseStderr { |
| name, |
| stderr_message, |
| }) => { |
| let strings = line_buffer_std_message( |
| &name, |
| stderr_message, |
| exclude_empty_logs, |
| &mut buffered_stderr, |
| ); |
| for s in strings { |
| events.push(RunEvent::case_stderr(name.clone(), s)); |
| } |
| } |
| test_manager_test_lib::SuiteEventPayload::RunEvent(e) => events.push(e), |
| test_manager_test_lib::SuiteEventPayload::SuiteLog { log_stream } => { |
| let t = fasync::Task::spawn(log_stream.collect::<Vec<_>>()); |
| log_tasks.push(t); |
| } |
| test_manager_test_lib::SuiteEventPayload::TestCaseLog { .. } => { |
| panic!("not supported yet!") |
| } |
| } |
| } |
| execution_task.await.context("test execution failed")?; |
| |
| for (name, log) in buffered_stdout { |
| events.push(RunEvent::case_stdout(name, log)); |
| } |
| for (name, log) in buffered_stderr { |
| events.push(RunEvent::case_stderr(name, log)); |
| } |
| |
| let mut collected_logs = vec![]; |
| for t in log_tasks { |
| let logs = t.await; |
| for log_result in logs { |
| let log = log_result?; |
| collected_logs.push(log.msg().unwrap().to_string()); |
| } |
| } |
| |
| Ok((events, collected_logs)) |
| } |
| |
| // Process stdout/stderr messages and return Vec of processed strings |
| fn line_buffer_std_message( |
| name: &str, |
| std_message: String, |
| exclude_empty_logs: bool, |
| buffer: &mut HashMap<String, String>, |
| ) -> Vec<String> { |
| let mut ret = vec![]; |
| let logs = std_message.split("\n"); |
| let mut logs = logs.collect::<Vec<&str>>(); |
| // discard last empty log(if it ended in newline, or store im-complete line) |
| let mut last_incomplete_line = logs.pop(); |
| if std_message.as_bytes().last() == Some(&b'\n') { |
| last_incomplete_line = None; |
| } |
| for log in logs { |
| if exclude_empty_logs && log.len() == 0 { |
| continue; |
| } |
| let mut msg = log.to_owned(); |
| // This is only executed for first log line and used to concat previous |
| // buffered line. |
| if let Some(prev_log) = buffer.remove(name) { |
| msg = format!("{}{}", prev_log, msg); |
| } |
| ret.push(msg); |
| } |
| if let Some(log) = last_incomplete_line { |
| let mut log = log.to_owned(); |
| if let Some(prev_log) = buffer.remove(name) { |
| log = format!("{}{}", prev_log, log); |
| } |
| buffer.insert(name.to_string(), log); |
| } |
| ret |
| } |
| |
| // Binds to test manager component and returns run builder service. |
| pub async fn connect_to_test_manager() -> Result<ftest_manager::RunBuilderProxy, Error> { |
| let realm = client::connect_to_protocol::<fcomponent::RealmMarker>() |
| .context("could not connect to Realm service")?; |
| |
| let mut child_ref = fdecl::ChildRef { name: "test_manager".to_owned(), collection: None }; |
| let (dir, server_end) = endpoints::create_proxy::<fio::DirectoryMarker>()?; |
| realm |
| .open_exposed_dir(&mut child_ref, server_end) |
| .await |
| .context("open_exposed_dir fidl call failed for test manager")? |
| .map_err(|e| format_err!("failed to create test manager: {:?}", e))?; |
| |
| connect_to_protocol_at_dir_root::<ftest_manager::RunBuilderMarker>(&dir) |
| .context("failed to open test suite service") |
| } |
| |
| fn create_ns_from_current_ns( |
| dir_paths: Vec<(&str, fio::OpenFlags)>, |
| ) -> Result<ComponentNamespace, ComponentNamespaceError> { |
| let mut ns = vec![]; |
| for (path, permission) in dir_paths { |
| let chan = fuchsia_fs::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) |
| } |
| |
| /// Create a new component object for testing purposes. |
| pub async fn test_component( |
| url: &str, |
| name: &str, |
| binary: &str, |
| args: Vec<String>, |
| ) -> Result<Arc<Component>, Error> { |
| let ns = create_ns_from_current_ns(vec![( |
| "/pkg", |
| fuchsia_fs::OpenFlags::RIGHT_READABLE | fuchsia_fs::OpenFlags::RIGHT_EXECUTABLE, |
| )])?; |
| let component = Component::create_for_tests(BuilderArgs { |
| url: url.to_string(), |
| name: name.to_string(), |
| binary: binary.to_string(), |
| args, |
| environ: None, |
| ns, |
| job: job_default().duplicate(zx::Rights::SAME_RIGHTS)?, |
| }) |
| .await?; |
| Ok(Arc::new(component)) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use fidl_fuchsia_test::Status; |
| use maplit::hashmap; |
| |
| #[test] |
| fn test_ordering_by_enum() { |
| let expected_events = vec![ |
| ListenerEvent::start_test("a"), |
| ListenerEvent::finish_test( |
| "a", |
| TestResult { status: Some(Status::Passed), ..TestResult::EMPTY }, |
| ), |
| ListenerEvent::finish_all_test(), |
| ]; |
| |
| let mut events = expected_events.clone(); |
| events.reverse(); |
| |
| assert_ne!(events, expected_events); |
| events.sort(); |
| assert_eq!(events, expected_events); |
| } |
| |
| #[test] |
| fn test_ordering_by_test_name() { |
| let mut events = vec![ |
| ListenerEvent::start_test("b"), |
| ListenerEvent::start_test("a"), |
| ListenerEvent::finish_test( |
| "a", |
| TestResult { status: Some(Status::Passed), ..TestResult::EMPTY }, |
| ), |
| ListenerEvent::start_test("c"), |
| ListenerEvent::finish_test( |
| "b", |
| TestResult { status: Some(Status::Passed), ..TestResult::EMPTY }, |
| ), |
| ListenerEvent::finish_test( |
| "c", |
| TestResult { status: Some(Status::Passed), ..TestResult::EMPTY }, |
| ), |
| ListenerEvent::finish_all_test(), |
| ]; |
| |
| let expected_events = vec![ |
| ListenerEvent::start_test("a"), |
| ListenerEvent::finish_test( |
| "a", |
| TestResult { status: Some(Status::Passed), ..TestResult::EMPTY }, |
| ), |
| ListenerEvent::start_test("b"), |
| ListenerEvent::finish_test( |
| "b", |
| TestResult { status: Some(Status::Passed), ..TestResult::EMPTY }, |
| ), |
| ListenerEvent::start_test("c"), |
| ListenerEvent::finish_test( |
| "c", |
| TestResult { status: Some(Status::Passed), ..TestResult::EMPTY }, |
| ), |
| ListenerEvent::finish_all_test(), |
| ]; |
| events.sort(); |
| assert_eq!(events, expected_events); |
| } |
| |
| #[test] |
| fn line_buffer_std_message_incomplete_line() { |
| let mut buf = HashMap::new(); |
| buf.insert("test".to_string(), "some_prev_text".to_string()); |
| let strings = line_buffer_std_message("test", "a \nb\nc\nd".into(), false, &mut buf); |
| assert_eq!(strings, vec!["some_prev_texta ".to_owned(), "b".to_owned(), "c".to_owned()]); |
| assert_eq!(buf, hashmap! {"test".to_string() => "d".to_string()}); |
| } |
| |
| #[test] |
| fn line_buffer_std_message_complete_line() { |
| let mut buf = HashMap::new(); |
| buf.insert("test".to_string(), "some_prev_text".to_string()); |
| let strings = line_buffer_std_message("test", "a \nb\nc\n".into(), false, &mut buf); |
| assert_eq!(strings, vec!["some_prev_texta ".to_owned(), "b".to_owned(), "c".to_owned()]); |
| assert_eq!(buf.len(), 0); |
| |
| // test when initial buf is empty |
| let strings = line_buffer_std_message("test", "d \ne\nf\n".into(), false, &mut buf); |
| assert_eq!(strings, vec!["d ".to_owned(), "e".to_owned(), "f".to_owned()]); |
| assert_eq!(buf.len(), 0); |
| } |
| } |