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