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