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