section_testing/lib.rs
1//! This is a small library that enables section-style testing in Rust.
2//! Section-style testing makes writing many similar test cases easy, natural,
3//! and concise.
4//!
5//! Each top-level section test is run repeatedly, once for every unique
6//! section inside the test. This is more expressive and natural than fixtures
7//! because it lets you use local variables from parent scopes inside a section
8//! and because you can nest sections to an arbitrary depth.
9//!
10//! Here's an example:
11//!
12//! ```rust,ignore
13//! #[macro_use]
14//! extern crate section_testing;
15//!
16//! enable_sections! {
17//! #[test]
18//! fn example_test() {
19//! let mut v: Vec<i32> = vec![];
20//!
21//! fn check_123(v: &mut Vec<i32>) {
22//! assert_eq!(*v, vec![1, 2, 3]);
23//!
24//! if section!("reverse") {
25//! v.reverse();
26//! assert_eq!(*v, vec![3, 2, 1]);
27//! }
28//!
29//! if section!("pop+remove+insert+push") {
30//! let three = v.pop().unwrap();
31//! let one = v.remove(0);
32//! v.insert(0, three);
33//! v.push(one);
34//! assert_eq!(*v, vec![3, 2, 1]);
35//! }
36//! }
37//!
38//! if section!("push") {
39//! v.push(1);
40//! v.push(2);
41//! v.push(3);
42//! check_123(&mut v);
43//! }
44//!
45//! if section!("insert") {
46//! v.insert(0, 3);
47//! v.insert(0, 1);
48//! v.insert(1, 2);
49//! check_123(&mut v);
50//! }
51//! }
52//! }
53//! ```
54//!
55//! The `enable_sections!` macro modifies the test functions inside of it so
56//! that they run repeatedly until all sections have been visited. The
57//! `section!` macro returns a `bool` for whether or not that section should be
58//! run this iteration. This example test will check the following combinations:
59//!
60//! ```text
61//! push
62//! push, reverse
63//! push, pop+remove+insert+push
64//! insert
65//! insert, reverse
66//! insert, pop+remove+insert+push
67//! ```
68//!
69//! When a test fails, the enclosing sections will be printed to stderr. Here's
70//! what happens if we comment out `v.push(one);` in the example above:
71//!
72//! ```text
73//! running 1 test
74//! thread 'example_test' panicked at 'assertion failed: `(left == right)`
75//! left: `[3, 2]`,
76//! right: `[3, 2, 1]`', src/main.rs:30:9
77//! note: Run with `RUST_BACKTRACE=1` for a backtrace.
78//! ---- the failure was inside these sections ----
79//! 0) "push" at src/main.rs:34
80//! 1) "pop+remove+insert+push" at src/main.rs:25
81//! test example_test ... FAILED
82//! ```
83//!
84//! Note that like all tests in Rust, a section-style test will stop on the
85//! first failure. This means you will only be able to see the first combination
86//! that failed instead of being able to see all failed combinations. The above
87//! example would have also failed for the combination `insert,
88//! pop+remove+insert+push` if the other combination hadn't failed first. This
89//! is because Rust's built-in test runner has no API for adding new tests at
90//! runtime.
91
92use std::mem::swap;
93use std::fmt::Write;
94use std::cell::RefCell;
95use std::collections::{HashMap, VecDeque};
96
97thread_local! {
98 static CURRENT_RUNNER: RefCell<Runner> = RefCell::new(Runner::new());
99}
100
101#[derive(PartialEq, Eq, Hash, Clone, Copy)]
102struct Section {
103 name: &'static str,
104 file: &'static str,
105 line: u32,
106}
107
108#[derive(Clone, Copy)]
109struct Entry {
110 should_enter: bool,
111 index: usize,
112}
113
114struct Runner {
115 is_running: bool,
116 queue: VecDeque<HashMap<Section, Entry>>,
117 current: HashMap<Section, Entry>,
118 new: Vec<Section>,
119}
120
121impl Runner {
122 fn new() -> Runner {
123 Runner {
124 is_running: false,
125 queue: vec![HashMap::new()].into(),
126 current: HashMap::new(),
127 new: vec![],
128 }
129 }
130}
131
132pub struct DropHandler {
133 pub is_top_level: bool,
134 pub was_success: bool,
135}
136
137impl Drop for DropHandler {
138 fn drop(&mut self) {
139 if !self.is_top_level {
140 return;
141 }
142
143 CURRENT_RUNNER.with(|r| {
144 r.borrow_mut().is_running = false;
145
146 // Did the test complete successfully?
147 if self.was_success {
148 let mut r = r.borrow_mut();
149 let mut new = vec![];
150 swap(&mut r.new, &mut new);
151
152 // If so, add newly-discovered sections to the queue
153 for section in &new {
154 let mut path = r.current.clone();
155 let count = r.current.values().filter(|x| x.should_enter).count();
156 for s in &new {
157 path.insert(*s, Entry {
158 should_enter: s == section,
159 index: count,
160 });
161 }
162 r.queue.push_back(path);
163 }
164 }
165
166 // Is the test in the middle of unwinding due to a panic?
167 else {
168 let mut current: Vec<_> = r.borrow().current.iter()
169 .map(|(k, v)| (*k, *v))
170 .filter(|(_, v)| v.should_enter)
171 .collect();
172 current.sort_unstable_by(|a, b| a.1.index.cmp(&b.1.index));
173
174 // Write out the failure as a single buffer to avoid it interleaving with other output
175 if !current.is_empty() {
176 let mut buffer = "---- the failure was inside these sections ----\n".to_owned();
177 for (i, (section, _)) in current.iter().enumerate() {
178 writeln!(&mut buffer, "{: >3}) {:?} at {}:{}",
179 i, section.name, section.file, section.line).unwrap();
180 }
181 eprint!("{}", buffer);
182 }
183 }
184 });
185 }
186}
187
188pub fn enable_sections_start() -> bool {
189 CURRENT_RUNNER.with(|r| {
190 if r.borrow().is_running {
191 false
192 } else {
193 r.replace(Runner::new());
194 true
195 }
196 })
197}
198
199pub fn enable_sections_step() -> bool {
200 CURRENT_RUNNER.with(|r| {
201 let mut r = r.borrow_mut();
202 if let Some(current) = r.queue.pop_front() {
203 r.current = current;
204 r.new.clear();
205 r.is_running = true;
206 true
207 } else {
208 false
209 }
210 })
211}
212
213pub fn enter_section(name: &'static str, file: &'static str, line: u32) -> bool {
214 CURRENT_RUNNER.with(|r| {
215 let section = Section {name, file, line};
216 let should_enter = r.borrow().current.get(§ion).map(|x| x.should_enter);
217 should_enter.unwrap_or_else(|| {
218 r.borrow_mut().new.push(section);
219 false
220 })
221 })
222}
223
224pub fn is_running() -> bool {
225 CURRENT_RUNNER.with(|r| r.borrow().is_running)
226}
227
228#[macro_export]
229macro_rules! enable_sections {
230 (
231 $(
232 $(#[$($attrs:tt)*])*
233 fn $name:ident() {
234 $($arg:tt)*
235 }
236 )*
237 ) => {
238 $(
239 $(#[$($attrs)*])*
240 fn $name() {
241 let is_top_level = $crate::enable_sections_start();
242 loop {
243 // Stop this run when the queue is empty
244 if is_top_level && !$crate::enable_sections_step() {
245 break;
246 }
247
248 // Run the function body
249 let mut scope = $crate::DropHandler {is_top_level, was_success: false};
250 $($arg)*
251 scope.was_success = true;
252
253 // Only run the function body once if we're not top-level
254 if !is_top_level {
255 break;
256 }
257 }
258 }
259 )*
260 }
261}
262
263#[macro_export]
264macro_rules! section {
265 ($name:expr) => {{
266 assert!($crate::is_running(), "{}", "\"section!(...)\" must be called from inside \"enable_sections! { ... }\"");
267 $crate::enter_section($name, file!(), line!())
268 }}
269}