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