blob: 979e02a31fbe9ba3f9324b968f4b0fa3314b2565 [file]
// Copyright 2022 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::Context as _;
use expectations_matcher::Outcome;
use fuchsia_component::client;
use fuchsia_fs::file::read_in_namespace_to_string;
use futures::{StreamExt as _, TryStreamExt as _};
use itertools::Itertools as _;
fn outcome_from_test_status(status: fidl_fuchsia_test::Status) -> Outcome {
match status {
fidl_fuchsia_test::Status::Passed => Outcome::Pass,
fidl_fuchsia_test::Status::Failed => Outcome::Fail,
fidl_fuchsia_test::Status::Skipped => Outcome::Skip,
}
}
#[derive(Debug)]
enum ExpectationError {
Mismatch { got: Outcome, want: Outcome },
NoExpectationFound,
}
struct CaseStart {
invocation: fidl_fuchsia_test::Invocation,
std_handles: fidl_fuchsia_test::StdHandles,
}
#[derive(Debug, Clone)]
struct CaseEnd {
result: fidl_fuchsia_test::Result_,
}
#[derive(Debug)]
struct ExpectationsComparer {
expectations: ser::Expectations,
}
impl ExpectationsComparer {
fn expected_outcome(&self, invocation: &fidl_fuchsia_test::Invocation) -> Option<Outcome> {
let name = invocation
.name
.as_ref()
.unwrap_or_else(|| panic!("invocation {invocation:?} did not have name"));
expectations_matcher::expected_outcome(name, &self.expectations)
}
fn check_against_expectation(
&self,
invocation: &fidl_fuchsia_test::Invocation,
status: fidl_fuchsia_test::Status,
) -> Result<fidl_fuchsia_test::Status, ExpectationError> {
let got_outcome = outcome_from_test_status(status);
let want_outcome = self.expected_outcome(invocation);
match (got_outcome, want_outcome) {
// TODO(https://fxbug.dev/113117): Determine how to handle tests skipped at runtime.
(Outcome::Skip, None | Some(Outcome::Fail | Outcome::Pass | Outcome::Skip)) => {
Ok(fidl_fuchsia_test::Status::Skipped)
}
(Outcome::Pass | Outcome::Fail, None) => Err(ExpectationError::NoExpectationFound),
(got_outcome, Some(want_outcome)) if got_outcome == want_outcome => {
Ok(fidl_fuchsia_test::Status::Passed)
}
(got_outcome, Some(want_outcome)) => {
Err(ExpectationError::Mismatch { got: got_outcome, want: want_outcome })
}
}
}
async fn handle_case(
&self,
run_listener_proxy: &fidl_fuchsia_test::RunListenerProxy,
CaseStart { invocation, std_handles }: CaseStart,
end_stream: impl futures::TryStream<Ok = CaseEnd, Error = anyhow::Error>,
) -> Result<Option<(fidl_fuchsia_test::Invocation, ExpectationError)>, anyhow::Error> {
let (case_listener_proxy, case_listener) =
fidl::endpoints::create_proxy().context("error creating CaseListenerProxy")?;
run_listener_proxy
.on_test_case_started(&invocation, std_handles, case_listener)
.context("error calling run_listener_proxy.on_test_case_started(...)")?;
let name = invocation.name.as_ref().expect("fuchsia.test/Invocation had no name");
let case_listener_proxy = &case_listener_proxy;
let result = match &end_stream
.try_collect::<Vec<_>>()
.await
.context("error getting case results")?[..]
{
[] => return Err(anyhow::anyhow!("Received no result for case {}", name)),
[CaseEnd { result }] => result.clone(),
results => {
return Err(anyhow::anyhow!(
"Received multiple results for case {}: {:?}",
name,
results
))
}
};
let fidl_fuchsia_test::Result_ { status, .. } = result;
let original_status = status.expect("fuchsia.test/Result had no status");
let (status, expectation_error) =
match self.check_against_expectation(&invocation, original_status) {
Ok(status) => (status, None),
Err(err) => {
match &err {
ExpectationError::Mismatch { got, want } => {
tracing::error!(
// TODO(https://fxbug.dev/113119): Decide what error message to use
// here.
"Failing test case {}: got {:?}, expected {:?}",
name,
got,
want,
);
}
ExpectationError::NoExpectationFound => {
tracing::error!("No expectation matches {}", name);
}
};
(fidl_fuchsia_test::Status::Failed, Some(err))
}
};
if matches!(
(original_status, status),
(fidl_fuchsia_test::Status::Failed, fidl_fuchsia_test::Status::Passed)
) {
tracing::info!("{name} failure is expected, so it will be reported to the test runner as having passed.")
} else if matches!(
(original_status, status),
(fidl_fuchsia_test::Status::Passed, fidl_fuchsia_test::Status::Passed)
) {
tracing::info!("{name} success is expected.")
}
case_listener_proxy
.finished(&fidl_fuchsia_test::Result_ { status: Some(status), ..Default::default() })
.context("case listener proxy fidl error")?;
Ok(expectation_error.map(|err| (invocation, err)))
}
async fn handle_suite_run_request(
&self,
suite_proxy: &fidl_fuchsia_test::SuiteProxy,
tests: Vec<fidl_fuchsia_test::Invocation>,
options: fidl_fuchsia_test::RunOptions,
listener: fidl::endpoints::ClientEnd<fidl_fuchsia_test::RunListenerMarker>,
) -> Result<Vec<(fidl_fuchsia_test::Invocation, ExpectationError)>, anyhow::Error> {
let tests_and_expects = tests.into_iter().map(|invocation| {
let outcome = self.expected_outcome(&invocation);
(invocation, outcome)
});
let (skipped, not_skipped): (Vec<_>, Vec<_>) = tests_and_expects
.partition(|(_invocation, outcome)| matches!(outcome, Some(Outcome::Skip)));
let listener_proxy =
listener.into_proxy().context("error turning RunListener client end into proxy")?;
for (invocation, _) in skipped {
let (case_listener_proxy, case_listener_server_end) =
fidl::endpoints::create_proxy().context("error creating case listener proxy")?;
let name = invocation.name.as_ref().expect("fuchsia.test/Invocation had no name");
tracing::info!("{name} skip is expected.");
listener_proxy
.on_test_case_started(
&invocation,
fidl_fuchsia_test::StdHandles::default(),
case_listener_server_end,
)
.context("error while telling run listener that a skipped test case had started")?;
case_listener_proxy
.finished(&fidl_fuchsia_test::Result_ {
status: Some(fidl_fuchsia_test::Status::Skipped),
..Default::default()
})
.context(
"error while telling run listener that a skipped test case had finished",
)?;
}
let failures = futures::lock::Mutex::new(Vec::new());
if !not_skipped.is_empty() {
let case_stream = {
let (listener, listener_request_stream) = fidl::endpoints::create_request_stream()
.context("error creating run listener request stream")?;
suite_proxy
.run(
&not_skipped
.into_iter()
.map(|(invocation, _outcome)| invocation)
.collect::<Vec<_>>(),
&options,
listener,
)
.context("error calling original test component's fuchsia.test/Suite#Run")?;
listener_request_stream
.try_take_while(|request| {
futures::future::ok(!matches!(
request,
fidl_fuchsia_test::RunListenerRequest::OnFinished { control_handle: _ }
))
})
.map_err(anyhow::Error::new)
.and_then(|request| match request {
fidl_fuchsia_test::RunListenerRequest::OnFinished { control_handle: _ } => {
unreachable!()
}
fidl_fuchsia_test::RunListenerRequest::OnTestCaseStarted {
invocation,
std_handles,
listener,
control_handle: _,
} => {
async move {
Ok((
CaseStart { invocation, std_handles },
listener
.into_stream()
.context("error getting CaseListener request stream")?
.map_ok(
|fidl_fuchsia_test::CaseListenerRequest::Finished {
result,
control_handle: _,
}| {
CaseEnd { result }
},
)
.map_err(anyhow::Error::new),
))
}
}
})
};
{
let listener_proxy = &listener_proxy;
let failures = &failures;
case_stream
.try_for_each_concurrent(None, |(start, end_stream)| async move {
if let Some(result) =
self.handle_case(listener_proxy, start, end_stream).await?
{
failures.lock().await.push(result);
}
Ok(())
})
.await
.context("error handling test case stream")?;
}
}
listener_proxy.on_finished().context("error calling listener_proxy.on_finished()")?;
Ok(failures.into_inner())
}
async fn handle_suite_request_stream(
&self,
suite_request_stream: fidl_fuchsia_test::SuiteRequestStream,
) -> Result<(), anyhow::Error> {
let suite_proxy = &client::connect_to_protocol::<fidl_fuchsia_test::SuiteMarker>()
.context("error connecting to original test component's fuchsia.test/Suite")?;
// `fx test`, via `ffx test`, connects to the `fuchsia.test/Suite` protocol only once, but
// it makes multiple invocations to `fuchsia.test/Suite#Run`. Therefore, in order to print
// all of the mismatched expectations at the end of the `fx test` invocation, we need to
// collect them across the entire `fuchsia.test/Suite` request stream and emit them once the
// `fuchsia.test/Suite` handle has been closed.
let failures = suite_request_stream
.map_err(anyhow::Error::new)
.and_then(|request| async move {
match request {
fidl_fuchsia_test::SuiteRequest::GetTests { iterator, control_handle: _ } => {
suite_proxy.get_tests(iterator).context("error enumerating test cases")?;
Ok(Vec::new())
}
fidl_fuchsia_test::SuiteRequest::Run {
tests,
options,
listener,
control_handle: _,
} => self
.handle_suite_run_request(suite_proxy, tests, options, listener)
.await
.context("error handling Suite run request"),
}
})
.try_collect::<Vec<_>>()
.await
.context("error handling suite request stream")?
.into_iter()
.flatten();
let (mismatch, missing): (Vec<_>, Vec<_>) =
failures.partition_map(|(invocation, error)| match error {
ExpectationError::Mismatch { got, want } => {
itertools::Either::Left((invocation, got, want))
}
ExpectationError::NoExpectationFound => itertools::Either::Right(invocation),
});
if !missing.is_empty() {
tracing::error!("Observed {} test results with no matching expectation", missing.len());
for invocation in missing {
let name = invocation.name.unwrap();
tracing::error!("{name} -- no expectation found");
}
}
if !mismatch.is_empty() {
tracing::error!(
"Observed {} test results that did not match expectations",
mismatch.len()
);
for (invocation, got, want) in mismatch {
let name = invocation.name.unwrap();
tracing::error!("{name} -- got {got:?}, expected {want:?}");
}
}
Ok(())
}
}
const EXPECTATIONS_SPECIFIC_PATH: &str = "/expectations/expectations.json5";
const EXPECTATIONS_PKG_PATH: &str = "/pkg/expectations.json5";
#[fuchsia::main]
async fn main() {
let mut fs = fuchsia_component::server::ServiceFs::new_local();
let _: &mut fuchsia_component::server::ServiceFsDir<'_, _> =
fs.dir("svc").add_fidl_service(|s: fidl_fuchsia_test::SuiteRequestStream| s);
let _: &mut fuchsia_component::server::ServiceFs<_> =
fs.take_and_serve_directory_handle().expect("failed to serve ServiceFs directory");
let expectations = if let Ok(expectations) =
read_in_namespace_to_string(EXPECTATIONS_SPECIFIC_PATH).await
{
expectations
} else {
read_in_namespace_to_string(EXPECTATIONS_PKG_PATH).await.unwrap_or_else(|err| {
panic!("failed to read expectations file at either {EXPECTATIONS_SPECIFIC_PATH} (for component-specific expectations) \
or {EXPECTATIONS_PKG_PATH} (for test-package-wide expectations): {err}")
})
};
let comparer = ExpectationsComparer {
expectations: serde_json5::from_str(&expectations).expect("failed to parse expectations"),
};
fs.then(|s| comparer.handle_suite_request_stream(s))
.for_each_concurrent(None, |result| {
let () = result.expect("error handling fuchsia.test/Suite request stream");
futures::future::ready(())
})
.await
}
#[cfg(test)]
mod test {
#[test]
fn a_passing_test() {
println!("this is a passing test")
}
#[fuchsia::test]
async fn a_passing_test_with_err_logs() {
tracing::error!("this is an error");
println!("this is a passing test");
}
#[test]
fn a_failing_test() {
panic!("this is a failing test")
}
#[fuchsia::test]
async fn a_failing_test_with_err_logs() {
tracing::error!("this is an error");
panic!("this is a failing test")
}
#[test]
fn a_skipped_test() {
unreachable!("this is a skipped test")
}
}