File: mod.rs

package info (click to toggle)
rust-cxx 1.0.192-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 1,808 kB
  • sloc: cpp: 1,625; javascript: 124; sh: 11; makefile: 8
file content (194 lines) | stat: -rw-r--r-- 6,568 bytes parent folder | download | duplicates (7)
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
    }
}