1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
|
//! Exhaustive tests for `f16` and `f32`, high-iteration for `f64` and `f128`.
use std::fmt;
use std::io::{self, IsTerminal};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use indicatif::{ProgressBar, ProgressStyle};
use libm_test::generate::spaced;
use libm_test::mpfloat::MpOp;
use libm_test::{
CheckBasis, CheckCtx, CheckOutput, GeneratorKind, MathOp, TestResult, TupleCall,
skip_extensive_test,
};
use libtest_mimic::{Arguments, Trial};
use rayon::prelude::*;
use spaced::SpacedInput;
const BASIS: CheckBasis = CheckBasis::Mpfr;
/// Run the extensive test suite.
pub fn run() {
let mut args = Arguments::from_args();
// Prevent multiple tests from running in parallel, each test gets parallized internally.
args.test_threads = Some(1);
let tests = register_all_tests();
// With default parallelism, the CPU doesn't saturate. We don't need to be nice to
// other processes, so do 1.5x to make sure we use all available resources.
let threads = std::thread::available_parallelism()
.map(Into::into)
.unwrap_or(0)
* 3
/ 2;
rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build_global()
.unwrap();
libtest_mimic::run(&args, tests).exit();
}
macro_rules! mp_extensive_tests {
(
fn_name: $fn_name:ident,
attrs: [$($attr:meta),*],
extra: [$push_to:ident],
) => {
$(#[$attr])*
register_single_test::<libm_test::op::$fn_name::Routine>(&mut $push_to);
};
}
/// Create a list of tests for consumption by `libtest_mimic`.
fn register_all_tests() -> Vec<Trial> {
let mut all_tests = Vec::new();
libm_macros::for_each_function! {
callback: mp_extensive_tests,
extra: [all_tests],
skip: [
// FIXME: test needed, see
// https://github.com/rust-lang/libm/pull/311#discussion_r1818273392
nextafter,
nextafterf,
],
}
all_tests
}
/// Add a single test to the list.
fn register_single_test<Op>(all: &mut Vec<Trial>)
where
Op: MathOp + MpOp,
Op::RustArgs: SpacedInput<Op> + Send,
{
let test_name = format!("mp_extensive_{}", Op::NAME);
let ctx = CheckCtx::new(Op::IDENTIFIER, BASIS, GeneratorKind::Spaced).extensive(true);
let skip = skip_extensive_test(&ctx);
let runner = move || {
if !cfg!(optimizations_enabled) {
panic!("extensive tests should be run with --release");
}
let res = run_single_test::<Op>(&ctx);
let e = match res {
Ok(()) => return Ok(()),
Err(e) => e,
};
// Format with the `Debug` implementation so we get the error cause chain, and print it
// here so we see the result immediately (rather than waiting for all tests to conclude).
let e = format!("{e:?}");
eprintln!("failure testing {}:{e}\n", Op::IDENTIFIER);
Err(e.into())
};
all.push(Trial::test(test_name, runner).with_ignored_flag(skip));
}
/// Test runner for a signle routine.
fn run_single_test<Op>(ctx: &CheckCtx) -> TestResult
where
Op: MathOp + MpOp,
Op::RustArgs: SpacedInput<Op> + Send,
{
// Small delay before printing anything so other output from the runner has a chance to flush.
std::thread::sleep(Duration::from_millis(500));
eprintln!();
let completed = AtomicU64::new(0);
let (ref mut cases, total) = spaced::get_test_cases::<Op>(ctx);
let pb = Progress::new(Op::NAME, total);
let test_single_chunk = |mp_vals: &mut Op::MpTy, input_vec: Vec<Op::RustArgs>| -> TestResult {
for input in input_vec {
// Test the input.
let mp_res = Op::run(mp_vals, input);
let crate_res = input.call_intercept_panics(Op::ROUTINE);
crate_res.validate(mp_res, input, ctx)?;
let completed = completed.fetch_add(1, Ordering::Relaxed) + 1;
pb.update(completed, input);
}
Ok(())
};
// Chunk the cases so Rayon doesn't switch threads between each iterator item. 50k seems near
// a performance sweet spot. Ideally we would reuse these allocations rather than discarding,
// but that is difficult with Rayon's API.
let chunk_size = 50_000;
let chunks = std::iter::from_fn(move || {
let mut v = Vec::with_capacity(chunk_size);
v.extend(cases.take(chunk_size));
(!v.is_empty()).then_some(v)
});
// Run the actual tests
let res = chunks
.par_bridge()
.try_for_each_init(Op::new_mp, test_single_chunk);
let real_total = completed.load(Ordering::Relaxed);
pb.complete(real_total);
if res.is_ok() && real_total != total {
// Provide a warning if our estimate needs to be updated.
panic!("total run {real_total} does not match expected {total}");
}
res
}
/// Wrapper around a `ProgressBar` that handles styles and non-TTY messages.
struct Progress {
pb: ProgressBar,
name_padded: String,
final_style: ProgressStyle,
is_tty: bool,
}
impl Progress {
const PB_TEMPLATE: &str = "[{elapsed:3} {percent:3}%] {bar:20.cyan/blue} NAME \
{human_pos:>13}/{human_len:13} {per_sec:18} eta {eta:8} {msg}";
const PB_TEMPLATE_FINAL: &str = "[{elapsed:3} {percent:3}%] {bar:20.cyan/blue} NAME \
{human_pos:>13}/{human_len:13} {per_sec:18} done in {elapsed_precise}";
fn new(name: &str, total: u64) -> Self {
eprintln!("starting extensive tests for `{name}`");
let name_padded = format!("{name:9}");
let is_tty = io::stderr().is_terminal();
let initial_style =
ProgressStyle::with_template(&Self::PB_TEMPLATE.replace("NAME", &name_padded))
.unwrap()
.progress_chars("##-");
let final_style =
ProgressStyle::with_template(&Self::PB_TEMPLATE_FINAL.replace("NAME", &name_padded))
.unwrap()
.progress_chars("##-");
let pb = ProgressBar::new(total);
pb.set_style(initial_style);
Self {
pb,
final_style,
name_padded,
is_tty,
}
}
fn update(&self, completed: u64, input: impl fmt::Debug) {
// Infrequently update the progress bar.
if completed.is_multiple_of(20_000) {
self.pb.set_position(completed);
}
if completed.is_multiple_of(500_000) {
self.pb.set_message(format!("input: {input:<24?}"));
}
if !self.is_tty && completed.is_multiple_of(5_000_000) {
let len = self.pb.length().unwrap_or_default();
eprintln!(
"[{elapsed:3?}s {percent:3.0}%] {name} \
{human_pos:>10}/{human_len:<10} {per_sec:14.2}/s eta {eta:4}s {input:<24?}",
elapsed = self.pb.elapsed().as_secs(),
percent = completed as f32 * 100.0 / len as f32,
name = self.name_padded,
human_pos = completed,
human_len = len,
per_sec = self.pb.per_sec(),
eta = self.pb.eta().as_secs()
);
}
}
fn complete(self, real_total: u64) {
self.pb.set_style(self.final_style);
self.pb.set_position(real_total);
self.pb.abandon();
if !self.is_tty {
let len = self.pb.length().unwrap_or_default();
eprintln!(
"[{elapsed:3}s {percent:3.0}%] {name} \
{human_pos:>10}/{human_len:<10} {per_sec:14.2}/s done in {elapsed_precise}",
elapsed = self.pb.elapsed().as_secs(),
percent = real_total as f32 * 100.0 / len as f32,
name = self.name_padded,
human_pos = real_total,
human_len = len,
per_sec = self.pb.per_sec(),
elapsed_precise = self.pb.elapsed().as_secs(),
);
}
eprintln!();
}
}
|