diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9bad2fa..17eaee7 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -9,20 +9,58 @@ on: env: CARGO_TERM_COLOR: always CARGO_NET_GIT_FETCH_WITH_CLI: true + RUSTFLAGS: -D warnings jobs: - build: + check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: "true" + - name: Check + run: cargo check + check-with-rand-v09: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: "true" + - name: Check with rand v0.9 + run: cargo check --features rand-v09 + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 with: cache-on-failure: "true" - - name: Build - run: cargo build - name: Run tests run: cargo test + check-example: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: "true" + - name: Check example + # note: if ran `cargo test`, it intentionally fails with 1/10 probability + run: (cd examples/simple_usage; cargo check --tests) + fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 - name: Check formatting run: cargo fmt --all -- --check + clippy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: "true" - name: Run clippy run: cargo clippy -- -D clippy::all diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..989f1cf --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,30 @@ +## v0.2.0 +* BREAKING: update rand_core dep to latest `v0.9` +* POSSIBLY BREAKING: the seed is now printed to stderr instead of stdout +* Replace dependency on `hex` with better-maintained `const-hex v1` +* Add optional feature `rand-v09`: when it's enabled, library re-exports `rand v0.9` that will be + accessible as `rand_dev::rand` +* Improve CI workflow + +See [#4](https://github.com/survived/rand_dev/pull/4) + +## v0.1.1 +* Add `DevRng::fork` method that derives a new randomness generator from existing one \ + May be useful when you have several threads/futures/places where you need access the randomness + generation, but you don't want to mess with ownership system. +* Changes format of seed printed to stdout \ + Old format: + ```text + Tests seed: {seed} + ``` + New format: + ```text + RUST_TESTS_SEED={seed} + ``` + New format makes it easier to copy-paste env var when you want to reproduce the tests. +* Add Github Actions + +See [#2](https://github.com/survived/rand_dev/pull/2) + +# v0.1.0 +First release of the library diff --git a/Cargo.toml b/Cargo.toml index 47f8437..2d7cdff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "rand_dev" -version = "0.1.1" +version = "0.2.0" license = "MIT OR Apache-2.0" -edition = "2021" +edition = "2024" description = "Reproducible randomness source for tests" categories = ["development-tools", "development-tools::debugging", "development-tools::testing"] @@ -10,13 +10,17 @@ keywords = ["tests", "reproducibility", "rand"] repository = "https://github.com/survived/rand_dev" [dependencies] -rand_core = { version = "0.6", features = ["std"] } -rand_chacha = "0.3" -hex = "0.4" +rand_core = { version = "0.9", features = ["os_rng"] } +rand_chacha = "0.9" +getrandom = { version = "0.3", default-features = false } +const-hex = { version = "1", default-features = false, features = ["alloc"] } + +rand = { version = "0.9", optional = true } [dev-dependencies] -rand = "0.8" +rand = "0.9" [features] default = [] +rand-v09 = ["dep:rand"] diff --git a/README.md b/README.md index 6672905..742ddeb 100644 --- a/README.md +++ b/README.md @@ -6,25 +6,25 @@ Having reproducible tests helps debugging problems that have probabilistic nature. This library provides a random numbers generator `DevRng` compatible with [`rand`] crate (it implements `Rng`, `RngCore`, -`SeedableRng` traits). When generator is constructed, its seed is printed to stdout. You can override a +`SeedableRng` traits). When generator is constructed, its seed is printed to stderr. You can override a seed by setting `RUST_TESTS_SEED` env variable. Same seed leads to same randomness generated across all platforms. ## Usage Reproducible source of randomness can be added in one line: -```rust +```rust,no_run use rand::Rng; use rand_dev::DevRng; #[test] fn it_works() { let mut rng = DevRng::new(); - assert!(rng.gen_range(0..=10) < 10); + assert!(rng.random_range(0..=10) < 10); } ``` -Then if test fails, you can observe seed of randomness generator in stdout: +Then if test fails, you can observe seed of randomness generator in captured stderr: ```text $ cargo test Finished test [unoptimized + debuginfo] target(s) in 0.00s @@ -36,7 +36,7 @@ test tests::it_works ... FAILED failures: ---- tests::it_works stdout ---- -RUST_TESTS_SEED=cab4ab5c8471fa03691bb86d96c2febeb9b1099a78d164e8addbe7f83d107c78 +RUST_TESTS_SEED=fa48105a3c2ada139e0aa234f235a7af5c766cac4daefca97b57d73915c5b736 thread 'tests::it_works' panicked at 'assertion failed: rng.gen_range(0..=10) < 10', src/lib.rs:9:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace @@ -55,6 +55,9 @@ $ cargo test [`rand`]: https://docs.rs/rand +## Features +* `rand-v09` _(disabled by default)_ when enabled, library re-exports `rand v0.9` which will be accessible as `rand_dev::rand`. + ## License Licensed under either of diff --git a/examples/simple_usage/Cargo.toml b/examples/simple_usage/Cargo.toml index 1eb1c02..e526397 100644 --- a/examples/simple_usage/Cargo.toml +++ b/examples/simple_usage/Cargo.toml @@ -1,11 +1,10 @@ [package] name = "simple_usage" version = "0.1.0" -edition = "2021" +edition = "2024" publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rand = "0.8" -rand_dev = { path = "../../" } +rand_dev = { path = "../../", features = ["rand-v09"] } diff --git a/examples/simple_usage/src/lib.rs b/examples/simple_usage/src/lib.rs index 15e87c2..5df7c27 100644 --- a/examples/simple_usage/src/lib.rs +++ b/examples/simple_usage/src/lib.rs @@ -1,11 +1,10 @@ #[cfg(test)] mod tests { - use rand::Rng; - use rand_dev::DevRng; + use rand_dev::{DevRng, rand::Rng}; #[test] fn it_works() { let mut rng = DevRng::new(); - assert!(rng.gen_range(0..=10) < 10); + assert!(rng.random_range(0..=10) < 10); } } diff --git a/src/lib.rs b/src/lib.rs index 057c924..692348b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,10 @@ #![doc = include_str!("../README.md")] +#[cfg(feature = "rand-v09")] +pub use rand; + use rand_chacha::ChaCha8Rng; -use rand_core::{CryptoRng, OsRng, RngCore, SeedableRng}; +use rand_core::{CryptoRng, OsRng, RngCore, SeedableRng, TryRngCore}; /// Reproducible random generator for tests #[derive(Debug, Clone)] @@ -13,22 +16,23 @@ impl DevRng { /// Constructs randomness generator /// /// Reads a seed from env variable `RUST_TESTS_SEED` or generates a random seed if env variable is not set. - /// Prints seed to stdout. + /// Prints seed to stderr. /// /// Panics if `RUST_TESTS_SEED` contains invalid value. #[track_caller] pub fn new() -> Self { let mut seed = [0u8; 32]; match std::env::var(Self::VAR_NAME) { - Ok(provided_seed) => { - hex::decode_to_slice(provided_seed, &mut seed).expect("provided seed is not valid") - } + Ok(provided_seed) => const_hex::decode_to_slice(provided_seed, &mut seed) + .expect("provided seed is not valid"), Err(std::env::VarError::NotUnicode(_)) => { panic!("provided seed is not a valid unicode") } - Err(std::env::VarError::NotPresent) => OsRng.fill_bytes(&mut seed), + Err(std::env::VarError::NotPresent) => OsRng + .try_fill_bytes(&mut seed) + .expect("system randomness unavailable"), } - println!("RUST_TESTS_SEED={}", hex::encode(seed)); + eprintln!("RUST_TESTS_SEED={}", const_hex::encode(seed)); DevRng(ChaCha8Rng::from_seed(seed)) } @@ -36,6 +40,9 @@ impl DevRng { /// Derives another randomness generator from this instance /// /// Uses `self` to generate a seed and constructs a new instance of `DevRng` from the seed. + /// + /// May be useful when you have several threads/futures/places where you need access the + /// randomness generation, but you don't want to mess with ownership system. pub fn fork(&mut self) -> Self { let mut seed = [0u8; 32]; self.fill_bytes(&mut seed); @@ -66,10 +73,6 @@ impl RngCore for DevRng { fn fill_bytes(&mut self, dest: &mut [u8]) { self.0.fill_bytes(dest) } - - fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { - self.0.try_fill_bytes(dest) - } } impl SeedableRng for DevRng { @@ -83,12 +86,20 @@ impl SeedableRng for DevRng { DevRng(ChaCha8Rng::seed_from_u64(state)) } - fn from_rng(rng: R) -> Result { - ChaCha8Rng::from_rng(rng).map(DevRng) + fn from_rng(rng: &mut impl RngCore) -> Self { + Self(ChaCha8Rng::from_rng(rng)) + } + + fn try_from_rng(rng: &mut R) -> Result { + ChaCha8Rng::try_from_rng(rng).map(Self) + } + + fn from_os_rng() -> Self { + Self(ChaCha8Rng::from_os_rng()) } - fn from_entropy() -> Self { - DevRng(ChaCha8Rng::from_entropy()) + fn try_from_os_rng() -> Result { + ChaCha8Rng::try_from_os_rng().map(Self) } } diff --git a/tests/tests.rs b/tests/tests.rs index 311082b..bc3b0f2 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -5,13 +5,21 @@ const VAR_NAME: &str = "RUST_TESTS_SEED"; #[test] fn reproducibility() { - std::env::remove_var(VAR_NAME); + // SAFETY: setting/removing env vars is safe in single-threaded programs. Since there's + // only one test, this is safe to do. + unsafe { + std::env::remove_var(VAR_NAME); + } let mut rng1 = DevRng::new(); let mut rng2 = DevRng::new(); assert_ne!(rng1.get_seed(), rng2.get_seed()); - std::env::set_var(VAR_NAME, hex::encode(rng1.get_seed())); + // SAFETY: setting/removing env vars is safe in single-threaded programs. Since there's + // only one test, this is safe to do. + unsafe { + std::env::set_var(VAR_NAME, const_hex::encode(rng1.get_seed())); + } let mut rng3 = DevRng::new(); assert_eq!(rng1.get_seed(), rng3.get_seed());