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] = &[ "fenv.rs", "sincos.rs", // more than 1 result "sincosf.rs", // more than 1 result "jn.rs", // passed, but very slow "jnf.rs", // passed, but very slow ]; struct Function { name: String, args: Vec, ret: Vec, tests: Vec, } enum Ty { F32, F64, I32, Bool, } struct Test { inputs: Vec, outputs: Vec, } 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); //panic!("Boo"); // ... 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_retty(tail.replace("{", "").trim()); 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 parse_retty(s: &str) -> Vec { match s { "(f32, f32)" => vec![Ty::F32, Ty::F32], "(f32, i32)" => vec![Ty::F32, Ty::I32], "(f64, f64)" => vec![Ty::F64, Ty::F64], "(f64, i32)" => vec![Ty::F64, Ty::I32], other => vec![parse_ty(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, outputs: vec![], } } } 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 libc_pty(&self) -> &'static str { match self { Ty::F32 => "*mut f32", Ty::F64 => "*mut f64", Ty::I32 => "*mut i32", Ty::Bool => "*mut i32", } } fn default(&self) -> &'static str { match self { Ty::F32 => "0_f32", Ty::F64 => "0_f64", Ty::I32 => "0_i32", Ty::Bool => "false", } } fn to_i64(&self) -> &'static str { match self { Ty::F32 => ".to_bits() as i64", Ty::F64 => ".to_bits() as i64", Ty::I32 => " as i64", Ty::Bool => " as i64", } } } 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())); } for (i, ret) in function.ret.iter().skip(1).enumerate() { src.push_str(&format!("argret{}: {},", i, ret.libc_pty())); } src.push_str(") -> "); src.push_str(function.ret[0].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 {"); for (i, arg) in function.ret.iter().skip(1).enumerate() { src.push_str(&format!("let mut argret{} = {};", i, arg.default())); src.push_str(&format!( "let argret_ptr{0} = &mut argret{0} as *mut {1};", i, arg.libc_ty() )); } 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(","); } for (i, _) in function.ret.iter().skip(1).enumerate() { src.push_str(&format!("argret_ptr{},", i)); } src.push_str(");"); src.push_str(&format!("let output = output{};", function.ret[0].to_i64())); src.push_str("result.extend_from_slice(&output.to_le_bytes());"); for (i, ret) in function.ret.iter().skip(1).enumerate() { src.push_str(&format!("let output{0} = argret{0}{1};", i, ret.to_i64())); src.push_str(&format!( "result.extend_from_slice(&output{}.to_le_bytes());", i )); } 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 f in functions.iter_mut() { for test in f.tests.iter_mut() { test.outputs = vec![results.next().unwrap()]; for _ in f.ret.iter().skip(1) { test.outputs.push(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(), function.ret.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("["); for val in test.outputs.iter() { src.push_str(&val.to_string()); src.push_str(","); } src.push_str("],"); 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(");"); if function.ret.len() > 1 { for (i, ret) in function.ret.iter().enumerate() { src.push_str(&(match ret { Ty::F32 => format!("if _eqf(output.{0}, f32::from_bits(expected[{0}] as u32)).is_ok() {{ continue }}", i), Ty::F64 => format!("if _eq(output.{0}, f64::from_bits(expected[{0}] as u64)).is_ok() {{ continue }}", i), Ty::I32 => format!("if output.{0} as i64 == expected[{0}] {{ continue }}", i), Ty::Bool => unreachable!(), })); } } else { src.push_str(match function.ret[0] { Ty::F32 => { "if _eqf(output, f32::from_bits(expected[0] as u32)).is_ok() { continue }" } Ty::F64 => { "if _eq(output, f64::from_bits(expected[0] as u64)).is_ok() { continue }" } Ty::I32 => "if output as i64 == expected[0] { 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()); } }