use std::env;
use std::io;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::TestResult;
fn parse_number<T>(var: &str, raw: &str) -> TestResult<T>
where
T: FromNumber,
{
let trimmed = raw.trim();
let (radix, digits) = if let Some(rest) = trimmed.strip_prefix("0x") {
(16, rest)
} else if let Some(rest) = trimmed.strip_prefix("0b") {
(2, rest)
} else if let Some(rest) = trimmed.strip_prefix("0o") {
(8, rest)
} else {
(10, trimmed)
};
let cleaned: String = digits.chars().filter(|c| *c != '_').collect();
T::from_str_radix(&cleaned, radix).map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"environment variable {var:?} has an invalid value {raw:?}: {err}; \
expected a decimal integer or a 0x / 0b / 0o prefixed value (e.g. 0xDEAD_BEEF, 1_000_000)"
),
)
.into()
})
}
pub fn seed_from_env_or_time(var: &str) -> TestResult<u64> {
match env::var(var) {
Ok(raw) => parse_number(var, &raw),
Err(env::VarError::NotPresent) => Ok(time_seed()),
Err(env::VarError::NotUnicode(_)) => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("environment variable {var:?} is not valid UTF-8"),
)
.into()),
}
}
trait FromNumber: Sized {
fn from_str_radix(src: &str, radix: u32) -> Result<Self, std::num::ParseIntError>;
}
impl FromNumber for u64 {
fn from_str_radix(src: &str, radix: u32) -> Result<Self, std::num::ParseIntError> {
u64::from_str_radix(src, radix)
}
}
impl FromNumber for usize {
fn from_str_radix(src: &str, radix: u32) -> Result<Self, std::num::ParseIntError> {
usize::from_str_radix(src, radix)
}
}
fn time_seed() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_number_accepts_decimal() {
let v: u64 = parse_number("SEED", "42").unwrap();
assert_eq!(v, 42);
}
#[test]
fn parse_number_accepts_hex_prefix() {
let v: u64 = parse_number("SEED", "0xDEAD_BEEF").unwrap();
assert_eq!(v, 0xDEAD_BEEF);
}
#[test]
fn parse_number_accepts_binary_and_octal_prefixes() {
let b: u64 = parse_number("SEED", "0b1010").unwrap();
assert_eq!(b, 0b1010);
let o: u64 = parse_number("SEED", "0o17").unwrap();
assert_eq!(o, 0o17);
}
#[test]
fn parse_number_accepts_underscore_separators() {
let v: u64 = parse_number("SEED", "1_000_000").unwrap();
assert_eq!(v, 1_000_000);
let h: u64 = parse_number("SEED", "0xDEAD_BEEF_CAFE").unwrap();
assert_eq!(h, 0xDEAD_BEEF_CAFE);
}
#[test]
fn parse_number_accepts_valid_usize() {
let v: usize = parse_number("ITER", "128").unwrap();
assert_eq!(v, 128);
}
#[test]
fn parse_number_reports_invalid_value_with_context() {
let err = parse_number::<u64>("SEED", "not-a-number").unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("SEED"),
"var name must be in the message: {msg}"
);
assert!(
msg.contains("not-a-number"),
"raw value must be in the message: {msg}"
);
assert!(
msg.contains("0x") && msg.contains("0b"),
"prefix examples must be in the message: {msg}"
);
}
#[test]
fn parse_number_reports_negative_for_usize() {
let err = parse_number::<usize>("ITER", "-1").unwrap_err();
assert!(err.to_string().contains("ITER"));
}
#[test]
fn time_seed_returns_recent_nanos() {
let a = time_seed();
let b = time_seed();
assert!(a != 0 || b != 0);
}
#[test]
fn seed_from_env_or_time_falls_back_when_variable_unset() {
let name = "NOPROP_CONFIG_TESTS_ABSOLUTELY_UNSET_SEED_9F3A_2E7B";
let a = seed_from_env_or_time(name).expect("unset var must fall back");
let b = seed_from_env_or_time(name).expect("unset var must fall back");
assert!(a != 0 || b != 0);
}
}