use bindgen::{clang_version, Builder}; use owo_colors::{OwoColorize, Style}; use similar::{ChangeTag, TextDiff}; use std::env; use std::fmt; use std::fs; use std::io::{BufRead, BufReader, Error, ErrorKind, Read, Write}; use std::path::{Path, PathBuf}; use crate::options::builder_from_flags; #[path = "../../bindgen-cli/options.rs"] mod options; mod parse_callbacks; // Format the given source string. It can fail if the source string does not contain syntactically // valid Rust. fn format_code>(source: S) -> syn::Result { use prettyplease::unparse; use syn::{parse_str, File}; let file = parse_str::(source.as_ref())?; Ok(unparse(&file)) } 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); } show_diff(expected, actual); 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()?; } 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.")) } struct Line(Option); impl fmt::Display for Line { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self.0 { None => write!(f, " "), Some(idx) => write!(f, "{:<4}", idx + 1), } } } fn show_diff(old: &str, new: &str) { let diff = TextDiff::from_lines(old, new); for (count, group) in diff.grouped_ops(3).iter().enumerate() { if count > 0 { let message = format!("(chunk {count}/n)"); println!("{}", message.cyan().dimmed()); } for diff_op in group { for change in diff.iter_inline_changes(diff_op) { let (sign, color) = match change.tag() { ChangeTag::Delete => ("-", Style::new().red()), ChangeTag::Insert => ("+", Style::new().green()), ChangeTag::Equal => (" ", Style::new()), }; print!( "{}{}| {}", Line(change.old_index()).style(color).dimmed(), Line(change.new_index()).style(color).dimmed(), sign.style(color).bold(), ); for (emphasized, text) in change.iter_strings_lossy() { if emphasized { print!("{}", text.style(color).underline()); } else { print!("{}", text.style(color)); } } if change.missing_newline() { println!(); } } } } } 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_16") { expectation.push("libclang-16"); } else if cfg!(feature = "__testing_only_libclang_9") { expectation.push("libclang-9"); } else { match clang_version().parsed { None => expectation.push("libclang-16"), Some(version) => { let (maj, min) = version; let version_str = if maj >= 16 { "16".to_owned() } else if maj >= 9 { "9".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 = match builder.generate() { Ok(bindings) => format_code(bindings.to_string()).map_err(|err| { Error::new( ErrorKind::Other, format!("Cannot parse the generated bindings: {}", err), ) })?, Err(_) => "/* error generating bindings */\n".into(), }; if actual.is_empty() { return Err(Error::new( ErrorKind::Other, "Something's gone really wrong!", )); } if actual != expected { 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); } 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. "--formatter=none", "--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); 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\ #if defined NOT_THREE && NOT_THREE == 1\nextern const int z[] = { 42 };\n#endif\n", ) .generate() .unwrap() .to_string(); let actual = format_code(actual).unwrap(); let expected = format_code( "extern \"C\" { pub static x: [::std::os::raw::c_int; 1usize]; } extern \"C\" { pub static y: [::std::os::raw::c_int; 1usize]; } ", ) .unwrap(); 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 = format_code(actual).unwrap(); let expected = format_code( "extern \"C\" { pub fn foo(a: *const ::std::os::raw::c_char) -> ::std::os::raw::c_int; } ", ) .unwrap(); 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 = format_code(actual).unwrap(); 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 = format_code(expected).unwrap(); if actual != expected { println!("Generated bindings differ from expected!"); error_diff_mismatch( &actual, &expected, None, Path::new(expected_filename), ) .unwrap(); } } #[test] fn test_headers_call_in_builder() { let actual = builder() .headers([ concat!(env!("CARGO_MANIFEST_DIR"), "/tests/headers/func_ptr.h"), concat!(env!("CARGO_MANIFEST_DIR"), "/tests/headers/char.h"), ]) .clang_arg("--target=x86_64-unknown-linux") .generate() .unwrap() .to_string(); let actual = format_code(actual).unwrap(); 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 = format_code(expected).unwrap(); 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 = format_code(actual).unwrap(); let expected = format_code( "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; } ", ) .unwrap(); 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 = format_code(actual).unwrap(); 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 = format_code(expected).unwrap(); if expected != actual { error_diff_mismatch( &actual, &expected, None, Path::new(expected_filename), ) .unwrap(); } } #[test] fn test_macro_fallback_non_system_dir() { let actual = builder() .header(concat!( env!("CARGO_MANIFEST_DIR"), "/tests/macro_fallback_test_headers/one_header.h" )) .header(concat!( env!("CARGO_MANIFEST_DIR"), "/tests/macro_fallback_test_headers/another_header.h" )) .clang_macro_fallback() .clang_arg(format!("-I{}/tests/headers", env!("CARGO_MANIFEST_DIR"))) .generate() .unwrap() .to_string(); let actual = format_code(actual).unwrap(); let (expected_filename, expected) = match clang_version().parsed { Some((9, _)) => { let expected_filename = concat!( env!("CARGO_MANIFEST_DIR"), "/tests/expectations/tests/libclang-9/macro_fallback_non_system_dir.rs", ); let expected = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/tests/expectations/tests/libclang-9/macro_fallback_non_system_dir.rs", )); (expected_filename, expected) } _ => { let expected_filename = concat!( env!("CARGO_MANIFEST_DIR"), "/tests/expectations/tests/test_macro_fallback_non_system_dir.rs", ); let expected = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/tests/expectations/tests/test_macro_fallback_non_system_dir.rs", )); (expected_filename, expected) } }; let expected = format_code(expected).unwrap(); 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" ); } fn build_flags_output_helper(builder: &bindgen::Builder) { let mut command_line_flags = builder.command_line_flags(); command_line_flags.insert(0, "bindgen".to_string()); let flags_quoted: Vec = command_line_flags .iter() .map(|x| format!("{}", shlex::try_quote(x).unwrap())) .collect(); let flags_str = flags_quoted.join(" "); println!("{}", flags_str); let (builder, _output, _verbose) = crate::options::builder_from_flags(command_line_flags.into_iter()) .unwrap(); builder.generate().expect("failed to generate bindings"); } #[test] fn commandline_multiple_headers() { let bindings = bindgen::Builder::default() .header("tests/headers/char.h") .header("tests/headers/func_ptr.h") .header("tests/headers/16-byte-alignment.h"); build_flags_output_helper(&bindings); } #[test] fn test_wrap_static_fns() { // This test is for testing diffs of the generated C source and header files // TODO: If another such feature is added, convert this test into a more generic // test that looks at `tests/headers/generated` directory. let expect_path = PathBuf::from("tests/expectations/tests/generated") .join("wrap_static_fns"); println!("In path is ::: {}", expect_path.display()); let generated_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("wrap_static_fns"); println!("Out path is ::: {}", generated_path.display()); let _bindings = Builder::default() .header("tests/headers/wrap-static-fns.h") .wrap_static_fns(true) .wrap_static_fns_path(generated_path.display().to_string()) .parse_callbacks(Box::new(parse_callbacks::WrapAsVariadicFn)) .generate() .expect("Failed to generate bindings"); let expected_c = fs::read_to_string(expect_path.with_extension("c")) .expect("Could not read generated wrap_static_fns.c"); let actual_c = fs::read_to_string(generated_path.with_extension("c")) .expect("Could not read actual wrap_static_fns.c"); if expected_c != actual_c { error_diff_mismatch( &actual_c, &expected_c, None, &expect_path.with_extension("c"), ) .unwrap(); } }