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