extern crate bindgen; extern crate clap; extern crate diff; #[cfg(feature = "logging")] extern crate env_logger; extern crate shlex; use bindgen::{clang_version, Builder}; use std::env; use std::fs; use std::io::{self, BufRead, BufReader, Error, ErrorKind, Read, Write}; use std::path::{Path, PathBuf}; use std::process; use std::sync::Once; #[path = "../src/options.rs"] mod options; use crate::options::builder_from_flags; mod parse_callbacks; // Run `rustfmt` on the given source string and return a tuple of the formatted // bindings, and rustfmt's stderr. fn rustfmt(source: String) -> (String, String) { static CHECK_RUSTFMT: Once = Once::new(); CHECK_RUSTFMT.call_once(|| { if env::var_os("RUSTFMT").is_some() { return; } let mut rustfmt = { let mut p = process::Command::new("rustup"); p.args(&["run", "nightly", "rustfmt", "--version"]); p }; let have_working_rustfmt = rustfmt .stdout(process::Stdio::null()) .stderr(process::Stdio::null()) .status() .ok() .map_or(false, |status| status.success()); if !have_working_rustfmt { panic!( " The latest `rustfmt` is required to run the `bindgen` test suite. Install `rustfmt` with: $ rustup update nightly $ rustup component add rustfmt --toolchain nightly " ); } }); let mut child = match env::var_os("RUSTFMT") { Some(r) => process::Command::new(r), None => { let mut p = process::Command::new("rustup"); p.args(&["run", "nightly", "rustfmt"]); p } }; let mut child = child .args(&[ "--config-path", concat!(env!("CARGO_MANIFEST_DIR"), "/tests/rustfmt.toml"), ]) .stdin(process::Stdio::piped()) .stdout(process::Stdio::piped()) .stderr(process::Stdio::piped()) .spawn() .expect("should spawn `rustup run nightly rustfmt`"); let mut stdin = child.stdin.take().unwrap(); let mut stdout = child.stdout.take().unwrap(); let mut stderr = child.stderr.take().unwrap(); // Write to stdin in a new thread, so that we can read from stdout on this // thread. This keeps the child from blocking on writing to its stdout which // might block us from writing to its stdin. let stdin_handle = ::std::thread::spawn(move || stdin.write_all(source.as_bytes())); // Read stderr on a new thread for similar reasons. let stderr_handle = ::std::thread::spawn(move || { let mut output = vec![]; io::copy(&mut stderr, &mut output) .map(|_| String::from_utf8_lossy(&output).to_string()) }); let mut output = vec![]; io::copy(&mut stdout, &mut output).expect("Should copy stdout into vec OK"); // Ignore actual rustfmt status because it is often non-zero for trivial // things. let _ = child.wait().expect("should wait on rustfmt child OK"); stdin_handle .join() .expect("writer thread should not have panicked") .expect("should have written to child rustfmt's stdin OK"); let bindings = String::from_utf8(output) .expect("rustfmt should only emit valid utf-8"); let stderr = stderr_handle .join() .expect("stderr reader thread should not have panicked") .expect("should have read child rustfmt's stderr OK"); (bindings, stderr) } fn should_overwrite_expected() -> bool { if let Some(var) = env::var_os("BINDGEN_OVERWRITE_EXPECTED") { if var == "1" { return true; } if var != "0" && var != "" { panic!("Invalid value of BINDGEN_OVERWRITE_EXPECTED"); } } false } fn error_diff_mismatch( actual: &str, expected: &str, header: Option<&Path>, filename: &Path, ) -> Result<(), Error> { println!("diff expected generated"); println!("--- expected: {:?}", filename); if let Some(header) = header { println!("+++ generated from: {:?}", header); } for diff in diff::lines(&expected, &actual) { match diff { diff::Result::Left(l) => println!("-{}", l), diff::Result::Both(l, _) => println!(" {}", l), diff::Result::Right(r) => println!("+{}", r), } } if should_overwrite_expected() { // Overwrite the expectation with actual output. let mut expectation_file = fs::File::create(filename)?; expectation_file.write_all(actual.as_bytes())?; } if let Some(var) = env::var_os("BINDGEN_TESTS_DIFFTOOL") { //usecase: var = "meld" -> You can hand check differences let name = match filename.components().last() { Some(std::path::Component::Normal(name)) => name, _ => panic!("Why is the header variable so weird?"), }; let actual_result_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join(name); let mut actual_result_file = fs::File::create(&actual_result_path)?; actual_result_file.write_all(actual.as_bytes())?; std::process::Command::new(var) .args(&[filename, &actual_result_path]) .output()?; } return 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.")); } fn compare_generated_header( header: &Path, builder: BuilderState, check_roundtrip: bool, ) -> Result<(), Error> { let file_name = header.file_name().ok_or_else(|| { Error::new(ErrorKind::Other, "compare_generated_header expects a file") })?; let mut expectation = PathBuf::from(header); expectation.pop(); expectation.pop(); expectation.push("expectations"); expectation.push("tests"); let mut looked_at = vec![]; let mut expectation_file; // Try more specific expectations first. { let mut expectation = expectation.clone(); if cfg!(feature = "testing_only_libclang_9") { expectation.push("libclang-9"); } else if cfg!(feature = "testing_only_libclang_5") { expectation.push("libclang-5"); } else { match clang_version().parsed { None => expectation.push("libclang-9"), Some(version) => { let (maj, min) = version; let version_str = if maj >= 9 { "9".to_owned() } else if maj >= 5 { "5".to_owned() } else if maj >= 4 { "4".to_owned() } else { format!("{}.{}", maj, min) }; expectation.push(format!("libclang-{}", version_str)); } } } expectation.push(file_name); expectation.set_extension("rs"); expectation_file = fs::File::open(&expectation).ok(); looked_at.push(expectation); } // Try the default path otherwise. if expectation_file.is_none() { expectation.push(file_name); expectation.set_extension("rs"); expectation_file = fs::File::open(&expectation).ok(); looked_at.push(expectation.clone()); } let mut expected = String::new(); match expectation_file { Some(f) => { BufReader::new(f).read_to_string(&mut expected)?; } None => panic!( "missing test expectation file and/or 'testing_only_libclang_$VERSION' \ feature for header '{}'; looking for expectation file at '{:?}'", header.display(), looked_at, ), }; let (builder, roundtrip_builder) = builder.into_builder(check_roundtrip)?; // We skip the generate() error here so we get a full diff below let (actual, rustfmt_stderr) = match builder.generate() { Ok(bindings) => { let actual = bindings.to_string(); rustfmt(actual) } Err(_) => ("".to_string(), "".to_string()), }; println!("{}", rustfmt_stderr); let (expected, rustfmt_stderr) = rustfmt(expected); println!("{}", rustfmt_stderr); if actual.is_empty() { return Err(Error::new( ErrorKind::Other, "Something's gone really wrong!", )); } if actual != expected { println!("{}", rustfmt_stderr); return error_diff_mismatch( &actual, &expected, Some(header), looked_at.last().unwrap(), ); } if let Some(roundtrip_builder) = roundtrip_builder { if let Err(e) = compare_generated_header(header, roundtrip_builder, false) { return Err(Error::new(ErrorKind::Other, format!("Checking CLI flags roundtrip errored! You probably need to fix Builder::command_line_flags. {}", e))); } } Ok(()) } fn builder() -> Builder { #[cfg(feature = "logging")] let _ = env_logger::try_init(); bindgen::builder().disable_header_comment() } struct BuilderState { builder: Builder, parse_callbacks: Option, } impl BuilderState { fn into_builder( self, with_roundtrip_builder: bool, ) -> Result<(Builder, Option), Error> { let roundtrip_builder = if with_roundtrip_builder { let mut flags = self.builder.command_line_flags(); flags.insert(0, "bindgen".into()); let mut builder = builder_from_flags(flags.into_iter())?.0; if let Some(ref parse_cb) = self.parse_callbacks { builder = builder.parse_callbacks(parse_callbacks::lookup(parse_cb)); } Some(BuilderState { builder, parse_callbacks: self.parse_callbacks, }) } else { None }; Ok((self.builder, roundtrip_builder)) } } fn create_bindgen_builder(header: &Path) -> Result { #[cfg(feature = "logging")] let _ = env_logger::try_init(); let source = fs::File::open(header)?; let reader = BufReader::new(source); // Scoop up bindgen-flags from test header let mut flags = Vec::with_capacity(2); let mut parse_callbacks = None; for line in reader.lines() { let line = line?; if !line.starts_with("// bindgen") { continue; } if line.contains("bindgen-flags: ") { let extra_flags = line .split("bindgen-flags: ") .last() .and_then(shlex::split) .unwrap(); flags.extend(extra_flags.into_iter()); } else if line.contains("bindgen-osx-only") { let prepend_flags = ["--raw-line", "#![cfg(target_os=\"macos\")]"]; flags = prepend_flags .iter() .map(ToString::to_string) .chain(flags) .collect(); } else if line.contains("bindgen-parse-callbacks: ") { let parse_cb = line.split("bindgen-parse-callbacks: ").last().unwrap(); parse_callbacks = Some(parse_cb.to_owned()); } } // Different platforms have various different conventions like struct padding, mangling, etc. // We make the default target as x86_64-unknown-linux if flags.iter().all(|flag| !flag.starts_with("--target=")) { if !flags.iter().any(|flag| flag == "--") { flags.push("--".into()); } flags.push("--target=x86_64-unknown-linux".into()); } // Fool builder_from_flags() into believing it has real env::args_os... // - add "bindgen" as executable name 0th element // - add header filename as 1st element // - prepend raw lines so they're in the right order for expected output // - append the test header's bindgen flags let header_str = header.to_str().ok_or_else(|| { Error::new(ErrorKind::Other, "Invalid header file name") })?; let prepend = [ "bindgen", // We format in `compare_generated_header` ourselves to have a little // more control. "--no-rustfmt-bindings", "--with-derive-default", "--disable-header-comment", "--vtable-generation", header_str, "--raw-line", "", "--raw-line", "#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)]", "--raw-line", "", ]; let args = prepend .iter() .map(ToString::to_string) .chain(flags.into_iter()); let mut builder = builder_from_flags(args)?.0; if let Some(ref parse_cb) = parse_callbacks { builder = builder.parse_callbacks(parse_callbacks::lookup(parse_cb)); } Ok(BuilderState { builder, parse_callbacks, }) } macro_rules! test_header { ($function:ident, $header:expr) => { #[test] fn $function() { let header = PathBuf::from($header); let result = create_bindgen_builder(&header).and_then(|builder| { let check_roundtrip = env::var_os("BINDGEN_DISABLE_ROUNDTRIP_TEST").is_none(); compare_generated_header(&header, builder, check_roundtrip) }); if let Err(err) = result { panic!("{}", err); } } }; } // This file is generated by build.rs include!(concat!(env!("OUT_DIR"), "/tests.rs")); #[test] #[cfg_attr(target_os = "windows", ignore)] fn test_clang_env_args() { std::env::set_var( "BINDGEN_EXTRA_CLANG_ARGS", "-D_ENV_ONE=1 -D_ENV_TWO=\"2 -DNOT_THREE=1\"", ); let actual = builder() .disable_header_comment() .header_contents( "test.hpp", "#ifdef _ENV_ONE\nextern const int x[] = { 42 };\n#endif\n\ #ifdef _ENV_TWO\nextern const int y[] = { 42 };\n#endif\n\ #ifdef NOT_THREE\nextern const int z[] = { 42 };\n#endif\n", ) .generate() .unwrap() .to_string(); let (actual, stderr) = rustfmt(actual); println!("{}", stderr); let (expected, _) = rustfmt( "extern \"C\" { pub static x: [::std::os::raw::c_int; 1usize]; } extern \"C\" { pub static y: [::std::os::raw::c_int; 1usize]; } " .to_string(), ); assert_eq!(expected, actual); } #[test] fn test_header_contents() { let actual = builder() .disable_header_comment() .header_contents("test.h", "int foo(const char* a);") .clang_arg("--target=x86_64-unknown-linux") .generate() .unwrap() .to_string(); let (actual, stderr) = rustfmt(actual); println!("{}", stderr); let (expected, _) = rustfmt( "extern \"C\" { pub fn foo(a: *const ::std::os::raw::c_char) -> ::std::os::raw::c_int; } " .to_string(), ); assert_eq!(expected, actual); } #[test] fn test_multiple_header_calls_in_builder() { let actual = builder() .header(concat!( env!("CARGO_MANIFEST_DIR"), "/tests/headers/func_ptr.h" )) .header(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/headers/char.h")) .clang_arg("--target=x86_64-unknown-linux") .generate() .unwrap() .to_string(); let (actual, stderr) = rustfmt(actual); println!("{}", stderr); let expected_filename = concat!( env!("CARGO_MANIFEST_DIR"), "/tests/expectations/tests/test_multiple_header_calls_in_builder.rs" ); let expected = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/tests/expectations/tests/test_multiple_header_calls_in_builder.rs" )); let (expected, _) = rustfmt(expected.to_string()); if actual != expected { println!("Generated bindings differ from expected!"); error_diff_mismatch( &actual, &expected, None, Path::new(expected_filename), ) .unwrap(); } } #[test] fn test_multiple_header_contents() { let actual = builder() .header_contents("test.h", "int foo(const char* a);") .header_contents("test2.h", "float foo2(const char* b);") .clang_arg("--target=x86_64-unknown-linux") .generate() .unwrap() .to_string(); let (actual, stderr) = rustfmt(actual); println!("{}", stderr); let (expected, _) = rustfmt( "extern \"C\" { pub fn foo2(b: *const ::std::os::raw::c_char) -> f32; } extern \"C\" { pub fn foo(a: *const ::std::os::raw::c_char) -> ::std::os::raw::c_int; } " .to_string(), ); assert_eq!(expected, actual); } #[test] fn test_mixed_header_and_header_contents() { let actual = builder() .header(concat!( env!("CARGO_MANIFEST_DIR"), "/tests/headers/func_ptr.h" )) .header(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/headers/char.h")) .header_contents("test.h", "int bar(const char* a);") .header_contents("test2.h", "float bar2(const char* b);") .clang_arg("--target=x86_64-unknown-linux") .generate() .unwrap() .to_string(); let (actual, stderr) = rustfmt(actual); println!("{}", stderr); let expected_filename = concat!( env!("CARGO_MANIFEST_DIR"), "/tests/expectations/tests/test_mixed_header_and_header_contents.rs" ); let expected = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/tests/expectations/tests/test_mixed_header_and_header_contents.rs" )); let (expected, _) = rustfmt(expected.to_string()); if expected != actual { error_diff_mismatch( &actual, &expected, None, Path::new(expected_filename), ) .unwrap(); } } #[test] // Doesn't support executing sh file on Windows. // We may want to implement it in Rust so that we support all systems. #[cfg(not(target_os = "windows"))] fn no_system_header_includes() { use std::process::Command; assert!(Command::new("./ci/no-includes.sh") .current_dir(env!("CARGO_MANIFEST_DIR")) .spawn() .expect("should spawn ./ci/no-includes.sh OK") .wait() .expect("should wait for ./ci/no-includes OK") .success()); } #[test] fn emit_depfile() { let header = PathBuf::from("tests/headers/enum-default-rust.h"); let expected_depfile = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests") .join("expectations") .join("tests") .join("enum-default-rust.d"); let observed_depfile = tempfile::NamedTempFile::new().unwrap(); let mut builder = create_bindgen_builder(&header).unwrap(); builder.builder = builder.builder.depfile( "tests/expectations/tests/enum-default-rust.rs", observed_depfile.path(), ); let check_roundtrip = env::var_os("BINDGEN_DISABLE_ROUNDTRIP_TEST").is_none(); let (builder, _roundtrip_builder) = builder.into_builder(check_roundtrip).unwrap(); let _bindings = builder.generate().unwrap(); let observed = std::fs::read_to_string(observed_depfile).unwrap(); let expected = std::fs::read_to_string(expected_depfile).unwrap(); assert_eq!(observed.trim(), expected.trim()); } #[test] fn dump_preprocessed_input() { let arg_keyword = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/headers/arg_keyword.hpp"); let empty_layout = concat!( env!("CARGO_MANIFEST_DIR"), "/tests/headers/cpp-empty-layout.hpp" ); builder() .header(arg_keyword) .header(empty_layout) .dump_preprocessed_input() .expect("should dump preprocessed input"); fn slurp(p: &str) -> String { let mut contents = String::new(); let mut file = fs::File::open(p).unwrap(); file.read_to_string(&mut contents).unwrap(); contents } let bindgen_ii = slurp("__bindgen.ii"); let arg_keyword = slurp(arg_keyword); let empty_layout = slurp(empty_layout); assert!( bindgen_ii.contains(&arg_keyword), "arg_keyword.hpp is in the preprocessed file" ); assert!( bindgen_ii.contains(&empty_layout), "cpp-empty-layout.hpp is in the preprocessed file" ); }