diff --git a/src/etc/test-float-parse/src/gen/exhaustive.rs b/src/etc/test-float-parse/src/gen/exhaustive.rs index 5d4b6df8e59b9..01458fb0b6084 100644 --- a/src/etc/test-float-parse/src/gen/exhaustive.rs +++ b/src/etc/test-float-parse/src/gen/exhaustive.rs @@ -13,13 +13,12 @@ impl Generator for Exhaustive where RangeInclusive: Iterator, { - const NAME: &'static str = "exhaustive"; const SHORT_NAME: &'static str = "exhaustive"; type WriteCtx = F; fn total_tests() -> u64 { - F::Int::MAX.try_into().unwrap_or(u64::MAX) + 1u64.checked_shl(F::Int::BITS).expect("More than u64::MAX tests") } fn new() -> Self { diff --git a/src/etc/test-float-parse/src/gen/fuzz.rs b/src/etc/test-float-parse/src/gen/fuzz.rs index 0c63e8aae26ea..7fc999d167113 100644 --- a/src/etc/test-float-parse/src/gen/fuzz.rs +++ b/src/etc/test-float-parse/src/gen/fuzz.rs @@ -49,7 +49,6 @@ impl Generator for Fuzz where Standard: Distribution<::Int>, { - const NAME: &'static str = "fuzz"; const SHORT_NAME: &'static str = "fuzz"; type WriteCtx = F; diff --git a/src/etc/test-float-parse/src/gen/sparse.rs b/src/etc/test-float-parse/src/gen/sparse.rs index 389b71056a3e5..72b65d4ce7f10 100644 --- a/src/etc/test-float-parse/src/gen/sparse.rs +++ b/src/etc/test-float-parse/src/gen/sparse.rs @@ -35,7 +35,6 @@ impl Generator for FewOnesInt where >::Error: std::fmt::Debug, { - const NAME: &'static str = "few ones int"; const SHORT_NAME: &'static str = "few ones int"; type WriteCtx = F::Int; diff --git a/src/etc/test-float-parse/src/lib.rs b/src/etc/test-float-parse/src/lib.rs index 3c71b0dc32e74..def66398d9fd1 100644 --- a/src/etc/test-float-parse/src/lib.rs +++ b/src/etc/test-float-parse/src/lib.rs @@ -2,19 +2,19 @@ mod traits; mod ui; mod validate; -use std::any::{TypeId, type_name}; +use std::any::type_name; use std::cmp::min; use std::ops::RangeInclusive; use std::process::ExitCode; +use std::sync::OnceLock; use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::{OnceLock, mpsc}; use std::{fmt, time}; -use indicatif::{MultiProgress, ProgressBar}; use rand::distributions::{Distribution, Standard}; use rayon::prelude::*; use time::{Duration, Instant}; use traits::{Float, Generator, Int}; +use validate::CheckError; /// Test generators. mod gen { @@ -43,7 +43,7 @@ const HUGE_TEST_CUTOFF: u64 = 5_000_000; /// Seed for tests that use a deterministic RNG. const SEED: [u8; 32] = *b"3.141592653589793238462643383279"; -/// Global configuration +/// Global configuration. #[derive(Debug)] pub struct Config { pub timeout: Duration, @@ -104,9 +104,9 @@ pub fn run(cfg: Config, include: &[String], exclude: &[String]) -> ExitCode { println!("Skipping test '{exc}'"); } - println!("launching"); + println!("Launching all"); let elapsed = launch_tests(&mut tests, &cfg); - ui::finish(&tests, elapsed, &cfg) + ui::finish_all(&tests, elapsed, &cfg) } /// Enumerate tests to run but don't actually run them. @@ -160,18 +160,18 @@ where #[derive(Debug)] pub struct TestInfo { pub name: String, - /// Tests are identified by the type ID of `(F, G)` (tuple of the float and generator type). - /// This gives an easy way to associate messages with tests. - id: TypeId, float_name: &'static str, + float_bits: u32, gen_name: &'static str, /// Name for display in the progress bar. short_name: String, + /// Pad the short name to a common width for progress bar use. + short_name_padded: String, total_tests: u64, /// Function to launch this test. - launch: fn(&mpsc::Sender, &TestInfo, &Config), + launch: fn(&TestInfo, &Config), /// Progress bar to be updated. - pb: Option, + progress: Option, /// Once completed, this will be set. completed: OnceLock, } @@ -187,14 +187,18 @@ impl TestInfo { let f_name = type_name::(); let gen_name = G::NAME; let gen_short_name = G::SHORT_NAME; + let name = format!("{f_name} {gen_name}"); + let short_name = format!("{f_name} {gen_short_name}"); + let short_name_padded = format!("{short_name:18}"); let info = TestInfo { - id: TypeId::of::<(F, G)>(), float_name: f_name, + float_bits: F::BITS, gen_name, - pb: None, - name: format!("{f_name} {gen_name}"), - short_name: format!("{f_name} {gen_short_name}"), + progress: None, + name, + short_name_padded, + short_name, launch: test_runner::, total_tests: G::total_tests(), completed: OnceLock::new(), @@ -202,106 +206,18 @@ impl TestInfo { v.push(info); } - /// Pad the short name to a common width for progress bar use. - fn short_name_padded(&self) -> String { - format!("{:18}", self.short_name) - } - - /// Create a progress bar for this test within a multiprogress bar. - fn register_pb(&mut self, mp: &MultiProgress, drop_bars: &mut Vec) { - self.pb = Some(ui::create_pb(mp, self.total_tests, &self.short_name_padded(), drop_bars)); - } - - /// When the test is finished, update progress bar messages and finalize. - fn finalize_pb(&self, c: &Completed) { - let pb = self.pb.as_ref().unwrap(); - ui::finalize_pb(pb, &self.short_name_padded(), c); - } - /// True if this should be run after all others. fn is_huge_test(&self) -> bool { self.total_tests >= HUGE_TEST_CUTOFF } -} - -/// A message sent from test runner threads to the UI/log thread. -#[derive(Clone, Debug)] -struct Msg { - id: TypeId, - update: Update, -} -impl Msg { - /// Wrap an `Update` into a message for the specified type. We use the `TypeId` of `(F, G)` to - /// identify which test a message in the channel came from. - fn new>(u: Update) -> Self { - Self { id: TypeId::of::<(F, G)>(), update: u } - } - - /// Get the matching test from a list. Panics if not found. - fn find_test<'a>(&self, tests: &'a [TestInfo]) -> &'a TestInfo { - tests.iter().find(|t| t.id == self.id).unwrap() - } - - /// Update UI as needed for a single message received from the test runners. - fn handle(self, tests: &[TestInfo], mp: &MultiProgress) { - let test = self.find_test(tests); - let pb = test.pb.as_ref().unwrap(); - - match self.update { - Update::Started => { - mp.println(format!("Testing '{}'", test.name)).unwrap(); - } - Update::Progress { executed, failures } => { - pb.set_message(format! {"{failures}"}); - pb.set_position(executed); - } - Update::Failure { fail, input, float_res } => { - mp.println(format!( - "Failure in '{}': {fail}. parsing '{input}'. Parsed as: {float_res}", - test.name - )) - .unwrap(); - } - Update::Completed(c) => { - test.finalize_pb(&c); - - let prefix = match c.result { - Ok(FinishedAll) => "Completed tests for", - Err(EarlyExit::Timeout) => "Timed out", - Err(EarlyExit::MaxFailures) => "Max failures reached for", - }; - - mp.println(format!( - "{prefix} generator '{}' in {:?}. {} tests run, {} failures", - test.name, c.elapsed, c.executed, c.failures - )) - .unwrap(); - test.completed.set(c).unwrap(); - } - }; + /// When the test is finished, update progress bar messages and finalize. + fn complete(&self, c: Completed) { + self.progress.as_ref().unwrap().complete(&c, 0); + self.completed.set(c).unwrap(); } } -/// Status sent with a message. -#[derive(Clone, Debug)] -enum Update { - /// Starting a new test runner. - Started, - /// Completed a out of b tests. - Progress { executed: u64, failures: u64 }, - /// Received a failed test. - Failure { - fail: CheckFailure, - /// String for which parsing was attempted. - input: Box, - /// The parsed & decomposed `FloatRes`, already stringified so we don't need generics here. - float_res: Box, - }, - /// Exited with an unexpected condition. - Completed(Completed), -} - /// Result of an input did not parsing successfully. #[derive(Clone, Debug)] enum CheckFailure { @@ -329,6 +245,10 @@ enum CheckFailure { /// two representable values. incorrect_midpoint_rounding: bool, }, + /// String did not parse successfully. + ParsingFailed(Box), + /// A panic was caught. + Panic(Box), } impl fmt::Display for CheckFailure { @@ -363,6 +283,8 @@ impl fmt::Display for CheckFailure { } Ok(()) } + CheckFailure::ParsingFailed(e) => write!(f, "parsing failed: {e}"), + CheckFailure::Panic(e) => write!(f, "function panicked: {e}"), } } } @@ -398,55 +320,21 @@ enum EarlyExit { /// This launches a main thread that receives messages and handlees UI updates, and uses the /// rest of the thread pool to execute the tests. fn launch_tests(tests: &mut [TestInfo], cfg: &Config) -> Duration { - // Run shorter tests first - tests.sort_unstable_by_key(|test| test.total_tests); + // Run shorter tests and smaller float types first. + tests.sort_unstable_by_key(|test| (test.total_tests, test.float_bits)); for test in tests.iter() { println!("Launching test '{}'", test.name); } - // Configure progress bars let mut all_progress_bars = Vec::new(); - let mp = MultiProgress::new(); - mp.set_move_cursor(true); - for test in tests.iter_mut() { - test.register_pb(&mp, &mut all_progress_bars); - } - - ui::set_panic_hook(all_progress_bars); - - let (tx, rx) = mpsc::channel::(); let start = Instant::now(); - rayon::scope(|scope| { - // Thread that updates the UI - scope.spawn(|_scope| { - let rx = rx; // move rx - - loop { - if tests.iter().all(|t| t.completed.get().is_some()) { - break; - } - - let msg = rx.recv().unwrap(); - msg.handle(tests, &mp); - } - - // All tests completed; finish things up - drop(mp); - assert_eq!(rx.try_recv().unwrap_err(), mpsc::TryRecvError::Empty); - }); - - // Don't let the thread pool be starved by huge tests. Run faster tests first in parallel, - // then parallelize only within the rest of the tests. - let (huge_tests, normal_tests): (Vec<_>, Vec<_>) = - tests.iter().partition(|t| t.is_huge_test()); - - // Run the actual tests - normal_tests.par_iter().for_each(|test| ((test.launch)(&tx, test, cfg))); - - huge_tests.par_iter().for_each(|test| ((test.launch)(&tx, test, cfg))); - }); + for test in tests.iter_mut() { + test.progress = Some(ui::Progress::new(test, &mut all_progress_bars)); + ui::set_panic_hook(&all_progress_bars); + ((test.launch)(test, cfg)); + } start.elapsed() } @@ -454,15 +342,12 @@ fn launch_tests(tests: &mut [TestInfo], cfg: &Config) -> Duration { /// Test runer for a single generator. /// /// This calls the generator's iterator multiple times (in parallel) and validates each output. -fn test_runner>(tx: &mpsc::Sender, _info: &TestInfo, cfg: &Config) { - tx.send(Msg::new::(Update::Started)).unwrap(); - - let total = G::total_tests(); +fn test_runner>(test: &TestInfo, cfg: &Config) { let gen = G::new(); let executed = AtomicU64::new(0); let failures = AtomicU64::new(0); - let checks_per_update = min(total, 1000); + let checks_per_update = min(test.total_tests, 1000); let started = Instant::now(); // Function to execute for a single test iteration. @@ -474,7 +359,12 @@ fn test_runner>(tx: &mpsc::Sender, _info: &TestIn match validate::validate::(buf) { Ok(()) => (), Err(e) => { - tx.send(Msg::new::(e)).unwrap(); + let CheckError { fail, input, float_res } = e; + test.progress.as_ref().unwrap().println(&format!( + "Failure in '{}': {fail}. parsing '{input}'. Parsed as: {float_res}", + test.name + )); + let f = failures.fetch_add(1, Ordering::Relaxed); // End early if the limit is exceeded. if f >= cfg.max_failures { @@ -486,9 +376,7 @@ fn test_runner>(tx: &mpsc::Sender, _info: &TestIn // Send periodic updates if executed % checks_per_update == 0 { let failures = failures.load(Ordering::Relaxed); - - tx.send(Msg::new::(Update::Progress { executed, failures })).unwrap(); - + test.progress.as_ref().unwrap().update(executed, failures); if started.elapsed() > cfg.timeout { return Err(EarlyExit::Timeout); } @@ -499,15 +387,19 @@ fn test_runner>(tx: &mpsc::Sender, _info: &TestIn // Run the test iterations in parallel. Each thread gets a string buffer to write // its check values to. - let res = gen.par_bridge().try_for_each_init(|| String::with_capacity(100), check_one); + let res = gen.par_bridge().try_for_each_init(String::new, check_one); let elapsed = started.elapsed(); let executed = executed.into_inner(); let failures = failures.into_inner(); // Warn about bad estimates if relevant. - let warning = if executed != total && res.is_ok() { - let msg = format!("executed tests != estimated ({executed} != {total}) for {}", G::NAME); + let warning = if executed != test.total_tests && res.is_ok() { + let msg = format!( + "executed tests != estimated ({executed} != {}) for {}", + test.total_tests, + G::NAME + ); Some(msg.into()) } else { @@ -515,12 +407,5 @@ fn test_runner>(tx: &mpsc::Sender, _info: &TestIn }; let result = res.map(|()| FinishedAll); - tx.send(Msg::new::(Update::Completed(Completed { - executed, - failures, - result, - warning, - elapsed, - }))) - .unwrap(); + test.complete(Completed { executed, failures, result, warning, elapsed }); } diff --git a/src/etc/test-float-parse/src/traits.rs b/src/etc/test-float-parse/src/traits.rs index f5333d63b3693..c8407ba7cc50a 100644 --- a/src/etc/test-float-parse/src/traits.rs +++ b/src/etc/test-float-parse/src/traits.rs @@ -177,7 +177,7 @@ impl_float!(f32, u32, 32; f64, u64, 64); /// allocations (which otherwise turn out to be a pretty expensive part of these tests). pub trait Generator: Iterator + Send + 'static { /// Full display and filtering name - const NAME: &'static str; + const NAME: &'static str = Self::SHORT_NAME; /// Name for display with the progress bar const SHORT_NAME: &'static str; diff --git a/src/etc/test-float-parse/src/ui.rs b/src/etc/test-float-parse/src/ui.rs index f333bd4a55dc9..1ee57723e6a62 100644 --- a/src/etc/test-float-parse/src/ui.rs +++ b/src/etc/test-float-parse/src/ui.rs @@ -1,67 +1,92 @@ //! Progress bars and such. +use std::any::type_name; +use std::fmt; use std::io::{self, Write}; use std::process::ExitCode; use std::time::Duration; -use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; +use indicatif::{ProgressBar, ProgressStyle}; use crate::{Completed, Config, EarlyExit, FinishedAll, TestInfo}; /// Templates for progress bars. -const PB_TEMPLATE: &str = "[{elapsed:3} {percent:3}%] {bar:20.cyan/blue} NAME ({pos}/{len}, {msg} f, {per_sec}, eta {eta})"; -const PB_TEMPLATE_FINAL: &str = - "[{elapsed:3} {percent:3}%] NAME ({pos}/{len}, {msg:.COLOR}, {per_sec}, {elapsed_precise})"; - -/// Create a new progress bar within a multiprogress bar. -pub fn create_pb( - mp: &MultiProgress, - total_tests: u64, - short_name_padded: &str, - all_bars: &mut Vec, -) -> ProgressBar { - let pb = mp.add(ProgressBar::new(total_tests)); - let pb_style = ProgressStyle::with_template(&PB_TEMPLATE.replace("NAME", short_name_padded)) - .unwrap() - .progress_chars("##-"); - - pb.set_style(pb_style.clone()); - pb.set_message("0"); - all_bars.push(pb.clone()); - pb +const PB_TEMPLATE: &str = "[{elapsed:3} {percent:3}%] {bar:20.cyan/blue} NAME \ + {human_pos:>8}/{human_len:8} {msg} f {per_sec:14} eta {eta:8}"; +const PB_TEMPLATE_FINAL: &str = "[{elapsed:3} {percent:3}%] {bar:20.cyan/blue} NAME \ + {human_pos:>8}/{human_len:8} {msg:.COLOR} {per_sec:18} {elapsed_precise}"; + +/// Thin abstraction over our usage of a `ProgressBar`. +#[derive(Debug)] +pub struct Progress { + pb: ProgressBar, + make_final_style: NoDebug ProgressStyle + Sync>>, } -/// Removes the status bar and replace it with a message. -pub fn finalize_pb(pb: &ProgressBar, short_name_padded: &str, c: &Completed) { - let f = c.failures; +impl Progress { + /// Create a new progress bar within a multiprogress bar. + pub fn new(test: &TestInfo, all_bars: &mut Vec) -> Self { + let initial_template = PB_TEMPLATE.replace("NAME", &test.short_name_padded); + let final_template = PB_TEMPLATE_FINAL.replace("NAME", &test.short_name_padded); + let initial_style = + ProgressStyle::with_template(&initial_template).unwrap().progress_chars("##-"); + let make_final_style = move |color| { + ProgressStyle::with_template(&final_template.replace("COLOR", color)) + .unwrap() + .progress_chars("##-") + }; - // Use a tuple so we can use colors - let (color, msg, finish_pb): (&str, String, fn(&ProgressBar, String)) = match &c.result { - Ok(FinishedAll) if f > 0 => { - ("red", format!("{f} f (finished with errors)",), ProgressBar::finish_with_message) - } - Ok(FinishedAll) => { - ("green", format!("{f} f (finished successfully)",), ProgressBar::finish_with_message) - } - Err(EarlyExit::Timeout) => { - ("red", format!("{f} f (timed out)"), ProgressBar::abandon_with_message) + let pb = ProgressBar::new(test.total_tests); + pb.set_style(initial_style); + pb.set_length(test.total_tests); + pb.set_message("0"); + all_bars.push(pb.clone()); + + Progress { pb, make_final_style: NoDebug(Box::new(make_final_style)) } + } + + /// Completed a out of b tests. + pub fn update(&self, completed: u64, failures: u64) { + // Infrequently update the progress bar. + if completed % 5_000 == 0 || failures > 0 { + self.pb.set_position(completed); } - Err(EarlyExit::MaxFailures) => { - ("red", format!("{f} f (failure limit)"), ProgressBar::abandon_with_message) + + if failures > 0 { + self.pb.set_message(format! {"{failures}"}); } - }; + } + + /// Finalize the progress bar. + pub fn complete(&self, c: &Completed, real_total: u64) { + let f = c.failures; + let (color, msg, finish_fn): (&str, String, fn(&ProgressBar)) = match &c.result { + Ok(FinishedAll) if f > 0 => { + ("red", format!("{f} f (completed with errors)",), ProgressBar::finish) + } + Ok(FinishedAll) => { + ("green", format!("{f} f (completed successfully)",), ProgressBar::finish) + } + Err(EarlyExit::Timeout) => ("red", format!("{f} f (timed out)"), ProgressBar::abandon), + Err(EarlyExit::MaxFailures) => { + ("red", format!("{f} f (failure limit)"), ProgressBar::abandon) + } + }; - let pb_style = ProgressStyle::with_template( - &PB_TEMPLATE_FINAL.replace("NAME", short_name_padded).replace("COLOR", color), - ) - .unwrap(); + self.pb.set_position(real_total); + self.pb.set_style(self.make_final_style.0(color)); + self.pb.set_message(msg); + finish_fn(&self.pb); + } - pb.set_style(pb_style); - finish_pb(pb, msg); + /// Print a message to stdout above the current progress bar. + pub fn println(&self, msg: &str) { + self.pb.suspend(|| println!("{msg}")); + } } /// Print final messages after all tests are complete. -pub fn finish(tests: &[TestInfo], total_elapsed: Duration, cfg: &Config) -> ExitCode { +pub fn finish_all(tests: &[TestInfo], total_elapsed: Duration, cfg: &Config) -> ExitCode { println!("\n\nResults:"); let mut failed_generators = 0; @@ -118,8 +143,9 @@ pub fn finish(tests: &[TestInfo], total_elapsed: Duration, cfg: &Config) -> Exit /// indicatif likes to eat panic messages. This workaround isn't ideal, but it improves things. /// . -pub fn set_panic_hook(drop_bars: Vec) { +pub fn set_panic_hook(drop_bars: &[ProgressBar]) { let hook = std::panic::take_hook(); + let drop_bars = drop_bars.to_owned(); std::panic::set_hook(Box::new(move |info| { for bar in &drop_bars { bar.abandon(); @@ -130,3 +156,13 @@ pub fn set_panic_hook(drop_bars: Vec) { hook(info); })); } + +/// Allow non-Debug items in a `derive(Debug)` struct`. +#[derive(Clone)] +struct NoDebug(T); + +impl fmt::Debug for NoDebug { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(type_name::()) + } +} diff --git a/src/etc/test-float-parse/src/validate.rs b/src/etc/test-float-parse/src/validate.rs index 1eb3699cfb9f7..40dda274e3b86 100644 --- a/src/etc/test-float-parse/src/validate.rs +++ b/src/etc/test-float-parse/src/validate.rs @@ -1,6 +1,6 @@ //! Everything related to verifying that parsed outputs are correct. -use std::any::type_name; +use std::any::{Any, type_name}; use std::collections::BTreeMap; use std::ops::RangeInclusive; use std::str::FromStr; @@ -9,7 +9,7 @@ use std::sync::LazyLock; use num::bigint::ToBigInt; use num::{BigInt, BigRational, FromPrimitive, Signed, ToPrimitive}; -use crate::{CheckFailure, Float, Int, Update}; +use crate::{CheckFailure, Float, Int}; /// Powers of two that we store for constants. Account for binary128 which has a 15-bit exponent. const POWERS_OF_TWO_RANGE: RangeInclusive = (-(2 << 15))..=(2 << 15); @@ -89,10 +89,16 @@ impl Constants { } /// Validate that a string parses correctly -pub fn validate(input: &str) -> Result<(), Update> { - let parsed: F = input - .parse() - .unwrap_or_else(|e| panic!("parsing failed for {}: {e}. Input: {input}", type_name::())); +pub fn validate(input: &str) -> Result<(), CheckError> { + // Catch panics in case debug assertions within `std` fail. + let parsed = std::panic::catch_unwind(|| { + input.parse::().map_err(|e| CheckError { + fail: CheckFailure::ParsingFailed(e.to_string().into()), + input: input.into(), + float_res: "none".into(), + }) + }) + .map_err(|e| convert_panic_error(&e, input))??; // Parsed float, decoded into significand and exponent let decoded = decode(parsed); @@ -104,6 +110,21 @@ pub fn validate(input: &str) -> Result<(), Update> { decoded.check(rational, input) } +/// Turn panics into concrete error types. +fn convert_panic_error(e: &dyn Any, input: &str) -> CheckError { + let msg = e + .downcast_ref::() + .map(|s| s.as_str()) + .or_else(|| e.downcast_ref::<&str>().copied()) + .unwrap_or("(no contents)"); + + CheckError { + fail: CheckFailure::Panic(msg.into()), + input: input.into(), + float_res: "none".into(), + } +} + /// The result of parsing a string to a float type. #[derive(Clone, Copy, Debug, PartialEq)] pub enum FloatRes { @@ -118,10 +139,19 @@ pub enum FloatRes { }, } +#[derive(Clone, Debug)] +pub struct CheckError { + pub fail: CheckFailure, + /// String for which parsing was attempted. + pub input: Box, + /// The parsed & decomposed `FloatRes`, already stringified so we don't need generics here. + pub float_res: Box, +} + impl FloatRes { /// Given a known exact rational, check that this representation is accurate within the /// limits of the float representation. If not, construct a failure `Update` to send. - fn check(self, expected: Rational, input: &str) -> Result<(), Update> { + fn check(self, expected: Rational, input: &str) -> Result<(), CheckError> { let consts = F::constants(); // let bool_helper = |cond: bool, err| cond.then_some(()).ok_or(err); @@ -173,7 +203,7 @@ impl FloatRes { (Rational::Finite(r), FloatRes::Real { sig, exp }) => Self::validate_real(r, sig, exp), }; - res.map_err(|fail| Update::Failure { + res.map_err(|fail| CheckError { fail, input: input.into(), float_res: format!("{self:?}").into(),