| // 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. |
| |
| /** |
| * This program integration-tests the triage-detect program using the OpaqueTest library |
| * to inject a fake CrashReporter, ArchiveAccessor, and config-file directory. |
| * |
| * triage-detect will be able to fetch Diagnostic data, evaluate it according to the .triage |
| * files it finds, and request whatever crash reports are appropriate. Meanwhile, the fakes |
| * will be writing TestEvent entries to a stream for later evaluation. |
| * |
| * Each integration test is stored in a file whose name starts with "test". This supplies: |
| * 1) Some number of Diagnostic data strings. When the program tries to fetch Diagnostic data |
| * after these strings are exhausted, the fake ArchiveAccessor writes to a special "done" channel |
| * to terminate the test. |
| * 2) Some number of config files to place in the fake directory. |
| * 3) A vector of vectors of crash report signatures. The inner vector should match the crash |
| * report requests sent between each fetch of Diagnostic data. Order of the inner vector does |
| * not matter, but duplicates do matter. |
| */ |
| mod fake_archive_accessor; |
| mod fake_crash_reporter; |
| mod fake_crash_reporting_product_register; |
| |
| use { |
| anyhow::{bail, Error}, |
| fake_archive_accessor::FakeArchiveAccessor, |
| fake_crash_reporter::FakeCrashReporter, |
| fake_crash_reporting_product_register::FakeCrashReportingProductRegister, |
| fuchsia_async as fasync, |
| futures::{channel::mpsc, FutureExt, SinkExt, StreamExt}, |
| log::*, |
| std::sync::Arc, |
| test_utils_lib::{ |
| events::{Event, EventMode, EventStream, EventSubscription, Stopped}, |
| injectors::{CapabilityInjector, DirectoryInjector}, |
| matcher::EventMatcher, |
| opaque_test::OpaqueTest, |
| }, |
| vfs::{ |
| directory::{ |
| helper::DirectlyMutable, immutable::connection::io1::ImmutableConnection, |
| simple::Simple, |
| }, |
| file::pcb::asynchronous::read_only_const, |
| pseudo_directory, |
| }, |
| }; |
| |
| const DETECT_PROGRAM_URL: &str = |
| "fuchsia-pkg://fuchsia.com/detect-integration-test#meta/detect-component.cm"; |
| // Keep this the same as the command line arg in meta/detect.cml. |
| const CHECK_PERIOD_SECONDS: u64 = 5; |
| // The capability name Detect needs for its config/data directory |
| const CONFIG_DATA_CAPABILITY_NAME: &str = "config-data"; |
| // The capability name Detect needs for its ArchiveAccessor connection |
| const ARCHIVE_ACCESSOR_CAPABILITY_NAME: &str = "fuchsia.diagnostics.FeedbackArchiveAccessor"; |
| |
| // Test that the "repeat" field of snapshots works correctly. |
| mod test_snapshot_throttle; |
| // Test that the "trigger" field of snapshots works correctly. |
| mod test_trigger_truth; |
| // Test that no report is filed unless "config.json" has the magic contents. |
| mod test_filing_enable; |
| // Test that all characters other than [a-z-] are converted to that set. |
| mod test_snapshot_sanitizing; |
| // run_a_test() verifies the Diagnostic-reading period by checking that no test completes too early. |
| // Manually verified: test fails if command line is 4 seconds and CHECK_PERIOD_SECONDS is 5. |
| |
| async fn run_all_tests() -> Vec<Result<(), Error>> { |
| futures::future::join_all(vec![ |
| run_a_test(test_snapshot_throttle::test()), |
| run_a_test(test_trigger_truth::test()), |
| run_a_test(test_filing_enable::test_with_enable()), |
| run_a_test(test_filing_enable::test_bad_enable()), |
| run_a_test(test_filing_enable::test_false_enable()), |
| run_a_test(test_filing_enable::test_no_enable()), |
| run_a_test(test_filing_enable::test_without_file()), |
| run_a_test(test_snapshot_sanitizing::test()), |
| ]) |
| .await |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn entry_point() -> Result<(), Error> { |
| fuchsia_syslog::init().unwrap(); |
| fuchsia_syslog::set_severity(fuchsia_syslog::levels::DEBUG); |
| |
| for result in run_all_tests().await.into_iter() { |
| if result.is_err() { |
| error!("{:?}", result); |
| return result; |
| } |
| } |
| Ok(()) |
| } |
| |
| // Each test*.rs file returns one of these from its "pub test()" method. |
| pub struct TestData { |
| // Just used for logging. |
| name: String, |
| // Contents of the /config/data directory. May include config.json. |
| config_files: Vec<ConfigFile>, |
| // Inspect-json-format strings, to be returned each time Detect fetches Inspect data. |
| inspect_data: Vec<String>, |
| // Between each fetch, the program should file these crash reports. For the inner vec, |
| // the order doesn't matter but duplicates will be retained and checked (it's not a set). |
| snapshots: Vec<Vec<String>>, |
| // Does the Detect program quit without fetching Inspect data? |
| bails: bool, |
| } |
| |
| // ConfigFile contains file information to help build a PseudoDirectory of configuration files. |
| struct ConfigFile { |
| name: String, |
| contents: String, |
| } |
| |
| // These are written to the reporting stream by the injected protocol fakes. |
| #[derive(Debug)] |
| pub enum TestEvent { |
| CrashReport(String), |
| DiagnosticFetch, |
| } |
| |
| type TestEventSender = mpsc::UnboundedSender<Result<TestEvent, Error>>; |
| type TestEventReceiver = mpsc::UnboundedReceiver<Result<TestEvent, Error>>; |
| type DoneSender = mpsc::Sender<()>; |
| type DoneReceiver = mpsc::Receiver<()>; |
| |
| #[derive(Clone, Debug)] |
| pub struct DoneSignaler { |
| sender: DoneSender, |
| } |
| |
| impl DoneSignaler { |
| async fn signal_done(&self) { |
| self.sender.clone().send(()).await.unwrap(); |
| } |
| } |
| |
| #[derive(Debug)] |
| pub struct DoneWaiter { |
| sender: DoneSender, |
| receiver: DoneReceiver, |
| } |
| |
| impl DoneWaiter { |
| fn new() -> DoneWaiter { |
| let (sender, receiver) = mpsc::channel(0); |
| DoneWaiter { sender, receiver } |
| } |
| |
| fn get_signaler(&self) -> DoneSignaler { |
| DoneSignaler { sender: self.sender.clone() } |
| } |
| |
| async fn wait(&mut self, exit_stream: Option<&mut EventStream>) { |
| match exit_stream { |
| Some(exit_stream) => { |
| let receiver = self.receiver.next().fuse(); |
| let exit_event = exit_stream.next().fuse(); |
| pin_utils::pin_mut!(receiver, exit_event); |
| futures::select!(_ = receiver => {}, _ = exit_event => {}); |
| } |
| None => { |
| self.receiver.next().await; |
| } |
| } |
| } |
| } |
| |
| async fn run_a_test(test_data: TestData) -> Result<(), Error> { |
| info!("Running test {}", test_data.name); |
| let start_time = std::time::Instant::now(); |
| let mut done_waiter = DoneWaiter::new(); |
| let (events_sender, events_receiver) = mpsc::unbounded(); |
| |
| // Start a component_manager as a v1 component |
| let test = OpaqueTest::default(DETECT_PROGRAM_URL).await.unwrap(); |
| let event_source = test.connect_to_event_source().await.unwrap(); |
| let mut exit_stream = event_source |
| .subscribe(vec![EventSubscription::new(vec![Stopped::NAME], EventMode::Sync)]) |
| .await |
| .unwrap(); |
| |
| DirectoryInjector::new(prepare_injected_config_directory(&test_data)) |
| .inject(&event_source, EventMatcher::ok().capability_name(CONFIG_DATA_CAPABILITY_NAME)) |
| .await; |
| |
| let registration_capability = |
| FakeCrashReportingProductRegister::new(done_waiter.get_signaler()); |
| registration_capability.inject(&event_source, EventMatcher::ok()).await; |
| |
| let archive_capability = |
| FakeArchiveAccessor::new(&test_data, events_sender.clone(), done_waiter.get_signaler()); |
| archive_capability |
| .inject(&event_source, EventMatcher::ok().capability_name(ARCHIVE_ACCESSOR_CAPABILITY_NAME)) |
| .await; |
| |
| let report_capability = |
| FakeCrashReporter::new(events_sender.clone(), done_waiter.get_signaler()); |
| report_capability.inject(&event_source, EventMatcher::ok()).await; |
| |
| // Unblock the component_manager. |
| event_source.start_component_tree().await; |
| |
| // Await the test result. |
| if test_data.bails { |
| // If it should bail, exit_stream will tell us it has exited. However, |
| // still collect events in case we observe more activity than we should. |
| done_waiter.wait(Some(&mut exit_stream)).await; |
| } else { |
| // If it shouldn't bail, avoid race conditions by not checking exit_stream. |
| // If the program doesn't do what it should and done_waiter never fires, |
| // the test will eventually time out and fail. |
| done_waiter.wait(None).await; |
| } |
| let events = drain(events_receiver).await?; |
| let end_time = std::time::Instant::now(); |
| let minimum_test_time_seconds = CHECK_PERIOD_SECONDS * (test_data.inspect_data.len() as u64); |
| |
| // Product-name registration is oneway FIDL - the test can proceed without it |
| // having arrived and been recorded. To avoid any possibility of flakes, if the |
| // test takes less than 10 seconds, only insist that no error was detected; don't insist that |
| // a correct registration has arrived. |
| if registration_capability.detected_error() |
| || (minimum_test_time_seconds >= 10 |
| && !registration_capability.detected_good_registration()) |
| { |
| error!("Test {} failed: Incorrect registration.", test_data.name); |
| bail!("Test {} failed: Incorrect registration.", test_data.name); |
| } |
| let too_fast = end_time - start_time < std::time::Duration::new(minimum_test_time_seconds, 0); |
| match evaluate_test_events(&test_data, &events) { |
| Ok(()) => {} |
| Err(e) => { |
| error!("Test {} failed: {}", test_data.name, e); |
| bail!("Test {} failed: {}", test_data.name, e); |
| } |
| } |
| if too_fast && !test_data.bails { |
| error!("Test {} finished too quickly.", test_data.name); |
| bail!("Test {} finished too quickly.", test_data.name); |
| } |
| Ok(()) |
| } |
| |
| // Some of the senders of the Event stream will remain open, so we can't wait for it to be |
| // conventionally "done." After the Done stream has been written to, whatever events |
| // are in the event stream constitute the test result. Read them out using poll and |
| // return them. |
| async fn drain(mut stream: TestEventReceiver) -> Result<Vec<TestEvent>, Error> { |
| let mut result = Vec::new(); |
| loop { |
| match stream.try_next() { |
| Ok(Some(item)) => result.push(item?), |
| Ok(None) => unreachable!(), |
| Err(_) => return Ok(result), |
| } |
| } |
| } |
| |
| fn evaluate_test_events(test: &TestData, events: &Vec<TestEvent>) -> Result<(), Error> { |
| if test.bails { |
| match events.len() { |
| 0 => return Ok(()), |
| _ => bail!("{}: Test should bail but events were {:?}", test.name, events), |
| } |
| } |
| let mut crash_list = Vec::new(); |
| let mut crashes_list = Vec::new(); |
| let mut events = events.iter(); |
| match events.next() { |
| Some(TestEvent::DiagnosticFetch) => {} |
| Some(TestEvent::CrashReport(r)) => { |
| bail!("{}: First event was a crash report: {}", test.name, r) |
| } |
| None => bail!("{}: Events were empty", test.name), |
| } |
| // If all goes well, the last event should be a DiagnosticFetch (which the tester never |
| // replies to). In the loop below, this will cause the previous fetch's crash_list to be |
| // added to crashes_list. The final crash_list created should remain empty.) |
| for event in events { |
| match event { |
| TestEvent::DiagnosticFetch => { |
| crashes_list.push(crash_list); |
| crash_list = Vec::new(); |
| } |
| TestEvent::CrashReport(signature) => crash_list.push(signature.to_string()), |
| } |
| } |
| if crash_list.len() != 0 { |
| bail!("{}: Crashes were filed after the final fetch: {:?}", test.name, crash_list); |
| } |
| let mut desired_events = test.snapshots.iter(); |
| let mut actual_events = crashes_list.iter(); |
| let mut which_iteration = 0; |
| loop { |
| match (desired_events.next(), actual_events.next()) { |
| (None, None) => break, |
| (Some(desired), Some(actual)) => { |
| let mut desired = desired.clone(); |
| let mut actual = actual.clone(); |
| desired.sort_unstable(); |
| actual.sort_unstable(); |
| if desired != actual { |
| bail!( |
| "{}: Step {}, desired {:?}, actual {:?}", |
| test.name, |
| which_iteration, |
| desired, |
| actual |
| ); |
| } |
| } |
| (desired, actual) => { |
| bail!( |
| "{}: Step {}, desired {:?}, actual {:?}", |
| test.name, |
| which_iteration, |
| desired, |
| actual |
| ); |
| } |
| } |
| which_iteration += 1; |
| } |
| Ok(()) |
| } |
| |
| fn prepare_injected_config_directory(test: &TestData) -> Arc<Simple<ImmutableConnection>> { |
| let leaf; |
| let data_directory = pseudo_directory! { |
| "data" => pseudo_directory! {leaf -> }, |
| }; |
| for ConfigFile { name, contents } in test.config_files.iter() { |
| leaf.add_entry(name, read_only_const(contents.to_string().into_bytes())).unwrap(); |
| } |
| data_directory |
| } |