mock_reboot/
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 anyhow::Error;
6use fidl_fuchsia_hardware_power_statecontrol::{
7    AdminPerformRebootResult, AdminProxy, AdminRequest, AdminRequestStream, RebootOptions,
8    RebootReason2, ShutdownOptions, ShutdownReason,
9};
10use fuchsia_async as fasync;
11use futures::{TryFutureExt, TryStreamExt};
12use std::sync::Arc;
13
14// TODO(https://fxbug.dev/465530639): remove once clients are migrated from PerformReboot to
15// Shutdown.
16fn shutdown_reason_to_reboot_reason(reason: ShutdownReason) -> Option<RebootReason2> {
17    match reason {
18        ShutdownReason::UserRequest => Some(RebootReason2::UserRequest),
19        ShutdownReason::DeveloperRequest => Some(RebootReason2::DeveloperRequest),
20        ShutdownReason::SystemUpdate => Some(RebootReason2::SystemUpdate),
21        ShutdownReason::RetrySystemUpdate => Some(RebootReason2::RetrySystemUpdate),
22        ShutdownReason::HighTemperature => Some(RebootReason2::HighTemperature),
23        ShutdownReason::FactoryDataReset => Some(RebootReason2::FactoryDataReset),
24        ShutdownReason::SessionFailure => Some(RebootReason2::SessionFailure),
25        ShutdownReason::CriticalComponentFailure => Some(RebootReason2::CriticalComponentFailure),
26        ShutdownReason::ZbiSwap => Some(RebootReason2::ZbiSwap),
27        ShutdownReason::OutOfMemory => Some(RebootReason2::OutOfMemory),
28        ShutdownReason::NetstackMigration => Some(RebootReason2::NetstackMigration),
29        ShutdownReason::AndroidUnexpectedReason => Some(RebootReason2::AndroidUnexpectedReason),
30        ShutdownReason::AndroidRescueParty => Some(RebootReason2::AndroidRescueParty),
31        ShutdownReason::AndroidCriticalProcessFailure => {
32            Some(RebootReason2::AndroidCriticalProcessFailure)
33        }
34        _ => None,
35    }
36}
37
38fn shutdown_options_to_reboot_options(options: ShutdownOptions) -> RebootOptions {
39    let reasons = options
40        .reasons
41        .map(|reasons| reasons.into_iter().filter_map(shutdown_reason_to_reboot_reason).collect());
42    RebootOptions { reasons, ..Default::default() }
43}
44
45pub struct MockRebootService {
46    call_hook: Box<dyn Fn(RebootOptions) -> AdminPerformRebootResult + Send + Sync>,
47}
48
49impl MockRebootService {
50    /// Creates a new MockRebootService with a given callback to run per call to the service.
51    /// `call_hook` must return a `Result` for each call, which will be sent to
52    /// the caller as the result of the reboot call.
53    pub fn new(
54        call_hook: Box<dyn Fn(RebootOptions) -> AdminPerformRebootResult + Send + Sync>,
55    ) -> Self {
56        Self { call_hook }
57    }
58
59    /// Serves only the reboot portion of the fuchsia.hardware.power.statecontrol protocol on the
60    /// given request stream.
61    pub async fn run_reboot_service(
62        self: Arc<Self>,
63        mut stream: AdminRequestStream,
64    ) -> Result<(), Error> {
65        while let Some(event) = stream.try_next().await.expect("received request") {
66            match event {
67                AdminRequest::PerformReboot { options, responder } => {
68                    let result = (self.call_hook)(options);
69                    responder.send(result)?;
70                }
71                AdminRequest::Shutdown { options, responder } => {
72                    let result = (self.call_hook)(shutdown_options_to_reboot_options(options));
73                    responder.send(result)?;
74                }
75                _ => {
76                    panic!("unhandled RebootService method {event:?}");
77                }
78            }
79        }
80        Ok(())
81    }
82
83    /// Spawns and detaches a Fuchsia async Task which serves the reboot portion of the
84    /// fuchsia.hardware.power.statecontrol protocol, returning a proxy directly.
85    pub fn spawn_reboot_service(self: Arc<Self>) -> AdminProxy {
86        let (proxy, stream) = fidl::endpoints::create_proxy_and_stream::<
87            fidl_fuchsia_hardware_power_statecontrol::AdminMarker,
88        >();
89
90        fasync::Task::spawn(
91            self.run_reboot_service(stream)
92                .unwrap_or_else(|e| panic!("error running reboot service: {e:?}")),
93        )
94        .detach();
95
96        proxy
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use fidl_fuchsia_hardware_power_statecontrol::RebootReason2;
104    use fuchsia_async as fasync;
105    use std::sync::atomic::{AtomicU32, Ordering};
106
107    #[fasync::run_singlethreaded(test)]
108    async fn test_mock_reboot() {
109        let reboot_service = Arc::new(MockRebootService::new(Box::new(|_| Ok(()))));
110
111        let reboot_service_clone = Arc::clone(&reboot_service);
112        let proxy = reboot_service_clone.spawn_reboot_service();
113
114        proxy
115            .perform_reboot(&RebootOptions {
116                reasons: Some(vec![RebootReason2::SystemUpdate]),
117                ..Default::default()
118            })
119            .await
120            .expect("made reboot call")
121            .expect("reboot call succeeded");
122    }
123
124    #[fasync::run_singlethreaded(test)]
125    async fn test_mock_reboot_fails() {
126        let reboot_service =
127            Arc::new(MockRebootService::new(Box::new(|_| Err(zx::Status::INTERNAL.into_raw()))));
128
129        let reboot_service_clone = Arc::clone(&reboot_service);
130        let proxy = reboot_service_clone.spawn_reboot_service();
131
132        let reboot_result = proxy
133            .perform_reboot(&RebootOptions {
134                reasons: Some(vec![RebootReason2::SystemUpdate]),
135                ..Default::default()
136            })
137            .await
138            .expect("made reboot call");
139        assert_eq!(reboot_result, Err(zx::Status::INTERNAL.into_raw()));
140    }
141
142    #[fasync::run_singlethreaded(test)]
143    async fn test_mock_reboot_call_hook() {
144        let reboot_service = Arc::new(MockRebootService::new(Box::new(|options| {
145            if let Some(reasons) = options.reasons {
146                match &reasons[..] {
147                    [RebootReason2::DeveloperRequest] => Ok(()),
148                    _ => Err(zx::Status::NOT_SUPPORTED.into_raw()),
149                }
150            } else {
151                Err(zx::Status::NOT_SUPPORTED.into_raw())
152            }
153        })));
154
155        let reboot_service_clone = Arc::clone(&reboot_service);
156        let proxy = reboot_service_clone.spawn_reboot_service();
157
158        // Succeed when given expected reboot reason.
159        let () = proxy
160            .perform_reboot(&RebootOptions {
161                reasons: Some(vec![RebootReason2::DeveloperRequest]),
162                ..Default::default()
163            })
164            .await
165            .expect("made reboot call")
166            .expect("reboot call succeeded");
167
168        // Error when given unexpected reboot reason.
169        let error_reboot_result = proxy
170            .perform_reboot(&RebootOptions {
171                reasons: Some(vec![RebootReason2::SystemUpdate]),
172                ..Default::default()
173            })
174            .await
175            .expect("made reboot call");
176        assert_eq!(error_reboot_result, Err(zx::Status::NOT_SUPPORTED.into_raw()));
177    }
178
179    #[fasync::run_singlethreaded(test)]
180    async fn test_mock_reboot_with_external_state() {
181        let called = Arc::new(AtomicU32::new(0));
182        let called_clone = Arc::clone(&called);
183        let reboot_service = Arc::new(MockRebootService::new(Box::new(move |_| {
184            called_clone.fetch_add(1, Ordering::SeqCst);
185            Ok(())
186        })));
187
188        let reboot_service_clone = Arc::clone(&reboot_service);
189        let proxy = reboot_service_clone.spawn_reboot_service();
190
191        proxy
192            .perform_reboot(&RebootOptions {
193                reasons: Some(vec![RebootReason2::SystemUpdate]),
194                ..Default::default()
195            })
196            .await
197            .expect("made reboot call")
198            .expect("reboot call succeeded");
199        assert_eq!(called.load(Ordering::SeqCst), 1);
200    }
201}