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