blob: 94a5c93670dbab98fd387244dee60dc4bf30165a [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.
use {
event_queue::Event, fidl_fuchsia_update as fidl, proptest::prelude::*,
proptest_derive::Arbitrary, std::convert::TryFrom, thiserror::Error,
};
/// Wrapper type for [`fidl_fuchsia_update::State`] which works with
/// [`event_queue`] and [`proptest`].
///
/// Use [`From`] (and [`Into`]) to convert between the fidl type and this one.
///
/// See [`fidl_fuchsia_update::State`] for docs on what each state means.
#[allow(missing_docs)] // states are documented in fidl.
#[derive(Clone, Debug, PartialEq, Arbitrary)]
pub enum State {
CheckingForUpdates,
ErrorCheckingForUpdate,
NoUpdateAvailable,
InstallationDeferredByPolicy(InstallationDeferredData),
InstallingUpdate(InstallingData),
WaitingForReboot(InstallingData),
InstallationError(InstallationErrorData),
}
impl State {
/// Returns true if this state is an error state.
pub fn is_error(&self) -> bool {
match self {
State::ErrorCheckingForUpdate | State::InstallationError(_) => true,
State::CheckingForUpdates
| State::NoUpdateAvailable
| State::InstallationDeferredByPolicy(_)
| State::InstallingUpdate(_)
| State::WaitingForReboot(_) => false,
}
}
/// Returns true if this state is a terminal state.
pub fn is_terminal(&self) -> bool {
match self {
State::CheckingForUpdates | State::InstallingUpdate(_) => false,
State::ErrorCheckingForUpdate
| State::InstallationError(_)
| State::NoUpdateAvailable
| State::InstallationDeferredByPolicy(_)
| State::WaitingForReboot(_) => true,
}
}
}
impl Event for State {
fn can_merge(&self, other: &State) -> bool {
if self == other {
return true;
}
// Merge states that have the same update info but different installation
// progress
if let State::InstallingUpdate(InstallingData { update: update0, .. }) = self {
if let State::InstallingUpdate(InstallingData { update: update1, .. }) = other {
return update0 == update1;
}
}
false
}
}
impl From<State> for fidl::State {
fn from(other: State) -> Self {
match other {
State::CheckingForUpdates => {
fidl::State::CheckingForUpdates(fidl::CheckingForUpdatesData::EMPTY)
}
State::ErrorCheckingForUpdate => {
fidl::State::ErrorCheckingForUpdate(fidl::ErrorCheckingForUpdateData::EMPTY)
}
State::NoUpdateAvailable => {
fidl::State::NoUpdateAvailable(fidl::NoUpdateAvailableData::EMPTY)
}
State::InstallationDeferredByPolicy(data) => {
fidl::State::InstallationDeferredByPolicy(data.into())
}
State::InstallingUpdate(data) => fidl::State::InstallingUpdate(data.into()),
State::WaitingForReboot(data) => fidl::State::WaitingForReboot(data.into()),
State::InstallationError(data) => fidl::State::InstallationError(data.into()),
}
}
}
impl From<fidl::State> for State {
fn from(fidl_state: fidl::State) -> Self {
match fidl_state {
fidl::State::CheckingForUpdates(_) => State::CheckingForUpdates,
fidl::State::ErrorCheckingForUpdate(_) => State::ErrorCheckingForUpdate,
fidl::State::NoUpdateAvailable(_) => State::NoUpdateAvailable,
fidl::State::InstallationDeferredByPolicy(data) => {
State::InstallationDeferredByPolicy(data.into())
}
fidl::State::InstallingUpdate(data) => State::InstallingUpdate(data.into()),
fidl::State::WaitingForReboot(data) => State::WaitingForReboot(data.into()),
fidl::State::InstallationError(data) => State::InstallationError(data.into()),
}
}
}
/// An error which can be returned when validating [`AttemptOptions`].
#[derive(Debug, Error, PartialEq, Eq)]
pub enum AttemptOptionsDecodeError {
/// The initiator field was not set.
#[error("missing field 'initiator'")]
MissingInitiator,
}
/// Wrapper type for [`fidl_fuchsia_update::AttemptOptions`] which validates the
/// options on construction and works with [`proptest`].
///
/// Use [`TryFrom`] (or [`TryInto`]) to convert the fidl type and this one, and
/// [`From`] (or [`Into`]) to convert back to the fidl type.
#[derive(Clone, Debug, PartialEq, Arbitrary)]
pub struct AttemptOptions {
pub initiator: Initiator,
}
impl Event for AttemptOptions {
fn can_merge(&self, _other: &AttemptOptions) -> bool {
true
}
}
impl TryFrom<fidl::AttemptOptions> for AttemptOptions {
type Error = AttemptOptionsDecodeError;
fn try_from(o: fidl::AttemptOptions) -> Result<Self, Self::Error> {
Ok(Self {
initiator: o.initiator.ok_or(AttemptOptionsDecodeError::MissingInitiator)?.into(),
})
}
}
impl From<AttemptOptions> for fidl::AttemptOptions {
fn from(o: AttemptOptions) -> Self {
Self { initiator: Some(o.initiator.into()), ..Self::EMPTY }
}
}
impl From<CheckOptions> for AttemptOptions {
fn from(o: CheckOptions) -> Self {
Self { initiator: o.initiator }
}
}
/// Wrapper type for [`fidl_fuchsia_update::InstallationErrorData`] which works
/// with [`proptest`].
///
/// Use [`From`] (and [`Into`]) to convert between the fidl type and this one.
#[derive(Clone, Debug, PartialEq, Arbitrary)]
pub struct InstallationErrorData {
pub update: Option<UpdateInfo>,
pub installation_progress: Option<InstallationProgress>,
}
impl From<InstallationErrorData> for fidl::InstallationErrorData {
fn from(other: InstallationErrorData) -> Self {
fidl::InstallationErrorData {
update: other.update.map(|ext| ext.into()),
installation_progress: other.installation_progress.map(|ext| ext.into()),
..fidl::InstallationErrorData::EMPTY
}
}
}
impl From<fidl::InstallationErrorData> for InstallationErrorData {
fn from(data: fidl::InstallationErrorData) -> Self {
Self {
update: data.update.map(|o| o.into()),
installation_progress: data.installation_progress.map(|o| o.into()),
}
}
}
/// Wrapper type for [`fidl_fuchsia_update::InstallationProgress`] which works
/// with [`proptest`].
///
/// Use [`From`] (and [`Into`]) to convert between the fidl type and this one.
#[derive(Clone, Debug, PartialEq, Arbitrary)]
pub struct InstallationProgress {
#[proptest(strategy = "prop::option::of(prop::num::f32::NORMAL)")]
pub fraction_completed: Option<f32>,
}
impl From<InstallationProgress> for fidl::InstallationProgress {
fn from(other: InstallationProgress) -> Self {
fidl::InstallationProgress {
fraction_completed: other.fraction_completed,
..fidl::InstallationProgress::EMPTY
}
}
}
impl From<fidl::InstallationProgress> for InstallationProgress {
fn from(progress: fidl::InstallationProgress) -> Self {
Self { fraction_completed: progress.fraction_completed }
}
}
/// Wrapper type for [`fidl_fuchsia_update::InstallingData`] which works with
/// [`proptest`].
///
/// Use [`From`] (and [`Into`]) to convert between the fidl type and this one.
#[derive(Clone, Debug, PartialEq, Arbitrary)]
pub struct InstallingData {
pub update: Option<UpdateInfo>,
pub installation_progress: Option<InstallationProgress>,
}
impl From<InstallingData> for fidl::InstallingData {
fn from(other: InstallingData) -> Self {
fidl::InstallingData {
update: other.update.map(|ext| ext.into()),
installation_progress: other.installation_progress.map(|ext| ext.into()),
..fidl::InstallingData::EMPTY
}
}
}
impl From<fidl::InstallingData> for InstallingData {
fn from(data: fidl::InstallingData) -> Self {
Self {
update: data.update.map(|o| o.into()),
installation_progress: data.installation_progress.map(|o| o.into()),
}
}
}
/// Wrapper type for [`fidl_fuchsia_update::InstallationDeferredData`] which
/// works with [`proptest`].
///
/// Use [`From`] (and [`Into`]) to convert between the fidl type and this one.
#[derive(Clone, Debug, PartialEq)]
pub struct InstallationDeferredData {
pub update: Option<UpdateInfo>,
pub deferral_reason: Option<fidl::InstallationDeferralReason>,
}
impl From<InstallationDeferredData> for fidl::InstallationDeferredData {
fn from(other: InstallationDeferredData) -> Self {
fidl::InstallationDeferredData {
update: other.update.map(|ext| ext.into()),
deferral_reason: other.deferral_reason,
..fidl::InstallationDeferredData::EMPTY
}
}
}
impl From<fidl::InstallationDeferredData> for InstallationDeferredData {
fn from(data: fidl::InstallationDeferredData) -> Self {
Self { update: data.update.map(|o| o.into()), deferral_reason: data.deferral_reason }
}
}
// Manually impl Arbitrary because fidl::InstallationDeferralReason does not
// impl Arbitrary. We could have created another wrapper, but we opted not to in
// order to guarantee the ext crate stays in sync with the FIDL for
// InstallationDeferralReason.
impl Arbitrary for InstallationDeferredData {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
any::<UpdateInfo>()
.prop_flat_map(|info| {
(
proptest::option::of(Just(info)),
proptest::option::of(Just(
fidl::InstallationDeferralReason::CurrentSystemNotCommitted,
)),
)
})
.prop_map(|(update, deferral_reason)| Self { update, deferral_reason })
.boxed()
}
}
/// Wrapper type for [`fidl_fuchsia_update::UpdateInfo`] which works with
/// [`proptest`].
///
/// Use [`From`] (and [`Into`]) to convert between the fidl type and this one.
#[derive(Clone, Debug, PartialEq, Arbitrary)]
pub struct UpdateInfo {
#[proptest(strategy = "proptest_util::random_version_available()")]
pub version_available: Option<String>,
pub download_size: Option<u64>,
}
impl From<UpdateInfo> for fidl::UpdateInfo {
fn from(other: UpdateInfo) -> Self {
fidl::UpdateInfo {
version_available: other.version_available,
download_size: other.download_size,
..fidl::UpdateInfo::EMPTY
}
}
}
impl From<fidl::UpdateInfo> for UpdateInfo {
fn from(info: fidl::UpdateInfo) -> Self {
Self { version_available: info.version_available, download_size: info.download_size }
}
}
/// Wrapper type for [`fidl_fuchsia_update::Initiator`] which works with
/// [`proptest`].
///
/// Use [`From`] (and [`Into`]) to convert between the fidl type and this one.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary)]
pub enum Initiator {
User,
Service,
}
impl From<fidl::Initiator> for Initiator {
fn from(initiator: fidl::Initiator) -> Self {
match initiator {
fidl::Initiator::User => Initiator::User,
fidl::Initiator::Service => Initiator::Service,
}
}
}
impl From<Initiator> for fidl::Initiator {
fn from(initiator: Initiator) -> Self {
match initiator {
Initiator::User => fidl::Initiator::User,
Initiator::Service => fidl::Initiator::Service,
}
}
}
/// An error which can be returned when validating [`CheckOptions`].
#[derive(Debug, Error, PartialEq, Eq)]
pub enum CheckOptionsDecodeError {
/// The initiator field was not set.
#[error("missing field 'initiator'")]
MissingInitiator,
}
/// Wrapper type for [`fidl_fuchsia_update::CheckOptions`] which validates the
/// options on construction and works with [`proptest`].
///
/// Use [`TryFrom`] (or [`TryInto`]) to convert the fidl type and this one, and
/// [`From`] (or [`Into`]) to convert back to the fidl type.
#[derive(Clone, Debug, PartialEq, Arbitrary)]
pub struct CheckOptions {
pub initiator: Initiator,
pub allow_attaching_to_existing_update_check: bool,
}
impl CheckOptions {
/// Create a `[CheckOptionsBuilder]` for constructing a new
/// `[CheckOptions]`.
pub fn builder() -> CheckOptionsBuilder {
CheckOptionsBuilder
}
}
impl TryFrom<fidl::CheckOptions> for CheckOptions {
type Error = CheckOptionsDecodeError;
fn try_from(o: fidl::CheckOptions) -> Result<Self, Self::Error> {
Ok(Self {
initiator: o.initiator.ok_or(CheckOptionsDecodeError::MissingInitiator)?.into(),
allow_attaching_to_existing_update_check: o
.allow_attaching_to_existing_update_check
.unwrap_or(false),
})
}
}
impl From<CheckOptions> for fidl::CheckOptions {
fn from(o: CheckOptions) -> Self {
Self {
initiator: Some(o.initiator.into()),
allow_attaching_to_existing_update_check: Some(
o.allow_attaching_to_existing_update_check,
),
..Self::EMPTY
}
}
}
/// A builder for constructing a new [`CheckOptions`]
#[derive(Clone, Debug, PartialEq, Default)]
pub struct CheckOptionsBuilder;
/// A builder for constructing a new [`CheckOptions`] with initiator already
/// set.
#[derive(Clone, Debug, PartialEq)]
pub struct CheckOptionsBuilderWithInitiator {
initiator: Initiator,
allow_attaching_to_existing_update_check: bool,
}
impl CheckOptionsBuilder {
/// Create a `[CheckOptionsBuilder]` for constructing a new
/// `[CheckOptions]`.
pub fn new() -> Self {
Self
}
/// Set the initiator of the update.
pub fn initiator(self, initiator: Initiator) -> CheckOptionsBuilderWithInitiator {
CheckOptionsBuilderWithInitiator {
initiator,
allow_attaching_to_existing_update_check: false,
}
}
}
impl CheckOptionsBuilderWithInitiator {
/// Set the allow_attaching_to_existing_update_check flag.
pub fn allow_attaching_to_existing_update_check(mut self, allow: bool) -> Self {
self.allow_attaching_to_existing_update_check = allow;
self
}
/// Build the [`CheckOptions`].
pub fn build(self) -> CheckOptions {
CheckOptions {
initiator: self.initiator,
allow_attaching_to_existing_update_check: self.allow_attaching_to_existing_update_check,
}
}
}
pub mod proptest_util {
use proptest::prelude::*;
prop_compose! {
/// pick an arbitrary version_available value for `UpdateInfo`
pub fn random_version_available()(
version_available in proptest::option::of("[0-9A-Z]{10,20}")
) -> Option<String> {
version_available
}
}
}
#[cfg(test)]
mod tests {
use super::*;
proptest! {
// states with the same update info but different progress should merge
#[test]
fn test_state_can_merge(
update_info: Option<UpdateInfo>,
progress0: Option<InstallationProgress>,
progress1: Option<InstallationProgress>,
) {
let event0 = State::InstallingUpdate(
InstallingData {
update: update_info.clone(),
installation_progress: progress0,
}
);
let event1 = State::InstallingUpdate(
InstallingData {
update: update_info,
installation_progress: progress1,
}
);
prop_assert!(event0.can_merge(&event1));
}
#[test]
fn test_attempt_options_can_merge(
initiator0: Initiator,
initiator1: Initiator,
) {
let attempt_options0 = AttemptOptions {
initiator: initiator0,
};
let attempt_options1 = AttemptOptions {
initiator: initiator1,
};
prop_assert!(attempt_options0.can_merge(&attempt_options1));
}
#[test]
fn test_state_roundtrips(state: State) {
let state0: State = state.clone();
let fidl_intermediate: fidl::State = state.into();
let state1: State = fidl_intermediate.into();
prop_assert_eq!(state0, state1);
}
#[test]
fn test_initiator_roundtrips(initiator: Initiator) {
prop_assert_eq!(
Initiator::from(fidl::Initiator::from(initiator.clone())),
initiator
);
}
#[test]
fn test_check_options_roundtrips(check_options: CheckOptions) {
prop_assert_eq!(
CheckOptions::try_from(fidl::CheckOptions::from(check_options.clone())),
Ok(check_options)
);
}
#[test]
fn test_check_options_initiator_required(allow_attaching_to_existing_update_check: bool) {
prop_assert_eq!(
CheckOptions::try_from(fidl::CheckOptions {
initiator: None,
allow_attaching_to_existing_update_check: Some(allow_attaching_to_existing_update_check),
..fidl::CheckOptions::EMPTY
}),
Err(CheckOptionsDecodeError::MissingInitiator)
);
}
}
#[test]
fn check_options_builder() {
assert_eq!(
CheckOptions::builder()
.initiator(Initiator::User)
.allow_attaching_to_existing_update_check(true)
.build(),
CheckOptions {
initiator: Initiator::User,
allow_attaching_to_existing_update_check: true,
}
);
}
}