Skip to content
This repository was archived by the owner on Apr 28, 2025. It is now read-only.

Commit 28c69b4

Browse files
committed
Rework how testing is done
Use a build script to generate musl reference outputs and then ensure that everything gets hooked up to actually run reference tests.
1 parent 575e81c commit 28c69b4

File tree

10 files changed

+351
-974
lines changed

10 files changed

+351
-974
lines changed

Cargo.toml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,19 @@ license = "MIT OR Apache-2.0"
88
name = "libm"
99
repository = "https://github.com/japaric/libm"
1010
version = "0.1.2"
11+
edition = "2018"
1112

1213
[features]
1314
# only used to run our test suite
1415
checked = []
1516
default = ['stable']
1617
stable = []
18+
musl-reference-tests = ['rand']
1719

1820
[workspace]
1921
members = [
2022
"crates/compiler-builtins-smoke-test",
21-
"crates/input-generator",
22-
"crates/musl-generator",
23-
"crates/shared",
2423
]
2524

26-
[dev-dependencies]
27-
shared = { path = "shared" }
25+
[build-dependencies]
26+
rand = { version = "0.6.5", optional = true }

build.rs

Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
fn main() {
2+
println!("cargo:rerun-if-changed=build.rs");
3+
4+
#[cfg(feature = "musl-reference-tests")]
5+
musl_reference_tests::generate();
6+
}
7+
8+
#[cfg(feature = "musl-reference-tests")]
9+
mod musl_reference_tests {
10+
use rand::seq::SliceRandom;
11+
use rand::Rng;
12+
use std::fs;
13+
use std::process::Command;
14+
15+
// Number of tests to generate for each function
16+
const NTESTS: usize = 500;
17+
18+
// These files are all internal functions or otherwise miscellaneous, not
19+
// defining a function we want to test.
20+
const IGNORED_FILES: &[&str] = &[
21+
"expo2.rs",
22+
"fenv.rs",
23+
"k_cos.rs",
24+
"k_cosf.rs",
25+
"k_expo2.rs",
26+
"k_expo2f.rs",
27+
"k_sin.rs",
28+
"k_sinf.rs",
29+
"k_tan.rs",
30+
"k_tanf.rs",
31+
"mod.rs",
32+
"rem_pio2.rs",
33+
"rem_pio2_large.rs",
34+
"rem_pio2f.rs",
35+
];
36+
37+
struct Function {
38+
name: String,
39+
args: Vec<Ty>,
40+
ret: Ty,
41+
tests: Vec<Test>,
42+
}
43+
44+
enum Ty {
45+
F32,
46+
F64,
47+
I32,
48+
Bool,
49+
}
50+
51+
struct Test {
52+
inputs: Vec<i64>,
53+
output: i64,
54+
}
55+
56+
pub fn generate() {
57+
let files = fs::read_dir("src/math")
58+
.unwrap()
59+
.map(|f| f.unwrap().path())
60+
.collect::<Vec<_>>();
61+
62+
let mut math = Vec::new();
63+
for file in files {
64+
if IGNORED_FILES.iter().any(|f| file.ends_with(f)) {
65+
continue;
66+
}
67+
68+
println!("generating musl reference tests in {:?}", file);
69+
70+
let contents = fs::read_to_string(file).unwrap();
71+
let mut functions = contents.lines().filter(|f| f.starts_with("pub fn"));
72+
let function_to_test = functions.next().unwrap();
73+
if functions.next().is_some() {
74+
panic!("more than one function in");
75+
}
76+
77+
math.push(parse(function_to_test));
78+
}
79+
80+
// Generate a bunch of random inputs for each function. This will
81+
// attempt to generate a good set of uniform test cases for exercising
82+
// all the various functionality.
83+
generate_random_tests(&mut math, &mut rand::thread_rng());
84+
85+
// After we have all our inputs, use the x86_64-unknown-linux-musl
86+
// target to generate the expected output.
87+
generate_test_outputs(&mut math);
88+
89+
// ... and now that we have both inputs and expected outputs, do a bunch
90+
// of codegen to create the unit tests which we'll actually execute.
91+
generate_unit_tests(&math);
92+
}
93+
94+
/// A "poor man's" parser for the signature of a function
95+
fn parse(s: &str) -> Function {
96+
let s = eat(s, "pub fn ");
97+
let pos = s.find('(').unwrap();
98+
let name = &s[..pos];
99+
let s = &s[pos + 1..];
100+
let end = s.find(')').unwrap();
101+
let args = s[..end]
102+
.split(',')
103+
.map(|arg| {
104+
let colon = arg.find(':').unwrap();
105+
parse_ty(arg[colon + 1..].trim())
106+
})
107+
.collect::<Vec<_>>();
108+
let tail = &s[end + 1..];
109+
let tail = eat(tail, " -> ");
110+
let ret = parse_ty(tail.trim().split(' ').next().unwrap());
111+
112+
return Function {
113+
name: name.to_string(),
114+
args,
115+
ret,
116+
tests: Vec::new(),
117+
};
118+
119+
fn parse_ty(s: &str) -> Ty {
120+
match s {
121+
"f32" => Ty::F32,
122+
"f64" => Ty::F64,
123+
"i32" => Ty::I32,
124+
"bool" => Ty::Bool,
125+
other => panic!("unknown type `{}`", other),
126+
}
127+
}
128+
129+
fn eat<'a>(s: &'a str, prefix: &str) -> &'a str {
130+
if s.starts_with(prefix) {
131+
&s[prefix.len()..]
132+
} else {
133+
panic!("{:?} didn't start with {:?}", s, prefix)
134+
}
135+
}
136+
}
137+
138+
fn generate_random_tests<R: Rng>(functions: &mut [Function], rng: &mut R) {
139+
for function in functions {
140+
for _ in 0..NTESTS {
141+
function.tests.push(generate_test(&function.args, rng));
142+
}
143+
}
144+
145+
fn generate_test<R: Rng>(args: &[Ty], rng: &mut R) -> Test {
146+
let inputs = args.iter().map(|ty| ty.gen_i64(rng)).collect();
147+
// zero output for now since we'll generate it later
148+
Test { inputs, output: 0 }
149+
}
150+
}
151+
152+
impl Ty {
153+
fn gen_i64<R: Rng>(&self, r: &mut R) -> i64 {
154+
match self {
155+
Ty::F32 => r.gen::<f32>().to_bits().into(),
156+
Ty::F64 => r.gen::<f64>().to_bits() as i64,
157+
Ty::I32 => {
158+
if r.gen_range(0, 10) < 1 {
159+
let i = *[i32::max_value(), 0, i32::min_value()].choose(r).unwrap();
160+
i.into()
161+
} else {
162+
r.gen::<i32>().into()
163+
}
164+
}
165+
Ty::Bool => r.gen::<bool>() as i64,
166+
}
167+
}
168+
169+
fn libc_ty(&self) -> &'static str {
170+
match self {
171+
Ty::F32 => "f32",
172+
Ty::F64 => "f64",
173+
Ty::I32 => "i32",
174+
Ty::Bool => "i32",
175+
}
176+
}
177+
}
178+
179+
fn generate_test_outputs(functions: &mut [Function]) {
180+
let mut src = String::new();
181+
let dst = std::env::var("OUT_DIR").unwrap();
182+
183+
// Generate a program which will run all tests with all inputs in
184+
// `functions`. This program will write all outputs to stdout (in a
185+
// binary format).
186+
src.push_str("use std::io::Write;");
187+
src.push_str("fn main() {");
188+
src.push_str("let mut result = Vec::new();");
189+
for function in functions.iter_mut() {
190+
src.push_str("unsafe {");
191+
src.push_str("extern { fn ");
192+
src.push_str(&function.name);
193+
src.push_str("(");
194+
for (i, arg) in function.args.iter().enumerate() {
195+
src.push_str(&format!("arg{}: {},", i, arg.libc_ty()));
196+
}
197+
src.push_str(") -> ");
198+
src.push_str(function.ret.libc_ty());
199+
src.push_str("; }");
200+
201+
src.push_str(&format!("static TESTS: &[[i64; {}]]", function.args.len()));
202+
src.push_str(" = &[");
203+
for test in function.tests.iter() {
204+
src.push_str("[");
205+
for val in test.inputs.iter() {
206+
src.push_str(&val.to_string());
207+
src.push_str(",");
208+
}
209+
src.push_str("],");
210+
}
211+
src.push_str("];");
212+
213+
src.push_str("for test in TESTS {");
214+
src.push_str("let output = ");
215+
src.push_str(&function.name);
216+
src.push_str("(");
217+
for (i, arg) in function.args.iter().enumerate() {
218+
src.push_str(&match arg {
219+
Ty::F32 => format!("f32::from_bits(test[{}] as u32)", i),
220+
Ty::F64 => format!("f64::from_bits(test[{}] as u64)", i),
221+
Ty::I32 => format!("test[{}] as i32", i),
222+
Ty::Bool => format!("test[{}] as i32", i),
223+
});
224+
src.push_str(",");
225+
}
226+
src.push_str(");");
227+
src.push_str("let output = ");
228+
src.push_str(match function.ret {
229+
Ty::F32 => "output.to_bits() as i64",
230+
Ty::F64 => "output.to_bits() as i64",
231+
Ty::I32 => "output as i64",
232+
Ty::Bool => "output as i64",
233+
});
234+
src.push_str(";");
235+
src.push_str("result.extend_from_slice(&output.to_le_bytes());");
236+
237+
src.push_str("}");
238+
239+
src.push_str("}");
240+
}
241+
242+
src.push_str("std::io::stdout().write_all(&result).unwrap();");
243+
244+
src.push_str("}");
245+
246+
let path = format!("{}/gen.rs", dst);
247+
fs::write(&path, src).unwrap();
248+
249+
// Make it somewhat pretty if something goes wrong
250+
drop(Command::new("rustfmt").arg(&path).status());
251+
252+
// Compile and execute this tests for the musl target, assuming we're an
253+
// x86_64 host effectively.
254+
let status = Command::new("rustc")
255+
.current_dir(&dst)
256+
.arg(&path)
257+
.arg("--target=x86_64-unknown-linux-musl")
258+
.status()
259+
.unwrap();
260+
assert!(status.success());
261+
let output = Command::new("./gen")
262+
.current_dir(&dst)
263+
.output()
264+
.unwrap();
265+
assert!(output.status.success());
266+
assert!(output.stderr.is_empty());
267+
268+
// Map all the output bytes back to an `i64` and then shove it all into
269+
// the expected results.
270+
let mut results =
271+
output.stdout.chunks_exact(8)
272+
.map(|buf| {
273+
let mut exact = [0; 8];
274+
exact.copy_from_slice(buf);
275+
i64::from_le_bytes(exact)
276+
});
277+
278+
for test in functions.iter_mut().flat_map(|f| f.tests.iter_mut()) {
279+
test.output = results.next().unwrap();
280+
}
281+
assert!(results.next().is_none());
282+
}
283+
284+
/// Codegens a file which has a ton of `#[test]` annotations for all the
285+
/// tests that we generated above.
286+
fn generate_unit_tests(functions: &[Function]) {
287+
let mut src = String::new();
288+
let dst = std::env::var("OUT_DIR").unwrap();
289+
290+
for function in functions {
291+
src.push_str("#[test]");
292+
src.push_str("fn ");
293+
src.push_str(&function.name);
294+
src.push_str("_matches_musl() {");
295+
src.push_str(&format!("static TESTS: &[([i64; {}], i64)]", function.args.len()));
296+
src.push_str(" = &[");
297+
for test in function.tests.iter() {
298+
src.push_str("([");
299+
for val in test.inputs.iter() {
300+
src.push_str(&val.to_string());
301+
src.push_str(",");
302+
}
303+
src.push_str("],");
304+
src.push_str(&test.output.to_string());
305+
src.push_str("),");
306+
}
307+
src.push_str("];");
308+
309+
src.push_str("for (test, expected) in TESTS {");
310+
src.push_str("let output = ");
311+
src.push_str(&function.name);
312+
src.push_str("(");
313+
for (i, arg) in function.args.iter().enumerate() {
314+
src.push_str(&match arg {
315+
Ty::F32 => format!("f32::from_bits(test[{}] as u32)", i),
316+
Ty::F64 => format!("f64::from_bits(test[{}] as u64)", i),
317+
Ty::I32 => format!("test[{}] as i32", i),
318+
Ty::Bool => format!("test[{}] as i32", i),
319+
});
320+
src.push_str(",");
321+
}
322+
src.push_str(");");
323+
src.push_str(match function.ret {
324+
Ty::F32 => "if _eqf(output, f32::from_bits(*expected as u32)).is_ok() { continue }",
325+
Ty::F64 => "if _eq(output, f64::from_bits(*expected as u64)).is_ok() { continue }",
326+
Ty::I32 => "if output as i64 == expected { continue }",
327+
Ty::Bool => unreachable!(),
328+
});
329+
330+
src.push_str(r#"
331+
panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", test, expected, output);
332+
"#);
333+
src.push_str("}");
334+
335+
src.push_str("}");
336+
}
337+
338+
let path = format!("{}/tests.rs", dst);
339+
fs::write(&path, src).unwrap();
340+
341+
// Try to make it somewhat pretty
342+
drop(Command::new("rustfmt").arg(&path).status());
343+
}
344+
}

crates/input-generator/Cargo.toml

Lines changed: 0 additions & 7 deletions
This file was deleted.

0 commit comments

Comments
 (0)