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