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