use std::env; fn main() { println!("cargo:rerun-if-changed=build.rs"); #[cfg(feature = "musl-reference-tests")] musl_reference_tests::generate(); if !cfg!(feature = "checked") { let lvl = env::var("OPT_LEVEL").unwrap(); if lvl != "0" { println!("cargo:rustc-cfg=assert_no_panic"); } } } #[cfg(feature = "musl-reference-tests")] mod musl_reference_tests { use rand::seq::SliceRandom; use rand::Rng; use std::fs; use std::process::Command; // Number of tests to generate for each function const NTESTS: usize = 500; // These files are all internal functions or otherwise miscellaneous, not // defining a function we want to test. const IGNORED_FILES: &[&str] = &[ "expo2.rs", "fenv.rs", "k_cos.rs", "k_cosf.rs", "k_expo2.rs", "k_expo2f.rs", "k_sin.rs", "k_sinf.rs", "k_tan.rs", "k_tanf.rs", "mod.rs", "rem_pio2.rs", "rem_pio2_large.rs", "rem_pio2f.rs", "remquo.rs", "remquof.rs", "lgamma.rs", // lgamma passed, lgamma_r has more than 1 result "lgammaf.rs", // lgammaf passed, lgammaf_r has more than 1 result "frexp.rs", // more than 1 result "frexpf.rs", // more than 1 result "sincos.rs", // more than 1 result "sincosf.rs", // more than 1 result "modf.rs", // more than 1 result "modff.rs", // more than 1 result "asinef.rs", // not exists "jn.rs", // passed, but very slow "jnf.rs", // passed, but very slow ]; struct Function { name: String, args: Vec, ret: Ty, tests: Vec, } enum Ty { F32, F64, I32, Bool, } struct Test { inputs: Vec, output: i64, } pub fn generate() { let files = fs::read_dir("src/math") .unwrap() .map(|f| f.unwrap().path()) .collect::>(); let mut math = Vec::new(); for file in files { if IGNORED_FILES.iter().any(|f| file.ends_with(f)) { continue; } println!("generating musl reference tests in {:?}", file); let contents = fs::read_to_string(file).unwrap(); let mut functions = contents.lines().filter(|f| f.starts_with("pub fn")); while let Some(function_to_test) = functions.next() { math.push(parse(function_to_test)); } } // Generate a bunch of random inputs for each function. This will // attempt to generate a good set of uniform test cases for exercising // all the various functionality. generate_random_tests(&mut math, &mut rand::thread_rng()); // After we have all our inputs, use the x86_64-unknown-linux-musl // target to generate the expected output. generate_test_outputs(&mut math); // ... and now that we have both inputs and expected outputs, do a bunch // of codegen to create the unit tests which we'll actually execute. generate_unit_tests(&math); } /// A "poor man's" parser for the signature of a function fn parse(s: &str) -> Function { let s = eat(s, "pub fn "); let pos = s.find('(').unwrap(); let name = &s[..pos]; let s = &s[pos + 1..]; let end = s.find(')').unwrap(); let args = s[..end] .split(',') .map(|arg| { let colon = arg.find(':').unwrap(); parse_ty(arg[colon + 1..].trim()) }) .collect::>(); let tail = &s[end + 1..]; let tail = eat(tail, " -> "); let ret = parse_ty(tail.trim().split(' ').next().unwrap()); return Function { name: name.to_string(), args, ret, tests: Vec::new(), }; fn parse_ty(s: &str) -> Ty { match s { "f32" => Ty::F32, "f64" => Ty::F64, "i32" => Ty::I32, "bool" => Ty::Bool, other => panic!("unknown type `{}`", other), } } fn eat<'a>(s: &'a str, prefix: &str) -> &'a str { if s.starts_with(prefix) { &s[prefix.len()..] } else { panic!("{:?} didn't start with {:?}", s, prefix) } } } fn generate_random_tests(functions: &mut [Function], rng: &mut R) { for function in functions { for _ in 0..NTESTS { function.tests.push(generate_test(&function.args, rng)); } } fn generate_test(args: &[Ty], rng: &mut R) -> Test { let inputs = args.iter().map(|ty| ty.gen_i64(rng)).collect(); // zero output for now since we'll generate it later Test { inputs, output: 0 } } } impl Ty { fn gen_i64(&self, r: &mut R) -> i64 { match self { Ty::F32 => r.gen::().to_bits().into(), Ty::F64 => r.gen::().to_bits() as i64, Ty::I32 => { if r.gen_range(0, 10) < 1 { let i = *[i32::max_value(), 0, i32::min_value()].choose(r).unwrap(); i.into() } else { r.gen::().into() } } Ty::Bool => r.gen::() as i64, } } fn libc_ty(&self) -> &'static str { match self { Ty::F32 => "f32", Ty::F64 => "f64", Ty::I32 => "i32", Ty::Bool => "i32", } } } fn generate_test_outputs(functions: &mut [Function]) { let mut src = String::new(); let dst = std::env::var("OUT_DIR").unwrap(); // Generate a program which will run all tests with all inputs in // `functions`. This program will write all outputs to stdout (in a // binary format). src.push_str("use std::io::Write;"); src.push_str("fn main() {"); src.push_str("let mut result = Vec::new();"); for function in functions.iter_mut() { src.push_str("unsafe {"); src.push_str("extern { fn "); src.push_str(&function.name); src.push_str("("); for (i, arg) in function.args.iter().enumerate() { src.push_str(&format!("arg{}: {},", i, arg.libc_ty())); } src.push_str(") -> "); src.push_str(function.ret.libc_ty()); src.push_str("; }"); src.push_str(&format!("static TESTS: &[[i64; {}]]", function.args.len())); src.push_str(" = &["); for test in function.tests.iter() { src.push_str("["); for val in test.inputs.iter() { src.push_str(&val.to_string()); src.push_str(","); } src.push_str("],"); } src.push_str("];"); src.push_str("for test in TESTS {"); src.push_str("let output = "); src.push_str(&function.name); src.push_str("("); for (i, arg) in function.args.iter().enumerate() { src.push_str(&match arg { Ty::F32 => format!("f32::from_bits(test[{}] as u32)", i), Ty::F64 => format!("f64::from_bits(test[{}] as u64)", i), Ty::I32 => format!("test[{}] as i32", i), Ty::Bool => format!("test[{}] as i32", i), }); src.push_str(","); } src.push_str(");"); src.push_str("let output = "); src.push_str(match function.ret { Ty::F32 => "output.to_bits() as i64", Ty::F64 => "output.to_bits() as i64", Ty::I32 => "output as i64", Ty::Bool => "output as i64", }); src.push_str(";"); src.push_str("result.extend_from_slice(&output.to_le_bytes());"); src.push_str("}"); src.push_str("}"); } src.push_str("std::io::stdout().write_all(&result).unwrap();"); src.push_str("}"); let path = format!("{}/gen.rs", dst); fs::write(&path, src).unwrap(); // Make it somewhat pretty if something goes wrong drop(Command::new("rustfmt").arg(&path).status()); // Compile and execute this tests for the musl target, assuming we're an // x86_64 host effectively. let status = Command::new("rustc") .current_dir(&dst) .arg(&path) .arg("--target=x86_64-unknown-linux-musl") .status() .unwrap(); assert!(status.success()); let output = Command::new("./gen").current_dir(&dst).output().unwrap(); assert!(output.status.success()); assert!(output.stderr.is_empty()); // Map all the output bytes back to an `i64` and then shove it all into // the expected results. let mut results = output.stdout.chunks_exact(8).map(|buf| { let mut exact = [0; 8]; exact.copy_from_slice(buf); i64::from_le_bytes(exact) }); for test in functions.iter_mut().flat_map(|f| f.tests.iter_mut()) { test.output = results.next().unwrap(); } assert!(results.next().is_none()); } /// Codegens a file which has a ton of `#[test]` annotations for all the /// tests that we generated above. fn generate_unit_tests(functions: &[Function]) { let mut src = String::new(); let dst = std::env::var("OUT_DIR").unwrap(); for function in functions { src.push_str("#[test]"); src.push_str("fn "); src.push_str(&function.name); src.push_str("_matches_musl() {"); src.push_str(&format!( "static TESTS: &[([i64; {}], i64)]", function.args.len() )); src.push_str(" = &["); for test in function.tests.iter() { src.push_str("(["); for val in test.inputs.iter() { src.push_str(&val.to_string()); src.push_str(","); } src.push_str("],"); src.push_str(&test.output.to_string()); src.push_str("),"); } src.push_str("];"); src.push_str("for (test, expected) in TESTS {"); src.push_str("let output = "); src.push_str(&function.name); src.push_str("("); for (i, arg) in function.args.iter().enumerate() { src.push_str(&match arg { Ty::F32 => format!("f32::from_bits(test[{}] as u32)", i), Ty::F64 => format!("f64::from_bits(test[{}] as u64)", i), Ty::I32 => format!("test[{}] as i32", i), Ty::Bool => format!("test[{}] as i32", i), }); src.push_str(","); } src.push_str(");"); src.push_str(match function.ret { Ty::F32 => "if _eqf(output, f32::from_bits(*expected as u32)).is_ok() { continue }", Ty::F64 => "if _eq(output, f64::from_bits(*expected as u64)).is_ok() { continue }", Ty::I32 => "if output as i64 == *expected { continue }", Ty::Bool => unreachable!(), }); src.push_str( r#" panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", test, expected, output); "#, ); src.push_str("}"); src.push_str("}"); } let path = format!("{}/musl-tests.rs", dst); fs::write(&path, src).unwrap(); // Try to make it somewhat pretty drop(Command::new("rustfmt").arg(&path).status()); } }