• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 #![cfg(all(unix, feature = "clock", feature = "std"))]
2 
3 use std::{path, process, thread};
4 
5 #[cfg(target_os = "linux")]
6 use chrono::Days;
7 use chrono::{Datelike, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Timelike};
8 
verify_against_date_command_local(path: &'static str, dt: NaiveDateTime)9 fn verify_against_date_command_local(path: &'static str, dt: NaiveDateTime) {
10     let output = process::Command::new(path)
11         .arg("-d")
12         .arg(format!("{}-{:02}-{:02} {:02}:05:01", dt.year(), dt.month(), dt.day(), dt.hour()))
13         .arg("+%Y-%m-%d %H:%M:%S %:z")
14         .output()
15         .unwrap();
16 
17     let date_command_str = String::from_utf8(output.stdout).unwrap();
18 
19     // The below would be preferred. At this stage neither earliest() or latest()
20     // seems to be consistent with the output of the `date` command, so we simply
21     // compare both.
22     // let local = Local
23     //     .with_ymd_and_hms(year, month, day, hour, 5, 1)
24     //     // looks like the "date" command always returns a given time when it is ambiguous
25     //     .earliest();
26 
27     // if let Some(local) = local {
28     //     assert_eq!(format!("{}\n", local), date_command_str);
29     // } else {
30     //     // we are in a "Spring forward gap" due to DST, and so date also returns ""
31     //     assert_eq!("", date_command_str);
32     // }
33 
34     // This is used while a decision is made whether the `date` output needs to
35     // be exactly matched, or whether MappedLocalTime::Ambiguous should be handled
36     // differently
37 
38     let date = NaiveDate::from_ymd_opt(dt.year(), dt.month(), dt.day()).unwrap();
39     match Local.from_local_datetime(&date.and_hms_opt(dt.hour(), 5, 1).unwrap()) {
40         chrono::MappedLocalTime::Ambiguous(a, b) => assert!(
41             format!("{}\n", a) == date_command_str || format!("{}\n", b) == date_command_str
42         ),
43         chrono::MappedLocalTime::Single(a) => {
44             assert_eq!(format!("{}\n", a), date_command_str);
45         }
46         chrono::MappedLocalTime::None => {
47             assert_eq!("", date_command_str);
48         }
49     }
50 }
51 
52 /// path to Unix `date` command. Should work on most Linux and Unixes. Not the
53 /// path for MacOS (/bin/date) which uses a different version of `date` with
54 /// different arguments (so it won't run which is okay).
55 /// for testing only
56 #[allow(dead_code)]
57 #[cfg(not(target_os = "aix"))]
58 const DATE_PATH: &str = "/usr/bin/date";
59 #[allow(dead_code)]
60 #[cfg(target_os = "aix")]
61 const DATE_PATH: &str = "/opt/freeware/bin/date";
62 
63 #[cfg(test)]
64 /// test helper to sanity check the date command behaves as expected
65 /// asserts the command succeeded
assert_run_date_version()66 fn assert_run_date_version() {
67     // note environment variable `LANG`
68     match std::env::var_os("LANG") {
69         Some(lang) => eprintln!("LANG: {:?}", lang),
70         None => eprintln!("LANG not set"),
71     }
72     let out = process::Command::new(DATE_PATH).arg("--version").output().unwrap();
73     let stdout = String::from_utf8(out.stdout).unwrap();
74     let stderr = String::from_utf8(out.stderr).unwrap();
75     // note the `date` binary version
76     eprintln!("command: {:?} --version\nstdout: {:?}\nstderr: {:?}", DATE_PATH, stdout, stderr);
77     assert!(out.status.success(), "command failed: {:?} --version", DATE_PATH);
78 }
79 
80 #[test]
try_verify_against_date_command()81 fn try_verify_against_date_command() {
82     if !path::Path::new(DATE_PATH).exists() {
83         eprintln!("date command {:?} not found, skipping", DATE_PATH);
84         return;
85     }
86     assert_run_date_version();
87 
88     eprintln!(
89         "Run command {:?} for every hour from 1975 to 2077, skipping some years...",
90         DATE_PATH,
91     );
92 
93     let mut children = vec![];
94     for year in [1975, 1976, 1977, 2020, 2021, 2022, 2073, 2074, 2075, 2076, 2077].iter() {
95         children.push(thread::spawn(|| {
96             let mut date = NaiveDate::from_ymd_opt(*year, 1, 1).unwrap().and_time(NaiveTime::MIN);
97             let end = NaiveDate::from_ymd_opt(*year + 1, 1, 1).unwrap().and_time(NaiveTime::MIN);
98             while date <= end {
99                 verify_against_date_command_local(DATE_PATH, date);
100                 date += chrono::TimeDelta::try_hours(1).unwrap();
101             }
102         }));
103     }
104     for child in children {
105         // Wait for the thread to finish. Returns a result.
106         let _ = child.join();
107     }
108 }
109 
110 #[cfg(target_os = "linux")]
verify_against_date_command_format_local(path: &'static str, dt: NaiveDateTime)111 fn verify_against_date_command_format_local(path: &'static str, dt: NaiveDateTime) {
112     let required_format =
113         "d%d D%D F%F H%H I%I j%j k%k l%l m%m M%M S%S T%T u%u U%U w%w W%W X%X y%y Y%Y z%:z";
114     // a%a - depends from localization
115     // A%A - depends from localization
116     // b%b - depends from localization
117     // B%B - depends from localization
118     // h%h - depends from localization
119     // c%c - depends from localization
120     // p%p - depends from localization
121     // r%r - depends from localization
122     // x%x - fails, date is dd/mm/yyyy, chrono is dd/mm/yy, same as %D
123     // Z%Z - too many ways to represent it, will most likely fail
124 
125     let output = process::Command::new(path)
126         .env("LANG", "c")
127         .env("LC_ALL", "c")
128         .arg("-d")
129         .arg(format!(
130             "{}-{:02}-{:02} {:02}:{:02}:{:02}",
131             dt.year(),
132             dt.month(),
133             dt.day(),
134             dt.hour(),
135             dt.minute(),
136             dt.second()
137         ))
138         .arg(format!("+{}", required_format))
139         .output()
140         .unwrap();
141 
142     let date_command_str = String::from_utf8(output.stdout).unwrap();
143     let date = NaiveDate::from_ymd_opt(dt.year(), dt.month(), dt.day()).unwrap();
144     let ldt = Local
145         .from_local_datetime(&date.and_hms_opt(dt.hour(), dt.minute(), dt.second()).unwrap())
146         .unwrap();
147     let formatted_date = format!("{}\n", ldt.format(required_format));
148     assert_eq!(date_command_str, formatted_date);
149 }
150 
151 #[test]
152 #[cfg(target_os = "linux")]
try_verify_against_date_command_format()153 fn try_verify_against_date_command_format() {
154     if !path::Path::new(DATE_PATH).exists() {
155         eprintln!("date command {:?} not found, skipping", DATE_PATH);
156         return;
157     }
158     assert_run_date_version();
159 
160     let mut date = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap().and_hms_opt(12, 11, 13).unwrap();
161     while date.year() < 2008 {
162         verify_against_date_command_format_local(DATE_PATH, date);
163         date = date + Days::new(55);
164     }
165 }
166