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