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}