Skip to main content

hegel/
explicit_test_case.rs

1use std::any::Any;
2use std::cell::RefCell;
3use std::collections::HashMap;
4use std::fmt::Debug;
5
6use crate::generators::Generator;
7use crate::test_case::ASSUME_FAIL_STRING;
8
9struct ExplicitValue {
10    source_expr: String,
11    value: Option<Box<dyn Any>>,
12    debug_repr: String,
13}
14
15/// A test case with pre-defined values for explicit/example-based testing.
16///
17/// Created by `#[hegel::explicit_test_case]`. Values are looked up by name
18/// when `draw` or `__draw_named` is called, instead of being generated by
19/// the server.
20///
21/// # Threading
22///
23/// Unlike [`TestCase`](crate::TestCase), `ExplicitTestCase` is neither `Send`
24/// nor `Sync` and does not currently support being used from multiple
25/// threads. A test body that clones `tc` and moves it into a spawned thread
26/// therefore cannot be combined with `#[hegel::explicit_test_case]`: the
27/// macro expands the body with `tc: &ExplicitTestCase` for the explicit run,
28/// and `&ExplicitTestCase` will not pass `std::thread::spawn`'s `Send` bound.
29pub struct ExplicitTestCase {
30    values: RefCell<HashMap<String, ExplicitValue>>,
31    notes: RefCell<Vec<String>>,
32}
33
34impl ExplicitTestCase {
35    #[doc(hidden)]
36    pub fn new() -> Self {
37        ExplicitTestCase {
38            values: RefCell::new(HashMap::new()),
39            notes: RefCell::new(Vec::new()),
40        }
41    }
42
43    #[doc(hidden)]
44    pub fn with_value<T: Any + Debug>(self, name: &str, source_expr: &str, value: T) -> Self {
45        let debug_repr = format!("{:?}", value);
46        self.values.borrow_mut().insert(
47            name.to_string(),
48            ExplicitValue {
49                source_expr: source_expr.to_string(),
50                value: Some(Box::new(value)),
51                debug_repr,
52            },
53        );
54        self
55    }
56
57    pub fn draw<T: Debug + 'static>(&self, generator: impl Generator<T>) -> T {
58        self.__draw_named(generator, "draw", true)
59    }
60
61    pub fn __draw_named<T: Debug + 'static>(
62        &self,
63        _generator: impl Generator<T>,
64        name: &str,
65        _repeatable: bool,
66    ) -> T {
67        let mut values = self.values.borrow_mut();
68        let entry = match values.get_mut(name) {
69            Some(e) => e,
70            None => {
71                let available: Vec<_> = values.keys().cloned().collect();
72                panic!(
73                    "Explicit test case: no value provided for {:?}. Available: {:?}",
74                    name, available
75                );
76            }
77        };
78
79        let boxed = match entry.value.take() {
80            Some(v) => v,
81            None => {
82                panic!(
83                    "Explicit test case: value {:?} was already consumed by a previous draw",
84                    name
85                );
86            }
87        };
88
89        let source = &entry.source_expr;
90        let debug = &entry.debug_repr;
91
92        // Only show the "// = debug" comment if the source and debug differ
93        // (ignoring whitespace).
94        let source_normalized: String = source.chars().filter(|c| !c.is_whitespace()).collect();
95        let debug_normalized: String = debug.chars().filter(|c| !c.is_whitespace()).collect();
96
97        if source_normalized == debug_normalized {
98            eprintln!("let {} = {};", name, source);
99        } else {
100            eprintln!("let {} = {}; // = {}", name, source, debug);
101        }
102
103        match boxed.downcast::<T>() {
104            Ok(typed) => *typed,
105            Err(_) => panic!(
106                "Explicit test case: type mismatch for {:?}. \
107                 The value provided in #[hegel::explicit_test_case] \
108                 does not match the type expected by draw.",
109                name
110            ),
111        }
112    }
113
114    pub fn draw_silent<T>(&self, _generator: impl Generator<T>) -> T {
115        panic!("draw_silent is not supported in explicit test cases");
116    }
117
118    pub fn note(&self, message: &str) {
119        self.notes.borrow_mut().push(message.to_string());
120    }
121
122    pub fn assume(&self, condition: bool) {
123        if !condition {
124            self.reject();
125        }
126    }
127
128    pub fn reject(&self) -> ! {
129        panic!("{}", ASSUME_FAIL_STRING);
130    }
131
132    #[doc(hidden)]
133    pub fn start_span(&self, _label: u64) {
134        panic!("start_span is not supported in explicit test cases");
135    }
136
137    #[doc(hidden)]
138    pub fn stop_span(&self, _discard: bool) {
139        panic!("stop_span is not supported in explicit test cases");
140    }
141
142    #[doc(hidden)]
143    pub fn run<F: FnOnce(&ExplicitTestCase)>(&self, f: F) {
144        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
145            f(self);
146        }));
147
148        match result {
149            Ok(()) => {
150                let values = self.values.borrow();
151                let unused: Vec<_> = values
152                    .iter()
153                    .filter(|(_, v)| v.value.is_some())
154                    .map(|(k, _)| k.clone())
155                    .collect();
156                if !unused.is_empty() {
157                    panic!(
158                        "Explicit test case: the following values were provided \
159                         but never drawn: {:?}",
160                        unused
161                    );
162                }
163            }
164            Err(payload) => {
165                let notes = self.notes.borrow();
166                for note in notes.iter() {
167                    eprintln!("{}", note);
168                }
169                std::panic::resume_unwind(payload);
170            }
171        }
172    }
173}