1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
//! Tests for sharing a TestCase across threads.
//!
//! `TestCase` is `Send` (but not `Sync`): a clone may be moved to another
//! thread, and data generation works from that thread. A shared mutex
//! serialises individual backend calls, so concurrent backend traffic never
//! interleaves mid-message.
//!
//! These tests cover *fully deterministic* uses where the test interleaves
//! thread work but does not race — that is, one thread does work, another
//! picks up after it, and the outcome is independent of scheduling.
mod common;
use hegel::TestCase;
use hegel::generators as gs;
use std::sync::{Arc, Mutex};
/// Compile-time check: `TestCase` implements `Send`.
fn _assert_test_case_is_send()
where
TestCase: Send,
{
}
/// Compile-time check: `TestCase` does *not* implement `Sync`.
///
/// Uses the standard trick of two blanket impls that would be ambiguous iff
/// `TestCase: Sync`. If someone accidentally makes `TestCase: Sync`, the
/// `not_sync` function below fails to compile with an ambiguity error.
#[allow(dead_code)]
fn _assert_test_case_is_not_sync() {
trait AmbiguousIfSync<A> {
fn some_item() {}
}
impl<T: ?Sized> AmbiguousIfSync<()> for T {}
impl<T: ?Sized + Sync> AmbiguousIfSync<u8> for T {}
<TestCase as AmbiguousIfSync<_>>::some_item();
}
/// Compile-time check: `ExplicitTestCase` does *not* implement `Send`.
///
/// Explicit test cases hold user values as `Box<dyn Any>` inside a `RefCell`,
/// neither of which is `Send` or `Sync`. This means a test body that spawns a
/// thread touching `tc` cannot be combined with `#[hegel::explicit_test_case]`
/// — the macro rebinds `tc` to `&ExplicitTestCase` for the explicit run and
/// `&ExplicitTestCase` is not `Send`.
#[allow(dead_code)]
fn _assert_explicit_test_case_is_not_send() {
trait AmbiguousIfSend<A> {
fn some_item() {}
}
impl<T: ?Sized> AmbiguousIfSend<()> for T {}
impl<T: ?Sized + Send> AmbiguousIfSend<u8> for T {}
<hegel::ExplicitTestCase as AmbiguousIfSend<_>>::some_item();
}
/// Compile-time check: `ExplicitTestCase` does *not* implement `Sync`.
#[allow(dead_code)]
fn _assert_explicit_test_case_is_not_sync() {
trait AmbiguousIfSync<A> {
fn some_item() {}
}
impl<T: ?Sized> AmbiguousIfSync<()> for T {}
impl<T: ?Sized + Sync> AmbiguousIfSync<u8> for T {}
<hegel::ExplicitTestCase as AmbiguousIfSync<_>>::some_item();
}
#[hegel::test(test_cases = 20)]
fn test_spawn_thread_with_clone_does_generation(tc: TestCase) {
let tc_clone = tc.clone();
let handle = std::thread::spawn(move || {
let n: u32 = tc_clone.draw(gs::integers());
n
});
let thread_value = handle.join().expect("thread panicked");
// After the thread has joined, the main thread resumes generation.
let main_value: bool = tc.draw(gs::booleans());
let _ = (thread_value, main_value);
}
#[hegel::test(test_cases = 20)]
fn test_main_then_thread_then_main(tc: TestCase) {
let _a: u32 = tc.draw(gs::integers());
let tc_clone = tc.clone();
let handle = std::thread::spawn(move || {
let b: String = tc_clone.draw(gs::text());
let c: bool = tc_clone.draw(gs::booleans());
(b, c)
});
let _ = handle.join().expect("thread panicked");
let _d: u32 = tc.draw(gs::integers());
}
#[hegel::test(test_cases = 10)]
fn test_sequential_threads_each_do_one_draw(tc: TestCase) {
// Spawn threads one at a time, fully joining each before the next.
// This is deterministic: no concurrent generation ever happens.
for _ in 0..3 {
let tc_clone = tc.clone();
let handle = std::thread::spawn(move || {
let v: u32 = tc_clone.draw(gs::integers());
v
});
let _ = handle.join().expect("thread panicked");
}
let _: bool = tc.draw(gs::booleans());
}
#[hegel::test(test_cases = 5)]
fn test_nested_generators_work_across_thread_boundary(tc: TestCase) {
// Composed generators (map, flat_map, vecs) must still work when
// drawn from another thread.
let tc_clone = tc.clone();
let captured: Arc<Mutex<Option<Vec<i32>>>> = Arc::new(Mutex::new(None));
let captured_clone = Arc::clone(&captured);
let handle = std::thread::spawn(move || {
let xs: Vec<i32> = tc_clone.draw(gs::vecs(gs::integers::<i32>()).max_size(5));
*captured_clone.lock().unwrap() = Some(xs);
});
handle.join().expect("thread panicked");
let xs = captured
.lock()
.unwrap()
.take()
.expect("thread produced no value");
assert!(xs.len() <= 5);
}
/// Composite generator that draws, spawns a worker thread that also draws
/// and returns a value, joins it, then draws again. Under the previous
/// coarse-grained reentrant lock this pattern deadlocked: `tc.draw(composite)`
/// held the lock on the main thread while the worker tried to acquire the
/// same (reentrant) lock from a different thread. Fine-grained locking fixes
/// this.
#[hegel::composite]
fn thread_mid_generator(tc: TestCase) -> (i32, i64, bool) {
let a: i32 = tc.draw(gs::integers::<i32>());
let tc_worker = tc.clone();
let handle = std::thread::spawn(move || tc_worker.draw(gs::integers::<i64>()));
let b: i64 = handle.join().expect("thread panicked");
let c: bool = tc.draw(gs::booleans());
(a, b, c)
}
#[hegel::test(test_cases = 10)]
fn test_thread_inside_composite_does_not_deadlock(tc: TestCase) {
let (_a, _b, _c) = tc.draw(thread_mid_generator());
}
mod threading {
//! Omitted: test_threadlocal_setattr_and_getattr, test_nonexistent_getattr_raises,
//! test_nonexistent_setattr_raises, test_raises_if_not_passed_callable — these test
//! hypothesis.utils.threading.ThreadLocal, a Python-specific utility class wrapping
//! Python's threading.local() with dunder attribute access; no Rust counterpart.
//!
//! Omitted: TestNoDifferingExecutorsHealthCheck — relies on pytest parametrize class
//! instantiation behavior; Python/pytest-specific infrastructure.
use hegel::generators::{self as gs};
use hegel::{Hegel, Settings};
use std::sync::{Arc, Barrier};
use std::thread;
#[test]
fn test_run_given_concurrently() {
let n_threads = 2;
let barrier = Arc::new(Barrier::new(n_threads));
let handles: Vec<_> = (0..n_threads)
.map(|_| {
let barrier = Arc::clone(&barrier);
thread::spawn(move || {
Hegel::new(move |tc| {
let _n: i64 = tc.draw(gs::integers());
barrier.wait();
})
.settings(Settings::new().database(None))
.run();
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
}
}