1use 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
30pub 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") .flag_if_supported("/GX") .flag_if_supported("-Wall")
51 .flag_if_supported("-Werror")
52}
53
54pub enum RsFindMode {
57 AutocxxRs,
58 AutocxxRsArchive,
59 AutocxxRsFile,
60 Custom(Box<dyn FnOnce(&Path)>),
63}
64
65pub 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 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); }
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
107struct LinkableTryBuilder {
113 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 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#[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
219pub 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
233pub type CodeChecker = Box<dyn CodeCheckerFns>;
236
237pub 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#[allow(clippy::too_many_arguments)] pub 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#[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)] pub 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);
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
408pub 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 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 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 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 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); }
513 Ok(())
514}
515
516fn 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}