hegel/test_case.rs
1pub use crate::backend::{DataSource, DataSourceError};
2use crate::generators::Generator;
3use crate::runner::Mode;
4use ciborium::Value;
5use parking_lot::Mutex;
6use std::any::Any;
7use std::cell::RefCell;
8use std::collections::{HashMap, HashSet};
9use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
10use std::sync::Arc;
11
12use crate::generators::value;
13
14// We use the __IsTestCase trait internally to provide nice error messages for misuses of #[composite].
15// It should not be used by users.
16//
17// The idea is #[composite] calls __assert_is_test_case(<first param>), which errors with our on_unimplemented
18// message iff the first param does not have type TestCase.
19
20#[diagnostic::on_unimplemented(
21 // NOTE: worth checking if edits to this message should also be applied to the similar-but-different
22 // error message in #[composite] in hegel-macros.
23 message = "The first parameter in a #[composite] generator must have type TestCase.",
24 label = "This type does not match `TestCase`."
25)]
26pub trait __IsTestCase {}
27impl __IsTestCase for TestCase {}
28pub fn __assert_is_test_case<T: __IsTestCase>() {}
29
30pub(crate) const ASSUME_FAIL_STRING: &str = "__HEGEL_ASSUME_FAIL";
31
32/// The sentinel string used to identify overflow/StopTest panics.
33/// Distinct from ASSUME_FAIL_STRING so callers can tell user-initiated
34/// assumption failures apart from backend-initiated data exhaustion.
35pub(crate) const STOP_TEST_STRING: &str = "__HEGEL_STOP_TEST";
36
37/// The sentinel string used by `TestCase::repeat` to signal that its loop
38/// completed naturally (the collection said "stop" and no panic occurred
39/// inside the body). Because `repeat` returns `!`, it has no normal-return
40/// path; this panic is how it tells the runner "this test case finished
41/// successfully, record it as Valid".
42pub(crate) const LOOP_DONE_STRING: &str = "__HEGEL_LOOP_DONE";
43
44/// Panic with the appropriate sentinel for the given data source error.
45fn panic_on_data_source_error(e: DataSourceError) -> ! {
46 match e {
47 DataSourceError::StopTest => panic!("{}", STOP_TEST_STRING),
48 DataSourceError::Assume => panic!("{}", ASSUME_FAIL_STRING), // nocov
49 DataSourceError::ServerError(msg) => panic!("{}", msg),
50 }
51}
52
53pub(crate) struct TestCaseGlobalData {
54 is_last_run: bool,
55 mode: Mode,
56 /// Fine-grained lock over the state shared between clones of a
57 /// `TestCase`. Acquired briefly around each individual backend call
58 /// and around each mutation of the draw-tracking bookkeeping, not
59 /// around entire user-visible operations like a `draw`. The mutex is
60 /// non-reentrant; no method holds it while calling back into
61 /// `TestCase`.
62 shared: Mutex<SharedState>,
63}
64
65pub(crate) struct SharedState {
66 data_source: Box<dyn DataSource>,
67 draw_state: DrawState,
68}
69
70pub(crate) struct DrawState {
71 named_draw_counts: HashMap<String, usize>,
72 named_draw_repeatable: HashMap<String, bool>,
73 allocated_display_names: HashSet<String>,
74}
75
76#[derive(Clone)]
77pub(crate) struct TestCaseLocalData {
78 span_depth: usize,
79 indent: usize,
80 on_draw: OutputSink,
81}
82
83/// A handle to the current test case.
84///
85/// This is passed to `#[hegel::test]` functions and provides methods
86/// for drawing values, making assumptions, and recording notes.
87///
88/// # Example
89///
90/// ```no_run
91/// use hegel::generators as gs;
92///
93/// #[hegel::test]
94/// fn my_test(tc: hegel::TestCase) {
95/// let x: i32 = tc.draw(gs::integers());
96/// tc.assume(x > 0);
97/// tc.note(&format!("x = {}", x));
98/// }
99/// ```
100///
101/// # Threading
102///
103/// `TestCase` is `Send` but not `Sync`. To drive generation from another
104/// thread, clone the test case and move the clone. Clones share the same
105/// underlying backend connection — they are views onto one test case, not
106/// independent test cases.
107///
108/// ```no_run
109/// use hegel::generators as gs;
110///
111/// #[hegel::test]
112/// fn my_test(tc: hegel::TestCase) {
113/// let tc_worker = tc.clone();
114/// let handle = std::thread::spawn(move || {
115/// tc_worker.draw(gs::integers::<i32>())
116/// });
117/// let n = handle.join().unwrap();
118/// let _b: bool = tc.draw(gs::booleans());
119/// let _ = n;
120/// }
121/// ```
122///
123/// ## What is guaranteed
124///
125/// Individual backend operations (a single `generate`, `start_span`,
126/// `stop_span`, or pool/collection call) are serialised by a shared
127/// mutex, so the bytes on the wire to the backend stay well-formed no
128/// matter how clones are used across threads.
129///
130/// This is enough for patterns where threads do not race on generation —
131/// for example:
132///
133/// - Spawn a worker, let it draw, `join` it, then continue on the main
134/// thread.
135/// - Repeatedly spawn-and-join one worker at a time.
136/// - Any pattern where exactly one thread is drawing at a time, with a
137/// happens-before relationship (join, channel receive, barrier) between
138/// each thread's work.
139///
140/// ## What is not guaranteed
141///
142/// Concurrent generation will get progressively better over time, but
143/// right now should be considered a borderline-internal feature. If
144/// you do not know exactly what you're doing it probably won't work.
145///
146/// Two or more threads drawing concurrently from clones of the same
147/// `TestCase` is allowed by the type system but is **not deterministic**:
148/// the order in which draws interleave depends on thread scheduling, and
149/// the backend has no way to reproduce that order on replay. Composite
150/// draws are also not atomic with respect to other threads — another
151/// thread's draws can land between this thread's `start_span` and
152/// `stop_span`, corrupting the shrink-friendly span structure. In
153/// practice this means such tests may:
154///
155/// - Produce different values on successive runs of the same seed.
156/// - Shrink poorly or not at all.
157/// - Surface backend errors (e.g. `StopTest`) in one thread caused by
158/// another thread's draws exhausting the budget.
159///
160/// ## Panics inside spawned threads
161///
162/// If a worker thread panics with an assumption failure or a backend
163/// `StopTest`, that panic stays inside the thread's `JoinHandle` until
164/// the main thread joins it. The main thread is responsible for
165/// propagating (or suppressing) the panic — typically by calling
166/// `handle.join().unwrap()`, which resumes the panic on the main thread
167/// so Hegel's runner can observe it.
168pub struct TestCase {
169 global: Arc<TestCaseGlobalData>,
170 // RefCell makes `TestCase: !Sync`. Local data is per-clone: each clone gets
171 // its own span depth, indent, and on_draw. Concurrent use across threads
172 // therefore requires cloning, which is enforced by the `!Sync` bound.
173 local: RefCell<TestCaseLocalData>,
174}
175
176impl Clone for TestCase {
177 fn clone(&self) -> Self {
178 TestCase {
179 global: self.global.clone(),
180 local: RefCell::new(self.local.borrow().clone()),
181 }
182 }
183}
184
185impl std::fmt::Debug for TestCase {
186 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187 f.debug_struct("TestCase").finish_non_exhaustive()
188 }
189}
190
191/// A callback invoked for each line of draw/note output during the final replay.
192pub(crate) type OutputSink = Arc<dyn Fn(&str) + Send + Sync>;
193
194thread_local! {
195 static OUTPUT_OVERRIDE: RefCell<Option<OutputSink>> = const { RefCell::new(None) };
196}
197
198/// Install a custom output sink for the duration of `f`, replacing the usual
199/// `eprintln!` behavior of draw and note output. Intended for tests that want
200/// to capture what a test case would print.
201///
202/// While active, notes and draws from the final replay go to `sink` instead of
203/// stderr. Non-final test cases still drop their draw/note output as usual.
204#[doc(hidden)]
205pub fn with_output_override<R>(sink: OutputSink, f: impl FnOnce() -> R) -> R {
206 let prev = OUTPUT_OVERRIDE.with(|cell| cell.borrow_mut().replace(sink));
207 let result = f();
208 OUTPUT_OVERRIDE.with(|cell| *cell.borrow_mut() = prev);
209 result
210}
211
212fn panic_message(payload: &Box<dyn Any + Send>) -> String {
213 if let Some(s) = payload.downcast_ref::<&str>() {
214 s.to_string()
215 } else if let Some(s) = payload.downcast_ref::<String>() {
216 s.clone()
217 } else {
218 "Unknown panic".to_string() // nocov
219 }
220}
221
222impl TestCase {
223 pub(crate) fn new(data_source: Box<dyn DataSource>, is_last_run: bool, mode: Mode) -> Self {
224 let override_sink = OUTPUT_OVERRIDE.with(|cell| cell.borrow().clone());
225 let on_draw: OutputSink = match override_sink {
226 Some(sink) if is_last_run => sink,
227 _ if is_last_run => Arc::new(|msg| eprintln!("{}", msg)),
228 _ => Arc::new(|_| {}),
229 };
230 TestCase {
231 global: Arc::new(TestCaseGlobalData {
232 is_last_run,
233 mode,
234 shared: Mutex::new(SharedState {
235 data_source,
236 draw_state: DrawState {
237 named_draw_counts: HashMap::new(),
238 named_draw_repeatable: HashMap::new(),
239 allocated_display_names: HashSet::new(),
240 },
241 }),
242 }),
243 local: RefCell::new(TestCaseLocalData {
244 span_depth: 0,
245 indent: 0,
246 on_draw,
247 }),
248 }
249 }
250
251 pub(crate) fn mode(&self) -> Mode {
252 self.global.mode
253 }
254
255 /// Acquire the shared mutex for the duration of `f`.
256 ///
257 /// Held briefly around individual backend calls or draw-state updates,
258 /// never around whole user-visible operations. The mutex is
259 /// non-reentrant, so `f` must not call any other method that also
260 /// acquires the shared mutex.
261 fn with_shared<R>(&self, f: impl FnOnce(&mut SharedState) -> R) -> R {
262 let mut guard = self.global.shared.lock();
263 f(&mut guard)
264 }
265
266 /// Draw a value from a generator.
267 ///
268 /// # Example
269 ///
270 /// ```no_run
271 /// use hegel::generators as gs;
272 ///
273 /// #[hegel::test]
274 /// fn my_test(tc: hegel::TestCase) {
275 /// let x: i32 = tc.draw(gs::integers());
276 /// let s: String = tc.draw(gs::text());
277 /// }
278 /// ```
279 ///
280 /// Note: when run inside a `#[hegel::test]`, `draw()` will typically be
281 /// rewritten to `__draw_named()` with an appropriate variable name
282 /// in order to give better test output.
283 pub fn draw<T: std::fmt::Debug>(&self, generator: impl Generator<T>) -> T {
284 self.__draw_named(generator, "draw", true)
285 }
286
287 /// Draw a value from a generator with a specific name for output.
288 ///
289 /// When `repeatable` is true, a counter suffix is appended (e.g. `x_1`, `x_2`).
290 /// When `repeatable` is false, reusing the same name panics.
291 ///
292 /// Using the same name with different values of `repeatable` is an error.
293 ///
294 /// On the final replay of a failing test case, this prints:
295 /// - `let name = value;` (when not repeatable)
296 /// - `let name_N = value;` (when repeatable)
297 ///
298 /// Not intended for direct use. This is the target that `#[hegel::test]` rewrites `draw()`
299 /// calls to where appropriate.
300 pub fn __draw_named<T: std::fmt::Debug>(
301 &self,
302 generator: impl Generator<T>,
303 name: &str,
304 repeatable: bool,
305 ) -> T {
306 let value = generator.do_draw(self);
307 if self.local.borrow().span_depth == 0 {
308 self.record_named_draw(&value, name, repeatable);
309 }
310 value
311 }
312
313 /// Draw a value from a generator without recording it in the output.
314 ///
315 /// Unlike [`draw`](Self::draw), this does not require `T: Debug` and
316 /// will not print the value in the failing-test summary.
317 pub fn draw_silent<T>(&self, generator: impl Generator<T>) -> T {
318 generator.do_draw(self)
319 }
320
321 /// Assume a condition is true. If false, reject the current test input.
322 ///
323 /// # Example
324 ///
325 /// ```no_run
326 /// use hegel::generators as gs;
327 ///
328 /// #[hegel::test]
329 /// fn my_test(tc: hegel::TestCase) {
330 /// let age: u32 = tc.draw(gs::integers());
331 /// tc.assume(age >= 18);
332 /// }
333 /// ```
334 pub fn assume(&self, condition: bool) {
335 if !condition {
336 self.reject();
337 }
338 }
339
340 /// Reject the current test input unconditionally.
341 ///
342 /// Equivalent to `assume(false)`, but with a `!` return type so that code
343 /// following the call is statically known to be unreachable.
344 ///
345 /// # Example
346 ///
347 /// ```no_run
348 /// use hegel::generators as gs;
349 ///
350 /// #[hegel::test]
351 /// fn my_test(tc: hegel::TestCase) {
352 /// let n: i32 = tc.draw(gs::integers());
353 /// let positive: u32 = match u32::try_from(n) {
354 /// Ok(v) => v,
355 /// Err(_) => tc.reject(),
356 /// };
357 /// let _ = positive;
358 /// }
359 /// ```
360 pub fn reject(&self) -> ! {
361 panic!("{}", ASSUME_FAIL_STRING);
362 }
363
364 /// Note a message which will be displayed with the reported failing test case.
365 ///
366 /// Only prints during the final replay of a failing test case.
367 ///
368 /// # Example
369 ///
370 /// ```no_run
371 /// use hegel::generators as gs;
372 ///
373 /// #[hegel::test]
374 /// fn my_test(tc: hegel::TestCase) {
375 /// let x: i32 = tc.draw(gs::integers());
376 /// tc.note(&format!("Generated x = {}", x));
377 /// }
378 /// ```
379 pub fn note(&self, message: &str) {
380 if !self.global.is_last_run {
381 return;
382 }
383 let local = self.local.borrow();
384 let indent = local.indent;
385 (local.on_draw)(&format!("{:indent$}{}", "", message, indent = indent));
386 }
387
388 /// Run `body` in a loop that should runs "logically infinitely" or until
389 /// error. Roughly equivalent to a `loop` but with better interaction with
390 /// the test runner: This loop will never exit until the test case completes.
391 ///
392 /// At the start of each iteration a `// Loop iteration N` note is emitted
393 /// into the failing-test replay output.
394 ///
395 /// # Example
396 ///
397 /// ```no_run
398 /// use hegel::generators as gs;
399 ///
400 /// #[hegel::test]
401 /// fn my_test(tc: hegel::TestCase) {
402 /// let mut total: i32 = 0;
403 /// tc.repeat(|| {
404 /// let n: i32 = tc.draw(gs::integers().min_value(0).max_value(10));
405 /// total += n;
406 /// assert!(total >= 0);
407 /// });
408 /// }
409 /// ```
410 pub fn repeat<F: FnMut()>(&self, mut body: F) -> ! {
411 if self.global.mode == Mode::SingleTestCase {
412 self.repeat_single_test_case(&mut body);
413 }
414 self.repeat_property_test(&mut body);
415 }
416
417 fn repeat_single_test_case(&self, body: &mut dyn FnMut()) -> ! {
418 let mut iteration: u64 = 0;
419 loop {
420 iteration += 1;
421 self.note(&format!("// Repetition #{}", iteration));
422
423 let prev_indent = self.local.borrow().indent;
424 self.local.borrow_mut().indent = prev_indent + 2;
425 body();
426 self.local.borrow_mut().indent = prev_indent;
427 }
428 }
429
430 fn repeat_property_test(&self, body: &mut dyn FnMut()) -> ! {
431 use crate::generators::{booleans, integers};
432
433 const MAX_SAFE_MIN_SIZE: usize = 1 << 40;
434 let min_size = self.draw_silent(integers::<usize>().max_value(MAX_SAFE_MIN_SIZE));
435
436 let mut collection = Collection::new(self, min_size, None);
437 let mut iteration: u64 = 0;
438
439 while collection.more() {
440 iteration += 1;
441 self.note(&format!("// Repetition #{}", iteration));
442
443 let prev_indent = self.local.borrow().indent;
444 self.local.borrow_mut().indent = prev_indent + 2;
445 let result = catch_unwind(AssertUnwindSafe(&mut *body));
446 self.local.borrow_mut().indent = prev_indent;
447
448 match result {
449 Ok(()) => {}
450 Err(e) => {
451 let msg = panic_message(&e);
452 if msg == ASSUME_FAIL_STRING {
453 } else if msg == STOP_TEST_STRING {
454 resume_unwind(e);
455 } else {
456 self.draw_silent(booleans());
457 resume_unwind(e);
458 }
459 }
460 }
461 }
462
463 panic!("{}", LOOP_DONE_STRING);
464 }
465
466 pub(crate) fn child(&self, extra_indent: usize) -> Self {
467 let local = self.local.borrow();
468 TestCase {
469 global: self.global.clone(),
470 local: RefCell::new(TestCaseLocalData {
471 span_depth: 0,
472 indent: local.indent + extra_indent,
473 on_draw: local.on_draw.clone(),
474 }),
475 }
476 }
477
478 fn record_named_draw<T: std::fmt::Debug>(&self, value: &T, name: &str, repeatable: bool) {
479 let display_name = self.with_shared(|shared| {
480 let draw_state = &mut shared.draw_state;
481
482 match draw_state.named_draw_repeatable.get(name) {
483 Some(&prev) if prev != repeatable => {
484 panic!(
485 "__draw_named: name {:?} used with inconsistent repeatable flag (was {}, now {}). \
486 If you have not called __draw_named deliberately yourself, this is likely a bug in \
487 hegel. Please file a bug report at https://github.com/hegeldev/hegel-rust/issues",
488 name, prev, repeatable
489 );
490 }
491 _ => {
492 draw_state
493 .named_draw_repeatable
494 .insert(name.to_string(), repeatable);
495 }
496 }
497
498 let count = draw_state
499 .named_draw_counts
500 .entry(name.to_string())
501 .or_insert(0);
502 *count += 1;
503 let current_count = *count;
504
505 if !repeatable && current_count > 1 {
506 panic!(
507 "__draw_named: name {:?} used more than once but repeatable is false. \
508 This is almost certainly a bug in hegel - please report it at https://github.com/hegeldev/hegel-rust/issues",
509 name
510 );
511 }
512
513 if repeatable {
514 let mut candidate = current_count;
515 loop {
516 let name = format!("{}_{}", name, candidate);
517 if draw_state.allocated_display_names.insert(name.clone()) {
518 break name;
519 }
520 candidate += 1;
521 }
522 } else {
523 let name = name.to_string();
524 draw_state.allocated_display_names.insert(name.clone());
525 name
526 }
527 });
528
529 let local = self.local.borrow();
530 let indent = local.indent;
531
532 (local.on_draw)(&format!(
533 "{:indent$}let {} = {:?};",
534 "",
535 display_name,
536 value,
537 indent = indent
538 ));
539 }
540
541 /// Run `f` with access to this test case's data source.
542 ///
543 /// Acquires the shared mutex for the duration of the call so
544 /// concurrent threads don't scramble backend traffic. The closure
545 /// must not call back into any other `TestCase` method that would
546 /// re-acquire the shared mutex.
547 pub(crate) fn with_data_source<R>(&self, f: impl FnOnce(&dyn DataSource) -> R) -> R {
548 self.with_shared(|shared| f(shared.data_source.as_ref()))
549 }
550
551 /// Report whether the test case has been aborted (StopTest/overflow).
552 ///
553 /// Used by the runner to decide whether to send `mark_complete`.
554 pub(crate) fn test_aborted(&self) -> bool {
555 self.with_data_source(|ds| ds.test_aborted())
556 }
557
558 /// Send `mark_complete` on this test case's data source.
559 pub(crate) fn mark_complete(&self, status: &str, origin: Option<&str>) {
560 self.with_data_source(|ds| ds.mark_complete(status, origin));
561 }
562
563 #[doc(hidden)]
564 pub fn start_span(&self, label: u64) {
565 self.local.borrow_mut().span_depth += 1;
566 if let Err(e) = self.with_data_source(|ds| ds.start_span(label)) {
567 // nocov start
568 let mut local = self.local.borrow_mut();
569 assert!(local.span_depth > 0);
570 local.span_depth -= 1;
571 drop(local);
572 panic_on_data_source_error(e);
573 // nocov end
574 }
575 }
576
577 #[doc(hidden)]
578 pub fn stop_span(&self, discard: bool) {
579 {
580 let mut local = self.local.borrow_mut();
581 assert!(local.span_depth > 0);
582 local.span_depth -= 1;
583 }
584 let _ = self.with_data_source(|ds| ds.stop_span(discard));
585 }
586}
587
588/// Send a schema to the backend and return the raw CBOR response.
589#[doc(hidden)]
590pub fn generate_raw(tc: &TestCase, schema: &Value) -> Value {
591 match tc.with_data_source(|ds| ds.generate(schema)) {
592 Ok(v) => v,
593 Err(e) => panic_on_data_source_error(e),
594 }
595}
596
597#[doc(hidden)]
598pub fn generate_from_schema<T: serde::de::DeserializeOwned>(tc: &TestCase, schema: &Value) -> T {
599 deserialize_value(generate_raw(tc, schema))
600}
601
602/// Deserialize a raw CBOR value into a Rust type.
603///
604/// This is a public helper for use by derived generators (proc macros)
605/// that need to deserialize individual field values from CBOR.
606pub fn deserialize_value<T: serde::de::DeserializeOwned>(raw: Value) -> T {
607 let hv = value::HegelValue::from(raw.clone());
608 value::from_hegel_value(hv).unwrap_or_else(|e| {
609 panic!("Failed to deserialize value: {}\nValue: {:?}", e, raw); // nocov
610 })
611}
612
613/// Uses the backend to determine collection sizing.
614///
615/// The backend-side collection object is created lazily on the first call to
616/// [`more()`](Collection::more).
617pub struct Collection<'a> {
618 tc: &'a TestCase,
619 min_size: usize,
620 max_size: Option<usize>,
621 handle: Option<String>,
622 finished: bool,
623}
624
625impl<'a> Collection<'a> {
626 /// Create a new backend-managed collection.
627 pub fn new(tc: &'a TestCase, min_size: usize, max_size: Option<usize>) -> Self {
628 Collection {
629 tc,
630 min_size,
631 max_size,
632 handle: None,
633 finished: false,
634 }
635 }
636
637 fn ensure_initialized(&mut self) -> &str {
638 if self.handle.is_none() {
639 let result = self.tc.with_data_source(|ds| {
640 ds.new_collection(self.min_size as u64, self.max_size.map(|m| m as u64))
641 });
642 let name = match result {
643 Ok(name) => name,
644 Err(e) => panic_on_data_source_error(e), // nocov
645 };
646 self.handle = Some(name);
647 }
648 self.handle.as_ref().unwrap()
649 }
650
651 /// Ask the backend whether to produce another element.
652 pub fn more(&mut self) -> bool {
653 if self.finished {
654 return false; // nocov
655 }
656 let handle = self.ensure_initialized().to_string();
657 let result = match self.tc.with_data_source(|ds| ds.collection_more(&handle)) {
658 Ok(b) => b,
659 Err(e) => {
660 self.finished = true;
661 panic_on_data_source_error(e);
662 }
663 };
664 if !result {
665 self.finished = true;
666 }
667 result
668 }
669
670 /// Reject the last element (don't count it towards the size budget).
671 pub fn reject(&mut self, why: Option<&str>) {
672 // nocov start
673 if self.finished {
674 return;
675 }
676 let handle = self.ensure_initialized().to_string();
677 let _ = self
678 .tc
679 .with_data_source(|ds| ds.collection_reject(&handle, why));
680 // nocov end
681 }
682}
683
684#[doc(hidden)]
685pub mod labels {
686 pub const LIST: u64 = 1;
687 pub const LIST_ELEMENT: u64 = 2;
688 pub const SET: u64 = 3;
689 pub const SET_ELEMENT: u64 = 4;
690 pub const MAP: u64 = 5;
691 pub const MAP_ENTRY: u64 = 6;
692 pub const TUPLE: u64 = 7;
693 pub const ONE_OF: u64 = 8;
694 pub const OPTIONAL: u64 = 9;
695 pub const FIXED_DICT: u64 = 10;
696 pub const FLAT_MAP: u64 = 11;
697 pub const FILTER: u64 = 12;
698 pub const MAPPED: u64 = 13;
699 pub const SAMPLED_FROM: u64 = 14;
700 pub const ENUM_VARIANT: u64 = 15;
701}