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:
parent
9af8985e05
commit
7603e0104a
7 changed files with 175 additions and 227 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue