| // Copyright 2019 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 { |
| anyhow::anyhow, |
| event_queue::{ControlHandle, EventQueue, Notify}, |
| fidl_fuchsia_update_ext::State, |
| fuchsia_inspect_contrib::inspectable::InspectableDebugString, |
| fuchsia_syslog::{fx_log_err, fx_log_warn}, |
| futures::prelude::*, |
| }; |
| |
| pub trait StateNotifier: Notify<State> + Send + Sync + 'static {} |
| |
| impl<T> StateNotifier for T where T: Notify<State> + Send + Sync + 'static {} |
| |
| #[derive(Debug)] |
| pub struct UpdateMonitor<N> |
| where |
| N: StateNotifier, |
| { |
| temporary_queue: ControlHandle<N, State>, |
| update_state: InspectableDebugString<Option<State>>, |
| version_available: InspectableDebugString<Option<String>>, |
| inspect_node: fuchsia_inspect::Node, |
| } |
| |
| impl<N> UpdateMonitor<N> |
| where |
| N: StateNotifier, |
| { |
| pub fn from_inspect_node(node: fuchsia_inspect::Node) -> (impl Future<Output = ()>, Self) { |
| let (temporary_fut, temporary_queue) = EventQueue::new(); |
| ( |
| temporary_fut, |
| UpdateMonitor { |
| temporary_queue, |
| update_state: InspectableDebugString::new(None, &node, "update-state"), |
| version_available: InspectableDebugString::new(None, &node, "version-available"), |
| inspect_node: node, |
| }, |
| ) |
| } |
| |
| #[cfg(test)] |
| pub fn new() -> (impl Future<Output = ()>, Self) { |
| Self::from_inspect_node( |
| fuchsia_inspect::Inspector::new().root().create_child("test-update-monitor-root-node"), |
| ) |
| } |
| |
| pub async fn add_temporary_callback(&mut self, callback: N) { |
| if let Err(e) = self.temporary_queue.add_client(callback).await { |
| fx_log_err!("error adding client to temporary queue: {:#}", anyhow!(e)) |
| } |
| } |
| |
| pub async fn advance_update_state(&mut self, next_update_state: Option<State>) { |
| *self.update_state.get_mut() = next_update_state; |
| if *self.update_state == None { |
| *self.version_available.get_mut() = None; |
| } |
| self.send_on_state().await; |
| if *self.update_state == None { |
| if let Err(e) = self.temporary_queue.clear().await { |
| fx_log_warn!("error clearing clients of temporary queue: {:#}", anyhow!(e)) |
| } |
| } |
| } |
| |
| pub fn set_version_available(&mut self, version_available: String) { |
| *self.version_available.get_mut() = Some(version_available); |
| } |
| |
| #[cfg(test)] |
| pub fn get_version_available(&self) -> Option<String> { |
| self.version_available.clone() |
| } |
| |
| pub fn update_state(&self) -> Option<State> { |
| self.update_state.clone() |
| } |
| |
| pub async fn send_on_state(&mut self) { |
| if let Some(state) = self.update_state() { |
| if let Err(e) = self.temporary_queue.queue_event(state).await { |
| fx_log_warn!("error sending state to temporary queue: {:#}", anyhow!(e)) |
| } |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use super::*; |
| use event_queue::{ClosedClient, Notify}; |
| use fidl_fuchsia_update_ext::{ |
| random_installation_deferred_data, random_installation_error_data, random_installing_data, |
| random_version_available, |
| }; |
| use fuchsia_async as fasync; |
| use fuchsia_zircon as zx; |
| use futures::future::BoxFuture; |
| use parking_lot::Mutex; |
| use proptest::prelude::*; |
| use std::sync::Arc; |
| |
| const VERSION_AVAILABLE: &str = "fake-version-available"; |
| |
| #[derive(Clone, Debug)] |
| struct FakeStateNotifier { |
| states: Arc<Mutex<Vec<State>>>, |
| } |
| impl FakeStateNotifier { |
| fn new() -> Self { |
| Self { states: Arc::new(Mutex::new(vec![])) } |
| } |
| } |
| impl Notify<State> for FakeStateNotifier { |
| fn notify(&self, state: State) -> BoxFuture<'static, Result<(), ClosedClient>> { |
| self.states.lock().push(state); |
| future::ready(Ok(())).boxed() |
| } |
| } |
| |
| prop_compose! { |
| fn random_update_state() |
| ( |
| installation_deferred_data in random_installation_deferred_data(), |
| installing_data in random_installing_data(), |
| installation_error_data in random_installation_error_data() |
| ) |
| ( |
| state in prop_oneof![ |
| Just(Some(State::CheckingForUpdates)), |
| Just(Some(State::ErrorCheckingForUpdate)), |
| Just(Some(State::NoUpdateAvailable)), |
| Just(Some(State::InstallationDeferredByPolicy(installation_deferred_data))), |
| Just(Some(State::InstallingUpdate(installing_data.clone()))), |
| Just(Some(State::WaitingForReboot(installing_data))), |
| Just(Some(State::InstallationError(installation_error_data))), |
| Just(None), |
| ]) -> Option<State> |
| { |
| state |
| } |
| } |
| |
| async fn random_update_monitor( |
| update_state: Option<State>, |
| version_available: Option<String>, |
| ) -> UpdateMonitor<FakeStateNotifier> { |
| let (fut, mut mms) = UpdateMonitor::<FakeStateNotifier>::new(); |
| fasync::spawn(fut); |
| version_available.map(|s| mms.set_version_available(s)); |
| mms.advance_update_state(update_state).await; |
| mms |
| } |
| |
| async fn wait_for_states(callback: &FakeStateNotifier, len: usize) { |
| while callback.states.lock().len() != len { |
| fasync::Timer::new(fasync::Time::after(zx::Duration::from_millis(10))).await; |
| } |
| } |
| |
| proptest! { |
| #[test] |
| fn test_adding_callback_sends_current_state( |
| update_state in random_update_state(), |
| version_available in random_version_available(), |
| ) { |
| fasync::Executor::new().unwrap().run_singlethreaded(async { |
| let mut update_monitor = random_update_monitor(update_state.clone(), version_available).await; |
| let expected_states: Vec<_> = update_state.into_iter().collect(); |
| let temporary_callback = FakeStateNotifier::new(); |
| |
| update_monitor.add_temporary_callback(temporary_callback.clone()).await; |
| |
| wait_for_states(&temporary_callback, expected_states.len()).await; |
| prop_assert_eq!( |
| &temporary_callback.states.lock().clone(), |
| &expected_states |
| ); |
| Ok(()) |
| }).unwrap(); |
| } |
| |
| #[test] |
| fn test_advance_update_state_calls_callbacks( |
| initial_state in random_update_state(), |
| version_available in random_version_available(), |
| next_state in random_update_state(), |
| ) { |
| fasync::Executor::new().unwrap().run_singlethreaded(async { |
| let mut update_monitor = random_update_monitor(initial_state.clone(), version_available).await; |
| let temporary_callback = FakeStateNotifier::new(); |
| let expected_states: Vec<_> = vec![initial_state, next_state.clone()].into_iter().filter_map(|o|o).collect(); |
| |
| update_monitor.add_temporary_callback(temporary_callback.clone()).await; |
| |
| update_monitor.advance_update_state(next_state).await; |
| |
| wait_for_states(&temporary_callback, expected_states.len()).await; |
| prop_assert_eq!( |
| &temporary_callback.states.lock().clone(), |
| &expected_states |
| ); |
| Ok(()) |
| }).unwrap(); |
| } |
| |
| #[test] |
| fn test_advance_updater_state_to_none_clears_version_available( |
| update_state in random_update_state(), |
| version_available in random_version_available(), |
| ) { |
| fasync::Executor::new().unwrap().run_singlethreaded(async { |
| let mut update_monitor = random_update_monitor(update_state, version_available).await; |
| update_monitor.set_version_available(VERSION_AVAILABLE.to_string()); |
| |
| update_monitor.advance_update_state(None).await; |
| |
| prop_assert_eq!( |
| update_monitor.get_version_available(), |
| None |
| ); |
| Ok(()) |
| }).unwrap(); |
| } |
| |
| #[test] |
| fn test_advance_update_state_to_idle_clears_temporary_callbacks( |
| update_state in random_update_state(), |
| version_available in random_version_available(), |
| ) { |
| fasync::Executor::new().unwrap().run_singlethreaded(async { |
| let mut update_monitor = random_update_monitor(update_state, version_available).await; |
| let temporary_callback = FakeStateNotifier::new(); |
| |
| update_monitor.add_temporary_callback(temporary_callback.clone()).await; |
| update_monitor.advance_update_state(None).await; |
| temporary_callback.states.lock().clear(); |
| update_monitor.advance_update_state(Some(State::CheckingForUpdates)).await; |
| |
| prop_assert_eq!(temporary_callback.states.lock().clone(), vec![]); |
| Ok(()) |
| }).unwrap(); |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod test_inspect { |
| use super::*; |
| use event_queue::{ClosedClient, Notify}; |
| use fuchsia_async as fasync; |
| use fuchsia_inspect::assert_inspect_tree; |
| use futures::future::BoxFuture; |
| |
| #[derive(Clone, Debug)] |
| struct FakeStateNotifier; |
| impl Notify<State> for FakeStateNotifier { |
| fn notify(&self, _state: State) -> BoxFuture<'static, Result<(), ClosedClient>> { |
| future::ready(Ok(())).boxed() |
| } |
| } |
| |
| #[test] |
| fn test_inspect_initial_state() { |
| let inspector = fuchsia_inspect::Inspector::new(); |
| let _update_monitor = UpdateMonitor::<FakeStateNotifier>::from_inspect_node( |
| inspector.root().create_child("update-monitor"), |
| ); |
| |
| assert_inspect_tree!( |
| inspector, |
| root: { |
| "update-monitor": { |
| "update-state": "None", |
| "version-available": "None", |
| } |
| } |
| ); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_inspect_update_state() { |
| let inspector = fuchsia_inspect::Inspector::new(); |
| let (fut, mut update_monitor) = UpdateMonitor::<FakeStateNotifier>::from_inspect_node( |
| inspector.root().create_child("update-monitor"), |
| ); |
| fasync::spawn(fut); |
| |
| update_monitor.advance_update_state(Some(State::CheckingForUpdates)).await; |
| |
| assert_inspect_tree!( |
| inspector, |
| root: { |
| "update-monitor": { |
| "update-state": format!("{:?}", Some(State::CheckingForUpdates)), |
| "version-available": "None", |
| } |
| } |
| ); |
| } |
| |
| #[test] |
| fn test_inspect_version_available() { |
| let inspector = fuchsia_inspect::Inspector::new(); |
| let (_fut, mut update_monitor) = UpdateMonitor::<FakeStateNotifier>::from_inspect_node( |
| inspector.root().create_child("update-monitor"), |
| ); |
| let version_available = "fake-version-available"; |
| |
| update_monitor.set_version_available(version_available.to_string()); |
| |
| assert_inspect_tree!( |
| inspector, |
| root: { |
| "update-monitor": { |
| "update-state": "None", |
| "version-available": format!("{:?}", Some(version_available)), |
| } |
| } |
| ); |
| } |
| } |