tanu_core/runner.rs
1//! # Test Runner Module
2//!
3//! The core test execution engine for tanu. This module provides the `Runner` struct
4//! that orchestrates test discovery, execution, filtering, reporting, and event publishing.
5//! It supports concurrent test execution with retry capabilities and comprehensive
6//! event-driven reporting.
7//!
8//! ## Key Components
9//!
10//! - **`Runner`**: Main test execution engine
11//! - **Event System**: Real-time test execution events via channels
12//! - **Filtering**: Project, module, and test name filtering
13//! - **Reporting**: Pluggable reporter system for test output
14//! - **Retry Logic**: Configurable retry with exponential backoff
15//!
16//! ## Basic Usage
17//!
18//! ```rust,ignore
19//! use tanu_core::Runner;
20//!
21//! let mut runner = Runner::new();
22//! runner.add_test("my_test", "my_module", test_factory);
23//! runner.run(&[], &[], &[]).await?;
24//! ```
25use backon::Retryable;
26use eyre::WrapErr;
27use futures::{stream::FuturesUnordered, FutureExt, StreamExt};
28use once_cell::sync::Lazy;
29use std::{
30 collections::HashMap,
31 ops::Deref,
32 pin::Pin,
33 sync::{
34 atomic::{AtomicUsize, Ordering},
35 Arc, Mutex,
36 },
37 time::Duration,
38};
39use tokio::sync::{broadcast, Semaphore};
40use tracing::*;
41
42use crate::{
43 config::{self, get_tanu_config, ProjectConfig},
44 http,
45 reporter::Reporter,
46 Config, ModuleName, ProjectName,
47};
48
49tokio::task_local! {
50 pub(crate) static TEST_INFO: TestInfo;
51}
52
53pub(crate) fn get_test_info() -> TestInfo {
54 TEST_INFO.with(|info| info.clone())
55}
56
57// NOTE: Keep the runner receiver alive here so that sender never fails to send.
58#[allow(clippy::type_complexity)]
59pub(crate) static CHANNEL: Lazy<
60 Mutex<Option<(broadcast::Sender<Event>, broadcast::Receiver<Event>)>>,
61> = Lazy::new(|| Mutex::new(Some(broadcast::channel(1000))));
62
63/// Publishes an event to the runner's event channel.
64///
65/// This function is used throughout the test execution pipeline to broadcast
66/// real-time events including test starts, check results, HTTP logs, retries,
67/// and test completions. All events are timestamped and include test context.
68///
69/// # Examples
70///
71/// ```rust,ignore
72/// use tanu_core::runner::{publish, EventBody, Check};
73///
74/// // Publish a successful check
75/// let check = Check::success("response.status() == 200");
76/// publish(EventBody::Check(Box::new(check)))?;
77///
78/// // Publish test start
79/// publish(EventBody::Start)?;
80/// ```
81///
82/// # Errors
83///
84/// Returns an error if:
85/// - The channel lock cannot be acquired
86/// - The channel has been closed
87/// - The send operation fails
88pub fn publish(e: impl Into<Event>) -> eyre::Result<()> {
89 let Ok(guard) = CHANNEL.lock() else {
90 eyre::bail!("failed to acquire runner channel lock");
91 };
92 let Some((tx, _)) = guard.deref() else {
93 eyre::bail!("runner channel has been already closed");
94 };
95
96 tx.send(e.into())
97 .wrap_err("failed to publish message to the runner channel")?;
98
99 Ok(())
100}
101
102/// Subscribe to the channel to see the real-time test execution events.
103pub fn subscribe() -> eyre::Result<broadcast::Receiver<Event>> {
104 let Ok(guard) = CHANNEL.lock() else {
105 eyre::bail!("failed to acquire runner channel lock");
106 };
107 let Some((tx, _)) = guard.deref() else {
108 eyre::bail!("runner channel has been already closed");
109 };
110
111 Ok(tx.subscribe())
112}
113
114/// Test execution errors.
115///
116/// Represents the different ways a test can fail during execution.
117/// These errors are captured and reported by the runner system.
118#[derive(Debug, Clone, thiserror::Error)]
119pub enum Error {
120 #[error("panic: {0}")]
121 Panicked(String),
122 #[error("error: {0}")]
123 ErrorReturned(String),
124}
125
126/// Represents the result of a check/assertion within a test.
127///
128/// Checks are created by assertion macros (`check!`, `check_eq!`, etc.) and
129/// track both the success/failure status and the original expression that
130/// was evaluated. This information is used for detailed test reporting.
131///
132/// # Examples
133///
134/// ```rust,ignore
135/// use tanu_core::runner::Check;
136///
137/// // Create a successful check
138/// let check = Check::success("response.status() == 200");
139/// assert!(check.result);
140///
141/// // Create a failed check
142/// let check = Check::error("user_count != 0");
143/// assert!(!check.result);
144/// ```
145#[derive(Debug, Clone)]
146pub struct Check {
147 pub result: bool,
148 pub expr: String,
149}
150
151impl Check {
152 pub fn success(expr: impl Into<String>) -> Check {
153 Check {
154 result: true,
155 expr: expr.into(),
156 }
157 }
158
159 pub fn error(expr: impl Into<String>) -> Check {
160 Check {
161 result: false,
162 expr: expr.into(),
163 }
164 }
165}
166
167/// A test execution event with full context.
168///
169/// Events are published throughout test execution and include the project,
170/// module, and test name for complete traceability. The event body contains
171/// the specific event data (start, check, HTTP, retry, or end).
172///
173/// # Event Flow
174///
175/// 1. `Start` - Test begins execution
176/// 2. `Check` - Assertion results (can be multiple per test)
177/// 3. `Http` - HTTP request/response logs (can be multiple per test)
178/// 4. `Retry` - Test retry attempts (if configured)
179/// 5. `End` - Test completion with final result
180#[derive(Debug, Clone)]
181pub struct Event {
182 pub project: ProjectName,
183 pub module: ModuleName,
184 pub test: ModuleName,
185 pub body: EventBody,
186}
187
188/// The specific event data published during test execution.
189///
190/// Each event type carries different information:
191/// - `Start`: Signals test execution beginning
192/// - `Check`: Contains assertion results with expression details
193/// - `Http`: HTTP request/response logs for debugging
194/// - `Retry`: Indicates a test retry attempt
195/// - `End`: Final test result with timing and outcome
196#[derive(Debug, Clone)]
197pub enum EventBody {
198 Start,
199 Check(Box<Check>),
200 Http(Box<http::Log>),
201 Retry,
202 End(Test),
203}
204
205impl From<EventBody> for Event {
206 fn from(body: EventBody) -> Self {
207 let project = crate::config::get_config();
208 let test_info = crate::runner::get_test_info();
209 Event {
210 project: project.name,
211 module: test_info.module,
212 test: test_info.name,
213 body,
214 }
215 }
216}
217
218/// Final test execution result.
219///
220/// Contains the complete outcome of a test execution including metadata,
221/// execution time, and the final result (success or specific error type).
222/// This is published in the `End` event when a test completes.
223#[derive(Debug, Clone)]
224pub struct Test {
225 pub info: TestInfo,
226 pub request_time: Duration,
227 pub result: Result<(), Error>,
228}
229
230/// Test metadata and identification.
231///
232/// Contains the module and test name for a test case. This information
233/// is used for test filtering, reporting, and event context throughout
234/// the test execution pipeline.
235#[derive(Debug, Clone, Default)]
236pub struct TestInfo {
237 pub module: String,
238 pub name: String,
239}
240
241impl TestInfo {
242 /// Full test name including module
243 pub fn full_name(&self) -> String {
244 format!("{}::{}", self.module, self.name)
245 }
246
247 /// Unique test name including project and module names
248 pub fn unique_name(&self, project: &str) -> String {
249 format!("{project}::{}::{}", self.module, self.name)
250 }
251}
252
253type TestCaseFactory = Arc<
254 dyn Fn() -> Pin<Box<dyn futures::Future<Output = eyre::Result<()>> + Send + 'static>>
255 + Sync
256 + Send
257 + 'static,
258>;
259
260/// Configuration options for test runner behavior.
261///
262/// Controls various aspects of test execution including logging,
263/// concurrency, and channel management. These options can be set
264/// via the builder pattern on the `Runner`.
265///
266/// # Examples
267///
268/// ```rust,ignore
269/// use tanu_core::Runner;
270///
271/// let mut runner = Runner::new();
272/// runner.capture_http(); // Enable HTTP logging
273/// runner.set_concurrency(4); // Limit to 4 concurrent tests
274/// ```
275#[derive(Debug, Clone, Default)]
276pub struct Options {
277 pub debug: bool,
278 pub capture_http: bool,
279 pub capture_rust: bool,
280 pub terminate_channel: bool,
281 pub concurrency: Option<usize>,
282}
283
284/// Trait for filtering test cases during execution.
285///
286/// Filters allow selective test execution based on project configuration
287/// and test metadata. Multiple filters can be applied simultaneously,
288/// and a test must pass all filters to be executed.
289///
290/// # Examples
291///
292/// ```rust,ignore
293/// use tanu_core::runner::{Filter, TestInfo, ProjectConfig};
294///
295/// struct CustomFilter;
296///
297/// impl Filter for CustomFilter {
298/// fn filter(&self, project: &ProjectConfig, info: &TestInfo) -> bool {
299/// // Only run tests with "integration" in the name
300/// info.name.contains("integration")
301/// }
302/// }
303/// ```
304pub trait Filter {
305 fn filter(&self, project: &ProjectConfig, info: &TestInfo) -> bool;
306}
307
308/// Filters tests to only run from specified projects.
309///
310/// When project names are provided, only tests from those projects
311/// will be executed. If the list is empty, all projects are included.
312///
313/// # Examples
314///
315/// ```rust,ignore
316/// use tanu_core::runner::ProjectFilter;
317///
318/// let filter = ProjectFilter { project_names: &["staging".to_string()] };
319/// // Only tests from "staging" project will run
320/// ```
321pub struct ProjectFilter<'a> {
322 project_names: &'a [String],
323}
324
325impl Filter for ProjectFilter<'_> {
326 fn filter(&self, project: &ProjectConfig, _info: &TestInfo) -> bool {
327 if self.project_names.is_empty() {
328 return true;
329 }
330
331 self.project_names
332 .iter()
333 .any(|project_name| &project.name == project_name)
334 }
335}
336
337/// Filters tests to only run from specified modules.
338///
339/// When module names are provided, only tests from those modules
340/// will be executed. If the list is empty, all modules are included.
341/// Module names correspond to Rust module paths.
342///
343/// # Examples
344///
345/// ```rust,ignore
346/// use tanu_core::runner::ModuleFilter;
347///
348/// let filter = ModuleFilter { module_names: &["api".to_string(), "auth".to_string()] };
349/// // Only tests from "api" and "auth" modules will run
350/// ```
351pub struct ModuleFilter<'a> {
352 module_names: &'a [String],
353}
354
355impl Filter for ModuleFilter<'_> {
356 fn filter(&self, _project: &ProjectConfig, info: &TestInfo) -> bool {
357 if self.module_names.is_empty() {
358 return true;
359 }
360
361 self.module_names
362 .iter()
363 .any(|module_name| &info.module == module_name)
364 }
365}
366
367/// Filters tests to only run specific named tests.
368///
369/// When test names are provided, only those exact tests will be executed.
370/// Test names should include the module (e.g., "api::health_check").
371/// If the list is empty, all tests are included.
372///
373/// # Examples
374///
375/// ```rust,ignore
376/// use tanu_core::runner::TestNameFilter;
377///
378/// let filter = TestNameFilter {
379/// test_names: &["api::health_check".to_string(), "auth::login".to_string()]
380/// };
381/// // Only the specified tests will run
382/// ```
383pub struct TestNameFilter<'a> {
384 test_names: &'a [String],
385}
386
387impl Filter for TestNameFilter<'_> {
388 fn filter(&self, _project: &ProjectConfig, info: &TestInfo) -> bool {
389 if self.test_names.is_empty() {
390 return true;
391 }
392
393 self.test_names
394 .iter()
395 .any(|test_name| &info.full_name() == test_name)
396 }
397}
398
399/// Filters out tests that are configured to be ignored.
400///
401/// This filter reads the `test_ignore` configuration from each project
402/// and excludes those tests from execution. Tests are matched by their
403/// full name (module::test_name).
404///
405/// # Configuration
406///
407/// In `tanu.toml`:
408/// ```toml
409/// [[projects]]
410/// name = "staging"
411/// test_ignore = ["flaky_test", "slow_integration_test"]
412/// ```
413///
414/// # Examples
415///
416/// ```rust,ignore
417/// use tanu_core::runner::TestIgnoreFilter;
418///
419/// let filter = TestIgnoreFilter::default();
420/// // Tests listed in test_ignore config will be skipped
421/// ```
422pub struct TestIgnoreFilter {
423 test_ignores: HashMap<String, Vec<String>>,
424}
425
426impl Default for TestIgnoreFilter {
427 fn default() -> TestIgnoreFilter {
428 TestIgnoreFilter {
429 test_ignores: get_tanu_config()
430 .projects
431 .iter()
432 .map(|proj| (proj.name.clone(), proj.test_ignore.clone()))
433 .collect(),
434 }
435 }
436}
437
438impl Filter for TestIgnoreFilter {
439 fn filter(&self, project: &ProjectConfig, info: &TestInfo) -> bool {
440 let Some(test_ignore) = self.test_ignores.get(&project.name) else {
441 return true;
442 };
443
444 test_ignore
445 .iter()
446 .all(|test_name| &info.full_name() != test_name)
447 }
448}
449
450/// The main test execution engine for tanu.
451///
452/// `Runner` is responsible for orchestrating the entire test execution pipeline:
453/// test discovery, filtering, concurrent execution, retry handling, event publishing,
454/// and result reporting. It supports multiple projects, configurable concurrency,
455/// and pluggable reporters.
456///
457/// # Features
458///
459/// - **Concurrent Execution**: Tests run in parallel with configurable limits
460/// - **Retry Logic**: Automatic retry with exponential backoff for flaky tests
461/// - **Event System**: Real-time event publishing for UI integration
462/// - **Filtering**: Filter tests by project, module, or test name
463/// - **Reporting**: Support for multiple output formats via reporters
464/// - **HTTP Logging**: Capture and log all HTTP requests/responses
465///
466/// # Examples
467///
468/// ```rust,ignore
469/// use tanu_core::{Runner, reporter::TableReporter};
470///
471/// let mut runner = Runner::new();
472/// runner.capture_http();
473/// runner.set_concurrency(8);
474/// runner.add_reporter(TableReporter::new());
475///
476/// // Add tests (typically done by procedural macros)
477/// runner.add_test("health_check", "api", test_factory);
478///
479/// // Run all tests
480/// runner.run(&[], &[], &[]).await?;
481/// ```
482///
483/// # Architecture
484///
485/// Tests are executed in separate tokio tasks with:
486/// - Project-scoped configuration
487/// - Test-scoped context for event publishing
488/// - Semaphore-based concurrency control
489/// - Panic recovery and error handling
490/// - Automatic retry with configurable backoff
491#[derive(Default)]
492pub struct Runner {
493 cfg: Config,
494 options: Options,
495 test_cases: Vec<(TestInfo, TestCaseFactory)>,
496 reporters: Vec<Box<dyn Reporter + Send>>,
497}
498
499impl Runner {
500 /// Creates a new runner with the global tanu configuration.
501 ///
502 /// This loads the configuration from `tanu.toml` and sets up
503 /// default options. Use `with_config()` for custom configuration.
504 ///
505 /// # Examples
506 ///
507 /// ```rust,ignore
508 /// use tanu_core::Runner;
509 ///
510 /// let runner = Runner::new();
511 /// ```
512 pub fn new() -> Runner {
513 Runner::with_config(get_tanu_config().clone())
514 }
515
516 /// Creates a new runner with the specified configuration.
517 ///
518 /// This allows for custom configuration beyond what's in `tanu.toml`,
519 /// useful for testing or programmatic setup.
520 ///
521 /// # Examples
522 ///
523 /// ```rust,ignore
524 /// use tanu_core::{Runner, Config};
525 ///
526 /// let config = Config::default();
527 /// let runner = Runner::with_config(config);
528 /// ```
529 pub fn with_config(cfg: Config) -> Runner {
530 Runner {
531 cfg,
532 options: Options::default(),
533 test_cases: Vec::new(),
534 reporters: Vec::new(),
535 }
536 }
537
538 /// Enables HTTP request/response logging.
539 ///
540 /// When enabled, all HTTP requests made via tanu's HTTP client
541 /// will be logged and included in test reports. This is useful
542 /// for debugging API tests and understanding request/response flow.
543 ///
544 /// # Examples
545 ///
546 /// ```rust,ignore
547 /// let mut runner = Runner::new();
548 /// runner.capture_http();
549 /// ```
550 pub fn capture_http(&mut self) {
551 self.options.capture_http = true;
552 }
553
554 /// Enables Rust logging output during test execution.
555 ///
556 /// This initializes the tracing subscriber to capture debug, info,
557 /// warn, and error logs from tests and the framework itself.
558 /// Useful for debugging test execution issues.
559 ///
560 /// # Examples
561 ///
562 /// ```rust,ignore
563 /// let mut runner = Runner::new();
564 /// runner.capture_rust();
565 /// ```
566 pub fn capture_rust(&mut self) {
567 self.options.capture_rust = true;
568 }
569
570 /// Configures the runner to close the event channel after test execution.
571 ///
572 /// By default, the event channel remains open for continued monitoring.
573 /// This option closes the channel when all tests complete, signaling
574 /// that no more events will be published.
575 ///
576 /// # Examples
577 ///
578 /// ```rust,ignore
579 /// let mut runner = Runner::new();
580 /// runner.terminate_channel();
581 /// ```
582 pub fn terminate_channel(&mut self) {
583 self.options.terminate_channel = true;
584 }
585
586 /// Adds a reporter for test output formatting.
587 ///
588 /// Reporters receive test events and format them for different output
589 /// destinations (console, files, etc.). Multiple reporters can be added
590 /// to generate multiple output formats simultaneously.
591 ///
592 /// # Examples
593 ///
594 /// ```rust,ignore
595 /// use tanu_core::{Runner, reporter::TableReporter};
596 ///
597 /// let mut runner = Runner::new();
598 /// runner.add_reporter(TableReporter::new());
599 /// ```
600 pub fn add_reporter(&mut self, reporter: impl Reporter + 'static + Send) {
601 self.reporters.push(Box::new(reporter));
602 }
603
604 /// Adds a boxed reporter for test output formatting.
605 ///
606 /// Similar to `add_reporter()` but accepts an already-boxed reporter.
607 /// Useful when working with dynamic reporter selection.
608 ///
609 /// # Examples
610 ///
611 /// ```rust,ignore
612 /// use tanu_core::{Runner, reporter::ListReporter};
613 ///
614 /// let mut runner = Runner::new();
615 /// let reporter: Box<dyn Reporter + Send> = Box::new(ListReporter::new());
616 /// runner.add_boxed_reporter(reporter);
617 /// ```
618 pub fn add_boxed_reporter(&mut self, reporter: Box<dyn Reporter + 'static + Send>) {
619 self.reporters.push(reporter);
620 }
621
622 /// Add a test case to the runner.
623 pub fn add_test(&mut self, name: &str, module: &str, factory: TestCaseFactory) {
624 self.test_cases.push((
625 TestInfo {
626 name: name.into(),
627 module: module.into(),
628 },
629 factory,
630 ));
631 }
632
633 /// Sets the maximum number of tests to run concurrently.
634 ///
635 /// By default, tests run with unlimited concurrency. This setting
636 /// allows you to limit concurrent execution to reduce resource usage
637 /// or avoid overwhelming external services.
638 ///
639 /// # Examples
640 ///
641 /// ```rust,ignore
642 /// let mut runner = Runner::new();
643 /// runner.set_concurrency(4); // Max 4 tests at once
644 /// ```
645 pub fn set_concurrency(&mut self, concurrency: usize) {
646 self.options.concurrency = Some(concurrency);
647 }
648
649 /// Executes all registered tests with optional filtering.
650 ///
651 /// Runs tests concurrently according to the configured options and filters.
652 /// Tests can be filtered by project name, module name, or specific test names.
653 /// Empty filter arrays mean "include all".
654 ///
655 /// # Parameters
656 ///
657 /// - `project_names`: Only run tests from these projects (empty = all projects)
658 /// - `module_names`: Only run tests from these modules (empty = all modules)
659 /// - `test_names`: Only run these specific tests (empty = all tests)
660 ///
661 /// # Examples
662 ///
663 /// ```rust,ignore
664 /// let mut runner = Runner::new();
665 ///
666 /// // Run all tests
667 /// runner.run(&[], &[], &[]).await?;
668 ///
669 /// // Run only "staging" project tests
670 /// runner.run(&["staging".to_string()], &[], &[]).await?;
671 ///
672 /// // Run specific test
673 /// runner.run(&[], &[], &["api::health_check".to_string()]).await?;
674 /// ```
675 ///
676 /// # Errors
677 ///
678 /// Returns an error if:
679 /// - Any test fails (unless configured to continue on failure)
680 /// - A test panics and cannot be recovered
681 /// - Reporter setup or execution fails
682 /// - Event channel operations fail
683 #[allow(clippy::too_many_lines)]
684 pub async fn run(
685 &mut self,
686 project_names: &[String],
687 module_names: &[String],
688 test_names: &[String],
689 ) -> eyre::Result<()> {
690 if self.options.capture_rust {
691 tracing_subscriber::fmt::init();
692 }
693
694 let mut reporters = std::mem::take(&mut self.reporters);
695
696 let project_filter = ProjectFilter { project_names };
697 let module_filter = ModuleFilter { module_names };
698 let test_name_filter = TestNameFilter { test_names };
699 let test_ignore_filter = TestIgnoreFilter::default();
700
701 let start = std::time::Instant::now();
702 let handles: FuturesUnordered<_> = {
703 // Create a semaphore to limit concurrency if specified
704 let semaphore = Arc::new(tokio::sync::Semaphore::new(
705 self.options.concurrency.unwrap_or(Semaphore::MAX_PERMITS),
706 ));
707
708 self.test_cases
709 .iter()
710 .flat_map(|(info, factory)| {
711 let projects = self.cfg.projects.clone();
712 let projects = if projects.is_empty() {
713 vec![ProjectConfig {
714 name: "default".into(),
715 ..Default::default()
716 }]
717 } else {
718 projects
719 };
720 projects
721 .into_iter()
722 .map(move |project| (project.clone(), info.clone(), factory.clone()))
723 })
724 .filter(move |(project, info, _)| test_name_filter.filter(project, info))
725 .filter(move |(project, info, _)| module_filter.filter(project, info))
726 .filter(move |(project, info, _)| project_filter.filter(project, info))
727 .filter(move |(project, info, _)| test_ignore_filter.filter(project, info))
728 .map(|(project, info, factory)| {
729 let semaphore = semaphore.clone();
730 tokio::spawn(async move {
731 let _permit = semaphore.acquire().await.unwrap();
732 config::PROJECT
733 .scope(project.clone(), async {
734 TEST_INFO
735 .scope(info.clone(), async {
736 let test_name = info.name.clone();
737 publish(EventBody::Start)?;
738
739 let retry_count =
740 AtomicUsize::new(project.retry.count.unwrap_or(0));
741 let f = || async {
742 let res = factory().await;
743
744 if res.is_err()
745 && retry_count.load(Ordering::SeqCst) > 0
746 {
747 publish(EventBody::Retry)?;
748 retry_count.fetch_sub(1, Ordering::SeqCst);
749 };
750 res
751 };
752 let started = std::time::Instant::now();
753 let fut = f.retry(project.retry.backoff());
754 let fut = std::panic::AssertUnwindSafe(fut).catch_unwind();
755 let res = fut.await;
756 let request_time = started.elapsed();
757
758 let result = match res {
759 Ok(Ok(_)) => {
760 debug!("{test_name} ok");
761 Ok(())
762 }
763 Ok(Err(e)) => {
764 debug!("{test_name} failed: {e:#}");
765 Err(Error::ErrorReturned(format!("{e:?}")))
766 }
767 Err(e) => {
768 let panic_message = if let Some(panic_message) =
769 e.downcast_ref::<&str>()
770 {
771 format!(
772 "{test_name} failed with message: {panic_message}"
773 )
774 } else if let Some(panic_message) =
775 e.downcast_ref::<String>()
776 {
777 format!(
778 "{test_name} failed with message: {panic_message}"
779 )
780 } else {
781 format!(
782 "{test_name} failed with unknown message"
783 )
784 };
785 let e = eyre::eyre!(panic_message);
786 Err(Error::Panicked(format!("{e:?}")))
787 }
788 };
789
790 let is_err = result.is_err();
791 publish(EventBody::End(Test {
792 info,
793 request_time,
794 result,
795 }))?;
796
797 eyre::ensure!(!is_err);
798 eyre::Ok(())
799 })
800 .await
801 })
802 .await
803 })
804 })
805 .collect()
806 };
807 debug!(
808 "created handles for {} test cases; took {}s",
809 handles.len(),
810 start.elapsed().as_secs_f32()
811 );
812
813 let reporters =
814 futures::future::join_all(reporters.iter_mut().map(|reporter| reporter.run().boxed()));
815
816 let mut has_any_error = false;
817 let options = self.options.clone();
818 let runner = async move {
819 let results = handles.collect::<Vec<_>>().await;
820 if results.is_empty() {
821 console::Term::stdout().write_line("no test cases found")?;
822 }
823 for result in results {
824 match result {
825 Ok(res) => {
826 if let Err(e) = res {
827 debug!("test case failed: {e:#}");
828 has_any_error = true;
829 }
830 }
831 Err(e) => {
832 if e.is_panic() {
833 // Resume the panic on the main task
834 error!("{e}");
835 has_any_error = true;
836 }
837 }
838 }
839 }
840 debug!("all test finished. sending stop signal to the background tasks.");
841
842 if options.terminate_channel {
843 let Ok(mut guard) = CHANNEL.lock() else {
844 eyre::bail!("failed to acquire runner channel lock");
845 };
846 guard.take(); // closing the runner channel.
847 }
848
849 if has_any_error {
850 eyre::bail!("one or more tests failed");
851 }
852
853 eyre::Ok(())
854 };
855
856 let (handles, reporters) = tokio::join!(runner, reporters);
857 for reporter in reporters {
858 if let Err(e) = reporter {
859 error!("reporter failed: {e:#}");
860 }
861 }
862
863 debug!("runner stopped");
864
865 handles
866 }
867
868 /// Returns a list of all registered test metadata.
869 ///
870 /// This provides access to test information without executing the tests.
871 /// Useful for building test UIs, generating reports, or implementing
872 /// custom filtering logic.
873 ///
874 /// # Examples
875 ///
876 /// ```rust,ignore
877 /// let runner = Runner::new();
878 /// let tests = runner.list();
879 ///
880 /// for test in tests {
881 /// println!("Test: {}", test.full_name());
882 /// }
883 /// ```
884 pub fn list(&self) -> Vec<&TestInfo> {
885 self.test_cases
886 .iter()
887 .map(|(meta, _test)| meta)
888 .collect::<Vec<_>>()
889 }
890}
891
892#[cfg(test)]
893mod test {
894 use super::*;
895 use crate::config::RetryConfig;
896
897 fn create_config() -> Config {
898 Config {
899 projects: vec![ProjectConfig {
900 name: "default".into(),
901 ..Default::default()
902 }],
903 ..Default::default()
904 }
905 }
906
907 fn create_config_with_retry() -> Config {
908 Config {
909 projects: vec![ProjectConfig {
910 name: "default".into(),
911 retry: RetryConfig {
912 count: Some(1),
913 ..Default::default()
914 },
915 ..Default::default()
916 }],
917 ..Default::default()
918 }
919 }
920
921 #[tokio::test]
922 async fn runner_fail_because_no_retry_configured() -> eyre::Result<()> {
923 let mut server = mockito::Server::new_async().await;
924 let m1 = server
925 .mock("GET", "/")
926 .with_status(500)
927 .expect(1)
928 .create_async()
929 .await;
930 let m2 = server
931 .mock("GET", "/")
932 .with_status(200)
933 .expect(0)
934 .create_async()
935 .await;
936
937 let factory: TestCaseFactory = Arc::new(move || {
938 let url = server.url();
939 Box::pin(async move {
940 let res = reqwest::get(url).await?;
941 if res.status().is_success() {
942 Ok(())
943 } else {
944 eyre::bail!("request failed")
945 }
946 })
947 });
948
949 let _runner_rx = subscribe()?;
950 let mut runner = Runner::with_config(create_config());
951 runner.add_test("retry_test", "module", factory);
952
953 let result = runner.run(&[], &[], &[]).await;
954 m1.assert_async().await;
955 m2.assert_async().await;
956
957 assert!(result.is_err());
958 Ok(())
959 }
960
961 #[tokio::test]
962 async fn runner_retry_successful_after_failure() -> eyre::Result<()> {
963 let mut server = mockito::Server::new_async().await;
964 let m1 = server
965 .mock("GET", "/")
966 .with_status(500)
967 .expect(1)
968 .create_async()
969 .await;
970 let m2 = server
971 .mock("GET", "/")
972 .with_status(200)
973 .expect(1)
974 .create_async()
975 .await;
976
977 let factory: TestCaseFactory = Arc::new(move || {
978 let url = server.url();
979 Box::pin(async move {
980 let res = reqwest::get(url).await?;
981 if res.status().is_success() {
982 Ok(())
983 } else {
984 eyre::bail!("request failed")
985 }
986 })
987 });
988
989 let _runner_rx = subscribe()?;
990 let mut runner = Runner::with_config(create_config_with_retry());
991 runner.add_test("retry_test", "module", factory);
992
993 let result = runner.run(&[], &[], &[]).await;
994 m1.assert_async().await;
995 m2.assert_async().await;
996
997 assert!(result.is_ok());
998
999 Ok(())
1000 }
1001}