blob: 4f1a343e205a352084f84d5657e6085141181594 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Wrapper types for the State union.
use {
event_queue::Event,
fidl_fuchsia_update_installer as fidl, fuchsia_inspect as inspect,
proptest::prelude::*,
proptest_derive::Arbitrary,
serde::{Deserialize, Serialize},
std::convert::{TryFrom, TryInto},
thiserror::Error,
};
/// The state of an update installation attempt.
#[derive(Arbitrary, Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(tag = "id", rename_all = "snake_case")]
#[allow(missing_docs)]
pub enum State {
Prepare,
Fetch(UpdateInfoAndProgress),
Stage(UpdateInfoAndProgress),
WaitToReboot(UpdateInfoAndProgress),
Reboot(UpdateInfoAndProgress),
DeferReboot(UpdateInfoAndProgress),
Complete(UpdateInfoAndProgress),
FailPrepare(PrepareFailureReason),
FailFetch(FailFetchData),
FailStage(UpdateInfoAndProgress),
}
/// The variant names for each state, with data stripped.
#[allow(missing_docs)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum StateId {
Prepare,
Fetch,
Stage,
WaitToReboot,
Reboot,
DeferReboot,
Complete,
FailPrepare,
FailFetch,
FailStage,
}
/// Immutable metadata for an update attempt.
#[derive(Arbitrary, Clone, Copy, Debug, Serialize, Deserialize, PartialEq, PartialOrd)]
pub struct UpdateInfo {
download_size: u64,
}
/// Builder of UpdateInfo
#[derive(Clone, Debug)]
pub struct UpdateInfoBuilder;
/// Builder of UpdateInfo, with a known download_size field.
#[derive(Clone, Debug)]
pub struct UpdateInfoBuilderWithDownloadSize {
download_size: u64,
}
/// Mutable progress information for an update attempt.
#[derive(Arbitrary, Clone, Copy, Debug, Serialize, PartialEq, PartialOrd)]
pub struct Progress {
/// Within the range of [0.0, 1.0]
#[proptest(strategy = "0.0f32 ..= 1.0")]
fraction_completed: f32,
bytes_downloaded: u64,
}
/// Builder of Progress.
#[derive(Clone, Debug)]
pub struct ProgressBuilder;
/// Builder of Progress, with a known fraction_completed field.
#[derive(Clone, Debug)]
pub struct ProgressBuilderWithFraction {
fraction_completed: f32,
}
/// Builder of Progress, with a known fraction_completed and bytes_downloaded field.
#[derive(Clone, Debug)]
pub struct ProgressBuilderWithFractionAndBytes {
fraction_completed: f32,
bytes_downloaded: u64,
}
/// An UpdateInfo and Progress that are guaranteed to be consistent with each other.
///
/// Specifically, `progress.bytes_downloaded <= info.download_size`.
#[derive(Clone, Copy, Debug, Serialize, PartialEq, PartialOrd)]
pub struct UpdateInfoAndProgress {
info: UpdateInfo,
progress: Progress,
}
/// Builder of UpdateInfoAndProgress.
#[derive(Clone, Debug)]
pub struct UpdateInfoAndProgressBuilder;
/// Builder of UpdateInfoAndProgress, with a known UpdateInfo field.
#[derive(Clone, Debug)]
pub struct UpdateInfoAndProgressBuilderWithInfo {
info: UpdateInfo,
}
/// Builder of UpdateInfoAndProgress, with a known UpdateInfo and Progress field.
#[derive(Clone, Debug)]
pub struct UpdateInfoAndProgressBuilderWithInfoAndProgress {
info: UpdateInfo,
progress: Progress,
}
#[derive(Arbitrary, Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
#[serde(tag = "reason", rename_all = "snake_case")]
#[allow(missing_docs)]
pub enum PrepareFailureReason {
Internal,
OutOfSpace,
}
#[derive(Arbitrary, Copy, Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
#[allow(missing_docs)]
pub enum FetchFailureReason {
Internal,
OutOfSpace,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[allow(missing_docs)]
pub struct FailFetchData {
info_and_progress: UpdateInfoAndProgress,
reason: FetchFailureReason,
}
impl State {
/// Obtain the variant name (strip out the data).
pub fn id(&self) -> StateId {
match self {
State::Prepare => StateId::Prepare,
State::Fetch(_) => StateId::Fetch,
State::Stage(_) => StateId::Stage,
State::WaitToReboot(_) => StateId::WaitToReboot,
State::Reboot(_) => StateId::Reboot,
State::DeferReboot(_) => StateId::DeferReboot,
State::Complete(_) => StateId::Complete,
State::FailPrepare(_) => StateId::FailPrepare,
State::FailFetch(_) => StateId::FailFetch,
State::FailStage(_) => StateId::FailStage,
}
}
/// Determines if this state is terminal and represents a successful attempt.
pub fn is_success(&self) -> bool {
match self.id() {
StateId::Reboot | StateId::DeferReboot | StateId::Complete => true,
_ => false,
}
}
/// Determines if this state is terminal and represents a failure.
pub fn is_failure(&self) -> bool {
match self.id() {
StateId::FailPrepare | StateId::FailFetch | StateId::FailStage => true,
_ => false,
}
}
/// Determines if this state is terminal (terminal states are final, no futher state
/// transitions should occur).
pub fn is_terminal(&self) -> bool {
self.is_success() || self.is_failure()
}
/// Returns the name of the state, intended for use in log/diagnostics output.
pub fn name(&self) -> &'static str {
match self {
State::Prepare => "prepare",
State::Fetch(_) => "fetch",
State::Stage(_) => "stage",
State::WaitToReboot(_) => "wait_to_reboot",
State::Reboot(_) => "reboot",
State::DeferReboot(_) => "defer_reboot",
State::Complete(_) => "complete",
State::FailPrepare(_) => "fail_prepare",
State::FailFetch(_) => "fail_fetch",
State::FailStage(_) => "fail_stage",
}
}
/// Serializes this state to a Fuchsia Inspect node.
pub fn write_to_inspect(&self, node: &inspect::Node) {
node.record_string("state", self.name());
use State::*;
match self {
Prepare => {}
FailFetch(data) => data.write_to_inspect(node),
FailPrepare(reason) => reason.write_to_inspect(node),
Fetch(info_progress)
| Stage(info_progress)
| WaitToReboot(info_progress)
| Reboot(info_progress)
| DeferReboot(info_progress)
| Complete(info_progress)
| FailStage(info_progress) => {
info_progress.write_to_inspect(node);
}
}
}
/// Extracts info_and_progress, if the state supports it.
fn info_and_progress(&self) -> Option<&UpdateInfoAndProgress> {
match self {
State::Prepare | State::FailPrepare(_) => None,
State::FailFetch(data) => Some(&data.info_and_progress),
State::Fetch(data)
| State::Stage(data)
| State::WaitToReboot(data)
| State::Reboot(data)
| State::DeferReboot(data)
| State::Complete(data)
| State::FailStage(data) => Some(data),
}
}
/// Extracts progress, if the state supports it.
pub fn progress(&self) -> Option<&Progress> {
match self.info_and_progress() {
Some(UpdateInfoAndProgress { info: _, progress }) => Some(progress),
_ => None,
}
}
/// Extracts the download_size field in UpdateInfo, if the state supports it.
pub fn download_size(&self) -> Option<u64> {
match self.info_and_progress() {
Some(UpdateInfoAndProgress { info, progress: _ }) => Some(info.download_size()),
_ => None,
}
}
}
impl Event for State {
fn can_merge(&self, other: &Self) -> bool {
self.id() == other.id()
}
}
impl UpdateInfo {
/// Starts building an instance of UpdateInfo.
pub fn builder() -> UpdateInfoBuilder {
UpdateInfoBuilder
}
/// Gets the download_size field.
pub fn download_size(&self) -> u64 {
self.download_size
}
fn write_to_inspect(&self, node: &inspect::Node) {
let UpdateInfo { download_size } = self;
node.record_uint("download_size", *download_size)
}
}
impl UpdateInfoBuilder {
/// Sets the download_size field.
pub fn download_size(self, download_size: u64) -> UpdateInfoBuilderWithDownloadSize {
UpdateInfoBuilderWithDownloadSize { download_size }
}
}
impl UpdateInfoBuilderWithDownloadSize {
/// Builds the UpdateInfo instance.
pub fn build(self) -> UpdateInfo {
let Self { download_size } = self;
UpdateInfo { download_size }
}
}
impl Progress {
/// Starts building an instance of Progress.
pub fn builder() -> ProgressBuilder {
ProgressBuilder
}
/// Produces a Progress at 0% complete and 0 bytes downloaded.
pub fn none() -> Self {
Self { fraction_completed: 0.0, bytes_downloaded: 0 }
}
/// Produces a Progress at 100% complete and all bytes downloaded, based on the download_size
/// in `info`.
pub fn done(info: &UpdateInfo) -> Self {
Self { fraction_completed: 1.0, bytes_downloaded: info.download_size }
}
/// Gets the fraction_completed field.
pub fn fraction_completed(&self) -> f32 {
self.fraction_completed
}
/// Gets the bytes_downloaded field.
pub fn bytes_downloaded(&self) -> u64 {
self.bytes_downloaded
}
fn write_to_inspect(&self, node: &inspect::Node) {
let Progress { fraction_completed, bytes_downloaded } = self;
node.record_double("fraction_completed", *fraction_completed as f64);
node.record_uint("bytes_downloaded", *bytes_downloaded);
}
}
impl ProgressBuilder {
/// Sets the fraction_completed field, claming the provided float to the range [0.0, 1.0] and
/// converting NaN to 0.0.
pub fn fraction_completed(self, fraction_completed: f32) -> ProgressBuilderWithFraction {
ProgressBuilderWithFraction { fraction_completed: fraction_completed.max(0.0).min(1.0) }
}
}
impl ProgressBuilderWithFraction {
/// Sets the bytes_downloaded field.
pub fn bytes_downloaded(self, bytes_downloaded: u64) -> ProgressBuilderWithFractionAndBytes {
ProgressBuilderWithFractionAndBytes {
fraction_completed: self.fraction_completed,
bytes_downloaded,
}
}
}
impl ProgressBuilderWithFractionAndBytes {
/// Builds the Progress instance.
pub fn build(self) -> Progress {
let Self { fraction_completed, bytes_downloaded } = self;
Progress { fraction_completed, bytes_downloaded }
}
}
impl UpdateInfoAndProgress {
/// Starts building an instance of UpdateInfoAndProgress.
pub fn builder() -> UpdateInfoAndProgressBuilder {
UpdateInfoAndProgressBuilder
}
/// Constructs an UpdateInfoAndProgress from the 2 fields, ensuring that the 2 structs are
/// consistent with each other, returning an error if they are not.
pub fn new(
info: UpdateInfo,
progress: Progress,
) -> Result<Self, BytesFetchedExceedsDownloadSize> {
if progress.bytes_downloaded > info.download_size {
return Err(BytesFetchedExceedsDownloadSize);
}
Ok(Self { info, progress })
}
/// Constructs an UpdateInfoAndProgress from an UpdateInfo, setting the progress fields to be
/// 100% done with all bytes downloaded.
pub fn done(info: UpdateInfo) -> Self {
Self { progress: Progress::done(&info), info }
}
/// Returns the info field.
pub fn info(&self) -> UpdateInfo {
self.info
}
/// Returns the progress field.
pub fn progress(&self) -> &Progress {
&self.progress
}
/// Constructs a FailFetchData with the given reason.
pub fn with_reason(self, reason: FetchFailureReason) -> FailFetchData {
FailFetchData { info_and_progress: self, reason }
}
fn write_to_inspect(&self, node: &inspect::Node) {
node.record_child("info", |n| {
self.info.write_to_inspect(n);
});
node.record_child("progress", |n| {
self.progress.write_to_inspect(n);
});
}
}
impl UpdateInfoAndProgressBuilder {
/// Sets the UpdateInfo field.
pub fn info(self, info: UpdateInfo) -> UpdateInfoAndProgressBuilderWithInfo {
UpdateInfoAndProgressBuilderWithInfo { info }
}
}
impl UpdateInfoAndProgressBuilderWithInfo {
/// Sets the Progress field, clamping `progress.bytes_downloaded` to be `<=
/// info.download_size`. Users of this API should independently ensure that this invariant is
/// not violated.
pub fn progress(
self,
mut progress: Progress,
) -> UpdateInfoAndProgressBuilderWithInfoAndProgress {
if progress.bytes_downloaded > self.info.download_size {
progress.bytes_downloaded = self.info.download_size;
}
UpdateInfoAndProgressBuilderWithInfoAndProgress { info: self.info, progress }
}
}
impl UpdateInfoAndProgressBuilderWithInfoAndProgress {
/// Builds the UpdateInfoAndProgress instance.
pub fn build(self) -> UpdateInfoAndProgress {
let Self { info, progress } = self;
UpdateInfoAndProgress { info, progress }
}
}
impl FailFetchData {
fn write_to_inspect(&self, node: &inspect::Node) {
self.info_and_progress.write_to_inspect(node);
self.reason.write_to_inspect(node);
}
/// Get the reason associated with this FetchFailData
pub fn reason(&self) -> FetchFailureReason {
self.reason
}
}
impl PrepareFailureReason {
fn write_to_inspect(&self, node: &inspect::Node) {
node.record_string("reason", format!("{:?}", self))
}
}
impl From<fidl::PrepareFailureReason> for PrepareFailureReason {
fn from(reason: fidl::PrepareFailureReason) -> Self {
match reason {
fidl::PrepareFailureReason::Internal => PrepareFailureReason::Internal,
fidl::PrepareFailureReason::OutOfSpace => PrepareFailureReason::OutOfSpace,
}
}
}
impl From<PrepareFailureReason> for fidl::PrepareFailureReason {
fn from(reason: PrepareFailureReason) -> Self {
match reason {
PrepareFailureReason::Internal => fidl::PrepareFailureReason::Internal,
PrepareFailureReason::OutOfSpace => fidl::PrepareFailureReason::OutOfSpace,
}
}
}
impl FetchFailureReason {
fn write_to_inspect(&self, node: &inspect::Node) {
node.record_string("reason", format!("{:?}", self))
}
}
impl From<fidl::FetchFailureReason> for FetchFailureReason {
fn from(reason: fidl::FetchFailureReason) -> Self {
match reason {
fidl::FetchFailureReason::Internal => FetchFailureReason::Internal,
fidl::FetchFailureReason::OutOfSpace => FetchFailureReason::OutOfSpace,
}
}
}
impl From<FetchFailureReason> for fidl::FetchFailureReason {
fn from(reason: FetchFailureReason) -> Self {
match reason {
FetchFailureReason::Internal => fidl::FetchFailureReason::Internal,
FetchFailureReason::OutOfSpace => fidl::FetchFailureReason::OutOfSpace,
}
}
}
impl<'de> Deserialize<'de> for UpdateInfoAndProgress {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
#[derive(Debug, Deserialize)]
pub struct DeUpdateInfoAndProgress {
info: UpdateInfo,
progress: Progress,
}
let info_progress = DeUpdateInfoAndProgress::deserialize(deserializer)?;
UpdateInfoAndProgress::new(info_progress.info, info_progress.progress)
.map_err(|e| D::Error::custom(e.to_string()))
}
}
impl<'de> Deserialize<'de> for Progress {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Debug, Deserialize)]
pub struct DeProgress {
fraction_completed: f32,
bytes_downloaded: u64,
}
let progress = DeProgress::deserialize(deserializer)?;
Ok(Progress::builder()
.fraction_completed(progress.fraction_completed)
.bytes_downloaded(progress.bytes_downloaded)
.build())
}
}
impl Serialize for FailFetchData {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("FailFetchData", 3)?;
state.serialize_field("info", &self.info_and_progress.info)?;
state.serialize_field("progress", &self.info_and_progress.progress)?;
state.serialize_field("reason", &self.reason)?;
state.end()
}
}
impl<'de> Deserialize<'de> for FailFetchData {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
#[derive(Debug, Deserialize)]
pub struct DeFailFetchData {
info: UpdateInfo,
progress: Progress,
reason: FetchFailureReason,
}
let DeFailFetchData { info, progress, reason } =
DeFailFetchData::deserialize(deserializer)?;
UpdateInfoAndProgress::new(info, progress)
.map_err(|e| D::Error::custom(e.to_string()))
.map(|info_and_progress| info_and_progress.with_reason(reason))
}
}
/// An error encountered while pairing an [`UpdateInfo`] and [`Progress`].
#[derive(Debug, Error, PartialEq, Eq)]
#[error("more bytes were fetched than should have been fetched")]
pub struct BytesFetchedExceedsDownloadSize;
/// An error encountered while decoding a [fidl_fuchsia_update_installer::State]
/// into a [State].
#[derive(Debug, Error, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum DecodeStateError {
#[error("missing field {0:?}")]
MissingField(RequiredStateField),
#[error("state contained invalid 'info' field")]
DecodeUpdateInfo(#[source] DecodeUpdateInfoError),
#[error("state contained invalid 'progress' field")]
DecodeProgress(#[source] DecodeProgressError),
#[error("the provided update info and progress are inconsistent with each other")]
InconsistentUpdateInfoAndProgress(#[source] BytesFetchedExceedsDownloadSize),
}
/// Required fields in a [fidl_fuchsia_update_installer::State].
#[derive(Debug, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum RequiredStateField {
Info,
Progress,
Reason,
}
impl From<State> for fidl::State {
fn from(state: State) -> Self {
match state {
State::Prepare => fidl::State::Prepare(fidl::PrepareData::EMPTY),
State::Fetch(UpdateInfoAndProgress { info, progress }) => {
fidl::State::Fetch(fidl::FetchData {
info: Some(info.into()),
progress: Some(progress.into()),
..fidl::FetchData::EMPTY
})
}
State::Stage(UpdateInfoAndProgress { info, progress }) => {
fidl::State::Stage(fidl::StageData {
info: Some(info.into()),
progress: Some(progress.into()),
..fidl::StageData::EMPTY
})
}
State::WaitToReboot(UpdateInfoAndProgress { info, progress }) => {
fidl::State::WaitToReboot(fidl::WaitToRebootData {
info: Some(info.into()),
progress: Some(progress.into()),
..fidl::WaitToRebootData::EMPTY
})
}
State::Reboot(UpdateInfoAndProgress { info, progress }) => {
fidl::State::Reboot(fidl::RebootData {
info: Some(info.into()),
progress: Some(progress.into()),
..fidl::RebootData::EMPTY
})
}
State::DeferReboot(UpdateInfoAndProgress { info, progress }) => {
fidl::State::DeferReboot(fidl::DeferRebootData {
info: Some(info.into()),
progress: Some(progress.into()),
..fidl::DeferRebootData::EMPTY
})
}
State::Complete(UpdateInfoAndProgress { info, progress }) => {
fidl::State::Complete(fidl::CompleteData {
info: Some(info.into()),
progress: Some(progress.into()),
..fidl::CompleteData::EMPTY
})
}
State::FailPrepare(reason) => fidl::State::FailPrepare(fidl::FailPrepareData {
reason: Some(reason.into()),
..fidl::FailPrepareData::EMPTY
}),
State::FailFetch(FailFetchData { info_and_progress, reason }) => {
fidl::State::FailFetch(fidl::FailFetchData {
info: Some(info_and_progress.info.into()),
progress: Some(info_and_progress.progress.into()),
reason: Some(reason.into()),
..fidl::FailFetchData::EMPTY
})
}
State::FailStage(UpdateInfoAndProgress { info, progress }) => {
fidl::State::FailStage(fidl::FailStageData {
info: Some(info.into()),
progress: Some(progress.into()),
..fidl::FailStageData::EMPTY
})
}
}
}
}
impl TryFrom<fidl::State> for State {
type Error = DecodeStateError;
fn try_from(state: fidl::State) -> Result<Self, Self::Error> {
fn decode_info_progress(
info: Option<fidl::UpdateInfo>,
progress: Option<fidl::InstallationProgress>,
) -> Result<UpdateInfoAndProgress, DecodeStateError> {
let info: UpdateInfo = info
.ok_or(DecodeStateError::MissingField(RequiredStateField::Info))?
.try_into()
.map_err(DecodeStateError::DecodeUpdateInfo)?;
let progress: Progress = progress
.ok_or(DecodeStateError::MissingField(RequiredStateField::Progress))?
.try_into()
.map_err(DecodeStateError::DecodeProgress)?;
UpdateInfoAndProgress::new(info, progress)
.map_err(DecodeStateError::InconsistentUpdateInfoAndProgress)
}
Ok(match state {
fidl::State::Prepare(fidl::PrepareData { .. }) => State::Prepare,
fidl::State::Fetch(fidl::FetchData { info, progress, .. }) => {
State::Fetch(decode_info_progress(info, progress)?)
}
fidl::State::Stage(fidl::StageData { info, progress, .. }) => {
State::Stage(decode_info_progress(info, progress)?)
}
fidl::State::WaitToReboot(fidl::WaitToRebootData { info, progress, .. }) => {
State::WaitToReboot(decode_info_progress(info, progress)?)
}
fidl::State::Reboot(fidl::RebootData { info, progress, .. }) => {
State::Reboot(decode_info_progress(info, progress)?)
}
fidl::State::DeferReboot(fidl::DeferRebootData { info, progress, .. }) => {
State::DeferReboot(decode_info_progress(info, progress)?)
}
fidl::State::Complete(fidl::CompleteData { info, progress, .. }) => {
State::Complete(decode_info_progress(info, progress)?)
}
fidl::State::FailPrepare(fidl::FailPrepareData { reason, .. }) => State::FailPrepare(
reason.ok_or(DecodeStateError::MissingField(RequiredStateField::Reason))?.into(),
),
fidl::State::FailFetch(fidl::FailFetchData { info, progress, reason, .. }) => {
State::FailFetch(
decode_info_progress(info, progress)?.with_reason(
reason
.ok_or(DecodeStateError::MissingField(RequiredStateField::Reason))?
.into(),
),
)
}
fidl::State::FailStage(fidl::FailStageData { info, progress, .. }) => {
State::FailStage(decode_info_progress(info, progress)?)
}
})
}
}
// TODO remove ambiguous mapping of 0 to/from None when the system-updater actually computes a
// download size and emits bytes_downloaded information.
fn none_or_some_nonzero(n: u64) -> Option<u64> {
if n == 0 {
None
} else {
Some(n)
}
}
/// An error encountered while decoding a [fidl_fuchsia_update_installer::UpdateInfo] into a
/// [UpdateInfo].
#[derive(Debug, Error, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum DecodeUpdateInfoError {}
impl From<UpdateInfo> for fidl::UpdateInfo {
fn from(info: UpdateInfo) -> Self {
fidl::UpdateInfo {
download_size: none_or_some_nonzero(info.download_size),
..fidl::UpdateInfo::EMPTY
}
}
}
impl TryFrom<fidl::UpdateInfo> for UpdateInfo {
type Error = DecodeUpdateInfoError;
fn try_from(info: fidl::UpdateInfo) -> Result<Self, Self::Error> {
Ok(UpdateInfo { download_size: info.download_size.unwrap_or(0) })
}
}
/// An error encountered while decoding a [fidl_fuchsia_update_installer::InstallationProgress]
/// into a [Progress].
#[derive(Debug, Error, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum DecodeProgressError {
#[error("missing field {0:?}")]
MissingField(RequiredProgressField),
#[error("fraction completed not in range [0.0, 1.0]")]
FractionCompletedOutOfRange,
}
/// Required fields in a [fidl_fuchsia_update_installer::InstallationProgress].
#[derive(Debug, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum RequiredProgressField {
FractionCompleted,
}
impl From<Progress> for fidl::InstallationProgress {
fn from(progress: Progress) -> Self {
fidl::InstallationProgress {
fraction_completed: Some(progress.fraction_completed),
bytes_downloaded: none_or_some_nonzero(progress.bytes_downloaded),
..fidl::InstallationProgress::EMPTY
}
}
}
impl TryFrom<fidl::InstallationProgress> for Progress {
type Error = DecodeProgressError;
fn try_from(progress: fidl::InstallationProgress) -> Result<Self, Self::Error> {
Ok(Progress {
fraction_completed: {
let n = progress.fraction_completed.ok_or(DecodeProgressError::MissingField(
RequiredProgressField::FractionCompleted,
))?;
if n < 0.0 || n > 1.0 {
return Err(DecodeProgressError::FractionCompletedOutOfRange);
}
n
},
bytes_downloaded: progress.bytes_downloaded.unwrap_or(0),
})
}
}
impl Arbitrary for UpdateInfoAndProgress {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
arb_info_and_progress().prop_map(|(info, progress)| Self { info, progress }).boxed()
}
}
impl Arbitrary for FailFetchData {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
arb_info_and_progress()
.prop_flat_map(|(info, progress)| {
any::<FetchFailureReason>().prop_map(move |reason| {
UpdateInfoAndProgress { info, progress }.with_reason(reason)
})
})
.boxed()
}
}
/// Returns a strategy generating and UpdateInfo and Progress such that the Progress does not
/// exceed the bounds of the UpdateInfo.
fn arb_info_and_progress() -> impl Strategy<Value = (UpdateInfo, Progress)> {
prop_compose! {
fn arb_progress_for_info(
info: UpdateInfo
)(
fraction_completed: f32,
bytes_downloaded in 0..=info.download_size
) -> Progress {
Progress::builder()
.fraction_completed(fraction_completed)
.bytes_downloaded(bytes_downloaded)
.build()
}
}
any::<UpdateInfo>().prop_flat_map(|info| (Just(info), arb_progress_for_info(info)))
}
#[cfg(test)]
mod tests {
use {
super::*,
fuchsia_inspect::{assert_inspect_tree, Inspector},
matches::assert_matches,
serde_json::json,
};
prop_compose! {
fn arb_progress()(fraction_completed: f32, bytes_downloaded: u64) -> Progress {
Progress::builder()
.fraction_completed(fraction_completed)
.bytes_downloaded(bytes_downloaded)
.build()
}
}
/// Returns a strategy generating (a, b) such that a < b.
fn a_lt_b() -> impl Strategy<Value = (u64, u64)> {
(0..u64::MAX).prop_flat_map(|a| (Just(a), a + 1..))
}
proptest! {
#[test]
fn progress_builder_clamps_fraction_completed(progress in arb_progress()) {
prop_assert!(progress.fraction_completed() >= 0.0);
prop_assert!(progress.fraction_completed() <= 1.0);
}
#[test]
fn progress_builder_roundtrips(progress: Progress) {
prop_assert_eq!(
Progress::builder()
.fraction_completed(progress.fraction_completed())
.bytes_downloaded(progress.bytes_downloaded())
.build(),
progress
);
}
#[test]
fn update_info_builder_roundtrips(info: UpdateInfo) {
prop_assert_eq!(
UpdateInfo::builder()
.download_size(info.download_size())
.build(),
info
);
}
#[test]
fn update_info_and_progress_builder_roundtrips(info_progress: UpdateInfoAndProgress) {
prop_assert_eq!(
UpdateInfoAndProgress::builder()
.info(info_progress.info)
.progress(info_progress.progress.clone())
.build(),
info_progress
);
}
#[test]
fn update_info_roundtrips_through_fidl(info: UpdateInfo) {
let as_fidl: fidl::UpdateInfo = info.clone().into();
prop_assert_eq!(as_fidl.try_into(), Ok(info));
}
#[test]
fn progress_roundtrips_through_fidl(progress: Progress) {
let as_fidl: fidl::InstallationProgress = progress.clone().into();
prop_assert_eq!(as_fidl.try_into(), Ok(progress));
}
#[test]
fn update_info_and_progress_builder_produces_valid_instances(
info: UpdateInfo,
progress: Progress
) {
let info_progress = UpdateInfoAndProgress::builder()
.info(info)
.progress(progress)
.build();
prop_assert_eq!(
UpdateInfoAndProgress::new(info_progress.info.clone(), info_progress.progress.clone()),
Ok(info_progress)
);
}
#[test]
fn update_info_and_progress_new_rejects_too_many_bytes(
(a, b) in a_lt_b(),
mut info: UpdateInfo,
mut progress: Progress
) {
info.download_size = a;
progress.bytes_downloaded = b;
prop_assert_eq!(
UpdateInfoAndProgress::new(info, progress),
Err(BytesFetchedExceedsDownloadSize)
);
}
#[test]
fn state_roundtrips_through_fidl(state: State) {
let as_fidl: fidl::State = state.clone().into();
prop_assert_eq!(as_fidl.try_into(), Ok(state));
}
#[test]
fn state_roundtrips_through_json(state: State) {
let as_json = serde_json::to_value(&state).unwrap();
let state2 = serde_json::from_value(as_json).unwrap();
prop_assert_eq!(state, state2);
}
// Test that:
// * write_to_inspect doesn't panic on arbitrary inputs
// * we create a string property called 'state' in all cases
#[test]
fn state_populates_inspect_with_id(state: State) {
let inspector = Inspector::new();
state.write_to_inspect(inspector.root());
assert_inspect_tree! {
inspector,
root: contains {
"state": state.name(),
}
};
}
#[test]
fn progress_rejects_invalid_fraction_completed(progress: Progress, fraction_completed: f32) {
let fraction_valid = fraction_completed >= 0.0 && fraction_completed <= 1.0;
prop_assume!(!fraction_valid);
// Note, the above doesn't look simplified, but not all the usual math rules apply to
// types that are PartialOrd and not Ord:
//use std::f32::NAN;
//assert!(!(NAN >= 0.0 && NAN <= 1.0)); // This assertion passes.
//assert!(NAN < 0.0 || NAN > 1.0); // This assertion fails.
let mut as_fidl: fidl::InstallationProgress = progress.into();
as_fidl.fraction_completed = Some(fraction_completed);
prop_assert_eq!(Progress::try_from(as_fidl), Err(DecodeProgressError::FractionCompletedOutOfRange));
}
#[test]
fn state_rejects_too_many_bytes_fetched(state: State, (a, b) in a_lt_b()) {
let mut as_fidl: fidl::State = state.into();
let break_info_progress = |info: &mut Option<fidl::UpdateInfo>, progress: &mut Option<fidl::InstallationProgress>| {
info.as_mut().unwrap().download_size = Some(a);
progress.as_mut().unwrap().bytes_downloaded = Some(b);
};
match &mut as_fidl {
fidl::State::Prepare(fidl::PrepareData { .. }) => prop_assume!(false),
fidl::State::Fetch(fidl::FetchData { info, progress, .. }) => break_info_progress(info, progress),
fidl::State::Stage(fidl::StageData { info, progress, .. }) => break_info_progress(info, progress),
fidl::State::WaitToReboot(fidl::WaitToRebootData { info, progress, .. }) => break_info_progress(info, progress),
fidl::State::Reboot(fidl::RebootData { info, progress, .. }) => break_info_progress(info, progress),
fidl::State::DeferReboot(fidl::DeferRebootData { info, progress, .. }) => break_info_progress(info, progress),
fidl::State::Complete(fidl::CompleteData { info, progress, .. }) => break_info_progress(info, progress),
fidl::State::FailPrepare(fidl::FailPrepareData { .. }) => prop_assume!(false),
fidl::State::FailFetch(fidl::FailFetchData { info, progress, .. }) => break_info_progress(info, progress),
fidl::State::FailStage(fidl::FailStageData { info, progress, .. }) => break_info_progress(info, progress),
}
prop_assert_eq!(
State::try_from(as_fidl),
Err(DecodeStateError::InconsistentUpdateInfoAndProgress(BytesFetchedExceedsDownloadSize))
);
}
// States can merge with identical states.
#[test]
fn state_can_merge_reflexive(state: State) {
prop_assert!(state.can_merge(&state));
}
// States with the same ids can merge, even if the data is different.
#[test]
fn states_with_same_ids_can_merge(
state: State,
different_data: UpdateInfoAndProgress,
different_prepare_reason: PrepareFailureReason,
different_fetch_reason: FetchFailureReason,
) {
let state_with_different_data = match state.clone() {
State::Prepare => State::Prepare,
State::Fetch(_) => State::Fetch(different_data),
State::Stage(_) => State::Stage(different_data),
State::WaitToReboot(_) => State::WaitToReboot(different_data),
State::Reboot(_) => State::Reboot(different_data),
State::DeferReboot(_) => State::DeferReboot(different_data),
State::Complete(_) => State::Complete(different_data),
// We currently allow merging states with different failure reasons, though
// we don't expect that to ever happen in practice.
State::FailPrepare(_) => State::FailPrepare(different_prepare_reason),
State::FailFetch(_) =>
State::FailFetch(different_data.with_reason(different_fetch_reason)),
State::FailStage(_) => State::FailStage(different_data),
};
prop_assert!(state.can_merge(&state_with_different_data));
}
#[test]
fn states_with_different_ids_cannot_merge(state0: State, state1: State) {
prop_assume!(state0.id() != state1.id());
prop_assert!(!state0.can_merge(&state1));
}
}
#[test]
fn populates_inspect_fail_fetch() {
let state = State::FailFetch(
UpdateInfoAndProgress {
info: UpdateInfo { download_size: 4096 },
progress: Progress { bytes_downloaded: 2048, fraction_completed: 0.5 },
}
.with_reason(FetchFailureReason::Internal),
);
let inspector = Inspector::new();
state.write_to_inspect(&inspector.root());
assert_inspect_tree! {
inspector,
root: {
"state": "fail_fetch",
"info": {
"download_size": 4096u64,
},
"progress": {
"bytes_downloaded": 2048u64,
"fraction_completed": 0.5f64,
},
"reason": "Internal",
}
}
}
#[test]
fn populates_inspect_fail_prepare() {
let state = State::FailPrepare(PrepareFailureReason::OutOfSpace);
let inspector = Inspector::new();
state.write_to_inspect(&inspector.root());
assert_inspect_tree! {
inspector,
root: {
"state": "fail_prepare",
"reason": "OutOfSpace",
}
}
}
#[test]
fn populates_inspect_reboot() {
let state = State::Reboot(UpdateInfoAndProgress {
info: UpdateInfo { download_size: 4096 },
progress: Progress { bytes_downloaded: 2048, fraction_completed: 0.5 },
});
let inspector = Inspector::new();
state.write_to_inspect(&inspector.root());
assert_inspect_tree! {
inspector,
root: {
"state": "reboot",
"info": {
"download_size": 4096u64,
},
"progress": {
"bytes_downloaded": 2048u64,
"fraction_completed": 0.5f64,
}
}
}
}
#[test]
fn progress_fraction_completed_required() {
assert_eq!(
Progress::try_from(fidl::InstallationProgress::EMPTY),
Err(DecodeProgressError::MissingField(RequiredProgressField::FractionCompleted)),
);
}
#[test]
fn json_deserializes_state_reboot() {
assert_eq!(
serde_json::from_value::<State>(json!({
"id": "reboot",
"info": {
"download_size": 100,
},
"progress": {
"bytes_downloaded": 100,
"fraction_completed": 1.0,
},
}))
.unwrap(),
State::Reboot(UpdateInfoAndProgress {
info: UpdateInfo { download_size: 100 },
progress: Progress { bytes_downloaded: 100, fraction_completed: 1.0 },
})
);
}
#[test]
fn json_deserializes_state_fail_prepare() {
assert_eq!(
serde_json::from_value::<State>(json!({
"id": "fail_prepare",
"reason": "internal",
}))
.unwrap(),
State::FailPrepare(PrepareFailureReason::Internal)
);
}
#[test]
fn json_deserializes_state_fail_fetch() {
assert_eq!(
serde_json::from_value::<State>(json!({
"id": "fail_fetch",
"info": {
"download_size": 100,
},
"progress": {
"bytes_downloaded": 100,
"fraction_completed": 1.0,
},
"reason": "out_of_space",
}))
.unwrap(),
State::FailFetch(
UpdateInfoAndProgress {
info: UpdateInfo { download_size: 100 },
progress: Progress { bytes_downloaded: 100, fraction_completed: 1.0 },
}
.with_reason(FetchFailureReason::OutOfSpace)
)
);
}
#[test]
fn json_deserialize_detects_inconsistent_info_and_progress() {
let too_much_download = json!({
"id": "reboot",
"info": {
"download_size": 100,
},
"progress": {
"bytes_downloaded": 101,
"fraction_completed": 1.0,
},
});
assert_matches!(serde_json::from_value::<State>(too_much_download), Err(_));
}
#[test]
fn json_deserialize_clamps_invalid_fraction_completed() {
let too_much_progress = json!({
"bytes_downloaded": 0,
"fraction_completed": 1.1,
});
assert_eq!(
serde_json::from_value::<Progress>(too_much_progress).unwrap(),
Progress { bytes_downloaded: 0, fraction_completed: 1.0 }
);
let negative_progress = json!({
"bytes_downloaded": 0,
"fraction_completed": -0.5,
});
assert_eq!(
serde_json::from_value::<Progress>(negative_progress).unwrap(),
Progress { bytes_downloaded: 0, fraction_completed: 0.0 }
);
}
#[test]
fn update_info_and_progress_builder_clamps_bytes_downloaded_to_download_size() {
assert_eq!(
UpdateInfoAndProgress::builder()
.info(UpdateInfo { download_size: 100 })
.progress(Progress { bytes_downloaded: 200, fraction_completed: 1.0 })
.build(),
UpdateInfoAndProgress {
info: UpdateInfo { download_size: 100 },
progress: Progress { bytes_downloaded: 100, fraction_completed: 1.0 },
}
);
}
}