#![allow(clippy::print_stdout, clippy::print_stderr)]
use rand::{RngCore, SeedableRng};
use rand_chacha::ChaCha12Rng as TestingRng;
type Seed = <TestingRng as SeedableRng>::Seed;
const DEFAULT_SEED: Seed = *b"4 // chosen by fair dice roll.";
const PRNG_VAR: &str = "ARTI_TEST_PRNG";
pub fn testing_rng() -> TestingRng {
Config::from_env().unwrap_or(Config::Random).into_rng()
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[non_exhaustive]
pub enum Config {
Random,
Deterministic,
Seeded(Seed),
}
impl Config {
pub fn from_env() -> Option<Self> {
match Self::from_env_result(std::env::var(PRNG_VAR)) {
Ok(c) => c,
Err(e) => {
panic!(
"Bad value for {}: {}\n\
We recognize `random`, `deterministic`, or a hexadecimal seed.",
PRNG_VAR, e
);
}
}
}
fn from_env_result(var: Result<String, std::env::VarError>) -> Result<Option<Self>, Error> {
match var {
Ok(s) if s.is_empty() => Ok(None),
Ok(s) => Ok(Some(Config::from_str(&s)?)),
Err(std::env::VarError::NotPresent) => Ok(None),
Err(std::env::VarError::NotUnicode(_)) => Err(Error::InvalidUnicode),
}
}
fn from_str(s: &str) -> Result<Self, Error> {
Ok(if s == "random" {
Self::Random
} else if s == "deterministic" {
Self::Deterministic
} else if let Some(seed) = decode_seed_bytes(s) {
Self::Seeded(seed)
} else {
return Err(Error::UnrecognizedValue(s.to_string()));
})
}
fn into_seed(self) -> Seed {
match self {
Config::Deterministic => DEFAULT_SEED,
Config::Seeded(seed) => seed,
Config::Random => {
let mut seed = Seed::default();
rand::thread_rng().fill_bytes(&mut seed[..]);
seed
}
}
}
pub fn into_rng(self) -> TestingRng {
let seed = self.into_seed();
println!(" Using RNG seed {}={}", PRNG_VAR, format_seed_bytes(&seed));
TestingRng::from_seed(seed)
}
}
fn format_seed_bytes(seed: &Seed) -> String {
hex::encode(seed)
}
fn decode_seed_bytes(s: &str) -> Option<Seed> {
if s.is_empty() {
return None;
}
let bytes = hex::decode(s).ok()?;
let mut seed = Seed::default();
let n = std::cmp::min(seed.len(), bytes.len());
seed[..n].copy_from_slice(&bytes[..n]);
Some(seed)
}
#[derive(Clone, Debug, thiserror::Error, Eq, PartialEq)]
enum Error {
#[error("Value was not UTF-8")]
InvalidUnicode,
#[error("Could not interpret {0:?} as a PRNG seed.")]
UnrecognizedValue(String),
}
#[cfg(test)]
mod test {
#![allow(clippy::unwrap_used)]
use std::env::VarError;
use super::*;
#[test]
fn from_str() {
assert_eq!(Ok(Config::Deterministic), Config::from_str("deterministic"));
assert_eq!(Ok(Config::Random), Config::from_str("random"));
assert_eq!(Ok(Config::Seeded([0x00; 32])), Config::from_str("00"));
{
let s = "aaaaaaaa";
let seed = [
0xaa, 0xaa, 0xaa, 0xaa, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
];
assert_eq!(Ok(Config::Seeded(seed)), Config::from_str(s));
}
{
let seed = *b"hello world. this is a longer st";
let mut s = hex::encode(seed);
assert_eq!(Ok(Config::Seeded(seed)), Config::from_str(&s));
s.push_str("aabbccddeeff");
assert_eq!(Ok(Config::Seeded(seed)), Config::from_str(&s));
}
assert_eq!(
Err(Error::UnrecognizedValue("".to_string())),
Config::from_str("")
);
assert_eq!(
Err(Error::UnrecognizedValue("return 4".to_string())),
Config::from_str("return 4")
);
}
#[test]
fn from_env() {
assert_eq!(
Ok(Some(Config::Deterministic)),
Config::from_env_result(Ok("deterministic".to_string()))
);
assert_eq!(
Ok(Some(Config::Random)),
Config::from_env_result(Ok("random".to_string()))
);
assert_eq!(
Ok(Some(Config::Seeded([0xcd; 32]))),
Config::from_env_result(Ok("cd".repeat(32)))
);
assert_eq!(Ok(None), Config::from_env_result(Ok("".to_string())));
assert_eq!(Ok(None), Config::from_env_result(Err(VarError::NotPresent)));
assert_eq!(
Err(Error::InvalidUnicode),
Config::from_env_result(Err(VarError::NotUnicode("3".into())))
);
assert_eq!(
Err(Error::UnrecognizedValue("123".to_string())),
Config::from_env_result(Ok("123".to_string()))
);
}
#[test]
fn make_seed() {
assert_eq!(Config::Deterministic.into_seed(), DEFAULT_SEED);
assert_eq!(Config::Seeded([0x24; 32]).into_seed(), [0x24; 32]);
let s1 = Config::Random.into_seed();
let s2 = Config::Random.into_seed();
assert_ne!(s1, s2);
}
#[test]
fn code_decode() {
assert_eq!(
decode_seed_bytes(&format_seed_bytes(&DEFAULT_SEED)).unwrap(),
DEFAULT_SEED
);
}
#[test]
fn determinism() {
let mut d_rng = Config::Deterministic.into_rng();
let values: Vec<_> = std::iter::repeat_with(|| d_rng.next_u32())
.take(8)
.collect();
let deterministic_values = vec![
4222362647, 2976626662, 1407369338, 1087750672, 196711223, 996083910, 836259566,
2589890951,
];
assert_eq!(values, deterministic_values);
let mut r_rng = Config::Random.into_rng();
let values: Vec<_> = std::iter::repeat_with(|| r_rng.next_u32())
.take(8)
.collect();
assert_ne!(values, deterministic_values);
}
}