1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
|
//! This test harness is for verifying that the C++ code from cxx's C++ code
//! generator (via `cxx_gen`) triggers the intended C++ compiler diagnostics.
#![allow(unknown_lints, mismatched_lifetime_syntaxes)]
use proc_macro2::TokenStream;
use std::borrow::Cow;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{self, Stdio};
use tempfile::TempDir;
mod smoke_test;
/// 1. Takes a `#[cxx::bridge]` and generates `.cc` and `.h` files,
/// 2. Places additional source files (typically handwritten header files),
/// 3. Compiles the generated `.cc` file.
pub struct Test {
temp_dir: TempDir,
/// Path to the `.cc` file (in `temp_dir`) that is generated by the
/// `cxx_gen` crate out of the `cxx_bridge` argument passed to `Test::new`.
generated_cc: PathBuf,
}
impl Test {
/// Creates a new test for the given `cxx_bridge`.
///
/// Example:
///
/// ```
/// let test = Test::new(quote!{
/// #[cxx::bridge]
/// mod ffi {
/// unsafe extern "C++" {
/// include!("include.h");
/// pub fn do_cpp_thing();
/// }
/// }
/// });
/// ```
///
/// # Panics
///
/// Panics if there is a failure when generating `.cc` and `.h` files from
/// the `cxx_bridge`.
#[must_use]
pub fn new(cxx_bridge: TokenStream) -> Self {
let prefix = concat!(env!("CARGO_CRATE_NAME"), "-");
let scratch = scratch::path("cxx-test-suite");
let temp_dir = TempDir::with_prefix_in(prefix, scratch).unwrap();
let generated_h = temp_dir.path().join("cxx_bridge.generated.h");
let generated_cc = temp_dir.path().join("cxx_bridge.generated.cc");
let opt = cxx_gen::Opt::default();
let generated = cxx_gen::generate_header_and_cc(cxx_bridge, &opt).unwrap();
fs::write(&generated_h, &generated.header).unwrap();
fs::write(&generated_cc, &generated.implementation).unwrap();
Self {
temp_dir,
generated_cc,
}
}
/// Writes a file to the temporary test directory.
///
/// The new file will be present in the `-I` include path passed to the C++
/// compiler.
///
/// # Panics
///
/// Panics if there is an error when writing the file.
pub fn write_file(&self, filename: impl AsRef<Path>, contents: &str) {
fs::write(self.temp_dir.path().join(filename), contents).unwrap();
}
/// Compiles the `.cc` file generated in `Self::new`.
///
/// # Panics
///
/// Panics if there is a problem with spawning the C++ compiler.
/// (Compilation errors will *not* result in a panic.)
#[must_use]
pub fn compile(&self) -> CompilationResult {
let mut build = cc::Build::new();
build
.include(self.temp_dir.path())
.out_dir(self.temp_dir.path())
.cpp(true);
// Arbitrarily using C++20 for now. If some test cases require a
// specific C++ standard, we can make this configurable.
build.std("c++20");
// Set info required by the `cc` crate.
//
// The correct host triple during execution of this test is the target
// triple from the Rust compilation of this test -- not the Rust host
// triple.
build
.opt_level(3)
.host(target_triple::TARGET)
.target(target_triple::TARGET);
// The `cc` crate does not currently expose the `Command` for building a
// single C++ source file. Work around that by passing `-c <file.cc>`.
let mut command = build.get_compiler().to_command();
command
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.current_dir(self.temp_dir.path())
.arg("-c")
.arg(&self.generated_cc);
let output = command.spawn().unwrap().wait_with_output().unwrap();
CompilationResult(output)
}
}
/// Wrapper around the output from a C++ compiler.
pub struct CompilationResult(process::Output);
impl CompilationResult {
fn stdout(&self) -> Cow<str> {
String::from_utf8_lossy(&self.0.stdout)
}
fn stderr(&self) -> Cow<str> {
String::from_utf8_lossy(&self.0.stderr)
}
fn dump_output_and_panic(&self, msg: &str) -> ! {
eprintln!("{}", self.stdout());
eprintln!("{}", self.stderr());
panic!("{msg}");
}
fn error_lines(&self) -> Vec<String> {
assert!(!self.0.status.success());
// MSVC reports errors to stdout rather than stderr, so consider both.
let stdout = self.stdout();
let stderr = self.stderr();
let all_lines = stdout.lines().chain(stderr.lines());
all_lines
.filter(|line| {
// This should match MSVC error output
// (e.g. `file.cc(<line number>): error C2338: static_assert failed: ...`)
// as well as Clang or GCC error output
// (e.g. `file.cc:<line>:<column>: error: static assertion failed: ...`
line.contains(": error")
})
.map(str::to_owned)
.collect()
}
/// Asserts that the C++ compilation succeeded.
///
/// # Panics
///
/// Panics if the C++ compiler reported an error.
pub fn assert_success(&self) {
if !self.0.status.success() {
self.dump_output_and_panic("Compiler reported an error");
}
}
/// Verifies that the compilation failed with a single error, and return the
/// stderr line describing this error.
///
/// Note that different compilers may return slightly different error
/// messages, so tests should be careful to only verify presence of some
/// substrings.
///
/// # Panics
///
/// Panics if there was no error, or if there was more than a single error.
#[must_use]
pub fn expect_single_error(&self) -> String {
let error_lines = self.error_lines();
if error_lines.is_empty() {
self.dump_output_and_panic("No error lines found, despite non-zero exit code?");
}
if error_lines.len() > 1 {
self.dump_output_and_panic("Unexpectedly more than 1 error line was present");
}
// `eprintln` to help with debugging test failues that may happen later.
let single_error_line = error_lines.into_iter().next().unwrap();
eprintln!("Got single error as expected: {single_error_line}");
single_error_line
}
}
|