use assert_cmd::assert::OutputAssertExt;
use assert_cmd::output::OutputOkExt;
use chrono::{DateTime, TimeZone, Timelike};
use speculoos::assert_that;
use speculoos::iter::ContainingIntoIterAssertions;
use speculoos::numeric::OrderedAssertions;
use speculoos::string::StrAssertions;
use std::convert::TryFrom;
use std::ffi::{OsStr, OsString};
use std::fmt::Debug;
use std::ops::RangeBounds;
use std::path::Path;
use std::sync::{Mutex, PoisonError};
use test_binary::build_test_binary;
static LOCK: Mutex<()> = Mutex::new(());
struct TempEnvVarChange {
name: OsString,
previous_value: Option<OsString>,
}
impl TempEnvVarChange {
pub fn new(name: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> Self {
let previous_value = std::env::var_os(name.as_ref());
unsafe {
std::env::set_var(name.as_ref(), value.as_ref());
}
Self {
name: OsString::from(name.as_ref()),
previous_value,
}
}
}
impl Drop for TempEnvVarChange {
fn drop(&mut self) {
if let Some(value) = self.previous_value.take() {
unsafe {
std::env::set_var(&self.name, value);
}
}
}
}
struct EraseCachedSourceTimeOnDrop;
impl Drop for EraseCachedSourceTimeOnDrop {
fn drop(&mut self) {
build_data::erase_cached_source_time();
}
}
fn epoch_time() -> i64 {
i64::try_from(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
)
.unwrap_or(i64::MAX)
}
fn exec_cargo_bin(name: &str) -> String {
let bin_path = build_test_binary(name, "testbins").unwrap();
String::from_utf8(
std::process::Command::new(bin_path)
.output()
.unwrap()
.ok()
.unwrap()
.stdout,
)
.unwrap()
}
fn expect_in<T: Debug + PartialOrd>(
value: &T,
range: impl RangeBounds<T> + Debug,
) -> Result<(), String> {
if !range.contains(value) {
return Err(format!("value `{value:?}` not in `{range:?}`"));
}
Ok(())
}
#[test]
fn escape_ascii() {
use build_data::escape_ascii;
assert_eq!("", escape_ascii(b""));
assert_eq!("abc", escape_ascii(b"abc"));
assert_eq!("\\r\\n", escape_ascii(b"\r\n"));
assert_eq!(
"\\xe2\\x82\\xac",
escape_ascii( "\u{20AC}".as_bytes())
);
assert_eq!("\\x01", escape_ascii(b"\x01"));
}
#[test]
fn exec() {
let _guard = LOCK.lock().unwrap_or_else(PoisonError::into_inner);
assert_that(&build_data::exec("nonexistent", &[]).unwrap_err().as_str())
.contains("error executing");
let err =
build_data::exec("bash", &["-c", "echo stdout1; echo stderr1 >&2; exit 1"]).unwrap_err();
assert_that(&err).contains("exit=1");
assert_that(&err).contains("stdout='stdout1\\n'");
assert_that(&err).contains("stderr='stderr1\\n'");
assert_that(&build_data::exec("bash", &["-c", "kill $$"]).unwrap_err()).contains("exit=signal");
assert_eq!(
"\\xef\\xbf\\xbd(",
&build_data::exec("bash", &["-c", "echo -e '\\xc3\\x28'"]).unwrap()
);
assert_eq!(
"hello1",
build_data::exec("bash", &["-c", "echo hello1"]).unwrap()
);
assert_eq!(
"hello1",
build_data::exec("bash", &["-c", "echo ' hello1 '"]).unwrap()
);
}
#[test]
#[allow(clippy::unreadable_literal)]
fn format_date() {
assert_eq!("2021-04-14Z", build_data::format_date(1618370707).unwrap());
}
#[test]
#[allow(clippy::unreadable_literal)]
fn format_time() {
assert_eq!("03:25:07Z", build_data::format_time(1618370707).unwrap());
}
#[test]
#[allow(clippy::unreadable_literal)]
fn format_timestamp() {
assert_eq!(
"2021-04-14T03:25:07Z",
build_data::format_timestamp(1618370707).unwrap()
);
}
#[test]
fn test_now() {
let before = u64::try_from(epoch_time()).unwrap();
let value: u64 = build_data::now();
let after = u64::try_from(epoch_time()).unwrap();
assert_eq!(value, build_data::now());
expect_in(&value, before..=after).unwrap();
}
#[test]
fn get_env() {
use std::os::unix::ffi::OsStringExt;
let _guard = LOCK.lock().unwrap_or_else(PoisonError::into_inner);
assert_eq!(None, build_data::get_env("NONEXISTENT_ENV_VAR").unwrap());
let _guard0 = TempEnvVarChange::new("TEST_GET_ENV__EMPTY", "");
assert_eq!(None, build_data::get_env("TEST_GET_ENV__EMPTY").unwrap());
let _guard1 = TempEnvVarChange::new("TEST_GET_ENV__WHITESPACE", "");
assert_eq!(
None,
build_data::get_env("TEST_GET_ENV__WHITESPACE").unwrap()
);
let _guard2 = TempEnvVarChange::new("TEST_GET_ENV__VALUE", "value1");
assert_eq!(
"value1",
&build_data::get_env("TEST_GET_ENV__VALUE").unwrap().unwrap()
);
let _guard3 = TempEnvVarChange::new("TEST_GET_ENV__TRIM", " value1 ");
assert_eq!(
"value1",
&build_data::get_env("TEST_GET_ENV__TRIM").unwrap().unwrap()
);
let non_utf8: OsString = OsString::from_vec(vec![0xC3_u8, 0x28]);
let _guard4 = TempEnvVarChange::new("TEST_GET_ENV__VAR_NON_UTF8", non_utf8);
assert_that(&build_data::get_env("TEST_GET_ENV__VAR_NON_UTF8").unwrap_err())
.contains("non-utf8");
}
#[test]
fn get_git_branch() {
let _guard = LOCK.lock().unwrap_or_else(PoisonError::into_inner);
let value: String = build_data::get_git_branch().unwrap();
let matcher: safe_regex::Matcher0<_> = safe_regex::regex!(br"[-_.+a-zA-Z0-9]+");
assert!(matcher.is_match(value.as_bytes()), "{value:?}");
assert_eq!(
format!("cargo:rustc-env=GIT_BRANCH={value}\n"),
exec_cargo_bin("test_set_git_branch")
);
}
#[test]
fn get_git_commit() {
let _guard = LOCK.lock().unwrap_or_else(PoisonError::into_inner);
let value: String = build_data::get_git_commit().unwrap();
assert!(safe_regex::regex!(br"[0-9a-f]{40}").is_match(value.as_bytes()));
assert_eq!(
format!("cargo:rustc-env=GIT_COMMIT={value}\n"),
exec_cargo_bin("test_set_git_commit")
);
}
fn set_bad_git_path() -> TempEnvVarChange {
TempEnvVarChange::new(
"PATH",
std::env::current_dir()
.unwrap()
.join("tests")
.join("git-truncated-commit"),
)
}
#[test]
fn get_git_commit_short() {
let _guard = LOCK.lock().unwrap_or_else(PoisonError::into_inner);
let bad_git_path_guard = set_bad_git_path();
let err = build_data::get_git_commit_short().unwrap_err();
assert_that(&err).contains("got malformed commit hash from git");
assert_that(&err).contains("bad1");
drop(bad_git_path_guard);
let value: String = build_data::get_git_commit_short().unwrap();
assert!(safe_regex::regex!(br"[0-9a-f]{7}").is_match(value.as_bytes()));
assert_eq!(
format!("cargo:rustc-env=GIT_COMMIT_SHORT={value}\n"),
exec_cargo_bin("test_set_git_commit_short")
);
}
#[test]
fn get_git_dirty() {
let _guard = LOCK.lock().unwrap_or_else(PoisonError::into_inner);
let value = build_data::get_git_dirty().unwrap();
assert_eq!(
format!("cargo:rustc-env=GIT_DIRTY={value}\n"),
exec_cargo_bin("test_set_git_dirty")
);
if value {
return;
}
let path = std::env::current_dir()
.unwrap()
.join("test_get_git_dirty.tmp");
std::fs::write(&path, "a").unwrap();
let value = build_data::get_git_dirty().unwrap();
std::fs::remove_file(&path).unwrap();
assert!(value);
}
#[test]
fn rerun_if_git_commit_or_branch_changed() {
let _guard = LOCK.lock().unwrap_or_else(PoisonError::into_inner);
let build_data_dirpath = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let git_dirpath = Path::new(&build_data_dirpath)
.join("../.git")
.canonicalize()
.unwrap()
.to_string_lossy()
.to_string();
assert_eq!(
format!("cargo::rerun-if-changed={git_dirpath}/index\ncargo::rerun-if-changed={git_dirpath}/logs/HEAD\n"),
exec_cargo_bin("test_rerun_if_git_commit_or_branch_changed")
);
}
#[test]
fn get_hostname() {
let _guard = LOCK.lock().unwrap_or_else(PoisonError::into_inner);
let expected_hostname = String::from_utf8(
std::process::Command::new("bash")
.arg("-lc")
.arg("echo $HOSTNAME")
.assert()
.success()
.get_output()
.stdout
.clone(),
)
.unwrap()
.trim()
.to_string();
assert_eq!(&expected_hostname, &build_data::get_hostname().unwrap());
assert_eq!(
format!("cargo:rustc-env=BUILD_HOSTNAME={expected_hostname}\n"),
exec_cargo_bin("test_set_build_hostname")
);
}
#[test]
fn get_rustc_version() {
let _guard = LOCK.lock().unwrap_or_else(PoisonError::into_inner);
let _change_guard = TempEnvVarChange::new(
"RUSTC",
Path::new(&std::env::var_os("CARGO").unwrap())
.parent()
.unwrap()
.join("rustc"),
);
let value: String = build_data::get_rustc_version().unwrap();
let matcher: safe_regex::Matcher0<_> =
safe_regex::regex!(br"rustc [0-9]+\.[0-9]+\.[0-9]+(?:-nightly|-beta)?(?: .*)?");
assert!(matcher.is_match(value.as_bytes()));
assert_eq!(
format!("cargo:rustc-env=RUSTC_VERSION={value}\n"),
exec_cargo_bin("test_set_rustc_version")
);
assert_eq!(
format!(
"cargo:rustc-env=RUSTC_VERSION_SEMVER={}\n",
build_data::parse_rustc_semver(&value).unwrap()
),
exec_cargo_bin("test_set_rustc_version_semver")
);
assert_eq!(
format!(
"cargo:rustc-env=RUST_CHANNEL={}\n",
build_data::parse_rustc_channel(&value).unwrap()
),
exec_cargo_bin("test_set_rust_channel")
);
let _change_guard = TempEnvVarChange::new("RUSTC", "");
build_data::get_rustc_version().unwrap_err();
}
#[test]
fn get_target_platform() {
let _guard = LOCK.lock().unwrap_or_else(PoisonError::into_inner);
let _change_guard = TempEnvVarChange::new("TARGET", "target1");
let value: String = build_data::get_target_platform().unwrap();
assert_eq!(value.as_str(), "target1");
}
#[test]
fn rust_channel() {
assert_eq!("stable", &format!("{}", build_data::RustChannel::Stable));
assert_eq!("beta", &format!("{}", build_data::RustChannel::Beta));
assert_eq!("nightly", &format!("{}", build_data::RustChannel::Nightly));
}
#[test]
fn parse_rustc_version() {
use build_data::RustChannel;
build_data::parse_rustc_version("").unwrap_err();
build_data::parse_rustc_version("not a rustc version").unwrap_err();
build_data::parse_rustc_version("rustc1.2.3").unwrap_err();
build_data::parse_rustc_version("other 1.2.3").unwrap_err();
build_data::parse_rustc_version("1").unwrap_err();
build_data::parse_rustc_version("1.2").unwrap_err();
build_data::parse_rustc_version("other 1..3").unwrap_err();
build_data::parse_rustc_version("1.2.3-invalid").unwrap_err();
build_data::parse_rustc_version("1.2.3x").unwrap_err();
build_data::parse_rustc_version("1.2.3-nightlyX").unwrap_err();
assert_eq!(
(String::from("1.53.0"), RustChannel::Stable),
build_data::parse_rustc_version("rustc 1.53.0 (07e0e2ec2 2021-03-24)").unwrap()
);
assert_eq!(
(String::from("1.53.0"), RustChannel::Beta),
build_data::parse_rustc_version("rustc 1.53.0-beta (07e0e2ec2 2021-03-24)").unwrap()
);
assert_eq!(
(String::from("1.53.0"), RustChannel::Nightly),
build_data::parse_rustc_version("rustc 1.53.0-nightly (07e0e2ec2 2021-03-24)").unwrap()
);
assert_eq!(
(String::from("1.53.0"), RustChannel::Stable),
build_data::parse_rustc_version("1.53.0 (07e0e2ec2 2021-03-24)").unwrap()
);
assert_eq!(
(String::from("1.53.0"), RustChannel::Stable),
build_data::parse_rustc_version("rustc 1.53.0").unwrap()
);
assert_eq!(
(String::from("1.53.0"), RustChannel::Stable),
build_data::parse_rustc_version("1.53.0").unwrap()
);
assert_eq!(
(String::from("1.53.0"), RustChannel::Nightly),
build_data::parse_rustc_version("1.53.0-nightly").unwrap()
);
}
#[test]
fn parse_rustc_semver() {
assert_eq!(
String::from("1.53.0"),
build_data::parse_rustc_semver("rustc 1.53.0 (07e0e2ec2 2021-03-24)").unwrap()
);
}
#[test]
fn parse_rustc_channel() {
assert_eq!(
build_data::RustChannel::Beta,
build_data::parse_rustc_channel("rustc 1.53.0-beta (07e0e2ec2 2021-03-24)").unwrap()
);
}
#[test]
fn set_target_platform() {
let _guard = LOCK.lock().unwrap_or_else(PoisonError::into_inner);
let _change_guard = TempEnvVarChange::new("TARGET", "target1");
assert_eq!(
"cargo:rustc-env=TARGET_PLATFORM=target1\n",
exec_cargo_bin("test_set_target_platform")
);
}
pub mod get_source_time {
use crate::*;
#[test]
fn reads_env_var() {
let _eraser = EraseCachedSourceTimeOnDrop;
let _lock_guard = LOCK.lock().unwrap_or_else(PoisonError::into_inner);
let _var_guard = TempEnvVarChange::new("SOURCE_DATE_EPOCH", "1713215656");
assert_eq!(1_713_215_656, build_data::get_source_time().unwrap());
assert_eq!(
"cargo:rustc-env=SOURCE_DATE=2024-04-15Z\n",
exec_cargo_bin("test_set_source_date")
);
assert_eq!(
"cargo:rustc-env=SOURCE_TIME=21:14:16Z\n",
exec_cargo_bin("test_set_source_time")
);
assert_eq!(
"cargo:rustc-env=SOURCE_TIMESTAMP=2024-04-15T21:14:16Z\n",
exec_cargo_bin("test_set_source_timestamp")
);
assert_eq!(
"cargo:rustc-env=SOURCE_EPOCH_TIME=1713215656\n",
exec_cargo_bin("test_set_source_epoch_time")
);
}
#[test]
fn bad_env_var() {
let _eraser = EraseCachedSourceTimeOnDrop;
let _lock_guard = LOCK.lock().unwrap_or_else(PoisonError::into_inner);
let _var_guard = TempEnvVarChange::new("SOURCE_DATE_EPOCH", "not-digits");
assert_that(&build_data::get_source_time().unwrap_err()).contains("error parsing");
}
#[test]
fn caches_value() {
let _eraser = EraseCachedSourceTimeOnDrop;
let _lock_guard = LOCK.lock().unwrap_or_else(PoisonError::into_inner);
let var_guard = TempEnvVarChange::new("SOURCE_DATE_EPOCH", "123");
assert_eq!(123, build_data::get_source_time().unwrap());
drop(var_guard);
let _var_guard = TempEnvVarChange::new("SOURCE_DATE_EPOCH", "not-digits");
assert_eq!(123, build_data::get_source_time().unwrap());
}
#[test]
fn runs_git() {
let _eraser = EraseCachedSourceTimeOnDrop;
let _lock_guard = LOCK.lock().unwrap_or_else(PoisonError::into_inner);
let _var_guard = TempEnvVarChange::new("SOURCE_DATE_EPOCH", "");
let value = build_data::get_source_time().unwrap();
assert_that(&value).is_greater_than(1_618_400_000);
assert_eq!(
format!("cargo:rustc-env=SOURCE_EPOCH_TIME={value}\n"),
exec_cargo_bin("test_set_source_epoch_time")
);
}
#[test]
fn git_error() {
let _eraser = EraseCachedSourceTimeOnDrop;
let _lock_guard = LOCK.lock().unwrap_or_else(PoisonError::into_inner);
let _var_guard = TempEnvVarChange::new("SOURCE_DATE_EPOCH", "");
let _git_guard = set_bad_git_path();
let err = build_data::get_source_time().unwrap_err();
assert_that(&err).contains("failed parsing");
assert_that(&err).contains("git");
}
}
#[test]
fn no_debug_rebuilds_debug() {
let _guard = LOCK.lock().unwrap_or_else(PoisonError::into_inner);
let _var_guard = TempEnvVarChange::new("PROFILE", "debug");
assert_eq!(
"cargo:rerun-if-env-changed=PROFILE\n",
exec_cargo_bin("test_no_debug_rebuilds")
);
}
#[test]
fn no_debug_rebuilds_release() {
let _guard = LOCK.lock().unwrap_or_else(PoisonError::into_inner);
let _var_guard = TempEnvVarChange::new("PROFILE", "release");
assert_eq!("", exec_cargo_bin("test_no_debug_rebuilds"));
}
#[test]
fn set_build_date() {
let _guard = LOCK.lock().unwrap_or_else(PoisonError::into_inner);
let before = chrono::Utc
.timestamp_opt(epoch_time(), 0)
.unwrap()
.date_naive();
let stdout = exec_cargo_bin("test_set_build_date");
let after = chrono::Utc
.timestamp_opt(epoch_time(), 0)
.unwrap()
.date_naive();
let value =
chrono::NaiveDate::parse_from_str(&stdout, "cargo:rustc-env=BUILD_DATE=%Y-%m-%dZ\n")
.map_err(|e| {
format!(
"error parsing output '{}': {e}",
build_data::escape_ascii(&stdout),
)
})
.unwrap();
assert_that(&[before, after]).contains(value);
}
#[test]
fn set_build_time() {
let _guard = LOCK.lock().unwrap_or_else(PoisonError::into_inner);
let before = epoch_time();
let stdout = exec_cargo_bin("test_set_build_time");
let after = epoch_time();
let time = chrono::NaiveTime::parse_from_str(&stdout, "cargo:rustc-env=BUILD_TIME=%H:%M:%SZ\n")
.map_err(|e| {
format!(
"error parsing output '{}': {e}",
build_data::escape_ascii(&stdout),
)
})
.unwrap();
let value = chrono::Utc
.timestamp_opt(if time.hour() == 0 { after } else { before }, 0)
.unwrap()
.date_naive()
.and_time(time)
.and_utc()
.timestamp();
expect_in(&value, before..=after).unwrap();
}
#[test]
fn set_build_timestamp() {
let _guard = LOCK.lock().unwrap_or_else(PoisonError::into_inner);
let before = epoch_time();
let stdout = exec_cargo_bin("test_set_build_timestamp");
let after = epoch_time();
let value = DateTime::parse_from_str(&stdout, "cargo:rustc-env=BUILD_TIMESTAMP=%+\n")
.map_err(|e| {
format!(
"error parsing output '{}': {e}",
build_data::escape_ascii(&stdout),
)
})
.unwrap()
.timestamp();
expect_in(&value, before..=after).unwrap();
}
#[test]
fn set_build_epoch_time() {
let _guard = LOCK.lock().unwrap_or_else(PoisonError::into_inner);
let before = epoch_time();
let stdout = exec_cargo_bin("test_set_build_epoch_time");
let after = epoch_time();
let value = DateTime::parse_from_str(&stdout, "cargo:rustc-env=BUILD_EPOCH_TIME=%s\n")
.map_err(|e| {
format!(
"error parsing output '{}': {e}",
build_data::escape_ascii(&stdout),
)
})
.unwrap()
.timestamp();
expect_in(&value, before..=after).unwrap();
}