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_props_to_tests(
624    tests: &[RegisteredTest],
625    props: &[RegisteredTestSuiteProperty],
626) -> Vec<RegisteredTest> {
627    let props_with_prefix = props
628        .iter()
629        .map(|prop| (prop.crate_and_module(), prop))
630        .collect::<Vec<_>>();
631
632    let mut result = Vec::new();
633    for test in tests {
634        let mut test = test.clone();
635        for (prefix, prop) in &props_with_prefix {
636            if test.crate_and_module().starts_with(prefix) {
637                match prop {
638                    RegisteredTestSuiteProperty::Tag { tag, .. } => {
639                        test.props.tags.push(tag.clone());
640                    }
641                    RegisteredTestSuiteProperty::Timeout { timeout, .. } => {
642                        if test.props.timeout.is_none() {
643                            test.props.timeout = Some(*timeout);
644                        }
645                    }
646                    RegisteredTestSuiteProperty::Sequential { .. } => {
647                        // handled in TestSuiteExecution
648                    }
649                }
650            }
651        }
652        result.push(test);
653    }
654    result
655}
656
657pub(crate) fn filter_registered_tests(
658    args: &Arguments,
659    registered_tests: &[RegisteredTest],
660) -> Vec<RegisteredTest> {
661    registered_tests
662        .iter()
663        .filter(|registered_test| {
664            !args
665                .skip
666                .iter()
667                .any(|skip| filter_test(registered_test, skip, args.exact))
668        })
669        .filter(|registered_test| {
670            args.filter.is_empty()
671                || args
672                    .filter
673                    .iter()
674                    .any(|filter| filter_test(registered_test, filter, args.exact))
675        })
676        .filter(|registered_tests| {
677            (args.bench && registered_tests.run.is_bench())
678                || (args.test && !registered_tests.run.is_bench())
679                || (!args.bench && !args.test)
680        })
681        .filter(|registered_test| {
682            !args.exclude_should_panic || registered_test.props.should_panic == ShouldPanic::No
683        })
684        .cloned()
685        .collect::<Vec<_>>()
686}
687
688fn add_generated_tests(
689    target: &mut Vec<RegisteredTest>,
690    generator: &RegisteredTestGenerator,
691    generated: Vec<GeneratedTest>,
692) {
693    target.extend(generated.into_iter().map(|mut test| {
694        test.props.is_ignored |= generator.is_ignored;
695        RegisteredTest {
696            name: format!("{}::{}", generator.name, test.name),
697            crate_name: generator.crate_name.clone(),
698            module_path: generator.module_path.clone(),
699            run: test.run,
700            props: test.props,
701            dependencies: test.dependencies,
702        }
703    }));
704}
705
706#[cfg(feature = "tokio")]
707pub(crate) async fn generate_tests(generators: &[RegisteredTestGenerator]) -> Vec<RegisteredTest> {
708    let mut result = Vec::new();
709    for generator in generators {
710        match &generator.run {
711            TestGeneratorFunction::Sync(generator_fn) => {
712                let tests = generator_fn();
713                add_generated_tests(&mut result, generator, tests);
714            }
715            TestGeneratorFunction::Async(generator_fn) => {
716                let tests = generator_fn().await;
717                add_generated_tests(&mut result, generator, tests);
718            }
719        }
720    }
721    result
722}
723
724pub(crate) fn generate_tests_sync(generators: &[RegisteredTestGenerator]) -> Vec<RegisteredTest> {
725    let mut result = Vec::new();
726    for generator in generators {
727        match &generator.run {
728            TestGeneratorFunction::Sync(generator_fn) => {
729                let tests = generator_fn();
730                add_generated_tests(&mut result, generator, tests);
731            }
732            TestGeneratorFunction::Async(_) => {
733                panic!("Async test generators are not supported in sync mode")
734            }
735        }
736    }
737    result
738}
739
740pub(crate) fn get_ensure_time(args: &Arguments, test: &RegisteredTest) -> Option<TimeThreshold> {
741    let should_ensure_time = match test.props.ensure_time_control {
742        ReportTimeControl::Default => args.ensure_time,
743        ReportTimeControl::Enabled => true,
744        ReportTimeControl::Disabled => false,
745    };
746    if should_ensure_time {
747        match test.props.test_type {
748            TestType::UnitTest => Some(args.unit_test_threshold()),
749            TestType::IntegrationTest => Some(args.integration_test_threshold()),
750        }
751    } else {
752        None
753    }
754}
755
756#[derive(Clone)]
757pub enum TestResult {
758    Passed {
759        captured: Vec<CapturedOutput>,
760        exec_time: Duration,
761    },
762    Benchmarked {
763        captured: Vec<CapturedOutput>,
764        exec_time: Duration,
765        ns_iter_summ: Summary,
766        mb_s: usize,
767    },
768    Failed {
769        cause: FailureCause,
770        captured: Vec<CapturedOutput>,
771        exec_time: Duration,
772    },
773    Ignored {
774        captured: Vec<CapturedOutput>,
775    },
776}
777
778impl TestResult {
779    pub fn passed(exec_time: Duration) -> Self {
780        TestResult::Passed {
781            captured: Vec::new(),
782            exec_time,
783        }
784    }
785
786    pub fn benchmarked(exec_time: Duration, ns_iter_summ: Summary, mb_s: usize) -> Self {
787        TestResult::Benchmarked {
788            captured: Vec::new(),
789            exec_time,
790            ns_iter_summ,
791            mb_s,
792        }
793    }
794
795    pub fn failed(exec_time: Duration, cause: FailureCause) -> Self {
796        TestResult::Failed {
797            cause,
798            captured: Vec::new(),
799            exec_time,
800        }
801    }
802
803    pub fn ignored() -> Self {
804        TestResult::Ignored {
805            captured: Vec::new(),
806        }
807    }
808
809    pub(crate) fn is_passed(&self) -> bool {
810        matches!(self, TestResult::Passed { .. })
811    }
812
813    pub(crate) fn is_benchmarked(&self) -> bool {
814        matches!(self, TestResult::Benchmarked { .. })
815    }
816
817    pub(crate) fn is_failed(&self) -> bool {
818        matches!(self, TestResult::Failed { .. })
819    }
820
821    pub(crate) fn is_ignored(&self) -> bool {
822        matches!(self, TestResult::Ignored { .. })
823    }
824
825    pub(crate) fn captured_output(&self) -> &Vec<CapturedOutput> {
826        match self {
827            TestResult::Passed { captured, .. } => captured,
828            TestResult::Failed { captured, .. } => captured,
829            TestResult::Ignored { captured, .. } => captured,
830            TestResult::Benchmarked { captured, .. } => captured,
831        }
832    }
833
834    pub(crate) fn stats(&self) -> Option<&Summary> {
835        match self {
836            TestResult::Benchmarked { ns_iter_summ, .. } => Some(ns_iter_summ),
837            _ => None,
838        }
839    }
840
841    pub(crate) fn set_captured_output(&mut self, captured: Vec<CapturedOutput>) {
842        match self {
843            TestResult::Passed {
844                captured: captured_ref,
845                ..
846            } => *captured_ref = captured,
847            TestResult::Failed {
848                captured: captured_ref,
849                ..
850            } => *captured_ref = captured,
851            TestResult::Ignored {
852                captured: captured_ref,
853            } => *captured_ref = captured,
854            TestResult::Benchmarked {
855                captured: captured_ref,
856                ..
857            } => *captured_ref = captured,
858        }
859    }
860
861    pub(crate) fn from_result<A>(
862        should_panic: &ShouldPanic,
863        elapsed: Duration,
864        result: Result<Result<A, FailureCause>, Box<dyn Any + Send>>,
865    ) -> Self {
866        match result {
867            Ok(Ok(_)) => {
868                if should_panic == &ShouldPanic::No {
869                    TestResult::passed(elapsed)
870                } else {
871                    TestResult::failed(
872                        elapsed,
873                        FailureCause::HarnessError("Test did not panic as expected".to_string()),
874                    )
875                }
876            }
877            Ok(Err(cause)) => TestResult::failed(elapsed, cause),
878            Err(panic) => TestResult::from_panic(should_panic, elapsed, panic),
879        }
880    }
881
882    pub(crate) fn from_summary(
883        should_panic: &ShouldPanic,
884        elapsed: Duration,
885        result: Result<Summary, Box<dyn Any + Send>>,
886        bytes: u64,
887    ) -> Self {
888        match result {
889            Ok(summary) => {
890                let ns_iter = max(summary.median as u64, 1);
891                let mb_s = bytes * 1000 / ns_iter;
892                TestResult::benchmarked(elapsed, summary, mb_s as usize)
893            }
894            Err(panic) => Self::from_panic(should_panic, elapsed, panic),
895        }
896    }
897
898    fn from_panic(
899        should_panic: &ShouldPanic,
900        elapsed: Duration,
901        panic: Box<dyn Any + Send>,
902    ) -> Self {
903        let captured = crate::panic_hook::take_current_panic_capture();
904
905        let panic_cause = if let Some(cause) = captured {
906            cause
907        } else {
908            let message = panic
909                .downcast_ref::<String>()
910                .cloned()
911                .or(panic.downcast_ref::<&str>().map(|s| s.to_string()));
912            PanicCause {
913                message,
914                location: None,
915                backtrace: None,
916            }
917        };
918
919        match should_panic {
920            ShouldPanic::WithMessage(expected) => match &panic_cause.message {
921                Some(message) if message.contains(expected) => TestResult::passed(elapsed),
922                _ => TestResult::failed(
923                    elapsed,
924                    FailureCause::Panic(PanicCause {
925                        message: Some(format!(
926                            "Test panicked with unexpected message: {}",
927                            panic_cause.message.as_deref().unwrap_or_default()
928                        )),
929                        location: None,
930                        backtrace: None,
931                    }),
932                ),
933            },
934            ShouldPanic::Yes => TestResult::passed(elapsed),
935            ShouldPanic::No => TestResult::failed(elapsed, FailureCause::Panic(panic_cause)),
936        }
937    }
938
939    pub(crate) fn failure_message(&self) -> Option<String> {
940        self.failure_cause().map(|c| c.render())
941    }
942
943    pub fn failure_cause(&self) -> Option<&FailureCause> {
944        match self {
945            TestResult::Failed { cause, .. } => Some(cause),
946            _ => None,
947        }
948    }
949}
950
951pub struct SuiteResult {
952    pub passed: usize,
953    pub failed: usize,
954    pub ignored: usize,
955    pub measured: usize,
956    pub filtered_out: usize,
957    pub exec_time: Duration,
958}
959
960impl SuiteResult {
961    pub fn from_test_results(
962        registered_tests: &[RegisteredTest],
963        results: &[(RegisteredTest, TestResult)],
964        exec_time: Duration,
965    ) -> Self {
966        let passed = results
967            .iter()
968            .filter(|(_, result)| result.is_passed())
969            .count();
970        let measured = results
971            .iter()
972            .filter(|(_, result)| result.is_benchmarked())
973            .count();
974        let failed = results
975            .iter()
976            .filter(|(_, result)| result.is_failed())
977            .count();
978        let ignored = results
979            .iter()
980            .filter(|(_, result)| result.is_ignored())
981            .count();
982        let filtered_out = registered_tests.len() - results.len();
983
984        Self {
985            passed,
986            failed,
987            ignored,
988            measured,
989            filtered_out,
990            exec_time,
991        }
992    }
993
994    pub fn exit_code(results: &[(RegisteredTest, TestResult)]) -> ExitCode {
995        if results.iter().any(|(_, result)| result.is_failed()) {
996            ExitCode::from(101)
997        } else {
998            ExitCode::SUCCESS
999        }
1000    }
1001}
1002
1003pub trait DependencyView: Debug {
1004    fn get(&self, name: &str) -> Option<Arc<dyn Any + Send + Sync>>;
1005}
1006
1007impl DependencyView for Arc<dyn DependencyView + Send + Sync> {
1008    fn get(&self, name: &str) -> Option<Arc<dyn Any + Send + Sync>> {
1009        self.as_ref().get(name)
1010    }
1011}
1012
1013#[derive(Debug, Clone, Eq, PartialEq)]
1014pub enum CapturedOutput {
1015    Stdout { timestamp: SystemTime, line: String },
1016    Stderr { timestamp: SystemTime, line: String },
1017}
1018
1019impl CapturedOutput {
1020    pub fn stdout(line: String) -> Self {
1021        CapturedOutput::Stdout {
1022            timestamp: SystemTime::now(),
1023            line,
1024        }
1025    }
1026
1027    pub fn stderr(line: String) -> Self {
1028        CapturedOutput::Stderr {
1029            timestamp: SystemTime::now(),
1030            line,
1031        }
1032    }
1033
1034    pub fn timestamp(&self) -> SystemTime {
1035        match self {
1036            CapturedOutput::Stdout { timestamp, .. } => *timestamp,
1037            CapturedOutput::Stderr { timestamp, .. } => *timestamp,
1038        }
1039    }
1040
1041    pub fn line(&self) -> &str {
1042        match self {
1043            CapturedOutput::Stdout { line, .. } => line,
1044            CapturedOutput::Stderr { line, .. } => line,
1045        }
1046    }
1047}
1048
1049impl PartialOrd for CapturedOutput {
1050    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1051        Some(self.cmp(other))
1052    }
1053}
1054
1055impl Ord for CapturedOutput {
1056    fn cmp(&self, other: &Self) -> Ordering {
1057        self.timestamp().cmp(&other.timestamp())
1058    }
1059}
1060
1061#[cfg(test)]
1062mod error_reporting_tests {
1063    use super::*;
1064    use std::panic::{catch_unwind, AssertUnwindSafe};
1065    use std::time::Duration;
1066
1067    fn simulate_runner(
1068        test_fn: impl FnOnce() -> Box<dyn TestReturnValue> + std::panic::UnwindSafe,
1069    ) -> TestResult {
1070        crate::panic_hook::install_panic_hook();
1071        let test_id = crate::panic_hook::next_test_id();
1072        crate::panic_hook::set_current_test_id(test_id);
1073        let result = catch_unwind(AssertUnwindSafe(move || {
1074            let ret = test_fn();
1075            ret.into_result()?;
1076            Ok(())
1077        }));
1078        let test_result =
1079            TestResult::from_result(&ShouldPanic::No, Duration::from_millis(1), result);
1080        crate::panic_hook::clear_current_test_id();
1081        test_result
1082    }
1083
1084    #[test]
1085    fn panic_with_assert_eq() {
1086        let result = simulate_runner(|| {
1087            assert_eq!(1, 2);
1088            Box::new(())
1089        });
1090        assert!(result.is_failed());
1091        let msg = result.failure_message().unwrap();
1092        println!("=== panic assert_eq failure message ===\n{msg}\n===");
1093        assert!(
1094            msg.contains("assertion `left == right` failed"),
1095            "Expected assertion message, got: {msg}"
1096        );
1097        assert!(
1098            msg.contains("at "),
1099            "Expected location info in message, got: {msg}"
1100        );
1101    }
1102
1103    #[test]
1104    fn string_error() {
1105        let result = simulate_runner(|| {
1106            let r: Result<(), String> = Err("something went wrong".to_string());
1107            Box::new(r)
1108        });
1109        assert!(result.is_failed());
1110        let msg = result.failure_message().unwrap();
1111        println!("=== string error failure message ===\n{msg}\n===");
1112        assert_eq!(msg, "something went wrong");
1113    }
1114
1115    #[test]
1116    fn anyhow_error() {
1117        let result = simulate_runner(|| {
1118            let inner = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
1119            let err = anyhow::anyhow!(inner).context("operation failed");
1120            let r: Result<(), anyhow::Error> = Err(err);
1121            Box::new(r)
1122        });
1123        assert!(result.is_failed());
1124        let msg = result.failure_message().unwrap();
1125        println!("=== anyhow error failure message ===\n{msg}\n===");
1126        assert!(
1127            msg.contains("operation failed"),
1128            "Expected 'operation failed', got: {msg}"
1129        );
1130        assert!(
1131            msg.contains("file not found"),
1132            "Expected 'file not found', got: {msg}"
1133        );
1134    }
1135
1136    #[test]
1137    fn std_io_error() {
1138        let result = simulate_runner(|| {
1139            let r: Result<(), std::io::Error> = Err(std::io::Error::new(
1140                std::io::ErrorKind::NotFound,
1141                "file not found",
1142            ));
1143            Box::new(r)
1144        });
1145        assert!(result.is_failed());
1146        let msg = result.failure_message().unwrap();
1147        println!("=== std io error failure message ===\n{msg}\n===");
1148        // Should use Display (not Debug), so no "Custom { kind: NotFound, ... }"
1149        assert_eq!(msg, "file not found");
1150    }
1151
1152    #[test]
1153    fn panic_with_location_info() {
1154        let result = simulate_runner(|| {
1155            panic!("test panic with location");
1156            #[allow(unreachable_code)]
1157            Box::new(())
1158        });
1159        assert!(result.is_failed());
1160        let cause = result.failure_cause().unwrap();
1161        match cause {
1162            FailureCause::Panic(p) => {
1163                assert!(p.location.is_some(), "Expected location info");
1164                let loc = p.location.as_ref().unwrap();
1165                assert!(
1166                    loc.file.contains("internal.rs"),
1167                    "Expected file to contain internal.rs, got: {}",
1168                    loc.file
1169                );
1170                assert!(loc.line > 0, "Expected non-zero line number");
1171            }
1172            other => panic!("Expected Panic cause, got: {other:?}"),
1173        }
1174    }
1175
1176    #[test]
1177    fn panic_render_includes_location() {
1178        let result = simulate_runner(|| {
1179            panic!("location test");
1180            #[allow(unreachable_code)]
1181            Box::new(())
1182        });
1183        let msg = result.failure_message().unwrap();
1184        assert!(
1185            msg.contains("location test"),
1186            "Expected panic message, got: {msg}"
1187        );
1188        assert!(
1189            msg.contains("\n  at "),
1190            "Expected location line in render, got: {msg}"
1191        );
1192    }
1193
1194    #[test]
1195    fn should_panic_with_message_matching() {
1196        crate::panic_hook::install_panic_hook();
1197        let test_id = crate::panic_hook::next_test_id();
1198        crate::panic_hook::set_current_test_id(test_id);
1199        let result = catch_unwind(AssertUnwindSafe(|| {
1200            panic!("expected panic message");
1201        }));
1202        let test_result = TestResult::from_result(
1203            &ShouldPanic::WithMessage("expected panic".to_string()),
1204            Duration::from_millis(1),
1205            result.map(|_| Ok(())),
1206        );
1207        crate::panic_hook::clear_current_test_id();
1208        assert!(
1209            test_result.is_passed(),
1210            "Expected test to pass with matching panic message"
1211        );
1212    }
1213
1214    #[test]
1215    fn should_panic_with_wrong_message() {
1216        crate::panic_hook::install_panic_hook();
1217        let test_id = crate::panic_hook::next_test_id();
1218        crate::panic_hook::set_current_test_id(test_id);
1219        let result = catch_unwind(AssertUnwindSafe(|| {
1220            panic!("actual panic message");
1221        }));
1222        let test_result = TestResult::from_result(
1223            &ShouldPanic::WithMessage("completely different".to_string()),
1224            Duration::from_millis(1),
1225            result.map(|_| Ok(())),
1226        );
1227        crate::panic_hook::clear_current_test_id();
1228        assert!(
1229            test_result.is_failed(),
1230            "Expected test to fail with wrong panic message"
1231        );
1232        let msg = test_result.failure_message().unwrap();
1233        assert!(
1234            msg.contains("unexpected message"),
1235            "Expected 'unexpected message' in: {msg}"
1236        );
1237    }
1238
1239    #[test]
1240    fn pretty_assertions_diff() {
1241        let result = simulate_runner(|| {
1242            pretty_assertions::assert_eq!("hello world\nfoo\nbar\n", "hello world\nbaz\nbar\n");
1243            Box::new(())
1244        });
1245        assert!(result.is_failed());
1246        let cause = result.failure_cause().unwrap();
1247
1248        // Should be a Panic variant (assert_eq! panics)
1249        let panic_cause = match cause {
1250            FailureCause::Panic(p) => p,
1251            other => panic!("Expected Panic cause, got: {other:?}"),
1252        };
1253
1254        // The panic message should contain the colorful diff from pretty_assertions
1255        let message = panic_cause.message.as_deref().unwrap();
1256        println!("=== pretty_assertions failure message ===\n{message}\n===");
1257        assert!(
1258            message.contains("foo") && message.contains("baz"),
1259            "Expected diff with 'foo' and 'baz', got: {message}"
1260        );
1261
1262        // Location should be captured
1263        assert!(panic_cause.location.is_some(), "Expected location info");
1264
1265        // The rendered output should NOT contain backtrace noise when RUST_BACKTRACE is unset
1266        let rendered = cause.render();
1267        println!("=== pretty_assertions rendered ===\n{rendered}\n===");
1268        assert!(
1269            !rendered.contains("stack backtrace") && !rendered.contains("Stack backtrace"),
1270            "Expected no backtrace noise in rendered output, got: {rendered}"
1271        );
1272        // Should contain location
1273        assert!(
1274            rendered.contains("\n  at "),
1275            "Expected location in rendered output, got: {rendered}"
1276        );
1277    }
1278
1279    #[test]
1280    fn detached_thread_panic_detected() {
1281        crate::panic_hook::install_panic_hook();
1282        let test_id = crate::panic_hook::next_test_id();
1283        crate::panic_hook::set_current_test_id(test_id);
1284        crate::panic_hook::create_detached_collector(test_id);
1285
1286        let result = catch_unwind(AssertUnwindSafe(|| {
1287            let handle = crate::spawn::spawn_thread(|| {
1288                panic!("background thread panic");
1289            });
1290            let _ = handle.join();
1291        }));
1292
1293        let mut test_result = TestResult::from_result(
1294            &ShouldPanic::No,
1295            Duration::from_millis(1),
1296            result.map(|_| Ok(())),
1297        );
1298
1299        if let Some(collector) = crate::panic_hook::take_detached_collector(test_id) {
1300            let panics = match collector.lock() {
1301                Ok(p) => p,
1302                Err(poisoned) => poisoned.into_inner(),
1303            };
1304            if !panics.is_empty() && test_result.is_passed() {
1305                let messages: Vec<String> = panics.iter().map(|p| p.render()).collect();
1306                test_result = TestResult::failed(
1307                    Duration::from_millis(1),
1308                    FailureCause::Panic(PanicCause {
1309                        message: Some(format!(
1310                            "Detached task(s) panicked:\n{}",
1311                            messages.join("\n---\n")
1312                        )),
1313                        location: panics.first().and_then(|p| p.location.clone()),
1314                        backtrace: panics.first().and_then(|p| p.backtrace.clone()),
1315                    }),
1316                );
1317            }
1318        }
1319
1320        crate::panic_hook::clear_current_test_id();
1321
1322        assert!(
1323            test_result.is_failed(),
1324            "Expected test to fail due to detached panic"
1325        );
1326        let msg = test_result.failure_message().unwrap();
1327        assert!(
1328            msg.contains("Detached task(s) panicked"),
1329            "Expected detached panic message, got: {msg}"
1330        );
1331        assert!(
1332            msg.contains("background thread panic"),
1333            "Expected original panic message, got: {msg}"
1334        );
1335    }
1336
1337    #[test]
1338    fn detached_thread_panic_ignored_with_policy() {
1339        crate::panic_hook::install_panic_hook();
1340        let test_id = crate::panic_hook::next_test_id();
1341        crate::panic_hook::set_current_test_id(test_id);
1342        crate::panic_hook::create_detached_collector(test_id);
1343
1344        let result = catch_unwind(AssertUnwindSafe(|| {
1345            let handle = crate::spawn::spawn_thread(|| {
1346                panic!("ignored thread panic");
1347            });
1348            let _ = handle.join();
1349        }));
1350
1351        let test_result = TestResult::from_result(
1352            &ShouldPanic::No,
1353            Duration::from_millis(1),
1354            result.map(|_| Ok(())),
1355        );
1356
1357        if let Some(collector) = crate::panic_hook::take_detached_collector(test_id) {
1358            let panics = match collector.lock() {
1359                Ok(p) => p,
1360                Err(poisoned) => poisoned.into_inner(),
1361            };
1362            // Verify panics were captured but Ignore policy does not fail the test
1363            assert!(
1364                !panics.is_empty(),
1365                "Expected panics in collector even with Ignore policy"
1366            );
1367        }
1368
1369        crate::panic_hook::clear_current_test_id();
1370
1371        assert!(
1372            test_result.is_passed(),
1373            "Expected test to pass with Ignore policy"
1374        );
1375    }
1376
1377    #[cfg(feature = "tokio")]
1378    #[test]
1379    fn detached_task_panic_detected() {
1380        let rt = tokio::runtime::Runtime::new().unwrap();
1381        rt.block_on(async {
1382            crate::panic_hook::install_panic_hook();
1383            let test_id = crate::panic_hook::next_test_id();
1384            crate::panic_hook::set_current_test_id(test_id);
1385            crate::panic_hook::create_detached_collector(test_id);
1386
1387            let handle = crate::spawn::spawn(async {
1388                panic!("detached task panic");
1389            });
1390            let _ = handle.await;
1391
1392            let collector = crate::panic_hook::take_detached_collector(test_id).unwrap();
1393            let panics = collector.lock().unwrap();
1394
1395            assert_eq!(panics.len(), 1);
1396            assert!(
1397                panics[0]
1398                    .message
1399                    .as_ref()
1400                    .unwrap()
1401                    .contains("detached task panic"),
1402                "Expected panic message, got: {:?}",
1403                panics[0].message
1404            );
1405
1406            crate::panic_hook::clear_current_test_id();
1407        });
1408    }
1409
1410    #[test]
1411    fn failure_cause_variants() {
1412        // ReturnedMessage
1413        let cause = FailureCause::ReturnedMessage("simple message".to_string());
1414        assert_eq!(cause.render(), "simple message");
1415        assert!(cause.panic_message().is_none());
1416
1417        // ReturnedError (prefer display)
1418        let cause = FailureCause::ReturnedError {
1419            display: "display text".to_string(),
1420            debug: "debug text".to_string(),
1421            prefer_debug: false,
1422            error: Arc::new("display text".to_string()),
1423        };
1424        assert_eq!(cause.render(), "display text");
1425
1426        // ReturnedError (prefer debug, e.g. anyhow)
1427        let cause = FailureCause::ReturnedError {
1428            display: "display text".to_string(),
1429            debug: "debug text".to_string(),
1430            prefer_debug: true,
1431            error: Arc::new("debug text".to_string()),
1432        };
1433        assert_eq!(cause.render(), "debug text");
1434
1435        // HarnessError
1436        let cause = FailureCause::HarnessError("harness error".to_string());
1437        assert_eq!(cause.render(), "harness error");
1438
1439        // Panic with message
1440        let cause = FailureCause::Panic(PanicCause {
1441            message: Some("panic msg".to_string()),
1442            location: None,
1443            backtrace: None,
1444        });
1445        assert_eq!(cause.render(), "panic msg");
1446        assert_eq!(cause.panic_message(), Some("panic msg"));
1447    }
1448}
1449
1450#[cfg(test)]
1451mod filter_tests {
1452    use super::*;
1453
1454    fn make_test(name: &str, module_path: &str) -> RegisteredTest {
1455        RegisteredTest {
1456            name: name.to_string(),
1457            crate_name: "mycrate".to_string(),
1458            module_path: module_path.to_string(),
1459            run: TestFunction::Sync(Arc::new(|_| Box::new(()))),
1460            props: TestProperties::default(),
1461            dependencies: None,
1462        }
1463    }
1464
1465    fn make_tagged_test(name: &str, module_path: &str, tags: Vec<&str>) -> RegisteredTest {
1466        let mut test = make_test(name, module_path);
1467        test.props.tags = tags.into_iter().map(String::from).collect();
1468        test
1469    }
1470
1471    fn make_args(filters: Vec<&str>, skip: Vec<&str>, exact: bool) -> Arguments {
1472        Arguments {
1473            filter: filters.into_iter().map(String::from).collect(),
1474            skip: skip.into_iter().map(String::from).collect(),
1475            exact,
1476            ..Default::default()
1477        }
1478    }
1479
1480    fn filtered_names(args: &Arguments, tests: &[RegisteredTest]) -> Vec<String> {
1481        filter_registered_tests(args, tests)
1482            .into_iter()
1483            .map(|t| t.filterable_name())
1484            .collect()
1485    }
1486
1487    // --- filter_test unit tests ---
1488
1489    #[test]
1490    fn filter_test_substring_match() {
1491        let test = make_test("hello_world", "mod1");
1492        assert!(filter_test(&test, "hello", false));
1493        assert!(filter_test(&test, "world", false));
1494        assert!(filter_test(&test, "mod1::hello", false));
1495        assert!(!filter_test(&test, "nonexistent", false));
1496    }
1497
1498    #[test]
1499    fn filter_test_exact_match() {
1500        let test = make_test("hello_world", "mod1");
1501        assert!(filter_test(&test, "mod1::hello_world", true));
1502        assert!(!filter_test(&test, "hello_world", true));
1503        assert!(!filter_test(&test, "hello", true));
1504    }
1505
1506    #[test]
1507    fn filter_test_tag_match() {
1508        let test = make_tagged_test("t1", "mod1", vec!["fast", "unit"]);
1509        assert!(filter_test(&test, ":tag:fast", false));
1510        assert!(filter_test(&test, ":tag:unit", false));
1511        assert!(!filter_test(&test, ":tag:slow", false));
1512    }
1513
1514    #[test]
1515    fn filter_test_tag_empty_matches_untagged() {
1516        let untagged = make_test("t1", "mod1");
1517        let tagged = make_tagged_test("t2", "mod1", vec!["fast"]);
1518        assert!(filter_test(&untagged, ":tag:", false));
1519        assert!(!filter_test(&tagged, ":tag:", false));
1520    }
1521
1522    // --- filter_registered_tests: multiple include filters (OR semantics) ---
1523
1524    #[test]
1525    fn no_filters_includes_all() {
1526        let tests = vec![make_test("a", "m"), make_test("b", "m")];
1527        let args = make_args(vec![], vec![], false);
1528        assert_eq!(filtered_names(&args, &tests), vec!["m::a", "m::b"]);
1529    }
1530
1531    #[test]
1532    fn single_filter_substring() {
1533        let tests = vec![
1534            make_test("alpha", "m"),
1535            make_test("beta", "m"),
1536            make_test("alphabet", "m"),
1537        ];
1538        let args = make_args(vec!["alpha"], vec![], false);
1539        assert_eq!(
1540            filtered_names(&args, &tests),
1541            vec!["m::alpha", "m::alphabet"]
1542        );
1543    }
1544
1545    #[test]
1546    fn multiple_filters_or_semantics() {
1547        let tests = vec![
1548            make_test("alpha", "m"),
1549            make_test("beta", "m"),
1550            make_test("gamma", "m"),
1551        ];
1552        let args = make_args(vec!["alpha", "gamma"], vec![], false);
1553        assert_eq!(filtered_names(&args, &tests), vec!["m::alpha", "m::gamma"]);
1554    }
1555
1556    #[test]
1557    fn multiple_filters_exact() {
1558        let tests = vec![
1559            make_test("alpha", "m"),
1560            make_test("alphabet", "m"),
1561            make_test("beta", "m"),
1562        ];
1563        let args = make_args(vec!["m::alpha", "m::beta"], vec![], true);
1564        assert_eq!(filtered_names(&args, &tests), vec!["m::alpha", "m::beta"]);
1565    }
1566
1567    // --- skip behavior ---
1568
1569    #[test]
1570    fn skip_substring_match() {
1571        let tests = vec![
1572            make_test("fast_test", "m"),
1573            make_test("slow_test", "m"),
1574            make_test("slower_test", "m"),
1575        ];
1576        let args = make_args(vec![], vec!["slow"], false);
1577        assert_eq!(filtered_names(&args, &tests), vec!["m::fast_test"]);
1578    }
1579
1580    #[test]
1581    fn skip_exact_match() {
1582        let tests = vec![make_test("slow_test", "m"), make_test("slower_test", "m")];
1583        let args = make_args(vec![], vec!["m::slow_test"], true);
1584        assert_eq!(filtered_names(&args, &tests), vec!["m::slower_test"]);
1585    }
1586
1587    #[test]
1588    fn skip_with_tag() {
1589        let tests = vec![
1590            make_tagged_test("t1", "m", vec!["slow"]),
1591            make_tagged_test("t2", "m", vec!["fast"]),
1592            make_test("t3", "m"),
1593        ];
1594        let args = make_args(vec![], vec![":tag:slow"], false);
1595        assert_eq!(filtered_names(&args, &tests), vec!["m::t2", "m::t3"]);
1596    }
1597
1598    // --- combined include + skip ---
1599
1600    #[test]
1601    fn include_and_skip_combined() {
1602        let tests = vec![
1603            make_test("alpha_fast", "m"),
1604            make_test("alpha_slow", "m"),
1605            make_test("beta_fast", "m"),
1606        ];
1607        // Include anything with "alpha", but skip anything with "slow"
1608        let args = make_args(vec!["alpha"], vec!["slow"], false);
1609        assert_eq!(filtered_names(&args, &tests), vec!["m::alpha_fast"]);
1610    }
1611
1612    #[test]
1613    fn skip_wins_over_include() {
1614        let tests = vec![make_test("target", "m")];
1615        // Both include and skip match the same test — skip should win
1616        let args = make_args(vec!["target"], vec!["target"], false);
1617        assert_eq!(filtered_names(&args, &tests), Vec::<String>::new());
1618    }
1619
1620    // --- tag boolean expression syntax ---
1621
1622    #[test]
1623    fn filter_test_tag_or_expression() {
1624        // `:tag:a|b` matches tests tagged with `a` OR `b`
1625        let test_a = make_tagged_test("t1", "m", vec!["a"]);
1626        let test_b = make_tagged_test("t2", "m", vec!["b"]);
1627        let test_c = make_tagged_test("t3", "m", vec!["c"]);
1628        assert!(filter_test(&test_a, ":tag:a|b", false));
1629        assert!(filter_test(&test_b, ":tag:a|b", false));
1630        assert!(!filter_test(&test_c, ":tag:a|b", false));
1631    }
1632
1633    #[test]
1634    fn filter_test_tag_and_expression() {
1635        // `:tag:a&b` matches tests tagged with BOTH `a` AND `b`
1636        let test_ab = make_tagged_test("t1", "m", vec!["a", "b"]);
1637        let test_a = make_tagged_test("t2", "m", vec!["a"]);
1638        let test_b = make_tagged_test("t3", "m", vec!["b"]);
1639        assert!(filter_test(&test_ab, ":tag:a&b", false));
1640        assert!(!filter_test(&test_a, ":tag:a&b", false));
1641        assert!(!filter_test(&test_b, ":tag:a&b", false));
1642    }
1643
1644    #[test]
1645    fn filter_test_tag_mixed_and_or() {
1646        // `:tag:a|b&c` means `a OR (b AND c)` — `&` has higher precedence
1647        let test_a = make_tagged_test("t1", "m", vec!["a"]);
1648        let test_bc = make_tagged_test("t2", "m", vec!["b", "c"]);
1649        let test_b = make_tagged_test("t3", "m", vec!["b"]);
1650        let test_c = make_tagged_test("t4", "m", vec!["c"]);
1651        let test_none = make_test("t5", "m");
1652        assert!(filter_test(&test_a, ":tag:a|b&c", false));
1653        assert!(filter_test(&test_bc, ":tag:a|b&c", false));
1654        assert!(!filter_test(&test_b, ":tag:a|b&c", false));
1655        assert!(!filter_test(&test_c, ":tag:a|b&c", false));
1656        assert!(!filter_test(&test_none, ":tag:a|b&c", false));
1657    }
1658
1659    #[test]
1660    fn filter_test_tag_exact_flag_does_not_affect_tags() {
1661        // `--exact` should not change tag matching behavior
1662        let test = make_tagged_test("t1", "m", vec!["fast"]);
1663        assert!(filter_test(&test, ":tag:fast", true));
1664        assert!(!filter_test(&test, ":tag:slow", true));
1665    }
1666
1667    #[test]
1668    fn include_by_tag_or_expression() {
1669        let tests = vec![
1670            make_tagged_test("t1", "m", vec!["unit"]),
1671            make_tagged_test("t2", "m", vec!["integration"]),
1672            make_tagged_test("t3", "m", vec!["e2e"]),
1673        ];
1674        let args = make_args(vec![":tag:unit|integration"], vec![], false);
1675        assert_eq!(filtered_names(&args, &tests), vec!["m::t1", "m::t2"]);
1676    }
1677
1678    #[test]
1679    fn skip_by_tag_and_expression() {
1680        let tests = vec![
1681            make_tagged_test("t1", "m", vec!["slow", "network"]),
1682            make_tagged_test("t2", "m", vec!["slow"]),
1683            make_tagged_test("t3", "m", vec!["network"]),
1684            make_test("t4", "m"),
1685        ];
1686        // Skip only tests that are BOTH slow AND network
1687        let args = make_args(vec![], vec![":tag:slow&network"], false);
1688        assert_eq!(
1689            filtered_names(&args, &tests),
1690            vec!["m::t2", "m::t3", "m::t4"]
1691        );
1692    }
1693}