1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
// Copyright 2021 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 {
    fidl_fuchsia_feedback::{
        CrashReporterMarker, CrashReporterProxy, CrashReporterRequestStream, FileReportResults,
        FilingError,
    },
    fuchsia_async::Task,
    futures::{channel::mpsc, future::BoxFuture, lock::Mutex, prelude::*, TryStreamExt},
    std::sync::Arc,
};

pub use fidl_fuchsia_feedback::CrashReport;

/// A call hook that can be used to inject responses into the CrashReporter service.
pub trait Hook: Send + Sync {
    /// Describes what the file_report call will return.
    fn file_report(
        &self,
        report: CrashReport,
    ) -> BoxFuture<'static, Result<FileReportResults, FilingError>>;
}

impl<F> Hook for F
where
    F: Fn(CrashReport) -> Result<FileReportResults, FilingError> + Send + Sync,
{
    fn file_report(
        &self,
        report: CrashReport,
    ) -> BoxFuture<'static, Result<FileReportResults, FilingError>> {
        future::ready(self(report)).boxed()
    }
}

pub struct MockCrashReporterService {
    call_hook: Box<dyn Hook>,
}

impl MockCrashReporterService {
    /// Creates a new MockCrashReporterService with a given callback to run per call to the service.
    pub fn new(hook: impl Hook + 'static) -> Self {
        Self { call_hook: Box::new(hook) }
    }

    /// Spawns an `fasync::Task` which serves fuchsia.feedback/CrashReporter.
    pub fn spawn_crash_reporter_service(self: Arc<Self>) -> (CrashReporterProxy, Task<()>) {
        let (proxy, stream) =
            fidl::endpoints::create_proxy_and_stream::<CrashReporterMarker>().unwrap();

        let task = Task::spawn(self.run_crash_reporter_service(stream));

        (proxy, task)
    }

    /// Serves fuchsia.feedback/CrashReporter.FileReport requests on the given request stream.
    pub async fn run_crash_reporter_service(
        self: Arc<Self>,
        mut stream: CrashReporterRequestStream,
    ) {
        while let Some(event) = stream.try_next().await.expect("received CrashReporter request") {
            match event {
                fidl_fuchsia_feedback::CrashReporterRequest::FileReport { report, responder } => {
                    let res = self.call_hook.file_report(report).await;
                    match res {
                        Err(_) => responder.send(Err(FilingError::InvalidArgsError)).unwrap(),
                        Ok(_) => responder.send(Ok(&FileReportResults::default())).unwrap(),
                    }
                }
            }
        }
    }
}

/// Hook that can be used to yield control of the `file_report` call to the caller. The caller can
/// control when `file_report` calls complete by pulling from the mpsc::Receiver.
pub struct ThrottleHook {
    file_response: Result<FileReportResults, FilingError>,
    sender: Arc<Mutex<mpsc::Sender<CrashReport>>>,
}

impl ThrottleHook {
    pub fn new(
        file_response: Result<FileReportResults, FilingError>,
    ) -> (Self, mpsc::Receiver<CrashReport>) {
        // We deliberately give a capacity of 1 so that the caller must pull from the
        // receiver in order to unblock the `file_report` call.
        let (sender, recv) = mpsc::channel(0);
        (Self { file_response, sender: Arc::new(Mutex::new(sender)) }, recv)
    }
}
impl Hook for ThrottleHook {
    fn file_report(
        &self,
        report: CrashReport,
    ) -> BoxFuture<'static, Result<FileReportResults, FilingError>> {
        let sender = Arc::clone(&self.sender);
        let file_response = self.file_response.clone();

        async move {
            sender.lock().await.send(report).await.unwrap();
            file_response
        }
        .boxed()
    }
}

#[cfg(test)]
mod tests {
    use {
        super::*,
        fuchsia_async as fasync,
        std::sync::atomic::{AtomicU32, Ordering},
    };

    #[fasync::run_singlethreaded(test)]
    async fn test_mock_crash_reporter() {
        let mock = Arc::new(MockCrashReporterService::new(|_| Ok(FileReportResults::default())));
        let (proxy, _server) = mock.spawn_crash_reporter_service();

        let file_result = proxy.file_report(CrashReport::default()).await.expect("made fidl call");

        assert_eq!(file_result, Ok(FileReportResults::default()));
    }

    #[fasync::run_singlethreaded(test)]
    async fn test_mock_crash_reporter_fails() {
        let mock = Arc::new(MockCrashReporterService::new(|_| Err(FilingError::InvalidArgsError)));
        let (proxy, _server) = mock.spawn_crash_reporter_service();

        let file_result = proxy.file_report(CrashReport::default()).await.expect("made fidl call");

        assert_eq!(file_result, Err(FilingError::InvalidArgsError));
    }

    #[fasync::run_singlethreaded(test)]
    async fn test_mock_crash_reporter_with_external_state() {
        let called = Arc::new(AtomicU32::new(0));
        let called_clone = Arc::clone(&called);
        let mock = Arc::new(MockCrashReporterService::new(move |_| {
            called_clone.fetch_add(1, Ordering::SeqCst);
            Ok(FileReportResults::default())
        }));
        let (proxy, _server) = mock.spawn_crash_reporter_service();

        let file_result = proxy.file_report(CrashReport::default()).await.expect("made fidl call");

        assert_eq!(file_result, Ok(FileReportResults::default()));
        assert_eq!(called.load(Ordering::SeqCst), 1);
    }
}