bashkit 0.20.1

Awesomely fast virtual sandbox with bash and file system
Documentation
//! Portable differential checks against GNU `date` when available.

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",
        ] {
            // Freeze Bashkit at GNU's reference instant. If the host crosses
            // local midnight during the pair, that case has no stable oracle.
            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}");
        }
    }
}