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::env; 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", // These are giving slightly different results compared to musl "lgamma.rs", "lgammaf.rs", "tgamma.rs", "j0.rs", "j0f.rs", "jn.rs", "jnf.rs", "j1.rs", "j1f.rs", ]; 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() { // PowerPC tests are failing on LLVM 13: https://github.com/rust-lang/rust/issues/88520 let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap(); if target_arch == "powerpc64" { return; } 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, rng)); } } fn generate_test(function: &Function, rng: &mut R) -> Test { let mut inputs = function .args .iter() .map(|ty| ty.gen_i64(rng)) .collect::>(); // First argument to this function appears to be a number of // iterations, so passing in massive random numbers causes it to // take forever to execute, so make sure we're not running random // math code until the heat death of the universe. if function.name == "jn" || function.name == "jnf" { inputs[0] &= 0xffff; } Test { inputs, // zero output for now since we'll generate it later outputs: vec![], } } } impl Ty { fn gen_i64(&self, r: &mut R) -> i64 { use std::f32; use std::f64; return match self { Ty::F32 => { if r.gen_range(0, 20) < 1 { let i = *[f32::NAN, f32::INFINITY, f32::NEG_INFINITY] .choose(r) .unwrap(); i.to_bits().into() } else { r.gen::().to_bits().into() } } Ty::F64 => { if r.gen_range(0, 20) < 1 { let i = *[f64::NAN, f64::INFINITY, f64::NEG_INFINITY] .choose(r) .unwrap(); i.to_bits() as i64 } else { 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("("); let (ret, retptr) = match function.name.as_str() { "sincos" | "sincosf" => (None, &function.ret[..]), _ => (Some(&function.ret[0]), &function.ret[1..]), }; for (i, arg) in function.args.iter().enumerate() { src.push_str(&format!("arg{}: {},", i, arg.libc_ty())); } for (i, ret) in retptr.iter().enumerate() { src.push_str(&format!("argret{}: {},", i, ret.libc_pty())); } src.push_str(")"); if let Some(ty) = ret { src.push_str(" -> "); src.push_str(ty.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 retptr.iter().enumerate() { src.push_str(&format!("let mut argret{} = {};", i, arg.default())); } 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 retptr.iter().enumerate() { src.push_str(&format!("&mut argret{},", i)); } src.push_str(");"); if let Some(ty) = &ret { src.push_str(&format!("let output = output{};", ty.to_i64())); src.push_str("result.extend_from_slice(&output.to_le_bytes());"); } for (i, ret) in retptr.iter().enumerate() { src.push_str(&format!( "result.extend_from_slice(&(argret{}{}).to_le_bytes());", i, ret.to_i64(), )); } 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 = (0..f.ret.len()).map(|_| results.next().unwrap()).collect(); } } 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(");"); for (i, ret) in function.ret.iter().enumerate() { let get = if function.ret.len() == 1 { String::new() } else { format!(".{}", i) }; src.push_str(&(match ret { Ty::F32 => format!("if _eqf(output{}, f32::from_bits(expected[{}] as u32)).is_ok() {{ continue }}", get, i), Ty::F64 => format!("if _eq(output{}, f64::from_bits(expected[{}] as u64)).is_ok() {{ continue }}", get, i), Ty::I32 => format!("if output{} as i64 == expected[{}] {{ continue }}", get, i), 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()); } }