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