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 // A semantically-invalid schema. In the main library this only fires
51 // if a generator builds a malformed schema (a bug), so we surface the
52 // diagnostic as a panic. libhegel never reaches here — it maps the
53 // error to `HEGEL_E_INVALID_ARG` instead.
54 DataSourceError::InvalidArgument(msg) => panic!("{}", msg),
55 }
56}
57
58pub(crate) struct TestCaseGlobalData {
59 mode: Mode,
60 /// Whether drawn-value records and notes are surfaced for this test case
61 /// (true on the final replay of a failure, or when verbose output is on).
62 /// When false `on_draw` is a no-op, so the draw-recording bookkeeping in
63 /// [`TestCase::record_named_draw`] (display-name allocation + `Debug`
64 /// rendering of the value) can be skipped entirely.
65 emit: bool,
66 /// Fine-grained lock over the state shared between clones of a
67 /// `TestCase`. Acquired briefly around each individual backend call
68 /// and around each mutation of the draw-tracking bookkeeping, not
69 /// around entire user-visible operations like a `draw`. The mutex is
70 /// non-reentrant; no method holds it while calling back into
71 /// `TestCase`.
72 shared: Mutex<SharedState>,
73}
74
75pub(crate) struct SharedState {
76 data_source: Box<dyn DataSource + Send + Sync>,
77 draw_state: DrawState,
78}
79
80pub(crate) struct DrawState {
81 named_draw_counts: HashMap<String, usize>,
82 named_draw_repeatable: HashMap<String, bool>,
83 allocated_display_names: HashSet<String>,
84}
85
86#[derive(Clone)]
87pub(crate) struct TestCaseLocalData {
88 span_depth: usize,
89 indent: usize,
90 on_draw: OutputSink,
91}
92
93/// A handle to the current test case.
94///
95/// This is passed to `#[hegel::test]` functions and provides methods
96/// for drawing values, making assumptions, and recording notes.
97///
98/// # Example
99///
100/// ```no_run
101/// use hegel::generators as gs;
102///
103/// #[hegel::test]
104/// fn my_test(tc: hegel::TestCase) {
105/// let x: i32 = tc.draw(gs::integers());
106/// tc.assume(x > 0);
107/// tc.note(&format!("x = {}", x));
108/// }
109/// ```
110///
111/// # Threading
112///
113/// `TestCase` is `Send` but not `Sync`. To drive generation from another
114/// thread, clone the test case and move the clone. Clones share the same
115/// underlying backend connection — they are views onto one test case, not
116/// independent test cases.
117///
118/// ```no_run
119/// use hegel::generators as gs;
120///
121/// #[hegel::test]
122/// fn my_test(tc: hegel::TestCase) {
123/// let tc_worker = tc.clone();
124/// let handle = std::thread::spawn(move || {
125/// tc_worker.draw(gs::integers::<i32>())
126/// });
127/// let n = handle.join().unwrap();
128/// let _b: bool = tc.draw(gs::booleans());
129/// let _ = n;
130/// }
131/// ```
132///
133/// ## What is guaranteed
134///
135/// Individual backend operations (a single `generate`, `start_span`,
136/// `stop_span`, or pool/collection call) are serialised by a shared
137/// mutex, so the bytes on the wire to the backend stay well-formed no
138/// matter how clones are used across threads.
139///
140/// This is enough for patterns where threads do not race on generation —
141/// for example:
142///
143/// - Spawn a worker, let it draw, `join` it, then continue on the main
144/// thread.
145/// - Repeatedly spawn-and-join one worker at a time.
146/// - Any pattern where exactly one thread is drawing at a time, with a
147/// happens-before relationship (join, channel receive, barrier) between
148/// each thread's work.
149///
150/// ## What is not guaranteed
151///
152/// Concurrent generation will get progressively better over time, but
153/// right now should be considered a borderline-internal feature. If
154/// you do not know exactly what you're doing it probably won't work.
155///
156/// Two or more threads drawing concurrently from clones of the same
157/// `TestCase` is allowed by the type system but is **not deterministic**:
158/// the order in which draws interleave depends on thread scheduling, and
159/// the backend has no way to reproduce that order on replay. Composite
160/// draws are also not atomic with respect to other threads — another
161/// thread's draws can land between this thread's `start_span` and
162/// `stop_span`, corrupting the shrink-friendly span structure. In
163/// practice this means such tests may:
164///
165/// - Produce different values on successive runs of the same seed.
166/// - Shrink poorly or not at all.
167/// - Surface backend errors (e.g. `StopTest`) in one thread caused by
168/// another thread's draws exhausting the budget.
169///
170/// ## Panics inside spawned threads
171///
172/// If a worker thread panics with an assumption failure or a backend
173/// `StopTest`, that panic stays inside the thread's `JoinHandle` until
174/// the main thread joins it. The main thread is responsible for
175/// propagating (or suppressing) the panic — typically by calling
176/// `handle.join().unwrap()`, which resumes the panic on the main thread
177/// so Hegel's runner can observe it.
178pub struct TestCase {
179 global: Arc<TestCaseGlobalData>,
180 // RefCell makes `TestCase: !Sync`. Local data is per-clone: each clone gets
181 // its own span depth, indent, and on_draw. Concurrent use across threads
182 // therefore requires cloning, which is enforced by the `!Sync` bound.
183 local: RefCell<TestCaseLocalData>,
184}
185
186impl Clone for TestCase {
187 fn clone(&self) -> Self {
188 TestCase {
189 global: self.global.clone(),
190 local: RefCell::new(self.local.borrow().clone()),
191 }
192 }
193}
194
195impl std::fmt::Debug for TestCase {
196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197 f.debug_struct("TestCase").finish_non_exhaustive()
198 }
199}
200
201/// A callback invoked for each line of draw/note output during the final replay.
202pub(crate) type OutputSink = Arc<dyn Fn(&str) + Send + Sync>;
203
204thread_local! {
205 static OUTPUT_OVERRIDE: RefCell<Option<OutputSink>> = const { RefCell::new(None) };
206}
207
208/// Install a custom output sink for the duration of `f`, replacing the usual
209/// `eprintln!` behavior of draw and note output. Intended for tests that want
210/// to capture what a test case would print.
211///
212/// While active, notes and draws from the final replay go to `sink` instead of
213/// stderr. Non-final test cases still drop their draw/note output as usual.
214#[doc(hidden)]
215pub fn with_output_override<R>(sink: OutputSink, f: impl FnOnce() -> R) -> R {
216 let prev = OUTPUT_OVERRIDE.with(|cell| cell.borrow_mut().replace(sink));
217 let result = f();
218 OUTPUT_OVERRIDE.with(|cell| *cell.borrow_mut() = prev);
219 result
220}
221
222/// Return a clone of the currently-installed output sink, if any. Lets the
223/// run lifecycle's verbose output (stop-reason lines, per-test-case panic
224/// diagnostics) flow through `with_output_override` so tests can capture
225/// them in-process without having to spawn a subprocess.
226pub(crate) fn current_output_sink() -> Option<OutputSink> {
227 OUTPUT_OVERRIDE.with(|cell| cell.borrow().clone())
228}
229
230/// Emit a single line of verbose runner output, going through the
231/// installed output sink if there is one and otherwise to stderr.
232pub(crate) fn emit_verbose_line(msg: &str) {
233 if let Some(sink) = current_output_sink() {
234 sink(msg);
235 } else {
236 eprintln!("{}", msg);
237 }
238}
239
240fn panic_message(payload: &Box<dyn Any + Send>) -> String {
241 if let Some(s) = payload.downcast_ref::<&str>() {
242 s.to_string()
243 } else if let Some(s) = payload.downcast_ref::<String>() {
244 s.clone()
245 } else {
246 "Unknown panic".to_string() // nocov
247 }
248}
249
250impl TestCase {
251 pub(crate) fn new(
252 data_source: Box<dyn DataSource + Send + Sync>,
253 is_last_run: bool,
254 mode: Mode,
255 verbose: bool,
256 ) -> Self {
257 let override_sink = current_output_sink();
258 let should_emit = is_last_run || verbose;
259 let on_draw: OutputSink = match override_sink {
260 Some(sink) if should_emit => sink,
261 _ if should_emit => Arc::new(|msg| eprintln!("{}", msg)),
262 _ => Arc::new(|_| {}),
263 };
264 TestCase {
265 global: Arc::new(TestCaseGlobalData {
266 mode,
267 emit: should_emit,
268 shared: Mutex::new(SharedState {
269 data_source,
270 draw_state: DrawState {
271 named_draw_counts: HashMap::new(),
272 named_draw_repeatable: HashMap::new(),
273 allocated_display_names: HashSet::new(),
274 },
275 }),
276 }),
277 local: RefCell::new(TestCaseLocalData {
278 span_depth: 0,
279 indent: 0,
280 on_draw,
281 }),
282 }
283 }
284
285 pub(crate) fn mode(&self) -> Mode {
286 self.global.mode
287 }
288
289 /// Acquire the shared mutex for the duration of `f`.
290 ///
291 /// Held briefly around individual backend calls or draw-state updates,
292 /// never around whole user-visible operations. The mutex is
293 /// non-reentrant, so `f` must not call any other method that also
294 /// acquires the shared mutex.
295 fn with_shared<R>(&self, f: impl FnOnce(&mut SharedState) -> R) -> R {
296 let mut guard = self.global.shared.lock();
297 f(&mut guard)
298 }
299
300 /// Draw a value from a generator.
301 ///
302 /// # Example
303 ///
304 /// ```no_run
305 /// use hegel::generators as gs;
306 ///
307 /// #[hegel::test]
308 /// fn my_test(tc: hegel::TestCase) {
309 /// let x: i32 = tc.draw(gs::integers());
310 /// let s: String = tc.draw(gs::text());
311 /// }
312 /// ```
313 ///
314 /// Note: when run inside a `#[hegel::test]`, `draw()` will typically be
315 /// rewritten to `__draw_named()` with an appropriate variable name
316 /// in order to give better test output.
317 pub fn draw<T: std::fmt::Debug>(&self, generator: impl Generator<T>) -> T {
318 self.__draw_named(generator, "draw", true)
319 }
320
321 /// Draw a value from a generator with a specific name for output.
322 ///
323 /// When `repeatable` is true, a counter suffix is appended (e.g. `x_1`, `x_2`).
324 /// When `repeatable` is false, reusing the same name panics.
325 ///
326 /// Using the same name with different values of `repeatable` is an error.
327 ///
328 /// On the final replay of a failing test case, this prints:
329 /// - `let name = value;` (when not repeatable)
330 /// - `let name_N = value;` (when repeatable)
331 ///
332 /// Not intended for direct use. This is the target that `#[hegel::test]` rewrites `draw()`
333 /// calls to where appropriate.
334 pub fn __draw_named<T: std::fmt::Debug>(
335 &self,
336 generator: impl Generator<T>,
337 name: &str,
338 repeatable: bool,
339 ) -> T {
340 let value = generator.do_draw(self);
341 if self.local.borrow().span_depth == 0 {
342 self.record_named_draw(&value, name, repeatable);
343 }
344 value
345 }
346
347 /// Draw a value from a generator without recording it in the output.
348 ///
349 /// Unlike [`draw`](Self::draw), this does not require `T: Debug` and
350 /// will not print the value in the failing-test summary.
351 pub fn draw_silent<T>(&self, generator: impl Generator<T>) -> T {
352 generator.do_draw(self)
353 }
354
355 /// Assume a condition is true. If false, reject the current test input.
356 ///
357 /// # Example
358 ///
359 /// ```no_run
360 /// use hegel::generators as gs;
361 ///
362 /// #[hegel::test]
363 /// fn my_test(tc: hegel::TestCase) {
364 /// let age: u32 = tc.draw(gs::integers());
365 /// tc.assume(age >= 18);
366 /// }
367 /// ```
368 pub fn assume(&self, condition: bool) {
369 if !condition {
370 self.reject();
371 }
372 }
373
374 /// Reject the current test input unconditionally.
375 ///
376 /// Equivalent to `assume(false)`, but with a `!` return type so that code
377 /// following the call is statically known to be unreachable.
378 ///
379 /// # Example
380 ///
381 /// ```no_run
382 /// use hegel::generators as gs;
383 ///
384 /// #[hegel::test]
385 /// fn my_test(tc: hegel::TestCase) {
386 /// let n: i32 = tc.draw(gs::integers());
387 /// let positive: u32 = match u32::try_from(n) {
388 /// Ok(v) => v,
389 /// Err(_) => tc.reject(),
390 /// };
391 /// let _ = positive;
392 /// }
393 /// ```
394 pub fn reject(&self) -> ! {
395 panic!("{}", ASSUME_FAIL_STRING);
396 }
397
398 /// Note a message which will be displayed with the reported failing test case.
399 ///
400 /// At the default verbosity, only prints during the final replay of a
401 /// failing test case. At [`Verbose`](crate::Verbosity::Verbose) or
402 /// higher, prints on every test case.
403 ///
404 /// # Example
405 ///
406 /// ```no_run
407 /// use hegel::generators as gs;
408 ///
409 /// #[hegel::test]
410 /// fn my_test(tc: hegel::TestCase) {
411 /// let x: i32 = tc.draw(gs::integers());
412 /// tc.note(&format!("Generated x = {}", x));
413 /// }
414 /// ```
415 pub fn note(&self, message: &str) {
416 let local = self.local.borrow();
417 let indent = local.indent;
418 (local.on_draw)(&format!("{:indent$}{}", "", message, indent = indent));
419 }
420
421 /// Record a targeting observation to help the engine find extreme inputs.
422 ///
423 /// Call this inside a test body to guide generation toward inputs that
424 /// maximise `score`. Inside a `#[hegel::test]`, `#[hegel::main]`, or
425 /// `#[hegel::standalone_function]` body, `tc.target(expr)` is rewritten
426 /// to call [`target_labelled`](Self::target_labelled) with the source
427 /// text of `expr` as the label, so different targeting expressions are
428 /// tracked separately by default. Outside that rewrite, `tc.target(score)`
429 /// uses the empty label.
430 ///
431 /// Has no effect during replays or if the test case has been aborted.
432 ///
433 /// # Example
434 ///
435 /// ```no_run
436 /// use hegel::generators as gs;
437 ///
438 /// #[hegel::test]
439 /// fn my_test(tc: hegel::TestCase) {
440 /// let n: u32 = tc.draw(gs::integers::<u32>());
441 /// tc.target(n as f64);
442 /// }
443 /// ```
444 pub fn target(&self, score: f64) {
445 self.target_labelled(score, "");
446 }
447
448 /// Record a targeting observation under an explicit label.
449 ///
450 /// The label distinguishes multiple simultaneous targeting goals.
451 /// Use this directly when you want a specific label string;
452 /// [`target`](Self::target) is the usual entry point and will be
453 /// rewritten to call this with the source expression as the label
454 /// inside a `#[hegel::test]` body.
455 ///
456 /// Has no effect during replays or if the test case has been aborted.
457 pub fn target_labelled(&self, score: f64, label: impl Into<String>) {
458 let label = label.into();
459 self.with_data_source(|ds| ds.target_observation(score, &label));
460 }
461
462 /// Run `body` in a loop that should runs "logically infinitely" or until
463 /// error. Roughly equivalent to a `loop` but with better interaction with
464 /// the test runner: This loop will never exit until the test case completes.
465 ///
466 /// At the start of each iteration a `// Loop iteration N` note is emitted
467 /// into the failing-test replay output.
468 ///
469 /// # Example
470 ///
471 /// ```no_run
472 /// use hegel::generators as gs;
473 ///
474 /// #[hegel::test]
475 /// fn my_test(tc: hegel::TestCase) {
476 /// let mut total: i32 = 0;
477 /// tc.repeat(|| {
478 /// let n: i32 = tc.draw(gs::integers().min_value(0).max_value(10));
479 /// total += n;
480 /// assert!(total >= 0);
481 /// });
482 /// }
483 /// ```
484 pub fn repeat<F: FnMut()>(&self, mut body: F) -> ! {
485 if self.global.mode == Mode::SingleTestCase {
486 self.repeat_single_test_case(&mut body);
487 }
488 self.repeat_property_test(&mut body);
489 }
490
491 fn repeat_single_test_case(&self, body: &mut dyn FnMut()) -> ! {
492 let mut iteration: u64 = 0;
493 loop {
494 iteration += 1;
495 self.note(&format!("// Repetition #{}", iteration));
496
497 let prev_indent = self.local.borrow().indent;
498 self.local.borrow_mut().indent = prev_indent + 2;
499 body();
500 self.local.borrow_mut().indent = prev_indent;
501 }
502 }
503
504 fn repeat_property_test(&self, body: &mut dyn FnMut()) -> ! {
505 use crate::generators::{booleans, integers};
506
507 const MAX_SAFE_MIN_SIZE: usize = 1 << 40;
508 let min_size = self.draw_silent(integers::<usize>().max_value(MAX_SAFE_MIN_SIZE));
509
510 let mut collection = Collection::new(self, min_size, None);
511 let mut iteration: u64 = 0;
512
513 while collection.more() {
514 iteration += 1;
515 self.note(&format!("// Repetition #{}", iteration));
516
517 let prev_indent = self.local.borrow().indent;
518 self.local.borrow_mut().indent = prev_indent + 2;
519 let result = catch_unwind(AssertUnwindSafe(&mut *body));
520 self.local.borrow_mut().indent = prev_indent;
521
522 match result {
523 Ok(()) => {}
524 Err(e) => {
525 let msg = panic_message(&e);
526 if msg == ASSUME_FAIL_STRING {
527 } else if msg == STOP_TEST_STRING {
528 resume_unwind(e);
529 } else {
530 self.draw_silent(booleans());
531 resume_unwind(e);
532 }
533 }
534 }
535 }
536
537 panic!("{}", LOOP_DONE_STRING);
538 }
539
540 pub(crate) fn child(&self, extra_indent: usize) -> Self {
541 let local = self.local.borrow();
542 TestCase {
543 global: self.global.clone(),
544 local: RefCell::new(TestCaseLocalData {
545 span_depth: 0,
546 indent: local.indent + extra_indent,
547 on_draw: local.on_draw.clone(),
548 }),
549 }
550 }
551
552 fn record_named_draw<T: std::fmt::Debug>(&self, value: &T, name: &str, repeatable: bool) {
553 // The drawn-value record is only ever surfaced through `on_draw`, which
554 // is a no-op unless this is the final replay or verbose output is on.
555 // On ordinary generation/shrinking runs we therefore skip the
556 // display-name allocation and the (often expensive, e.g. Unicode) Debug
557 // render entirely. The usage-error checks below still run every test
558 // case so that misuse fails deterministically, not only on a failure's
559 // final replay.
560 let emit = self.global.emit;
561
562 let display_name = self.with_shared(|shared| {
563 let draw_state = &mut shared.draw_state;
564
565 match draw_state.named_draw_repeatable.get(name) {
566 Some(&prev) if prev != repeatable => {
567 panic!(
568 "__draw_named: name {:?} used with inconsistent repeatable flag (was {}, now {}). \
569 If you have not called __draw_named deliberately yourself, this is likely a bug in \
570 hegel. Please file a bug report at https://github.com/hegeldev/hegel-rust/issues",
571 name, prev, repeatable
572 );
573 }
574 Some(_) => {}
575 // Only the first occurrence of a name needs to allocate the key.
576 None => {
577 draw_state
578 .named_draw_repeatable
579 .insert(name.to_string(), repeatable);
580 }
581 }
582
583 // Look the counter up by `&str` first so repeated draws of the same
584 // name (e.g. a `draw` inside a loop) don't allocate a fresh key on
585 // every call.
586 let current_count = match draw_state.named_draw_counts.get_mut(name) {
587 Some(count) => {
588 *count += 1;
589 *count
590 }
591 None => {
592 draw_state.named_draw_counts.insert(name.to_string(), 1);
593 1
594 }
595 };
596
597 if !repeatable && current_count > 1 {
598 panic!(
599 "__draw_named: name {:?} used more than once but repeatable is false. \
600 This is almost certainly a bug in hegel - please report it at https://github.com/hegeldev/hegel-rust/issues",
601 name
602 );
603 }
604
605 // Display-name uniqueness bookkeeping is output-only; skip it (and
606 // its allocations) when nothing will be emitted.
607 if !emit {
608 return None;
609 }
610
611 let display = if repeatable {
612 let mut candidate = current_count;
613 loop {
614 let name = format!("{}_{}", name, candidate);
615 if draw_state.allocated_display_names.insert(name.clone()) {
616 break name;
617 }
618 candidate += 1;
619 }
620 } else {
621 let name = name.to_string();
622 draw_state.allocated_display_names.insert(name.clone());
623 name
624 };
625 Some(display)
626 });
627
628 let Some(display_name) = display_name else {
629 return;
630 };
631
632 let local = self.local.borrow();
633 let indent = local.indent;
634
635 (local.on_draw)(&format!(
636 "{:indent$}let {} = {:?};",
637 "",
638 display_name,
639 value,
640 indent = indent
641 ));
642 }
643
644 /// Run `f` with access to this test case's data source.
645 ///
646 /// Acquires the shared mutex for the duration of the call so
647 /// concurrent threads don't scramble backend traffic. The closure
648 /// must not call back into any other `TestCase` method that would
649 /// re-acquire the shared mutex.
650 pub(crate) fn with_data_source<R>(&self, f: impl FnOnce(&dyn DataSource) -> R) -> R {
651 self.with_shared(|shared| f(shared.data_source.as_ref()))
652 }
653
654 /// Send `mark_complete` on this test case's data source.
655 ///
656 /// Both backends use this to communicate the outcome — the full
657 /// [`TestCaseResult`], including any captured [`Failure`] — to whatever
658 /// owns the per-test-case bookkeeping (Hypothesis on the server backend;
659 /// the native engine, via a per-test-case outcome handle, on the native
660 /// backend).
661 pub(crate) fn mark_complete(&self, result: &crate::backend::TestCaseResult) {
662 self.with_data_source(|ds| ds.mark_complete(result));
663 }
664
665 #[doc(hidden)]
666 pub fn start_span(&self, label: u64) {
667 self.local.borrow_mut().span_depth += 1;
668 if let Err(e) = self.with_data_source(|ds| ds.start_span(label)) {
669 // nocov start
670 let mut local = self.local.borrow_mut();
671 assert!(local.span_depth > 0);
672 local.span_depth -= 1;
673 drop(local);
674 panic_on_data_source_error(e);
675 // nocov end
676 }
677 }
678
679 #[doc(hidden)]
680 pub fn stop_span(&self, discard: bool) {
681 {
682 let mut local = self.local.borrow_mut();
683 assert!(local.span_depth > 0);
684 local.span_depth -= 1;
685 }
686 let _ = self.with_data_source(|ds| ds.stop_span(discard));
687 }
688}
689
690/// Send a schema to the backend and return the raw CBOR response.
691#[doc(hidden)]
692pub fn generate_raw(tc: &TestCase, schema: &Value) -> Value {
693 match tc.with_data_source(|ds| ds.generate(schema)) {
694 Ok(v) => v,
695 Err(e) => panic_on_data_source_error(e),
696 }
697}
698
699#[doc(hidden)]
700pub fn generate_from_schema<T: serde::de::DeserializeOwned>(tc: &TestCase, schema: &Value) -> T {
701 deserialize_value(generate_raw(tc, schema))
702}
703
704/// Deserialize a raw CBOR value into a Rust type.
705///
706/// This is a public helper for use by derived generators (proc macros)
707/// that need to deserialize individual field values from CBOR.
708pub fn deserialize_value<T: serde::de::DeserializeOwned>(raw: Value) -> T {
709 let hv = value::HegelValue::from(raw.clone());
710 value::from_hegel_value(hv).unwrap_or_else(|e| {
711 panic!("Failed to deserialize value: {}\nValue: {:?}", e, raw); // nocov
712 })
713}
714
715/// Uses the backend to determine collection sizing.
716///
717/// The backend-side collection object is created lazily on the first call to
718/// [`more()`](Collection::more).
719pub struct Collection<'a> {
720 tc: &'a TestCase,
721 min_size: usize,
722 max_size: Option<usize>,
723 handle: Option<i64>,
724 finished: bool,
725}
726
727impl<'a> Collection<'a> {
728 /// Create a new backend-managed collection.
729 pub fn new(tc: &'a TestCase, min_size: usize, max_size: Option<usize>) -> Self {
730 Collection {
731 tc,
732 min_size,
733 max_size,
734 handle: None,
735 finished: false,
736 }
737 }
738
739 fn ensure_initialized(&mut self) -> i64 {
740 if self.handle.is_none() {
741 let result = self.tc.with_data_source(|ds| {
742 ds.new_collection(self.min_size as u64, self.max_size.map(|m| m as u64))
743 });
744 let id = match result {
745 Ok(id) => id,
746 Err(e) => panic_on_data_source_error(e), // nocov
747 };
748 self.handle = Some(id);
749 }
750 self.handle.unwrap()
751 }
752
753 /// Ask the backend whether to produce another element.
754 pub fn more(&mut self) -> bool {
755 if self.finished {
756 return false; // nocov
757 }
758 let handle = self.ensure_initialized();
759 let result = match self.tc.with_data_source(|ds| ds.collection_more(handle)) {
760 Ok(b) => b,
761 Err(e) => {
762 self.finished = true;
763 panic_on_data_source_error(e);
764 }
765 };
766 if !result {
767 self.finished = true;
768 }
769 result
770 }
771
772 /// Reject the last element (don't count it towards the size budget).
773 pub fn reject(&mut self, why: Option<&str>) {
774 // nocov start
775 if self.finished {
776 return;
777 }
778 let handle = self.ensure_initialized();
779 let _ = self
780 .tc
781 .with_data_source(|ds| ds.collection_reject(handle, why));
782 // nocov end
783 }
784}
785
786#[doc(hidden)]
787pub mod labels {
788 pub const LIST: u64 = 1;
789 pub const LIST_ELEMENT: u64 = 2;
790 pub const SET: u64 = 3;
791 pub const SET_ELEMENT: u64 = 4;
792 pub const MAP: u64 = 5;
793 pub const MAP_ENTRY: u64 = 6;
794 pub const TUPLE: u64 = 7;
795 pub const ONE_OF: u64 = 8;
796 pub const OPTIONAL: u64 = 9;
797 pub const FIXED_DICT: u64 = 10;
798 pub const FLAT_MAP: u64 = 11;
799 pub const FILTER: u64 = 12;
800 pub const MAPPED: u64 = 13;
801 pub const SAMPLED_FROM: u64 = 14;
802 pub const ENUM_VARIANT: u64 = 15;
803}
804
805#[cfg(test)]
806#[path = "../tests/embedded/test_case_tests.rs"]
807mod tests;