Skip to main content

omaha_client/
common.rs

1// Copyright 2019 The Fuchsia Authors
2//
3// Licensed under a BSD-style license <LICENSE-BSD>, Apache License, Version 2.0
4// <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
5// license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
6// This file may not be copied, modified, or distributed except according to
7// those terms.
8
9//! The omaha_client::common module contains those types that are common to many parts of the
10//! library.  Many of these don't belong to a specific sub-module.
11
12use crate::{
13    protocol::{self, Cohort, request::InstallSource},
14    storage::Storage,
15    time::PartialComplexTime,
16    version::Version,
17};
18use log::error;
19use serde::{Deserialize, Serialize};
20use std::collections::HashMap;
21use std::fmt;
22use std::time::Duration;
23use typed_builder::TypedBuilder;
24
25/// Omaha has historically supported multiple methods of counting devices.  Currently, the
26/// only recommended method is the Client Regulated - Date method.
27///
28/// See https://github.com/google/omaha/blob/HEAD/doc/ServerProtocolV3.md#client-regulated-counting-date-based
29#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
30pub enum UserCounting {
31    ClientRegulatedByDate(
32        /// Date (sent by the server) of the last contact with Omaha.
33        Option<u32>,
34    ),
35}
36
37/// Helper implementation to bridge from the protocol to the internal representation for tracking
38/// the data for client-regulated user counting.
39impl From<Option<protocol::response::DayStart>> for UserCounting {
40    fn from(opt_day_start: Option<protocol::response::DayStart>) -> Self {
41        match opt_day_start {
42            Some(day_start) => UserCounting::ClientRegulatedByDate(day_start.elapsed_days),
43            None => UserCounting::ClientRegulatedByDate(None),
44        }
45    }
46}
47
48/// The App struct holds information about an application to perform an update check for.
49#[derive(Clone, Debug, Eq, PartialEq, TypedBuilder)]
50pub struct App {
51    /// This is the app_id that Omaha uses to identify a given application.
52    #[builder(setter(into))]
53    pub id: String,
54
55    /// This is the current version of the application.
56    #[builder(setter(into))]
57    pub version: Version,
58
59    /// This is the fingerprint for the application package.
60    ///
61    /// See https://github.com/google/omaha/blob/HEAD/doc/ServerProtocolV3.md#packages--fingerprints
62    #[builder(default)]
63    #[builder(setter(into, strip_option))]
64    pub fingerprint: Option<String>,
65
66    /// The app's current cohort information (cohort id, hint, etc).  This is both provided to Omaha
67    /// as well as returned by Omaha.
68    #[builder(default)]
69    pub cohort: Cohort,
70
71    /// The app's current user-counting information.  This is both provided to Omaha as well as
72    /// returned by Omaha.
73    #[builder(default=UserCounting::ClientRegulatedByDate(None))]
74    pub user_counting: UserCounting,
75
76    /// Extra fields to include in requests to Omaha.  The client library does not inspect or
77    /// operate on these, it just sends them to the service as part of the "app" objects in each
78    /// request.
79    #[builder(default)]
80    #[builder(setter(into))]
81    pub extra_fields: HashMap<String, String>,
82}
83
84/// Structure used to serialize per app data to be persisted.
85/// Be careful when making changes to this struct to keep backward compatibility.
86#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
87pub struct PersistedApp {
88    pub cohort: Cohort,
89    pub user_counting: UserCounting,
90}
91
92impl From<&App> for PersistedApp {
93    fn from(app: &App) -> Self {
94        PersistedApp { cohort: app.cohort.clone(), user_counting: app.user_counting.clone() }
95    }
96}
97
98impl App {
99    /// Load data from |storage|, only overwrite existing fields if data exists.
100    pub async fn load<'a>(&'a mut self, storage: &'a impl Storage) {
101        if let Some(app_json) = storage.get_string(&self.id).await {
102            match serde_json::from_str::<PersistedApp>(&app_json) {
103                Ok(persisted_app) => {
104                    // Do not overwrite existing fields in app.
105                    if self.cohort.id.is_none() {
106                        self.cohort.id = persisted_app.cohort.id;
107                    }
108                    if self.cohort.hint.is_none() {
109                        self.cohort.hint = persisted_app.cohort.hint;
110                    }
111                    if self.cohort.name.is_none() {
112                        self.cohort.name = persisted_app.cohort.name;
113                    }
114                    if self.user_counting == UserCounting::ClientRegulatedByDate(None) {
115                        self.user_counting = persisted_app.user_counting;
116                    }
117                }
118                Err(e) => {
119                    error!("Unable to deserialize PersistedApp from json {}: {}", app_json, e);
120                }
121            }
122        }
123    }
124
125    /// Persist cohort and user counting to |storage|, will try to set all of them to storage even
126    /// if previous set fails.
127    /// It will NOT call commit() on |storage|, caller is responsible to call commit().
128    pub async fn persist<'a>(&'a self, storage: &'a mut impl Storage) {
129        let persisted_app = PersistedApp::from(self);
130        match serde_json::to_string(&persisted_app) {
131            Ok(json) => {
132                if let Err(e) = storage.set_string(&self.id, &json).await {
133                    error!("Unable to persist cohort id: {}", e);
134                }
135            }
136            Err(e) => {
137                error!("Unable to serialize PersistedApp {:?}: {}", persisted_app, e);
138            }
139        }
140    }
141
142    /// Get the current channel name from cohort name, returns empty string if no cohort name set
143    /// for the app.
144    pub fn get_current_channel(&self) -> &str {
145        self.cohort.name.as_deref().unwrap_or("")
146    }
147
148    /// Get the target channel name from cohort hint, fallback to current channel if no hint.
149    pub fn get_target_channel(&self) -> &str {
150        self.cohort.hint.as_deref().unwrap_or_else(|| self.get_current_channel())
151    }
152
153    /// Set the cohort hint to |channel|.
154    pub fn set_target_channel(&mut self, channel: Option<String>, id: Option<String>) {
155        self.cohort.hint = channel;
156        if let Some(id) = id {
157            self.id = id;
158        }
159    }
160
161    pub fn valid(&self) -> bool {
162        !self.id.is_empty() && self.version != Version::from([0])
163    }
164}
165
166/// Options controlling a single update check
167#[derive(Clone, Debug, Default, PartialEq, Eq)]
168pub struct CheckOptions {
169    /// Was this check initiated by a person that's waiting for an answer?
170    ///  This is used to ignore the background poll rate, and to be aggressive about
171    ///  failing fast, so as not to hang on not receiving a response.
172    pub source: InstallSource,
173}
174
175/// This describes the data around the scheduling of update checks
176#[derive(Clone, Copy, Default, PartialEq, Eq, TypedBuilder)]
177pub struct UpdateCheckSchedule {
178    // TODO(https://fxbug.dev/42143450): Theoretically last_update_time and last_update_check_time
179    // do not need to coexist and we can do all the reporting we want via
180    // last_update_time. However, the last update check metric doesn't (as currently
181    // worded) match up with what last_update_time actually records.
182    /// When the last update check was attempted (start time of the check process).
183    #[builder(default, setter(into))]
184    pub last_update_time: Option<PartialComplexTime>,
185
186    /// When the last update check was attempted.
187    #[builder(default, setter(into))]
188    pub last_update_check_time: Option<PartialComplexTime>,
189
190    /// When the next update should happen.
191    #[builder(default, setter(into))]
192    pub next_update_time: Option<CheckTiming>,
193}
194
195/// The fields used to describe the timing of the next update check.
196///
197/// This exists as a separate type mostly so that it can be moved around atomically, in a little bit
198/// neater fashion than it could be if it was a tuple of `(PartialComplexTime, Option<Duration>)`.
199#[derive(Clone, Copy, Debug, PartialEq, Eq, TypedBuilder)]
200pub struct CheckTiming {
201    /// The upper time bounds on when it should be performed (expressed as along those timelines
202    /// that are valid based on currently known time quality).
203    #[builder(setter(into))]
204    pub time: PartialComplexTime,
205
206    /// The minimum wait until the next check, regardless of the wall or monotonic time it should be
207    /// performed at.  This is handled separately as it creates a lower bound vs. the upper bound(s)
208    /// that the `time` field provides.
209    #[builder(default, setter(strip_option))]
210    pub minimum_wait: Option<Duration>,
211}
212
213/// Helper struct that provides a nicer format for Debug printing `Option` by dropping the
214/// `Some(...)` that wraps its value, and instead uses the Display trait implementation of the
215/// value.
216///
217/// Examples:
218/// `"MyStruct { option_string_field: None }"`
219/// `"MyStruct { option_string_field: "string field value" }"`
220///
221pub struct PrettyOptionDisplay<T>(pub Option<T>)
222where
223    T: fmt::Display;
224impl<T> fmt::Display for PrettyOptionDisplay<T>
225where
226    T: fmt::Display,
227{
228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        match &self.0 {
230            None => write!(f, "None"),
231            Some(value) => fmt::Display::fmt(value, f),
232        }
233    }
234}
235impl<T> fmt::Debug for PrettyOptionDisplay<T>
236where
237    T: fmt::Display,
238{
239    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240        fmt::Display::fmt(self, f)
241    }
242}
243
244/// The default Debug implementation for SystemTime will only print seconds since unix epoch, which
245/// is not terribly useful in logs, so this prints a more human-relatable format.
246///
247/// e.g.
248/// `UpdateCheckSchedule { last_update_time: None, next_uptime_time: None }`
249/// `UpdateCheckSchedule { last_update_time: "2001-07-08 16:34:56.026 UTC (994518299.026420000)", next_uptime_time: None }`
250impl fmt::Debug for UpdateCheckSchedule {
251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252        f.debug_struct("UpdateCheckSchedule")
253            .field("last_update_time", &PrettyOptionDisplay(self.last_update_time))
254            .field("next_update_time", &PrettyOptionDisplay(self.next_update_time))
255            .finish()
256    }
257}
258
259impl fmt::Display for CheckTiming {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        match self.minimum_wait {
262            None => fmt::Display::fmt(&self.time, f),
263            Some(wait) => write!(f, "{} wait: {:?}", self.time, wait),
264        }
265    }
266}
267
268/// These hold the data maintained request-to-request so that the requirements for
269/// backoffs, throttling, proxy use, etc. can all be properly maintained.  This is
270/// NOT the state machine's internal state.
271#[derive(Clone, Debug, Default, Eq, PartialEq)]
272pub struct ProtocolState {
273    /// If the server has dictated the next poll interval, this holds what that
274    /// interval is.
275    pub server_dictated_poll_interval: Option<std::time::Duration>,
276
277    /// The number of consecutive failed update checks.  Used to perform backoffs.
278    pub consecutive_failed_update_checks: u32,
279
280    /// The number of consecutive proxied requests.  Used to periodically not use
281    /// proxies, in the case of an invalid proxy configuration.
282    pub consecutive_proxied_requests: u32,
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    use crate::{
289        storage::MemStorage,
290        time::{MockTimeSource, TimeSource},
291    };
292    use futures::executor::block_on;
293    use pretty_assertions::assert_eq;
294    use std::str::FromStr;
295    use std::time::SystemTime;
296
297    #[test]
298    fn test_app_new_version() {
299        let app = App::builder()
300            .id("some_id")
301            .version([1, 2])
302            .cohort(Cohort::from_hint("some-channel"))
303            .build();
304        assert_eq!(app.id, "some_id");
305        assert_eq!(app.version, [1, 2].into());
306        assert_eq!(app.fingerprint, None);
307        assert_eq!(app.cohort.hint, Some("some-channel".to_string()));
308        assert_eq!(app.cohort.name, None);
309        assert_eq!(app.cohort.id, None);
310        assert_eq!(app.user_counting, UserCounting::ClientRegulatedByDate(None));
311        assert!(app.extra_fields.is_empty(), "Extra fields are not empty");
312    }
313
314    #[test]
315    fn test_app_with_fingerprint() {
316        let app = App::builder()
317            .id("some_id_2")
318            .version([4, 6])
319            .cohort(Cohort::from_hint("test-channel"))
320            .fingerprint("some_fp")
321            .build();
322        assert_eq!(app.id, "some_id_2");
323        assert_eq!(app.version, [4, 6].into());
324        assert_eq!(app.fingerprint, Some("some_fp".to_string()));
325        assert_eq!(app.cohort.hint, Some("test-channel".to_string()));
326        assert_eq!(app.cohort.name, None);
327        assert_eq!(app.cohort.id, None);
328        assert_eq!(app.user_counting, UserCounting::ClientRegulatedByDate(None));
329        assert!(app.extra_fields.is_empty(), "Extra fields are not empty");
330    }
331
332    #[test]
333    fn test_app_with_user_counting() {
334        let app = App::builder()
335            .id("some_id_2")
336            .version([4, 6])
337            .cohort(Cohort::from_hint("test-channel"))
338            .user_counting(UserCounting::ClientRegulatedByDate(Some(42)))
339            .build();
340        assert_eq!(app.id, "some_id_2");
341        assert_eq!(app.version, [4, 6].into());
342        assert_eq!(app.cohort.hint, Some("test-channel".to_string()));
343        assert_eq!(app.cohort.name, None);
344        assert_eq!(app.cohort.id, None);
345        assert_eq!(app.user_counting, UserCounting::ClientRegulatedByDate(Some(42)));
346        assert!(app.extra_fields.is_empty(), "Extra fields are not empty");
347    }
348
349    #[test]
350    fn test_app_with_extras() {
351        let app = App::builder()
352            .id("some_id_2")
353            .version([4, 6])
354            .cohort(Cohort::from_hint("test-channel"))
355            .extra_fields([
356                ("key1".to_string(), "value1".to_string()),
357                ("key2".to_string(), "value2".to_string()),
358            ])
359            .build();
360        assert_eq!(app.id, "some_id_2");
361        assert_eq!(app.version, [4, 6].into());
362        assert_eq!(app.cohort.hint, Some("test-channel".to_string()));
363        assert_eq!(app.cohort.name, None);
364        assert_eq!(app.cohort.id, None);
365        assert_eq!(app.user_counting, UserCounting::ClientRegulatedByDate(None));
366        assert_eq!(app.extra_fields.len(), 2);
367        assert_eq!(app.extra_fields["key1"], "value1");
368        assert_eq!(app.extra_fields["key2"], "value2");
369    }
370
371    #[test]
372    fn test_app_load() {
373        block_on(async {
374            let mut storage = MemStorage::new();
375            let json = serde_json::json!({
376            "cohort": {
377                "cohort": "some_id",
378                "cohorthint":"some_hint",
379                "cohortname": "some_name"
380            },
381            "user_counting": {
382                "ClientRegulatedByDate":123
383            }});
384            let json = serde_json::to_string(&json).unwrap();
385            let mut app = App::builder().id("some_id").version([1, 2]).build();
386            storage.set_string(&app.id, &json).await.unwrap();
387            app.load(&storage).await;
388
389            let cohort = Cohort {
390                id: Some("some_id".to_string()),
391                hint: Some("some_hint".to_string()),
392                name: Some("some_name".to_string()),
393            };
394            assert_eq!(cohort, app.cohort);
395            assert_eq!(UserCounting::ClientRegulatedByDate(Some(123)), app.user_counting);
396        });
397    }
398
399    #[test]
400    fn test_app_load_empty_storage() {
401        block_on(async {
402            let storage = MemStorage::new();
403            let cohort = Cohort {
404                id: Some("some_id".to_string()),
405                hint: Some("some_hint".to_string()),
406                name: Some("some_name".to_string()),
407            };
408            let mut app = App::builder()
409                .id("some_id")
410                .version([1, 2])
411                .cohort(cohort)
412                .user_counting(UserCounting::ClientRegulatedByDate(Some(123)))
413                .build();
414            app.load(&storage).await;
415
416            // existing data not overwritten
417            let cohort = Cohort {
418                id: Some("some_id".to_string()),
419                hint: Some("some_hint".to_string()),
420                name: Some("some_name".to_string()),
421            };
422            assert_eq!(cohort, app.cohort);
423            assert_eq!(UserCounting::ClientRegulatedByDate(Some(123)), app.user_counting);
424        });
425    }
426
427    #[test]
428    fn test_app_load_malformed() {
429        block_on(async {
430            let mut storage = MemStorage::new();
431            let cohort = Cohort {
432                id: Some("some_id".to_string()),
433                hint: Some("some_hint".to_string()),
434                name: Some("some_name".to_string()),
435            };
436            let mut app = App::builder()
437                .id("some_id")
438                .version([1, 2])
439                .cohort(cohort)
440                .user_counting(UserCounting::ClientRegulatedByDate(Some(123)))
441                .build();
442            storage.set_string(&app.id, "not a json").await.unwrap();
443            app.load(&storage).await;
444
445            // existing data not overwritten
446            let cohort = Cohort {
447                id: Some("some_id".to_string()),
448                hint: Some("some_hint".to_string()),
449                name: Some("some_name".to_string()),
450            };
451            assert_eq!(cohort, app.cohort);
452            assert_eq!(UserCounting::ClientRegulatedByDate(Some(123)), app.user_counting);
453        });
454    }
455
456    #[test]
457    fn test_app_load_partial() {
458        block_on(async {
459            let mut storage = MemStorage::new();
460            let json = serde_json::json!({
461            "cohort": {
462                "cohorthint":"some_hint_2",
463                "cohortname": "some_name_2"
464            },
465            "user_counting": {
466                "ClientRegulatedByDate":null
467            }});
468            let json = serde_json::to_string(&json).unwrap();
469            let cohort = Cohort {
470                id: Some("some_id".to_string()),
471                hint: Some("some_hint".to_string()),
472                name: Some("some_name".to_string()),
473            };
474            let mut app = App::builder()
475                .id("some_id")
476                .version([1, 2])
477                .cohort(cohort)
478                .user_counting(UserCounting::ClientRegulatedByDate(Some(123)))
479                .build();
480            storage.set_string(&app.id, &json).await.unwrap();
481            app.load(&storage).await;
482
483            // existing data not overwritten
484            let cohort = Cohort {
485                id: Some("some_id".to_string()),
486                hint: Some("some_hint".to_string()),
487                name: Some("some_name".to_string()),
488            };
489            assert_eq!(cohort, app.cohort);
490            assert_eq!(UserCounting::ClientRegulatedByDate(Some(123)), app.user_counting);
491        });
492    }
493
494    #[test]
495    fn test_app_load_override() {
496        block_on(async {
497            let mut storage = MemStorage::new();
498            let json = serde_json::json!({
499            "cohort": {
500                "cohort": "some_id_2",
501                "cohorthint":"some_hint_2",
502                "cohortname": "some_name_2"
503            },
504            "user_counting": {
505                "ClientRegulatedByDate":123
506            }});
507            let json = serde_json::to_string(&json).unwrap();
508            let cohort = Cohort {
509                id: Some("some_id".to_string()),
510                hint: Some("some_hint".to_string()),
511                name: None,
512            };
513            let mut app = App::builder()
514                .id("some_id")
515                .version([1, 2])
516                .cohort(cohort)
517                .user_counting(UserCounting::ClientRegulatedByDate(Some(123)))
518                .build();
519            storage.set_string(&app.id, &json).await.unwrap();
520            app.load(&storage).await;
521
522            // existing data not overwritten
523            let cohort = Cohort {
524                id: Some("some_id".to_string()),
525                hint: Some("some_hint".to_string()),
526                name: Some("some_name_2".to_string()),
527            };
528            assert_eq!(cohort, app.cohort);
529            assert_eq!(UserCounting::ClientRegulatedByDate(Some(123)), app.user_counting);
530        });
531    }
532
533    #[test]
534    fn test_app_persist() {
535        block_on(async {
536            let mut storage = MemStorage::new();
537            let cohort = Cohort {
538                id: Some("some_id".to_string()),
539                hint: Some("some_hint".to_string()),
540                name: Some("some_name".to_string()),
541            };
542            let app = App::builder()
543                .id("some_id")
544                .version([1, 2])
545                .cohort(cohort)
546                .user_counting(UserCounting::ClientRegulatedByDate(Some(123)))
547                .build();
548            app.persist(&mut storage).await;
549
550            let expected = serde_json::json!({
551            "cohort": {
552                "cohort": "some_id",
553                "cohorthint":"some_hint",
554                "cohortname": "some_name"
555            },
556            "user_counting": {
557                "ClientRegulatedByDate":123
558            }});
559            let json = storage.get_string(&app.id).await.unwrap();
560            assert_eq!(expected, serde_json::Value::from_str(&json).unwrap());
561            assert!(!storage.committed());
562        });
563    }
564
565    #[test]
566    fn test_app_persist_empty() {
567        block_on(async {
568            let mut storage = MemStorage::new();
569            let cohort = Cohort { id: None, hint: None, name: None };
570            let app = App::builder().id("some_id").version([1, 2]).cohort(cohort).build();
571            app.persist(&mut storage).await;
572
573            let expected = serde_json::json!({
574            "cohort": {},
575            "user_counting": {
576                "ClientRegulatedByDate":null
577            }});
578            let json = storage.get_string(&app.id).await.unwrap();
579            assert_eq!(expected, serde_json::Value::from_str(&json).unwrap());
580            assert!(!storage.committed());
581        });
582    }
583
584    #[test]
585    fn test_app_get_current_channel() {
586        let cohort = Cohort { name: Some("current-channel-123".to_string()), ..Cohort::default() };
587        let app = App::builder().id("some_id").version([0, 1]).cohort(cohort).build();
588        assert_eq!("current-channel-123", app.get_current_channel());
589    }
590
591    #[test]
592    fn test_app_get_current_channel_default() {
593        let app = App::builder().id("some_id").version([0, 1]).build();
594        assert_eq!("", app.get_current_channel());
595    }
596
597    #[test]
598    fn test_app_get_target_channel() {
599        let cohort = Cohort::from_hint("target-channel-456");
600        let app = App::builder().id("some_id").version([0, 1]).cohort(cohort).build();
601        assert_eq!("target-channel-456", app.get_target_channel());
602    }
603
604    #[test]
605    fn test_app_get_target_channel_fallback() {
606        let cohort = Cohort { name: Some("current-channel-123".to_string()), ..Cohort::default() };
607        let app = App::builder().id("some_id").version([0, 1]).cohort(cohort).build();
608        assert_eq!("current-channel-123", app.get_target_channel());
609    }
610
611    #[test]
612    fn test_app_get_target_channel_default() {
613        let app = App::builder().id("some_id").version([0, 1]).build();
614        assert_eq!("", app.get_target_channel());
615    }
616
617    #[test]
618    fn test_app_set_target_channel() {
619        let mut app = App::builder().id("some_id").version([0, 1]).build();
620        assert_eq!("", app.get_target_channel());
621        app.set_target_channel(Some("new-target-channel".to_string()), None);
622        assert_eq!("new-target-channel", app.get_target_channel());
623        app.set_target_channel(None, None);
624        assert_eq!("", app.get_target_channel());
625    }
626
627    #[test]
628    fn test_app_set_target_channel_and_id() {
629        let mut app = App::builder().id("some_id").version([0, 1]).build();
630        assert_eq!("", app.get_target_channel());
631        app.set_target_channel(Some("new-target-channel".to_string()), Some("new-id".to_string()));
632        assert_eq!("new-target-channel", app.get_target_channel());
633        assert_eq!("new-id", app.id);
634        app.set_target_channel(None, None);
635        assert_eq!("", app.get_target_channel());
636        assert_eq!("new-id", app.id);
637    }
638
639    #[test]
640    fn test_app_valid() {
641        let app = App::builder().id("some_id").version([0, 1]).build();
642        assert!(app.valid());
643    }
644
645    #[test]
646    fn test_app_not_valid() {
647        let app = App::builder().id("").version([0, 1]).build();
648        assert!(!app.valid());
649        let app = App::builder().id("some_id").version([0]).build();
650        assert!(!app.valid());
651    }
652
653    #[test]
654    fn test_pretty_option_display_with_none() {
655        assert_eq!("None", format!("{:?}", PrettyOptionDisplay(Option::<String>::None)));
656    }
657
658    #[test]
659    fn test_pretty_option_display_with_some() {
660        assert_eq!("this is a test", format!("{:?}", PrettyOptionDisplay(Some("this is a test"))));
661    }
662
663    #[test]
664    fn test_update_check_schedule_debug_with_defaults() {
665        assert_eq!(
666            "UpdateCheckSchedule { \
667                last_update_time: None, \
668                next_update_time: None \
669            }",
670            format!("{:?}", UpdateCheckSchedule::default())
671        );
672    }
673
674    #[test]
675    fn test_update_check_schedule_debug_with_values() {
676        let mock_time = MockTimeSource::new_from_now();
677        let last = mock_time.now();
678        let next = last + Duration::from_secs(1000);
679        assert_eq!(
680            format!(
681                "UpdateCheckSchedule {{ last_update_time: {}, next_update_time: {} }}",
682                PartialComplexTime::from(last),
683                next
684            ),
685            format!(
686                "{:?}",
687                UpdateCheckSchedule::builder()
688                    .last_update_time(last)
689                    .next_update_time(CheckTiming::builder().time(next).build())
690                    .build()
691            )
692        );
693    }
694
695    #[test]
696    fn test_update_check_schedule_builder_all_fields() {
697        let mock_time = MockTimeSource::new_from_now();
698        let now = PartialComplexTime::from(mock_time.now());
699        assert_eq!(
700            UpdateCheckSchedule::builder()
701                .last_update_time(PartialComplexTime::from(
702                    SystemTime::UNIX_EPOCH + Duration::from_secs(100000)
703                ))
704                .next_update_time(
705                    CheckTiming::builder().time(now).minimum_wait(Duration::from_secs(100)).build()
706                )
707                .build(),
708            UpdateCheckSchedule {
709                last_update_time: Some(PartialComplexTime::from(
710                    SystemTime::UNIX_EPOCH + Duration::from_secs(100000)
711                )),
712                next_update_time: Some(CheckTiming {
713                    time: now,
714                    minimum_wait: Some(Duration::from_secs(100))
715                }),
716                ..Default::default()
717            }
718        );
719    }
720
721    #[test]
722    fn test_update_check_schedule_builder_all_fields_from_options() {
723        let next_time = PartialComplexTime::from(MockTimeSource::new_from_now().now());
724        assert_eq!(
725            UpdateCheckSchedule::builder()
726                .last_update_time(Some(PartialComplexTime::from(
727                    SystemTime::UNIX_EPOCH + Duration::from_secs(100000)
728                )))
729                .next_update_time(Some(
730                    CheckTiming::builder()
731                        .time(next_time)
732                        .minimum_wait(Duration::from_secs(100))
733                        .build()
734                ))
735                .build(),
736            UpdateCheckSchedule {
737                last_update_time: Some(PartialComplexTime::from(
738                    SystemTime::UNIX_EPOCH + Duration::from_secs(100000)
739                )),
740                next_update_time: Some(CheckTiming {
741                    time: next_time,
742                    minimum_wait: Some(Duration::from_secs(100))
743                }),
744                ..Default::default()
745            }
746        );
747    }
748
749    #[test]
750    fn test_update_check_schedule_builder_subset_fields() {
751        assert_eq!(
752            UpdateCheckSchedule::builder()
753                .last_update_time(PartialComplexTime::from(
754                    SystemTime::UNIX_EPOCH + Duration::from_secs(100000)
755                ))
756                .build(),
757            UpdateCheckSchedule {
758                last_update_time: Some(PartialComplexTime::from(
759                    SystemTime::UNIX_EPOCH + Duration::from_secs(100000)
760                )),
761                ..Default::default()
762            }
763        );
764
765        let next_time = PartialComplexTime::from(MockTimeSource::new_from_now().now());
766        assert_eq!(
767            UpdateCheckSchedule::builder()
768                .next_update_time(
769                    CheckTiming::builder()
770                        .time(next_time)
771                        .minimum_wait(Duration::from_secs(5))
772                        .build()
773                )
774                .build(),
775            UpdateCheckSchedule {
776                next_update_time: Some(CheckTiming {
777                    time: next_time,
778                    minimum_wait: Some(Duration::from_secs(5))
779                }),
780                ..Default::default()
781            }
782        );
783    }
784
785    #[test]
786    fn test_update_check_schedule_builder_defaults_are_same_as_default_impl() {
787        assert_eq!(UpdateCheckSchedule::builder().build(), UpdateCheckSchedule::default());
788    }
789}