use std::path::PathBuf;
use std::process::Command;
use bashkit::Bash;
fn gnu_date() -> Option<PathBuf> {
for candidate in ["gdate", "date"] {
let Ok(output) = Command::new(candidate).arg("--version").output() else {
continue;
};
if output.status.success()
&& String::from_utf8_lossy(&output.stdout).contains("GNU coreutils")
{
return Some(PathBuf::from(candidate));
}
}
None
}
async fn assert_gnu_match(tz: &str, args: &[&str]) {
let Some(program) = gnu_date() else {
eprintln!("skip: GNU date is not installed");
return;
};
let host = Command::new(program)
.args(args)
.env("LC_ALL", "C")
.env("TZ", tz)
.output()
.expect("run GNU date");
let command = std::iter::once("date".to_string())
.chain(args.iter().map(|arg| shell_quote(arg)))
.collect::<Vec<_>>()
.join(" ");
let mut bash = Bash::builder().env("TZ", tz).build();
let actual = bash.exec(&command).await.unwrap();
assert_eq!(
actual.exit_code,
host.status.code().unwrap_or(-1),
"{command}"
);
assert_eq!(actual.stdout.as_bytes(), host.stdout, "{command}");
}
fn shell_quote(value: &str) -> String {
format!("'{}'", value.replace('\'', "'\\''"))
}
#[tokio::test]
async fn matches_gnu_date_for_timezone_sensitive_inputs() {
for (tz, args) in [
(
"America/Chicago",
&["-d", "@1710057600", "+%F %T %Z %z"] as &[&str],
),
(
"America/Chicago",
&["-d", "2024-01-15 10:40:00", "+%s %F %T %Z %z"],
),
(
"America/Chicago",
&["-d", "2024-01-15T12:00:00+02:00", "+%s %T %Z %z"],
),
(
"America/Chicago",
&["-u", "-d", "2024-01-15T10:40:00-06:00", "+%s %T %Z %z"],
),
(
"America/Chicago",
&["-d", "2024-07-15 10:40", "+%s %F %T %Z %z"],
),
(
"America/Chicago",
&["-d", "2024-07-15 10:40:00 UTC", "+%s %F %T %Z %z"],
),
(
"America/Chicago",
&["-d", "2024-07-15 10:40:00.5 +0200", "+%s %N %F %T %Z %z"],
),
("Etc/GMT+6", &["-d", "@0", "+%F %T %Z %z"]),
("UTC", &["-d", "@0", "+%C %G %g %u %V %3N %6N %N"]),
] {
assert_gnu_match(tz, args).await;
}
}
#[tokio::test]
async fn matches_gnu_date_for_weekdays() {
let Some(program) = gnu_date() else {
eprintln!("skip: GNU date is not installed");
return;
};
for tz in ["UTC", "America/Chicago", "Australia/Sydney"] {
for input in [
"monday",
"this Mon",
"next tuesday",
"last SUN",
"thursday",
"next Thursday",
"last thursday",
"this fri",
"saturday",
"next blursday",
"last blah",
] {
let reference = Command::new(&program)
.args(["+%s %F"])
.env("TZ", tz)
.output()
.unwrap();
assert!(reference.status.success());
let reference = String::from_utf8(reference.stdout).unwrap();
let (epoch, day) = reference.trim().split_once(' ').unwrap();
let host = Command::new(&program)
.args(["-d", input, "+%s %F %T %z"])
.env("TZ", tz)
.env("LC_ALL", "C")
.output()
.unwrap();
let after = Command::new(&program)
.arg("+%F")
.env("TZ", tz)
.output()
.unwrap();
if after.stdout != format!("{day}\n").as_bytes() {
eprintln!("skip: local midnight crossed during GNU weekday comparison");
continue;
}
let mut bash = Bash::builder()
.fixed_epoch(epoch.parse().unwrap())
.env("TZ", tz)
.build();
let result = bash
.exec(&format!("date -d {} '+%s %F %T %z'", shell_quote(input)))
.await
.unwrap();
assert_eq!(
result.exit_code,
host.status.code().unwrap_or(-1),
"TZ={tz} {input}"
);
assert_eq!(result.stdout.as_bytes(), host.stdout, "TZ={tz} {input}");
}
}
}