autocxx_integration_tests/
lib.rs

1// Copyright 2022 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use std::{
10    ffi::OsStr,
11    fs::File,
12    io::{Read, Write},
13    panic::RefUnwindSafe,
14    path::{Path, PathBuf},
15    sync::Mutex,
16};
17
18use autocxx_engine::{
19    Builder, BuilderBuild, BuilderContext, BuilderError, RebuildDependencyRecorder, HEADER,
20};
21use log::info;
22use once_cell::sync::OnceCell;
23use proc_macro2::{Span, TokenStream};
24use quote::{format_ident, quote, TokenStreamExt};
25use syn::Token;
26use tempfile::{tempdir, TempDir};
27
28const KEEP_TEMPDIRS: bool = false;
29
30/// API to run a documentation test. Panics if the test fails.
31/// Guarantees not to emit anything to stdout and so can be run in an mdbook context.
32pub fn doctest(
33    cxx_code: &str,
34    header_code: &str,
35    rust_code: TokenStream,
36    manifest_dir: &OsStr,
37) -> Result<(), TestError> {
38    std::env::set_var("CARGO_PKG_NAME", "autocxx-integration-tests");
39    std::env::set_var("CARGO_MANIFEST_DIR", manifest_dir);
40    do_run_test_manual(cxx_code, header_code, rust_code, None, None)
41}
42
43fn configure_builder(b: &mut BuilderBuild) -> &mut BuilderBuild {
44    let target = rust_info::get().target_triple.unwrap();
45    b.host(&target)
46        .target(&target)
47        .opt_level(1)
48        .flag("-std=c++14") // For clang
49        .flag_if_supported("/GX") // Enable C++ exceptions for msvc
50        .flag_if_supported("-Wall")
51        .flag_if_supported("-Werror")
52}
53
54/// What environment variables we should set in order to tell rustc how to find
55/// the Rust code.
56pub enum RsFindMode {
57    AutocxxRs,
58    AutocxxRsArchive,
59    AutocxxRsFile,
60    /// This just calls the callback instead of setting any environment variables. The callback
61    /// receives the path to the temporary directory.
62    Custom(Box<dyn FnOnce(&Path)>),
63}
64
65/// API to test building pre-generated files.
66pub fn build_from_folder(
67    folder: &Path,
68    main_rs_file: &Path,
69    generated_rs_files: Vec<PathBuf>,
70    cpp_files: &[&str],
71    rs_find_mode: RsFindMode,
72) -> Result<(), TestError> {
73    let target_dir = folder.join("target");
74    std::fs::create_dir(&target_dir).unwrap();
75    let mut b = BuilderBuild::new();
76    for cpp_file in cpp_files.iter() {
77        b.file(folder.join(cpp_file));
78    }
79    configure_builder(&mut b)
80        .out_dir(&target_dir)
81        .include(folder)
82        .include(folder.join("demo"))
83        .try_compile("autocxx-demo")
84        .map_err(TestError::CppBuild)?;
85    // use the trybuild crate to build the Rust file.
86    let r = get_builder().lock().unwrap().build(
87        &target_dir,
88        "autocxx-demo",
89        &folder,
90        &["input.h", "cxx.h"],
91        &main_rs_file,
92        generated_rs_files,
93        rs_find_mode,
94    );
95    if r.is_err() {
96        return Err(TestError::RsBuild); // details of Rust panic are a bit messy to include, and
97                                        // not important at the moment.
98    }
99    Ok(())
100}
101
102fn get_builder() -> &'static Mutex<LinkableTryBuilder> {
103    static INSTANCE: OnceCell<Mutex<LinkableTryBuilder>> = OnceCell::new();
104    INSTANCE.get_or_init(|| Mutex::new(LinkableTryBuilder::new()))
105}
106
107/// TryBuild which maintains a directory of libraries to link.
108/// This is desirable because otherwise, if we alter the RUSTFLAGS
109/// then trybuild rebuilds *everything* including all the dev-dependencies.
110/// This object exists purely so that we use the same RUSTFLAGS for every
111/// test case.
112struct LinkableTryBuilder {
113    /// Directory in which we'll keep any linkable libraries
114    temp_dir: TempDir,
115}
116
117impl LinkableTryBuilder {
118    fn new() -> Self {
119        LinkableTryBuilder {
120            temp_dir: tempdir().unwrap(),
121        }
122    }
123
124    fn move_items_into_temp_dir<P1: AsRef<Path>>(&self, src_path: &P1, pattern: &str) {
125        for item in std::fs::read_dir(src_path).unwrap() {
126            let item = item.unwrap();
127            if item.file_name().into_string().unwrap().contains(pattern) {
128                let dest = self.temp_dir.path().join(item.file_name());
129                if dest.exists() {
130                    std::fs::remove_file(&dest).unwrap();
131                }
132                if KEEP_TEMPDIRS {
133                    std::fs::copy(item.path(), dest).unwrap();
134                } else {
135                    std::fs::rename(item.path(), dest).unwrap();
136                }
137            }
138        }
139    }
140
141    #[allow(clippy::too_many_arguments)]
142    fn build<P1: AsRef<Path>, P2: AsRef<Path>, P3: AsRef<Path> + RefUnwindSafe>(
143        &self,
144        library_path: &P1,
145        library_name: &str,
146        header_path: &P2,
147        header_names: &[&str],
148        rs_path: &P3,
149        generated_rs_files: Vec<PathBuf>,
150        rs_find_mode: RsFindMode,
151    ) -> std::thread::Result<()> {
152        // Copy all items from the source dir into our temporary dir if their name matches
153        // the pattern given in `library_name`.
154        self.move_items_into_temp_dir(library_path, library_name);
155        for header_name in header_names {
156            self.move_items_into_temp_dir(header_path, header_name);
157        }
158        for generated_rs in generated_rs_files {
159            self.move_items_into_temp_dir(
160                &generated_rs.parent().unwrap(),
161                generated_rs.file_name().unwrap().to_str().unwrap(),
162            );
163        }
164        let temp_path = self.temp_dir.path().to_str().unwrap();
165        let mut rustflags = format!("-L {temp_path}");
166        if std::env::var_os("AUTOCXX_ASAN").is_some() {
167            rustflags.push_str(" -Z sanitizer=address -Clinker=clang++ -Clink-arg=-fuse-ld=lld");
168        }
169        std::env::set_var("RUSTFLAGS", rustflags);
170        match rs_find_mode {
171            RsFindMode::AutocxxRs => std::env::set_var("AUTOCXX_RS", temp_path),
172            RsFindMode::AutocxxRsArchive => std::env::set_var(
173                "AUTOCXX_RS_JSON_ARCHIVE",
174                self.temp_dir.path().join("gen.rs.json"),
175            ),
176            RsFindMode::AutocxxRsFile => std::env::set_var(
177                "AUTOCXX_RS_FILE",
178                self.temp_dir.path().join("gen0.include.rs"),
179            ),
180            RsFindMode::Custom(f) => f(self.temp_dir.path()),
181        };
182        std::panic::catch_unwind(|| {
183            let test_cases = trybuild::TestCases::new();
184            test_cases.pass(rs_path)
185        })
186    }
187}
188
189fn write_to_file(tdir: &TempDir, filename: &str, content: &str) -> PathBuf {
190    let path = tdir.path().join(filename);
191    let mut f = File::create(&path).unwrap();
192    f.write_all(content.as_bytes()).unwrap();
193    path
194}
195
196/// A positive test, we expect to pass.
197#[track_caller]
198pub fn run_test(
199    cxx_code: &str,
200    header_code: &str,
201    rust_code: TokenStream,
202    generate: &[&str],
203    generate_pods: &[&str],
204) {
205    do_run_test(
206        cxx_code,
207        header_code,
208        rust_code,
209        directives_from_lists(generate, generate_pods, None),
210        None,
211        None,
212        None,
213        "unsafe_ffi",
214        None,
215    )
216    .unwrap()
217}
218
219// A trait for objects which can check the output of the code creation
220// process.
221pub trait CodeCheckerFns {
222    fn check_rust(&self, _rs: syn::File) -> Result<(), TestError> {
223        Ok(())
224    }
225    fn check_cpp(&self, _cpp: &[PathBuf]) -> Result<(), TestError> {
226        Ok(())
227    }
228    fn skip_build(&self) -> bool {
229        false
230    }
231}
232
233// A function applied to the resultant generated Rust code
234// which can be used to inspect that code.
235pub type CodeChecker = Box<dyn CodeCheckerFns>;
236
237// A trait for objects which can modify builders for testing purposes.
238pub trait BuilderModifierFns {
239    fn modify_autocxx_builder<'a>(
240        &self,
241        builder: Builder<'a, TestBuilderContext>,
242    ) -> Builder<'a, TestBuilderContext>;
243    fn modify_cc_builder<'a>(&self, builder: &'a mut cc::Build) -> &'a mut cc::Build {
244        builder
245    }
246}
247
248pub type BuilderModifier = Box<dyn BuilderModifierFns>;
249
250/// A positive test, we expect to pass.
251#[allow(clippy::too_many_arguments)] // least typing for each test
252pub fn run_test_ex(
253    cxx_code: &str,
254    header_code: &str,
255    rust_code: TokenStream,
256    directives: TokenStream,
257    builder_modifier: Option<BuilderModifier>,
258    code_checker: Option<CodeChecker>,
259    extra_rust: Option<TokenStream>,
260) {
261    do_run_test(
262        cxx_code,
263        header_code,
264        rust_code,
265        directives,
266        builder_modifier,
267        code_checker,
268        extra_rust,
269        "unsafe_ffi",
270        None,
271    )
272    .unwrap()
273}
274
275pub fn run_generate_all_test(header_code: &str) {
276    run_test_ex(
277        "",
278        header_code,
279        quote! {},
280        quote! { generate_all!() },
281        None,
282        None,
283        None,
284    );
285}
286
287pub fn run_test_expect_fail(
288    cxx_code: &str,
289    header_code: &str,
290    rust_code: TokenStream,
291    generate: &[&str],
292    generate_pods: &[&str],
293) {
294    do_run_test(
295        cxx_code,
296        header_code,
297        rust_code,
298        directives_from_lists(generate, generate_pods, None),
299        None,
300        None,
301        None,
302        "unsafe_ffi",
303        None,
304    )
305    .expect_err("Unexpected success");
306}
307
308pub fn run_test_expect_fail_ex(
309    cxx_code: &str,
310    header_code: &str,
311    rust_code: TokenStream,
312    directives: TokenStream,
313    builder_modifier: Option<BuilderModifier>,
314    code_checker: Option<CodeChecker>,
315    extra_rust: Option<TokenStream>,
316) {
317    do_run_test(
318        cxx_code,
319        header_code,
320        rust_code,
321        directives,
322        builder_modifier,
323        code_checker,
324        extra_rust,
325        "unsafe_ffi",
326        None,
327    )
328    .expect_err("Unexpected success");
329}
330
331/// In the future maybe the tests will distinguish the exact type of failure expected.
332#[derive(Debug)]
333pub enum TestError {
334    AutoCxx(BuilderError),
335    CppBuild(cc::Error),
336    RsBuild,
337    NoRs,
338    RsFileOpen(std::io::Error),
339    RsFileRead(std::io::Error),
340    RsFileParse(syn::Error),
341    RsCodeExaminationFail(String),
342    CppCodeExaminationFail,
343}
344
345pub fn directives_from_lists(
346    generate: &[&str],
347    generate_pods: &[&str],
348    extra_directives: Option<TokenStream>,
349) -> TokenStream {
350    let generate = generate.iter().map(|s| {
351        quote! {
352            generate!(#s)
353        }
354    });
355    let generate_pods = generate_pods.iter().map(|s| {
356        quote! {
357            generate_pod!(#s)
358        }
359    });
360    quote! {
361        #(#generate)*
362        #(#generate_pods)*
363        #extra_directives
364    }
365}
366
367#[allow(clippy::too_many_arguments)] // least typing for each test
368pub fn do_run_test(
369    cxx_code: &str,
370    header_code: &str,
371    rust_code: TokenStream,
372    directives: TokenStream,
373    builder_modifier: Option<BuilderModifier>,
374    rust_code_checker: Option<CodeChecker>,
375    extra_rust: Option<TokenStream>,
376    safety_policy: &str,
377    module_attributes: Option<TokenStream>,
378) -> Result<(), TestError> {
379    let hexathorpe = Token![#](Span::call_site());
380    let safety_policy = format_ident!("{}", safety_policy);
381    let unexpanded_rust = quote! {
382            #module_attributes
383
384            use autocxx::prelude::*;
385
386            include_cpp!(
387                #hexathorpe include "input.h"
388                safety!(#safety_policy)
389                #directives
390            );
391
392            #extra_rust
393
394            fn main() {
395                #rust_code
396            }
397
398    };
399    do_run_test_manual(
400        cxx_code,
401        header_code,
402        unexpanded_rust,
403        builder_modifier,
404        rust_code_checker,
405    )
406}
407
408/// The [`BuilderContext`] used in autocxx's integration tests.
409pub struct TestBuilderContext;
410
411impl BuilderContext for TestBuilderContext {
412    fn get_dependency_recorder() -> Option<Box<dyn RebuildDependencyRecorder>> {
413        None
414    }
415}
416
417pub fn do_run_test_manual(
418    cxx_code: &str,
419    header_code: &str,
420    mut rust_code: TokenStream,
421    builder_modifier: Option<BuilderModifier>,
422    rust_code_checker: Option<CodeChecker>,
423) -> Result<(), TestError> {
424    let builder_modifier = consider_forcing_wrapper_generation(builder_modifier);
425
426    const HEADER_NAME: &str = "input.h";
427    // Step 2: Write the C++ header snippet to a temp file
428    let tdir = tempdir().unwrap();
429    write_to_file(&tdir, HEADER_NAME, &format!("#pragma once\n{header_code}"));
430    write_to_file(&tdir, "cxx.h", HEADER);
431
432    rust_code.append_all(quote! {
433        #[link(name="autocxx-demo")]
434        extern "C" {}
435    });
436    info!("Unexpanded Rust: {}", rust_code);
437
438    let write_rust_to_file = |ts: &TokenStream| -> PathBuf {
439        // Step 3: Write the Rust code to a temp file
440        let rs_code = format!("{ts}");
441        write_to_file(&tdir, "input.rs", &rs_code)
442    };
443
444    let target_dir = tdir.path().join("target");
445    std::fs::create_dir(&target_dir).unwrap();
446
447    let rs_path = write_rust_to_file(&rust_code);
448
449    info!("Path is {:?}", tdir.path());
450    let builder = Builder::<TestBuilderContext>::new(&rs_path, [tdir.path()])
451        .custom_gendir(target_dir.clone());
452    let builder = if let Some(builder_modifier) = &builder_modifier {
453        builder_modifier.modify_autocxx_builder(builder)
454    } else {
455        builder
456    };
457    let build_results = builder.build_listing_files().map_err(TestError::AutoCxx)?;
458    let mut b = build_results.0;
459    let generated_rs_files = build_results.1;
460
461    if let Some(code_checker) = &rust_code_checker {
462        let mut file = File::open(generated_rs_files.first().ok_or(TestError::NoRs)?)
463            .map_err(TestError::RsFileOpen)?;
464        let mut content = String::new();
465        file.read_to_string(&mut content)
466            .map_err(TestError::RsFileRead)?;
467
468        let ast = syn::parse_file(&content).map_err(TestError::RsFileParse)?;
469        code_checker.check_rust(ast)?;
470        code_checker.check_cpp(&build_results.2)?;
471        if code_checker.skip_build() {
472            return Ok(());
473        }
474    }
475
476    if !cxx_code.is_empty() {
477        // Step 4: Write the C++ code snippet to a .cc file, along with a #include
478        //         of the header emitted in step 5.
479        let cxx_code = format!("#include \"input.h\"\n#include \"cxxgen.h\"\n{cxx_code}");
480        let cxx_path = write_to_file(&tdir, "input.cxx", &cxx_code);
481        b.file(cxx_path);
482    }
483
484    let b = configure_builder(&mut b).out_dir(&target_dir);
485    let b = if let Some(builder_modifier) = builder_modifier {
486        builder_modifier.modify_cc_builder(b)
487    } else {
488        b
489    };
490    b.include(tdir.path())
491        .try_compile("autocxx-demo")
492        .map_err(TestError::CppBuild)?;
493    if KEEP_TEMPDIRS {
494        println!("Generated .rs files: {generated_rs_files:?}");
495    }
496    // Step 8: use the trybuild crate to build the Rust file.
497    let r = get_builder().lock().unwrap().build(
498        &target_dir,
499        "autocxx-demo",
500        &tdir.path(),
501        &["input.h", "cxx.h"],
502        &rs_path,
503        generated_rs_files,
504        RsFindMode::AutocxxRs,
505    );
506    if KEEP_TEMPDIRS {
507        println!("Tempdir: {:?}", tdir.into_path().to_str());
508    }
509    if r.is_err() {
510        return Err(TestError::RsBuild); // details of Rust panic are a bit messy to include, and
511                                        // not important at the moment.
512    }
513    Ok(())
514}
515
516/// If AUTOCXX_FORCE_WRAPPER_GENERATION is set, always force both C++
517/// and Rust side shims, for extra testing of obscure code paths.
518fn consider_forcing_wrapper_generation(
519    existing_builder_modifier: Option<BuilderModifier>,
520) -> Option<BuilderModifier> {
521    if std::env::var("AUTOCXX_FORCE_WRAPPER_GENERATION").is_err() {
522        existing_builder_modifier
523    } else {
524        Some(Box::new(ForceWrapperGeneration(existing_builder_modifier)))
525    }
526}
527
528struct ForceWrapperGeneration(Option<BuilderModifier>);
529
530impl BuilderModifierFns for ForceWrapperGeneration {
531    fn modify_autocxx_builder<'a>(
532        &self,
533        builder: Builder<'a, TestBuilderContext>,
534    ) -> Builder<'a, TestBuilderContext> {
535        let builder = builder.force_wrapper_generation(true);
536        if let Some(modifier) = &self.0 {
537            modifier.modify_autocxx_builder(builder)
538        } else {
539            builder
540        }
541    }
542    fn modify_cc_builder<'a>(&self, builder: &'a mut cc::Build) -> &'a mut cc::Build {
543        if let Some(modifier) = &self.0 {
544            modifier.modify_cc_builder(builder)
545        } else {
546            builder
547        }
548    }
549}