Brian Silverman | 4e662aa | 2022-05-11 23:10:19 -0700 | [diff] [blame^] | 1 | // 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 | |
| 9 | use std::{ |
| 10 | ffi::OsStr, |
| 11 | fs::File, |
| 12 | io::{Read, Write}, |
| 13 | panic::RefUnwindSafe, |
| 14 | path::{Path, PathBuf}, |
| 15 | sync::Mutex, |
| 16 | }; |
| 17 | |
| 18 | use autocxx_engine::{ |
| 19 | Builder, BuilderBuild, BuilderContext, BuilderError, RebuildDependencyRecorder, HEADER, |
| 20 | }; |
| 21 | use log::info; |
| 22 | use once_cell::sync::OnceCell; |
| 23 | use proc_macro2::{Span, TokenStream}; |
| 24 | use quote::{quote, TokenStreamExt}; |
| 25 | use syn::Token; |
| 26 | use tempfile::{tempdir, TempDir}; |
| 27 | |
| 28 | const 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. |
| 32 | pub 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 | |
| 43 | fn 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. |
| 56 | pub enum RsFindMode { |
| 57 | AutocxxRs, |
| 58 | AutocxxRsArchive, |
| 59 | } |
| 60 | |
| 61 | /// API to test building pre-generated files. |
| 62 | pub 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 | |
| 98 | fn 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. |
| 108 | struct LinkableTryBuilder { |
| 109 | /// Directory in which we'll keep any linkable libraries |
| 110 | temp_dir: TempDir, |
| 111 | } |
| 112 | |
| 113 | impl 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 | |
| 180 | fn 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. |
| 188 | pub 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. |
| 209 | pub 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. |
| 223 | pub type CodeChecker = Box<dyn CodeCheckerFns>; |
| 224 | |
| 225 | // A trait for objects which can modify builders for testing purposes. |
| 226 | pub 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 | |
| 236 | pub 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 |
| 240 | pub 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 | |
| 261 | pub 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 | |
| 280 | pub 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)] |
| 303 | pub 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 | |
| 315 | pub 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 |
| 338 | pub 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); |
| 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. |
| 374 | pub struct TestBuilderContext; |
| 375 | |
| 376 | impl BuilderContext for TestBuilderContext { |
| 377 | fn get_dependency_recorder() -> Option<Box<dyn RebuildDependencyRecorder>> { |
| 378 | None |
| 379 | } |
| 380 | } |
| 381 | |
| 382 | pub 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 | } |