From 1673f9987edd7f7462e097cc2282e7d13fda5e47 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 11 Sep 2025 16:46:48 -0500 Subject: [PATCH 01/12] fix(harness)!: Reove Debug from TestContext --- crates/libtest2-harness/src/context.rs | 1 - crates/libtest2-mimic/src/lib.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/crates/libtest2-harness/src/context.rs b/crates/libtest2-harness/src/context.rs index c026460..6c9cb20 100644 --- a/crates/libtest2-harness/src/context.rs +++ b/crates/libtest2-harness/src/context.rs @@ -1,6 +1,5 @@ pub(crate) use crate::*; -#[derive(Debug)] pub struct TestContext { mode: RunMode, run_ignored: bool, diff --git a/crates/libtest2-mimic/src/lib.rs b/crates/libtest2-mimic/src/lib.rs index 704adb7..5cb7bfc 100644 --- a/crates/libtest2-mimic/src/lib.rs +++ b/crates/libtest2-mimic/src/lib.rs @@ -144,7 +144,6 @@ impl RunError { } } -#[derive(Debug)] pub struct TestContext<'t> { inner: &'t libtest2_harness::TestContext, } From c643812423c54a64dacd63ab1adbb6ca26c1c55b Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 11 Sep 2025 16:50:11 -0500 Subject: [PATCH 02/12] fix(mimic)!: Experimenting with Context names --- crates/libtest2-mimic/examples/mimic-simple.rs | 12 ++++++------ crates/libtest2-mimic/src/lib.rs | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/libtest2-mimic/examples/mimic-simple.rs b/crates/libtest2-mimic/examples/mimic-simple.rs index bdfc68c..e3c9da2 100644 --- a/crates/libtest2-mimic/examples/mimic-simple.rs +++ b/crates/libtest2-mimic/examples/mimic-simple.rs @@ -1,6 +1,6 @@ +use libtest2_mimic::RunContext; use libtest2_mimic::RunError; use libtest2_mimic::RunResult; -use libtest2_mimic::TestContext; use libtest2_mimic::Trial; fn main() { @@ -17,21 +17,21 @@ fn main() { // Tests -fn check_toph(_context: TestContext<'_>) -> RunResult { +fn check_toph(_context: RunContext<'_>) -> RunResult { Ok(()) } -fn check_katara(_context: TestContext<'_>) -> RunResult { +fn check_katara(_context: RunContext<'_>) -> RunResult { Ok(()) } -fn check_sokka(_context: TestContext<'_>) -> RunResult { +fn check_sokka(_context: RunContext<'_>) -> RunResult { Err(RunError::fail("Sokka tripped and fell :(")) } -fn long_computation(context: TestContext<'_>) -> RunResult { +fn long_computation(context: RunContext<'_>) -> RunResult { context.ignore_for("slow")?; std::thread::sleep(std::time::Duration::from_secs(1)); Ok(()) } -fn compile_fail_dummy(_context: TestContext<'_>) -> RunResult { +fn compile_fail_dummy(_context: RunContext<'_>) -> RunResult { Ok(()) } diff --git a/crates/libtest2-mimic/src/lib.rs b/crates/libtest2-mimic/src/lib.rs index 5cb7bfc..d01bf0f 100644 --- a/crates/libtest2-mimic/src/lib.rs +++ b/crates/libtest2-mimic/src/lib.rs @@ -82,13 +82,13 @@ impl Harness { pub struct Trial { name: String, #[allow(clippy::type_complexity)] - runner: Box) -> Result<(), RunError> + Send + Sync>, + runner: Box) -> Result<(), RunError> + Send + Sync>, } impl Trial { pub fn test( name: impl Into, - runner: impl Fn(TestContext<'_>) -> Result<(), RunError> + Send + Sync + 'static, + runner: impl Fn(RunContext<'_>) -> Result<(), RunError> + Send + Sync + 'static, ) -> Self { Self { name: name.into(), @@ -119,7 +119,7 @@ impl libtest2_harness::Case for TrialCase { &self, context: &libtest2_harness::TestContext, ) -> Result<(), libtest2_harness::RunError> { - (self.inner.runner)(TestContext { inner: context }).map_err(|e| e.inner) + (self.inner.runner)(RunContext { inner: context }).map_err(|e| e.inner) } } @@ -144,11 +144,11 @@ impl RunError { } } -pub struct TestContext<'t> { +pub struct RunContext<'t> { inner: &'t libtest2_harness::TestContext, } -impl<'t> TestContext<'t> { +impl<'t> RunContext<'t> { pub fn ignore(&self) -> Result<(), RunError> { self.inner.ignore().map_err(|e| RunError { inner: e }) } From 12fcf99cbc1bbcba99780c2549172838fbbd1fa2 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 11 Sep 2025 16:58:11 -0500 Subject: [PATCH 03/12] refactor(harness): Clarify a variable --- crates/libtest2-harness/src/harness.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/libtest2-harness/src/harness.rs b/crates/libtest2-harness/src/harness.rs index d1f3897..22727dc 100644 --- a/crates/libtest2-harness/src/harness.rs +++ b/crates/libtest2-harness/src/harness.rs @@ -383,11 +383,11 @@ fn run( let sync_success = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(success)); let mut running_tests: TestMap = Default::default(); - let mut pending = 0; + let mut running = 0; let (tx, rx) = std::sync::mpsc::channel::(); let mut remaining = std::collections::VecDeque::from(concurrent_cases); - while pending > 0 || !remaining.is_empty() { - while pending < threads && !remaining.is_empty() { + while running > 0 || !remaining.is_empty() { + while running < threads && !remaining.is_empty() { let case = remaining.pop_front().unwrap(); let name = case.name().to_owned(); @@ -412,7 +412,7 @@ fn run( match join_handle { Ok(join_handle) => { running_tests.insert(name.clone(), RunningTest { join_handle }); - pending += 1; + running += 1; } Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { // `ErrorKind::WouldBlock` means hitting the thread limit on some @@ -439,7 +439,7 @@ fn run( if let notify::Event::CaseComplete(event) = &event { let running_test = running_tests.remove(&event.name).unwrap(); running_test.join(start, event, notifier)?; - pending -= 1; + running -= 1; } notifier.notify(event)?; success &= sync_success.load(std::sync::atomic::Ordering::SeqCst); From b248d8d0b581cf945705cefe7fdc8af62538c20a Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 11 Sep 2025 16:59:30 -0500 Subject: [PATCH 04/12] refactor(harness): Remove redundant bookkeeping --- crates/libtest2-harness/src/harness.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/libtest2-harness/src/harness.rs b/crates/libtest2-harness/src/harness.rs index 22727dc..f70c87f 100644 --- a/crates/libtest2-harness/src/harness.rs +++ b/crates/libtest2-harness/src/harness.rs @@ -383,11 +383,10 @@ fn run( let sync_success = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(success)); let mut running_tests: TestMap = Default::default(); - let mut running = 0; let (tx, rx) = std::sync::mpsc::channel::(); let mut remaining = std::collections::VecDeque::from(concurrent_cases); - while running > 0 || !remaining.is_empty() { - while running < threads && !remaining.is_empty() { + while !running_tests.is_empty() || !remaining.is_empty() { + while running_tests.len() < threads && !remaining.is_empty() { let case = remaining.pop_front().unwrap(); let name = case.name().to_owned(); @@ -412,7 +411,6 @@ fn run( match join_handle { Ok(join_handle) => { running_tests.insert(name.clone(), RunningTest { join_handle }); - running += 1; } Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { // `ErrorKind::WouldBlock` means hitting the thread limit on some @@ -439,7 +437,6 @@ fn run( if let notify::Event::CaseComplete(event) = &event { let running_test = running_tests.remove(&event.name).unwrap(); running_test.join(start, event, notifier)?; - running -= 1; } notifier.notify(event)?; success &= sync_success.load(std::sync::atomic::Ordering::SeqCst); From b30e3a0bfd5fcd1aa38d0f93e5e941efcffc4a02 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 11 Sep 2025 16:59:58 -0500 Subject: [PATCH 05/12] refactor(harness): Simplify a variable --- crates/libtest2-harness/src/harness.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/libtest2-harness/src/harness.rs b/crates/libtest2-harness/src/harness.rs index f70c87f..b6ba903 100644 --- a/crates/libtest2-harness/src/harness.rs +++ b/crates/libtest2-harness/src/harness.rs @@ -382,11 +382,11 @@ fn run( >; let sync_success = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(success)); - let mut running_tests: TestMap = Default::default(); + let mut running: TestMap = Default::default(); let (tx, rx) = std::sync::mpsc::channel::(); let mut remaining = std::collections::VecDeque::from(concurrent_cases); - while !running_tests.is_empty() || !remaining.is_empty() { - while running_tests.len() < threads && !remaining.is_empty() { + while !running.is_empty() || !remaining.is_empty() { + while running.len() < threads && !remaining.is_empty() { let case = remaining.pop_front().unwrap(); let name = case.name().to_owned(); @@ -410,7 +410,7 @@ fn run( }); match join_handle { Ok(join_handle) => { - running_tests.insert(name.clone(), RunningTest { join_handle }); + running.insert(name.clone(), RunningTest { join_handle }); } Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { // `ErrorKind::WouldBlock` means hitting the thread limit on some @@ -435,7 +435,7 @@ fn run( let event = rx.recv().unwrap(); if let notify::Event::CaseComplete(event) = &event { - let running_test = running_tests.remove(&event.name).unwrap(); + let running_test = running.remove(&event.name).unwrap(); running_test.join(start, event, notifier)?; } notifier.notify(event)?; From 59e23df3b77685de59cabf20e8c81b6db134867b Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 12 Sep 2025 11:26:22 -0500 Subject: [PATCH 06/12] refactor(harness): Switch to direct initialization --- crates/libtest2-harness/src/context.rs | 21 ++------------------- crates/libtest2-harness/src/harness.rs | 4 +--- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/crates/libtest2-harness/src/context.rs b/crates/libtest2-harness/src/context.rs index 6c9cb20..7a299f8 100644 --- a/crates/libtest2-harness/src/context.rs +++ b/crates/libtest2-harness/src/context.rs @@ -1,8 +1,8 @@ pub(crate) use crate::*; pub struct TestContext { - mode: RunMode, - run_ignored: bool, + pub(crate) mode: RunMode, + pub(crate) run_ignored: bool, } impl TestContext { @@ -26,20 +26,3 @@ impl TestContext { self.mode } } - -impl TestContext { - pub(crate) fn new() -> Self { - Self { - mode: Default::default(), - run_ignored: false, - } - } - - pub(crate) fn set_mode(&mut self, mode: RunMode) { - self.mode = mode; - } - - pub(crate) fn set_run_ignored(&mut self, yes: bool) { - self.run_ignored = yes; - } -} diff --git a/crates/libtest2-harness/src/harness.rs b/crates/libtest2-harness/src/harness.rs index b6ba903..c9f43c4 100644 --- a/crates/libtest2-harness/src/harness.rs +++ b/crates/libtest2-harness/src/harness.rs @@ -316,7 +316,6 @@ fn run( let threads = opts.test_threads.map(|t| t.get()).unwrap_or(1); - let mut context = TestContext::new(); let run_ignored = match opts.run_ignored { libtest_lexarg::RunIgnored::Yes | libtest_lexarg::RunIgnored::Only => true, libtest_lexarg::RunIgnored::No => false, @@ -331,8 +330,7 @@ fn run( (false, true) => RunMode::Bench, (false, false) => unreachable!("libtest-lexarg` should always ensure at least one is set"), }; - context.set_mode(mode); - context.set_run_ignored(run_ignored); + let context = TestContext { mode, run_ignored }; let context = std::sync::Arc::new(context); let mut success = true; From 42fbb8826b9ef7503ed278c1fd9a80fff475f2db Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 12 Sep 2025 11:29:14 -0500 Subject: [PATCH 07/12] feat(harness): Expose elapsed_s to Cases This is prep for allowing Cases to notify --- crates/libtest2-harness/src/context.rs | 5 +++++ crates/libtest2-harness/src/harness.rs | 31 ++++++++++++-------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/crates/libtest2-harness/src/context.rs b/crates/libtest2-harness/src/context.rs index 7a299f8..7c732cf 100644 --- a/crates/libtest2-harness/src/context.rs +++ b/crates/libtest2-harness/src/context.rs @@ -1,6 +1,7 @@ pub(crate) use crate::*; pub struct TestContext { + pub(crate) start: std::time::Instant, pub(crate) mode: RunMode, pub(crate) run_ignored: bool, } @@ -25,4 +26,8 @@ impl TestContext { pub fn current_mode(&self) -> RunMode { self.mode } + + pub fn elapased_s(&self) -> notify::Elapsed { + notify::Elapsed(self.start.elapsed()) + } } diff --git a/crates/libtest2-harness/src/harness.rs b/crates/libtest2-harness/src/harness.rs index c9f43c4..b4a6518 100644 --- a/crates/libtest2-harness/src/harness.rs +++ b/crates/libtest2-harness/src/harness.rs @@ -330,7 +330,11 @@ fn run( (false, true) => RunMode::Bench, (false, false) => unreachable!("libtest-lexarg` should always ensure at least one is set"), }; - let context = TestContext { mode, run_ignored }; + let context = TestContext { + start: *start, + mode, + run_ignored, + }; let context = std::sync::Arc::new(context); let mut success = true; @@ -389,7 +393,6 @@ fn run( let name = case.name().to_owned(); let cfg = std::thread::Builder::new().name(name.clone()); - let start = *start; let tx = tx.clone(); let case = std::sync::Arc::new(case); let case_fallback = case.clone(); @@ -399,9 +402,8 @@ fn run( let sync_success_fallback = sync_success.clone(); let join_handle = cfg.spawn(move || { let mut notifier = SenderNotifier { tx: tx.clone() }; - let case_success = - run_case(&start, case.as_ref().as_ref(), &context, &mut notifier) - .expect("`SenderNotifier` is infallible"); + let case_success = run_case(case.as_ref().as_ref(), &context, &mut notifier) + .expect("`SenderNotifier` is infallible"); if !case_success { sync_success.store(case_success, std::sync::atomic::Ordering::Relaxed); } @@ -413,13 +415,9 @@ fn run( Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { // `ErrorKind::WouldBlock` means hitting the thread limit on some // platforms, so run the test synchronously here instead. - let case_success = run_case( - &start, - case_fallback.as_ref().as_ref(), - &context_fallback, - notifier, - ) - .expect("`SenderNotifier` is infallible"); + let case_success = + run_case(case_fallback.as_ref().as_ref(), &context_fallback, notifier) + .expect("`SenderNotifier` is infallible"); if !case_success { sync_success_fallback .store(case_success, std::sync::atomic::Ordering::Relaxed); @@ -447,7 +445,7 @@ fn run( if !exclusive_cases.is_empty() { notifier.threaded(false); for case in exclusive_cases { - success &= run_case(start, case.as_ref(), &context, notifier)?; + success &= run_case(case.as_ref(), &context, notifier)?; if !success && opts.fail_fast { break; } @@ -465,7 +463,6 @@ fn run( } fn run_case( - start: &std::time::Instant, case: &dyn Case, context: &TestContext, notifier: &mut dyn notify::Notifier, @@ -473,7 +470,7 @@ fn run_case( notifier.notify( notify::event::CaseStart { name: case.name().to_owned(), - elapsed_s: Some(notify::Elapsed(start.elapsed())), + elapsed_s: Some(context.elapased_s()), } .into(), )?; @@ -507,7 +504,7 @@ fn run_case( name: case.name().to_owned(), kind, message, - elapsed_s: Some(notify::Elapsed(start.elapsed())), + elapsed_s: Some(context.elapased_s()), } .into(), )?; @@ -516,7 +513,7 @@ fn run_case( notifier.notify( notify::event::CaseComplete { name: case.name().to_owned(), - elapsed_s: Some(notify::Elapsed(start.elapsed())), + elapsed_s: Some(context.elapased_s()), } .into(), )?; From 0cd4963047d8f523e5323ca2bf94eaaf5bb2c031 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 12 Sep 2025 11:33:21 -0500 Subject: [PATCH 08/12] refactor(harness): Clarify thread's capture --- crates/libtest2-harness/src/harness.rs | 33 +++++++++++++------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/crates/libtest2-harness/src/harness.rs b/crates/libtest2-harness/src/harness.rs index b4a6518..c1635f0 100644 --- a/crates/libtest2-harness/src/harness.rs +++ b/crates/libtest2-harness/src/harness.rs @@ -390,22 +390,25 @@ fn run( while !running.is_empty() || !remaining.is_empty() { while running.len() < threads && !remaining.is_empty() { let case = remaining.pop_front().unwrap(); + let case = std::sync::Arc::new(case); let name = case.name().to_owned(); let cfg = std::thread::Builder::new().name(name.clone()); - let tx = tx.clone(); - let case = std::sync::Arc::new(case); - let case_fallback = case.clone(); - let context = context.clone(); - let context_fallback = context.clone(); - let sync_success = sync_success.clone(); - let sync_success_fallback = sync_success.clone(); + let thread_tx = tx.clone(); + let thread_case = case.clone(); + let thread_context = context.clone(); + let thread_sync_success = sync_success.clone(); let join_handle = cfg.spawn(move || { - let mut notifier = SenderNotifier { tx: tx.clone() }; - let case_success = run_case(case.as_ref().as_ref(), &context, &mut notifier) - .expect("`SenderNotifier` is infallible"); + let mut notifier = SenderNotifier { tx: thread_tx }; + let case_success = run_case( + thread_case.as_ref().as_ref(), + &thread_context, + &mut notifier, + ) + .expect("`SenderNotifier` is infallible"); if !case_success { - sync_success.store(case_success, std::sync::atomic::Ordering::Relaxed); + thread_sync_success + .store(case_success, std::sync::atomic::Ordering::Relaxed); } }); match join_handle { @@ -415,12 +418,10 @@ fn run( Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { // `ErrorKind::WouldBlock` means hitting the thread limit on some // platforms, so run the test synchronously here instead. - let case_success = - run_case(case_fallback.as_ref().as_ref(), &context_fallback, notifier) - .expect("`SenderNotifier` is infallible"); + let case_success = run_case(case.as_ref().as_ref(), &context, notifier) + .expect("`SenderNotifier` is infallible"); if !case_success { - sync_success_fallback - .store(case_success, std::sync::atomic::Ordering::Relaxed); + sync_success.store(case_success, std::sync::atomic::Ordering::Relaxed); } } Err(e) => { From 5c07681929fcb10ec1490654a5de7dd06d303100 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 11 Sep 2025 16:56:54 -0500 Subject: [PATCH 09/12] feat(harness): Allow tests access to send events This is the final step for recoverable errors --- crates/libtest2-harness/src/context.rs | 9 +++ crates/libtest2-harness/src/harness.rs | 106 +++++++------------------ 2 files changed, 36 insertions(+), 79 deletions(-) diff --git a/crates/libtest2-harness/src/context.rs b/crates/libtest2-harness/src/context.rs index 7c732cf..e5a8317 100644 --- a/crates/libtest2-harness/src/context.rs +++ b/crates/libtest2-harness/src/context.rs @@ -4,6 +4,7 @@ pub struct TestContext { pub(crate) start: std::time::Instant, pub(crate) mode: RunMode, pub(crate) run_ignored: bool, + pub(crate) notifier: std::sync::Mutex>, } impl TestContext { @@ -27,7 +28,15 @@ impl TestContext { self.mode } + pub fn notify(&self, event: notify::Event) -> std::io::Result<()> { + self.notifier().notify(event) + } + pub fn elapased_s(&self) -> notify::Elapsed { notify::Elapsed(self.start.elapsed()) } + + pub(crate) fn notifier(&self) -> std::sync::MutexGuard<'_, Box> { + self.notifier.lock().unwrap() + } } diff --git a/crates/libtest2-harness/src/harness.rs b/crates/libtest2-harness/src/harness.rs index c1635f0..7da6332 100644 --- a/crates/libtest2-harness/src/harness.rs +++ b/crates/libtest2-harness/src/harness.rs @@ -83,7 +83,7 @@ impl Harness { pub struct StateParsed { start: std::time::Instant, opts: libtest_lexarg::TestOpts, - notifier: Box, + notifier: Box, } impl HarnessState for StateParsed {} impl sealed::_HarnessState_is_Sealed for StateParsed {} @@ -144,14 +144,14 @@ impl Harness { pub struct StateDiscovered { start: std::time::Instant, opts: libtest_lexarg::TestOpts, - notifier: Box, + notifier: Box, cases: Vec>, } impl HarnessState for StateDiscovered {} impl sealed::_HarnessState_is_Sealed for StateDiscovered {} impl Harness { - pub fn run(mut self) -> std::io::Result { + pub fn run(self) -> std::io::Result { if self.state.opts.list { Ok(true) } else { @@ -159,7 +159,7 @@ impl Harness { &self.state.start, &self.state.opts, self.state.cases, - self.state.notifier.as_mut(), + self.state.notifier, ) } } @@ -252,7 +252,7 @@ fn parse<'p>(parser: &mut cli::Parser<'p>) -> Result Box { +fn notifier(opts: &libtest_lexarg::TestOpts) -> Box { #[cfg(feature = "color")] let stdout = anstream::stdout(); #[cfg(not(feature = "color"))] @@ -292,7 +292,7 @@ fn run( start: &std::time::Instant, opts: &libtest_lexarg::TestOpts, cases: Vec>, - notifier: &mut dyn notify::Notifier, + mut notifier: Box, ) -> std::io::Result { notifier.notify( notify::event::RunStart { @@ -334,6 +334,7 @@ fn run( start: *start, mode, run_ignored, + notifier: std::sync::Mutex::new(notifier), }; let context = std::sync::Arc::new(context); @@ -347,45 +348,18 @@ fn run( .partition::, _>(|c| c.exclusive(&context)) }; if !concurrent_cases.is_empty() { - notifier.threaded(true); - struct RunningTest { - join_handle: std::thread::JoinHandle<()>, - } - - impl RunningTest { - fn join( - self, - start: &std::time::Instant, - event: ¬ify::event::CaseComplete, - notifier: &mut dyn notify::Notifier, - ) -> std::io::Result<()> { - if self.join_handle.join().is_err() { - let kind = notify::MessageKind::Error; - let message = Some("panicked after reporting success".to_owned()); - notifier.notify( - notify::event::CaseMessage { - name: event.name.clone(), - kind, - message, - elapsed_s: Some(notify::Elapsed(start.elapsed())), - } - .into(), - )?; - } - Ok(()) - } - } + context.notifier().threaded(true); // Use a deterministic hasher type TestMap = std::collections::HashMap< String, - RunningTest, + std::thread::JoinHandle>, std::hash::BuildHasherDefault, >; let sync_success = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(success)); let mut running: TestMap = Default::default(); - let (tx, rx) = std::sync::mpsc::channel::(); + let (tx, rx) = std::sync::mpsc::channel::(); let mut remaining = std::collections::VecDeque::from(concurrent_cases); while !running.is_empty() || !remaining.is_empty() { while running.len() < threads && !remaining.is_empty() { @@ -399,27 +373,21 @@ fn run( let thread_context = context.clone(); let thread_sync_success = sync_success.clone(); let join_handle = cfg.spawn(move || { - let mut notifier = SenderNotifier { tx: thread_tx }; - let case_success = run_case( - thread_case.as_ref().as_ref(), - &thread_context, - &mut notifier, - ) - .expect("`SenderNotifier` is infallible"); - if !case_success { - thread_sync_success - .store(case_success, std::sync::atomic::Ordering::Relaxed); + let status = run_case(thread_case.as_ref().as_ref(), &thread_context); + if !matches!(status, Ok(true)) { + thread_sync_success.store(false, std::sync::atomic::Ordering::Relaxed); } + let _ = thread_tx.send(thread_case.name().to_owned()); + status }); match join_handle { Ok(join_handle) => { - running.insert(name.clone(), RunningTest { join_handle }); + running.insert(name.clone(), join_handle); } Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { // `ErrorKind::WouldBlock` means hitting the thread limit on some // platforms, so run the test synchronously here instead. - let case_success = run_case(case.as_ref().as_ref(), &context, notifier) - .expect("`SenderNotifier` is infallible"); + let case_success = run_case(case.as_ref().as_ref(), &context)?; if !case_success { sync_success.store(case_success, std::sync::atomic::Ordering::Relaxed); } @@ -430,12 +398,9 @@ fn run( } } - let event = rx.recv().unwrap(); - if let notify::Event::CaseComplete(event) = &event { - let running_test = running.remove(&event.name).unwrap(); - running_test.join(start, event, notifier)?; - } - notifier.notify(event)?; + let test_name = rx.recv().unwrap(); + let running_test = running.remove(&test_name).unwrap(); + let _ = running_test.join(); success &= sync_success.load(std::sync::atomic::Ordering::SeqCst); if !success && opts.fail_fast { break; @@ -444,16 +409,16 @@ fn run( } if !exclusive_cases.is_empty() { - notifier.threaded(false); + context.notifier().threaded(false); for case in exclusive_cases { - success &= run_case(case.as_ref(), &context, notifier)?; + success &= run_case(case.as_ref(), &context)?; if !success && opts.fail_fast { break; } } } - notifier.notify( + context.notifier().notify( notify::event::RunComplete { elapsed_s: Some(notify::Elapsed(start.elapsed())), } @@ -463,12 +428,8 @@ fn run( Ok(success) } -fn run_case( - case: &dyn Case, - context: &TestContext, - notifier: &mut dyn notify::Notifier, -) -> std::io::Result { - notifier.notify( +fn run_case(case: &dyn Case, context: &TestContext) -> std::io::Result { + context.notifier().notify( notify::event::CaseStart { name: case.name().to_owned(), elapsed_s: Some(context.elapased_s()), @@ -500,7 +461,7 @@ fn run_case( let kind = err.status(); case_status = Some(kind); let message = err.cause().map(|c| c.to_string()); - notifier.notify( + context.notifier().notify( notify::event::CaseMessage { name: case.name().to_owned(), kind, @@ -511,7 +472,7 @@ fn run_case( )?; } - notifier.notify( + context.notifier().notify( notify::event::CaseComplete { name: case.name().to_owned(), elapsed_s: Some(context.elapased_s()), @@ -530,16 +491,3 @@ fn __rust_begin_short_backtrace T>(f: F) -> T { // prevent this frame from being tail-call optimised away std::hint::black_box(result) } - -#[derive(Clone, Debug)] -struct SenderNotifier { - tx: std::sync::mpsc::Sender, -} - -impl notify::Notifier for SenderNotifier { - fn notify(&mut self, event: notify::Event) -> std::io::Result<()> { - // If the sender doesn't care, neither do we - let _ = self.tx.send(event); - Ok(()) - } -} From 74aa933fc0513c592ecb5407bb7d69ab0a20944a Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 12 Sep 2025 15:21:03 -0500 Subject: [PATCH 10/12] refactor(harness): Make boxed Notifier an Arc --- crates/libtest2-harness/src/context.rs | 6 ++--- crates/libtest2-harness/src/harness.rs | 20 ++++++++-------- crates/libtest2-harness/src/notify/mod.rs | 28 +++++++++++++++++++++++ 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/crates/libtest2-harness/src/context.rs b/crates/libtest2-harness/src/context.rs index e5a8317..00f702e 100644 --- a/crates/libtest2-harness/src/context.rs +++ b/crates/libtest2-harness/src/context.rs @@ -4,7 +4,7 @@ pub struct TestContext { pub(crate) start: std::time::Instant, pub(crate) mode: RunMode, pub(crate) run_ignored: bool, - pub(crate) notifier: std::sync::Mutex>, + pub(crate) notifier: notify::ArcNotifier, } impl TestContext { @@ -36,7 +36,7 @@ impl TestContext { notify::Elapsed(self.start.elapsed()) } - pub(crate) fn notifier(&self) -> std::sync::MutexGuard<'_, Box> { - self.notifier.lock().unwrap() + pub(crate) fn notifier(&self) -> ¬ify::ArcNotifier { + &self.notifier } } diff --git a/crates/libtest2-harness/src/harness.rs b/crates/libtest2-harness/src/harness.rs index 7da6332..5eb7fee 100644 --- a/crates/libtest2-harness/src/harness.rs +++ b/crates/libtest2-harness/src/harness.rs @@ -83,14 +83,14 @@ impl Harness { pub struct StateParsed { start: std::time::Instant, opts: libtest_lexarg::TestOpts, - notifier: Box, + notifier: notify::ArcNotifier, } impl HarnessState for StateParsed {} impl sealed::_HarnessState_is_Sealed for StateParsed {} impl Harness { pub fn discover( - mut self, + self, cases: impl IntoIterator, ) -> std::io::Result> { self.state.notifier.notify( @@ -144,7 +144,7 @@ impl Harness { pub struct StateDiscovered { start: std::time::Instant, opts: libtest_lexarg::TestOpts, - notifier: Box, + notifier: notify::ArcNotifier, cases: Vec>, } impl HarnessState for StateDiscovered {} @@ -252,16 +252,16 @@ fn parse<'p>(parser: &mut cli::Parser<'p>) -> Result Box { +fn notifier(opts: &libtest_lexarg::TestOpts) -> notify::ArcNotifier { #[cfg(feature = "color")] let stdout = anstream::stdout(); #[cfg(not(feature = "color"))] let stdout = std::io::stdout(); match opts.format { - OutputFormat::Json => Box::new(notify::JsonNotifier::new(stdout)), - _ if opts.list => Box::new(notify::TerseListNotifier::new(stdout)), - OutputFormat::Pretty => Box::new(notify::PrettyRunNotifier::new(stdout)), - OutputFormat::Terse => Box::new(notify::TerseRunNotifier::new(stdout)), + OutputFormat::Json => notify::ArcNotifier::new(notify::JsonNotifier::new(stdout)), + _ if opts.list => notify::ArcNotifier::new(notify::TerseListNotifier::new(stdout)), + OutputFormat::Pretty => notify::ArcNotifier::new(notify::PrettyRunNotifier::new(stdout)), + OutputFormat::Terse => notify::ArcNotifier::new(notify::TerseRunNotifier::new(stdout)), } } @@ -292,7 +292,7 @@ fn run( start: &std::time::Instant, opts: &libtest_lexarg::TestOpts, cases: Vec>, - mut notifier: Box, + notifier: notify::ArcNotifier, ) -> std::io::Result { notifier.notify( notify::event::RunStart { @@ -334,7 +334,7 @@ fn run( start: *start, mode, run_ignored, - notifier: std::sync::Mutex::new(notifier), + notifier, }; let context = std::sync::Arc::new(context); diff --git a/crates/libtest2-harness/src/notify/mod.rs b/crates/libtest2-harness/src/notify/mod.rs index 23c4a91..c0d6d76 100644 --- a/crates/libtest2-harness/src/notify/mod.rs +++ b/crates/libtest2-harness/src/notify/mod.rs @@ -22,6 +22,34 @@ pub(crate) trait Notifier { fn notify(&mut self, event: Event) -> std::io::Result<()>; } +pub(crate) struct ArcNotifier { + inner: std::sync::Arc>, +} + +impl ArcNotifier { + pub(crate) fn new(inner: impl Notifier + Send + 'static) -> Self { + Self { + inner: std::sync::Arc::new(std::sync::Mutex::new(inner)), + } + } + + pub(crate) fn threaded(&self, yes: bool) { + let mut notifier = match self.inner.lock() { + Ok(notifier) => notifier, + Err(poison) => poison.into_inner(), + }; + notifier.threaded(yes); + } + + pub(crate) fn notify(&self, event: Event) -> std::io::Result<()> { + let mut notifier = match self.inner.lock() { + Ok(notifier) => notifier, + Err(poison) => poison.into_inner(), + }; + notifier.notify(event) + } +} + pub(crate) use libtest_json::*; pub use libtest_json::RunMode; From 942f5e0ecace275e249554ace9fc39036b5eb765 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 12 Sep 2025 15:31:27 -0500 Subject: [PATCH 11/12] refactor(harness): Make TestContext not Arced --- crates/libtest2-harness/src/context.rs | 9 +++++++++ crates/libtest2-harness/src/harness.rs | 1 - crates/libtest2-harness/src/notify/mod.rs | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/libtest2-harness/src/context.rs b/crates/libtest2-harness/src/context.rs index 00f702e..7e15d1f 100644 --- a/crates/libtest2-harness/src/context.rs +++ b/crates/libtest2-harness/src/context.rs @@ -39,4 +39,13 @@ impl TestContext { pub(crate) fn notifier(&self) -> ¬ify::ArcNotifier { &self.notifier } + + pub(crate) fn clone(&self) -> Self { + Self { + start: self.start, + mode: self.mode, + run_ignored: self.run_ignored, + notifier: self.notifier.clone(), + } + } } diff --git a/crates/libtest2-harness/src/harness.rs b/crates/libtest2-harness/src/harness.rs index 5eb7fee..bea89ca 100644 --- a/crates/libtest2-harness/src/harness.rs +++ b/crates/libtest2-harness/src/harness.rs @@ -336,7 +336,6 @@ fn run( run_ignored, notifier, }; - let context = std::sync::Arc::new(context); let mut success = true; diff --git a/crates/libtest2-harness/src/notify/mod.rs b/crates/libtest2-harness/src/notify/mod.rs index c0d6d76..fdf8233 100644 --- a/crates/libtest2-harness/src/notify/mod.rs +++ b/crates/libtest2-harness/src/notify/mod.rs @@ -22,6 +22,7 @@ pub(crate) trait Notifier { fn notify(&mut self, event: Event) -> std::io::Result<()>; } +#[derive(Clone)] pub(crate) struct ArcNotifier { inner: std::sync::Arc>, } From 1b875d3b0a7aa44606a4ec3b8bf22f41d0ad8eb9 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 12 Sep 2025 15:36:35 -0500 Subject: [PATCH 12/12] feat(harness): Report the test's name This is needed for actually sending custom events --- crates/libtest2-harness/src/context.rs | 6 ++++++ crates/libtest2-harness/src/harness.rs | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/libtest2-harness/src/context.rs b/crates/libtest2-harness/src/context.rs index 7e15d1f..b90a921 100644 --- a/crates/libtest2-harness/src/context.rs +++ b/crates/libtest2-harness/src/context.rs @@ -5,6 +5,7 @@ pub struct TestContext { pub(crate) mode: RunMode, pub(crate) run_ignored: bool, pub(crate) notifier: notify::ArcNotifier, + pub(crate) test_name: String, } impl TestContext { @@ -36,6 +37,10 @@ impl TestContext { notify::Elapsed(self.start.elapsed()) } + pub fn test_name(&self) -> &str { + &self.test_name + } + pub(crate) fn notifier(&self) -> ¬ify::ArcNotifier { &self.notifier } @@ -46,6 +51,7 @@ impl TestContext { mode: self.mode, run_ignored: self.run_ignored, notifier: self.notifier.clone(), + test_name: self.test_name.clone(), } } } diff --git a/crates/libtest2-harness/src/harness.rs b/crates/libtest2-harness/src/harness.rs index bea89ca..dd443f2 100644 --- a/crates/libtest2-harness/src/harness.rs +++ b/crates/libtest2-harness/src/harness.rs @@ -335,6 +335,7 @@ fn run( mode, run_ignored, notifier, + test_name: String::new(), }; let mut success = true; @@ -369,7 +370,8 @@ fn run( let cfg = std::thread::Builder::new().name(name.clone()); let thread_tx = tx.clone(); let thread_case = case.clone(); - let thread_context = context.clone(); + let mut thread_context = context.clone(); + thread_context.test_name = name.clone(); let thread_sync_success = sync_success.clone(); let join_handle = cfg.spawn(move || { let status = run_case(thread_case.as_ref().as_ref(), &thread_context);