|
| 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 | +} |
0 commit comments