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(&section).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}