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 =
730                info.ok_or(DecodeStateError::MissingField(RequiredStateField::Info))?.into();
731            let progress: Progress = progress
732                .ok_or(DecodeStateError::MissingField(RequiredStateField::Progress))?
733                .try_into()
734                .map_err(DecodeStateError::DecodeProgress)?;
735
736            UpdateInfoAndProgress::new(info, progress)
737                .map_err(DecodeStateError::InconsistentUpdateInfoAndProgress)
738        }
739
740        Ok(match state {
741            fidl::State::Prepare(fidl::PrepareData { .. }) => State::Prepare,
742            fidl::State::Stage(fidl::StageData { info, progress, .. }) => {
743                State::Stage(decode_info_progress(info, progress)?)
744            }
745            fidl::State::Fetch(fidl::FetchData { info, progress, .. }) => {
746                State::Fetch(decode_info_progress(info, progress)?)
747            }
748            fidl::State::Commit(fidl::CommitData { info, progress, .. }) => {
749                State::Commit(decode_info_progress(info, progress)?)
750            }
751            fidl::State::WaitToReboot(fidl::WaitToRebootData { info, progress, .. }) => {
752                State::WaitToReboot(decode_info_progress(info, progress)?)
753            }
754            fidl::State::Reboot(fidl::RebootData { info, progress, .. }) => {
755                State::Reboot(decode_info_progress(info, progress)?)
756            }
757            fidl::State::DeferReboot(fidl::DeferRebootData { info, progress, .. }) => {
758                State::DeferReboot(decode_info_progress(info, progress)?)
759            }
760            fidl::State::Complete(fidl::CompleteData { info, progress, .. }) => {
761                State::Complete(decode_info_progress(info, progress)?)
762            }
763            fidl::State::FailPrepare(fidl::FailPrepareData { reason, .. }) => State::FailPrepare(
764                reason.ok_or(DecodeStateError::MissingField(RequiredStateField::Reason))?.into(),
765            ),
766            fidl::State::FailStage(fidl::FailStageData { info, progress, reason, .. }) => {
767                State::FailStage(
768                    decode_info_progress(info, progress)?.with_stage_reason(
769                        reason
770                            .ok_or(DecodeStateError::MissingField(RequiredStateField::Reason))?
771                            .into(),
772                    ),
773                )
774            }
775            fidl::State::FailFetch(fidl::FailFetchData { info, progress, reason, .. }) => {
776                State::FailFetch(
777                    decode_info_progress(info, progress)?.with_fetch_reason(
778                        reason
779                            .ok_or(DecodeStateError::MissingField(RequiredStateField::Reason))?
780                            .into(),
781                    ),
782                )
783            }
784            fidl::State::FailCommit(fidl::FailCommitData { info, progress, .. }) => {
785                State::FailCommit(decode_info_progress(info, progress)?)
786            }
787            fidl::State::Canceled(fidl::CanceledData { .. }) => State::Canceled,
788        })
789    }
790}
791
792// TODO remove ambiguous mapping of 0 to/from None when the system-updater actually computes a
793// download size and emits bytes_downloaded information.
794fn none_or_some_nonzero(n: u64) -> Option<u64> {
795    if n == 0 {
796        None
797    } else {
798        Some(n)
799    }
800}
801
802/// An error encountered while decoding a [fidl_fuchsia_update_installer::UpdateInfo] into a
803/// [UpdateInfo].
804#[derive(Debug, Error, PartialEq, Eq)]
805#[allow(missing_docs)]
806pub enum DecodeUpdateInfoError {}
807
808impl From<UpdateInfo> for fidl::UpdateInfo {
809    fn from(info: UpdateInfo) -> Self {
810        fidl::UpdateInfo {
811            download_size: none_or_some_nonzero(info.download_size),
812            ..Default::default()
813        }
814    }
815}
816
817impl From<fidl::UpdateInfo> for UpdateInfo {
818    fn from(info: fidl::UpdateInfo) -> Self {
819        UpdateInfo { download_size: info.download_size.unwrap_or(0) }
820    }
821}
822
823/// An error encountered while decoding a [fidl_fuchsia_update_installer::InstallationProgress]
824/// into a [Progress].
825#[derive(Debug, Error, PartialEq, Eq)]
826#[allow(missing_docs)]
827pub enum DecodeProgressError {
828    #[error("missing field {0:?}")]
829    MissingField(RequiredProgressField),
830
831    #[error("fraction completed not in range [0.0, 1.0]")]
832    FractionCompletedOutOfRange,
833}
834
835/// Required fields in a [fidl_fuchsia_update_installer::InstallationProgress].
836#[derive(Debug, PartialEq, Eq)]
837#[allow(missing_docs)]
838pub enum RequiredProgressField {
839    FractionCompleted,
840}
841
842impl From<Progress> for fidl::InstallationProgress {
843    fn from(progress: Progress) -> Self {
844        fidl::InstallationProgress {
845            fraction_completed: Some(progress.fraction_completed),
846            bytes_downloaded: none_or_some_nonzero(progress.bytes_downloaded),
847            ..Default::default()
848        }
849    }
850}
851
852impl TryFrom<fidl::InstallationProgress> for Progress {
853    type Error = DecodeProgressError;
854
855    fn try_from(progress: fidl::InstallationProgress) -> Result<Self, Self::Error> {
856        Ok(Progress {
857            fraction_completed: {
858                let n = progress.fraction_completed.ok_or(DecodeProgressError::MissingField(
859                    RequiredProgressField::FractionCompleted,
860                ))?;
861                if !(0.0..=1.0).contains(&n) {
862                    return Err(DecodeProgressError::FractionCompletedOutOfRange);
863                }
864                n
865            },
866            bytes_downloaded: progress.bytes_downloaded.unwrap_or(0),
867        })
868    }
869}
870
871impl Arbitrary for UpdateInfoAndProgress {
872    type Parameters = ();
873    type Strategy = BoxedStrategy<Self>;
874
875    fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
876        arb_info_and_progress().prop_map(|(info, progress)| Self { info, progress }).boxed()
877    }
878}
879
880impl Arbitrary for FailStageData {
881    type Parameters = ();
882    type Strategy = BoxedStrategy<Self>;
883
884    fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
885        arb_info_and_progress()
886            .prop_flat_map(|(info, progress)| {
887                any::<StageFailureReason>().prop_map(move |reason| {
888                    UpdateInfoAndProgress { info, progress }.with_stage_reason(reason)
889                })
890            })
891            .boxed()
892    }
893}
894
895impl Arbitrary for FailFetchData {
896    type Parameters = ();
897    type Strategy = BoxedStrategy<Self>;
898
899    fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
900        arb_info_and_progress()
901            .prop_flat_map(|(info, progress)| {
902                any::<FetchFailureReason>().prop_map(move |reason| {
903                    UpdateInfoAndProgress { info, progress }.with_fetch_reason(reason)
904                })
905            })
906            .boxed()
907    }
908}
909
910/// Returns a strategy generating and UpdateInfo and Progress such that the Progress does not
911/// exceed the bounds of the UpdateInfo.
912fn arb_info_and_progress() -> impl Strategy<Value = (UpdateInfo, Progress)> {
913    prop_compose! {
914        fn arb_progress_for_info(
915            info: UpdateInfo
916        )(
917            fraction_completed: f32,
918            bytes_downloaded in 0..=info.download_size
919        ) -> Progress {
920            Progress::builder()
921                .fraction_completed(fraction_completed)
922                .bytes_downloaded(bytes_downloaded)
923                .build()
924        }
925    }
926
927    any::<UpdateInfo>().prop_flat_map(|info| (Just(info), arb_progress_for_info(info)))
928}
929
930#[cfg(test)]
931mod tests {
932    use super::*;
933    use assert_matches::assert_matches;
934    use diagnostics_assertions::assert_data_tree;
935    use fuchsia_inspect::Inspector;
936    use serde_json::json;
937
938    prop_compose! {
939        fn arb_progress()(fraction_completed: f32, bytes_downloaded: u64) -> Progress {
940            Progress::builder()
941                .fraction_completed(fraction_completed)
942                .bytes_downloaded(bytes_downloaded)
943                .build()
944        }
945    }
946
947    /// Returns a strategy generating (a, b) such that a < b.
948    fn a_lt_b() -> impl Strategy<Value = (u64, u64)> {
949        (0..u64::MAX).prop_flat_map(|a| (Just(a), a + 1..))
950    }
951
952    proptest! {
953        #[test]
954        fn progress_builder_clamps_fraction_completed(progress in arb_progress()) {
955            prop_assert!(progress.fraction_completed() >= 0.0);
956            prop_assert!(progress.fraction_completed() <= 1.0);
957        }
958
959        #[test]
960        fn progress_builder_roundtrips(progress: Progress) {
961            prop_assert_eq!(
962                Progress::builder()
963                    .fraction_completed(progress.fraction_completed())
964                    .bytes_downloaded(progress.bytes_downloaded())
965                    .build(),
966                progress
967            );
968        }
969
970        #[test]
971        fn update_info_builder_roundtrips(info: UpdateInfo) {
972            prop_assert_eq!(
973                UpdateInfo::builder()
974                    .download_size(info.download_size())
975                    .build(),
976                info
977            );
978        }
979
980        #[test]
981        fn update_info_and_progress_builder_roundtrips(info_progress: UpdateInfoAndProgress) {
982            prop_assert_eq!(
983                UpdateInfoAndProgress::builder()
984                    .info(info_progress.info)
985                    .progress(info_progress.progress)
986                    .build(),
987                info_progress
988            );
989        }
990
991        #[test]
992        fn update_info_roundtrips_through_fidl(info: UpdateInfo) {
993            let as_fidl: fidl::UpdateInfo = info.into();
994            prop_assert_eq!(UpdateInfo::from(as_fidl), info);
995        }
996
997        #[test]
998        fn progress_roundtrips_through_fidl(progress: Progress) {
999            let as_fidl: fidl::InstallationProgress = progress.into();
1000            prop_assert_eq!(as_fidl.try_into(), Ok(progress));
1001        }
1002
1003        #[test]
1004        fn update_info_and_progress_builder_produces_valid_instances(
1005            info: UpdateInfo,
1006            progress: Progress
1007        ) {
1008            let info_progress = UpdateInfoAndProgress::builder()
1009                .info(info)
1010                .progress(progress)
1011                .build();
1012
1013            prop_assert_eq!(
1014                UpdateInfoAndProgress::new(info_progress.info, info_progress.progress),
1015                Ok(info_progress)
1016            );
1017        }
1018
1019        #[test]
1020        fn update_info_and_progress_new_rejects_too_many_bytes(
1021            (a, b) in a_lt_b(),
1022            mut info: UpdateInfo,
1023            mut progress: Progress
1024        ) {
1025            info.download_size = a;
1026            progress.bytes_downloaded = b;
1027
1028            prop_assert_eq!(
1029                UpdateInfoAndProgress::new(info, progress),
1030                Err(BytesFetchedExceedsDownloadSize)
1031            );
1032        }
1033
1034        #[test]
1035        fn state_roundtrips_through_fidl(state: State) {
1036            let as_fidl: fidl::State = state.clone().into();
1037            prop_assert_eq!(as_fidl.try_into(), Ok(state));
1038        }
1039
1040        #[test]
1041        fn state_roundtrips_through_json(state: State) {
1042            let as_json = serde_json::to_value(&state).unwrap();
1043            let state2 = serde_json::from_value(as_json).unwrap();
1044            prop_assert_eq!(state, state2);
1045        }
1046
1047
1048        // Test that:
1049        // * write_to_inspect doesn't panic on arbitrary inputs
1050        // * we create a string property called 'state' in all cases
1051        #[test]
1052        fn state_populates_inspect_with_id(state: State) {
1053            let inspector = Inspector::default();
1054            state.write_to_inspect(inspector.root());
1055
1056            let mut executor = fuchsia_async::TestExecutor::new();
1057            assert_data_tree! {
1058                @executor executor,
1059                inspector,
1060                root: contains {
1061                    "state": state.name(),
1062                }
1063            };
1064        }
1065
1066        #[test]
1067        fn progress_rejects_invalid_fraction_completed(progress: Progress, fraction_completed: f32) {
1068            let fraction_valid = (0.0..=1.0).contains(&fraction_completed);
1069            prop_assume!(!fraction_valid);
1070            // Note, the above doesn't look simplified, but not all the usual math rules apply to
1071            // types that are PartialOrd and not Ord:
1072            //use std::f32::NAN;
1073            //assert!(!(NAN >= 0.0 && NAN <= 1.0)); // This assertion passes.
1074            //assert!(NAN < 0.0 || NAN > 1.0); // This assertion fails.
1075
1076            let mut as_fidl: fidl::InstallationProgress = progress.into();
1077            as_fidl.fraction_completed = Some(fraction_completed);
1078            prop_assert_eq!(Progress::try_from(as_fidl), Err(DecodeProgressError::FractionCompletedOutOfRange));
1079        }
1080
1081        #[test]
1082        fn state_rejects_too_many_bytes_fetched(state: State, (a, b) in a_lt_b()) {
1083            let mut as_fidl: fidl::State = state.into();
1084
1085            let break_info_progress = |info: &mut Option<fidl::UpdateInfo>, progress: &mut Option<fidl::InstallationProgress>| {
1086                info.as_mut().unwrap().download_size = Some(a);
1087                progress.as_mut().unwrap().bytes_downloaded = Some(b);
1088            };
1089
1090            match &mut as_fidl {
1091                fidl::State::Prepare(fidl::PrepareData { .. }) => prop_assume!(false),
1092                fidl::State::Stage(fidl::StageData { info, progress, .. }) => break_info_progress(info, progress),
1093                fidl::State::Fetch(fidl::FetchData { info, progress, .. }) => break_info_progress(info, progress),
1094                fidl::State::Commit(fidl::CommitData { info, progress, .. }) => break_info_progress(info, progress),
1095                fidl::State::WaitToReboot(fidl::WaitToRebootData { info, progress, .. }) => break_info_progress(info, progress),
1096                fidl::State::Reboot(fidl::RebootData { info, progress, .. }) => break_info_progress(info, progress),
1097                fidl::State::DeferReboot(fidl::DeferRebootData { info, progress, .. }) => break_info_progress(info, progress),
1098                fidl::State::Complete(fidl::CompleteData { info, progress, .. }) => break_info_progress(info, progress),
1099                fidl::State::FailPrepare(fidl::FailPrepareData { .. }) => prop_assume!(false),
1100                fidl::State::FailStage(fidl::FailStageData { info, progress, .. }) => break_info_progress(info, progress),
1101                fidl::State::FailFetch(fidl::FailFetchData { info, progress, .. }) => break_info_progress(info, progress),
1102                fidl::State::FailCommit(fidl::FailCommitData { info, progress, .. }) => break_info_progress(info, progress),
1103                fidl::State::Canceled(fidl::CanceledData { .. }) => prop_assume!(false),
1104            }
1105            prop_assert_eq!(
1106                State::try_from(as_fidl),
1107                Err(DecodeStateError::InconsistentUpdateInfoAndProgress(BytesFetchedExceedsDownloadSize))
1108            );
1109        }
1110
1111        // States can merge with identical states.
1112        #[test]
1113        fn state_can_merge_reflexive(state: State) {
1114            prop_assert!(state.can_merge(&state));
1115        }
1116
1117        // States with the same ids can merge, even if the data is different.
1118        #[test]
1119        fn states_with_same_ids_can_merge(
1120            state: State,
1121            different_data: UpdateInfoAndProgress,
1122            different_prepare_reason: PrepareFailureReason,
1123            different_fetch_reason: FetchFailureReason,
1124            different_stage_reason: StageFailureReason,
1125        ) {
1126            let state_with_different_data = match state {
1127                State::Prepare => State::Prepare,
1128                State::Stage(_) => State::Stage(different_data),
1129                State::Fetch(_) => State::Fetch(different_data),
1130                State::Commit(_) => State::Commit(different_data),
1131                State::WaitToReboot(_) => State::WaitToReboot(different_data),
1132                State::Reboot(_) => State::Reboot(different_data),
1133                State::DeferReboot(_) => State::DeferReboot(different_data),
1134                State::Complete(_) => State::Complete(different_data),
1135                // We currently allow merging states with different failure reasons, though
1136                // we don't expect that to ever happen in practice.
1137                State::FailPrepare(_) => State::FailPrepare(different_prepare_reason),
1138                State::FailStage(_) => State::FailStage(different_data.with_stage_reason(different_stage_reason)),
1139                State::FailFetch(_) => State::FailFetch(different_data.with_fetch_reason(different_fetch_reason)),
1140                State::FailCommit(_) => State::FailCommit(different_data),
1141                State::Canceled => State::Canceled,
1142            };
1143            prop_assert!(state.can_merge(&state_with_different_data));
1144        }
1145
1146        #[test]
1147        fn states_with_different_ids_cannot_merge(state0: State, state1: State) {
1148            prop_assume!(state0.id() != state1.id());
1149            prop_assert!(!state0.can_merge(&state1));
1150        }
1151
1152    }
1153
1154    #[fuchsia::test]
1155    async fn populates_inspect_fail_stage() {
1156        let state = State::FailStage(
1157            UpdateInfoAndProgress {
1158                info: UpdateInfo { download_size: 4096 },
1159                progress: Progress { bytes_downloaded: 2048, fraction_completed: 0.5 },
1160            }
1161            .with_stage_reason(StageFailureReason::Internal),
1162        );
1163        let inspector = Inspector::default();
1164        state.write_to_inspect(inspector.root());
1165        assert_data_tree! {
1166            inspector,
1167            root: {
1168                "state": "fail_stage",
1169                "info": {
1170                    "download_size": 4096u64,
1171                },
1172                "progress": {
1173                    "bytes_downloaded": 2048u64,
1174                    "fraction_completed": 0.5f64,
1175                },
1176                "reason": "Internal",
1177            }
1178        }
1179    }
1180
1181    #[fuchsia::test]
1182    async fn populates_inspect_fail_fetch() {
1183        let state = State::FailFetch(
1184            UpdateInfoAndProgress {
1185                info: UpdateInfo { download_size: 4096 },
1186                progress: Progress { bytes_downloaded: 2048, fraction_completed: 0.5 },
1187            }
1188            .with_fetch_reason(FetchFailureReason::Internal),
1189        );
1190        let inspector = Inspector::default();
1191        state.write_to_inspect(inspector.root());
1192        assert_data_tree! {
1193            inspector,
1194            root: {
1195                "state": "fail_fetch",
1196                "info": {
1197                    "download_size": 4096u64,
1198                },
1199                "progress": {
1200                    "bytes_downloaded": 2048u64,
1201                    "fraction_completed": 0.5f64,
1202                },
1203                "reason": "Internal",
1204            }
1205        }
1206    }
1207
1208    #[fuchsia::test]
1209    async fn populates_inspect_fail_prepare() {
1210        let state = State::FailPrepare(PrepareFailureReason::OutOfSpace);
1211        let inspector = Inspector::default();
1212        state.write_to_inspect(inspector.root());
1213        assert_data_tree! {
1214            inspector,
1215            root: {
1216                "state": "fail_prepare",
1217                "reason": "OutOfSpace",
1218            }
1219        }
1220    }
1221
1222    #[fuchsia::test]
1223    async fn populates_inspect_reboot() {
1224        let state = State::Reboot(UpdateInfoAndProgress {
1225            info: UpdateInfo { download_size: 4096 },
1226            progress: Progress { bytes_downloaded: 2048, fraction_completed: 0.5 },
1227        });
1228        let inspector = Inspector::default();
1229        state.write_to_inspect(inspector.root());
1230        assert_data_tree! {
1231            inspector,
1232            root: {
1233                "state": "reboot",
1234                "info": {
1235                    "download_size": 4096u64,
1236                },
1237                "progress": {
1238                    "bytes_downloaded": 2048u64,
1239                    "fraction_completed": 0.5f64,
1240                }
1241            }
1242        }
1243    }
1244
1245    #[test]
1246    fn progress_fraction_completed_required() {
1247        assert_eq!(
1248            Progress::try_from(fidl::InstallationProgress::default()),
1249            Err(DecodeProgressError::MissingField(RequiredProgressField::FractionCompleted)),
1250        );
1251    }
1252
1253    #[test]
1254    fn json_deserializes_state_reboot() {
1255        assert_eq!(
1256            serde_json::from_value::<State>(json!({
1257                "id": "reboot",
1258                "info": {
1259                    "download_size": 100,
1260                },
1261                "progress": {
1262                    "bytes_downloaded": 100,
1263                    "fraction_completed": 1.0,
1264                },
1265            }))
1266            .unwrap(),
1267            State::Reboot(UpdateInfoAndProgress {
1268                info: UpdateInfo { download_size: 100 },
1269                progress: Progress { bytes_downloaded: 100, fraction_completed: 1.0 },
1270            })
1271        );
1272    }
1273
1274    #[test]
1275    fn json_deserializes_state_fail_prepare() {
1276        assert_eq!(
1277            serde_json::from_value::<State>(json!({
1278                "id": "fail_prepare",
1279                "reason": "internal",
1280            }))
1281            .unwrap(),
1282            State::FailPrepare(PrepareFailureReason::Internal)
1283        );
1284    }
1285
1286    #[test]
1287    fn json_deserializes_state_fail_stage() {
1288        assert_eq!(
1289            serde_json::from_value::<State>(json!({
1290                "id": "fail_stage",
1291                "info": {
1292                    "download_size": 100,
1293                },
1294                "progress": {
1295                    "bytes_downloaded": 100,
1296                    "fraction_completed": 1.0,
1297                },
1298                "reason": "out_of_space",
1299            }))
1300            .unwrap(),
1301            State::FailStage(
1302                UpdateInfoAndProgress {
1303                    info: UpdateInfo { download_size: 100 },
1304                    progress: Progress { bytes_downloaded: 100, fraction_completed: 1.0 },
1305                }
1306                .with_stage_reason(StageFailureReason::OutOfSpace)
1307            )
1308        );
1309    }
1310
1311    #[test]
1312    fn json_deserializes_state_fail_fetch() {
1313        assert_eq!(
1314            serde_json::from_value::<State>(json!({
1315                "id": "fail_fetch",
1316                "info": {
1317                    "download_size": 100,
1318                },
1319                "progress": {
1320                    "bytes_downloaded": 100,
1321                    "fraction_completed": 1.0,
1322                },
1323                "reason": "out_of_space",
1324            }))
1325            .unwrap(),
1326            State::FailFetch(
1327                UpdateInfoAndProgress {
1328                    info: UpdateInfo { download_size: 100 },
1329                    progress: Progress { bytes_downloaded: 100, fraction_completed: 1.0 },
1330                }
1331                .with_fetch_reason(FetchFailureReason::OutOfSpace)
1332            )
1333        );
1334    }
1335
1336    #[test]
1337    fn json_deserialize_detects_inconsistent_info_and_progress() {
1338        let too_much_download = json!({
1339            "id": "reboot",
1340            "info": {
1341                "download_size": 100,
1342            },
1343            "progress": {
1344                "bytes_downloaded": 101,
1345                "fraction_completed": 1.0,
1346            },
1347        });
1348
1349        assert_matches!(serde_json::from_value::<State>(too_much_download), Err(_));
1350    }
1351
1352    #[test]
1353    fn json_deserialize_clamps_invalid_fraction_completed() {
1354        let too_much_progress = json!({
1355            "bytes_downloaded": 0,
1356            "fraction_completed": 1.1,
1357        });
1358        assert_eq!(
1359            serde_json::from_value::<Progress>(too_much_progress).unwrap(),
1360            Progress { bytes_downloaded: 0, fraction_completed: 1.0 }
1361        );
1362
1363        let negative_progress = json!({
1364            "bytes_downloaded": 0,
1365            "fraction_completed": -0.5,
1366        });
1367        assert_eq!(
1368            serde_json::from_value::<Progress>(negative_progress).unwrap(),
1369            Progress { bytes_downloaded: 0, fraction_completed: 0.0 }
1370        );
1371    }
1372
1373    #[test]
1374    fn update_info_and_progress_builder_clamps_bytes_downloaded_to_download_size() {
1375        assert_eq!(
1376            UpdateInfoAndProgress::builder()
1377                .info(UpdateInfo { download_size: 100 })
1378                .progress(Progress { bytes_downloaded: 200, fraction_completed: 1.0 })
1379                .build(),
1380            UpdateInfoAndProgress {
1381                info: UpdateInfo { download_size: 100 },
1382                progress: Progress { bytes_downloaded: 100, fraction_completed: 1.0 },
1383            }
1384        );
1385    }
1386}