Skip to main content

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