blob: c4c36a41ed92910378b364e89c3e94c5c2aac737 [file] [log] [blame]
// Copyright 2019 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.
//! The omaha_client::common module contains those types that are common to many parts of the
//! library. Many of these don't belong to a specific sub-module.
use crate::{
protocol::{self, request::InstallSource, Cohort},
storage::Storage,
time::PartialComplexTime,
};
use log::error;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::time::Duration;
use version::Version;
/// Omaha has historically supported multiple methods of counting devices. Currently, the
/// only recommended method is the Client Regulated - Date method.
///
/// See https://github.com/google/omaha/blob/HEAD/doc/ServerProtocolV3.md#client-regulated-counting-date-based
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum UserCounting {
ClientRegulatedByDate(
/// Date (sent by the server) of the last contact with Omaha.
Option<i32>,
),
}
/// Helper implementation to bridge from the protocol to the internal representation for tracking
/// the data for client-regulated user counting.
impl From<Option<protocol::response::DayStart>> for UserCounting {
fn from(opt_day_start: Option<protocol::response::DayStart>) -> Self {
match opt_day_start {
Some(day_start) => UserCounting::ClientRegulatedByDate(day_start.elapsed_days),
None => UserCounting::ClientRegulatedByDate(None),
}
}
}
/// The App struct holds information about an application to perform an update check for.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct App {
/// This is the app_id that Omaha uses to identify a given application.
pub id: String,
/// This is the current version of the application.
pub version: Version,
/// This is the fingerprint for the application package.
///
/// See https://github.com/google/omaha/blob/HEAD/doc/ServerProtocolV3.md#packages--fingerprints
pub fingerprint: Option<String>,
/// The app's current cohort information (cohort id, hint, etc). This is both provided to Omaha
/// as well as returned by Omaha.
pub cohort: Cohort,
/// The app's current user-counting information. This is both provided to Omaha as well as
/// returned by Omaha.
pub user_counting: UserCounting,
/// Extra fields to include in requests to Omaha. The client library does not inspect or
/// operate on these, it just sends them to the service as part of the "app" objects in each
/// request.
pub extra_fields: HashMap<String, String>,
}
/// Structure used to serialize per app data to be persisted.
/// Be careful when making changes to this struct to keep backward compatibility.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct PersistedApp {
pub cohort: Cohort,
pub user_counting: UserCounting,
}
impl From<&App> for PersistedApp {
fn from(app: &App) -> Self {
PersistedApp { cohort: app.cohort.clone(), user_counting: app.user_counting.clone() }
}
}
impl App {
/// Start cnstructing an App.
pub fn builder<I: Into<String>, V: Into<Version>>(id: I, version: V) -> AppBuilder {
AppBuilder {
id: id.into(),
version: version.into(),
fingerprint: None,
cohort: None,
user_counting: None,
extra_fields: HashMap::new(),
}
}
}
/// Builder for an App, enforces minimim requirements are met.
pub struct AppBuilder {
id: String,
version: Version,
fingerprint: Option<String>,
cohort: Option<Cohort>,
user_counting: Option<UserCounting>,
extra_fields: HashMap<String, String>,
}
impl AppBuilder {
/// Add a cohort for the app.
pub fn with_cohort(mut self, cohort: Cohort) -> Self {
self.cohort = Some(cohort);
self
}
/// Add a fingerprint to the constructed app.
pub fn with_fingerprint(mut self, fingerprint: impl Into<String>) -> Self {
self.fingerprint = Some(fingerprint.into());
self
}
/// Specify the user-counting mechanism to use
pub fn with_user_counting(mut self, user_counting: UserCounting) -> Self {
self.user_counting = Some(user_counting);
self
}
/// Add additional fields for including in the update check requests.
///
/// # Use with caution
///
/// This can overwrite fields used by the protocol itself.
pub fn with_extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.extra_fields.insert(key.into(), value.into());
self
}
pub fn build(self) -> App {
App {
id: self.id,
version: self.version,
fingerprint: self.fingerprint,
cohort: self.cohort.unwrap_or_default(),
user_counting: self.user_counting.unwrap_or(UserCounting::ClientRegulatedByDate(None)),
extra_fields: self.extra_fields,
}
}
}
impl App {
/// Load data from |storage|, only overwrite existing fields if data exists.
pub async fn load<'a>(&'a mut self, storage: &'a impl Storage) {
if let Some(app_json) = storage.get_string(&self.id).await {
match serde_json::from_str::<PersistedApp>(&app_json) {
Ok(persisted_app) => {
// Do not overwrite existing fields in app.
if self.cohort.id == None {
self.cohort.id = persisted_app.cohort.id;
}
if self.cohort.hint == None {
self.cohort.hint = persisted_app.cohort.hint;
}
if self.cohort.name == None {
self.cohort.name = persisted_app.cohort.name;
}
if self.user_counting == UserCounting::ClientRegulatedByDate(None) {
self.user_counting = persisted_app.user_counting;
}
}
Err(e) => {
error!("Unable to deserialize PersistedApp from json {}: {}", app_json, e);
}
}
}
}
/// Persist cohort and user counting to |storage|, will try to set all of them to storage even
/// if previous set fails.
/// It will NOT call commit() on |storage|, caller is responsible to call commit().
pub async fn persist<'a>(&'a self, storage: &'a mut impl Storage) {
let persisted_app = PersistedApp::from(self);
match serde_json::to_string(&persisted_app) {
Ok(json) => {
if let Err(e) = storage.set_string(&self.id, &json).await {
error!("Unable to persist cohort id: {}", e);
}
}
Err(e) => {
error!("Unable to serialize PersistedApp {:?}: {}", persisted_app, e);
}
}
}
/// Get the current channel name from cohort name, returns empty string if no cohort name set
/// for the app.
pub fn get_current_channel(&self) -> &str {
self.cohort.name.as_deref().unwrap_or("")
}
/// Get the target channel name from cohort hint, fallback to current channel if no hint.
pub fn get_target_channel(&self) -> &str {
self.cohort.hint.as_deref().unwrap_or_else(|| self.get_current_channel())
}
/// Set the cohort hint to |channel|.
pub fn set_target_channel(&mut self, channel: Option<String>, id: Option<String>) {
self.cohort.hint = channel;
if let Some(id) = id {
self.id = id;
}
}
pub fn valid(&self) -> bool {
!self.id.is_empty() && self.version != Version::from([0])
}
}
/// Options controlling a single update check
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CheckOptions {
/// Was this check initiated by a person that's waiting for an answer?
/// This is used to ignore the background poll rate, and to be aggressive about
/// failing fast, so as not to hang on not receiving a response.
pub source: InstallSource,
}
/// This describes the data around the scheduling of update checks
#[derive(Clone, Copy, Default, PartialEq)]
pub struct UpdateCheckSchedule {
// TODO(fxb/64804): Theoretically last_update_time and last_update_check_time
// do not need to coexist and we can do all the reporting we want via
// last_update_time. However, the last update check metric doesn't (as currently
// worded) match up with what last_update_time actually records.
/// When the last update check was attempted (start time of the check process).
pub last_update_time: Option<PartialComplexTime>,
/// When the last update check was attempted.
pub last_update_check_time: Option<PartialComplexTime>,
/// When the next update should happen.
pub next_update_time: Option<CheckTiming>,
}
/// The fields used to describe the timing of the next update check.
///
/// This exists as a separate type mostly so that it can be moved around atomically, in a little bit
/// neater fashion than it could be if it was a tuple of `(PartialComplexTime, Option<Duration>)`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CheckTiming {
/// The upper time bounds on when it should be performed (expressed as along those timelines
/// that are valid based on currently known time quality).
pub time: PartialComplexTime,
/// The minimum wait until the next check, regardless of the wall or monotonic time it should be
/// performed at. This is handled separately as it creates a lower bound vs. the upper bound(s)
/// that the `time` field provides.
pub minimum_wait: Option<Duration>,
}
impl UpdateCheckSchedule {
pub fn builder() -> ScheduleBuilder {
ScheduleBuilder::default()
}
}
impl CheckTiming {
pub fn builder() -> CheckTimingBuilder {
CheckTimingBuilder::default()
}
}
/// This is a builder for the UpdateCheckSchedule.
#[derive(Clone, Debug, Default)]
pub struct ScheduleBuilder {
last_time: Option<PartialComplexTime>,
last_check_time: Option<PartialComplexTime>,
next_timing: Option<CheckTiming>,
}
impl ScheduleBuilder {
/// Set the last_update_time for the UpdateCheckSchedule that's to be built.
/// This method takes both ComplexTime and Option<ComplexTime>.
pub fn last_time(mut self, last_update_time: impl Into<Option<PartialComplexTime>>) -> Self {
self.last_time = last_update_time.into();
self
}
/// Set the last_check_time for the under-construction UpdateCheckSchedule.
/// This method takes both ComplexTime and Option<ComplexTime>.
pub fn last_check_time(
mut self,
last_check_time: impl Into<Option<PartialComplexTime>>,
) -> Self {
self.last_check_time = last_check_time.into();
self
}
/// Set the CheckTiming for the next update check to use.
pub fn next_timing(mut self, next_timing: impl Into<Option<CheckTiming>>) -> Self {
self.next_timing = next_timing.into();
self
}
/// Build the UpdateCheckSchedule.
pub fn build(self) -> UpdateCheckSchedule {
UpdateCheckSchedule {
last_update_time: self.last_time,
last_update_check_time: self.last_check_time,
next_update_time: self.next_timing,
}
}
}
/// This is a builder for the `CheckTiming` struct.
///
/// It uses a type-state to ensure that the time of the check has been set before allowing the
/// construction of the `CheckTiming` itself.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct CheckTimingBuilder;
/// This is the internal type-state for ensuring the time has been set.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CheckTimingBuilderWithTime {
time: PartialComplexTime,
minimum_wait: Option<Duration>,
}
impl CheckTimingBuilder {
/// The time of the next check must be set in order to construct the `CheckTiming` type.
pub fn time(self, time: impl Into<PartialComplexTime>) -> CheckTimingBuilderWithTime {
CheckTimingBuilderWithTime { time: time.into(), minimum_wait: None }
}
}
impl CheckTimingBuilderWithTime {
/// Set the minimum wait until the next check.
pub fn minimum_wait(mut self, minimum_wait: impl Into<Option<Duration>>) -> Self {
self.minimum_wait = minimum_wait.into();
self
}
/// Build the CheckTiming.
pub fn build(self) -> CheckTiming {
CheckTiming { time: self.time, minimum_wait: self.minimum_wait }
}
}
/// Helper struct that provides a nicer format for Debug printing `Option` by dropping the
/// `Some(...)` that wraps its value, and instead uses the Display trait implementation of the
/// value.
///
/// Examples:
/// `"MyStruct { option_string_field: None }"`
/// `"MyStruct { option_string_field: "string field value" }"`
///
pub struct PrettyOptionDisplay<T>(pub Option<T>)
where
T: fmt::Display;
impl<T> fmt::Display for PrettyOptionDisplay<T>
where
T: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.0 {
None => write!(f, "None"),
Some(value) => fmt::Display::fmt(value, f),
}
}
}
impl<T> fmt::Debug for PrettyOptionDisplay<T>
where
T: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
/// The default Debug implementation for SystemTime will only print seconds since unix epoch, which
/// is not terribly useful in logs, so this prints a more human-relatable format.
///
/// e.g.
/// `UpdateCheckSchedule { last_update_time: None, next_uptime_time: None }`
/// `UpdateCheckSchedule { last_update_time: "2001-07-08 16:34:56.026 UTC (994518299.026420000)", next_uptime_time: None }`
impl fmt::Debug for UpdateCheckSchedule {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("UpdateCheckSchedule")
.field("last_update_time", &PrettyOptionDisplay(self.last_update_time))
.field("next_update_time", &PrettyOptionDisplay(self.next_update_time))
.finish()
}
}
impl fmt::Display for CheckTiming {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.minimum_wait {
None => fmt::Display::fmt(&self.time, f),
Some(wait) => write!(f, "{} wait: {:?}", &self.time, &wait),
}
}
}
/// These hold the data maintained request-to-request so that the requirements for
/// backoffs, throttling, proxy use, etc. can all be properly maintained. This is
/// NOT the state machine's internal state.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ProtocolState {
/// If the server has dictated the next poll interval, this holds what that
/// interval is.
pub server_dictated_poll_interval: Option<std::time::Duration>,
/// The number of consecutive failed update checks. Used to perform backoffs.
pub consecutive_failed_update_checks: u32,
/// The number of consecutive proxied requests. Used to periodically not use
/// proxies, in the case of an invalid proxy configuration.
pub consecutive_proxied_requests: u32,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
storage::MemStorage,
time::{MockTimeSource, TimeSource},
};
use futures::executor::block_on;
use pretty_assertions::assert_eq;
use std::str::FromStr;
use std::time::SystemTime;
#[test]
fn test_app_new_version() {
let app =
App::builder("some_id", [1, 2]).with_cohort(Cohort::from_hint("some-channel")).build();
assert_eq!(app.id, "some_id");
assert_eq!(app.version, [1, 2].into());
assert_eq!(app.fingerprint, None);
assert_eq!(app.cohort.hint, Some("some-channel".to_string()));
assert_eq!(app.cohort.name, None);
assert_eq!(app.cohort.id, None);
assert_eq!(app.user_counting, UserCounting::ClientRegulatedByDate(None));
assert!(app.extra_fields.is_empty(), "Extra fields are not empty");
}
#[test]
fn test_app_with_fingerprint() {
let app = App::builder("some_id_2", [4, 6])
.with_cohort(Cohort::from_hint("test-channel"))
.with_fingerprint("some_fp")
.build();
assert_eq!(app.id, "some_id_2");
assert_eq!(app.version, [4, 6].into());
assert_eq!(app.fingerprint, Some("some_fp".to_string()));
assert_eq!(app.cohort.hint, Some("test-channel".to_string()));
assert_eq!(app.cohort.name, None);
assert_eq!(app.cohort.id, None);
assert_eq!(app.user_counting, UserCounting::ClientRegulatedByDate(None));
assert!(app.extra_fields.is_empty(), "Extra fields are not empty");
}
#[test]
fn test_app_with_user_counting() {
let app = App::builder("some_id_2", [4, 6])
.with_cohort(Cohort::from_hint("test-channel"))
.with_user_counting(UserCounting::ClientRegulatedByDate(Some(42)))
.build();
assert_eq!(app.id, "some_id_2");
assert_eq!(app.version, [4, 6].into());
assert_eq!(app.cohort.hint, Some("test-channel".to_string()));
assert_eq!(app.cohort.name, None);
assert_eq!(app.cohort.id, None);
assert_eq!(app.user_counting, UserCounting::ClientRegulatedByDate(Some(42)));
assert!(app.extra_fields.is_empty(), "Extra fields are not empty");
}
#[test]
fn test_app_with_extras() {
let app = App::builder("some_id_2", [4, 6])
.with_cohort(Cohort::from_hint("test-channel"))
.with_extra("key1", "value1")
.with_extra("key2", "value2")
.build();
assert_eq!(app.id, "some_id_2");
assert_eq!(app.version, [4, 6].into());
assert_eq!(app.cohort.hint, Some("test-channel".to_string()));
assert_eq!(app.cohort.name, None);
assert_eq!(app.cohort.id, None);
assert_eq!(app.user_counting, UserCounting::ClientRegulatedByDate(None));
assert_eq!(app.extra_fields.len(), 2);
assert_eq!(app.extra_fields["key1"], "value1");
assert_eq!(app.extra_fields["key2"], "value2");
}
#[test]
fn test_app_load() {
block_on(async {
let mut storage = MemStorage::new();
let json = serde_json::json!({
"cohort": {
"cohort": "some_id",
"cohorthint":"some_hint",
"cohortname": "some_name"
},
"user_counting": {
"ClientRegulatedByDate":123
}});
let json = serde_json::to_string(&json).unwrap();
let mut app = App::builder("some_id", [1, 2]).build();
storage.set_string(&app.id, &json).await.unwrap();
app.load(&storage).await;
let cohort = Cohort {
id: Some("some_id".to_string()),
hint: Some("some_hint".to_string()),
name: Some("some_name".to_string()),
};
assert_eq!(cohort, app.cohort);
assert_eq!(UserCounting::ClientRegulatedByDate(Some(123)), app.user_counting);
});
}
#[test]
fn test_app_load_empty_storage() {
block_on(async {
let storage = MemStorage::new();
let cohort = Cohort {
id: Some("some_id".to_string()),
hint: Some("some_hint".to_string()),
name: Some("some_name".to_string()),
};
let mut app = App::builder("some_id", [1, 2])
.with_cohort(cohort)
.with_user_counting(UserCounting::ClientRegulatedByDate(Some(123)))
.build();
app.load(&storage).await;
// existing data not overwritten
let cohort = Cohort {
id: Some("some_id".to_string()),
hint: Some("some_hint".to_string()),
name: Some("some_name".to_string()),
};
assert_eq!(cohort, app.cohort);
assert_eq!(UserCounting::ClientRegulatedByDate(Some(123)), app.user_counting);
});
}
#[test]
fn test_app_load_malformed() {
block_on(async {
let mut storage = MemStorage::new();
let cohort = Cohort {
id: Some("some_id".to_string()),
hint: Some("some_hint".to_string()),
name: Some("some_name".to_string()),
};
let mut app = App::builder("some_id", [1, 2])
.with_cohort(cohort)
.with_user_counting(UserCounting::ClientRegulatedByDate(Some(123)))
.build();
storage.set_string(&app.id, "not a json").await.unwrap();
app.load(&storage).await;
// existing data not overwritten
let cohort = Cohort {
id: Some("some_id".to_string()),
hint: Some("some_hint".to_string()),
name: Some("some_name".to_string()),
};
assert_eq!(cohort, app.cohort);
assert_eq!(UserCounting::ClientRegulatedByDate(Some(123)), app.user_counting);
});
}
#[test]
fn test_app_load_partial() {
block_on(async {
let mut storage = MemStorage::new();
let json = serde_json::json!({
"cohort": {
"cohorthint":"some_hint_2",
"cohortname": "some_name_2"
},
"user_counting": {
"ClientRegulatedByDate":null
}});
let json = serde_json::to_string(&json).unwrap();
let cohort = Cohort {
id: Some("some_id".to_string()),
hint: Some("some_hint".to_string()),
name: Some("some_name".to_string()),
};
let mut app = App::builder("some_id", [1, 2])
.with_cohort(cohort)
.with_user_counting(UserCounting::ClientRegulatedByDate(Some(123)))
.build();
storage.set_string(&app.id, &json).await.unwrap();
app.load(&storage).await;
// existing data not overwritten
let cohort = Cohort {
id: Some("some_id".to_string()),
hint: Some("some_hint".to_string()),
name: Some("some_name".to_string()),
};
assert_eq!(cohort, app.cohort);
assert_eq!(UserCounting::ClientRegulatedByDate(Some(123)), app.user_counting);
});
}
#[test]
fn test_app_load_override() {
block_on(async {
let mut storage = MemStorage::new();
let json = serde_json::json!({
"cohort": {
"cohort": "some_id_2",
"cohorthint":"some_hint_2",
"cohortname": "some_name_2"
},
"user_counting": {
"ClientRegulatedByDate":123
}});
let json = serde_json::to_string(&json).unwrap();
let cohort = Cohort {
id: Some("some_id".to_string()),
hint: Some("some_hint".to_string()),
name: None,
};
let mut app = App::builder("some_id", [1, 2])
.with_cohort(cohort)
.with_user_counting(UserCounting::ClientRegulatedByDate(Some(123)))
.build();
storage.set_string(&app.id, &json).await.unwrap();
app.load(&storage).await;
// existing data not overwritten
let cohort = Cohort {
id: Some("some_id".to_string()),
hint: Some("some_hint".to_string()),
name: Some("some_name_2".to_string()),
};
assert_eq!(cohort, app.cohort);
assert_eq!(UserCounting::ClientRegulatedByDate(Some(123)), app.user_counting);
});
}
#[test]
fn test_app_persist() {
block_on(async {
let mut storage = MemStorage::new();
let cohort = Cohort {
id: Some("some_id".to_string()),
hint: Some("some_hint".to_string()),
name: Some("some_name".to_string()),
};
let app = App::builder("some_id", [1, 2])
.with_cohort(cohort)
.with_user_counting(UserCounting::ClientRegulatedByDate(Some(123)))
.build();
app.persist(&mut storage).await;
let expected = serde_json::json!({
"cohort": {
"cohort": "some_id",
"cohorthint":"some_hint",
"cohortname": "some_name"
},
"user_counting": {
"ClientRegulatedByDate":123
}});
let json = storage.get_string(&app.id).await.unwrap();
assert_eq!(expected, serde_json::Value::from_str(&json).unwrap());
assert_eq!(false, storage.committed());
});
}
#[test]
fn test_app_persist_empty() {
block_on(async {
let mut storage = MemStorage::new();
let cohort = Cohort { id: None, hint: None, name: None };
let app = App::builder("some_id", [1, 2]).with_cohort(cohort).build();
app.persist(&mut storage).await;
let expected = serde_json::json!({
"cohort": {},
"user_counting": {
"ClientRegulatedByDate":null
}});
let json = storage.get_string(&app.id).await.unwrap();
assert_eq!(expected, serde_json::Value::from_str(&json).unwrap());
assert_eq!(false, storage.committed());
});
}
#[test]
fn test_app_get_current_channel() {
let cohort = Cohort { name: Some("current-channel-123".to_string()), ..Cohort::default() };
let app = App::builder("some_id", [0, 1]).with_cohort(cohort).build();
assert_eq!("current-channel-123", app.get_current_channel());
}
#[test]
fn test_app_get_current_channel_default() {
let app = App::builder("some_id", [0, 1]).build();
assert_eq!("", app.get_current_channel());
}
#[test]
fn test_app_get_target_channel() {
let cohort = Cohort::from_hint("target-channel-456");
let app = App::builder("some_id", [0, 1]).with_cohort(cohort).build();
assert_eq!("target-channel-456", app.get_target_channel());
}
#[test]
fn test_app_get_target_channel_fallback() {
let cohort = Cohort { name: Some("current-channel-123".to_string()), ..Cohort::default() };
let app = App::builder("some_id", [0, 1]).with_cohort(cohort).build();
assert_eq!("current-channel-123", app.get_target_channel());
}
#[test]
fn test_app_get_target_channel_default() {
let app = App::builder("some_id", [0, 1]).build();
assert_eq!("", app.get_target_channel());
}
#[test]
fn test_app_set_target_channel() {
let mut app = App::builder("some_id", [0, 1]).build();
assert_eq!("", app.get_target_channel());
app.set_target_channel(Some("new-target-channel".to_string()), None);
assert_eq!("new-target-channel", app.get_target_channel());
app.set_target_channel(None, None);
assert_eq!("", app.get_target_channel());
}
#[test]
fn test_app_set_target_channel_and_id() {
let mut app = App::builder("some_id", [0, 1]).build();
assert_eq!("", app.get_target_channel());
app.set_target_channel(Some("new-target-channel".to_string()), Some("new-id".to_string()));
assert_eq!("new-target-channel", app.get_target_channel());
assert_eq!("new-id", app.id);
app.set_target_channel(None, None);
assert_eq!("", app.get_target_channel());
assert_eq!("new-id", app.id);
}
#[test]
fn test_app_valid() {
let app = App::builder("some_id", [0, 1]).build();
assert!(app.valid());
}
#[test]
fn test_app_not_valid() {
let app = App::builder("", [0, 1]).build();
assert!(!app.valid());
let app = App::builder("some_id", [0]).build();
assert!(!app.valid());
}
#[test]
fn test_pretty_option_display_with_none() {
assert_eq!("None", format!("{:?}", PrettyOptionDisplay(Option::<String>::None)));
}
#[test]
fn test_pretty_option_display_with_some() {
assert_eq!("this is a test", format!("{:?}", PrettyOptionDisplay(Some("this is a test"))));
}
#[test]
fn test_update_check_schedule_debug_with_defaults() {
assert_eq!(
"UpdateCheckSchedule { \
last_update_time: None, \
next_update_time: None \
}",
format!("{:?}", UpdateCheckSchedule::default())
);
}
#[test]
fn test_update_check_schedule_debug_with_values() {
let mock_time = MockTimeSource::new_from_now();
let last = mock_time.now();
let next = last + Duration::from_secs(1000);
assert_eq!(
format!(
"UpdateCheckSchedule {{ last_update_time: {}, next_update_time: {} }}",
PartialComplexTime::from(last),
next
),
format!(
"{:?}",
UpdateCheckSchedule::builder()
.last_time(last)
.next_timing(CheckTiming::builder().time(next).build())
.build()
)
);
}
#[test]
fn test_update_check_schedule_builder_all_fields() {
let mock_time = MockTimeSource::new_from_now();
let now = PartialComplexTime::from(mock_time.now());
assert_eq!(
UpdateCheckSchedule::builder()
.last_time(PartialComplexTime::from(
SystemTime::UNIX_EPOCH + Duration::from_secs(100000)
))
.next_timing(
CheckTiming::builder().time(now).minimum_wait(Duration::from_secs(100)).build()
)
.build(),
UpdateCheckSchedule {
last_update_time: Some(PartialComplexTime::from(
SystemTime::UNIX_EPOCH + Duration::from_secs(100000)
)),
next_update_time: Some(CheckTiming {
time: now,
minimum_wait: Some(Duration::from_secs(100))
}),
..Default::default()
}
);
}
#[test]
fn test_update_check_schedule_builder_all_fields_from_options() {
let next_time = PartialComplexTime::from(MockTimeSource::new_from_now().now());
assert_eq!(
UpdateCheckSchedule::builder()
.last_time(Some(PartialComplexTime::from(
SystemTime::UNIX_EPOCH + Duration::from_secs(100000)
)))
.next_timing(Some(
CheckTiming::builder()
.time(next_time)
.minimum_wait(Some(Duration::from_secs(100)))
.build()
))
.build(),
UpdateCheckSchedule {
last_update_time: Some(PartialComplexTime::from(
SystemTime::UNIX_EPOCH + Duration::from_secs(100000)
)),
next_update_time: Some(CheckTiming {
time: next_time,
minimum_wait: Some(Duration::from_secs(100))
}),
..Default::default()
}
);
}
#[test]
fn test_update_check_schedule_builder_subset_fields() {
assert_eq!(
UpdateCheckSchedule::builder()
.last_time(PartialComplexTime::from(
SystemTime::UNIX_EPOCH + Duration::from_secs(100000)
))
.build(),
UpdateCheckSchedule {
last_update_time: Some(PartialComplexTime::from(
SystemTime::UNIX_EPOCH + Duration::from_secs(100000)
)),
..Default::default()
}
);
let next_time = PartialComplexTime::from(MockTimeSource::new_from_now().now());
assert_eq!(
UpdateCheckSchedule::builder()
.next_timing(
CheckTiming::builder()
.time(next_time)
.minimum_wait(Duration::from_secs(5))
.build()
)
.build(),
UpdateCheckSchedule {
next_update_time: Some(CheckTiming {
time: next_time,
minimum_wait: Some(Duration::from_secs(5))
}),
..Default::default()
}
);
}
#[test]
fn test_update_check_schedule_builder_defaults_are_same_as_default_impl() {
assert_eq!(
UpdateCheckSchedule::builder().build(),
UpdateCheckSchedule { ..Default::default() }
);
}
}