1use crate::runner::{self, RunConfig, Suite, TestNode};
4use std::cell::RefCell;
5
6thread_local! {
11 static BUILDER: RefCell<Option<SuiteBuilder>> = const { RefCell::new(None) };
12}
13
14pub(crate) struct SuiteBuilder {
15 stack: Vec<GroupFrame>,
16}
17
18struct GroupFrame {
19 name: String,
20 focused: bool,
21 pending: bool,
22 labels: Vec<String>,
23 before_each: Vec<Box<dyn Fn()>>,
24 after_each: Vec<Box<dyn Fn()>>,
25 before_all: Vec<Box<dyn Fn()>>,
26 after_all: Vec<Box<dyn Fn()>>,
27 just_before_each: Vec<Box<dyn Fn()>>,
28 children: Vec<TestNode>,
29}
30
31impl GroupFrame {
32 fn root() -> Self {
33 GroupFrame {
34 name: String::new(),
35 focused: false,
36 pending: false,
37 labels: Vec::new(),
38 before_each: Vec::new(),
39 after_each: Vec::new(),
40 before_all: Vec::new(),
41 after_all: Vec::new(),
42 just_before_each: Vec::new(),
43 children: Vec::new(),
44 }
45 }
46}
47
48impl SuiteBuilder {
49 fn new() -> Self {
50 SuiteBuilder {
51 stack: vec![GroupFrame::root()],
52 }
53 }
54
55 pub(crate) fn push_group(&mut self, name: String, focused: bool, pending: bool) {
56 self.stack.push(GroupFrame {
57 name,
58 focused,
59 pending,
60 labels: Vec::new(),
61 before_each: Vec::new(),
62 after_each: Vec::new(),
63 before_all: Vec::new(),
64 after_all: Vec::new(),
65 just_before_each: Vec::new(),
66 children: Vec::new(),
67 });
68 }
69
70 pub(crate) fn pop_group(&mut self) {
71 let frame = self.stack.pop().expect("rsspec: unbalanced group push/pop");
72 let node = TestNode::Describe {
73 name: frame.name,
74 focused: frame.focused,
75 pending: frame.pending,
76 labels: frame.labels,
77 before_each: frame.before_each,
78 after_each: frame.after_each,
79 before_all: frame.before_all,
80 after_all: frame.after_all,
81 just_before_each: frame.just_before_each,
82 children: frame.children,
83 };
84 self.current_frame_mut().children.push(node);
85 }
86
87 pub(crate) fn add_node(&mut self, node: TestNode) {
88 self.current_frame_mut().children.push(node);
89 }
90
91 fn add_before_each(&mut self, hook: Box<dyn Fn()>) {
92 self.current_frame_mut().before_each.push(hook);
93 }
94
95 fn add_after_each(&mut self, hook: Box<dyn Fn()>) {
96 self.current_frame_mut().after_each.push(hook);
97 }
98
99 fn add_before_all(&mut self, hook: Box<dyn Fn()>) {
100 self.current_frame_mut().before_all.push(hook);
101 }
102
103 fn add_after_all(&mut self, hook: Box<dyn Fn()>) {
104 self.current_frame_mut().after_all.push(hook);
105 }
106
107 fn add_just_before_each(&mut self, hook: Box<dyn Fn()>) {
108 self.current_frame_mut().just_before_each.push(hook);
109 }
110
111 fn add_labels(&mut self, labels: Vec<String>) {
112 self.current_frame_mut().labels.extend(labels);
113 }
114
115 fn current_frame_mut(&mut self) -> &mut GroupFrame {
116 self.stack.last_mut().expect("rsspec: empty builder stack")
117 }
118
119 fn into_nodes(mut self) -> Vec<TestNode> {
120 assert_eq!(
121 self.stack.len(),
122 1,
123 "rsspec: unbalanced group push/pop at finalization"
124 );
125 self.stack.pop().unwrap().children
126 }
127}
128
129pub(crate) fn with_builder<R>(f: impl FnOnce(&mut SuiteBuilder) -> R) -> R {
131 BUILDER.with(|cell| {
132 let mut opt = cell.borrow_mut();
133 let builder = opt
134 .as_mut()
135 .expect("rsspec: Context used outside of rsspec::run()");
136 f(builder)
137 })
138}
139
140#[derive(Copy, Clone)]
158pub struct Context;
159
160impl Context {
161 pub fn describe(&self, name: &str, body: impl FnOnce(Context)) {
165 self.describe_impl(name, false, false, body);
166 }
167
168 pub fn fdescribe(&self, name: &str, body: impl FnOnce(Context)) {
171 self.describe_impl(name, true, false, body);
172 }
173
174 pub fn xdescribe(&self, name: &str, body: impl FnOnce(Context)) {
177 self.describe_impl(name, false, true, body);
178 }
179
180 pub fn context(&self, name: &str, body: impl FnOnce(Context)) {
182 self.describe(name, body);
183 }
184
185 pub fn fcontext(&self, name: &str, body: impl FnOnce(Context)) {
187 self.fdescribe(name, body);
188 }
189
190 pub fn xcontext(&self, name: &str, body: impl FnOnce(Context)) {
192 self.xdescribe(name, body);
193 }
194
195 pub fn when(&self, name: &str, body: impl FnOnce(Context)) {
197 self.describe(name, body);
198 }
199
200 pub fn fwhen(&self, name: &str, body: impl FnOnce(Context)) {
202 self.fdescribe(name, body);
203 }
204
205 pub fn xwhen(&self, name: &str, body: impl FnOnce(Context)) {
207 self.xdescribe(name, body);
208 }
209
210 fn describe_impl(&self, name: &str, focused: bool, pending: bool, body: impl FnOnce(Context)) {
211 with_builder(|b| b.push_group(name.to_string(), focused, pending));
212 body(Context);
213 with_builder(|b| b.pop_group());
214 }
215
216 pub fn it(&self, name: &str, body: impl Fn() + 'static) -> ItBuilder {
231 ItBuilder::new(name.to_string(), body, false, false)
232 }
233
234 pub fn fit(&self, name: &str, body: impl Fn() + 'static) -> ItBuilder {
236 ItBuilder::new(name.to_string(), body, true, false)
237 }
238
239 pub fn xit(&self, name: &str, body: impl Fn() + 'static) -> ItBuilder {
241 ItBuilder::new(name.to_string(), body, false, true)
242 }
243
244 pub fn specify(&self, name: &str, body: impl Fn() + 'static) -> ItBuilder {
246 self.it(name, body)
247 }
248
249 pub fn fspecify(&self, name: &str, body: impl Fn() + 'static) -> ItBuilder {
251 self.fit(name, body)
252 }
253
254 pub fn xspecify(&self, name: &str, body: impl Fn() + 'static) -> ItBuilder {
256 self.xit(name, body)
257 }
258
259 pub fn before_each(&self, hook: impl Fn() + 'static) {
264 with_builder(|b| b.add_before_each(Box::new(hook)));
265 }
266
267 pub fn after_each(&self, hook: impl Fn() + 'static) {
270 with_builder(|b| b.add_after_each(Box::new(hook)));
271 }
272
273 pub fn before_all(&self, hook: impl Fn() + 'static) {
276 with_builder(|b| b.add_before_all(Box::new(hook)));
277 }
278
279 pub fn after_all(&self, hook: impl Fn() + 'static) {
282 with_builder(|b| b.add_after_all(Box::new(hook)));
283 }
284
285 pub fn just_before_each(&self, hook: impl Fn() + 'static) {
288 with_builder(|b| b.add_just_before_each(Box::new(hook)));
289 }
290
291 pub fn labels(&self, labels: &[&str]) {
305 let labels: Vec<String> = labels.iter().map(|s| s.to_string()).collect();
306 with_builder(|b| b.add_labels(labels));
307 }
308
309 pub fn describe_table(&self, name: &str) -> crate::table::TableBuilder {
324 crate::table::TableBuilder::new(name.to_string())
325 }
326
327 pub fn ordered(&self, name: &str, body: impl FnOnce(&mut crate::ordered::OrderedContext)) {
342 let mut oct = crate::ordered::OrderedContext::new(name.to_string(), false);
343 body(&mut oct);
344 with_builder(|b| b.add_node(oct.into_node()));
345 }
346
347 pub fn ordered_continue_on_failure(
349 &self,
350 name: &str,
351 body: impl FnOnce(&mut crate::ordered::OrderedContext),
352 ) {
353 let mut oct = crate::ordered::OrderedContext::new(name.to_string(), true);
354 body(&mut oct);
355 with_builder(|b| b.add_node(oct.into_node()));
356 }
357}
358
359#[cfg(feature = "tokio")]
364impl Context {
365 pub fn async_it<F, Fut>(&self, name: &str, body: F) -> ItBuilder
378 where
379 F: Fn() -> Fut + 'static,
380 Fut: std::future::Future<Output = ()> + 'static,
381 {
382 self.it(name, crate::async_test(body))
383 }
384
385 pub fn async_fit<F, Fut>(&self, name: &str, body: F) -> ItBuilder
387 where
388 F: Fn() -> Fut + 'static,
389 Fut: std::future::Future<Output = ()> + 'static,
390 {
391 self.fit(name, crate::async_test(body))
392 }
393
394 pub fn async_xit<F, Fut>(&self, name: &str, body: F) -> ItBuilder
396 where
397 F: Fn() -> Fut + 'static,
398 Fut: std::future::Future<Output = ()> + 'static,
399 {
400 self.xit(name, crate::async_test(body))
401 }
402
403 pub fn async_specify<F, Fut>(&self, name: &str, body: F) -> ItBuilder
405 where
406 F: Fn() -> Fut + 'static,
407 Fut: std::future::Future<Output = ()> + 'static,
408 {
409 self.async_it(name, body)
410 }
411
412 pub fn async_fspecify<F, Fut>(&self, name: &str, body: F) -> ItBuilder
414 where
415 F: Fn() -> Fut + 'static,
416 Fut: std::future::Future<Output = ()> + 'static,
417 {
418 self.async_fit(name, body)
419 }
420
421 pub fn async_xspecify<F, Fut>(&self, name: &str, body: F) -> ItBuilder
423 where
424 F: Fn() -> Fut + 'static,
425 Fut: std::future::Future<Output = ()> + 'static,
426 {
427 self.async_xit(name, body)
428 }
429
430 pub fn async_before_each<F, Fut>(&self, hook: F)
435 where
436 F: Fn() -> Fut + 'static,
437 Fut: std::future::Future<Output = ()> + 'static,
438 {
439 self.before_each(crate::async_test(hook));
440 }
441
442 pub fn async_after_each<F, Fut>(&self, hook: F)
445 where
446 F: Fn() -> Fut + 'static,
447 Fut: std::future::Future<Output = ()> + 'static,
448 {
449 self.after_each(crate::async_test(hook));
450 }
451
452 pub fn async_before_all<F, Fut>(&self, hook: F)
455 where
456 F: Fn() -> Fut + 'static,
457 Fut: std::future::Future<Output = ()> + 'static,
458 {
459 self.before_all(crate::async_test(hook));
460 }
461
462 pub fn async_after_all<F, Fut>(&self, hook: F)
465 where
466 F: Fn() -> Fut + 'static,
467 Fut: std::future::Future<Output = ()> + 'static,
468 {
469 self.after_all(crate::async_test(hook));
470 }
471
472 pub fn async_just_before_each<F, Fut>(&self, hook: F)
475 where
476 F: Fn() -> Fut + 'static,
477 Fut: std::future::Future<Output = ()> + 'static,
478 {
479 self.just_before_each(crate::async_test(hook));
480 }
481}
482
483pub struct ItBuilder {
503 name: String,
504 body: Option<Box<dyn Fn()>>,
505 focused: bool,
506 pending: bool,
507 labels: Vec<String>,
508 retries: Option<u32>,
509 timeout_ms: Option<u64>,
510 must_pass_repeatedly: Option<u32>,
511}
512
513impl ItBuilder {
514 fn new(name: String, body: impl Fn() + 'static, focused: bool, pending: bool) -> Self {
515 ItBuilder {
516 name,
517 body: Some(Box::new(body)),
518 focused,
519 pending,
520 labels: Vec::new(),
521 retries: None,
522 timeout_ms: None,
523 must_pass_repeatedly: None,
524 }
525 }
526
527 pub fn labels(mut self, labels: &[&str]) -> Self {
530 self.labels.extend(labels.iter().map(|s| s.to_string()));
531 self
532 }
533
534 pub fn retries(mut self, n: u32) -> Self {
536 self.retries = Some(n);
537 self
538 }
539
540 pub fn timeout(mut self, ms: u64) -> Self {
546 self.timeout_ms = Some(ms);
547 self
548 }
549
550 pub fn must_pass_repeatedly(mut self, n: u32) -> Self {
552 self.must_pass_repeatedly = Some(n);
553 self
554 }
555}
556
557impl Drop for ItBuilder {
558 fn drop(&mut self) {
559 if std::thread::panicking() {
562 return;
563 }
564 let Some(body) = self.body.take() else {
565 return;
566 };
567 let node = TestNode::It {
568 name: std::mem::take(&mut self.name),
569 focused: self.focused,
570 pending: self.pending,
571 labels: std::mem::take(&mut self.labels),
572 retries: self.retries,
573 timeout_ms: self.timeout_ms,
574 must_pass_repeatedly: self.must_pass_repeatedly,
575 test_fn: body,
576 };
577 with_builder(|b| b.add_node(node));
578 }
579}
580
581fn build_tree(body: impl FnOnce(Context)) -> Vec<TestNode> {
587 BUILDER.with(|cell| {
588 *cell.borrow_mut() = Some(SuiteBuilder::new());
589 });
590
591 body(Context);
592
593 BUILDER.with(|cell| {
594 cell.borrow_mut()
595 .take()
596 .expect("rsspec: builder missing after run")
597 .into_nodes()
598 })
599}
600
601pub fn run(body: impl FnOnce(Context)) {
620 let nodes = build_tree(body);
621
622 let args: Vec<String> = std::env::args().collect();
624 let inside_harness = runner::detect_libtest_args(&args[1..]).is_some();
625
626 let config = if inside_harness {
627 RunConfig {
628 filter: None,
629 list: false,
630 include_ignored: false,
631 }
632 } else {
633 RunConfig::from_args()
634 };
635
636 let suite = Suite::new("", nodes);
637 let result = runner::run_suites(&[suite], &config);
638
639 if result.failed > 0 {
640 if inside_harness {
641 let details = result
643 .failures
644 .iter()
645 .enumerate()
646 .map(|(i, f)| format!(" {}. {}", i + 1, f))
647 .collect::<Vec<_>>()
648 .join("\n");
649 panic!(
650 "rsspec: {} test(s) failed\n{}",
651 result.failed, details
652 );
653 } else {
654 std::process::exit(1);
655 }
656 }
657}
658
659pub fn run_inline(body: impl FnOnce(Context)) {
678 let nodes = build_tree(body);
679 let config = RunConfig {
680 filter: None,
681 list: false,
682 include_ignored: false,
683 };
684 let suite = Suite::new("", nodes);
685 let result = runner::run_suites(&[suite], &config);
686
687 if result.failed > 0 {
688 let details = result
689 .failures
690 .iter()
691 .enumerate()
692 .map(|(i, f)| format!(" {}. {}", i + 1, f))
693 .collect::<Vec<_>>()
694 .join("\n");
695 panic!(
696 "rsspec: {} test(s) failed\n{}",
697 result.failed, details
698 );
699 }
700}