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