use assert_cmd::assert::OutputAssertExt; use assert_cmd::cargo::CommandCargoExt; use assert_cmd::output::OutputOkExt; use chrono::{DateTime, TimeZone, Timelike}; use core::time::Duration; 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; static LOCK: safe_lock::SafeLock = safe_lock::SafeLock::new(); struct TempEnvVarChange { name: OsString, previous_value: Option, } impl TempEnvVarChange { pub fn new(name: impl AsRef, value: impl AsRef) -> Self { let previous_value = std::env::var_os(name.as_ref()); 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() { std::env::set_var(&self.name, value); } } } 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 { String::from_utf8( std::process::Command::cargo_bin(name) .unwrap() .output() .unwrap() .ok() .unwrap() .stdout, ) .unwrap() } fn expect_in( value: &T, range: impl RangeBounds + 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(/* Euro sign */ "\u{20AC}".as_bytes()) ); assert_eq!("\\x01", escape_ascii(b"\x01")); } #[test] fn exec() { let _guard = LOCK.lock().unwrap(); 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_that(&build_data::exec("bash", &["-c", "echo -e '\\xc3\\x28'"]).unwrap_err()) .contains("non-utf8"); 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)); } #[test] #[allow(clippy::unreadable_literal)] fn format_time() { assert_eq!("03:25:07Z", build_data::format_time(1618370707)); } #[test] #[allow(clippy::unreadable_literal)] fn format_timestamp() { assert_eq!( "2021-04-14T03:25:07Z", build_data::format_timestamp(1618370707) ); } #[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; assert_eq!(None, build_data::get_env("NONEXISTENT_ENV_VAR").unwrap()); std::env::set_var("TEST_GET_ENV__EMPTY", ""); assert_eq!(None, build_data::get_env("TEST_GET_ENV__EMPTY").unwrap()); std::env::set_var("TEST_GET_ENV__WHITESPACE", " "); assert_eq!( None, build_data::get_env("TEST_GET_ENV__WHITESPACE").unwrap() ); std::env::set_var("TEST_GET_ENV__VALUE", "value1"); assert_eq!( "value1", &build_data::get_env("TEST_GET_ENV__VALUE").unwrap().unwrap() ); std::env::set_var("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]); std::env::set_var("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(); 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(); 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") ); } #[test] fn get_git_commit_short() { let _guard = LOCK.lock().unwrap(); 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(); 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 get_hostname() { let _guard = LOCK.lock().unwrap(); 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(); 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(); // rustc 1.53.0-nightly (07e0e2ec2 2021-03-24) 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 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] #[allow(clippy::unreadable_literal)] fn source_time_values() { let _guard = LOCK.lock().unwrap(); let empty_var_guard = TempEnvVarChange::new("SOURCE_DATE_EPOCH", ""); let bad_git_path_guard = TempEnvVarChange::new( "PATH", std::env::current_dir() .unwrap() .join("tests") .join("fake-bin"), ); let err = build_data::get_source_time().unwrap_err(); assert_that(&err).contains("failed parsing"); assert_that(&err).contains("git"); drop(bad_git_path_guard); let value = build_data::get_source_time().unwrap(); assert_that(&value).is_greater_than(1618400000); std::thread::sleep(Duration::from_millis(1100)); assert_eq!(value, build_data::get_source_time().unwrap()); assert_eq!( format!("cargo:rustc-env=SOURCE_EPOCH_TIME={value}\n"), exec_cargo_bin("test_set_source_epoch_time") ); drop(empty_var_guard); let bad_var_guard = TempEnvVarChange::new("SOURCE_DATE_EPOCH", "not-digits"); assert_that(&build_data::get_source_time().unwrap_err()).contains("failed parsing"); drop(bad_var_guard); let _var_guard = TempEnvVarChange::new("SOURCE_DATE_EPOCH", "1713215656"); assert_eq!(1713215656, 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 no_debug_rebuilds_debug() { let _guard = LOCK.lock().unwrap(); assert_eq!( "cargo:rerun-if-env-changed=PROFILE\n", exec_cargo_bin("test_no_debug_rebuilds_debug") ); } #[test] fn no_debug_rebuilds_release() { let _guard = LOCK.lock().unwrap(); assert_eq!("", exec_cargo_bin("test_no_debug_rebuilds_release")); } #[test] fn set_build_date() { let _guard = LOCK.lock().unwrap(); 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(); 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(); 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(); 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(); }