fidl_fuchsia_update_ext/
types.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 event_queue::Event;
6use fidl_fuchsia_update as fidl;
7use proptest::prelude::*;
8use proptest_derive::Arbitrary;
9use std::fmt;
10use thiserror::Error;
11use typed_builder::TypedBuilder;
12
13/// Wrapper type for [`fidl_fuchsia_update::State`] which works with
14/// [`event_queue`] and [`proptest`].
15///
16/// Use [`From`] (and [`Into`]) to convert between the fidl type and this one.
17///
18/// See [`fidl_fuchsia_update::State`] for docs on what each state means.
19#[allow(missing_docs)] // states are documented in fidl.
20#[derive(Clone, Debug, PartialEq, Arbitrary)]
21pub enum State {
22    CheckingForUpdates,
23    ErrorCheckingForUpdate,
24    NoUpdateAvailable,
25    InstallationDeferredByPolicy(InstallationDeferredData),
26    InstallingUpdate(InstallingData),
27    WaitingForReboot(InstallingData),
28    InstallationError(InstallationErrorData),
29}
30
31impl State {
32    /// Returns true if this state is an error state.
33    pub fn is_error(&self) -> bool {
34        match self {
35            State::ErrorCheckingForUpdate | State::InstallationError(_) => true,
36            State::CheckingForUpdates
37            | State::NoUpdateAvailable
38            | State::InstallationDeferredByPolicy(_)
39            | State::InstallingUpdate(_)
40            | State::WaitingForReboot(_) => false,
41        }
42    }
43
44    /// Returns true if this state is a terminal state.
45    pub fn is_terminal(&self) -> bool {
46        match self {
47            State::CheckingForUpdates | State::InstallingUpdate(_) => false,
48            State::ErrorCheckingForUpdate
49            | State::InstallationError(_)
50            | State::NoUpdateAvailable
51            | State::InstallationDeferredByPolicy(_)
52            | State::WaitingForReboot(_) => true,
53        }
54    }
55}
56
57impl fmt::Display for State {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        match self {
60            State::CheckingForUpdates => write!(f, "Checking for updates...")?,
61            State::ErrorCheckingForUpdate => write!(f, "Error checking for update!")?,
62            State::InstallationError(_) => write!(f, "Installation error: {self:?}")?,
63            State::NoUpdateAvailable => write!(f, "No update available.")?,
64            State::InstallationDeferredByPolicy(InstallationDeferredData {
65                deferral_reason,
66                ..
67            }) => {
68                write!(f, "Installation deferred by policy: {deferral_reason:?}")?;
69            }
70            State::InstallingUpdate(InstallingData { update, installation_progress }) => {
71                if let Some(UpdateInfo { version_available: Some(version), .. }) = update {
72                    write!(f, "Installing {version}")?;
73                } else {
74                    write!(f, "Installing update")?;
75                }
76                if let Some(InstallationProgress { fraction_completed: Some(fraction) }) =
77                    installation_progress
78                {
79                    write!(f, " ({:.2}%)", fraction * 100.0)?;
80                }
81            }
82            State::WaitingForReboot(_) => write!(f, "Waiting for reboot...")?,
83        }
84        Ok(())
85    }
86}
87
88impl Event for State {
89    fn can_merge(&self, other: &State) -> bool {
90        if self == other {
91            return true;
92        }
93        // Merge states that have the same update info but different installation
94        // progress
95        if let State::InstallingUpdate(InstallingData { update: update0, .. }) = self {
96            if let State::InstallingUpdate(InstallingData { update: update1, .. }) = other {
97                return update0 == update1;
98            }
99        }
100        false
101    }
102}
103
104impl From<State> for fidl::State {
105    fn from(other: State) -> Self {
106        match other {
107            State::CheckingForUpdates => {
108                fidl::State::CheckingForUpdates(fidl::CheckingForUpdatesData::default())
109            }
110            State::ErrorCheckingForUpdate => {
111                fidl::State::ErrorCheckingForUpdate(fidl::ErrorCheckingForUpdateData::default())
112            }
113            State::NoUpdateAvailable => {
114                fidl::State::NoUpdateAvailable(fidl::NoUpdateAvailableData::default())
115            }
116            State::InstallationDeferredByPolicy(data) => {
117                fidl::State::InstallationDeferredByPolicy(data.into())
118            }
119            State::InstallingUpdate(data) => fidl::State::InstallingUpdate(data.into()),
120            State::WaitingForReboot(data) => fidl::State::WaitingForReboot(data.into()),
121            State::InstallationError(data) => fidl::State::InstallationError(data.into()),
122        }
123    }
124}
125
126impl From<fidl::State> for State {
127    fn from(fidl_state: fidl::State) -> Self {
128        match fidl_state {
129            fidl::State::CheckingForUpdates(_) => State::CheckingForUpdates,
130            fidl::State::ErrorCheckingForUpdate(_) => State::ErrorCheckingForUpdate,
131            fidl::State::NoUpdateAvailable(_) => State::NoUpdateAvailable,
132            fidl::State::InstallationDeferredByPolicy(data) => {
133                State::InstallationDeferredByPolicy(data.into())
134            }
135            fidl::State::InstallingUpdate(data) => State::InstallingUpdate(data.into()),
136            fidl::State::WaitingForReboot(data) => State::WaitingForReboot(data.into()),
137            fidl::State::InstallationError(data) => State::InstallationError(data.into()),
138        }
139    }
140}
141
142/// An error which can be returned when validating [`AttemptOptions`].
143#[derive(Debug, Error, PartialEq, Eq)]
144pub enum AttemptOptionsDecodeError {
145    /// The initiator field was not set.
146    #[error("missing field 'initiator'")]
147    MissingInitiator,
148}
149
150/// Wrapper type for [`fidl_fuchsia_update::AttemptOptions`] which validates the
151/// options on construction and works with [`proptest`].
152///
153/// Use [`TryFrom`] (or [`TryInto`]) to convert the fidl type and this one, and
154/// [`From`] (or [`Into`]) to convert back to the fidl type.
155#[derive(Clone, Debug, PartialEq, Arbitrary)]
156pub struct AttemptOptions {
157    pub initiator: Initiator,
158}
159
160impl Event for AttemptOptions {
161    fn can_merge(&self, _other: &AttemptOptions) -> bool {
162        true
163    }
164}
165
166impl TryFrom<fidl::AttemptOptions> for AttemptOptions {
167    type Error = AttemptOptionsDecodeError;
168
169    fn try_from(o: fidl::AttemptOptions) -> Result<Self, Self::Error> {
170        Ok(Self {
171            initiator: o.initiator.ok_or(AttemptOptionsDecodeError::MissingInitiator)?.into(),
172        })
173    }
174}
175
176impl From<AttemptOptions> for fidl::AttemptOptions {
177    fn from(o: AttemptOptions) -> Self {
178        Self { initiator: Some(o.initiator.into()), ..Default::default() }
179    }
180}
181
182impl From<CheckOptions> for AttemptOptions {
183    fn from(o: CheckOptions) -> Self {
184        Self { initiator: o.initiator }
185    }
186}
187
188/// Wrapper type for [`fidl_fuchsia_update::InstallationErrorData`] which works
189/// with [`proptest`].
190///
191/// Use [`From`] (and [`Into`]) to convert between the fidl type and this one.
192#[derive(Clone, Debug, Default, PartialEq, Arbitrary)]
193pub struct InstallationErrorData {
194    pub update: Option<UpdateInfo>,
195    pub installation_progress: Option<InstallationProgress>,
196}
197impl From<InstallationErrorData> for fidl::InstallationErrorData {
198    fn from(other: InstallationErrorData) -> Self {
199        fidl::InstallationErrorData {
200            update: other.update.map(|ext| ext.into()),
201            installation_progress: other.installation_progress.map(|ext| ext.into()),
202            ..Default::default()
203        }
204    }
205}
206impl From<fidl::InstallationErrorData> for InstallationErrorData {
207    fn from(data: fidl::InstallationErrorData) -> Self {
208        Self {
209            update: data.update.map(|o| o.into()),
210            installation_progress: data.installation_progress.map(|o| o.into()),
211        }
212    }
213}
214
215/// Wrapper type for [`fidl_fuchsia_update::InstallationProgress`] which works
216/// with [`proptest`].
217///
218/// Use [`From`] (and [`Into`]) to convert between the fidl type and this one.
219#[derive(Clone, Debug, PartialEq, Arbitrary)]
220pub struct InstallationProgress {
221    #[proptest(strategy = "prop::option::of(prop::num::f32::NORMAL)")]
222    pub fraction_completed: Option<f32>,
223}
224impl From<InstallationProgress> for fidl::InstallationProgress {
225    fn from(other: InstallationProgress) -> Self {
226        fidl::InstallationProgress {
227            fraction_completed: other.fraction_completed,
228            ..Default::default()
229        }
230    }
231}
232impl From<fidl::InstallationProgress> for InstallationProgress {
233    fn from(progress: fidl::InstallationProgress) -> Self {
234        Self { fraction_completed: progress.fraction_completed }
235    }
236}
237
238/// Wrapper type for [`fidl_fuchsia_update::InstallingData`] which works with
239/// [`proptest`].
240///
241/// Use [`From`] (and [`Into`]) to convert between the fidl type and this one.
242#[derive(Clone, Debug, Default, PartialEq, Arbitrary)]
243pub struct InstallingData {
244    pub update: Option<UpdateInfo>,
245    pub installation_progress: Option<InstallationProgress>,
246}
247impl From<InstallingData> for fidl::InstallingData {
248    fn from(other: InstallingData) -> Self {
249        fidl::InstallingData {
250            update: other.update.map(|ext| ext.into()),
251            installation_progress: other.installation_progress.map(|ext| ext.into()),
252            ..Default::default()
253        }
254    }
255}
256impl From<fidl::InstallingData> for InstallingData {
257    fn from(data: fidl::InstallingData) -> Self {
258        Self {
259            update: data.update.map(|o| o.into()),
260            installation_progress: data.installation_progress.map(|o| o.into()),
261        }
262    }
263}
264
265/// Wrapper type for [`fidl_fuchsia_update::InstallationDeferredData`] which
266/// works with [`proptest`].
267///
268/// Use [`From`] (and [`Into`]) to convert between the fidl type and this one.
269#[derive(Clone, Debug, Default, PartialEq)]
270pub struct InstallationDeferredData {
271    pub update: Option<UpdateInfo>,
272    pub deferral_reason: Option<fidl::InstallationDeferralReason>,
273}
274
275impl From<InstallationDeferredData> for fidl::InstallationDeferredData {
276    fn from(other: InstallationDeferredData) -> Self {
277        fidl::InstallationDeferredData {
278            update: other.update.map(|ext| ext.into()),
279            deferral_reason: other.deferral_reason,
280            ..Default::default()
281        }
282    }
283}
284impl From<fidl::InstallationDeferredData> for InstallationDeferredData {
285    fn from(data: fidl::InstallationDeferredData) -> Self {
286        Self { update: data.update.map(|o| o.into()), deferral_reason: data.deferral_reason }
287    }
288}
289
290// Manually impl Arbitrary because fidl::InstallationDeferralReason does not
291// impl Arbitrary. We could have created another wrapper, but we opted not to in
292// order to guarantee the ext crate stays in sync with the FIDL for
293// InstallationDeferralReason.
294impl Arbitrary for InstallationDeferredData {
295    type Parameters = ();
296    type Strategy = BoxedStrategy<Self>;
297
298    fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
299        any::<UpdateInfo>()
300            .prop_flat_map(|info| {
301                (
302                    proptest::option::of(Just(info)),
303                    proptest::option::of(Just(
304                        fidl::InstallationDeferralReason::CurrentSystemNotCommitted,
305                    )),
306                )
307            })
308            .prop_map(|(update, deferral_reason)| Self { update, deferral_reason })
309            .boxed()
310    }
311}
312
313/// Wrapper type for [`fidl_fuchsia_update::UpdateInfo`] which works with
314/// [`proptest`].
315///
316/// Use [`From`] (and [`Into`]) to convert between the fidl type and this one.
317#[derive(Clone, Debug, PartialEq, Arbitrary)]
318pub struct UpdateInfo {
319    #[proptest(strategy = "proptest_util::random_version_available()")]
320    pub version_available: Option<String>,
321    pub download_size: Option<u64>,
322}
323impl From<UpdateInfo> for fidl::UpdateInfo {
324    fn from(other: UpdateInfo) -> Self {
325        fidl::UpdateInfo {
326            version_available: other.version_available,
327            download_size: other.download_size,
328            ..Default::default()
329        }
330    }
331}
332impl From<fidl::UpdateInfo> for UpdateInfo {
333    fn from(info: fidl::UpdateInfo) -> Self {
334        Self { version_available: info.version_available, download_size: info.download_size }
335    }
336}
337
338/// Wrapper type for [`fidl_fuchsia_update::Initiator`] which works with
339/// [`proptest`].
340///
341/// Use [`From`] (and [`Into`]) to convert between the fidl type and this one.
342#[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary)]
343pub enum Initiator {
344    User,
345    Service,
346}
347
348impl From<fidl::Initiator> for Initiator {
349    fn from(initiator: fidl::Initiator) -> Self {
350        match initiator {
351            fidl::Initiator::User => Initiator::User,
352            fidl::Initiator::Service => Initiator::Service,
353        }
354    }
355}
356
357impl From<Initiator> for fidl::Initiator {
358    fn from(initiator: Initiator) -> Self {
359        match initiator {
360            Initiator::User => fidl::Initiator::User,
361            Initiator::Service => fidl::Initiator::Service,
362        }
363    }
364}
365
366/// An error which can be returned when validating [`CheckOptions`].
367#[derive(Debug, Error, PartialEq, Eq)]
368pub enum CheckOptionsDecodeError {
369    /// The initiator field was not set.
370    #[error("missing field 'initiator'")]
371    MissingInitiator,
372}
373
374/// Wrapper type for [`fidl_fuchsia_update::CheckOptions`] which validates the
375/// options on construction and works with [`proptest`].
376///
377/// Use [`TryFrom`] (or [`TryInto`]) to convert the fidl type and this one, and
378/// [`From`] (or [`Into`]) to convert back to the fidl type.
379#[derive(Clone, Debug, PartialEq, Arbitrary, TypedBuilder)]
380pub struct CheckOptions {
381    pub initiator: Initiator,
382
383    #[builder(default)]
384    pub allow_attaching_to_existing_update_check: bool,
385}
386
387impl TryFrom<fidl::CheckOptions> for CheckOptions {
388    type Error = CheckOptionsDecodeError;
389
390    fn try_from(o: fidl::CheckOptions) -> Result<Self, Self::Error> {
391        Ok(Self {
392            initiator: o.initiator.ok_or(CheckOptionsDecodeError::MissingInitiator)?.into(),
393            allow_attaching_to_existing_update_check: o
394                .allow_attaching_to_existing_update_check
395                .unwrap_or(false),
396        })
397    }
398}
399
400impl From<CheckOptions> for fidl::CheckOptions {
401    fn from(o: CheckOptions) -> Self {
402        Self {
403            initiator: Some(o.initiator.into()),
404            allow_attaching_to_existing_update_check: Some(
405                o.allow_attaching_to_existing_update_check,
406            ),
407            ..Default::default()
408        }
409    }
410}
411
412pub mod proptest_util {
413    use proptest::prelude::*;
414
415    prop_compose! {
416        /// pick an arbitrary version_available value for `UpdateInfo`
417        pub fn random_version_available()(
418            version_available in proptest::option::of("[0-9A-Z]{10,20}")
419        ) -> Option<String> {
420            version_available
421        }
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428
429    proptest! {
430        // states with the same update info but different progress should merge
431        #[test]
432        fn test_state_can_merge(
433            update_info: Option<UpdateInfo>,
434            progress0: Option<InstallationProgress>,
435            progress1: Option<InstallationProgress>,
436        ) {
437            let event0 = State::InstallingUpdate(
438                InstallingData {
439                    update: update_info.clone(),
440                    installation_progress: progress0,
441                }
442            );
443            let event1 = State::InstallingUpdate(
444                InstallingData {
445                    update: update_info,
446                    installation_progress: progress1,
447                }
448            );
449            prop_assert!(event0.can_merge(&event1));
450        }
451
452        #[test]
453        fn test_attempt_options_can_merge(
454            initiator0: Initiator,
455            initiator1: Initiator,
456        ) {
457            let attempt_options0 = AttemptOptions {
458                initiator: initiator0,
459            };
460            let attempt_options1 = AttemptOptions {
461                initiator: initiator1,
462            };
463            prop_assert!(attempt_options0.can_merge(&attempt_options1));
464        }
465
466        #[test]
467        fn test_state_roundtrips(state: State) {
468            let state0: State = state.clone();
469            let fidl_intermediate: fidl::State = state.into();
470            let state1: State = fidl_intermediate.into();
471            prop_assert_eq!(state0, state1);
472        }
473
474        #[test]
475        fn test_initiator_roundtrips(initiator: Initiator) {
476            prop_assert_eq!(
477                Initiator::from(fidl::Initiator::from(initiator)),
478                initiator
479            );
480        }
481
482        #[test]
483        fn test_check_options_roundtrips(check_options: CheckOptions) {
484            prop_assert_eq!(
485                CheckOptions::try_from(fidl::CheckOptions::from(check_options.clone())),
486                Ok(check_options)
487            );
488        }
489
490        #[test]
491        fn test_check_options_initiator_required(allow_attaching_to_existing_update_check: bool) {
492            prop_assert_eq!(
493                CheckOptions::try_from(fidl::CheckOptions {
494                    initiator: None,
495                    allow_attaching_to_existing_update_check: Some(allow_attaching_to_existing_update_check),
496                    ..Default::default()
497                }),
498                Err(CheckOptionsDecodeError::MissingInitiator)
499            );
500        }
501    }
502
503    #[test]
504    fn check_options_builder() {
505        assert_eq!(
506            CheckOptions::builder()
507                .initiator(Initiator::User)
508                .allow_attaching_to_existing_update_check(true)
509                .build(),
510            CheckOptions {
511                initiator: Initiator::User,
512                allow_attaching_to_existing_update_check: true,
513            }
514        );
515    }
516}