hegeltest 0.6.0

Property-based testing for Rust, built on Hypothesis
Documentation
use std::any::Any;
use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt::Debug;

use crate::generators::Generator;
use crate::test_case::ASSUME_FAIL_STRING;

struct ExplicitValue {
    source_expr: String,
    value: Option<Box<dyn Any>>,
    debug_repr: String,
}

/// A test case with pre-defined values for explicit/example-based testing.
///
/// Created by `#[hegel::explicit_test_case]`. Values are looked up by name
/// when `draw` or `__draw_named` is called, instead of being generated by
/// the server.
pub struct ExplicitTestCase {
    values: RefCell<HashMap<String, ExplicitValue>>,
    notes: RefCell<Vec<String>>,
}

impl ExplicitTestCase {
    #[doc(hidden)]
    pub fn new() -> Self {
        ExplicitTestCase {
            values: RefCell::new(HashMap::new()),
            notes: RefCell::new(Vec::new()),
        }
    }

    #[doc(hidden)]
    pub fn with_value<T: Any + Debug>(self, name: &str, source_expr: &str, value: T) -> Self {
        let debug_repr = format!("{:?}", value);
        self.values.borrow_mut().insert(
            name.to_string(),
            ExplicitValue {
                source_expr: source_expr.to_string(),
                value: Some(Box::new(value)),
                debug_repr,
            },
        );
        self
    }

    pub fn draw<T: Debug + 'static>(&self, generator: impl Generator<T>) -> T {
        self.__draw_named(generator, "draw", true)
    }

    pub fn __draw_named<T: Debug + 'static>(
        &self,
        _generator: impl Generator<T>,
        name: &str,
        _repeatable: bool,
    ) -> T {
        let mut values = self.values.borrow_mut();
        let entry = match values.get_mut(name) {
            Some(e) => e,
            None => {
                let available: Vec<_> = values.keys().cloned().collect();
                panic!(
                    "Explicit test case: no value provided for {:?}. Available: {:?}",
                    name, available
                );
            }
        };

        let boxed = match entry.value.take() {
            Some(v) => v,
            None => {
                panic!(
                    "Explicit test case: value {:?} was already consumed by a previous draw",
                    name
                );
            }
        };

        let source = &entry.source_expr;
        let debug = &entry.debug_repr;

        // Only show the "// = debug" comment if the source and debug differ
        // (ignoring whitespace).
        let source_normalized: String = source.chars().filter(|c| !c.is_whitespace()).collect();
        let debug_normalized: String = debug.chars().filter(|c| !c.is_whitespace()).collect();

        if source_normalized == debug_normalized {
            eprintln!("let {} = {};", name, source);
        } else {
            eprintln!("let {} = {}; // = {}", name, source, debug);
        }

        match boxed.downcast::<T>() {
            Ok(typed) => *typed,
            Err(_) => panic!(
                "Explicit test case: type mismatch for {:?}. \
                 The value provided in #[hegel::explicit_test_case] \
                 does not match the type expected by draw.",
                name
            ),
        }
    }

    pub fn draw_silent<T>(&self, _generator: impl Generator<T>) -> T {
        panic!("draw_silent is not supported in explicit test cases");
    }

    pub fn note(&self, message: &str) {
        self.notes.borrow_mut().push(message.to_string());
    }

    pub fn assume(&self, condition: bool) {
        if !condition {
            panic!("{}", ASSUME_FAIL_STRING);
        }
    }

    #[doc(hidden)]
    pub fn start_span(&self, _label: u64) {
        panic!("start_span is not supported in explicit test cases");
    }

    #[doc(hidden)]
    pub fn stop_span(&self, _discard: bool) {
        panic!("stop_span is not supported in explicit test cases");
    }

    #[doc(hidden)]
    pub fn run<F: FnOnce(&ExplicitTestCase)>(&self, f: F) {
        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            f(self);
        }));

        match result {
            Ok(()) => {
                let values = self.values.borrow();
                let unused: Vec<_> = values
                    .iter()
                    .filter(|(_, v)| v.value.is_some())
                    .map(|(k, _)| k.clone())
                    .collect();
                if !unused.is_empty() {
                    panic!(
                        "Explicit test case: the following values were provided \
                         but never drawn: {:?}",
                        unused
                    );
                }
            }
            Err(payload) => {
                let notes = self.notes.borrow();
                for note in notes.iter() {
                    eprintln!("{}", note);
                }
                std::panic::resume_unwind(payload);
            }
        }
    }
}