blob: 1eca0642d0aa3f80c681bf1179d10eb89019bc30 [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 {
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);
}
}