blob: 72d07bbff4f7c165b1f2993332cffc745b794658 [file] [log] [blame]
// Copyright 2023 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 {
crate::{helpers::*, results_parser::*},
anyhow::{Context, Error},
fidl::endpoints::create_proxy,
fidl_fuchsia_component_runner as frunner,
fidl_fuchsia_test::{self as ftest, Result_ as TestResult, Status},
ftest::CaseListenerProxy,
fuchsia_async as fasync, fuchsia_zircon as zx,
futures::{io::BufReader, AsyncBufReadExt},
gtest_runner_lib::parser::*,
std::collections::HashMap,
test_runners_lib::cases::TestCaseInfo,
};
const DYNAMIC_SKIP_RESULT: &str = "SKIPPED";
const LIST_TESTS_ARG: &str = "list_tests";
const FILTER_ARG: &str = "filter=";
const OUTPUT_PATH: &str = "/test_data/";
/// Runs the test component with `--gunit_list_tests` and returns the parsed test cases list.
pub async fn get_cases_list_for_gtests(
mut start_info: frunner::ComponentStartInfo,
component_runner: &frunner::ComponentRunnerProxy,
test_type: TestType,
) -> Result<Vec<ftest::Case>, Error> {
// Replace the program args to get test cases in a json file.
let list_tests_arg = format_arg(test_type, LIST_TESTS_ARG);
let output_file_name = unique_filename();
let output_path = format!("output=json:{}{}", OUTPUT_PATH, output_file_name);
let output_arg = format_arg(test_type, &output_path);
replace_program_args(
vec![list_tests_arg, output_arg],
start_info.program.as_mut().context("No program.")?,
);
let (outgoing_dir, _outgoing_dir_server) = zx::Channel::create();
start_info.outgoing_dir = Some(outgoing_dir.into());
start_info.numbered_handles = Some(vec![]);
let output_dir = add_output_dir_to_namespace(&mut start_info)?;
let component_controller = start_test_component(start_info, component_runner)?;
let _ = read_result(component_controller.take_event_stream()).await;
// Parse tests from output file.
let read_content = read_file(&output_dir, &output_file_name)
.await
.expect("Failed to read tests from output file.");
let tests = parse_test_cases(read_content).expect("Failed to parse tests.");
Ok(tests
.iter()
.map(|TestCaseInfo { name, enabled }| ftest::Case {
name: Some(name.clone()),
enabled: Some(*enabled),
..Default::default()
})
.collect())
}
/// Runs a gunit test case associated with a single `ftest::SuiteRequest::Run` request.
///
/// Running the test component is delegated to an instance of the starnix kernel.
/// stdout logs are parsed for results and forwarded to the test framework.
///
/// # Parameters
/// - `tests`: The test invocations to run.
/// - `start_info`: The component start info of the test to run.
/// - `run_listener_proxy`: The proxy used to communicate results of the test run to the test
/// framework.
/// - `component_runner`: The runner that will run the test component.
/// - `test_type`: The type of test to run, used to determine which arguments to pass to the test.
pub async fn run_gunit_cases(
tests: Vec<ftest::Invocation>,
mut start_info: frunner::ComponentStartInfo,
run_listener_proxy: &ftest::RunListenerProxy,
component_runner: &frunner::ComponentRunnerProxy,
) -> Result<(), Error> {
let (numbered_handles, mut std_handles) = create_numbered_handles();
start_info.numbered_handles = numbered_handles;
// Grab stdout for result parsing.
let (stdout_reader, stdout_writer) = zx::Socket::create_stream();
let stdout_source = std_handles.out.take().unwrap();
std_handles.out = Some(stdout_reader);
// Hacky - send a fake case to test manager and use it to see all of stdout/stderr.
let (overall_test_listener_proxy, overall_test_listener) =
create_proxy::<ftest::CaseListenerMarker>()?;
run_listener_proxy.on_test_case_started(
&ftest::Invocation {
name: Some(start_info.resolved_url.clone().unwrap_or_default()),
tag: None,
..Default::default()
},
std_handles,
overall_test_listener,
)?;
// Notify test manager of cases starting.
let (mut run_listener_proxies, test_filter_arg) =
start_tests(tests, run_listener_proxy, TestType::Gunit)?;
append_program_args(vec![test_filter_arg], start_info.program.as_mut().context("No program.")?);
// Start the test component.
let component_controller = start_test_component(start_info, component_runner)?;
let _ = read_result(component_controller.take_event_stream()).await;
overall_test_listener_proxy
.finished(&TestResult { status: Some(Status::Passed), ..Default::default() })?;
// Parse stdout.
let mut reader = BufReader::new(fasync::Socket::from_socket(stdout_source));
let mut line = String::new();
while reader.read_line(&mut line).await? > 0 {
// Copy output to test's stdout.
let _ = stdout_writer.write(line.as_bytes())?;
let status = if line.starts_with("[ OK ]") {
Status::Passed
} else if line.starts_with("[ FAILED ]") {
Status::Failed
} else if line.starts_with("[ SKIPPED ]") {
Status::Skipped
} else {
line.clear();
continue;
};
let test_name = line[13..].split_whitespace().next().unwrap_or_default();
if let Some(case_listener_proxy) = run_listener_proxies.remove(test_name) {
case_listener_proxy
.finished(&TestResult { status: Some(status), ..Default::default() })?;
}
line.clear();
}
resolve_remaining_cases(run_listener_proxies)?;
Ok(())
}
/// Runs a gtest case associated with a single `ftest::SuiteRequest::Run` request.
///
/// Running the test component is delegated to an instance of the starnix kernel.
/// Results are read from and output file and stdout logs are forwarded to the test framework.
///
/// # Parameters
/// - `tests`: The test invocations to run.
/// - `start_info`: The component start info of the test to run.
/// - `run_listener_proxy`: The proxy used to communicate results of the test run to the test
/// framework.
/// - `component_runner`: The runner that will run the test component.
pub async fn run_gtest_cases(
tests: Vec<ftest::Invocation>,
mut start_info: frunner::ComponentStartInfo,
run_listener_proxy: &ftest::RunListenerProxy,
component_runner: &frunner::ComponentRunnerProxy,
test_type: TestType,
) -> Result<(), Error> {
let (numbered_handles, std_handles) = create_numbered_handles();
start_info.numbered_handles = numbered_handles;
// Hacky - send a fake case to test manager and use it to see all of stdout/stderr.
let (overall_test_listener_proxy, overall_test_listener) =
create_proxy::<ftest::CaseListenerMarker>()?;
run_listener_proxy.on_test_case_started(
&ftest::Invocation {
name: Some(start_info.resolved_url.clone().unwrap_or_default()),
tag: None,
..Default::default()
},
std_handles,
overall_test_listener,
)?;
// Notify test manager of cases starting.
let (mut run_listener_proxies, test_filter_arg) =
start_tests(tests, run_listener_proxy, test_type)?;
let output_file_name = unique_filename();
let output_format = match test_type {
TestType::Gtest => "json",
TestType::GtestXmlOutput => "xml",
_ => panic!("unexpected test type"),
};
let output_arg = format_arg(
test_type,
&format!("output={}:{}{}", output_format, OUTPUT_PATH, output_file_name),
);
tracing::info!("writing json output to {:?}", output_arg);
append_program_args(
vec![test_filter_arg, output_arg],
start_info.program.as_mut().expect("No program."),
);
let output_dir = add_output_dir_to_namespace(&mut start_info)?;
// Start the test component.
let component_controller = start_test_component(start_info, component_runner)?;
let _ = read_result(component_controller.take_event_stream()).await;
overall_test_listener_proxy
.finished(&TestResult { status: Some(Status::Passed), ..Default::default() })?;
// Parse test results.
let test_results = read_file(&output_dir, &output_file_name)
.await
.with_context(|| format!("Failed to read {}", output_file_name))
.and_then(|x| {
parse_results(test_type, x.trim())
.with_context(|| format!("Failed to parse {}", output_file_name))
});
// If tests crashes then we would fail to read or parse the output file. We
// still want to report these cases as failed before returning the error;
let (test_list, result) = match test_results {
Ok(results) => (results.testsuites, Ok(())),
Err(e) => (vec![], Err(e)),
};
for suite in &test_list {
for test in &suite.testsuite {
let name = format!("{}.{}", suite.name, test.name);
let status = match &test.status {
IndividualTestOutputStatus::NotRun => Status::Skipped,
IndividualTestOutputStatus::Run => match test.result.as_str() {
DYNAMIC_SKIP_RESULT => Status::Skipped,
_ => match &test.failures {
Some(_failures) => Status::Failed,
None => Status::Passed,
},
},
};
let case_listener_proxy = run_listener_proxies
.remove(&name)
.unwrap_or_else(|| panic!("No case listener for test case {name}"));
case_listener_proxy
.finished(&TestResult { status: Some(status), ..Default::default() })?;
}
}
resolve_remaining_cases(run_listener_proxies)?;
result
}
fn start_tests(
tests: Vec<ftest::Invocation>,
run_listener_proxy: &ftest::RunListenerProxy,
test_type: TestType,
) -> Result<(HashMap<String, CaseListenerProxy>, String), Error> {
let mut test_filter_arg = format_arg(test_type, FILTER_ARG);
let mut run_listener_proxies = HashMap::new();
for test in tests {
let test_name = test.name.clone().expect("No test name.");
test_filter_arg = format!("{}{}:", test_filter_arg, &test_name);
let (case_listener_proxy, case_listener) = create_proxy::<ftest::CaseListenerMarker>()?;
run_listener_proxy.on_test_case_started(
&test,
ftest::StdHandles::default(),
case_listener,
)?;
run_listener_proxies.insert(test_name, case_listener_proxy);
}
Ok((run_listener_proxies, test_filter_arg))
}
fn format_arg(test_type: TestType, test_arg: &str) -> String {
match test_type {
TestType::Gtest | TestType::GtestXmlOutput => format!("--gtest_{}", test_arg),
TestType::Gunit => format!("--gunit_{}", test_arg),
_ => panic!("unexpected test type"),
}
}
fn unique_filename() -> String {
format!("test_result-{}.json", uuid::Uuid::new_v4())
}
fn resolve_remaining_cases(
run_listener_proxies: HashMap<String, CaseListenerProxy>,
) -> Result<(), Error> {
for (name, case_listener_proxy) in run_listener_proxies {
tracing::warn!(
"Did not receive result for {name}. Marking as failed if not a disabled test."
);
let status = if name.contains("DISABLED") { Status::Skipped } else { Status::Failed };
case_listener_proxy.finished(&TestResult { status: Some(status), ..Default::default() })?;
}
Ok(())
}