Skip to main content

test_r_core/
internal.rs

1use crate::args::{Arguments, TimeThreshold};
2use crate::bench::Bencher;
3use crate::stats::Summary;
4use std::any::{Any, TypeId};
5use std::backtrace::Backtrace;
6use std::cmp::{max, Ordering};
7use std::fmt::{Debug, Display, Formatter};
8use std::future::Future;
9use std::hash::Hash;
10use std::pin::Pin;
11use std::process::ExitCode;
12use std::sync::{Arc, Mutex};
13use std::time::{Duration, SystemTime};
14
15#[derive(Clone)]
16#[allow(clippy::type_complexity)]
17pub enum TestFunction {
18    Sync(
19        Arc<
20            dyn Fn(Arc<dyn DependencyView + Send + Sync>) -> Box<dyn TestReturnValue>
21                + Send
22                + Sync
23                + 'static,
24        >,
25    ),
26    SyncBench(
27        Arc<dyn Fn(&mut Bencher, Arc<dyn DependencyView + Send + Sync>) + Send + Sync + 'static>,
28    ),
29    #[cfg(feature = "tokio")]
30    Async(
31        Arc<
32            dyn (Fn(
33                    Arc<dyn DependencyView + Send + Sync>,
34                ) -> Pin<Box<dyn Future<Output = Box<dyn TestReturnValue>>>>)
35                + Send
36                + Sync
37                + 'static,
38        >,
39    ),
40    #[cfg(feature = "tokio")]
41    AsyncBench(
42        Arc<
43            dyn for<'a> Fn(
44                    &'a mut crate::bench::AsyncBencher,
45                    Arc<dyn DependencyView + Send + Sync>,
46                ) -> Pin<Box<dyn Future<Output = ()> + 'a>>
47                + Send
48                + Sync
49                + 'static,
50        >,
51    ),
52}
53
54impl TestFunction {
55    #[cfg(not(feature = "tokio"))]
56    pub fn is_bench(&self) -> bool {
57        matches!(self, TestFunction::SyncBench(_))
58    }
59
60    #[cfg(feature = "tokio")]
61    pub fn is_bench(&self) -> bool {
62        matches!(
63            self,
64            TestFunction::SyncBench(_) | TestFunction::AsyncBench(_)
65        )
66    }
67}
68
69pub trait TestReturnValue {
70    fn into_result(self: Box<Self>) -> Result<(), FailureCause>;
71}
72
73impl TestReturnValue for () {
74    fn into_result(self: Box<Self>) -> Result<(), FailureCause> {
75        Ok(())
76    }
77}
78
79impl<T, E: Display + Debug + Send + Sync + 'static> TestReturnValue for Result<T, E> {
80    fn into_result(self: Box<Self>) -> Result<(), FailureCause> {
81        match *self {
82            Ok(_) => Ok(()),
83            Err(e) => Err(FailureCause::from_error(e)),
84        }
85    }
86}
87
88#[derive(Clone)]
89pub enum FailureCause {
90    /// Test returned Err(e) where E: Display + Debug — stores both representations
91    /// and the original error value for later downcasting
92    ReturnedError {
93        display: String,
94        debug: String,
95        prefer_debug: bool,
96        error: Arc<dyn Any + Send + Sync>,
97    },
98    /// Test returned Err(String) — stored as raw string without formatting
99    ReturnedMessage(String),
100    /// Test panicked
101    Panic(PanicCause),
102    /// Framework error (join failure, timeout, IPC deserialization, etc.)
103    HarnessError(String),
104}
105
106#[derive(Debug, Clone)]
107pub struct PanicCause {
108    pub message: Option<String>,
109    pub location: Option<PanicLocation>,
110    pub backtrace: Option<Arc<Backtrace>>,
111}
112
113#[derive(Debug, Clone)]
114pub struct PanicLocation {
115    pub file: String,
116    pub line: u32,
117    pub column: u32,
118}
119
120impl std::fmt::Debug for FailureCause {
121    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
122        match self {
123            FailureCause::ReturnedError { display, .. } => {
124                f.debug_tuple("ReturnedError").field(display).finish()
125            }
126            FailureCause::ReturnedMessage(s) => f.debug_tuple("ReturnedMessage").field(s).finish(),
127            FailureCause::Panic(p) => f.debug_tuple("Panic").field(p).finish(),
128            FailureCause::HarnessError(s) => f.debug_tuple("HarnessError").field(s).finish(),
129        }
130    }
131}
132
133impl FailureCause {
134    pub fn from_error<E: Display + Debug + Send + Sync + 'static>(e: E) -> Self {
135        if TypeId::of::<E>() == TypeId::of::<String>() {
136            let any: Box<dyn Any + Send + Sync> = Box::new(e);
137            return FailureCause::ReturnedMessage(*any.downcast::<String>().unwrap());
138        }
139
140        let mut _prefer_debug = false;
141        #[cfg(feature = "anyhow")]
142        {
143            _prefer_debug = TypeId::of::<E>() == TypeId::of::<anyhow::Error>();
144        }
145
146        FailureCause::ReturnedError {
147            display: format!("{e:#}"),
148            debug: format!("{e:?}"),
149            prefer_debug: _prefer_debug,
150            error: Arc::new(e),
151        }
152    }
153
154    pub fn render(&self) -> String {
155        match self {
156            FailureCause::ReturnedError {
157                display,
158                debug,
159                prefer_debug,
160                ..
161            } => {
162                if *prefer_debug {
163                    debug.clone()
164                } else {
165                    display.clone()
166                }
167            }
168            FailureCause::ReturnedMessage(s) => s.clone(),
169            FailureCause::Panic(p) => p.render(),
170            FailureCause::HarnessError(s) => s.clone(),
171        }
172    }
173
174    /// Get the message string for ShouldPanic matching (without backtrace)
175    pub fn panic_message(&self) -> Option<&str> {
176        match self {
177            FailureCause::Panic(p) => p.message.as_deref(),
178            _ => None,
179        }
180    }
181}
182
183impl PanicCause {
184    pub fn render(&self) -> String {
185        let mut out = self.message.clone().unwrap_or_default();
186        if let Some(loc) = &self.location {
187            out.push_str(&format!("\n  at {}:{}:{}", loc.file, loc.line, loc.column));
188        }
189        if let Some(bt) = &self.backtrace {
190            let bt_str = format!("{bt}");
191            if !bt_str.is_empty() && bt_str != "disabled backtrace" {
192                out.push_str(&format!("\n\nStack backtrace:\n{bt}"));
193            }
194        }
195        out
196    }
197}
198
199#[derive(Debug, Clone, PartialEq, Eq)]
200pub enum ShouldPanic {
201    No,
202    Yes,
203    WithMessage(String),
204}
205
206#[derive(Debug, Clone, PartialEq, Eq)]
207pub enum TestType {
208    UnitTest,
209    IntegrationTest,
210}
211
212impl TestType {
213    pub fn from_path(path: &str) -> Self {
214        if path.contains("/src/") {
215            TestType::UnitTest
216        } else {
217            TestType::IntegrationTest
218        }
219    }
220}
221
222#[derive(Debug, Clone, PartialEq, Eq)]
223pub enum FlakinessControl {
224    None,
225    ProveNonFlaky(usize),
226    RetryKnownFlaky(usize),
227}
228
229#[derive(Debug, Clone, PartialEq, Eq)]
230pub enum DetachedPanicPolicy {
231    FailTest,
232    Ignore,
233}
234
235#[derive(Debug, Clone, PartialEq, Eq)]
236pub enum CaptureControl {
237    Default,
238    AlwaysCapture,
239    NeverCapture,
240}
241
242impl CaptureControl {
243    pub fn requires_capturing(&self, default: bool) -> bool {
244        match self {
245            CaptureControl::Default => default,
246            CaptureControl::AlwaysCapture => true,
247            CaptureControl::NeverCapture => false,
248        }
249    }
250}
251
252#[derive(Debug, Clone, PartialEq, Eq)]
253pub enum ReportTimeControl {
254    Default,
255    Enabled,
256    Disabled,
257}
258
259#[derive(Clone)]
260pub struct TestProperties {
261    pub should_panic: ShouldPanic,
262    pub test_type: TestType,
263    pub timeout: Option<Duration>,
264    pub flakiness_control: FlakinessControl,
265    pub capture_control: CaptureControl,
266    pub report_time_control: ReportTimeControl,
267    pub ensure_time_control: ReportTimeControl,
268    pub tags: Vec<String>,
269    pub is_ignored: bool,
270    pub detached_panic_policy: DetachedPanicPolicy,
271}
272
273impl TestProperties {
274    pub fn unit_test() -> Self {
275        TestProperties {
276            test_type: TestType::UnitTest,
277            ..Default::default()
278        }
279    }
280
281    pub fn integration_test() -> Self {
282        TestProperties {
283            test_type: TestType::IntegrationTest,
284            ..Default::default()
285        }
286    }
287}
288
289impl Default for TestProperties {
290    fn default() -> Self {
291        Self {
292            should_panic: ShouldPanic::No,
293            test_type: TestType::UnitTest,
294            timeout: None,
295            flakiness_control: FlakinessControl::None,
296            capture_control: CaptureControl::Default,
297            report_time_control: ReportTimeControl::Default,
298            ensure_time_control: ReportTimeControl::Default,
299            tags: Vec::new(),
300            is_ignored: false,
301            detached_panic_policy: DetachedPanicPolicy::FailTest,
302        }
303    }
304}
305
306#[derive(Clone)]
307pub struct RegisteredTest {
308    pub name: String,
309    pub crate_name: String,
310    pub module_path: String,
311    pub run: TestFunction,
312    pub props: TestProperties,
313    pub dependencies: Option<Vec<String>>,
314}
315
316impl RegisteredTest {
317    pub fn filterable_name(&self) -> String {
318        if !self.module_path.is_empty() {
319            format!("{}::{}", self.module_path, self.name)
320        } else {
321            self.name.clone()
322        }
323    }
324
325    pub fn fully_qualified_name(&self) -> String {
326        [&self.crate_name, &self.module_path, &self.name]
327            .into_iter()
328            .filter(|s| !s.is_empty())
329            .cloned()
330            .collect::<Vec<String>>()
331            .join("::")
332    }
333
334    pub fn crate_and_module(&self) -> String {
335        [&self.crate_name, &self.module_path]
336            .into_iter()
337            .filter(|s| !s.is_empty())
338            .cloned()
339            .collect::<Vec<String>>()
340            .join("::")
341    }
342}
343
344impl Debug for RegisteredTest {
345    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
346        f.debug_struct("RegisteredTest")
347            .field("name", &self.name)
348            .field("crate_name", &self.crate_name)
349            .field("module_path", &self.module_path)
350            .finish()
351    }
352}
353
354pub static REGISTERED_TESTS: Mutex<Vec<RegisteredTest>> = Mutex::new(Vec::new());
355
356#[derive(Clone)]
357#[allow(clippy::type_complexity)]
358pub enum DependencyConstructor {
359    Sync(
360        Arc<
361            dyn (Fn(Arc<dyn DependencyView + Send + Sync>) -> Arc<dyn Any + Send + Sync + 'static>)
362                + Send
363                + Sync
364                + 'static,
365        >,
366    ),
367    Async(
368        Arc<
369            dyn (Fn(
370                    Arc<dyn DependencyView + Send + Sync>,
371                ) -> Pin<Box<dyn Future<Output = Arc<dyn Any + Send + Sync>>>>)
372                + Send
373                + Sync
374                + 'static,
375        >,
376    ),
377}
378
379#[derive(Clone)]
380pub struct RegisteredDependency {
381    pub name: String, // TODO: Should we use TypeId here?
382    pub crate_name: String,
383    pub module_path: String,
384    pub constructor: DependencyConstructor,
385    pub dependencies: Vec<String>,
386}
387
388impl Debug for RegisteredDependency {
389    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
390        f.debug_struct("RegisteredDependency")
391            .field("name", &self.name)
392            .field("crate_name", &self.crate_name)
393            .field("module_path", &self.module_path)
394            .finish()
395    }
396}
397
398impl PartialEq for RegisteredDependency {
399    fn eq(&self, other: &Self) -> bool {
400        self.name == other.name
401    }
402}
403
404impl Eq for RegisteredDependency {}
405
406impl Hash for RegisteredDependency {
407    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
408        self.name.hash(state);
409    }
410}
411
412impl RegisteredDependency {
413    pub fn crate_and_module(&self) -> String {
414        [&self.crate_name, &self.module_path]
415            .into_iter()
416            .filter(|s| !s.is_empty())
417            .cloned()
418            .collect::<Vec<String>>()
419            .join("::")
420    }
421}
422
423pub static REGISTERED_DEPENDENCY_CONSTRUCTORS: Mutex<Vec<RegisteredDependency>> =
424    Mutex::new(Vec::new());
425
426#[derive(Debug, Clone)]
427pub enum RegisteredTestSuiteProperty {
428    Sequential {
429        name: String,
430        crate_name: String,
431        module_path: String,
432    },
433    Tag {
434        name: String,
435        crate_name: String,
436        module_path: String,
437        tag: String,
438    },
439    Timeout {
440        name: String,
441        crate_name: String,
442        module_path: String,
443        timeout: Duration,
444    },
445}
446
447impl RegisteredTestSuiteProperty {
448    pub fn crate_name(&self) -> &String {
449        match self {
450            RegisteredTestSuiteProperty::Sequential { crate_name, .. } => crate_name,
451            RegisteredTestSuiteProperty::Tag { crate_name, .. } => crate_name,
452            RegisteredTestSuiteProperty::Timeout { crate_name, .. } => crate_name,
453        }
454    }
455
456    pub fn module_path(&self) -> &String {
457        match self {
458            RegisteredTestSuiteProperty::Sequential { module_path, .. } => module_path,
459            RegisteredTestSuiteProperty::Tag { module_path, .. } => module_path,
460            RegisteredTestSuiteProperty::Timeout { module_path, .. } => module_path,
461        }
462    }
463
464    pub fn name(&self) -> &String {
465        match self {
466            RegisteredTestSuiteProperty::Sequential { name, .. } => name,
467            RegisteredTestSuiteProperty::Tag { name, .. } => name,
468            RegisteredTestSuiteProperty::Timeout { name, .. } => name,
469        }
470    }
471
472    pub fn crate_and_module(&self) -> String {
473        [self.crate_name(), self.module_path(), self.name()]
474            .into_iter()
475            .filter(|s| !s.is_empty())
476            .cloned()
477            .collect::<Vec<String>>()
478            .join("::")
479    }
480}
481
482pub static REGISTERED_TESTSUITE_PROPS: Mutex<Vec<RegisteredTestSuiteProperty>> =
483    Mutex::new(Vec::new());
484
485#[derive(Clone)]
486#[allow(clippy::type_complexity)]
487pub enum TestGeneratorFunction {
488    Sync(Arc<dyn Fn() -> Vec<GeneratedTest> + Send + Sync + 'static>),
489    Async(
490        Arc<
491            dyn (Fn() -> Pin<Box<dyn Future<Output = Vec<GeneratedTest>> + Send>>)
492                + Send
493                + Sync
494                + 'static,
495        >,
496    ),
497}
498
499pub struct DynamicTestRegistration {
500    tests: Vec<GeneratedTest>,
501}
502
503impl Default for DynamicTestRegistration {
504    fn default() -> Self {
505        Self::new()
506    }
507}
508
509impl DynamicTestRegistration {
510    pub fn new() -> Self {
511        Self { tests: Vec::new() }
512    }
513
514    pub fn to_vec(self) -> Vec<GeneratedTest> {
515        self.tests
516    }
517
518    pub fn add_sync_test<R: TestReturnValue + 'static>(
519        &mut self,
520        name: impl AsRef<str>,
521        props: TestProperties,
522        dependencies: Option<Vec<String>>,
523        run: impl Fn(Arc<dyn DependencyView + Send + Sync>) -> R + Send + Sync + Clone + 'static,
524    ) {
525        self.tests.push(GeneratedTest {
526            name: name.as_ref().to_string(),
527            run: TestFunction::Sync(Arc::new(move |deps| {
528                Box::new(run(deps)) as Box<dyn TestReturnValue>
529            })),
530            props,
531            dependencies,
532        });
533    }
534
535    #[cfg(feature = "tokio")]
536    pub fn add_async_test<R: TestReturnValue + 'static>(
537        &mut self,
538        name: impl AsRef<str>,
539        props: TestProperties,
540        dependencies: Option<Vec<String>>,
541        run: impl (Fn(Arc<dyn DependencyView + Send + Sync>) -> Pin<Box<dyn Future<Output = R> + Send>>)
542            + Send
543            + Sync
544            + Clone
545            + 'static,
546    ) {
547        self.tests.push(GeneratedTest {
548            name: name.as_ref().to_string(),
549            run: TestFunction::Async(Arc::new(move |deps| {
550                let run = run.clone();
551                Box::pin(async move {
552                    let r = run(deps).await;
553                    Box::new(r) as Box<dyn TestReturnValue>
554                })
555            })),
556            props,
557            dependencies,
558        });
559    }
560}
561
562#[derive(Clone)]
563pub struct GeneratedTest {
564    pub name: String,
565    pub run: TestFunction,
566    pub props: TestProperties,
567    pub dependencies: Option<Vec<String>>,
568}
569
570#[derive(Clone)]
571pub struct RegisteredTestGenerator {
572    pub name: String,
573    pub crate_name: String,
574    pub module_path: String,
575    pub run: TestGeneratorFunction,
576    pub is_ignored: bool,
577}
578
579impl RegisteredTestGenerator {
580    pub fn crate_and_module(&self) -> String {
581        [&self.crate_name, &self.module_path]
582            .into_iter()
583            .filter(|s| !s.is_empty())
584            .cloned()
585            .collect::<Vec<String>>()
586            .join("::")
587    }
588}
589
590pub static REGISTERED_TEST_GENERATORS: Mutex<Vec<RegisteredTestGenerator>> = Mutex::new(Vec::new());
591
592pub(crate) fn filter_test(test: &RegisteredTest, filter: &str, exact: bool) -> bool {
593    if let Some(tag_list) = filter.strip_prefix(":tag:") {
594        if tag_list.is_empty() {
595            // Filtering for tags with NO TAGS
596            test.props.tags.is_empty()
597        } else {
598            let or_tags = tag_list.split('|').collect::<Vec<&str>>();
599            let mut result = false;
600            for or_tag in or_tags {
601                let and_tags = or_tag.split('&').collect::<Vec<&str>>();
602                let mut and_result = true;
603                for and_tag in and_tags {
604                    if !test.props.tags.contains(&and_tag.to_string()) {
605                        and_result = false;
606                        break;
607                    }
608                }
609                if and_result {
610                    result = true;
611                    break;
612                }
613            }
614            result
615        }
616    } else if exact {
617        test.filterable_name() == filter
618    } else {
619        test.filterable_name().contains(filter)
620    }
621}
622
623pub(crate) fn apply_suite_tags(
624    tests: &[RegisteredTest],
625    props: &[RegisteredTestSuiteProperty],
626) -> Vec<RegisteredTest> {
627    let tag_props = props
628        .iter()
629        .filter_map(|prop| match prop {
630            RegisteredTestSuiteProperty::Tag { tag, .. } => {
631                let prefix = prop.crate_and_module();
632                Some((prefix, tag.clone()))
633            }
634            _ => None,
635        })
636        .collect::<Vec<_>>();
637
638    let mut result = Vec::new();
639    for test in tests {
640        let mut test = test.clone();
641        for (prefix, tag) in &tag_props {
642            if &test.crate_and_module() == prefix {
643                test.props.tags.push(tag.clone());
644            }
645        }
646        result.push(test);
647    }
648    result
649}
650
651pub(crate) fn apply_suite_timeouts(
652    tests: &[RegisteredTest],
653    props: &[RegisteredTestSuiteProperty],
654) -> Vec<RegisteredTest> {
655    let timeout_props = props
656        .iter()
657        .filter_map(|prop| match prop {
658            RegisteredTestSuiteProperty::Timeout { timeout, .. } => {
659                let prefix = prop.crate_and_module();
660                Some((prefix, *timeout))
661            }
662            _ => None,
663        })
664        .collect::<Vec<_>>();
665
666    let mut result = Vec::new();
667    for test in tests {
668        let mut test = test.clone();
669        for (prefix, timeout) in &timeout_props {
670            if &test.crate_and_module() == prefix && test.props.timeout.is_none() {
671                test.props.timeout = Some(*timeout);
672            }
673        }
674        result.push(test);
675    }
676    result
677}
678
679pub(crate) fn filter_registered_tests(
680    args: &Arguments,
681    registered_tests: &[RegisteredTest],
682) -> Vec<RegisteredTest> {
683    registered_tests
684        .iter()
685        .filter(|registered_test| {
686            !args
687                .skip
688                .iter()
689                .any(|skip| filter_test(registered_test, skip, args.exact))
690        })
691        .filter(|registered_test| {
692            args.filter.is_empty()
693                || args
694                    .filter
695                    .iter()
696                    .any(|filter| filter_test(registered_test, filter, args.exact))
697        })
698        .filter(|registered_tests| {
699            (args.bench && registered_tests.run.is_bench())
700                || (args.test && !registered_tests.run.is_bench())
701                || (!args.bench && !args.test)
702        })
703        .filter(|registered_test| {
704            !args.exclude_should_panic || registered_test.props.should_panic == ShouldPanic::No
705        })
706        .cloned()
707        .collect::<Vec<_>>()
708}
709
710fn add_generated_tests(
711    target: &mut Vec<RegisteredTest>,
712    generator: &RegisteredTestGenerator,
713    generated: Vec<GeneratedTest>,
714) {
715    target.extend(generated.into_iter().map(|mut test| {
716        test.props.is_ignored |= generator.is_ignored;
717        RegisteredTest {
718            name: format!("{}::{}", generator.name, test.name),
719            crate_name: generator.crate_name.clone(),
720            module_path: generator.module_path.clone(),
721            run: test.run,
722            props: test.props,
723            dependencies: test.dependencies,
724        }
725    }));
726}
727
728#[cfg(feature = "tokio")]
729pub(crate) async fn generate_tests(generators: &[RegisteredTestGenerator]) -> Vec<RegisteredTest> {
730    let mut result = Vec::new();
731    for generator in generators {
732        match &generator.run {
733            TestGeneratorFunction::Sync(generator_fn) => {
734                let tests = generator_fn();
735                add_generated_tests(&mut result, generator, tests);
736            }
737            TestGeneratorFunction::Async(generator_fn) => {
738                let tests = generator_fn().await;
739                add_generated_tests(&mut result, generator, tests);
740            }
741        }
742    }
743    result
744}
745
746pub(crate) fn generate_tests_sync(generators: &[RegisteredTestGenerator]) -> Vec<RegisteredTest> {
747    let mut result = Vec::new();
748    for generator in generators {
749        match &generator.run {
750            TestGeneratorFunction::Sync(generator_fn) => {
751                let tests = generator_fn();
752                add_generated_tests(&mut result, generator, tests);
753            }
754            TestGeneratorFunction::Async(_) => {
755                panic!("Async test generators are not supported in sync mode")
756            }
757        }
758    }
759    result
760}
761
762pub(crate) fn get_ensure_time(args: &Arguments, test: &RegisteredTest) -> Option<TimeThreshold> {
763    let should_ensure_time = match test.props.ensure_time_control {
764        ReportTimeControl::Default => args.ensure_time,
765        ReportTimeControl::Enabled => true,
766        ReportTimeControl::Disabled => false,
767    };
768    if should_ensure_time {
769        match test.props.test_type {
770            TestType::UnitTest => Some(args.unit_test_threshold()),
771            TestType::IntegrationTest => Some(args.integration_test_threshold()),
772        }
773    } else {
774        None
775    }
776}
777
778#[derive(Clone)]
779pub enum TestResult {
780    Passed {
781        captured: Vec<CapturedOutput>,
782        exec_time: Duration,
783    },
784    Benchmarked {
785        captured: Vec<CapturedOutput>,
786        exec_time: Duration,
787        ns_iter_summ: Summary,
788        mb_s: usize,
789    },
790    Failed {
791        cause: FailureCause,
792        captured: Vec<CapturedOutput>,
793        exec_time: Duration,
794    },
795    Ignored {
796        captured: Vec<CapturedOutput>,
797    },
798}
799
800impl TestResult {
801    pub fn passed(exec_time: Duration) -> Self {
802        TestResult::Passed {
803            captured: Vec::new(),
804            exec_time,
805        }
806    }
807
808    pub fn benchmarked(exec_time: Duration, ns_iter_summ: Summary, mb_s: usize) -> Self {
809        TestResult::Benchmarked {
810            captured: Vec::new(),
811            exec_time,
812            ns_iter_summ,
813            mb_s,
814        }
815    }
816
817    pub fn failed(exec_time: Duration, cause: FailureCause) -> Self {
818        TestResult::Failed {
819            cause,
820            captured: Vec::new(),
821            exec_time,
822        }
823    }
824
825    pub fn ignored() -> Self {
826        TestResult::Ignored {
827            captured: Vec::new(),
828        }
829    }
830
831    pub(crate) fn is_passed(&self) -> bool {
832        matches!(self, TestResult::Passed { .. })
833    }
834
835    pub(crate) fn is_benchmarked(&self) -> bool {
836        matches!(self, TestResult::Benchmarked { .. })
837    }
838
839    pub(crate) fn is_failed(&self) -> bool {
840        matches!(self, TestResult::Failed { .. })
841    }
842
843    pub(crate) fn is_ignored(&self) -> bool {
844        matches!(self, TestResult::Ignored { .. })
845    }
846
847    pub(crate) fn captured_output(&self) -> &Vec<CapturedOutput> {
848        match self {
849            TestResult::Passed { captured, .. } => captured,
850            TestResult::Failed { captured, .. } => captured,
851            TestResult::Ignored { captured, .. } => captured,
852            TestResult::Benchmarked { captured, .. } => captured,
853        }
854    }
855
856    pub(crate) fn stats(&self) -> Option<&Summary> {
857        match self {
858            TestResult::Benchmarked { ns_iter_summ, .. } => Some(ns_iter_summ),
859            _ => None,
860        }
861    }
862
863    pub(crate) fn set_captured_output(&mut self, captured: Vec<CapturedOutput>) {
864        match self {
865            TestResult::Passed {
866                captured: captured_ref,
867                ..
868            } => *captured_ref = captured,
869            TestResult::Failed {
870                captured: captured_ref,
871                ..
872            } => *captured_ref = captured,
873            TestResult::Ignored {
874                captured: captured_ref,
875            } => *captured_ref = captured,
876            TestResult::Benchmarked {
877                captured: captured_ref,
878                ..
879            } => *captured_ref = captured,
880        }
881    }
882
883    pub(crate) fn from_result<A>(
884        should_panic: &ShouldPanic,
885        elapsed: Duration,
886        result: Result<Result<A, FailureCause>, Box<dyn Any + Send>>,
887    ) -> Self {
888        match result {
889            Ok(Ok(_)) => {
890                if should_panic == &ShouldPanic::No {
891                    TestResult::passed(elapsed)
892                } else {
893                    TestResult::failed(
894                        elapsed,
895                        FailureCause::HarnessError("Test did not panic as expected".to_string()),
896                    )
897                }
898            }
899            Ok(Err(cause)) => TestResult::failed(elapsed, cause),
900            Err(panic) => TestResult::from_panic(should_panic, elapsed, panic),
901        }
902    }
903
904    pub(crate) fn from_summary(
905        should_panic: &ShouldPanic,
906        elapsed: Duration,
907        result: Result<Summary, Box<dyn Any + Send>>,
908        bytes: u64,
909    ) -> Self {
910        match result {
911            Ok(summary) => {
912                let ns_iter = max(summary.median as u64, 1);
913                let mb_s = bytes * 1000 / ns_iter;
914                TestResult::benchmarked(elapsed, summary, mb_s as usize)
915            }
916            Err(panic) => Self::from_panic(should_panic, elapsed, panic),
917        }
918    }
919
920    fn from_panic(
921        should_panic: &ShouldPanic,
922        elapsed: Duration,
923        panic: Box<dyn Any + Send>,
924    ) -> Self {
925        let captured = crate::panic_hook::take_current_panic_capture();
926
927        let panic_cause = if let Some(cause) = captured {
928            cause
929        } else {
930            let message = panic
931                .downcast_ref::<String>()
932                .cloned()
933                .or(panic.downcast_ref::<&str>().map(|s| s.to_string()));
934            PanicCause {
935                message,
936                location: None,
937                backtrace: None,
938            }
939        };
940
941        match should_panic {
942            ShouldPanic::WithMessage(expected) => match &panic_cause.message {
943                Some(message) if message.contains(expected) => TestResult::passed(elapsed),
944                _ => TestResult::failed(
945                    elapsed,
946                    FailureCause::Panic(PanicCause {
947                        message: Some(format!(
948                            "Test panicked with unexpected message: {}",
949                            panic_cause.message.as_deref().unwrap_or_default()
950                        )),
951                        location: None,
952                        backtrace: None,
953                    }),
954                ),
955            },
956            ShouldPanic::Yes => TestResult::passed(elapsed),
957            ShouldPanic::No => TestResult::failed(elapsed, FailureCause::Panic(panic_cause)),
958        }
959    }
960
961    pub(crate) fn failure_message(&self) -> Option<String> {
962        self.failure_cause().map(|c| c.render())
963    }
964
965    pub fn failure_cause(&self) -> Option<&FailureCause> {
966        match self {
967            TestResult::Failed { cause, .. } => Some(cause),
968            _ => None,
969        }
970    }
971}
972
973pub struct SuiteResult {
974    pub passed: usize,
975    pub failed: usize,
976    pub ignored: usize,
977    pub measured: usize,
978    pub filtered_out: usize,
979    pub exec_time: Duration,
980}
981
982impl SuiteResult {
983    pub fn from_test_results(
984        registered_tests: &[RegisteredTest],
985        results: &[(RegisteredTest, TestResult)],
986        exec_time: Duration,
987    ) -> Self {
988        let passed = results
989            .iter()
990            .filter(|(_, result)| result.is_passed())
991            .count();
992        let measured = results
993            .iter()
994            .filter(|(_, result)| result.is_benchmarked())
995            .count();
996        let failed = results
997            .iter()
998            .filter(|(_, result)| result.is_failed())
999            .count();
1000        let ignored = results
1001            .iter()
1002            .filter(|(_, result)| result.is_ignored())
1003            .count();
1004        let filtered_out = registered_tests.len() - results.len();
1005
1006        Self {
1007            passed,
1008            failed,
1009            ignored,
1010            measured,
1011            filtered_out,
1012            exec_time,
1013        }
1014    }
1015
1016    pub fn exit_code(results: &[(RegisteredTest, TestResult)]) -> ExitCode {
1017        if results.iter().any(|(_, result)| result.is_failed()) {
1018            ExitCode::from(101)
1019        } else {
1020            ExitCode::SUCCESS
1021        }
1022    }
1023}
1024
1025pub trait DependencyView: Debug {
1026    fn get(&self, name: &str) -> Option<Arc<dyn Any + Send + Sync>>;
1027}
1028
1029impl DependencyView for Arc<dyn DependencyView + Send + Sync> {
1030    fn get(&self, name: &str) -> Option<Arc<dyn Any + Send + Sync>> {
1031        self.as_ref().get(name)
1032    }
1033}
1034
1035#[derive(Debug, Clone, Eq, PartialEq)]
1036pub enum CapturedOutput {
1037    Stdout { timestamp: SystemTime, line: String },
1038    Stderr { timestamp: SystemTime, line: String },
1039}
1040
1041impl CapturedOutput {
1042    pub fn stdout(line: String) -> Self {
1043        CapturedOutput::Stdout {
1044            timestamp: SystemTime::now(),
1045            line,
1046        }
1047    }
1048
1049    pub fn stderr(line: String) -> Self {
1050        CapturedOutput::Stderr {
1051            timestamp: SystemTime::now(),
1052            line,
1053        }
1054    }
1055
1056    pub fn timestamp(&self) -> SystemTime {
1057        match self {
1058            CapturedOutput::Stdout { timestamp, .. } => *timestamp,
1059            CapturedOutput::Stderr { timestamp, .. } => *timestamp,
1060        }
1061    }
1062
1063    pub fn line(&self) -> &str {
1064        match self {
1065            CapturedOutput::Stdout { line, .. } => line,
1066            CapturedOutput::Stderr { line, .. } => line,
1067        }
1068    }
1069}
1070
1071impl PartialOrd for CapturedOutput {
1072    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1073        Some(self.cmp(other))
1074    }
1075}
1076
1077impl Ord for CapturedOutput {
1078    fn cmp(&self, other: &Self) -> Ordering {
1079        self.timestamp().cmp(&other.timestamp())
1080    }
1081}
1082
1083#[cfg(test)]
1084mod error_reporting_tests {
1085    use super::*;
1086    use std::panic::{catch_unwind, AssertUnwindSafe};
1087    use std::time::Duration;
1088
1089    fn simulate_runner(
1090        test_fn: impl FnOnce() -> Box<dyn TestReturnValue> + std::panic::UnwindSafe,
1091    ) -> TestResult {
1092        crate::panic_hook::install_panic_hook();
1093        let test_id = crate::panic_hook::next_test_id();
1094        crate::panic_hook::set_current_test_id(test_id);
1095        let result = catch_unwind(AssertUnwindSafe(move || {
1096            let ret = test_fn();
1097            ret.into_result()?;
1098            Ok(())
1099        }));
1100        let test_result =
1101            TestResult::from_result(&ShouldPanic::No, Duration::from_millis(1), result);
1102        crate::panic_hook::clear_current_test_id();
1103        test_result
1104    }
1105
1106    #[test]
1107    fn panic_with_assert_eq() {
1108        let result = simulate_runner(|| {
1109            assert_eq!(1, 2);
1110            Box::new(())
1111        });
1112        assert!(result.is_failed());
1113        let msg = result.failure_message().unwrap();
1114        println!("=== panic assert_eq failure message ===\n{msg}\n===");
1115        assert!(
1116            msg.contains("assertion `left == right` failed"),
1117            "Expected assertion message, got: {msg}"
1118        );
1119        assert!(
1120            msg.contains("at "),
1121            "Expected location info in message, got: {msg}"
1122        );
1123    }
1124
1125    #[test]
1126    fn string_error() {
1127        let result = simulate_runner(|| {
1128            let r: Result<(), String> = Err("something went wrong".to_string());
1129            Box::new(r)
1130        });
1131        assert!(result.is_failed());
1132        let msg = result.failure_message().unwrap();
1133        println!("=== string error failure message ===\n{msg}\n===");
1134        assert_eq!(msg, "something went wrong");
1135    }
1136
1137    #[test]
1138    fn anyhow_error() {
1139        let result = simulate_runner(|| {
1140            let inner = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
1141            let err = anyhow::anyhow!(inner).context("operation failed");
1142            let r: Result<(), anyhow::Error> = Err(err);
1143            Box::new(r)
1144        });
1145        assert!(result.is_failed());
1146        let msg = result.failure_message().unwrap();
1147        println!("=== anyhow error failure message ===\n{msg}\n===");
1148        assert!(
1149            msg.contains("operation failed"),
1150            "Expected 'operation failed', got: {msg}"
1151        );
1152        assert!(
1153            msg.contains("file not found"),
1154            "Expected 'file not found', got: {msg}"
1155        );
1156    }
1157
1158    #[test]
1159    fn std_io_error() {
1160        let result = simulate_runner(|| {
1161            let r: Result<(), std::io::Error> = Err(std::io::Error::new(
1162                std::io::ErrorKind::NotFound,
1163                "file not found",
1164            ));
1165            Box::new(r)
1166        });
1167        assert!(result.is_failed());
1168        let msg = result.failure_message().unwrap();
1169        println!("=== std io error failure message ===\n{msg}\n===");
1170        // Should use Display (not Debug), so no "Custom { kind: NotFound, ... }"
1171        assert_eq!(msg, "file not found");
1172    }
1173
1174    #[test]
1175    fn panic_with_location_info() {
1176        let result = simulate_runner(|| {
1177            panic!("test panic with location");
1178            #[allow(unreachable_code)]
1179            Box::new(())
1180        });
1181        assert!(result.is_failed());
1182        let cause = result.failure_cause().unwrap();
1183        match cause {
1184            FailureCause::Panic(p) => {
1185                assert!(p.location.is_some(), "Expected location info");
1186                let loc = p.location.as_ref().unwrap();
1187                assert!(
1188                    loc.file.contains("internal.rs"),
1189                    "Expected file to contain internal.rs, got: {}",
1190                    loc.file
1191                );
1192                assert!(loc.line > 0, "Expected non-zero line number");
1193            }
1194            other => panic!("Expected Panic cause, got: {other:?}"),
1195        }
1196    }
1197
1198    #[test]
1199    fn panic_render_includes_location() {
1200        let result = simulate_runner(|| {
1201            panic!("location test");
1202            #[allow(unreachable_code)]
1203            Box::new(())
1204        });
1205        let msg = result.failure_message().unwrap();
1206        assert!(
1207            msg.contains("location test"),
1208            "Expected panic message, got: {msg}"
1209        );
1210        assert!(
1211            msg.contains("\n  at "),
1212            "Expected location line in render, got: {msg}"
1213        );
1214    }
1215
1216    #[test]
1217    fn should_panic_with_message_matching() {
1218        crate::panic_hook::install_panic_hook();
1219        let test_id = crate::panic_hook::next_test_id();
1220        crate::panic_hook::set_current_test_id(test_id);
1221        let result = catch_unwind(AssertUnwindSafe(|| {
1222            panic!("expected panic message");
1223        }));
1224        let test_result = TestResult::from_result(
1225            &ShouldPanic::WithMessage("expected panic".to_string()),
1226            Duration::from_millis(1),
1227            result.map(|_| Ok(())),
1228        );
1229        crate::panic_hook::clear_current_test_id();
1230        assert!(
1231            test_result.is_passed(),
1232            "Expected test to pass with matching panic message"
1233        );
1234    }
1235
1236    #[test]
1237    fn should_panic_with_wrong_message() {
1238        crate::panic_hook::install_panic_hook();
1239        let test_id = crate::panic_hook::next_test_id();
1240        crate::panic_hook::set_current_test_id(test_id);
1241        let result = catch_unwind(AssertUnwindSafe(|| {
1242            panic!("actual panic message");
1243        }));
1244        let test_result = TestResult::from_result(
1245            &ShouldPanic::WithMessage("completely different".to_string()),
1246            Duration::from_millis(1),
1247            result.map(|_| Ok(())),
1248        );
1249        crate::panic_hook::clear_current_test_id();
1250        assert!(
1251            test_result.is_failed(),
1252            "Expected test to fail with wrong panic message"
1253        );
1254        let msg = test_result.failure_message().unwrap();
1255        assert!(
1256            msg.contains("unexpected message"),
1257            "Expected 'unexpected message' in: {msg}"
1258        );
1259    }
1260
1261    #[test]
1262    fn pretty_assertions_diff() {
1263        let result = simulate_runner(|| {
1264            pretty_assertions::assert_eq!("hello world\nfoo\nbar\n", "hello world\nbaz\nbar\n");
1265            Box::new(())
1266        });
1267        assert!(result.is_failed());
1268        let cause = result.failure_cause().unwrap();
1269
1270        // Should be a Panic variant (assert_eq! panics)
1271        let panic_cause = match cause {
1272            FailureCause::Panic(p) => p,
1273            other => panic!("Expected Panic cause, got: {other:?}"),
1274        };
1275
1276        // The panic message should contain the colorful diff from pretty_assertions
1277        let message = panic_cause.message.as_deref().unwrap();
1278        println!("=== pretty_assertions failure message ===\n{message}\n===");
1279        assert!(
1280            message.contains("foo") && message.contains("baz"),
1281            "Expected diff with 'foo' and 'baz', got: {message}"
1282        );
1283
1284        // Location should be captured
1285        assert!(panic_cause.location.is_some(), "Expected location info");
1286
1287        // The rendered output should NOT contain backtrace noise when RUST_BACKTRACE is unset
1288        let rendered = cause.render();
1289        println!("=== pretty_assertions rendered ===\n{rendered}\n===");
1290        assert!(
1291            !rendered.contains("stack backtrace") && !rendered.contains("Stack backtrace"),
1292            "Expected no backtrace noise in rendered output, got: {rendered}"
1293        );
1294        // Should contain location
1295        assert!(
1296            rendered.contains("\n  at "),
1297            "Expected location in rendered output, got: {rendered}"
1298        );
1299    }
1300
1301    #[test]
1302    fn detached_thread_panic_detected() {
1303        crate::panic_hook::install_panic_hook();
1304        let test_id = crate::panic_hook::next_test_id();
1305        crate::panic_hook::set_current_test_id(test_id);
1306        crate::panic_hook::create_detached_collector(test_id);
1307
1308        let result = catch_unwind(AssertUnwindSafe(|| {
1309            let handle = crate::spawn::spawn_thread(|| {
1310                panic!("background thread panic");
1311            });
1312            let _ = handle.join();
1313        }));
1314
1315        let mut test_result = TestResult::from_result(
1316            &ShouldPanic::No,
1317            Duration::from_millis(1),
1318            result.map(|_| Ok(())),
1319        );
1320
1321        if let Some(collector) = crate::panic_hook::take_detached_collector(test_id) {
1322            let panics = match collector.lock() {
1323                Ok(p) => p,
1324                Err(poisoned) => poisoned.into_inner(),
1325            };
1326            if !panics.is_empty() && test_result.is_passed() {
1327                let messages: Vec<String> = panics.iter().map(|p| p.render()).collect();
1328                test_result = TestResult::failed(
1329                    Duration::from_millis(1),
1330                    FailureCause::Panic(PanicCause {
1331                        message: Some(format!(
1332                            "Detached task(s) panicked:\n{}",
1333                            messages.join("\n---\n")
1334                        )),
1335                        location: panics.first().and_then(|p| p.location.clone()),
1336                        backtrace: panics.first().and_then(|p| p.backtrace.clone()),
1337                    }),
1338                );
1339            }
1340        }
1341
1342        crate::panic_hook::clear_current_test_id();
1343
1344        assert!(
1345            test_result.is_failed(),
1346            "Expected test to fail due to detached panic"
1347        );
1348        let msg = test_result.failure_message().unwrap();
1349        assert!(
1350            msg.contains("Detached task(s) panicked"),
1351            "Expected detached panic message, got: {msg}"
1352        );
1353        assert!(
1354            msg.contains("background thread panic"),
1355            "Expected original panic message, got: {msg}"
1356        );
1357    }
1358
1359    #[test]
1360    fn detached_thread_panic_ignored_with_policy() {
1361        crate::panic_hook::install_panic_hook();
1362        let test_id = crate::panic_hook::next_test_id();
1363        crate::panic_hook::set_current_test_id(test_id);
1364        crate::panic_hook::create_detached_collector(test_id);
1365
1366        let result = catch_unwind(AssertUnwindSafe(|| {
1367            let handle = crate::spawn::spawn_thread(|| {
1368                panic!("ignored thread panic");
1369            });
1370            let _ = handle.join();
1371        }));
1372
1373        let test_result = TestResult::from_result(
1374            &ShouldPanic::No,
1375            Duration::from_millis(1),
1376            result.map(|_| Ok(())),
1377        );
1378
1379        if let Some(collector) = crate::panic_hook::take_detached_collector(test_id) {
1380            let panics = match collector.lock() {
1381                Ok(p) => p,
1382                Err(poisoned) => poisoned.into_inner(),
1383            };
1384            // Verify panics were captured but Ignore policy does not fail the test
1385            assert!(
1386                !panics.is_empty(),
1387                "Expected panics in collector even with Ignore policy"
1388            );
1389        }
1390
1391        crate::panic_hook::clear_current_test_id();
1392
1393        assert!(
1394            test_result.is_passed(),
1395            "Expected test to pass with Ignore policy"
1396        );
1397    }
1398
1399    #[cfg(feature = "tokio")]
1400    #[test]
1401    fn detached_task_panic_detected() {
1402        let rt = tokio::runtime::Runtime::new().unwrap();
1403        rt.block_on(async {
1404            crate::panic_hook::install_panic_hook();
1405            let test_id = crate::panic_hook::next_test_id();
1406            crate::panic_hook::set_current_test_id(test_id);
1407            crate::panic_hook::create_detached_collector(test_id);
1408
1409            let handle = crate::spawn::spawn(async {
1410                panic!("detached task panic");
1411            });
1412            let _ = handle.await;
1413
1414            let collector = crate::panic_hook::take_detached_collector(test_id).unwrap();
1415            let panics = collector.lock().unwrap();
1416
1417            assert_eq!(panics.len(), 1);
1418            assert!(
1419                panics[0]
1420                    .message
1421                    .as_ref()
1422                    .unwrap()
1423                    .contains("detached task panic"),
1424                "Expected panic message, got: {:?}",
1425                panics[0].message
1426            );
1427
1428            crate::panic_hook::clear_current_test_id();
1429        });
1430    }
1431
1432    #[test]
1433    fn failure_cause_variants() {
1434        // ReturnedMessage
1435        let cause = FailureCause::ReturnedMessage("simple message".to_string());
1436        assert_eq!(cause.render(), "simple message");
1437        assert!(cause.panic_message().is_none());
1438
1439        // ReturnedError (prefer display)
1440        let cause = FailureCause::ReturnedError {
1441            display: "display text".to_string(),
1442            debug: "debug text".to_string(),
1443            prefer_debug: false,
1444            error: Arc::new("display text".to_string()),
1445        };
1446        assert_eq!(cause.render(), "display text");
1447
1448        // ReturnedError (prefer debug, e.g. anyhow)
1449        let cause = FailureCause::ReturnedError {
1450            display: "display text".to_string(),
1451            debug: "debug text".to_string(),
1452            prefer_debug: true,
1453            error: Arc::new("debug text".to_string()),
1454        };
1455        assert_eq!(cause.render(), "debug text");
1456
1457        // HarnessError
1458        let cause = FailureCause::HarnessError("harness error".to_string());
1459        assert_eq!(cause.render(), "harness error");
1460
1461        // Panic with message
1462        let cause = FailureCause::Panic(PanicCause {
1463            message: Some("panic msg".to_string()),
1464            location: None,
1465            backtrace: None,
1466        });
1467        assert_eq!(cause.render(), "panic msg");
1468        assert_eq!(cause.panic_message(), Some("panic msg"));
1469    }
1470}
1471
1472#[cfg(test)]
1473mod filter_tests {
1474    use super::*;
1475
1476    fn make_test(name: &str, module_path: &str) -> RegisteredTest {
1477        RegisteredTest {
1478            name: name.to_string(),
1479            crate_name: "mycrate".to_string(),
1480            module_path: module_path.to_string(),
1481            run: TestFunction::Sync(Arc::new(|_| Box::new(()))),
1482            props: TestProperties::default(),
1483            dependencies: None,
1484        }
1485    }
1486
1487    fn make_tagged_test(name: &str, module_path: &str, tags: Vec<&str>) -> RegisteredTest {
1488        let mut test = make_test(name, module_path);
1489        test.props.tags = tags.into_iter().map(String::from).collect();
1490        test
1491    }
1492
1493    fn make_args(filters: Vec<&str>, skip: Vec<&str>, exact: bool) -> Arguments {
1494        Arguments {
1495            filter: filters.into_iter().map(String::from).collect(),
1496            skip: skip.into_iter().map(String::from).collect(),
1497            exact,
1498            ..Default::default()
1499        }
1500    }
1501
1502    fn filtered_names(args: &Arguments, tests: &[RegisteredTest]) -> Vec<String> {
1503        filter_registered_tests(args, tests)
1504            .into_iter()
1505            .map(|t| t.filterable_name())
1506            .collect()
1507    }
1508
1509    // --- filter_test unit tests ---
1510
1511    #[test]
1512    fn filter_test_substring_match() {
1513        let test = make_test("hello_world", "mod1");
1514        assert!(filter_test(&test, "hello", false));
1515        assert!(filter_test(&test, "world", false));
1516        assert!(filter_test(&test, "mod1::hello", false));
1517        assert!(!filter_test(&test, "nonexistent", false));
1518    }
1519
1520    #[test]
1521    fn filter_test_exact_match() {
1522        let test = make_test("hello_world", "mod1");
1523        assert!(filter_test(&test, "mod1::hello_world", true));
1524        assert!(!filter_test(&test, "hello_world", true));
1525        assert!(!filter_test(&test, "hello", true));
1526    }
1527
1528    #[test]
1529    fn filter_test_tag_match() {
1530        let test = make_tagged_test("t1", "mod1", vec!["fast", "unit"]);
1531        assert!(filter_test(&test, ":tag:fast", false));
1532        assert!(filter_test(&test, ":tag:unit", false));
1533        assert!(!filter_test(&test, ":tag:slow", false));
1534    }
1535
1536    #[test]
1537    fn filter_test_tag_empty_matches_untagged() {
1538        let untagged = make_test("t1", "mod1");
1539        let tagged = make_tagged_test("t2", "mod1", vec!["fast"]);
1540        assert!(filter_test(&untagged, ":tag:", false));
1541        assert!(!filter_test(&tagged, ":tag:", false));
1542    }
1543
1544    // --- filter_registered_tests: multiple include filters (OR semantics) ---
1545
1546    #[test]
1547    fn no_filters_includes_all() {
1548        let tests = vec![make_test("a", "m"), make_test("b", "m")];
1549        let args = make_args(vec![], vec![], false);
1550        assert_eq!(filtered_names(&args, &tests), vec!["m::a", "m::b"]);
1551    }
1552
1553    #[test]
1554    fn single_filter_substring() {
1555        let tests = vec![
1556            make_test("alpha", "m"),
1557            make_test("beta", "m"),
1558            make_test("alphabet", "m"),
1559        ];
1560        let args = make_args(vec!["alpha"], vec![], false);
1561        assert_eq!(
1562            filtered_names(&args, &tests),
1563            vec!["m::alpha", "m::alphabet"]
1564        );
1565    }
1566
1567    #[test]
1568    fn multiple_filters_or_semantics() {
1569        let tests = vec![
1570            make_test("alpha", "m"),
1571            make_test("beta", "m"),
1572            make_test("gamma", "m"),
1573        ];
1574        let args = make_args(vec!["alpha", "gamma"], vec![], false);
1575        assert_eq!(filtered_names(&args, &tests), vec!["m::alpha", "m::gamma"]);
1576    }
1577
1578    #[test]
1579    fn multiple_filters_exact() {
1580        let tests = vec![
1581            make_test("alpha", "m"),
1582            make_test("alphabet", "m"),
1583            make_test("beta", "m"),
1584        ];
1585        let args = make_args(vec!["m::alpha", "m::beta"], vec![], true);
1586        assert_eq!(filtered_names(&args, &tests), vec!["m::alpha", "m::beta"]);
1587    }
1588
1589    // --- skip behavior ---
1590
1591    #[test]
1592    fn skip_substring_match() {
1593        let tests = vec![
1594            make_test("fast_test", "m"),
1595            make_test("slow_test", "m"),
1596            make_test("slower_test", "m"),
1597        ];
1598        let args = make_args(vec![], vec!["slow"], false);
1599        assert_eq!(filtered_names(&args, &tests), vec!["m::fast_test"]);
1600    }
1601
1602    #[test]
1603    fn skip_exact_match() {
1604        let tests = vec![make_test("slow_test", "m"), make_test("slower_test", "m")];
1605        let args = make_args(vec![], vec!["m::slow_test"], true);
1606        assert_eq!(filtered_names(&args, &tests), vec!["m::slower_test"]);
1607    }
1608
1609    #[test]
1610    fn skip_with_tag() {
1611        let tests = vec![
1612            make_tagged_test("t1", "m", vec!["slow"]),
1613            make_tagged_test("t2", "m", vec!["fast"]),
1614            make_test("t3", "m"),
1615        ];
1616        let args = make_args(vec![], vec![":tag:slow"], false);
1617        assert_eq!(filtered_names(&args, &tests), vec!["m::t2", "m::t3"]);
1618    }
1619
1620    // --- combined include + skip ---
1621
1622    #[test]
1623    fn include_and_skip_combined() {
1624        let tests = vec![
1625            make_test("alpha_fast", "m"),
1626            make_test("alpha_slow", "m"),
1627            make_test("beta_fast", "m"),
1628        ];
1629        // Include anything with "alpha", but skip anything with "slow"
1630        let args = make_args(vec!["alpha"], vec!["slow"], false);
1631        assert_eq!(filtered_names(&args, &tests), vec!["m::alpha_fast"]);
1632    }
1633
1634    #[test]
1635    fn skip_wins_over_include() {
1636        let tests = vec![make_test("target", "m")];
1637        // Both include and skip match the same test — skip should win
1638        let args = make_args(vec!["target"], vec!["target"], false);
1639        assert_eq!(filtered_names(&args, &tests), Vec::<String>::new());
1640    }
1641
1642    // --- tag boolean expression syntax ---
1643
1644    #[test]
1645    fn filter_test_tag_or_expression() {
1646        // `:tag:a|b` matches tests tagged with `a` OR `b`
1647        let test_a = make_tagged_test("t1", "m", vec!["a"]);
1648        let test_b = make_tagged_test("t2", "m", vec!["b"]);
1649        let test_c = make_tagged_test("t3", "m", vec!["c"]);
1650        assert!(filter_test(&test_a, ":tag:a|b", false));
1651        assert!(filter_test(&test_b, ":tag:a|b", false));
1652        assert!(!filter_test(&test_c, ":tag:a|b", false));
1653    }
1654
1655    #[test]
1656    fn filter_test_tag_and_expression() {
1657        // `:tag:a&b` matches tests tagged with BOTH `a` AND `b`
1658        let test_ab = make_tagged_test("t1", "m", vec!["a", "b"]);
1659        let test_a = make_tagged_test("t2", "m", vec!["a"]);
1660        let test_b = make_tagged_test("t3", "m", vec!["b"]);
1661        assert!(filter_test(&test_ab, ":tag:a&b", false));
1662        assert!(!filter_test(&test_a, ":tag:a&b", false));
1663        assert!(!filter_test(&test_b, ":tag:a&b", false));
1664    }
1665
1666    #[test]
1667    fn filter_test_tag_mixed_and_or() {
1668        // `:tag:a|b&c` means `a OR (b AND c)` — `&` has higher precedence
1669        let test_a = make_tagged_test("t1", "m", vec!["a"]);
1670        let test_bc = make_tagged_test("t2", "m", vec!["b", "c"]);
1671        let test_b = make_tagged_test("t3", "m", vec!["b"]);
1672        let test_c = make_tagged_test("t4", "m", vec!["c"]);
1673        let test_none = make_test("t5", "m");
1674        assert!(filter_test(&test_a, ":tag:a|b&c", false));
1675        assert!(filter_test(&test_bc, ":tag:a|b&c", false));
1676        assert!(!filter_test(&test_b, ":tag:a|b&c", false));
1677        assert!(!filter_test(&test_c, ":tag:a|b&c", false));
1678        assert!(!filter_test(&test_none, ":tag:a|b&c", false));
1679    }
1680
1681    #[test]
1682    fn filter_test_tag_exact_flag_does_not_affect_tags() {
1683        // `--exact` should not change tag matching behavior
1684        let test = make_tagged_test("t1", "m", vec!["fast"]);
1685        assert!(filter_test(&test, ":tag:fast", true));
1686        assert!(!filter_test(&test, ":tag:slow", true));
1687    }
1688
1689    #[test]
1690    fn include_by_tag_or_expression() {
1691        let tests = vec![
1692            make_tagged_test("t1", "m", vec!["unit"]),
1693            make_tagged_test("t2", "m", vec!["integration"]),
1694            make_tagged_test("t3", "m", vec!["e2e"]),
1695        ];
1696        let args = make_args(vec![":tag:unit|integration"], vec![], false);
1697        assert_eq!(filtered_names(&args, &tests), vec!["m::t1", "m::t2"]);
1698    }
1699
1700    #[test]
1701    fn skip_by_tag_and_expression() {
1702        let tests = vec![
1703            make_tagged_test("t1", "m", vec!["slow", "network"]),
1704            make_tagged_test("t2", "m", vec!["slow"]),
1705            make_tagged_test("t3", "m", vec!["network"]),
1706            make_test("t4", "m"),
1707        ];
1708        // Skip only tests that are BOTH slow AND network
1709        let args = make_args(vec![], vec![":tag:slow&network"], false);
1710        assert_eq!(
1711            filtered_names(&args, &tests),
1712            vec!["m::t2", "m::t3", "m::t4"]
1713        );
1714    }
1715}