Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
16 changes: 10 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
[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"]
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"]
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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
Expand Down
5 changes: 2 additions & 3 deletions examples/simple_usage/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"] }
5 changes: 2 additions & 3 deletions examples/simple_usage/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
41 changes: 26 additions & 15 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -13,29 +16,33 @@ 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))
}

/// 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);
Expand Down Expand Up @@ -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 {
Expand All @@ -83,12 +86,20 @@ impl SeedableRng for DevRng {
DevRng(ChaCha8Rng::seed_from_u64(state))
}

fn from_rng<R: RngCore>(rng: R) -> Result<Self, rand_core::Error> {
ChaCha8Rng::from_rng(rng).map(DevRng)
fn from_rng(rng: &mut impl RngCore) -> Self {
Self(ChaCha8Rng::from_rng(rng))
}

fn try_from_rng<R: TryRngCore>(rng: &mut R) -> Result<Self, R::Error> {
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<Self, getrandom::Error> {
ChaCha8Rng::try_from_os_rng().map(Self)
}
}

Expand Down
12 changes: 10 additions & 2 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down