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