1extern crate bindgen; 2extern crate clap; 3extern crate diff; 4#[cfg(feature = "logging")] 5extern crate env_logger; 6extern crate shlex; 7 8use bindgen::{clang_version, Builder}; 9use std::env; 10use std::fs; 11use std::io::{self, BufRead, BufReader, Error, ErrorKind, Read, Write}; 12use std::path::{Path, PathBuf}; 13use std::process; 14use std::sync::Once; 15 16use crate::options::builder_from_flags; 17 18#[path = "../../bindgen-cli/options.rs"] 19mod options; 20 21mod parse_callbacks; 22 23// Run `rustfmt` on the given source string and return a tuple of the formatted 24// bindings, and rustfmt's stderr. 25fn rustfmt(source: String) -> (String, String) { 26 static CHECK_RUSTFMT: Once = Once::new(); 27 28 CHECK_RUSTFMT.call_once(|| { 29 if env::var_os("RUSTFMT").is_some() { 30 return; 31 } 32 33 let mut rustfmt = { 34 let mut p = process::Command::new("rustup"); 35 p.args(["run", "nightly", "rustfmt", "--version"]); 36 p 37 }; 38 39 let have_working_rustfmt = rustfmt 40 .stdout(process::Stdio::null()) 41 .stderr(process::Stdio::null()) 42 .status() 43 .ok() 44 .map_or(false, |status| status.success()); 45 46 if !have_working_rustfmt { 47 panic!( 48 " 49The latest `rustfmt` is required to run the `bindgen` test suite. Install 50`rustfmt` with: 51 52 $ rustup update nightly 53 $ rustup component add rustfmt --toolchain nightly 54" 55 ); 56 } 57 }); 58 59 let mut child = match env::var_os("RUSTFMT") { 60 Some(r) => process::Command::new(r), 61 None => { 62 let mut p = process::Command::new("rustup"); 63 p.args(["run", "nightly", "rustfmt"]); 64 p 65 } 66 }; 67 68 let mut child = child 69 .args([ 70 "--config-path", 71 concat!(env!("CARGO_MANIFEST_DIR"), "/tests/rustfmt.toml"), 72 ]) 73 .stdin(process::Stdio::piped()) 74 .stdout(process::Stdio::piped()) 75 .stderr(process::Stdio::piped()) 76 .spawn() 77 .expect("should spawn `rustup run nightly rustfmt`"); 78 79 let mut stdin = child.stdin.take().unwrap(); 80 let mut stdout = child.stdout.take().unwrap(); 81 let mut stderr = child.stderr.take().unwrap(); 82 83 // Write to stdin in a new thread, so that we can read from stdout on this 84 // thread. This keeps the child from blocking on writing to its stdout which 85 // might block us from writing to its stdin. 86 let stdin_handle = 87 ::std::thread::spawn(move || stdin.write_all(source.as_bytes())); 88 89 // Read stderr on a new thread for similar reasons. 90 let stderr_handle = ::std::thread::spawn(move || { 91 let mut output = vec![]; 92 io::copy(&mut stderr, &mut output) 93 .map(|_| String::from_utf8_lossy(&output).to_string()) 94 }); 95 96 let mut output = vec![]; 97 io::copy(&mut stdout, &mut output).expect("Should copy stdout into vec OK"); 98 99 // Ignore actual rustfmt status because it is often non-zero for trivial 100 // things. 101 let _ = child.wait().expect("should wait on rustfmt child OK"); 102 103 stdin_handle 104 .join() 105 .expect("writer thread should not have panicked") 106 .expect("should have written to child rustfmt's stdin OK"); 107 108 let bindings = String::from_utf8(output) 109 .expect("rustfmt should only emit valid utf-8"); 110 111 let stderr = stderr_handle 112 .join() 113 .expect("stderr reader thread should not have panicked") 114 .expect("should have read child rustfmt's stderr OK"); 115 116 (bindings, stderr) 117} 118 119fn should_overwrite_expected() -> bool { 120 if let Some(var) = env::var_os("BINDGEN_OVERWRITE_EXPECTED") { 121 if var == "1" { 122 return true; 123 } 124 if var != "0" && var != "" { 125 panic!("Invalid value of BINDGEN_OVERWRITE_EXPECTED"); 126 } 127 } 128 false 129} 130 131fn error_diff_mismatch( 132 actual: &str, 133 expected: &str, 134 header: Option<&Path>, 135 filename: &Path, 136) -> Result<(), Error> { 137 println!("diff expected generated"); 138 println!("--- expected: {:?}", filename); 139 if let Some(header) = header { 140 println!("+++ generated from: {:?}", header); 141 } 142 143 for diff in diff::lines(expected, actual) { 144 match diff { 145 diff::Result::Left(l) => println!("-{}", l), 146 diff::Result::Both(l, _) => println!(" {}", l), 147 diff::Result::Right(r) => println!("+{}", r), 148 } 149 } 150 151 if should_overwrite_expected() { 152 // Overwrite the expectation with actual output. 153 let mut expectation_file = fs::File::create(filename)?; 154 expectation_file.write_all(actual.as_bytes())?; 155 } 156 157 if let Some(var) = env::var_os("BINDGEN_TESTS_DIFFTOOL") { 158 //usecase: var = "meld" -> You can hand check differences 159 let name = match filename.components().last() { 160 Some(std::path::Component::Normal(name)) => name, 161 _ => panic!("Why is the header variable so weird?"), 162 }; 163 let actual_result_path = 164 PathBuf::from(env::var("OUT_DIR").unwrap()).join(name); 165 let mut actual_result_file = fs::File::create(&actual_result_path)?; 166 actual_result_file.write_all(actual.as_bytes())?; 167 std::process::Command::new(var) 168 .args([filename, &actual_result_path]) 169 .output()?; 170 } 171 172 Err(Error::new(ErrorKind::Other, "Header and binding differ! Run with BINDGEN_OVERWRITE_EXPECTED=1 in the environment to automatically overwrite the expectation or with BINDGEN_TESTS_DIFFTOOL=meld to do this manually.")) 173} 174 175fn compare_generated_header( 176 header: &Path, 177 builder: BuilderState, 178 check_roundtrip: bool, 179) -> Result<(), Error> { 180 let file_name = header.file_name().ok_or_else(|| { 181 Error::new(ErrorKind::Other, "compare_generated_header expects a file") 182 })?; 183 184 let mut expectation = PathBuf::from(header); 185 expectation.pop(); 186 expectation.pop(); 187 expectation.push("expectations"); 188 expectation.push("tests"); 189 190 let mut looked_at = vec![]; 191 let mut expectation_file; 192 193 // Try more specific expectations first. 194 { 195 let mut expectation = expectation.clone(); 196 197 if cfg!(feature = "testing_only_libclang_9") { 198 expectation.push("libclang-9"); 199 } else if cfg!(feature = "testing_only_libclang_5") { 200 expectation.push("libclang-5"); 201 } else { 202 match clang_version().parsed { 203 None => expectation.push("libclang-9"), 204 Some(version) => { 205 let (maj, min) = version; 206 let version_str = if maj >= 9 { 207 "9".to_owned() 208 } else if maj >= 5 { 209 "5".to_owned() 210 } else if maj >= 4 { 211 "4".to_owned() 212 } else { 213 format!("{}.{}", maj, min) 214 }; 215 expectation.push(format!("libclang-{}", version_str)); 216 } 217 } 218 } 219 220 expectation.push(file_name); 221 expectation.set_extension("rs"); 222 expectation_file = fs::File::open(&expectation).ok(); 223 looked_at.push(expectation); 224 } 225 226 // Try the default path otherwise. 227 if expectation_file.is_none() { 228 expectation.push(file_name); 229 expectation.set_extension("rs"); 230 expectation_file = fs::File::open(&expectation).ok(); 231 looked_at.push(expectation.clone()); 232 } 233 234 let mut expected = String::new(); 235 match expectation_file { 236 Some(f) => { 237 BufReader::new(f).read_to_string(&mut expected)?; 238 } 239 None => panic!( 240 "missing test expectation file and/or 'testing_only_libclang_$VERSION' \ 241 feature for header '{}'; looking for expectation file at '{:?}'", 242 header.display(), 243 looked_at, 244 ), 245 }; 246 247 let (builder, roundtrip_builder) = builder.into_builder(check_roundtrip)?; 248 249 // We skip the generate() error here so we get a full diff below 250 let (actual, rustfmt_stderr) = match builder.generate() { 251 Ok(bindings) => { 252 let actual = bindings.to_string(); 253 rustfmt(actual) 254 } 255 Err(_) => ("/* error generating bindings */\n".into(), "".to_string()), 256 }; 257 println!("{}", rustfmt_stderr); 258 259 let (expected, rustfmt_stderr) = rustfmt(expected); 260 println!("{}", rustfmt_stderr); 261 262 if actual.is_empty() { 263 return Err(Error::new( 264 ErrorKind::Other, 265 "Something's gone really wrong!", 266 )); 267 } 268 269 if actual != expected { 270 println!("{}", rustfmt_stderr); 271 return error_diff_mismatch( 272 &actual, 273 &expected, 274 Some(header), 275 looked_at.last().unwrap(), 276 ); 277 } 278 279 if let Some(roundtrip_builder) = roundtrip_builder { 280 if let Err(e) = 281 compare_generated_header(header, roundtrip_builder, false) 282 { 283 return Err(Error::new(ErrorKind::Other, format!("Checking CLI flags roundtrip errored! You probably need to fix Builder::command_line_flags. {}", e))); 284 } 285 } 286 287 Ok(()) 288} 289 290fn builder() -> Builder { 291 #[cfg(feature = "logging")] 292 let _ = env_logger::try_init(); 293 294 bindgen::builder().disable_header_comment() 295} 296 297struct BuilderState { 298 builder: Builder, 299 parse_callbacks: Option<String>, 300} 301 302impl BuilderState { 303 fn into_builder( 304 self, 305 with_roundtrip_builder: bool, 306 ) -> Result<(Builder, Option<BuilderState>), Error> { 307 let roundtrip_builder = if with_roundtrip_builder { 308 let mut flags = self.builder.command_line_flags(); 309 flags.insert(0, "bindgen".into()); 310 let mut builder = builder_from_flags(flags.into_iter())?.0; 311 if let Some(ref parse_cb) = self.parse_callbacks { 312 builder = 313 builder.parse_callbacks(parse_callbacks::lookup(parse_cb)); 314 } 315 Some(BuilderState { 316 builder, 317 parse_callbacks: self.parse_callbacks, 318 }) 319 } else { 320 None 321 }; 322 Ok((self.builder, roundtrip_builder)) 323 } 324} 325 326fn create_bindgen_builder(header: &Path) -> Result<BuilderState, Error> { 327 #[cfg(feature = "logging")] 328 let _ = env_logger::try_init(); 329 330 let source = fs::File::open(header)?; 331 let reader = BufReader::new(source); 332 333 // Scoop up bindgen-flags from test header 334 let mut flags = Vec::with_capacity(2); 335 let mut parse_callbacks = None; 336 337 for line in reader.lines() { 338 let line = line?; 339 if !line.starts_with("// bindgen") { 340 continue; 341 } 342 343 if line.contains("bindgen-flags: ") { 344 let extra_flags = line 345 .split("bindgen-flags: ") 346 .last() 347 .and_then(shlex::split) 348 .unwrap(); 349 flags.extend(extra_flags.into_iter()); 350 } else if line.contains("bindgen-osx-only") { 351 let prepend_flags = ["--raw-line", "#![cfg(target_os=\"macos\")]"]; 352 flags = prepend_flags 353 .iter() 354 .map(ToString::to_string) 355 .chain(flags) 356 .collect(); 357 } else if line.contains("bindgen-parse-callbacks: ") { 358 let parse_cb = 359 line.split("bindgen-parse-callbacks: ").last().unwrap(); 360 parse_callbacks = Some(parse_cb.to_owned()); 361 } 362 } 363 364 // Different platforms have various different conventions like struct padding, mangling, etc. 365 // We make the default target as x86_64-unknown-linux 366 if flags.iter().all(|flag| !flag.starts_with("--target=")) { 367 if !flags.iter().any(|flag| flag == "--") { 368 flags.push("--".into()); 369 } 370 flags.push("--target=x86_64-unknown-linux".into()); 371 } 372 373 // Fool builder_from_flags() into believing it has real env::args_os... 374 // - add "bindgen" as executable name 0th element 375 // - add header filename as 1st element 376 // - prepend raw lines so they're in the right order for expected output 377 // - append the test header's bindgen flags 378 let header_str = header.to_str().ok_or_else(|| { 379 Error::new(ErrorKind::Other, "Invalid header file name") 380 })?; 381 382 let prepend = [ 383 "bindgen", 384 // We format in `compare_generated_header` ourselves to have a little 385 // more control. 386 "--no-rustfmt-bindings", 387 "--with-derive-default", 388 "--disable-header-comment", 389 "--vtable-generation", 390 header_str, 391 "--raw-line", 392 "", 393 "--raw-line", 394 "#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)]", 395 "--raw-line", 396 "", 397 ]; 398 399 let args = prepend 400 .iter() 401 .map(ToString::to_string) 402 .chain(flags.into_iter()); 403 404 let mut builder = builder_from_flags(args)?.0; 405 if let Some(ref parse_cb) = parse_callbacks { 406 builder = builder.parse_callbacks(parse_callbacks::lookup(parse_cb)); 407 } 408 Ok(BuilderState { 409 builder, 410 parse_callbacks, 411 }) 412} 413 414macro_rules! test_header { 415 ($function:ident, $header:expr) => { 416 #[test] 417 fn $function() { 418 let header = PathBuf::from($header); 419 let result = create_bindgen_builder(&header).and_then(|builder| { 420 let check_roundtrip = 421 env::var_os("BINDGEN_DISABLE_ROUNDTRIP_TEST").is_none(); 422 compare_generated_header(&header, builder, check_roundtrip) 423 }); 424 425 if let Err(err) = result { 426 panic!("{}", err); 427 } 428 } 429 }; 430} 431 432// This file is generated by build.rs 433include!(concat!(env!("OUT_DIR"), "/tests.rs")); 434 435#[test] 436#[cfg_attr(target_os = "windows", ignore)] 437fn test_clang_env_args() { 438 std::env::set_var( 439 "BINDGEN_EXTRA_CLANG_ARGS", 440 "-D_ENV_ONE=1 -D_ENV_TWO=\"2 -DNOT_THREE=1\"", 441 ); 442 let actual = builder() 443 .disable_header_comment() 444 .header_contents( 445 "test.hpp", 446 "#ifdef _ENV_ONE\nextern const int x[] = { 42 };\n#endif\n\ 447 #ifdef _ENV_TWO\nextern const int y[] = { 42 };\n#endif\n\ 448 #ifdef NOT_THREE\nextern const int z[] = { 42 };\n#endif\n", 449 ) 450 .generate() 451 .unwrap() 452 .to_string(); 453 454 let (actual, stderr) = rustfmt(actual); 455 println!("{}", stderr); 456 457 let (expected, _) = rustfmt( 458 "extern \"C\" { 459 pub static x: [::std::os::raw::c_int; 1usize]; 460} 461extern \"C\" { 462 pub static y: [::std::os::raw::c_int; 1usize]; 463} 464" 465 .to_string(), 466 ); 467 468 assert_eq!(expected, actual); 469} 470 471#[test] 472fn test_header_contents() { 473 let actual = builder() 474 .disable_header_comment() 475 .header_contents("test.h", "int foo(const char* a);") 476 .clang_arg("--target=x86_64-unknown-linux") 477 .generate() 478 .unwrap() 479 .to_string(); 480 481 let (actual, stderr) = rustfmt(actual); 482 println!("{}", stderr); 483 484 let (expected, _) = rustfmt( 485 "extern \"C\" { 486 pub fn foo(a: *const ::std::os::raw::c_char) -> ::std::os::raw::c_int; 487} 488" 489 .to_string(), 490 ); 491 492 assert_eq!(expected, actual); 493} 494 495#[test] 496fn test_multiple_header_calls_in_builder() { 497 let actual = builder() 498 .header(concat!( 499 env!("CARGO_MANIFEST_DIR"), 500 "/tests/headers/func_ptr.h" 501 )) 502 .header(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/headers/char.h")) 503 .clang_arg("--target=x86_64-unknown-linux") 504 .generate() 505 .unwrap() 506 .to_string(); 507 508 let (actual, stderr) = rustfmt(actual); 509 println!("{}", stderr); 510 511 let expected_filename = concat!( 512 env!("CARGO_MANIFEST_DIR"), 513 "/tests/expectations/tests/test_multiple_header_calls_in_builder.rs" 514 ); 515 let expected = include_str!(concat!( 516 env!("CARGO_MANIFEST_DIR"), 517 "/tests/expectations/tests/test_multiple_header_calls_in_builder.rs" 518 )); 519 let (expected, _) = rustfmt(expected.to_string()); 520 521 if actual != expected { 522 println!("Generated bindings differ from expected!"); 523 error_diff_mismatch( 524 &actual, 525 &expected, 526 None, 527 Path::new(expected_filename), 528 ) 529 .unwrap(); 530 } 531} 532 533#[test] 534fn test_multiple_header_contents() { 535 let actual = builder() 536 .header_contents("test.h", "int foo(const char* a);") 537 .header_contents("test2.h", "float foo2(const char* b);") 538 .clang_arg("--target=x86_64-unknown-linux") 539 .generate() 540 .unwrap() 541 .to_string(); 542 543 let (actual, stderr) = rustfmt(actual); 544 println!("{}", stderr); 545 546 let (expected, _) = rustfmt( 547 "extern \"C\" { 548 pub fn foo2(b: *const ::std::os::raw::c_char) -> f32; 549} 550extern \"C\" { 551 pub fn foo(a: *const ::std::os::raw::c_char) -> ::std::os::raw::c_int; 552} 553" 554 .to_string(), 555 ); 556 557 assert_eq!(expected, actual); 558} 559 560#[test] 561fn test_mixed_header_and_header_contents() { 562 let actual = builder() 563 .header(concat!( 564 env!("CARGO_MANIFEST_DIR"), 565 "/tests/headers/func_ptr.h" 566 )) 567 .header(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/headers/char.h")) 568 .header_contents("test.h", "int bar(const char* a);") 569 .header_contents("test2.h", "float bar2(const char* b);") 570 .clang_arg("--target=x86_64-unknown-linux") 571 .generate() 572 .unwrap() 573 .to_string(); 574 575 let (actual, stderr) = rustfmt(actual); 576 println!("{}", stderr); 577 578 let expected_filename = concat!( 579 env!("CARGO_MANIFEST_DIR"), 580 "/tests/expectations/tests/test_mixed_header_and_header_contents.rs" 581 ); 582 let expected = include_str!(concat!( 583 env!("CARGO_MANIFEST_DIR"), 584 "/tests/expectations/tests/test_mixed_header_and_header_contents.rs" 585 )); 586 let (expected, _) = rustfmt(expected.to_string()); 587 if expected != actual { 588 error_diff_mismatch( 589 &actual, 590 &expected, 591 None, 592 Path::new(expected_filename), 593 ) 594 .unwrap(); 595 } 596} 597 598#[test] 599// Doesn't support executing sh file on Windows. 600// We may want to implement it in Rust so that we support all systems. 601#[cfg(not(target_os = "windows"))] 602fn no_system_header_includes() { 603 use std::process::Command; 604 assert!(Command::new("../ci/no-includes.sh") 605 .current_dir(env!("CARGO_MANIFEST_DIR")) 606 .spawn() 607 .expect("should spawn ../ci/no-includes.sh OK") 608 .wait() 609 .expect("should wait for ../ci/no-includes OK") 610 .success()); 611} 612 613#[test] 614fn emit_depfile() { 615 let header = PathBuf::from("tests/headers/enum-default-rust.h"); 616 let expected_depfile = PathBuf::from(env!("CARGO_MANIFEST_DIR")) 617 .join("tests") 618 .join("expectations") 619 .join("tests") 620 .join("enum-default-rust.d"); 621 let observed_depfile = tempfile::NamedTempFile::new().unwrap(); 622 let mut builder = create_bindgen_builder(&header).unwrap(); 623 builder.builder = builder.builder.depfile( 624 "tests/expectations/tests/enum-default-rust.rs", 625 observed_depfile.path(), 626 ); 627 628 let check_roundtrip = 629 env::var_os("BINDGEN_DISABLE_ROUNDTRIP_TEST").is_none(); 630 let (builder, _roundtrip_builder) = 631 builder.into_builder(check_roundtrip).unwrap(); 632 let _bindings = builder.generate().unwrap(); 633 634 let observed = std::fs::read_to_string(observed_depfile).unwrap(); 635 let expected = std::fs::read_to_string(expected_depfile).unwrap(); 636 assert_eq!(observed.trim(), expected.trim()); 637} 638 639#[test] 640fn dump_preprocessed_input() { 641 let arg_keyword = 642 concat!(env!("CARGO_MANIFEST_DIR"), "/tests/headers/arg_keyword.hpp"); 643 let empty_layout = concat!( 644 env!("CARGO_MANIFEST_DIR"), 645 "/tests/headers/cpp-empty-layout.hpp" 646 ); 647 648 builder() 649 .header(arg_keyword) 650 .header(empty_layout) 651 .dump_preprocessed_input() 652 .expect("should dump preprocessed input"); 653 654 fn slurp(p: &str) -> String { 655 let mut contents = String::new(); 656 let mut file = fs::File::open(p).unwrap(); 657 file.read_to_string(&mut contents).unwrap(); 658 contents 659 } 660 661 let bindgen_ii = slurp("__bindgen.ii"); 662 let arg_keyword = slurp(arg_keyword); 663 let empty_layout = slurp(empty_layout); 664 665 assert!( 666 bindgen_ii.contains(&arg_keyword), 667 "arg_keyword.hpp is in the preprocessed file" 668 ); 669 assert!( 670 bindgen_ii.contains(&empty_layout), 671 "cpp-empty-layout.hpp is in the preprocessed file" 672 ); 673} 674 675#[test] 676fn allowlist_warnings() { 677 let header = concat!( 678 env!("CARGO_MANIFEST_DIR"), 679 "/tests/headers/allowlist_warnings.h" 680 ); 681 682 let bindings = builder() 683 .header(header) 684 .allowlist_function("doesnt_match_anything") 685 .generate() 686 .expect("unable to generate bindings"); 687 688 assert_eq!(1, bindings.warnings().len()); 689} 690 691fn build_flags_output_helper(builder: &bindgen::Builder) { 692 let mut command_line_flags = builder.command_line_flags(); 693 command_line_flags.insert(0, "bindgen".to_string()); 694 695 let flags_quoted: Vec<String> = command_line_flags 696 .iter() 697 .map(|x| format!("{}", shlex::quote(x))) 698 .collect(); 699 let flags_str = flags_quoted.join(" "); 700 println!("{}", flags_str); 701 702 let (builder, _output, _verbose) = 703 crate::options::builder_from_flags(command_line_flags.into_iter()) 704 .unwrap(); 705 builder.generate().expect("failed to generate bindings"); 706} 707 708#[test] 709fn commandline_multiple_headers() { 710 let bindings = bindgen::Builder::default() 711 .header("tests/headers/char.h") 712 .header("tests/headers/func_ptr.h") 713 .header("tests/headers/16-byte-alignment.h"); 714 build_flags_output_helper(&bindings); 715} 716 717#[test] 718fn test_wrap_static_fns() { 719 // This test is for testing diffs of the generated C source and header files 720 // TODO: If another such feature is added, convert this test into a more generic 721 // test that looks at `tests/headers/generated` directory. 722 let expect_path = PathBuf::from("tests/expectations/tests/generated") 723 .join("wrap_static_fns"); 724 println!("In path is ::: {}", expect_path.display()); 725 726 let generated_path = 727 PathBuf::from(env::var("OUT_DIR").unwrap()).join("wrap_static_fns"); 728 println!("Out path is ::: {}", generated_path.display()); 729 730 let _bindings = Builder::default() 731 .header("tests/headers/wrap-static-fns.h") 732 .wrap_static_fns(true) 733 .wrap_static_fns_path(generated_path.display().to_string()) 734 .generate() 735 .expect("Failed to generate bindings"); 736 737 let expected_c = fs::read_to_string(expect_path.with_extension("c")) 738 .expect("Could not read generated wrap_static_fns.c"); 739 740 let actual_c = fs::read_to_string(generated_path.with_extension("c")) 741 .expect("Could not read actual wrap_static_fns.c"); 742 743 if expected_c != actual_c { 744 error_diff_mismatch( 745 &actual_c, 746 &expected_c, 747 None, 748 &expect_path.with_extension("c"), 749 ) 750 .unwrap(); 751 } 752} 753