fidl_fuchsia_update_installer_ext/
state.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
5//! Wrapper types for the State union.
6
7use event_queue::Event;
8use proptest::prelude::*;
9use proptest_derive::Arbitrary;
10use serde::{Deserialize, Serialize};
11use thiserror::Error;
12use typed_builder::TypedBuilder;
13use {fidl_fuchsia_update_installer as fidl, fuchsia_inspect as inspect};
14
15/// The state of an update installation attempt.
16#[derive(Arbitrary, Clone, Debug, Serialize, Deserialize, PartialEq)]
17#[serde(tag = "id", rename_all = "snake_case")]
18#[allow(missing_docs)]
19pub enum State {
20    Prepare,
21    Stage(UpdateInfoAndProgress),
22    Fetch(UpdateInfoAndProgress),
23    Commit(UpdateInfoAndProgress),
24    WaitToReboot(UpdateInfoAndProgress),
25    Reboot(UpdateInfoAndProgress),
26    DeferReboot(UpdateInfoAndProgress),
27    Complete(UpdateInfoAndProgress),
28    FailPrepare(PrepareFailureReason),
29    FailStage(FailStageData),
30    FailFetch(FailFetchData),
31    FailCommit(UpdateInfoAndProgress),
32    Canceled,
33}
34
35/// The variant names for each state, with data stripped.
36#[allow(missing_docs)]
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
38pub enum StateId {
39    Prepare,
40    Stage,
41    Fetch,
42    Commit,
43    WaitToReboot,
44    Reboot,
45    DeferReboot,
46    Complete,
47    FailPrepare,
48    FailStage,
49    FailFetch,
50    FailCommit,
51    Canceled,
52}
53
54/// Immutable metadata for an update attempt.
55#[derive(
56    Arbitrary, Clone, Copy, Debug, Serialize, Deserialize, PartialEq, PartialOrd, TypedBuilder,
57)]
58pub struct UpdateInfo {
59    download_size: u64,
60}
61
62/// Mutable progress information for an update attempt.
63#[derive(Arbitrary, Clone, Copy, Debug, Serialize, PartialEq, PartialOrd, TypedBuilder)]
64pub struct Progress {
65    /// Within the range of [0.0, 1.0]
66    #[proptest(strategy = "0.0f32 ..= 1.0")]
67    #[builder(setter(transform = |x: f32| x.clamp(0.0, 1.0)))]
68    fraction_completed: f32,
69
70    bytes_downloaded: u64,
71}
72
73/// An UpdateInfo and Progress that are guaranteed to be consistent with each other.
74///
75/// Specifically, `progress.bytes_downloaded <= info.download_size`.
76#[derive(Clone, Copy, Debug, Serialize, PartialEq, PartialOrd)]
77pub struct UpdateInfoAndProgress {
78    info: UpdateInfo,
79    progress: Progress,
80}
81
82/// Builder of UpdateInfoAndProgress.
83#[derive(Clone, Debug)]
84pub struct UpdateInfoAndProgressBuilder;
85
86/// Builder of UpdateInfoAndProgress, with a known UpdateInfo field.
87#[derive(Clone, Debug)]
88pub struct UpdateInfoAndProgressBuilderWithInfo {
89    info: UpdateInfo,
90}
91
92/// Builder of UpdateInfoAndProgress, with a known UpdateInfo and Progress field.
93#[derive(Clone, Debug)]
94pub struct UpdateInfoAndProgressBuilderWithInfoAndProgress {
95    info: UpdateInfo,
96    progress: Progress,
97}
98
99#[derive(Arbitrary, Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
100#[serde(tag = "reason", rename_all = "snake_case")]
101#[allow(missing_docs)]
102pub enum PrepareFailureReason {
103    Internal,
104    OutOfSpace,
105    UnsupportedDowngrade,
106}
107
108#[derive(Arbitrary, Copy, Clone, Debug, PartialEq, Deserialize, Serialize)]
109#[serde(rename_all = "snake_case")]
110#[allow(missing_docs)]
111pub enum StageFailureReason {
112    Internal,
113    OutOfSpace,
114}
115
116#[derive(Clone, Copy, Debug, PartialEq)]
117#[allow(missing_docs)]
118pub struct FailStageData {
119    info_and_progress: UpdateInfoAndProgress,
120    reason: StageFailureReason,
121}
122
123#[derive(Arbitrary, Copy, Clone, Debug, PartialEq, Deserialize, Serialize)]
124#[serde(rename_all = "snake_case")]
125#[allow(missing_docs)]
126pub enum FetchFailureReason {
127    Internal,
128    OutOfSpace,
129}
130
131#[derive(Clone, Copy, Debug, PartialEq)]
132#[allow(missing_docs)]
133pub struct FailFetchData {
134    info_and_progress: UpdateInfoAndProgress,
135    reason: FetchFailureReason,
136}
137
138impl State {
139    /// Obtain the variant name (strip out the data).
140    pub fn id(&self) -> StateId {
141        match self {
142            State::Prepare => StateId::Prepare,
143            State::Stage(_) => StateId::Stage,
144            State::Fetch(_) => StateId::Fetch,
145            State::Commit(_) => StateId::Commit,
146            State::WaitToReboot(_) => StateId::WaitToReboot,
147            State::Reboot(_) => StateId::Reboot,
148            State::DeferReboot(_) => StateId::DeferReboot,
149            State::Complete(_) => StateId::Complete,
150            State::FailPrepare(_) => StateId::FailPrepare,
151            State::FailStage(_) => StateId::FailStage,
152            State::FailFetch(_) => StateId::FailFetch,
153            State::FailCommit(_) => StateId::FailCommit,
154            State::Canceled => StateId::Canceled,
155        }
156    }
157
158    /// Determines if this state is terminal and represents a successful attempt.
159    pub fn is_success(&self) -> bool {
160        matches!(self.id(), StateId::Reboot | StateId::DeferReboot | StateId::Complete)
161    }
162
163    /// Determines if this state is terminal and represents a failure.
164    pub fn is_failure(&self) -> bool {
165        matches!(
166            self.id(),
167            StateId::FailPrepare | StateId::FailFetch | StateId::FailStage | StateId::Canceled
168        )
169    }
170
171    /// Determines if this state is terminal (terminal states are final, no further state
172    /// transitions should occur).
173    pub fn is_terminal(&self) -> bool {
174        self.is_success() || self.is_failure()
175    }
176
177    /// Returns the name of the state, intended for use in log/diagnostics output.
178    pub fn name(&self) -> &'static str {
179        match self {
180            State::Prepare => "prepare",
181            State::Stage(_) => "stage",
182            State::Fetch(_) => "fetch",
183            State::Commit(_) => "commit",
184            State::WaitToReboot(_) => "wait_to_reboot",
185            State::Reboot(_) => "reboot",
186            State::DeferReboot(_) => "defer_reboot",
187            State::Complete(_) => "complete",
188            State::FailPrepare(_) => "fail_prepare",
189            State::FailStage(_) => "fail_stage",
190            State::FailFetch(_) => "fail_fetch",
191            State::FailCommit(_) => "fail_commit",
192            State::Canceled => "canceled",
193        }
194    }
195
196    /// Serializes this state to a Fuchsia Inspect node.
197    pub fn write_to_inspect(&self, node: &inspect::Node) {
198        node.record_string("state", self.name());
199        use State::*;
200
201        match self {
202            Prepare | Canceled => {}
203            FailStage(data) => data.write_to_inspect(node),
204            FailFetch(data) => data.write_to_inspect(node),
205            FailPrepare(reason) => reason.write_to_inspect(node),
206            Stage(info_progress)
207            | Fetch(info_progress)
208            | Commit(info_progress)
209            | WaitToReboot(info_progress)
210            | Reboot(info_progress)
211            | DeferReboot(info_progress)
212            | Complete(info_progress)
213            | FailCommit(info_progress) => {
214                info_progress.write_to_inspect(node);
215            }
216        }
217    }
218
219    /// Extracts info_and_progress, if the state supports it.
220    fn info_and_progress(&self) -> Option<&UpdateInfoAndProgress> {
221        match self {
222            State::Prepare | State::FailPrepare(_) | State::Canceled => None,
223            State::FailStage(data) => Some(&data.info_and_progress),
224            State::FailFetch(data) => Some(&data.info_and_progress),
225            State::Stage(data)
226            | State::Fetch(data)
227            | State::Commit(data)
228            | State::WaitToReboot(data)
229            | State::Reboot(data)
230            | State::DeferReboot(data)
231            | State::Complete(data)
232            | State::FailCommit(data) => Some(data),
233        }
234    }
235
236    /// Extracts progress, if the state supports it.
237    pub fn progress(&self) -> Option<&Progress> {
238        match self.info_and_progress() {
239            Some(UpdateInfoAndProgress { info: _, progress }) => Some(progress),
240            _ => None,
241        }
242    }
243
244    /// Extracts the download_size field in UpdateInfo, if the state supports it.
245    pub fn download_size(&self) -> Option<u64> {
246        match self.info_and_progress() {
247            Some(UpdateInfoAndProgress { info, progress: _ }) => Some(info.download_size()),
248            _ => None,
249        }
250    }
251}
252
253impl Event for State {
254    fn can_merge(&self, other: &Self) -> bool {
255        self.id() == other.id()
256    }
257}
258
259impl UpdateInfo {
260    /// Gets the download_size field.
261    pub fn download_size(&self) -> u64 {
262        self.download_size
263    }
264
265    fn write_to_inspect(&self, node: &inspect::Node) {
266        let UpdateInfo { download_size } = self;
267        node.record_uint("download_size", *download_size)
268    }
269}
270
271impl Progress {
272    /// Produces a Progress at 0% complete and 0 bytes downloaded.
273    pub fn none() -> Self {
274        Self { fraction_completed: 0.0, bytes_downloaded: 0 }
275    }
276
277    /// Produces a Progress at 100% complete and all bytes downloaded, based on the download_size
278    /// in `info`.
279    pub fn done(info: &UpdateInfo) -> Self {
280        Self { fraction_completed: 1.0, bytes_downloaded: info.download_size }
281    }
282
283    /// Gets the fraction_completed field.
284    pub fn fraction_completed(&self) -> f32 {
285        self.fraction_completed
286    }
287
288    /// Gets the bytes_downloaded field.
289    pub fn bytes_downloaded(&self) -> u64 {
290        self.bytes_downloaded
291    }
292
293    fn write_to_inspect(&self, node: &inspect::Node) {
294        let Progress { fraction_completed, bytes_downloaded } = self;
295        node.record_double("fraction_completed", *fraction_completed as f64);
296        node.record_uint("bytes_downloaded", *bytes_downloaded);
297    }
298}
299
300impl UpdateInfoAndProgress {
301    /// Starts building an instance of UpdateInfoAndProgress.
302    pub fn builder() -> UpdateInfoAndProgressBuilder {
303        UpdateInfoAndProgressBuilder
304    }
305
306    /// Constructs an UpdateInfoAndProgress from the 2 fields, ensuring that the 2 structs are
307    /// consistent with each other, returning an error if they are not.
308    pub fn new(
309        info: UpdateInfo,
310        progress: Progress,
311    ) -> Result<Self, BytesFetchedExceedsDownloadSize> {
312        if progress.bytes_downloaded > info.download_size {
313            return Err(BytesFetchedExceedsDownloadSize);
314        }
315
316        Ok(Self { info, progress })
317    }
318
319    /// Constructs an UpdateInfoAndProgress from an UpdateInfo, setting the progress fields to be
320    /// 100% done with all bytes downloaded.
321    pub fn done(info: UpdateInfo) -> Self {
322        Self { progress: Progress::done(&info), info }
323    }
324
325    /// Returns the info field.
326    pub fn info(&self) -> UpdateInfo {
327        self.info
328    }
329
330    /// Returns the progress field.
331    pub fn progress(&self) -> &Progress {
332        &self.progress
333    }
334
335    /// Constructs a FailStageData with the given reason.
336    pub fn with_stage_reason(self, reason: StageFailureReason) -> FailStageData {
337        FailStageData { info_and_progress: self, reason }
338    }
339
340    /// Constructs a FailFetchData with the given reason.
341    pub fn with_fetch_reason(self, reason: FetchFailureReason) -> FailFetchData {
342        FailFetchData { info_and_progress: self, reason }
343    }
344
345    fn write_to_inspect(&self, node: &inspect::Node) {
346        node.record_child("info", |n| {
347            self.info.write_to_inspect(n);
348        });
349        node.record_child("progress", |n| {
350            self.progress.write_to_inspect(n);
351        });
352    }
353}
354
355impl UpdateInfoAndProgressBuilder {
356    /// Sets the UpdateInfo field.
357    pub fn info(self, info: UpdateInfo) -> UpdateInfoAndProgressBuilderWithInfo {
358        UpdateInfoAndProgressBuilderWithInfo { info }
359    }
360}
361
362impl UpdateInfoAndProgressBuilderWithInfo {
363    /// Sets the Progress field, clamping `progress.bytes_downloaded` to be `<=
364    /// info.download_size`. Users of this API should independently ensure that this invariant is
365    /// not violated.
366    pub fn progress(
367        self,
368        mut progress: Progress,
369    ) -> UpdateInfoAndProgressBuilderWithInfoAndProgress {
370        if progress.bytes_downloaded > self.info.download_size {
371            progress.bytes_downloaded = self.info.download_size;
372        }
373
374        UpdateInfoAndProgressBuilderWithInfoAndProgress { info: self.info, progress }
375    }
376}
377
378impl UpdateInfoAndProgressBuilderWithInfoAndProgress {
379    /// Builds the UpdateInfoAndProgress instance.
380    pub fn build(self) -> UpdateInfoAndProgress {
381        let Self { info, progress } = self;
382        UpdateInfoAndProgress { info, progress }
383    }
384}
385
386impl FailStageData {
387    fn write_to_inspect(&self, node: &inspect::Node) {
388        self.info_and_progress.write_to_inspect(node);
389        self.reason.write_to_inspect(node);
390    }
391
392    /// Get the reason associated with this FailStageData
393    pub fn reason(&self) -> StageFailureReason {
394        self.reason
395    }
396}
397
398impl FailFetchData {
399    fn write_to_inspect(&self, node: &inspect::Node) {
400        self.info_and_progress.write_to_inspect(node);
401        self.reason.write_to_inspect(node);
402    }
403
404    /// Get the reason associated with this FetchFailData
405    pub fn reason(&self) -> FetchFailureReason {
406        self.reason
407    }
408}
409
410impl PrepareFailureReason {
411    fn write_to_inspect(&self, node: &inspect::Node) {
412        node.record_string("reason", format!("{self:?}"))
413    }
414}
415
416impl From<fidl::PrepareFailureReason> for PrepareFailureReason {
417    fn from(reason: fidl::PrepareFailureReason) -> Self {
418        match reason {
419            fidl::PrepareFailureReason::Internal => PrepareFailureReason::Internal,
420            fidl::PrepareFailureReason::OutOfSpace => PrepareFailureReason::OutOfSpace,
421            fidl::PrepareFailureReason::UnsupportedDowngrade => {
422                PrepareFailureReason::UnsupportedDowngrade
423            }
424        }
425    }
426}
427
428impl From<PrepareFailureReason> for fidl::PrepareFailureReason {
429    fn from(reason: PrepareFailureReason) -> Self {
430        match reason {
431            PrepareFailureReason::Internal => fidl::PrepareFailureReason::Internal,
432            PrepareFailureReason::OutOfSpace => fidl::PrepareFailureReason::OutOfSpace,
433            PrepareFailureReason::UnsupportedDowngrade => {
434                fidl::PrepareFailureReason::UnsupportedDowngrade
435            }
436        }
437    }
438}
439
440impl StageFailureReason {
441    fn write_to_inspect(&self, node: &inspect::Node) {
442        node.record_string("reason", format!("{self:?}"))
443    }
444}
445
446impl From<fidl::StageFailureReason> for StageFailureReason {
447    fn from(reason: fidl::StageFailureReason) -> Self {
448        match reason {
449            fidl::StageFailureReason::Internal => StageFailureReason::Internal,
450            fidl::StageFailureReason::OutOfSpace => StageFailureReason::OutOfSpace,
451        }
452    }
453}
454
455impl From<StageFailureReason> for fidl::StageFailureReason {
456    fn from(reason: StageFailureReason) -> Self {
457        match reason {
458            StageFailureReason::Internal => fidl::StageFailureReason::Internal,
459            StageFailureReason::OutOfSpace => fidl::StageFailureReason::OutOfSpace,
460        }
461    }
462}
463
464impl FetchFailureReason {
465    fn write_to_inspect(&self, node: &inspect::Node) {
466        node.record_string("reason", format!("{self:?}"))
467    }
468}
469
470impl From<fidl::FetchFailureReason> for FetchFailureReason {
471    fn from(reason: fidl::FetchFailureReason) -> Self {
472        match reason {
473            fidl::FetchFailureReason::Internal => FetchFailureReason::Internal,
474            fidl::FetchFailureReason::OutOfSpace => FetchFailureReason::OutOfSpace,
475        }
476    }
477}
478
479impl From<FetchFailureReason> for fidl::FetchFailureReason {
480    fn from(reason: FetchFailureReason) -> Self {
481        match reason {
482            FetchFailureReason::Internal => fidl::FetchFailureReason::Internal,
483            FetchFailureReason::OutOfSpace => fidl::FetchFailureReason::OutOfSpace,
484        }
485    }
486}
487
488impl<'de> Deserialize<'de> for UpdateInfoAndProgress {
489    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
490    where
491        D: serde::Deserializer<'de>,
492    {
493        use serde::de::Error;
494
495        #[derive(Debug, Deserialize)]
496        pub struct DeUpdateInfoAndProgress {
497            info: UpdateInfo,
498            progress: Progress,
499        }
500
501        let info_progress = DeUpdateInfoAndProgress::deserialize(deserializer)?;
502
503        UpdateInfoAndProgress::new(info_progress.info, info_progress.progress)
504            .map_err(|e| D::Error::custom(e.to_string()))
505    }
506}
507
508impl<'de> Deserialize<'de> for Progress {
509    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
510    where
511        D: serde::Deserializer<'de>,
512    {
513        #[derive(Debug, Deserialize)]
514        pub struct DeProgress {
515            fraction_completed: f32,
516            bytes_downloaded: u64,
517        }
518
519        let progress = DeProgress::deserialize(deserializer)?;
520
521        Ok(Progress::builder()
522            .fraction_completed(progress.fraction_completed)
523            .bytes_downloaded(progress.bytes_downloaded)
524            .build())
525    }
526}
527
528impl Serialize for FailStageData {
529    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
530    where
531        S: serde::Serializer,
532    {
533        use serde::ser::SerializeStruct;
534
535        let mut state = serializer.serialize_struct("FailStageData", 3)?;
536        state.serialize_field("info", &self.info_and_progress.info)?;
537        state.serialize_field("progress", &self.info_and_progress.progress)?;
538        state.serialize_field("reason", &self.reason)?;
539        state.end()
540    }
541}
542
543impl<'de> Deserialize<'de> for FailStageData {
544    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
545    where
546        D: serde::Deserializer<'de>,
547    {
548        use serde::de::Error;
549
550        #[derive(Debug, Deserialize)]
551        pub struct DeFailStageData {
552            info: UpdateInfo,
553            progress: Progress,
554            reason: StageFailureReason,
555        }
556
557        let DeFailStageData { info, progress, reason } =
558            DeFailStageData::deserialize(deserializer)?;
559
560        UpdateInfoAndProgress::new(info, progress)
561            .map_err(|e| D::Error::custom(e.to_string()))
562            .map(|info_and_progress| info_and_progress.with_stage_reason(reason))
563    }
564}
565
566impl Serialize for FailFetchData {
567    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
568    where
569        S: serde::Serializer,
570    {
571        use serde::ser::SerializeStruct;
572
573        let mut state = serializer.serialize_struct("FailFetchData", 3)?;
574        state.serialize_field("info", &self.info_and_progress.info)?;
575        state.serialize_field("progress", &self.info_and_progress.progress)?;
576        state.serialize_field("reason", &self.reason)?;
577        state.end()
578    }
579}
580
581impl<'de> Deserialize<'de> for FailFetchData {
582    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
583    where
584        D: serde::Deserializer<'de>,
585    {
586        use serde::de::Error;
587
588        #[derive(Debug, Deserialize)]
589        pub struct DeFailFetchData {
590            info: UpdateInfo,
591            progress: Progress,
592            reason: FetchFailureReason,
593        }
594
595        let DeFailFetchData { info, progress, reason } =
596            DeFailFetchData::deserialize(deserializer)?;
597
598        UpdateInfoAndProgress::new(info, progress)
599            .map_err(|e| D::Error::custom(e.to_string()))
600            .map(|info_and_progress| info_and_progress.with_fetch_reason(reason))
601    }
602}
603
604/// An error encountered while pairing an [`UpdateInfo`] and [`Progress`].
605#[derive(Debug, Error, PartialEq, Eq)]
606#[error("more bytes were fetched than should have been fetched")]
607pub struct BytesFetchedExceedsDownloadSize;
608
609/// An error encountered while decoding a [fidl_fuchsia_update_installer::State]
610/// into a [State].
611#[derive(Debug, Error, PartialEq, Eq)]
612#[allow(missing_docs)]
613pub enum DecodeStateError {
614    #[error("missing field {0:?}")]
615    MissingField(RequiredStateField),
616
617    #[error("state contained invalid 'info' field")]
618    DecodeUpdateInfo(#[source] DecodeUpdateInfoError),
619
620    #[error("state contained invalid 'progress' field")]
621    DecodeProgress(#[source] DecodeProgressError),
622
623    #[error("the provided update info and progress are inconsistent with each other")]
624    InconsistentUpdateInfoAndProgress(#[source] BytesFetchedExceedsDownloadSize),
625}
626
627/// Required fields in a [fidl_fuchsia_update_installer::State].
628#[derive(Debug, PartialEq, Eq)]
629#[allow(missing_docs)]
630pub enum RequiredStateField {
631    Info,
632    Progress,
633    Reason,
634}
635
636impl From<State> for fidl::State {
637    fn from(state: State) -> Self {
638        match state {
639            State::Prepare => fidl::State::Prepare(fidl::PrepareData::default()),
640            State::Stage(UpdateInfoAndProgress { info, progress }) => {
641                fidl::State::Stage(fidl::StageData {
642                    info: Some(info.into()),
643                    progress: Some(progress.into()),
644                    ..Default::default()
645                })
646            }
647            State::Fetch(UpdateInfoAndProgress { info, progress }) => {
648                fidl::State::Fetch(fidl::FetchData {
649                    info: Some(info.into()),
650                    progress: Some(progress.into()),
651                    ..Default::default()
652                })
653            }
654            State::Commit(UpdateInfoAndProgress { info, progress }) => {
655                fidl::State::Commit(fidl::CommitData {
656                    info: Some(info.into()),
657                    progress: Some(progress.into()),
658                    ..Default::default()
659                })
660            }
661            State::WaitToReboot(UpdateInfoAndProgress { info, progress }) => {
662                fidl::State::WaitToReboot(fidl::WaitToRebootData {
663                    info: Some(info.into()),
664                    progress: Some(progress.into()),
665                    ..Default::default()
666                })
667            }
668            State::Reboot(UpdateInfoAndProgress { info, progress }) => {
669                fidl::State::Reboot(fidl::RebootData {
670                    info: Some(info.into()),
671                    progress: Some(progress.into()),
672                    ..Default::default()
673                })
674            }
675            State::DeferReboot(UpdateInfoAndProgress { info, progress }) => {
676                fidl::State::DeferReboot(fidl::DeferRebootData {
677                    info: Some(info.into()),
678                    progress: Some(progress.into()),
679                    ..Default::default()
680                })
681            }
682            State::Complete(UpdateInfoAndProgress { info, progress }) => {
683                fidl::State::Complete(fidl::CompleteData {
684                    info: Some(info.into()),
685                    progress: Some(progress.into()),
686                    ..Default::default()
687                })
688            }
689            State::FailPrepare(reason) => fidl::State::FailPrepare(fidl::FailPrepareData {
690                reason: Some(reason.into()),
691                ..Default::default()
692            }),
693            State::FailStage(FailStageData { info_and_progress, reason }) => {
694                fidl::State::FailStage(fidl::FailStageData {
695                    info: Some(info_and_progress.info.into()),
696                    progress: Some(info_and_progress.progress.into()),
697                    reason: Some(reason.into()),
698                    ..Default::default()
699                })
700            }
701            State::FailFetch(FailFetchData { info_and_progress, reason }) => {
702                fidl::State::FailFetch(fidl::FailFetchData {
703                    info: Some(info_and_progress.info.into()),
704                    progress: Some(info_and_progress.progress.into()),
705                    reason: Some(reason.into()),
706                    ..Default::default()
707                })
708            }
709            State::FailCommit(UpdateInfoAndProgress { info, progress }) => {
710                fidl::State::FailCommit(fidl::FailCommitData {
711                    info: Some(info.into()),
712                    progress: Some(progress.into()),
713                    ..Default::default()
714                })
715            }
716            State::Canceled => fidl::State::Canceled(fidl::CanceledData::default()),
717        }
718    }
719}
720
721impl TryFrom<fidl::State> for State {
722    type Error = DecodeStateError;
723
724    fn try_from(state: fidl::State) -> Result<Self, Self::Error> {
725        fn decode_info_progress(
726            info: Option<fidl::UpdateInfo>,
727            progress: Option<fidl::InstallationProgress>,
728        ) -> Result<UpdateInfoAndProgress, DecodeStateError> {
729            let info: UpdateInfo = info
730                .ok_or(DecodeStateError::MissingField(RequiredStateField::Info))?
731                .try_into()
732                .map_err(DecodeStateError::DecodeUpdateInfo)?;
733            let progress: Progress = progress
734                .ok_or(DecodeStateError::MissingField(RequiredStateField::Progress))?
735                .try_into()
736                .map_err(DecodeStateError::DecodeProgress)?;
737
738            UpdateInfoAndProgress::new(info, progress)
739                .map_err(DecodeStateError::InconsistentUpdateInfoAndProgress)
740        }
741
742        Ok(match state {
743            fidl::State::Prepare(fidl::PrepareData { .. }) => State::Prepare,
744            fidl::State::Stage(fidl::StageData { info, progress, .. }) => {
745                State::Stage(decode_info_progress(info, progress)?)
746            }
747            fidl::State::Fetch(fidl::FetchData { info, progress, .. }) => {
748                State::Fetch(decode_info_progress(info, progress)?)
749            }
750            fidl::State::Commit(fidl::CommitData { info, progress, .. }) => {
751                State::Commit(decode_info_progress(info, progress)?)
752            }
753            fidl::State::WaitToReboot(fidl::WaitToRebootData { info, progress, .. }) => {
754                State::WaitToReboot(decode_info_progress(info, progress)?)
755            }
756            fidl::State::Reboot(fidl::RebootData { info, progress, .. }) => {
757                State::Reboot(decode_info_progress(info, progress)?)
758            }
759            fidl::State::DeferReboot(fidl::DeferRebootData { info, progress, .. }) => {
760                State::DeferReboot(decode_info_progress(info, progress)?)
761            }
762            fidl::State::Complete(fidl::CompleteData { info, progress, .. }) => {
763                State::Complete(decode_info_progress(info, progress)?)
764            }
765            fidl::State::FailPrepare(fidl::FailPrepareData { reason, .. }) => State::FailPrepare(
766                reason.ok_or(DecodeStateError::MissingField(RequiredStateField::Reason))?.into(),
767            ),
768            fidl::State::FailStage(fidl::FailStageData { info, progress, reason, .. }) => {
769                State::FailStage(
770                    decode_info_progress(info, progress)?.with_stage_reason(
771                        reason
772                            .ok_or(DecodeStateError::MissingField(RequiredStateField::Reason))?
773                            .into(),
774                    ),
775                )
776            }
777            fidl::State::FailFetch(fidl::FailFetchData { info, progress, reason, .. }) => {
778                State::FailFetch(
779                    decode_info_progress(info, progress)?.with_fetch_reason(
780                        reason
781                            .ok_or(DecodeStateError::MissingField(RequiredStateField::Reason))?
782                            .into(),
783                    ),
784                )
785            }
786            fidl::State::FailCommit(fidl::FailCommitData { info, progress, .. }) => {
787                State::FailCommit(decode_info_progress(info, progress)?)
788            }
789            fidl::State::Canceled(fidl::CanceledData { .. }) => State::Canceled,
790        })
791    }
792}
793
794// TODO remove ambiguous mapping of 0 to/from None when the system-updater actually computes a
795// download size and emits bytes_downloaded information.
796fn none_or_some_nonzero(n: u64) -> Option<u64> {
797    if n == 0 {
798        None
799    } else {
800        Some(n)
801    }
802}
803
804/// An error encountered while decoding a [fidl_fuchsia_update_installer::UpdateInfo] into a
805/// [UpdateInfo].
806#[derive(Debug, Error, PartialEq, Eq)]
807#[allow(missing_docs)]
808pub enum DecodeUpdateInfoError {}
809
810impl From<UpdateInfo> for fidl::UpdateInfo {
811    fn from(info: UpdateInfo) -> Self {
812        fidl::UpdateInfo {
813            download_size: none_or_some_nonzero(info.download_size),
814            ..Default::default()
815        }
816    }
817}
818
819impl TryFrom<fidl::UpdateInfo> for UpdateInfo {
820    type Error = DecodeUpdateInfoError;
821
822    fn try_from(info: fidl::UpdateInfo) -> Result<Self, Self::Error> {
823        Ok(UpdateInfo { download_size: info.download_size.unwrap_or(0) })
824    }
825}
826
827/// An error encountered while decoding a [fidl_fuchsia_update_installer::InstallationProgress]
828/// into a [Progress].
829#[derive(Debug, Error, PartialEq, Eq)]
830#[allow(missing_docs)]
831pub enum DecodeProgressError {
832    #[error("missing field {0:?}")]
833    MissingField(RequiredProgressField),
834
835    #[error("fraction completed not in range [0.0, 1.0]")]
836    FractionCompletedOutOfRange,
837}
838
839/// Required fields in a [fidl_fuchsia_update_installer::InstallationProgress].
840#[derive(Debug, PartialEq, Eq)]
841#[allow(missing_docs)]
842pub enum RequiredProgressField {
843    FractionCompleted,
844}
845
846impl From<Progress> for fidl::InstallationProgress {
847    fn from(progress: Progress) -> Self {
848        fidl::InstallationProgress {
849            fraction_completed: Some(progress.fraction_completed),
850            bytes_downloaded: none_or_some_nonzero(progress.bytes_downloaded),
851            ..Default::default()
852        }
853    }
854}
855
856impl TryFrom<fidl::InstallationProgress> for Progress {
857    type Error = DecodeProgressError;
858
859    fn try_from(progress: fidl::InstallationProgress) -> Result<Self, Self::Error> {
860        Ok(Progress {
861            fraction_completed: {
862                let n = progress.fraction_completed.ok_or(DecodeProgressError::MissingField(
863                    RequiredProgressField::FractionCompleted,
864                ))?;
865                if !(0.0..=1.0).contains(&n) {
866                    return Err(DecodeProgressError::FractionCompletedOutOfRange);
867                }
868                n
869            },
870            bytes_downloaded: progress.bytes_downloaded.unwrap_or(0),
871        })
872    }
873}
874
875impl Arbitrary for UpdateInfoAndProgress {
876    type Parameters = ();
877    type Strategy = BoxedStrategy<Self>;
878
879    fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
880        arb_info_and_progress().prop_map(|(info, progress)| Self { info, progress }).boxed()
881    }
882}
883
884impl Arbitrary for FailStageData {
885    type Parameters = ();
886    type Strategy = BoxedStrategy<Self>;
887
888    fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
889        arb_info_and_progress()
890            .prop_flat_map(|(info, progress)| {
891                any::<StageFailureReason>().prop_map(move |reason| {
892                    UpdateInfoAndProgress { info, progress }.with_stage_reason(reason)
893                })
894            })
895            .boxed()
896    }
897}
898
899impl Arbitrary for FailFetchData {
900    type Parameters = ();
901    type Strategy = BoxedStrategy<Self>;
902
903    fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
904        arb_info_and_progress()
905            .prop_flat_map(|(info, progress)| {
906                any::<FetchFailureReason>().prop_map(move |reason| {
907                    UpdateInfoAndProgress { info, progress }.with_fetch_reason(reason)
908                })
909            })
910            .boxed()
911    }
912}
913
914/// Returns a strategy generating and UpdateInfo and Progress such that the Progress does not
915/// exceed the bounds of the UpdateInfo.
916fn arb_info_and_progress() -> impl Strategy<Value = (UpdateInfo, Progress)> {
917    prop_compose! {
918        fn arb_progress_for_info(
919            info: UpdateInfo
920        )(
921            fraction_completed: f32,
922            bytes_downloaded in 0..=info.download_size
923        ) -> Progress {
924            Progress::builder()
925                .fraction_completed(fraction_completed)
926                .bytes_downloaded(bytes_downloaded)
927                .build()
928        }
929    }
930
931    any::<UpdateInfo>().prop_flat_map(|info| (Just(info), arb_progress_for_info(info)))
932}
933
934#[cfg(test)]
935mod tests {
936    use super::*;
937    use assert_matches::assert_matches;
938    use diagnostics_assertions::assert_data_tree;
939    use fuchsia_inspect::Inspector;
940    use serde_json::json;
941
942    prop_compose! {
943        fn arb_progress()(fraction_completed: f32, bytes_downloaded: u64) -> Progress {
944            Progress::builder()
945                .fraction_completed(fraction_completed)
946                .bytes_downloaded(bytes_downloaded)
947                .build()
948        }
949    }
950
951    /// Returns a strategy generating (a, b) such that a < b.
952    fn a_lt_b() -> impl Strategy<Value = (u64, u64)> {
953        (0..u64::MAX).prop_flat_map(|a| (Just(a), a + 1..))
954    }
955
956    proptest! {
957        #[test]
958        fn progress_builder_clamps_fraction_completed(progress in arb_progress()) {
959            prop_assert!(progress.fraction_completed() >= 0.0);
960            prop_assert!(progress.fraction_completed() <= 1.0);
961        }
962
963        #[test]
964        fn progress_builder_roundtrips(progress: Progress) {
965            prop_assert_eq!(
966                Progress::builder()
967                    .fraction_completed(progress.fraction_completed())
968                    .bytes_downloaded(progress.bytes_downloaded())
969                    .build(),
970                progress
971            );
972        }
973
974        #[test]
975        fn update_info_builder_roundtrips(info: UpdateInfo) {
976            prop_assert_eq!(
977                UpdateInfo::builder()
978                    .download_size(info.download_size())
979                    .build(),
980                info
981            );
982        }
983
984        #[test]
985        fn update_info_and_progress_builder_roundtrips(info_progress: UpdateInfoAndProgress) {
986            prop_assert_eq!(
987                UpdateInfoAndProgress::builder()
988                    .info(info_progress.info)
989                    .progress(info_progress.progress)
990                    .build(),
991                info_progress
992            );
993        }
994
995        #[test]
996        fn update_info_roundtrips_through_fidl(info: UpdateInfo) {
997            let as_fidl: fidl::UpdateInfo = info.into();
998            prop_assert_eq!(as_fidl.try_into(), Ok(info));
999        }
1000
1001        #[test]
1002        fn progress_roundtrips_through_fidl(progress: Progress) {
1003            let as_fidl: fidl::InstallationProgress = progress.into();
1004            prop_assert_eq!(as_fidl.try_into(), Ok(progress));
1005        }
1006
1007        #[test]
1008        fn update_info_and_progress_builder_produces_valid_instances(
1009            info: UpdateInfo,
1010            progress: Progress
1011        ) {
1012            let info_progress = UpdateInfoAndProgress::builder()
1013                .info(info)
1014                .progress(progress)
1015                .build();
1016
1017            prop_assert_eq!(
1018                UpdateInfoAndProgress::new(info_progress.info, info_progress.progress),
1019                Ok(info_progress)
1020            );
1021        }
1022
1023        #[test]
1024        fn update_info_and_progress_new_rejects_too_many_bytes(
1025            (a, b) in a_lt_b(),
1026            mut info: UpdateInfo,
1027            mut progress: Progress
1028        ) {
1029            info.download_size = a;
1030            progress.bytes_downloaded = b;
1031
1032            prop_assert_eq!(
1033                UpdateInfoAndProgress::new(info, progress),
1034                Err(BytesFetchedExceedsDownloadSize)
1035            );
1036        }
1037
1038        #[test]
1039        fn state_roundtrips_through_fidl(state: State) {
1040            let as_fidl: fidl::State = state.clone().into();
1041            prop_assert_eq!(as_fidl.try_into(), Ok(state));
1042        }
1043
1044        #[test]
1045        fn state_roundtrips_through_json(state: State) {
1046            let as_json = serde_json::to_value(&state).unwrap();
1047            let state2 = serde_json::from_value(as_json).unwrap();
1048            prop_assert_eq!(state, state2);
1049        }
1050
1051
1052        // Test that:
1053        // * write_to_inspect doesn't panic on arbitrary inputs
1054        // * we create a string property called 'state' in all cases
1055        #[test]
1056        fn state_populates_inspect_with_id(state: State) {
1057            let inspector = Inspector::default();
1058            state.write_to_inspect(inspector.root());
1059
1060            assert_data_tree! {
1061                inspector,
1062                root: contains {
1063                    "state": state.name(),
1064                }
1065            };
1066        }
1067
1068        #[test]
1069        fn progress_rejects_invalid_fraction_completed(progress: Progress, fraction_completed: f32) {
1070            let fraction_valid = (0.0..=1.0).contains(&fraction_completed);
1071            prop_assume!(!fraction_valid);
1072            // Note, the above doesn't look simplified, but not all the usual math rules apply to
1073            // types that are PartialOrd and not Ord:
1074            //use std::f32::NAN;
1075            //assert!(!(NAN >= 0.0 && NAN <= 1.0)); // This assertion passes.
1076            //assert!(NAN < 0.0 || NAN > 1.0); // This assertion fails.
1077
1078            let mut as_fidl: fidl::InstallationProgress = progress.into();
1079            as_fidl.fraction_completed = Some(fraction_completed);
1080            prop_assert_eq!(Progress::try_from(as_fidl), Err(DecodeProgressError::FractionCompletedOutOfRange));
1081        }
1082
1083        #[test]
1084        fn state_rejects_too_many_bytes_fetched(state: State, (a, b) in a_lt_b()) {
1085            let mut as_fidl: fidl::State = state.into();
1086
1087            let break_info_progress = |info: &mut Option<fidl::UpdateInfo>, progress: &mut Option<fidl::InstallationProgress>| {
1088                info.as_mut().unwrap().download_size = Some(a);
1089                progress.as_mut().unwrap().bytes_downloaded = Some(b);
1090            };
1091
1092            match &mut as_fidl {
1093                fidl::State::Prepare(fidl::PrepareData { .. }) => prop_assume!(false),
1094                fidl::State::Stage(fidl::StageData { info, progress, .. }) => break_info_progress(info, progress),
1095                fidl::State::Fetch(fidl::FetchData { info, progress, .. }) => break_info_progress(info, progress),
1096                fidl::State::Commit(fidl::CommitData { info, progress, .. }) => break_info_progress(info, progress),
1097                fidl::State::WaitToReboot(fidl::WaitToRebootData { info, progress, .. }) => break_info_progress(info, progress),
1098                fidl::State::Reboot(fidl::RebootData { info, progress, .. }) => break_info_progress(info, progress),
1099                fidl::State::DeferReboot(fidl::DeferRebootData { info, progress, .. }) => break_info_progress(info, progress),
1100                fidl::State::Complete(fidl::CompleteData { info, progress, .. }) => break_info_progress(info, progress),
1101                fidl::State::FailPrepare(fidl::FailPrepareData { .. }) => prop_assume!(false),
1102                fidl::State::FailStage(fidl::FailStageData { info, progress, .. }) => break_info_progress(info, progress),
1103                fidl::State::FailFetch(fidl::FailFetchData { info, progress, .. }) => break_info_progress(info, progress),
1104                fidl::State::FailCommit(fidl::FailCommitData { info, progress, .. }) => break_info_progress(info, progress),
1105                fidl::State::Canceled(fidl::CanceledData { .. }) => prop_assume!(false),
1106            }
1107            prop_assert_eq!(
1108                State::try_from(as_fidl),
1109                Err(DecodeStateError::InconsistentUpdateInfoAndProgress(BytesFetchedExceedsDownloadSize))
1110            );
1111        }
1112
1113        // States can merge with identical states.
1114        #[test]
1115        fn state_can_merge_reflexive(state: State) {
1116            prop_assert!(state.can_merge(&state));
1117        }
1118
1119        // States with the same ids can merge, even if the data is different.
1120        #[test]
1121        fn states_with_same_ids_can_merge(
1122            state: State,
1123            different_data: UpdateInfoAndProgress,
1124            different_prepare_reason: PrepareFailureReason,
1125            different_fetch_reason: FetchFailureReason,
1126            different_stage_reason: StageFailureReason,
1127        ) {
1128            let state_with_different_data = match state {
1129                State::Prepare => State::Prepare,
1130                State::Stage(_) => State::Stage(different_data),
1131                State::Fetch(_) => State::Fetch(different_data),
1132                State::Commit(_) => State::Commit(different_data),
1133                State::WaitToReboot(_) => State::WaitToReboot(different_data),
1134                State::Reboot(_) => State::Reboot(different_data),
1135                State::DeferReboot(_) => State::DeferReboot(different_data),
1136                State::Complete(_) => State::Complete(different_data),
1137                // We currently allow merging states with different failure reasons, though
1138                // we don't expect that to ever happen in practice.
1139                State::FailPrepare(_) => State::FailPrepare(different_prepare_reason),
1140                State::FailStage(_) => State::FailStage(different_data.with_stage_reason(different_stage_reason)),
1141                State::FailFetch(_) => State::FailFetch(different_data.with_fetch_reason(different_fetch_reason)),
1142                State::FailCommit(_) => State::FailCommit(different_data),
1143                State::Canceled => State::Canceled,
1144            };
1145            prop_assert!(state.can_merge(&state_with_different_data));
1146        }
1147
1148        #[test]
1149        fn states_with_different_ids_cannot_merge(state0: State, state1: State) {
1150            prop_assume!(state0.id() != state1.id());
1151            prop_assert!(!state0.can_merge(&state1));
1152        }
1153
1154    }
1155
1156    #[test]
1157    fn populates_inspect_fail_stage() {
1158        let state = State::FailStage(
1159            UpdateInfoAndProgress {
1160                info: UpdateInfo { download_size: 4096 },
1161                progress: Progress { bytes_downloaded: 2048, fraction_completed: 0.5 },
1162            }
1163            .with_stage_reason(StageFailureReason::Internal),
1164        );
1165        let inspector = Inspector::default();
1166        state.write_to_inspect(inspector.root());
1167        assert_data_tree! {
1168            inspector,
1169            root: {
1170                "state": "fail_stage",
1171                "info": {
1172                    "download_size": 4096u64,
1173                },
1174                "progress": {
1175                    "bytes_downloaded": 2048u64,
1176                    "fraction_completed": 0.5f64,
1177                },
1178                "reason": "Internal",
1179            }
1180        }
1181    }
1182
1183    #[test]
1184    fn populates_inspect_fail_fetch() {
1185        let state = State::FailFetch(
1186            UpdateInfoAndProgress {
1187                info: UpdateInfo { download_size: 4096 },
1188                progress: Progress { bytes_downloaded: 2048, fraction_completed: 0.5 },
1189            }
1190            .with_fetch_reason(FetchFailureReason::Internal),
1191        );
1192        let inspector = Inspector::default();
1193        state.write_to_inspect(inspector.root());
1194        assert_data_tree! {
1195            inspector,
1196            root: {
1197                "state": "fail_fetch",
1198                "info": {
1199                    "download_size": 4096u64,
1200                },
1201                "progress": {
1202                    "bytes_downloaded": 2048u64,
1203                    "fraction_completed": 0.5f64,
1204                },
1205                "reason": "Internal",
1206            }
1207        }
1208    }
1209
1210    #[test]
1211    fn populates_inspect_fail_prepare() {
1212        let state = State::FailPrepare(PrepareFailureReason::OutOfSpace);
1213        let inspector = Inspector::default();
1214        state.write_to_inspect(inspector.root());
1215        assert_data_tree! {
1216            inspector,
1217            root: {
1218                "state": "fail_prepare",
1219                "reason": "OutOfSpace",
1220            }
1221        }
1222    }
1223
1224    #[test]
1225    fn populates_inspect_reboot() {
1226        let state = State::Reboot(UpdateInfoAndProgress {
1227            info: UpdateInfo { download_size: 4096 },
1228            progress: Progress { bytes_downloaded: 2048, fraction_completed: 0.5 },
1229        });
1230        let inspector = Inspector::default();
1231        state.write_to_inspect(inspector.root());
1232        assert_data_tree! {
1233            inspector,
1234            root: {
1235                "state": "reboot",
1236                "info": {
1237                    "download_size": 4096u64,
1238                },
1239                "progress": {
1240                    "bytes_downloaded": 2048u64,
1241                    "fraction_completed": 0.5f64,
1242                }
1243            }
1244        }
1245    }
1246
1247    #[test]
1248    fn progress_fraction_completed_required() {
1249        assert_eq!(
1250            Progress::try_from(fidl::InstallationProgress::default()),
1251            Err(DecodeProgressError::MissingField(RequiredProgressField::FractionCompleted)),
1252        );
1253    }
1254
1255    #[test]
1256    fn json_deserializes_state_reboot() {
1257        assert_eq!(
1258            serde_json::from_value::<State>(json!({
1259                "id": "reboot",
1260                "info": {
1261                    "download_size": 100,
1262                },
1263                "progress": {
1264                    "bytes_downloaded": 100,
1265                    "fraction_completed": 1.0,
1266                },
1267            }))
1268            .unwrap(),
1269            State::Reboot(UpdateInfoAndProgress {
1270                info: UpdateInfo { download_size: 100 },
1271                progress: Progress { bytes_downloaded: 100, fraction_completed: 1.0 },
1272            })
1273        );
1274    }
1275
1276    #[test]
1277    fn json_deserializes_state_fail_prepare() {
1278        assert_eq!(
1279            serde_json::from_value::<State>(json!({
1280                "id": "fail_prepare",
1281                "reason": "internal",
1282            }))
1283            .unwrap(),
1284            State::FailPrepare(PrepareFailureReason::Internal)
1285        );
1286    }
1287
1288    #[test]
1289    fn json_deserializes_state_fail_stage() {
1290        assert_eq!(
1291            serde_json::from_value::<State>(json!({
1292                "id": "fail_stage",
1293                "info": {
1294                    "download_size": 100,
1295                },
1296                "progress": {
1297                    "bytes_downloaded": 100,
1298                    "fraction_completed": 1.0,
1299                },
1300                "reason": "out_of_space",
1301            }))
1302            .unwrap(),
1303            State::FailStage(
1304                UpdateInfoAndProgress {
1305                    info: UpdateInfo { download_size: 100 },
1306                    progress: Progress { bytes_downloaded: 100, fraction_completed: 1.0 },
1307                }
1308                .with_stage_reason(StageFailureReason::OutOfSpace)
1309            )
1310        );
1311    }
1312
1313    #[test]
1314    fn json_deserializes_state_fail_fetch() {
1315        assert_eq!(
1316            serde_json::from_value::<State>(json!({
1317                "id": "fail_fetch",
1318                "info": {
1319                    "download_size": 100,
1320                },
1321                "progress": {
1322                    "bytes_downloaded": 100,
1323                    "fraction_completed": 1.0,
1324                },
1325                "reason": "out_of_space",
1326            }))
1327            .unwrap(),
1328            State::FailFetch(
1329                UpdateInfoAndProgress {
1330                    info: UpdateInfo { download_size: 100 },
1331                    progress: Progress { bytes_downloaded: 100, fraction_completed: 1.0 },
1332                }
1333                .with_fetch_reason(FetchFailureReason::OutOfSpace)
1334            )
1335        );
1336    }
1337
1338    #[test]
1339    fn json_deserialize_detects_inconsistent_info_and_progress() {
1340        let too_much_download = json!({
1341            "id": "reboot",
1342            "info": {
1343                "download_size": 100,
1344            },
1345            "progress": {
1346                "bytes_downloaded": 101,
1347                "fraction_completed": 1.0,
1348            },
1349        });
1350
1351        assert_matches!(serde_json::from_value::<State>(too_much_download), Err(_));
1352    }
1353
1354    #[test]
1355    fn json_deserialize_clamps_invalid_fraction_completed() {
1356        let too_much_progress = json!({
1357            "bytes_downloaded": 0,
1358            "fraction_completed": 1.1,
1359        });
1360        assert_eq!(
1361            serde_json::from_value::<Progress>(too_much_progress).unwrap(),
1362            Progress { bytes_downloaded: 0, fraction_completed: 1.0 }
1363        );
1364
1365        let negative_progress = json!({
1366            "bytes_downloaded": 0,
1367            "fraction_completed": -0.5,
1368        });
1369        assert_eq!(
1370            serde_json::from_value::<Progress>(negative_progress).unwrap(),
1371            Progress { bytes_downloaded: 0, fraction_completed: 0.0 }
1372        );
1373    }
1374
1375    #[test]
1376    fn update_info_and_progress_builder_clamps_bytes_downloaded_to_download_size() {
1377        assert_eq!(
1378            UpdateInfoAndProgress::builder()
1379                .info(UpdateInfo { download_size: 100 })
1380                .progress(Progress { bytes_downloaded: 200, fraction_completed: 1.0 })
1381                .build(),
1382            UpdateInfoAndProgress {
1383                info: UpdateInfo { download_size: 100 },
1384                progress: Progress { bytes_downloaded: 100, fraction_completed: 1.0 },
1385            }
1386        );
1387    }
1388}