danakj | 71dabfa | 2022-02-24 14:32:14 | [diff] [blame] | 1 | // Copyright 2022 The Chromium Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | /// Use `prelude:::*` to get access to all macros defined in this crate. |
| 6 | pub mod prelude { |
| 7 | // The #[gtest(TestSuite, TestName)] macro. |
| 8 | pub use gtest_attribute::gtest; |
| 9 | // Gtest expectation macros, which should be used to verify test expectations. These replace the |
| 10 | // standard practice of using assert/panic in Rust tests which would crash the test binary. |
| 11 | pub use crate::expect_eq; |
| 12 | pub use crate::expect_false; |
| 13 | pub use crate::expect_ge; |
| 14 | pub use crate::expect_gt; |
| 15 | pub use crate::expect_le; |
| 16 | pub use crate::expect_lt; |
| 17 | pub use crate::expect_ne; |
| 18 | pub use crate::expect_true; |
| 19 | } |
| 20 | |
| 21 | // The gtest_attribute proc-macro crate makes use of small_ctor, with a path through this crate here |
| 22 | // to ensure it's available. |
| 23 | #[doc(hidden)] |
| 24 | pub extern crate small_ctor; |
| 25 | |
| 26 | pub trait TestResult { |
| 27 | fn into_error_message(self) -> Option<String>; |
| 28 | } |
| 29 | impl TestResult for () { |
| 30 | fn into_error_message(self) -> Option<String> { |
| 31 | None |
| 32 | } |
| 33 | } |
| 34 | // This impl requires an `Error` not just a `String` so that in the future we could print things |
| 35 | // like the backtrace too (though that field is currently unstable). |
| 36 | impl<E: Into<Box<dyn std::error::Error>>> TestResult for std::result::Result<(), E> { |
| 37 | fn into_error_message(self) -> Option<String> { |
| 38 | match self { |
| 39 | Ok(_) => None, |
| 40 | Err(e) => Some(format!("Test returned error: {}", e.into())), |
| 41 | } |
| 42 | } |
| 43 | } |
| 44 | |
danakj | 8029f80 | 2022-04-01 14:43:43 | [diff] [blame] | 45 | /// Matches the C++ type `rust_gtest_interop::GtestFactoryFunction`, except replaces the return |
| 46 | /// type with an opaque `GtestSuitePtr` |
| 47 | /// |
| 48 | /// The actual return type is `testing::Test*` but we don't know that type in Rust currently, as we |
| 49 | /// don't have a Rust generator generating access to that type. |
| 50 | pub type GtestFactoryFunction = unsafe extern "C" fn(f: extern "C" fn()) -> GtestSuitePtr; |
| 51 | |
| 52 | /// Opaque replacement of a C++ `testing::Test*` pointer type. Only appears in the |
| 53 | /// GtestFactoryFunction signature, which is a function pointer that passed to C++, and never run |
| 54 | /// from within Rust. |
| 55 | #[repr(C)] |
| 56 | pub struct GtestSuitePtr(usize); |
| 57 | |
danakj | 71dabfa | 2022-02-24 14:32:14 | [diff] [blame] | 58 | // Internals used by code generated from the gtest-attriute proc-macro. Should not be used by |
| 59 | // human-written code. |
| 60 | #[doc(hidden)] |
| 61 | pub mod __private { |
danakj | 8029f80 | 2022-04-01 14:43:43 | [diff] [blame] | 62 | use super::GtestFactoryFunction; |
| 63 | |
danakj | 71dabfa | 2022-02-24 14:32:14 | [diff] [blame] | 64 | #[cxx::bridge(namespace=rust_gtest_interop)] |
| 65 | mod ffi { |
| 66 | unsafe extern "C++" { |
| 67 | include!("testing/rust_gtest_interop/rust_gtest_interop.h"); |
| 68 | // TODO(danakj): C++ wants an int, but cxx doesn't support c_int, so we use i32. |
| 69 | // Similarly, C++ wants a char* but cxx doesn't support c_char, so we use u8. |
| 70 | // https://github.com/dtolnay/cxx/issues/1015 |
| 71 | unsafe fn rust_gtest_add_failure_at(file: *const u8, line: i32, message: &str); |
| 72 | } |
| 73 | } |
| 74 | |
| 75 | /// Rust wrapper around the same C++ method. |
| 76 | /// |
| 77 | /// We have a wrapper to convert the file name into a C++-friendly string, and the line number |
| 78 | /// into a C++-friendly signed int. |
| 79 | /// |
| 80 | /// TODO(crbug.com/1298175): We should be able to receive a C++-friendly file path. |
| 81 | /// |
| 82 | /// TODO(danakj): We should be able to pass a `c_int` directly to C++: |
| 83 | /// https://github.com/dtolnay/cxx/issues/1015. |
| 84 | pub fn add_failure_at(file: &'static str, line: u32, message: &str) { |
| 85 | // TODO(danakj): Our own file!() macro should strip "../../" from the front of the string. |
| 86 | let file = file.replace("../", ""); |
| 87 | // TODO(danakj): Write a file!() macro that null-terminates the string so we can use it here |
| 88 | // directly and also for constructing base::Location. Then.. pass a base::Location here? |
| 89 | let null_term_file = std::ffi::CString::new(file).unwrap(); |
| 90 | unsafe { |
| 91 | ffi::rust_gtest_add_failure_at( |
| 92 | null_term_file.as_ptr() as *const u8, |
| 93 | line.try_into().unwrap_or(-1), |
| 94 | message, |
| 95 | ) |
| 96 | } |
| 97 | } |
| 98 | |
danakj | 71dabfa | 2022-02-24 14:32:14 | [diff] [blame] | 99 | /// Wrapper that calls C++ rust_gtest_add_test(). |
danakj | 84ba211 | 2022-03-15 18:40:39 | [diff] [blame] | 100 | /// |
| 101 | /// Note that the `factory` parameter is actually a C++ function pointer, of type |
danakj | 8029f80 | 2022-04-01 14:43:43 | [diff] [blame] | 102 | /// rust_gtest_interop::GtestFactoryFunction. |
danakj | 84ba211 | 2022-03-15 18:40:39 | [diff] [blame] | 103 | /// |
| 104 | /// # Safety |
| 105 | /// |
danakj | 8029f80 | 2022-04-01 14:43:43 | [diff] [blame] | 106 | /// The `factory` function pointer is to a C++ function that returns a `testing::Test*` |
| 107 | /// disguised as a `GtestSuitePtr` since we don't have generated bindings for the |
| 108 | /// `testing::Test` class. |
danakj | 84ba211 | 2022-03-15 18:40:39 | [diff] [blame] | 109 | /// |
| 110 | /// TODO(danakj): We do this by hand because cxx doesn't support passing raw function pointers |
| 111 | /// nor passing `*const c_char`: https://github.com/dtolnay/cxx/issues/1011 and |
| 112 | /// https://github.com/dtolnay/cxx/issues/1015. |
| 113 | unsafe fn rust_gtest_add_test( |
danakj | 8029f80 | 2022-04-01 14:43:43 | [diff] [blame] | 114 | factory: GtestFactoryFunction, |
danakj | 71dabfa | 2022-02-24 14:32:14 | [diff] [blame] | 115 | func: extern "C" fn(), |
| 116 | test_suite_name: *const std::os::raw::c_char, |
| 117 | test_name: *const std::os::raw::c_char, |
| 118 | file: *const std::os::raw::c_char, |
| 119 | line: i32, |
| 120 | ) { |
danakj | 84ba211 | 2022-03-15 18:40:39 | [diff] [blame] | 121 | extern "C" { |
| 122 | /// The C++ mangled name for rust_gtest_interop::rust_gtest_add_test(). This comes from |
| 123 | /// `objdump -t` on the C++ object file. |
| 124 | fn _ZN18rust_gtest_interop19rust_gtest_add_testEPFPN7testing4TestEPFvvEES4_PKcS8_S8_i( |
danakj | 8029f80 | 2022-04-01 14:43:43 | [diff] [blame] | 125 | factory: GtestFactoryFunction, |
danakj | 84ba211 | 2022-03-15 18:40:39 | [diff] [blame] | 126 | func: extern "C" fn(), |
| 127 | test_suite_name: *const std::os::raw::c_char, |
| 128 | test_name: *const std::os::raw::c_char, |
| 129 | file: *const std::os::raw::c_char, |
| 130 | line: i32, |
| 131 | ); |
danakj | 71dabfa | 2022-02-24 14:32:14 | [diff] [blame] | 132 | } |
danakj | 84ba211 | 2022-03-15 18:40:39 | [diff] [blame] | 133 | |
| 134 | _ZN18rust_gtest_interop19rust_gtest_add_testEPFPN7testing4TestEPFvvEES4_PKcS8_S8_i( |
| 135 | factory, |
| 136 | func, |
| 137 | test_suite_name, |
| 138 | test_name, |
| 139 | file, |
| 140 | line, |
| 141 | ) |
danakj | 71dabfa | 2022-02-24 14:32:14 | [diff] [blame] | 142 | } |
| 143 | |
| 144 | /// Information used to register a function pointer as a test with the C++ Gtest framework. |
| 145 | pub struct TestRegistration { |
| 146 | pub func: extern "C" fn(), |
| 147 | // TODO(danakj): These a C-String-Literals. Maybe we should expose that as a type somewhere. |
| 148 | pub test_suite_name: &'static [std::os::raw::c_char], |
| 149 | pub test_name: &'static [std::os::raw::c_char], |
| 150 | pub file: &'static [std::os::raw::c_char], |
| 151 | pub line: u32, |
danakj | 8029f80 | 2022-04-01 14:43:43 | [diff] [blame] | 152 | pub factory: GtestFactoryFunction, |
danakj | 71dabfa | 2022-02-24 14:32:14 | [diff] [blame] | 153 | } |
| 154 | |
| 155 | /// Register a given test function with the C++ Gtest framework. |
| 156 | /// |
| 157 | /// This function is called from static initializers. It may only be called from the main |
| 158 | /// thread, before main() is run. It may not panic, or call anything that may panic. |
| 159 | pub fn register_test(r: TestRegistration) { |
| 160 | let line = r.line.try_into().unwrap_or(-1); |
danakj | 8029f80 | 2022-04-01 14:43:43 | [diff] [blame] | 161 | // SAFETY: The `factory` parameter to rust_gtest_add_test() must be a C++ function that |
| 162 | // returns a `testing::Test*` disguised as a `GTestSuitePtr`. The #[gtest] macro will use |
| 163 | // `rust_gtest_interop::rust_gtest_default_factory()` by default. |
danakj | 84ba211 | 2022-03-15 18:40:39 | [diff] [blame] | 164 | unsafe { |
danakj | 84ba211 | 2022-03-15 18:40:39 | [diff] [blame] | 165 | rust_gtest_add_test( |
danakj | 8029f80 | 2022-04-01 14:43:43 | [diff] [blame] | 166 | r.factory, |
danakj | 84ba211 | 2022-03-15 18:40:39 | [diff] [blame] | 167 | r.func, |
| 168 | r.test_suite_name.as_ptr(), |
| 169 | r.test_name.as_ptr(), |
| 170 | r.file.as_ptr(), |
| 171 | line, |
| 172 | ) |
| 173 | }; |
danakj | 71dabfa | 2022-02-24 14:32:14 | [diff] [blame] | 174 | } |
| 175 | } |
| 176 | |
| 177 | mod expect_macros; |