blob: f4667d5234c4df7879a3dff5a930818ed8c0bc23 [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,
Brian Silverman021ead32022-08-14 14:04:05 -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();
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 ),
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 ),
Brian Silverman021ead32022-08-14 14:04:05 -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.
197pub fn run_test(
198 cxx_code: &str,
199 header_code: &str,
200 rust_code: TokenStream,
201 generate: &[&str],
202 generate_pods: &[&str],
203) {
204 do_run_test(
205 cxx_code,
206 header_code,
207 rust_code,
208 directives_from_lists(generate, generate_pods, None),
209 None,
210 None,
211 None,
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700212 "unsafe_ffi",
Brian Silverman4e662aa2022-05-11 23:10:19 -0700213 )
214 .unwrap()
215}
216
217// A trait for objects which can check the output of the code creation
218// process.
219pub trait CodeCheckerFns {
220 fn check_rust(&self, _rs: syn::File) -> Result<(), TestError> {
221 Ok(())
222 }
223 fn check_cpp(&self, _cpp: &[PathBuf]) -> Result<(), TestError> {
224 Ok(())
225 }
226 fn skip_build(&self) -> bool {
227 false
228 }
229}
230
231// A function applied to the resultant generated Rust code
232// which can be used to inspect that code.
233pub type CodeChecker = Box<dyn CodeCheckerFns>;
234
235// A trait for objects which can modify builders for testing purposes.
236pub trait BuilderModifierFns {
237 fn modify_autocxx_builder<'a>(
238 &self,
239 builder: Builder<'a, TestBuilderContext>,
240 ) -> Builder<'a, TestBuilderContext>;
241 fn modify_cc_builder<'a>(&self, builder: &'a mut cc::Build) -> &'a mut cc::Build {
242 builder
243 }
244}
245
246pub type BuilderModifier = Box<dyn BuilderModifierFns>;
247
248/// A positive test, we expect to pass.
249#[allow(clippy::too_many_arguments)] // least typing for each test
250pub fn run_test_ex(
251 cxx_code: &str,
252 header_code: &str,
253 rust_code: TokenStream,
254 directives: TokenStream,
255 builder_modifier: Option<BuilderModifier>,
256 code_checker: Option<CodeChecker>,
257 extra_rust: Option<TokenStream>,
258) {
259 do_run_test(
260 cxx_code,
261 header_code,
262 rust_code,
263 directives,
264 builder_modifier,
265 code_checker,
266 extra_rust,
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700267 "unsafe_ffi",
Brian Silverman4e662aa2022-05-11 23:10:19 -0700268 )
269 .unwrap()
270}
271
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700272pub fn run_generate_all_test(header_code: &str) {
273 run_test_ex(
274 "",
275 header_code,
276 quote! {},
277 quote! { generate_all!() },
278 None,
279 None,
280 None,
281 );
282}
283
Brian Silverman4e662aa2022-05-11 23:10:19 -0700284pub fn run_test_expect_fail(
285 cxx_code: &str,
286 header_code: &str,
287 rust_code: TokenStream,
288 generate: &[&str],
289 generate_pods: &[&str],
290) {
291 do_run_test(
292 cxx_code,
293 header_code,
294 rust_code,
295 directives_from_lists(generate, generate_pods, None),
296 None,
297 None,
298 None,
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700299 "unsafe_ffi",
Brian Silverman4e662aa2022-05-11 23:10:19 -0700300 )
301 .expect_err("Unexpected success");
302}
303
304pub fn run_test_expect_fail_ex(
305 cxx_code: &str,
306 header_code: &str,
307 rust_code: TokenStream,
308 directives: TokenStream,
309 builder_modifier: Option<BuilderModifier>,
310 code_checker: Option<CodeChecker>,
311 extra_rust: Option<TokenStream>,
312) {
313 do_run_test(
314 cxx_code,
315 header_code,
316 rust_code,
317 directives,
318 builder_modifier,
319 code_checker,
320 extra_rust,
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700321 "unsafe_ffi",
Brian Silverman4e662aa2022-05-11 23:10:19 -0700322 )
323 .expect_err("Unexpected success");
324}
325
326/// In the future maybe the tests will distinguish the exact type of failure expected.
327#[derive(Debug)]
328pub enum TestError {
329 AutoCxx(BuilderError),
330 CppBuild(cc::Error),
331 RsBuild,
332 NoRs,
333 RsFileOpen(std::io::Error),
334 RsFileRead(std::io::Error),
335 RsFileParse(syn::Error),
336 RsCodeExaminationFail(String),
337 CppCodeExaminationFail,
338}
339
340pub fn directives_from_lists(
341 generate: &[&str],
342 generate_pods: &[&str],
343 extra_directives: Option<TokenStream>,
344) -> TokenStream {
345 let generate = generate.iter().map(|s| {
346 quote! {
347 generate!(#s)
348 }
349 });
350 let generate_pods = generate_pods.iter().map(|s| {
351 quote! {
352 generate_pod!(#s)
353 }
354 });
355 quote! {
356 #(#generate)*
357 #(#generate_pods)*
358 #extra_directives
359 }
360}
361
362#[allow(clippy::too_many_arguments)] // least typing for each test
363pub fn do_run_test(
364 cxx_code: &str,
365 header_code: &str,
366 rust_code: TokenStream,
367 directives: TokenStream,
368 builder_modifier: Option<BuilderModifier>,
369 rust_code_checker: Option<CodeChecker>,
370 extra_rust: Option<TokenStream>,
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700371 safety_policy: &str,
Brian Silverman4e662aa2022-05-11 23:10:19 -0700372) -> Result<(), TestError> {
373 let hexathorpe = Token![#](Span::call_site());
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700374 let safety_policy = format_ident!("{}", safety_policy);
Brian Silverman4e662aa2022-05-11 23:10:19 -0700375 let unexpanded_rust = quote! {
376 use autocxx::prelude::*;
377
378 include_cpp!(
379 #hexathorpe include "input.h"
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700380 safety!(#safety_policy)
Brian Silverman4e662aa2022-05-11 23:10:19 -0700381 #directives
382 );
383
384 #extra_rust
385
386 fn main() {
387 #rust_code
388 }
389
390 };
391 do_run_test_manual(
392 cxx_code,
393 header_code,
394 unexpanded_rust,
395 builder_modifier,
396 rust_code_checker,
397 )
398}
399
400/// The [`BuilderContext`] used in autocxx's integration tests.
401pub struct TestBuilderContext;
402
403impl BuilderContext for TestBuilderContext {
404 fn get_dependency_recorder() -> Option<Box<dyn RebuildDependencyRecorder>> {
405 None
406 }
407}
408
409pub fn do_run_test_manual(
410 cxx_code: &str,
411 header_code: &str,
412 mut rust_code: TokenStream,
413 builder_modifier: Option<BuilderModifier>,
414 rust_code_checker: Option<CodeChecker>,
415) -> Result<(), TestError> {
416 const HEADER_NAME: &str = "input.h";
417 // Step 2: Write the C++ header snippet to a temp file
418 let tdir = tempdir().unwrap();
419 write_to_file(
420 &tdir,
421 HEADER_NAME,
422 &format!("#pragma once\n{}", header_code),
423 );
424 write_to_file(&tdir, "cxx.h", HEADER);
425
426 rust_code.append_all(quote! {
427 #[link(name="autocxx-demo")]
428 extern {}
429 });
430 info!("Unexpanded Rust: {}", rust_code);
431
432 let write_rust_to_file = |ts: &TokenStream| -> PathBuf {
433 // Step 3: Write the Rust code to a temp file
434 let rs_code = format!("{}", ts);
435 write_to_file(&tdir, "input.rs", &rs_code)
436 };
437
438 let target_dir = tdir.path().join("target");
439 std::fs::create_dir(&target_dir).unwrap();
440
441 let rs_path = write_rust_to_file(&rust_code);
442
443 info!("Path is {:?}", tdir.path());
444 let builder = Builder::<TestBuilderContext>::new(&rs_path, &[tdir.path()])
445 .custom_gendir(target_dir.clone());
446 let builder = if let Some(builder_modifier) = &builder_modifier {
447 builder_modifier.modify_autocxx_builder(builder)
448 } else {
449 builder
450 };
451 let build_results = builder.build_listing_files().map_err(TestError::AutoCxx)?;
452 let mut b = build_results.0;
453 let generated_rs_files = build_results.1;
454
455 if let Some(code_checker) = &rust_code_checker {
456 let mut file = File::open(generated_rs_files.get(0).ok_or(TestError::NoRs)?)
457 .map_err(TestError::RsFileOpen)?;
458 let mut content = String::new();
459 file.read_to_string(&mut content)
460 .map_err(TestError::RsFileRead)?;
461
462 let ast = syn::parse_file(&content).map_err(TestError::RsFileParse)?;
463 code_checker.check_rust(ast)?;
464 code_checker.check_cpp(&build_results.2)?;
465 if code_checker.skip_build() {
466 return Ok(());
467 }
468 }
469
470 if !cxx_code.is_empty() {
471 // Step 4: Write the C++ code snippet to a .cc file, along with a #include
472 // of the header emitted in step 5.
473 let cxx_code = format!("#include \"input.h\"\n#include \"cxxgen.h\"\n{}", cxx_code);
474 let cxx_path = write_to_file(&tdir, "input.cxx", &cxx_code);
475 b.file(cxx_path);
476 }
477
478 let b = configure_builder(&mut b).out_dir(&target_dir);
479 let b = if let Some(builder_modifier) = builder_modifier {
480 builder_modifier.modify_cc_builder(b)
481 } else {
482 b
483 };
484 b.include(tdir.path())
485 .try_compile("autocxx-demo")
486 .map_err(TestError::CppBuild)?;
487 if KEEP_TEMPDIRS {
488 println!("Generated .rs files: {:?}", generated_rs_files);
489 }
490 // Step 8: use the trybuild crate to build the Rust file.
491 let r = get_builder().lock().unwrap().build(
492 &target_dir,
493 "autocxx-demo",
494 &tdir.path(),
495 &["input.h", "cxx.h"],
496 &rs_path,
497 generated_rs_files,
498 RsFindMode::AutocxxRs,
499 );
500 if KEEP_TEMPDIRS {
501 println!("Tempdir: {:?}", tdir.into_path().to_str());
502 }
503 if r.is_err() {
504 return Err(TestError::RsBuild); // details of Rust panic are a bit messy to include, and
505 // not important at the moment.
506 }
507 Ok(())
508}