use crate::internal::int_bounds;
pub use crate::runner::MonkeyResult;
use crate::BoxGen;
use crate::BoxShrink;
use crate::ExampleSize;
use crate::Property;
use crate::Seed;
use rand::RngCore;
use rand::SeedableRng;
use std::fmt::Write;
use std::sync::mpsc;
#[derive(Clone)]
pub struct Conf {
pub example_count: u32,
pub seed: Seed,
pub size: ExampleSize,
}
#[derive(Clone)]
pub struct ConfAndGen<E>
where
E: Clone,
{
pub conf: Conf,
pub generator: BoxGen<E>,
pub title: Option<String>,
}
impl Conf {
pub fn with_generator<E>(&self, generator: BoxGen<E>) -> ConfAndGen<E>
where
E: Clone,
{
ConfAndGen {
conf: self.clone(),
generator,
title: None,
}
}
pub fn with_example_count(&self, example_count: u32) -> Conf {
Self {
example_count,
seed: self.seed,
size: self.size.clone(),
}
}
pub fn with_example_size<Size>(&self, size: Size) -> Conf
where
Size: std::ops::RangeBounds<usize>,
{
Self {
example_count: self.example_count,
seed: self.seed,
size: int_bounds::to_inclusive_range(&size),
}
}
pub fn with_seed(&self, seed: Seed) -> Conf {
Self {
example_count: self.example_count,
seed,
size: self.size.clone(),
}
}
}
#[deprecated = "Please use `global_seed()` function instead."]
pub fn seed_to_use() -> Seed {
global_seed()
}
pub fn global_seed() -> Seed {
rand_chacha::ChaCha8Rng::from_os_rng().next_u64()
}
pub const DEFAULT_EXAMPLE_SIZE: ExampleSize = 0..=1000;
pub fn global_example_size() -> ExampleSize {
DEFAULT_EXAMPLE_SIZE.clone()
}
pub const DEFAULT_EXAMPLE_COUNT: u32 = 100;
pub fn global_example_count() -> u32 {
DEFAULT_EXAMPLE_COUNT
}
impl Default for Conf {
fn default() -> Self {
Self {
example_count: global_example_count(),
seed: global_seed(),
size: global_example_size(),
}
}
}
impl<E> ConfAndGen<E>
where
E: std::fmt::Debug + std::panic::UnwindSafe + Clone + 'static,
{
pub fn test_true(&self, prop: Property<E>) -> MonkeyResult<E>
where
E: std::fmt::Debug + std::panic::UnwindSafe,
{
crate::runner::evaluate_property(
self,
catch_panic(|example: E| {
if prop(example) {
Ok(())
} else {
Err("Expecting 'true' but got 'false'.".into())
}
}),
)
}
#[track_caller]
pub fn assert_true(&self, prop: Property<E>) -> &ConfAndGen<E> {
panic_on_err(self.test_true(prop));
self
}
#[track_caller]
pub fn assert_no_panic(&self, prop: fn(E) -> ()) -> &ConfAndGen<E> {
panic_on_err(crate::runner::evaluate_property(
self,
catch_panic(|example| {
prop(example);
Ok(())
}),
));
self
}
#[track_caller]
pub fn assert_eq<D>(
&self,
expected: fn(E) -> D,
actual: fn(E) -> D,
) -> &ConfAndGen<E>
where
D: std::fmt::Debug + PartialEq,
{
panic_on_err(crate::runner::evaluate_property(
self,
catch_panic(|example: E| {
let a = actual(example.clone());
let e = expected(example);
if a == e {
Ok(())
} else {
Err(format!(
"Actual value should equal expected {e:?}, but got {a:?}."
))
}
}),
));
self
}
#[track_caller]
pub fn assert_ne<D>(
&self,
expected: fn(E) -> D,
actual: fn(E) -> D,
) -> &ConfAndGen<E>
where
D: std::fmt::Debug + PartialEq,
{
panic_on_err(crate::runner::evaluate_property(
self,
catch_panic(|example: E| {
let a = actual(example.clone());
let e = expected(example);
if a != e {
Ok(())
} else {
Err(format!(
"Actual value should not equal expected {e:?}, but got {a:?}."
))
}
}),
));
self
}
pub fn with_shrinker(&self, shrink: BoxShrink<E>) -> ConfAndGen<E> {
Self {
generator: self.generator.with_shrinker(shrink),
..self.clone()
}
}
pub fn title(&self, title: &str) -> ConfAndGen<E> {
Self {
title: Some(title.to_string()),
..self.clone()
}
}
}
fn panic_on_err<E>(result: MonkeyResult<E>)
where
E: std::fmt::Debug,
{
if let MonkeyResult::MonkeyErr {
minimum_failure,
seed,
success_count,
title,
reason,
some_other_failures,
original_failure,
..
} = result
{
let first_line = match title {
Some(t) => format!("Monkey test property \"{t}\" failed!"),
None => "Monkey test property failed!".into(),
};
let other_failures_text: String = some_other_failures.iter().fold(
String::new(),
|mut output, failure| {
let _ = write!(output, "\n\t{failure:?}");
output
},
);
panic!(
"{first_line}\n\
Failure: {minimum_failure:?}\n\
Reason: {reason}\n\
\n\
Reproduction seed: {seed}\n\
Success count before failure: {success_count}\n\
Other failures:\n\t{original_failure:?}{other_failures_text}\n",
)
}
}
fn catch_panic<E, P>(prop: P) -> impl Fn(E) -> Result<(), String>
where
E: std::fmt::Debug + std::panic::UnwindSafe + Clone + 'static,
P: std::panic::RefUnwindSafe + Fn(E) -> Result<(), String>,
{
move |example: E| {
let original_panic_hook = std::panic::take_hook();
let (tx, rx) = mpsc::channel();
std::panic::set_hook(Box::new(move |info| {
if let Some(loc) = info.location() {
let location_text =
format! {"in file '{}' at line {}", loc.file(), loc.line()};
let _ = tx.send(location_text);
}
}));
let result_or_panic = std::panic::catch_unwind(|| prop(example));
std::panic::set_hook(original_panic_hook);
match result_or_panic {
Ok(inner_result) => inner_result,
Err(panic) => {
let message =
panic_message::get_panic_message(&panic).unwrap_or("<?>");
let location =
rx.try_recv().unwrap_or("at unknown location".into());
Err(
format! {"Expecting no panic, but got panic {message:?} {location}." },
)
}
}
}
}