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