[FTF] Update test_executor to support disabled tests
Also, improve testing ergonomics and correctness with a new
`GroupByTestCase` trait, allowing collections of `TestEvent`s to be
de-interleaved without discarding the order of events within each test
case.
TEST:
fx set core.x64 --with \
//src/lib/test_executor/rust:tests;
fx build;
... deploy to device ...
fx test
Bug: 45852
Change-Id: I88a45e50a797206694d535be001d2b75c1c60bd4
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/404797
Reviewed-by: Bryan Henry <bryanhenry@google.com>
Reviewed-by: Ankur Mittal <anmittal@google.com>
Reviewed-by: Matthew Boetger <boetger@google.com>
Reviewed-by: Shai Barack <shayba@google.com>
Commit-Queue: Konstantin Pozin <kpozin@google.com>
Testability-Review: Shai Barack <shayba@google.com>
diff --git a/src/developer/ffx/plugins/test/src/lib.rs b/src/developer/ffx/plugins/test/src/lib.rs
index 7f525c6..bc2f0ea2 100644
--- a/src/developer/ffx/plugins/test/src/lib.rs
+++ b/src/developer/ffx/plugins/test/src/lib.rs
@@ -14,6 +14,7 @@
std::io::{stdout, Write},
test_executor::{
run_and_collect_results_for_invocations as run_tests_and_collect, TestEvent, TestResult,
+ TestRunOptions,
},
};
@@ -150,9 +151,11 @@
return Ok(());
}
writeln!(writer, "Running tests...")?;
+ // TODO(fxb/45852): Add handling for disabled tests.
+ let run_options = TestRunOptions::default();
let (successful_completion, ()) = futures::future::try_join(
collect_events(writer, recv).map(Ok),
- run_tests_and_collect(suite_proxy, sender, invocations),
+ run_tests_and_collect(suite_proxy, sender, invocations, run_options),
)
.await
.context("running test")?;
diff --git a/src/lib/fidl_fuchsia_test_ext/OWNERS b/src/lib/fidl_fuchsia_test_ext/OWNERS
new file mode 100644
index 0000000..9f03d0e
--- /dev/null
+++ b/src/lib/fidl_fuchsia_test_ext/OWNERS
@@ -0,0 +1,5 @@
+anmittal@google.com
+geb@google.com
+shayba@google.com
+
+# COMPONENT: ComponentFramework>Runtime
\ No newline at end of file
diff --git a/src/lib/fidl_fuchsia_test_ext/rust/BUILD.gn b/src/lib/fidl_fuchsia_test_ext/rust/BUILD.gn
new file mode 100644
index 0000000..8cd6537
--- /dev/null
+++ b/src/lib/fidl_fuchsia_test_ext/rust/BUILD.gn
@@ -0,0 +1,15 @@
+# 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.
+
+import("//build/rust/rustc_library.gni")
+
+rustc_library("fidl_fuchsia_test_ext") {
+ with_unit_tests = true
+ edition = "2018"
+
+ deps = [
+ "//sdk/fidl/fuchsia.test:fuchsia.test-rustc",
+ "//src/lib/fidl/rust/fidl",
+ ]
+}
diff --git a/src/lib/fidl_fuchsia_test_ext/rust/src/lib.rs b/src/lib/fidl_fuchsia_test_ext/rust/src/lib.rs
new file mode 100644
index 0000000..9bc397a
--- /dev/null
+++ b/src/lib/fidl_fuchsia_test_ext/rust/src/lib.rs
@@ -0,0 +1,18 @@
+// 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.
+
+//! Extensions for `fidl_fuchsia_test`.
+
+use fidl_fuchsia_test as ftest;
+
+/// Extension trait that provides a manual implementation of `std::clone::Clone`.
+pub trait CloneExt {
+ fn clone(&self) -> Self;
+}
+
+impl CloneExt for ftest::RunOptions {
+ fn clone(&self) -> Self {
+ ftest::RunOptions { include_disabled_tests: self.include_disabled_tests }
+ }
+}
diff --git a/src/lib/test_executor/rust/BUILD.gn b/src/lib/test_executor/rust/BUILD.gn
index f947a19..39dac33 100644
--- a/src/lib/test_executor/rust/BUILD.gn
+++ b/src/lib/test_executor/rust/BUILD.gn
@@ -14,17 +14,24 @@
"//sdk/fidl/fuchsia.test:fuchsia.test-rustc",
"//sdk/fidl/fuchsia.test.manager:fuchsia.test.manager-rustc",
"//src/lib/fidl/rust/fidl",
+ "//src/lib/fidl_fuchsia_test_ext/rust:fidl_fuchsia_test_ext",
"//src/lib/fuchsia-async",
"//src/lib/zircon/rust:fuchsia-zircon-status",
"//third_party/rust_crates:anyhow",
"//third_party/rust_crates:futures",
"//third_party/rust_crates:glob",
+ "//third_party/rust_crates:linked-hash-map-v0_5_2",
"//third_party/rust_crates:log",
"//third_party/rust_crates:thiserror",
]
if (is_fuchsia) {
deps += [ "//src/lib/fuchsia-component" ]
}
+
+ test_deps = [
+ "//third_party/rust_crates:maplit",
+ "//third_party/rust_crates:pretty_assertions",
+ ]
}
group("tests") {
diff --git a/src/lib/test_executor/rust/src/lib.rs b/src/lib/test_executor/rust/src/lib.rs
index bd0ccc9..d1500b4 100644
--- a/src/lib/test_executor/rust/src/lib.rs
+++ b/src/lib/test_executor/rust/src/lib.rs
@@ -12,6 +12,7 @@
CaseListenerRequestStream, Invocation,
RunListenerRequest::{OnFinished, OnTestCaseStarted},
},
+ fidl_fuchsia_test_ext::CloneExt as _,
fidl_fuchsia_test_manager::{HarnessProxy, LaunchOptions},
fuchsia_zircon_status as zx_status,
futures::{
@@ -24,13 +25,55 @@
task::{Context, Poll},
},
glob,
+ linked_hash_map::LinkedHashMap,
log::*,
- std::{cell::RefCell, marker::Unpin, pin::Pin},
+ std::{cell::RefCell, collections::HashMap, marker::Unpin, pin::Pin},
};
#[cfg(target_os = "fuchsia")]
use fidl_fuchsia_sys::LauncherProxy;
+/// Options that apply when executing a test suite.
+///
+/// For the FIDL equivalent, see [`fidl_fuchsia_test::RunOptions`].
+#[derive(Debug, Clone, Default, Eq, PartialEq)]
+pub struct TestRunOptions {
+ /// How to handle tests that were marked disabled/ignored by the developer.
+ pub disabled_tests: DisabledTestHandling,
+}
+
+/// How to handle tests that were marked disabled/ignored by the developer.
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum DisabledTestHandling {
+ /// Skip tests that were marked disabled/ignored by the developer.
+ Exclude,
+ /// Explicitly include tests that were marked disabled/ignored by the developer.
+ Include,
+}
+
+impl Default for DisabledTestHandling {
+ fn default() -> Self {
+ DisabledTestHandling::Exclude
+ }
+}
+
+impl From<TestRunOptions> for fidl_fuchsia_test::RunOptions {
+ #[allow(unreachable_patterns)]
+ fn from(test_run_options: TestRunOptions) -> Self {
+ // Note: This will *not* break if new members are added to the FIDL table.
+ let mut run_options = fidl_fuchsia_test::RunOptions::empty();
+ match test_run_options.disabled_tests {
+ DisabledTestHandling::Exclude => {
+ run_options.include_disabled_tests = Some(false);
+ }
+ DisabledTestHandling::Include => {
+ run_options.include_disabled_tests = Some(true);
+ }
+ }
+ run_options
+ }
+}
+
/// Defines the result of a test case run.
#[derive(PartialEq, Debug, Eq, Hash, Ord, PartialOrd, Copy, Clone)]
pub enum TestResult {
@@ -45,7 +88,7 @@
}
/// Event to send to caller of `run_test_component`
-#[derive(PartialEq, Debug, Eq, Hash, Ord, PartialOrd)]
+#[derive(PartialEq, Debug, Eq, Hash, Ord, PartialOrd, Clone)]
pub enum TestEvent {
/// A new test case is started.
TestCaseStarted { test_case_name: String },
@@ -76,8 +119,77 @@
pub fn test_finished() -> TestEvent {
TestEvent::Finish
}
+
+ /// Returns the name of the test case to which the event belongs, if applicable.
+ pub fn test_case_name(&self) -> Option<&String> {
+ match self {
+ TestEvent::TestCaseStarted { test_case_name } => Some(test_case_name),
+ TestEvent::TestCaseFinished { test_case_name, result: _ } => Some(test_case_name),
+ TestEvent::LogMessage { test_case_name, msg: _ } => Some(test_case_name),
+ TestEvent::Finish => None,
+ // NOTE: If new global event types (not tied to a specific test case) are added,
+ // `GroupByTestCase` must also be updated so as to preserve correct event ordering.
+ // Otherwise, all such events will end up with a key of `None` in the map, and might
+ // therefore be spuriously bunched together.
+ }
+ }
+
+ /// Same as `test_case_name`, but returns an owned `Option<String>`.
+ pub fn owned_test_case_name(&self) -> Option<String> {
+ self.test_case_name().map(String::from)
+ }
}
+/// Trait allowing iterators over `TestEvent` to be partitioned by test case name.
+///
+/// Note that the current implementation assumes that the only `TestEvent` type that occurs
+/// _outside of test cases_ is `TestEvent::Finish`. If new global `TestEvent` types are added,
+/// the implementation will have to be changed.
+pub trait GroupByTestCase: Iterator<Item = TestEvent> + Sized {
+ /// Groups the `TestEvent`s by test case name into a map that preserves insertion order.
+ /// The overall order of test cases (by first event) and the orders of events within each test
+ /// case are preserved, but events from different test cases are effectively de-interleaved.
+ ///
+ /// Example:
+ /// ```rust
+ /// use test_executor::{TestEvent, GroupByTestCase as _};
+ /// use linked_hash_map::LinkedHashMap;
+ ///
+ /// let events: Vec<TestEvent> = get_events();
+ /// let grouped: LinkedHashMap<Option<String>, TestEvent> =
+ /// events.into_iter().group_by_test_case();
+ /// ```
+ fn group_by_test_case_ordered(self) -> LinkedHashMap<Option<String>, Vec<TestEvent>> {
+ let mut map = LinkedHashMap::new();
+ for test_event in self {
+ map.entry(test_event.owned_test_case_name()).or_insert(Vec::new()).push(test_event);
+ }
+ map
+ }
+
+ /// De-interleaves the `TestEvents` by test case. The overall order of test cases (by first
+ /// event) and the orders of events within each test case are preserved.
+ fn deinterleave(self) -> Box<dyn Iterator<Item = TestEvent>> {
+ Box::new(
+ self.group_by_test_case_ordered()
+ .into_iter()
+ .flat_map(|(_, events)| events.into_iter()),
+ )
+ }
+
+ /// Groups the `TestEvent`s by test case name into an unordered map. The orders of events within
+ /// each test case are preserved, but the test cases themselves are not in a defined order.
+ fn group_by_test_case_unordered(self) -> HashMap<Option<String>, Vec<TestEvent>> {
+ let mut map = HashMap::new();
+ for test_event in self {
+ map.entry(test_event.owned_test_case_name()).or_insert(Vec::new()).push(test_event);
+ }
+ map
+ }
+}
+
+impl<T> GroupByTestCase for T where T: Iterator<Item = TestEvent> + Sized {}
+
#[must_use = "futures/streams"]
pub struct LoggerStream {
socket: fidl::AsyncSocket,
@@ -223,6 +335,7 @@
suite: fidl_fuchsia_test::SuiteProxy,
sender: mpsc::Sender<TestEvent>,
test_filter: Option<&str>,
+ run_options: TestRunOptions,
) -> Result<(), anyhow::Error> {
debug!("enumerating tests");
let (case_iterator, server_end) = fidl::endpoints::create_proxy()?;
@@ -249,8 +362,9 @@
invocations.push(Invocation { name: Some(case_name), tag: None });
}
}
+
debug!("invocations: {:#?}", invocations);
- run_and_collect_results_for_invocations(suite, sender, invocations).await
+ run_and_collect_results_for_invocations(suite, sender, invocations, run_options).await
}
/// Runs the test component using `suite` and collects logs and results.
@@ -258,18 +372,21 @@
suite: fidl_fuchsia_test::SuiteProxy,
mut sender: mpsc::Sender<TestEvent>,
invocations: Vec<Invocation>,
+ run_options: TestRunOptions,
) -> Result<(), anyhow::Error> {
debug!("running tests");
let mut successful_completion = true; // will remain true, if there are no tests to run.
let mut invocations_iter = invocations.into_iter();
+ let run_options: fidl_fuchsia_test::RunOptions = run_options.into();
loop {
const INVOCATIONS_CHUNK: usize = 50;
let chunk = invocations_iter.by_ref().take(INVOCATIONS_CHUNK).collect::<Vec<_>>();
if chunk.is_empty() {
break;
}
- successful_completion &=
- run_invocations(&suite, chunk, &mut sender).await.context("running test cases")?;
+ successful_completion &= run_invocations(&suite, chunk, run_options.clone(), &mut sender)
+ .await
+ .context("running test cases")?;
}
if successful_completion {
let () =
@@ -282,19 +399,13 @@
async fn run_invocations(
suite: &fidl_fuchsia_test::SuiteProxy,
invocations: Vec<Invocation>,
+ run_options: fidl_fuchsia_test::RunOptions,
sender: &mut mpsc::Sender<TestEvent>,
) -> Result<bool, anyhow::Error> {
let (run_listener_client, mut run_listener) =
fidl::endpoints::create_request_stream::<fidl_fuchsia_test::RunListenerMarker>()
.context("creating request stream")?;
- suite.run(
- &mut invocations.into_iter().map(|i| i.into()),
- fidl_fuchsia_test::RunOptions {
- // TODO(fxb/45852): Support disabled tests.
- include_disabled_tests: None,
- },
- run_listener_client,
- )?;
+ suite.run(&mut invocations.into_iter().map(|i| i.into()), run_options, run_listener_client)?;
let mut test_case_processors = Vec::new();
let mut successful_completion = false;
@@ -350,7 +461,10 @@
.connect_to_service::<fidl_fuchsia_test::SuiteMarker>()
.context("connecting to service")?;
- run_and_collect_results(suite, sender, None).await?;
+ // No custom run options for v1 tests.
+ let run_options = TestRunOptions::default();
+
+ run_and_collect_results(suite, sender, None, run_options).await?;
Ok(())
}
@@ -361,6 +475,7 @@
test_url: String,
sender: mpsc::Sender<TestEvent>,
test_filter: Option<&str>,
+ run_options: TestRunOptions,
) -> Result<(), anyhow::Error> {
if !test_url.ends_with(".cm") {
return Err(anyhow::anyhow!(
@@ -380,7 +495,7 @@
anyhow::anyhow!("error launching test: {:?}", e)
})?;
- run_and_collect_results(suite_proxy, sender, test_filter).await
+ run_and_collect_results(suite_proxy, sender, test_filter, run_options).await
}
fn suite_error(err: fidl::Error) -> anyhow::Error {
@@ -405,6 +520,8 @@
mod tests {
use super::*;
use fidl::HandleBased;
+ use maplit::hashmap;
+ use pretty_assertions::assert_eq;
#[fuchsia_async::run_singlethreaded(test)]
async fn collect_logs() {
@@ -448,4 +565,119 @@
msg = recv.next().await;
assert_eq!(msg, None);
}
+
+ #[test]
+ fn group_by_test_case_ordered() {
+ let events = vec![
+ TestEvent::test_case_started("a::a"),
+ TestEvent::test_case_started("b::b"),
+ TestEvent::log_message("a::a", "log"),
+ TestEvent::test_case_started("c::c"),
+ TestEvent::log_message("b::b", "log"),
+ TestEvent::test_case_finished("c::c", TestResult::Passed),
+ TestEvent::test_case_finished("a::a", TestResult::Failed),
+ TestEvent::test_case_finished("b::b", TestResult::Passed),
+ TestEvent::test_finished(),
+ ];
+
+ let actual: Vec<(Option<String>, Vec<TestEvent>)> =
+ events.into_iter().group_by_test_case_ordered().into_iter().collect();
+
+ let expected = vec![
+ (
+ Some("a::a".to_string()),
+ vec![
+ TestEvent::test_case_started("a::a"),
+ TestEvent::log_message("a::a", "log"),
+ TestEvent::test_case_finished("a::a", TestResult::Failed),
+ ],
+ ),
+ (
+ Some("b::b".to_string()),
+ vec![
+ TestEvent::test_case_started("b::b"),
+ TestEvent::log_message("b::b", "log"),
+ TestEvent::test_case_finished("b::b", TestResult::Passed),
+ ],
+ ),
+ (
+ Some("c::c".to_string()),
+ vec![
+ TestEvent::test_case_started("c::c"),
+ TestEvent::test_case_finished("c::c", TestResult::Passed),
+ ],
+ ),
+ (None, vec![TestEvent::test_finished()]),
+ ];
+
+ assert_eq!(actual, expected);
+ }
+
+ #[test]
+ fn deinterleave() {
+ let events = vec![
+ TestEvent::test_case_started("a::a"),
+ TestEvent::test_case_started("b::b"),
+ TestEvent::log_message("a::a", "log"),
+ TestEvent::test_case_started("c::c"),
+ TestEvent::log_message("b::b", "log"),
+ TestEvent::test_case_finished("c::c", TestResult::Passed),
+ TestEvent::test_case_finished("a::a", TestResult::Failed),
+ TestEvent::test_case_finished("b::b", TestResult::Passed),
+ TestEvent::test_finished(),
+ ];
+
+ let expected = vec![
+ TestEvent::test_case_started("a::a"),
+ TestEvent::log_message("a::a", "log"),
+ TestEvent::test_case_finished("a::a", TestResult::Failed),
+ TestEvent::test_case_started("b::b"),
+ TestEvent::log_message("b::b", "log"),
+ TestEvent::test_case_finished("b::b", TestResult::Passed),
+ TestEvent::test_case_started("c::c"),
+ TestEvent::test_case_finished("c::c", TestResult::Passed),
+ TestEvent::test_finished(),
+ ];
+
+ let actual: Vec<TestEvent> = events.into_iter().deinterleave().collect();
+
+ assert_eq!(actual, expected);
+ }
+
+ #[test]
+ fn group_by_test_case_unordered() {
+ let events = vec![
+ TestEvent::test_case_started("a::a"),
+ TestEvent::test_case_started("b::b"),
+ TestEvent::log_message("a::a", "log"),
+ TestEvent::test_case_started("c::c"),
+ TestEvent::log_message("b::b", "log"),
+ TestEvent::test_case_finished("c::c", TestResult::Passed),
+ TestEvent::test_case_finished("a::a", TestResult::Failed),
+ TestEvent::test_case_finished("b::b", TestResult::Passed),
+ TestEvent::test_finished(),
+ ];
+
+ let expected = hashmap! {
+ Some("a::a".to_string()) => vec![
+ TestEvent::test_case_started("a::a"),
+ TestEvent::log_message("a::a", "log"),
+ TestEvent::test_case_finished("a::a", TestResult::Failed),
+ ],
+ Some("b::b".to_string()) => vec![
+ TestEvent::test_case_started("b::b"),
+ TestEvent::log_message("b::b", "log"),
+ TestEvent::test_case_finished("b::b", TestResult::Passed),
+ ],
+ Some("c::c".to_string()) => vec![
+ TestEvent::test_case_started("c::c"),
+ TestEvent::test_case_finished("c::c", TestResult::Passed),
+ ],
+ None => vec![TestEvent::Finish],
+ };
+
+ let actual = events.into_iter().group_by_test_case_unordered();
+
+ assert_eq!(actual, expected)
+ }
}
diff --git a/src/sys/run_test_suite/src/lib.rs b/src/sys/run_test_suite/src/lib.rs
index 7547238..fbe952c 100644
--- a/src/sys/run_test_suite/src/lib.rs
+++ b/src/sys/run_test_suite/src/lib.rs
@@ -16,7 +16,7 @@
std::collections::HashSet,
std::fmt,
std::io::Write,
- test_executor::TestEvent,
+ test_executor::{TestEvent, TestRunOptions},
};
#[derive(PartialEq, Debug)]
@@ -86,7 +86,11 @@
let mut successful_completion = false;
- let test_fut = test_executor::run_v2_test_component(harness, url, sender, test_filter).fuse();
+ // TODO(fxb/45852): Support disabled tests.
+ let run_options = TestRunOptions::default();
+
+ let test_fut =
+ test_executor::run_v2_test_component(harness, url, sender, test_filter, run_options).fuse();
futures::pin_mut!(test_fut);
loop {
diff --git a/src/sys/test_manager/tests/src/main.rs b/src/sys/test_manager/tests/src/main.rs
index fdd9eb0..a829cce 100644
--- a/src/sys/test_manager/tests/src/main.rs
+++ b/src/sys/test_manager/tests/src/main.rs
@@ -21,8 +21,7 @@
std::collections::HashSet,
std::iter::FromIterator,
std::mem::drop,
- test_executor::TestEvent,
- test_executor::TestResult,
+ test_executor::{TestEvent, TestResult, TestRunOptions},
};
async fn connect_test_manager() -> Result<ftest_manager::HarnessProxy, Error> {
@@ -62,9 +61,11 @@
let (sender, recv) = mpsc::channel(1);
+ // TODO(fxb/45852): Support disabled tests.
+ let test_run_options = TestRunOptions::default();
let (events, ()) = futures::future::try_join(
recv.collect::<Vec<_>>().map(Ok),
- test_executor::run_and_collect_results(suite_proxy, sender, None),
+ test_executor::run_and_collect_results(suite_proxy, sender, None, test_run_options),
)
.await
.context("running test")?;
diff --git a/src/sys/test_runners/gtest/tests/main.rs b/src/sys/test_runners/gtest/tests/main.rs
index c5b1dcc..1e3c647 100644
--- a/src/sys/test_runners/gtest/tests/main.rs
+++ b/src/sys/test_runners/gtest/tests/main.rs
@@ -12,8 +12,7 @@
fuchsia_component::client,
fuchsia_component::client::connect_to_protocol_at_dir,
futures::{channel::mpsc, prelude::*},
- test_executor::TestEvent,
- test_executor::TestResult,
+ test_executor::{TestEvent, TestResult, TestRunOptions},
};
async fn connect_test_manager() -> Result<ftest_manager::HarnessProxy, Error> {
@@ -53,9 +52,11 @@
let (sender, recv) = mpsc::channel(1);
+ // TODO(fxb/45852): Support disabled tests.
+ let test_run_options = TestRunOptions::default();
let (events, ()) = futures::future::try_join(
recv.collect::<Vec<_>>().map(Ok),
- test_executor::run_and_collect_results(suite_proxy, sender, None),
+ test_executor::run_and_collect_results(suite_proxy, sender, None, test_run_options),
)
.await
.context("running test")?;
diff --git a/src/sys/test_runners/rust/tests/main.rs b/src/sys/test_runners/rust/tests/main.rs
index 2716ec4..938e9ff 100644
--- a/src/sys/test_runners/rust/tests/main.rs
+++ b/src/sys/test_runners/rust/tests/main.rs
@@ -12,8 +12,7 @@
fuchsia_component::client,
fuchsia_component::client::connect_to_protocol_at_dir,
futures::{channel::mpsc, prelude::*},
- test_executor::TestEvent,
- test_executor::TestResult,
+ test_executor::{TestEvent, TestResult, TestRunOptions},
};
async fn connect_test_manager() -> Result<ftest_manager::HarnessProxy, Error> {
@@ -53,9 +52,11 @@
let (sender, recv) = mpsc::channel(1);
+ // TODO(fxb/45852): Support disabled tests.
+ let run_options = TestRunOptions::default();
let (events, ()) = futures::future::try_join(
recv.collect::<Vec<_>>().map(Ok),
- test_executor::run_and_collect_results(suite_proxy, sender, None),
+ test_executor::run_and_collect_results(suite_proxy, sender, None, run_options),
)
.await
.context("running test")?;