Skip to main content

mock_installer/
lib.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use assert_matches::assert_matches;
6use flex_fuchsia_update_installer as fidl_installer;
7
8use fidl_fuchsia_update_installer_ext::{State, StateId};
9use fidl_installer::{
10    InstallerMarker, InstallerProxy, InstallerRequest, InstallerRequestStream, MonitorProxy,
11    Options, RebootControllerRequest, UpdateNotStartedReason,
12};
13use fuchsia_async as fasync;
14use fuchsia_sync::Mutex;
15use futures::channel::mpsc;
16use futures::prelude::*;
17use pretty_assertions::assert_eq;
18use std::sync::Arc;
19
20#[derive(PartialEq, Debug)]
21pub enum CapturedUpdateInstallerRequest {
22    StartUpdate { url: String, options: Options, reboot_controller_present: bool },
23    SuspendUpdate { attempt_id: Option<String> },
24    ResumeUpdate { attempt_id: Option<String> },
25    CancelUpdate { attempt_id: Option<String> },
26}
27
28// Options does not impl Eq, but it is semantically Eq.
29impl Eq for CapturedUpdateInstallerRequest {}
30
31#[derive(Eq, PartialEq, Debug)]
32pub enum CapturedRebootControllerRequest {
33    Unblock,
34    Detach,
35}
36
37pub struct MockUpdateInstallerServiceBuilder {
38    states: Vec<State>,
39    states_receiver: Option<mpsc::Receiver<State>>,
40    start_update_response: Result<String, UpdateNotStartedReason>,
41}
42
43impl MockUpdateInstallerServiceBuilder {
44    /// The mock installer will sends these states to the caller of StartUpdate.
45    /// It will only work for the first StartUpdate call.
46    /// Ignored if states_receiver exists.
47    pub fn states(self, states: Vec<State>) -> Self {
48        Self { states, ..self }
49    }
50
51    /// When the mock installer receives a state it will forward it to the caller of StartUpdate.
52    /// It will only work for the first StartUpdate call.
53    pub fn states_receiver(self, states_receiver: mpsc::Receiver<State>) -> Self {
54        Self { states_receiver: Some(states_receiver), ..self }
55    }
56
57    pub fn start_update_response(
58        self,
59        start_update_response: Result<String, UpdateNotStartedReason>,
60    ) -> Self {
61        Self { start_update_response, ..self }
62    }
63
64    pub fn build(self) -> MockUpdateInstallerService {
65        let Self { states, states_receiver, start_update_response } = self;
66        let states_receiver = match states_receiver {
67            Some(states_receiver) => states_receiver,
68            None => {
69                let (mut sender, receiver) = mpsc::channel(0);
70                fasync::Task::spawn(async move {
71                    for state in states {
72                        sender.send(state).await.unwrap();
73                    }
74                })
75                .detach();
76                receiver
77            }
78        };
79        MockUpdateInstallerService {
80            start_update_response: Mutex::new(start_update_response),
81            states_receiver: Mutex::new(Some(states_receiver)),
82            captured_args: Mutex::new(vec![]),
83            reboot_controller_requests: Mutex::new(vec![]),
84        }
85    }
86}
87
88pub struct MockUpdateInstallerService {
89    start_update_response: Mutex<Result<String, UpdateNotStartedReason>>,
90    states_receiver: Mutex<Option<mpsc::Receiver<State>>>,
91    captured_args: Mutex<Vec<CapturedUpdateInstallerRequest>>,
92    reboot_controller_requests: Mutex<Vec<CapturedRebootControllerRequest>>,
93}
94
95impl MockUpdateInstallerService {
96    pub fn builder() -> MockUpdateInstallerServiceBuilder {
97        MockUpdateInstallerServiceBuilder {
98            states: vec![],
99            start_update_response: Ok("id".into()),
100            states_receiver: None,
101        }
102    }
103
104    pub fn with_states(states: Vec<State>) -> Self {
105        Self::builder().states(states).build()
106    }
107
108    pub fn with_response(start_update_response: Result<String, UpdateNotStartedReason>) -> Self {
109        Self::builder().start_update_response(start_update_response).build()
110    }
111
112    pub async fn run_service(self: Arc<Self>, mut stream: InstallerRequestStream) {
113        while let Some(req) = stream.try_next().await.unwrap() {
114            match req {
115                InstallerRequest::StartUpdate {
116                    url,
117                    options,
118                    monitor,
119                    reboot_controller,
120                    responder,
121                } => {
122                    self.captured_args.lock().push(CapturedUpdateInstallerRequest::StartUpdate {
123                        url: url.url,
124                        options,
125                        reboot_controller_present: reboot_controller.is_some(),
126                    });
127                    let proxy = monitor.into_proxy();
128                    fasync::Task::spawn(Arc::clone(&self).send_states(proxy)).detach();
129                    if let Some(reboot_controller) = reboot_controller {
130                        let service = Arc::clone(&self);
131                        fasync::Task::spawn(async move {
132                            let mut stream = reboot_controller.into_stream();
133
134                            while let Some(request) = stream.try_next().await.unwrap() {
135                                let request = match request {
136                                    RebootControllerRequest::Unblock { .. } => {
137                                        CapturedRebootControllerRequest::Unblock
138                                    }
139                                    RebootControllerRequest::Detach { .. } => {
140                                        CapturedRebootControllerRequest::Detach
141                                    }
142                                };
143                                service.reboot_controller_requests.lock().push(request);
144                            }
145                        })
146                        .detach();
147                    }
148                    let response = self.start_update_response.lock();
149                    responder.send(response.as_deref().map_err(|e| *e)).unwrap();
150                }
151                InstallerRequest::SuspendUpdate { attempt_id, responder } => {
152                    self.captured_args
153                        .lock()
154                        .push(CapturedUpdateInstallerRequest::SuspendUpdate { attempt_id });
155                    responder.send(Ok(())).unwrap();
156                }
157                InstallerRequest::ResumeUpdate { attempt_id, responder } => {
158                    self.captured_args
159                        .lock()
160                        .push(CapturedUpdateInstallerRequest::ResumeUpdate { attempt_id });
161                    responder.send(Ok(())).unwrap();
162                }
163                InstallerRequest::CancelUpdate { attempt_id, responder } => {
164                    self.captured_args
165                        .lock()
166                        .push(CapturedUpdateInstallerRequest::CancelUpdate { attempt_id });
167                    responder.send(Ok(())).unwrap();
168                }
169                InstallerRequest::MonitorUpdate { .. } => {
170                    panic!("unexpected request: {req:?}");
171                }
172            }
173        }
174    }
175
176    async fn send_states(self: Arc<Self>, monitor: MonitorProxy) {
177        let mut receiver = self
178            .states_receiver
179            .lock()
180            .take()
181            .expect("mock installer only supports a single StartUpdate call");
182
183        while let Some(state) = receiver.next().await {
184            Self::send_state(state, &monitor).await;
185        }
186    }
187
188    async fn send_state(state: State, monitor: &MonitorProxy) {
189        let is_reboot = state.id() == StateId::Reboot;
190        let result = monitor.on_state(&state.into()).await;
191        if is_reboot {
192            assert_matches!(result, Err(_));
193        } else {
194            assert_matches!(result, Ok(()));
195        }
196    }
197
198    pub fn assert_installer_called_with(&self, expected_args: Vec<CapturedUpdateInstallerRequest>) {
199        assert_eq!(*self.captured_args.lock(), expected_args);
200    }
201
202    pub fn captured_args(&self) -> &Mutex<Vec<CapturedUpdateInstallerRequest>> {
203        &self.captured_args
204    }
205
206    pub fn assert_reboot_controller_called_with(
207        &self,
208        expected_requests: Vec<CapturedRebootControllerRequest>,
209    ) {
210        assert_eq!(*self.reboot_controller_requests.lock(), expected_requests);
211    }
212
213    #[cfg(feature = "fdomain")]
214    pub fn spawn_installer_service(
215        self: Arc<Self>,
216        client: Arc<fdomain_client::Client>,
217    ) -> InstallerProxy {
218        let (proxy, stream) = client.create_proxy_and_stream::<InstallerMarker>();
219
220        fasync::Task::spawn(self.run_service(stream)).detach();
221
222        proxy
223    }
224
225    #[cfg(not(feature = "fdomain"))]
226    pub fn spawn_installer_service(self: Arc<Self>) -> InstallerProxy {
227        let (proxy, stream) = fidl::endpoints::create_proxy_and_stream::<InstallerMarker>();
228
229        fasync::Task::spawn(self.run_service(stream)).detach();
230
231        proxy
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use flex_fuchsia_update_installer::{Initiator, MonitorMarker, MonitorRequest};
239    use pretty_assertions::assert_eq;
240
241    #[fasync::run_singlethreaded(test)]
242    async fn test_mock_installer() {
243        #[cfg(feature = "fdomain")]
244        let client = fdomain_local::local_client_empty();
245        #[cfg(not(feature = "fdomain"))]
246        let client = fidl::endpoints::ZirconClient;
247        let installer_service =
248            Arc::new(MockUpdateInstallerService::with_states(vec![State::Prepare]));
249        let proxy = Arc::clone(&installer_service).spawn_installer_service(
250            #[cfg(feature = "fdomain")]
251            client.clone(),
252        );
253        let url =
254            flex_fuchsia_pkg::PackageUrl { url: "fuchsia-pkg://fuchsia.com/update".to_string() };
255        let options = Options {
256            initiator: Some(Initiator::User),
257            should_write_recovery: Some(true),
258            allow_attach_to_existing_attempt: Some(true),
259            ..Default::default()
260        };
261        let (monitor_client_end, stream) = client.create_request_stream::<MonitorMarker>();
262        proxy
263            .start_update(&url, &options, monitor_client_end, None)
264            .await
265            .expect("made start_update call")
266            .expect("start_update call succeeded");
267        assert_eq!(
268            vec![State::Prepare],
269            stream
270                .map_ok(|MonitorRequest::OnState { state, responder }| {
271                    responder.send().unwrap();
272                    State::try_from(state).unwrap()
273                })
274                .try_collect::<Vec<_>>()
275                .await
276                .unwrap()
277        );
278    }
279}