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