blob: 9dc86ddd578cc6067ce546a03cd80d0ca55edf4d [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.
/**
* 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_crash_reporter;
mod fake_crash_reporting_product_register;
use {
anyhow::{bail, format_err, Error},
async_trait::async_trait,
component_events::{events::*, matcher::*},
fake_archive_accessor::FakeArchiveAccessor,
fake_crash_reporter::FakeCrashReporter,
fake_crash_reporting_product_register::FakeCrashReportingProductRegister,
fidl_fuchsia_diagnostics as diagnostics, fidl_fuchsia_feedback as fcrash,
fidl_fuchsia_io2 as fio2,
fuchsia_component::server::*,
fuchsia_component_test::{
mock::MockHandles, ChildProperties, RealmBuilder, RouteBuilder, RouteEndpoint,
},
fuchsia_zircon as zx,
futures::{channel::mpsc, future::BoxFuture, FutureExt, SinkExt, StreamExt},
std::sync::Arc,
tracing::*,
};
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;
// 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;
#[fuchsia::test]
async fn test_snapshot_throttle() -> Result<(), Error> {
run_a_test(test_snapshot_throttle::test()).await
}
#[fuchsia::test]
async fn test_trigger_truth() -> Result<(), Error> {
run_a_test(test_trigger_truth::test()).await
}
#[fuchsia::test]
async fn test_snapshot_sanitizing() -> Result<(), Error> {
run_a_test(test_snapshot_sanitizing::test()).await
}
#[fuchsia::test]
async fn test_filing_enable() -> Result<(), Error> {
run_a_test(test_filing_enable::test_with_enable()).await
}
#[fuchsia::test]
async fn test_filing_bad_enable() -> Result<(), Error> {
run_a_test(test_filing_enable::test_bad_enable()).await
}
#[fuchsia::test]
async fn test_filing_false_enable() -> Result<(), Error> {
run_a_test(test_filing_enable::test_false_enable()).await
}
#[fuchsia::test]
async fn test_filing_no_enable() -> Result<(), Error> {
run_a_test(test_filing_enable::test_no_enable()).await
}
#[fuchsia::test]
async fn test_filing_without_file() -> Result<(), Error> {
run_a_test(test_filing_enable::test_without_file()).await
}
// Each test*.rs file returns one of these from its "pub test()" method.
#[derive(Clone)]
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.
#[derive(Clone)]
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) {
self.receiver.next().await;
}
}
struct ArchiveEventSignaler {
event_sender: TestEventSender,
done_signaler: DoneSignaler,
}
impl ArchiveEventSignaler {
fn new(event_sender: TestEventSender, done_signaler: DoneSignaler) -> ArchiveEventSignaler {
ArchiveEventSignaler { event_sender, done_signaler }
}
}
#[async_trait]
impl fake_archive_accessor::EventSignaler for ArchiveEventSignaler {
async fn signal_fetch(&self) {
self.event_sender.clone().send(Ok(TestEvent::DiagnosticFetch)).await.unwrap();
}
async fn signal_done(&self) {
self.done_signaler.signal_done().await;
}
async fn signal_error(&self, error: &str) {
self.event_sender.clone().send(Err(format_err!("{}", error))).await.unwrap();
}
}
fn create_mock_component(
test_data: TestData,
crash_reporter: Arc<FakeCrashReporter>,
crash_reporting_product_register: Arc<FakeCrashReportingProductRegister>,
archive_accessor: Arc<FakeArchiveAccessor>,
) -> impl Fn(MockHandles) -> BoxFuture<'static, Result<(), anyhow::Error>> + Sync + Send + 'static {
move |mock_handles| {
let test_data = test_data.clone();
let crash_reporter = crash_reporter.clone();
let crash_reporting_product_register = crash_reporting_product_register.clone();
let archive_accessor = archive_accessor.clone();
async move {
let mut fs = ServiceFs::new();
// Serve data directory
let mut config_dir = fs.dir("config");
let mut data_dir = config_dir.dir("data");
for ConfigFile { name, contents } in test_data.config_files.iter() {
let size = contents.len() as u64;
let vmo = zx::Vmo::create(size).unwrap();
vmo.write(contents.as_bytes(), 0).unwrap();
data_dir.add_vmo_file_at(name, vmo, 0, size);
}
// Serve crash reporter, crash reporting product register, and archive accessor
fs.dir("svc")
.add_fidl_service(|stream: fcrash::CrashReporterRequestStream| {
crash_reporter.clone().serve_async(stream);
})
.add_fidl_service(|stream: fcrash::CrashReportingProductRegisterRequestStream| {
crash_reporting_product_register.clone().serve_async(stream);
})
.add_fidl_service_at(
"fuchsia.diagnostics.FeedbackArchiveAccessor",
|stream: diagnostics::ArchiveAccessorRequestStream| {
archive_accessor.clone().serve_async(stream);
},
);
fs.serve_connection(mock_handles.outgoing_dir.into_channel()).unwrap();
fs.collect::<()>().await;
Ok::<(), anyhow::Error>(())
}
.boxed()
}
}
async fn run_a_test(test_data: TestData) -> Result<(), Error> {
let start_time = std::time::Instant::now();
let mut done_waiter = DoneWaiter::new();
let (events_sender, events_receiver) = mpsc::unbounded();
let crash_reporter = FakeCrashReporter::new(events_sender.clone(), done_waiter.get_signaler());
let crash_reporting_product_register =
FakeCrashReportingProductRegister::new(done_waiter.get_signaler());
let event_signaler =
Box::new(ArchiveEventSignaler::new(events_sender.clone(), done_waiter.get_signaler()));
let archive_accessor = FakeArchiveAccessor::new(&test_data.inspect_data, Some(event_signaler));
let builder = RealmBuilder::new().await.unwrap();
builder.add_child("detect", DETECT_PROGRAM_URL, ChildProperties::new().eager()).await.unwrap();
let mock_component = create_mock_component(
test_data.clone(),
crash_reporter.clone(),
crash_reporting_product_register.clone(),
archive_accessor.clone(),
);
builder.add_mock_child("mocks", mock_component, ChildProperties::new()).await.unwrap();
// Forward logging to debug test breakages.
builder
.add_route(
RouteBuilder::protocol("fuchsia.logger.LogSink")
.source(RouteEndpoint::AboveRoot)
.targets(vec![RouteEndpoint::component("detect")]),
)
.await
.unwrap();
// Forward mocks to detect
builder
.add_route(
RouteBuilder::protocol("fuchsia.feedback.CrashReporter")
.source(RouteEndpoint::component("mocks"))
.targets(vec![RouteEndpoint::component("detect")]),
)
.await
.unwrap();
builder
.add_route(
RouteBuilder::protocol("fuchsia.feedback.CrashReportingProductRegister")
.source(RouteEndpoint::component("mocks"))
.targets(vec![RouteEndpoint::component("detect")]),
)
.await
.unwrap();
builder
.add_route(
RouteBuilder::protocol("fuchsia.diagnostics.FeedbackArchiveAccessor")
.source(RouteEndpoint::component("mocks"))
.targets(vec![RouteEndpoint::component("detect")]),
)
.await
.unwrap();
let rights = fio2::R_STAR_DIR;
builder
.add_route(
RouteBuilder::directory("config-data", "/config", rights)
.source(RouteEndpoint::component("mocks"))
.targets(vec![RouteEndpoint::component("detect")]),
)
.await
.unwrap();
// Register for stopped events
let event_source = EventSource::new().unwrap();
let mut exit_stream = event_source
.subscribe(vec![EventSubscription::new(vec![Stopped::NAME], EventMode::Async)])
.await
.unwrap();
// Start the component tree
let realm_instance = builder.build().await.unwrap();
// Await the test result.
if test_data.bails {
let root_name = realm_instance.root.child_name();
let moniker = format!(".*{}.*detect:0", root_name);
let exit_future = EventMatcher::ok()
.stop(None)
.moniker_regex(moniker)
.wait::<Stopped>(&mut exit_stream)
.boxed();
// 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.
futures::future::select(done_waiter.wait().boxed(), exit_future).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().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 crash_reporting_product_register.detected_error()
|| (minimum_test_time_seconds >= 10
&& !crash_reporting_product_register.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(())
}