1
Fork 0

Simplify parallelization in test-float-parse

Currently, test case generators are launched in parallel and their test
cases also run in parallel, all within the same pool. I originally
implemented this with the assumption that there would be an advantage in
parallelizing the generators themselves, but this turns out to not
really have any benefit.

Simplify things by running generators in series while keeping their test
cases parallelized. This makes the code easier to follow, and there is
no longer a need for MPSC or multiprogress bars. Additionally, the UI
output can be made cleaner.
This commit is contained in:
Trevor Gross 2024-12-31 21:39:13 +00:00
parent 9af8985e05
commit 7603e0104a
7 changed files with 175 additions and 227 deletions

View file

@ -13,13 +13,12 @@ impl<F: Float> Generator<F> for Exhaustive<F>
where where
RangeInclusive<F::Int>: Iterator<Item = F::Int>, RangeInclusive<F::Int>: Iterator<Item = F::Int>,
{ {
const NAME: &'static str = "exhaustive";
const SHORT_NAME: &'static str = "exhaustive"; const SHORT_NAME: &'static str = "exhaustive";
type WriteCtx = F; type WriteCtx = F;
fn total_tests() -> u64 { 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 { fn new() -> Self {

View file

@ -49,7 +49,6 @@ impl<F: Float> Generator<F> for Fuzz<F>
where where
Standard: Distribution<<F as Float>::Int>, Standard: Distribution<<F as Float>::Int>,
{ {
const NAME: &'static str = "fuzz";
const SHORT_NAME: &'static str = "fuzz"; const SHORT_NAME: &'static str = "fuzz";
type WriteCtx = F; type WriteCtx = F;

View file

@ -35,7 +35,6 @@ impl<F: Float> Generator<F> for FewOnesInt<F>
where where
<F::Int as TryFrom<u128>>::Error: std::fmt::Debug, <F::Int as TryFrom<u128>>::Error: std::fmt::Debug,
{ {
const NAME: &'static str = "few ones int";
const SHORT_NAME: &'static str = "few ones int"; const SHORT_NAME: &'static str = "few ones int";
type WriteCtx = F::Int; type WriteCtx = F::Int;

View file

@ -2,19 +2,19 @@ mod traits;
mod ui; mod ui;
mod validate; mod validate;
use std::any::{TypeId, type_name}; use std::any::type_name;
use std::cmp::min; use std::cmp::min;
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
use std::process::ExitCode; use std::process::ExitCode;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{OnceLock, mpsc};
use std::{fmt, time}; use std::{fmt, time};
use indicatif::{MultiProgress, ProgressBar};
use rand::distributions::{Distribution, Standard}; use rand::distributions::{Distribution, Standard};
use rayon::prelude::*; use rayon::prelude::*;
use time::{Duration, Instant}; use time::{Duration, Instant};
use traits::{Float, Generator, Int}; use traits::{Float, Generator, Int};
use validate::CheckError;
/// Test generators. /// Test generators.
mod gen { mod gen {
@ -43,7 +43,7 @@ const HUGE_TEST_CUTOFF: u64 = 5_000_000;
/// Seed for tests that use a deterministic RNG. /// Seed for tests that use a deterministic RNG.
const SEED: [u8; 32] = *b"3.141592653589793238462643383279"; const SEED: [u8; 32] = *b"3.141592653589793238462643383279";
/// Global configuration /// Global configuration.
#[derive(Debug)] #[derive(Debug)]
pub struct Config { pub struct Config {
pub timeout: Duration, pub timeout: Duration,
@ -104,9 +104,9 @@ pub fn run(cfg: Config, include: &[String], exclude: &[String]) -> ExitCode {
println!("Skipping test '{exc}'"); println!("Skipping test '{exc}'");
} }
println!("launching"); println!("Launching all");
let elapsed = launch_tests(&mut tests, &cfg); 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. /// Enumerate tests to run but don't actually run them.
@ -160,18 +160,18 @@ where
#[derive(Debug)] #[derive(Debug)]
pub struct TestInfo { pub struct TestInfo {
pub name: String, 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_name: &'static str,
float_bits: u32,
gen_name: &'static str, gen_name: &'static str,
/// Name for display in the progress bar. /// Name for display in the progress bar.
short_name: String, short_name: String,
/// Pad the short name to a common width for progress bar use.
short_name_padded: String,
total_tests: u64, total_tests: u64,
/// Function to launch this test. /// Function to launch this test.
launch: fn(&mpsc::Sender<Msg>, &TestInfo, &Config), launch: fn(&TestInfo, &Config),
/// Progress bar to be updated. /// Progress bar to be updated.
pb: Option<ProgressBar>, progress: Option<ui::Progress>,
/// Once completed, this will be set. /// Once completed, this will be set.
completed: OnceLock<Completed>, completed: OnceLock<Completed>,
} }
@ -187,14 +187,18 @@ impl TestInfo {
let f_name = type_name::<F>(); let f_name = type_name::<F>();
let gen_name = G::NAME; let gen_name = G::NAME;
let gen_short_name = G::SHORT_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 { let info = TestInfo {
id: TypeId::of::<(F, G)>(),
float_name: f_name, float_name: f_name,
float_bits: F::BITS,
gen_name, gen_name,
pb: None, progress: None,
name: format!("{f_name} {gen_name}"), name,
short_name: format!("{f_name} {gen_short_name}"), short_name_padded,
short_name,
launch: test_runner::<F, G>, launch: test_runner::<F, G>,
total_tests: G::total_tests(), total_tests: G::total_tests(),
completed: OnceLock::new(), completed: OnceLock::new(),
@ -202,104 +206,16 @@ impl TestInfo {
v.push(info); 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<ProgressBar>) {
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. /// True if this should be run after all others.
fn is_huge_test(&self) -> bool { fn is_huge_test(&self) -> bool {
self.total_tests >= HUGE_TEST_CUTOFF self.total_tests >= HUGE_TEST_CUTOFF
} }
}
/// A message sent from test runner threads to the UI/log thread. /// When the test is finished, update progress bar messages and finalize.
#[derive(Clone, Debug)] fn complete(&self, c: Completed) {
struct Msg { self.progress.as_ref().unwrap().complete(&c, 0);
id: TypeId, self.completed.set(c).unwrap();
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<F: Float, G: Generator<F>>(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();
}
};
}
}
/// 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<str>,
/// The parsed & decomposed `FloatRes`, already stringified so we don't need generics here.
float_res: Box<str>,
},
/// Exited with an unexpected condition.
Completed(Completed),
} }
/// Result of an input did not parsing successfully. /// Result of an input did not parsing successfully.
@ -329,6 +245,10 @@ enum CheckFailure {
/// two representable values. /// two representable values.
incorrect_midpoint_rounding: bool, incorrect_midpoint_rounding: bool,
}, },
/// String did not parse successfully.
ParsingFailed(Box<str>),
/// A panic was caught.
Panic(Box<str>),
} }
impl fmt::Display for CheckFailure { impl fmt::Display for CheckFailure {
@ -363,6 +283,8 @@ impl fmt::Display for CheckFailure {
} }
Ok(()) 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 /// This launches a main thread that receives messages and handlees UI updates, and uses the
/// rest of the thread pool to execute the tests. /// rest of the thread pool to execute the tests.
fn launch_tests(tests: &mut [TestInfo], cfg: &Config) -> Duration { fn launch_tests(tests: &mut [TestInfo], cfg: &Config) -> Duration {
// Run shorter tests first // Run shorter tests and smaller float types first.
tests.sort_unstable_by_key(|test| test.total_tests); tests.sort_unstable_by_key(|test| (test.total_tests, test.float_bits));
for test in tests.iter() { for test in tests.iter() {
println!("Launching test '{}'", test.name); println!("Launching test '{}'", test.name);
} }
// Configure progress bars
let mut all_progress_bars = Vec::new(); 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::<Msg>();
let start = Instant::now(); let start = Instant::now();
rayon::scope(|scope| { for test in tests.iter_mut() {
// Thread that updates the UI test.progress = Some(ui::Progress::new(test, &mut all_progress_bars));
scope.spawn(|_scope| { ui::set_panic_hook(&all_progress_bars);
let rx = rx; // move rx ((test.launch)(test, cfg));
}
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)));
});
start.elapsed() start.elapsed()
} }
@ -454,15 +342,12 @@ fn launch_tests(tests: &mut [TestInfo], cfg: &Config) -> Duration {
/// Test runer for a single generator. /// Test runer for a single generator.
/// ///
/// This calls the generator's iterator multiple times (in parallel) and validates each output. /// This calls the generator's iterator multiple times (in parallel) and validates each output.
fn test_runner<F: Float, G: Generator<F>>(tx: &mpsc::Sender<Msg>, _info: &TestInfo, cfg: &Config) { fn test_runner<F: Float, G: Generator<F>>(test: &TestInfo, cfg: &Config) {
tx.send(Msg::new::<F, G>(Update::Started)).unwrap();
let total = G::total_tests();
let gen = G::new(); let gen = G::new();
let executed = AtomicU64::new(0); let executed = AtomicU64::new(0);
let failures = 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(); let started = Instant::now();
// Function to execute for a single test iteration. // Function to execute for a single test iteration.
@ -474,7 +359,12 @@ fn test_runner<F: Float, G: Generator<F>>(tx: &mpsc::Sender<Msg>, _info: &TestIn
match validate::validate::<F>(buf) { match validate::validate::<F>(buf) {
Ok(()) => (), Ok(()) => (),
Err(e) => { Err(e) => {
tx.send(Msg::new::<F, G>(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); let f = failures.fetch_add(1, Ordering::Relaxed);
// End early if the limit is exceeded. // End early if the limit is exceeded.
if f >= cfg.max_failures { if f >= cfg.max_failures {
@ -486,9 +376,7 @@ fn test_runner<F: Float, G: Generator<F>>(tx: &mpsc::Sender<Msg>, _info: &TestIn
// Send periodic updates // Send periodic updates
if executed % checks_per_update == 0 { if executed % checks_per_update == 0 {
let failures = failures.load(Ordering::Relaxed); let failures = failures.load(Ordering::Relaxed);
test.progress.as_ref().unwrap().update(executed, failures);
tx.send(Msg::new::<F, G>(Update::Progress { executed, failures })).unwrap();
if started.elapsed() > cfg.timeout { if started.elapsed() > cfg.timeout {
return Err(EarlyExit::Timeout); return Err(EarlyExit::Timeout);
} }
@ -499,15 +387,19 @@ fn test_runner<F: Float, G: Generator<F>>(tx: &mpsc::Sender<Msg>, _info: &TestIn
// Run the test iterations in parallel. Each thread gets a string buffer to write // Run the test iterations in parallel. Each thread gets a string buffer to write
// its check values to. // 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 elapsed = started.elapsed();
let executed = executed.into_inner(); let executed = executed.into_inner();
let failures = failures.into_inner(); let failures = failures.into_inner();
// Warn about bad estimates if relevant. // Warn about bad estimates if relevant.
let warning = if executed != total && res.is_ok() { let warning = if executed != test.total_tests && res.is_ok() {
let msg = format!("executed tests != estimated ({executed} != {total}) for {}", G::NAME); let msg = format!(
"executed tests != estimated ({executed} != {}) for {}",
test.total_tests,
G::NAME
);
Some(msg.into()) Some(msg.into())
} else { } else {
@ -515,12 +407,5 @@ fn test_runner<F: Float, G: Generator<F>>(tx: &mpsc::Sender<Msg>, _info: &TestIn
}; };
let result = res.map(|()| FinishedAll); let result = res.map(|()| FinishedAll);
tx.send(Msg::new::<F, G>(Update::Completed(Completed { test.complete(Completed { executed, failures, result, warning, elapsed });
executed,
failures,
result,
warning,
elapsed,
})))
.unwrap();
} }

View file

@ -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). /// allocations (which otherwise turn out to be a pretty expensive part of these tests).
pub trait Generator<F: Float>: Iterator<Item = Self::WriteCtx> + Send + 'static { pub trait Generator<F: Float>: Iterator<Item = Self::WriteCtx> + Send + 'static {
/// Full display and filtering name /// Full display and filtering name
const NAME: &'static str; const NAME: &'static str = Self::SHORT_NAME;
/// Name for display with the progress bar /// Name for display with the progress bar
const SHORT_NAME: &'static str; const SHORT_NAME: &'static str;

View file

@ -1,67 +1,92 @@
//! Progress bars and such. //! Progress bars and such.
use std::any::type_name;
use std::fmt;
use std::io::{self, Write}; use std::io::{self, Write};
use std::process::ExitCode; use std::process::ExitCode;
use std::time::Duration; use std::time::Duration;
use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use indicatif::{ProgressBar, ProgressStyle};
use crate::{Completed, Config, EarlyExit, FinishedAll, TestInfo}; use crate::{Completed, Config, EarlyExit, FinishedAll, TestInfo};
/// Templates for progress bars. /// 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: &str = "[{elapsed:3} {percent:3}%] {bar:20.cyan/blue} NAME \
const PB_TEMPLATE_FINAL: &str = {human_pos:>8}/{human_len:8} {msg} f {per_sec:14} eta {eta:8}";
"[{elapsed:3} {percent:3}%] NAME ({pos}/{len}, {msg:.COLOR}, {per_sec}, {elapsed_precise})"; 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}";
/// Create a new progress bar within a multiprogress bar. /// Thin abstraction over our usage of a `ProgressBar`.
pub fn create_pb( #[derive(Debug)]
mp: &MultiProgress, pub struct Progress {
total_tests: u64, pb: ProgressBar,
short_name_padded: &str, make_final_style: NoDebug<Box<dyn Fn(&'static str) -> ProgressStyle + Sync>>,
all_bars: &mut Vec<ProgressBar>,
) -> 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
} }
/// Removes the status bar and replace it with a message. impl Progress {
pub fn finalize_pb(pb: &ProgressBar, short_name_padded: &str, c: &Completed) { /// Create a new progress bar within a multiprogress bar.
let f = c.failures; pub fn new(test: &TestInfo, all_bars: &mut Vec<ProgressBar>) -> 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 pb = ProgressBar::new(test.total_tests);
let (color, msg, finish_pb): (&str, String, fn(&ProgressBar, String)) = match &c.result { pb.set_style(initial_style);
Ok(FinishedAll) if f > 0 => { pb.set_length(test.total_tests);
("red", format!("{f} f (finished with errors)",), ProgressBar::finish_with_message) pb.set_message("0");
} all_bars.push(pb.clone());
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)
}
Err(EarlyExit::MaxFailures) => {
("red", format!("{f} f (failure limit)"), ProgressBar::abandon_with_message)
}
};
let pb_style = ProgressStyle::with_template( Progress { pb, make_final_style: NoDebug(Box::new(make_final_style)) }
&PB_TEMPLATE_FINAL.replace("NAME", short_name_padded).replace("COLOR", color), }
)
.unwrap();
pb.set_style(pb_style); /// Completed a out of b tests.
finish_pb(pb, msg); 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);
}
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)
}
};
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);
}
/// 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. /// 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:"); println!("\n\nResults:");
let mut failed_generators = 0; 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. /// indicatif likes to eat panic messages. This workaround isn't ideal, but it improves things.
/// <https://github.com/console-rs/indicatif/issues/121>. /// <https://github.com/console-rs/indicatif/issues/121>.
pub fn set_panic_hook(drop_bars: Vec<ProgressBar>) { pub fn set_panic_hook(drop_bars: &[ProgressBar]) {
let hook = std::panic::take_hook(); let hook = std::panic::take_hook();
let drop_bars = drop_bars.to_owned();
std::panic::set_hook(Box::new(move |info| { std::panic::set_hook(Box::new(move |info| {
for bar in &drop_bars { for bar in &drop_bars {
bar.abandon(); bar.abandon();
@ -130,3 +156,13 @@ pub fn set_panic_hook(drop_bars: Vec<ProgressBar>) {
hook(info); hook(info);
})); }));
} }
/// Allow non-Debug items in a `derive(Debug)` struct`.
#[derive(Clone)]
struct NoDebug<T>(T);
impl<T> fmt::Debug for NoDebug<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(type_name::<Self>())
}
}

View file

@ -1,6 +1,6 @@
//! Everything related to verifying that parsed outputs are correct. //! 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::collections::BTreeMap;
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
use std::str::FromStr; use std::str::FromStr;
@ -9,7 +9,7 @@ use std::sync::LazyLock;
use num::bigint::ToBigInt; use num::bigint::ToBigInt;
use num::{BigInt, BigRational, FromPrimitive, Signed, ToPrimitive}; 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. /// Powers of two that we store for constants. Account for binary128 which has a 15-bit exponent.
const POWERS_OF_TWO_RANGE: RangeInclusive<i32> = (-(2 << 15))..=(2 << 15); const POWERS_OF_TWO_RANGE: RangeInclusive<i32> = (-(2 << 15))..=(2 << 15);
@ -89,10 +89,16 @@ impl Constants {
} }
/// Validate that a string parses correctly /// Validate that a string parses correctly
pub fn validate<F: Float>(input: &str) -> Result<(), Update> { pub fn validate<F: Float>(input: &str) -> Result<(), CheckError> {
let parsed: F = input // Catch panics in case debug assertions within `std` fail.
.parse() let parsed = std::panic::catch_unwind(|| {
.unwrap_or_else(|e| panic!("parsing failed for {}: {e}. Input: {input}", type_name::<F>())); input.parse::<F>().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 // Parsed float, decoded into significand and exponent
let decoded = decode(parsed); let decoded = decode(parsed);
@ -104,6 +110,21 @@ pub fn validate<F: Float>(input: &str) -> Result<(), Update> {
decoded.check(rational, input) 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::<String>()
.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. /// The result of parsing a string to a float type.
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub enum FloatRes<F: Float> { pub enum FloatRes<F: Float> {
@ -118,10 +139,19 @@ pub enum FloatRes<F: Float> {
}, },
} }
#[derive(Clone, Debug)]
pub struct CheckError {
pub fail: CheckFailure,
/// String for which parsing was attempted.
pub input: Box<str>,
/// The parsed & decomposed `FloatRes`, already stringified so we don't need generics here.
pub float_res: Box<str>,
}
impl<F: Float> FloatRes<F> { impl<F: Float> FloatRes<F> {
/// Given a known exact rational, check that this representation is accurate within the /// 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. /// 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 consts = F::constants();
// let bool_helper = |cond: bool, err| cond.then_some(()).ok_or(err); // let bool_helper = |cond: bool, err| cond.then_some(()).ok_or(err);
@ -173,7 +203,7 @@ impl<F: Float> FloatRes<F> {
(Rational::Finite(r), FloatRes::Real { sig, exp }) => Self::validate_real(r, sig, exp), (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, fail,
input: input.into(), input: input.into(),
float_res: format!("{self:?}").into(), float_res: format!("{self:?}").into(),