blob: 352c8ab5b9f33c860d1f6a78e7ef1b7d0a3c1e0b [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 Silverman4e662aa2022-05-11 23:10:19 -070060}
61
62/// API to test building pre-generated files.
63pub fn build_from_folder(
64 folder: &Path,
65 main_rs_file: &Path,
66 generated_rs_files: Vec<PathBuf>,
67 cpp_files: &[&str],
68 rs_find_mode: RsFindMode,
69) -> Result<(), TestError> {
70 let target_dir = folder.join("target");
71 std::fs::create_dir(&target_dir).unwrap();
72 let mut b = BuilderBuild::new();
73 for cpp_file in cpp_files.iter() {
74 b.file(folder.join(cpp_file));
75 }
76 configure_builder(&mut b)
77 .out_dir(&target_dir)
78 .include(folder)
79 .include(folder.join("demo"))
80 .try_compile("autocxx-demo")
81 .map_err(TestError::CppBuild)?;
82 // use the trybuild crate to build the Rust file.
83 let r = get_builder().lock().unwrap().build(
84 &target_dir,
85 "autocxx-demo",
86 &folder,
87 &["input.h", "cxx.h"],
88 &main_rs_file,
89 generated_rs_files,
90 rs_find_mode,
91 );
92 if r.is_err() {
93 return Err(TestError::RsBuild); // details of Rust panic are a bit messy to include, and
94 // not important at the moment.
95 }
96 Ok(())
97}
98
99fn get_builder() -> &'static Mutex<LinkableTryBuilder> {
100 static INSTANCE: OnceCell<Mutex<LinkableTryBuilder>> = OnceCell::new();
101 INSTANCE.get_or_init(|| Mutex::new(LinkableTryBuilder::new()))
102}
103
104/// TryBuild which maintains a directory of libraries to link.
105/// This is desirable because otherwise, if we alter the RUSTFLAGS
106/// then trybuild rebuilds *everything* including all the dev-dependencies.
107/// This object exists purely so that we use the same RUSTFLAGS for every
108/// test case.
109struct LinkableTryBuilder {
110 /// Directory in which we'll keep any linkable libraries
111 temp_dir: TempDir,
112}
113
114impl LinkableTryBuilder {
115 fn new() -> Self {
116 LinkableTryBuilder {
117 temp_dir: tempdir().unwrap(),
118 }
119 }
120
121 fn move_items_into_temp_dir<P1: AsRef<Path>>(&self, src_path: &P1, pattern: &str) {
122 for item in std::fs::read_dir(src_path).unwrap() {
123 let item = item.unwrap();
124 if item.file_name().into_string().unwrap().contains(pattern) {
125 let dest = self.temp_dir.path().join(item.file_name());
126 if dest.exists() {
127 std::fs::remove_file(&dest).unwrap();
128 }
129 if KEEP_TEMPDIRS {
130 std::fs::copy(item.path(), dest).unwrap();
131 } else {
132 std::fs::rename(item.path(), dest).unwrap();
133 }
134 }
135 }
136 }
137
138 #[allow(clippy::too_many_arguments)]
139 fn build<P1: AsRef<Path>, P2: AsRef<Path>, P3: AsRef<Path> + RefUnwindSafe>(
140 &self,
141 library_path: &P1,
142 library_name: &str,
143 header_path: &P2,
144 header_names: &[&str],
145 rs_path: &P3,
146 generated_rs_files: Vec<PathBuf>,
147 rs_find_mode: RsFindMode,
148 ) -> std::thread::Result<()> {
149 // Copy all items from the source dir into our temporary dir if their name matches
150 // the pattern given in `library_name`.
151 self.move_items_into_temp_dir(library_path, library_name);
152 for header_name in header_names {
153 self.move_items_into_temp_dir(header_path, header_name);
154 }
155 for generated_rs in generated_rs_files {
156 self.move_items_into_temp_dir(
157 &generated_rs.parent().unwrap(),
158 generated_rs.file_name().unwrap().to_str().unwrap(),
159 );
160 }
161 let temp_path = self.temp_dir.path().to_str().unwrap();
162 let mut rustflags = format!("-L {}", temp_path);
163 if std::env::var_os("AUTOCXX_ASAN").is_some() {
164 rustflags.push_str(" -Z sanitizer=address -Clinker=clang++ -Clink-arg=-fuse-ld=lld");
165 }
166 std::env::set_var("RUSTFLAGS", rustflags);
167 match rs_find_mode {
168 RsFindMode::AutocxxRs => std::env::set_var("AUTOCXX_RS", temp_path),
169 RsFindMode::AutocxxRsArchive => std::env::set_var(
170 "AUTOCXX_RS_JSON_ARCHIVE",
171 self.temp_dir.path().join("gen.rs.json"),
172 ),
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700173 RsFindMode::AutocxxRsFile => std::env::set_var(
174 "AUTOCXX_RS_FILE",
175 self.temp_dir.path().join("gen0.include.rs"),
176 ),
Brian Silverman4e662aa2022-05-11 23:10:19 -0700177 };
178 std::panic::catch_unwind(|| {
179 let test_cases = trybuild::TestCases::new();
180 test_cases.pass(rs_path)
181 })
182 }
183}
184
185fn write_to_file(tdir: &TempDir, filename: &str, content: &str) -> PathBuf {
186 let path = tdir.path().join(filename);
187 let mut f = File::create(&path).unwrap();
188 f.write_all(content.as_bytes()).unwrap();
189 path
190}
191
192/// A positive test, we expect to pass.
193pub fn run_test(
194 cxx_code: &str,
195 header_code: &str,
196 rust_code: TokenStream,
197 generate: &[&str],
198 generate_pods: &[&str],
199) {
200 do_run_test(
201 cxx_code,
202 header_code,
203 rust_code,
204 directives_from_lists(generate, generate_pods, None),
205 None,
206 None,
207 None,
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700208 "unsafe_ffi",
Brian Silverman4e662aa2022-05-11 23:10:19 -0700209 )
210 .unwrap()
211}
212
213// A trait for objects which can check the output of the code creation
214// process.
215pub trait CodeCheckerFns {
216 fn check_rust(&self, _rs: syn::File) -> Result<(), TestError> {
217 Ok(())
218 }
219 fn check_cpp(&self, _cpp: &[PathBuf]) -> Result<(), TestError> {
220 Ok(())
221 }
222 fn skip_build(&self) -> bool {
223 false
224 }
225}
226
227// A function applied to the resultant generated Rust code
228// which can be used to inspect that code.
229pub type CodeChecker = Box<dyn CodeCheckerFns>;
230
231// A trait for objects which can modify builders for testing purposes.
232pub trait BuilderModifierFns {
233 fn modify_autocxx_builder<'a>(
234 &self,
235 builder: Builder<'a, TestBuilderContext>,
236 ) -> Builder<'a, TestBuilderContext>;
237 fn modify_cc_builder<'a>(&self, builder: &'a mut cc::Build) -> &'a mut cc::Build {
238 builder
239 }
240}
241
242pub type BuilderModifier = Box<dyn BuilderModifierFns>;
243
244/// A positive test, we expect to pass.
245#[allow(clippy::too_many_arguments)] // least typing for each test
246pub fn run_test_ex(
247 cxx_code: &str,
248 header_code: &str,
249 rust_code: TokenStream,
250 directives: TokenStream,
251 builder_modifier: Option<BuilderModifier>,
252 code_checker: Option<CodeChecker>,
253 extra_rust: Option<TokenStream>,
254) {
255 do_run_test(
256 cxx_code,
257 header_code,
258 rust_code,
259 directives,
260 builder_modifier,
261 code_checker,
262 extra_rust,
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700263 "unsafe_ffi",
Brian Silverman4e662aa2022-05-11 23:10:19 -0700264 )
265 .unwrap()
266}
267
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700268pub fn run_generate_all_test(header_code: &str) {
269 run_test_ex(
270 "",
271 header_code,
272 quote! {},
273 quote! { generate_all!() },
274 None,
275 None,
276 None,
277 );
278}
279
Brian Silverman4e662aa2022-05-11 23:10:19 -0700280pub fn run_test_expect_fail(
281 cxx_code: &str,
282 header_code: &str,
283 rust_code: TokenStream,
284 generate: &[&str],
285 generate_pods: &[&str],
286) {
287 do_run_test(
288 cxx_code,
289 header_code,
290 rust_code,
291 directives_from_lists(generate, generate_pods, None),
292 None,
293 None,
294 None,
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700295 "unsafe_ffi",
Brian Silverman4e662aa2022-05-11 23:10:19 -0700296 )
297 .expect_err("Unexpected success");
298}
299
300pub fn run_test_expect_fail_ex(
301 cxx_code: &str,
302 header_code: &str,
303 rust_code: TokenStream,
304 directives: TokenStream,
305 builder_modifier: Option<BuilderModifier>,
306 code_checker: Option<CodeChecker>,
307 extra_rust: Option<TokenStream>,
308) {
309 do_run_test(
310 cxx_code,
311 header_code,
312 rust_code,
313 directives,
314 builder_modifier,
315 code_checker,
316 extra_rust,
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700317 "unsafe_ffi",
Brian Silverman4e662aa2022-05-11 23:10:19 -0700318 )
319 .expect_err("Unexpected success");
320}
321
322/// In the future maybe the tests will distinguish the exact type of failure expected.
323#[derive(Debug)]
324pub enum TestError {
325 AutoCxx(BuilderError),
326 CppBuild(cc::Error),
327 RsBuild,
328 NoRs,
329 RsFileOpen(std::io::Error),
330 RsFileRead(std::io::Error),
331 RsFileParse(syn::Error),
332 RsCodeExaminationFail(String),
333 CppCodeExaminationFail,
334}
335
336pub fn directives_from_lists(
337 generate: &[&str],
338 generate_pods: &[&str],
339 extra_directives: Option<TokenStream>,
340) -> TokenStream {
341 let generate = generate.iter().map(|s| {
342 quote! {
343 generate!(#s)
344 }
345 });
346 let generate_pods = generate_pods.iter().map(|s| {
347 quote! {
348 generate_pod!(#s)
349 }
350 });
351 quote! {
352 #(#generate)*
353 #(#generate_pods)*
354 #extra_directives
355 }
356}
357
358#[allow(clippy::too_many_arguments)] // least typing for each test
359pub fn do_run_test(
360 cxx_code: &str,
361 header_code: &str,
362 rust_code: TokenStream,
363 directives: TokenStream,
364 builder_modifier: Option<BuilderModifier>,
365 rust_code_checker: Option<CodeChecker>,
366 extra_rust: Option<TokenStream>,
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700367 safety_policy: &str,
Brian Silverman4e662aa2022-05-11 23:10:19 -0700368) -> Result<(), TestError> {
369 let hexathorpe = Token![#](Span::call_site());
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700370 let safety_policy = format_ident!("{}", safety_policy);
Brian Silverman4e662aa2022-05-11 23:10:19 -0700371 let unexpanded_rust = quote! {
372 use autocxx::prelude::*;
373
374 include_cpp!(
375 #hexathorpe include "input.h"
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700376 safety!(#safety_policy)
Brian Silverman4e662aa2022-05-11 23:10:19 -0700377 #directives
378 );
379
380 #extra_rust
381
382 fn main() {
383 #rust_code
384 }
385
386 };
387 do_run_test_manual(
388 cxx_code,
389 header_code,
390 unexpanded_rust,
391 builder_modifier,
392 rust_code_checker,
393 )
394}
395
396/// The [`BuilderContext`] used in autocxx's integration tests.
397pub struct TestBuilderContext;
398
399impl BuilderContext for TestBuilderContext {
400 fn get_dependency_recorder() -> Option<Box<dyn RebuildDependencyRecorder>> {
401 None
402 }
403}
404
405pub fn do_run_test_manual(
406 cxx_code: &str,
407 header_code: &str,
408 mut rust_code: TokenStream,
409 builder_modifier: Option<BuilderModifier>,
410 rust_code_checker: Option<CodeChecker>,
411) -> Result<(), TestError> {
412 const HEADER_NAME: &str = "input.h";
413 // Step 2: Write the C++ header snippet to a temp file
414 let tdir = tempdir().unwrap();
415 write_to_file(
416 &tdir,
417 HEADER_NAME,
418 &format!("#pragma once\n{}", header_code),
419 );
420 write_to_file(&tdir, "cxx.h", HEADER);
421
422 rust_code.append_all(quote! {
423 #[link(name="autocxx-demo")]
424 extern {}
425 });
426 info!("Unexpanded Rust: {}", rust_code);
427
428 let write_rust_to_file = |ts: &TokenStream| -> PathBuf {
429 // Step 3: Write the Rust code to a temp file
430 let rs_code = format!("{}", ts);
431 write_to_file(&tdir, "input.rs", &rs_code)
432 };
433
434 let target_dir = tdir.path().join("target");
435 std::fs::create_dir(&target_dir).unwrap();
436
437 let rs_path = write_rust_to_file(&rust_code);
438
439 info!("Path is {:?}", tdir.path());
440 let builder = Builder::<TestBuilderContext>::new(&rs_path, &[tdir.path()])
441 .custom_gendir(target_dir.clone());
442 let builder = if let Some(builder_modifier) = &builder_modifier {
443 builder_modifier.modify_autocxx_builder(builder)
444 } else {
445 builder
446 };
447 let build_results = builder.build_listing_files().map_err(TestError::AutoCxx)?;
448 let mut b = build_results.0;
449 let generated_rs_files = build_results.1;
450
451 if let Some(code_checker) = &rust_code_checker {
452 let mut file = File::open(generated_rs_files.get(0).ok_or(TestError::NoRs)?)
453 .map_err(TestError::RsFileOpen)?;
454 let mut content = String::new();
455 file.read_to_string(&mut content)
456 .map_err(TestError::RsFileRead)?;
457
458 let ast = syn::parse_file(&content).map_err(TestError::RsFileParse)?;
459 code_checker.check_rust(ast)?;
460 code_checker.check_cpp(&build_results.2)?;
461 if code_checker.skip_build() {
462 return Ok(());
463 }
464 }
465
466 if !cxx_code.is_empty() {
467 // Step 4: Write the C++ code snippet to a .cc file, along with a #include
468 // of the header emitted in step 5.
469 let cxx_code = format!("#include \"input.h\"\n#include \"cxxgen.h\"\n{}", cxx_code);
470 let cxx_path = write_to_file(&tdir, "input.cxx", &cxx_code);
471 b.file(cxx_path);
472 }
473
474 let b = configure_builder(&mut b).out_dir(&target_dir);
475 let b = if let Some(builder_modifier) = builder_modifier {
476 builder_modifier.modify_cc_builder(b)
477 } else {
478 b
479 };
480 b.include(tdir.path())
481 .try_compile("autocxx-demo")
482 .map_err(TestError::CppBuild)?;
483 if KEEP_TEMPDIRS {
484 println!("Generated .rs files: {:?}", generated_rs_files);
485 }
486 // Step 8: use the trybuild crate to build the Rust file.
487 let r = get_builder().lock().unwrap().build(
488 &target_dir,
489 "autocxx-demo",
490 &tdir.path(),
491 &["input.h", "cxx.h"],
492 &rs_path,
493 generated_rs_files,
494 RsFindMode::AutocxxRs,
495 );
496 if KEEP_TEMPDIRS {
497 println!("Tempdir: {:?}", tdir.into_path().to_str());
498 }
499 if r.is_err() {
500 return Err(TestError::RsBuild); // details of Rust panic are a bit messy to include, and
501 // not important at the moment.
502 }
503 Ok(())
504}