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}