blob: 2335ee5ab37d05abc67746a417ed7e40434208f7 [file] [log] [blame]
Brian Silverman4e662aa2022-05-11 23:10:19 -07001// 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};
Brian Silvermanf3ec38b2022-07-06 20:43:36 -070024use quote::{format_ident, quote, TokenStreamExt};
Brian Silverman4e662aa2022-05-11 23:10:19 -070025use 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,
Brian Silvermanf3ec38b2022-07-06 20:43:36 -070059 AutocxxRsFile,
Austin Schuh6ea9bfa2023-08-06 19:05:10 -070060 /// 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)>),
Brian Silverman4e662aa2022-05-11 23:10:19 -070063}
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();
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700165 let mut rustflags = format!("-L {temp_path}");
Brian Silverman4e662aa2022-05-11 23:10:19 -0700166 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 ),
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700176 RsFindMode::AutocxxRsFile => std::env::set_var(
177 "AUTOCXX_RS_FILE",
178 self.temp_dir.path().join("gen0.include.rs"),
179 ),
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700180 RsFindMode::Custom(f) => f(self.temp_dir.path()),
Brian Silverman4e662aa2022-05-11 23:10:19 -0700181 };
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.
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700197#[track_caller]
Brian Silverman4e662aa2022-05-11 23:10:19 -0700198pub 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,
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700213 "unsafe_ffi",
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700214 None,
Brian Silverman4e662aa2022-05-11 23:10:19 -0700215 )
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,
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700269 "unsafe_ffi",
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700270 None,
Brian Silverman4e662aa2022-05-11 23:10:19 -0700271 )
272 .unwrap()
273}
274
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700275pub 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
Brian Silverman4e662aa2022-05-11 23:10:19 -0700287pub 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,
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700302 "unsafe_ffi",
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700303 None,
Brian Silverman4e662aa2022-05-11 23:10:19 -0700304 )
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,
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700325 "unsafe_ffi",
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700326 None,
Brian Silverman4e662aa2022-05-11 23:10:19 -0700327 )
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>,
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700376 safety_policy: &str,
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700377 module_attributes: Option<TokenStream>,
Brian Silverman4e662aa2022-05-11 23:10:19 -0700378) -> Result<(), TestError> {
379 let hexathorpe = Token![#](Span::call_site());
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700380 let safety_policy = format_ident!("{}", safety_policy);
Brian Silverman4e662aa2022-05-11 23:10:19 -0700381 let unexpanded_rust = quote! {
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700382 #module_attributes
383
Brian Silverman4e662aa2022-05-11 23:10:19 -0700384 use autocxx::prelude::*;
385
386 include_cpp!(
387 #hexathorpe include "input.h"
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700388 safety!(#safety_policy)
Brian Silverman4e662aa2022-05-11 23:10:19 -0700389 #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> {
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700424 let builder_modifier = consider_forcing_wrapper_generation(builder_modifier);
425
Brian Silverman4e662aa2022-05-11 23:10:19 -0700426 const HEADER_NAME: &str = "input.h";
427 // Step 2: Write the C++ header snippet to a temp file
428 let tdir = tempdir().unwrap();
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700429 write_to_file(&tdir, HEADER_NAME, &format!("#pragma once\n{header_code}"));
Brian Silverman4e662aa2022-05-11 23:10:19 -0700430 write_to_file(&tdir, "cxx.h", HEADER);
431
432 rust_code.append_all(quote! {
433 #[link(name="autocxx-demo")]
434 extern {}
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
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700440 let rs_code = format!("{ts}");
Brian Silverman4e662aa2022-05-11 23:10:19 -0700441 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());
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700450 let builder = Builder::<TestBuilderContext>::new(&rs_path, [tdir.path()])
Brian Silverman4e662aa2022-05-11 23:10:19 -0700451 .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.get(0).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.
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700479 let cxx_code = format!("#include \"input.h\"\n#include \"cxxgen.h\"\n{cxx_code}");
Brian Silverman4e662aa2022-05-11 23:10:19 -0700480 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 {
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700494 println!("Generated .rs files: {generated_rs_files:?}");
Brian Silverman4e662aa2022-05-11 23:10:19 -0700495 }
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}
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700515
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}