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