Skip to content

Commit

Permalink
Specify a Gtest factory by its typename, and give access to a wrapper
Browse files Browse the repository at this point in the history
The gtest macro takes as arguments the C++ typename of the TestSuite to
create (which must have a RUST_GTEST_TEST_SUITE_FACTORY() macro
somewhere that will be linked in with the test) and a Rust type which
must be an FFI wrapper around the C++ type. In particular it must be
valid to cast from a pointer to the C++ type into a pointer to the Rust
type.

Since the Rust type is an FFI wrapper and defined by the test file (or
some other crate), it can also expose methods that call through to the
C++ TestSuite class.

R=lukasza@chromium.org

Bug: 1305396
Change-Id: I2ddbfbd99d5fb005a22f822a441b9690544dab49
Cq-Include-Trybots: luci.chromium.try:android-rust-arm-dbg,android-rust-arm-rel,linux-rust-x64-dbg,linux-rust-x64-rel
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3561744
Commit-Queue: danakj <danakj@chromium.org>
Reviewed-by: Łukasz Anforowicz <lukasza@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1002837}
  • Loading branch information
danakj authored and Chromium LUCI CQ committed May 12, 2022
1 parent 30ea019 commit 2f9c627
Show file tree
Hide file tree
Showing 11 changed files with 420 additions and 151 deletions.
11 changes: 9 additions & 2 deletions testing/rust_gtest_interop/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,17 @@ if (enable_rust) {
}

mixed_static_library("test_support") {
rs_crate_name = "rust_gtest_interop_test_support"

testonly = true
sources = [
"test/test_factory.cc",
"test/test_factory.h",
"test/test_subclass.cc",
"test/test_subclass.h",
]
rs_crate_root = "test/test_subclass.rs"
rs_sources = [ "test/test_subclass.rs" ]
rs_cxx_bindings = [ "test/test_subclass.rs" ]
rs_deps = [ ":rust_gtest_interop" ]
public_deps = [
":rust_gtest_interop",
"//testing/gtest",
Expand All @@ -71,6 +77,7 @@ if (enable_rust) {
"//testing/gtest",
]

rs_deps = [ ":test_support" ]
rs_sources = [ "rust_gtest_interop_unittest.rs" ]
}
}
89 changes: 67 additions & 22 deletions testing/rust_gtest_interop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,36 +142,81 @@ src/

### Specifying a C++ TestSuite class

In C++, a chosen TestSuite, which subclasses `testing::Test`, can be specified
In C++, a specific TestSuite, which subclasses `testing::Test`, can be specified
with the `TEST_F()` macro. For example `TEST_F(SomeSubclassOfTestingTest,
Gadgets)`. The same can be done in Rust, albeit with a slight bit more
indirection. The `#[gtest_suite]` macro can be specified on the test function,
after the `#[gtest]` macro, in order to chose the TestSuite class. The macro
takes an argument which is the name of a C++ function that returns the output of
`rust_gtest_interop::rust_gtest_factory_for_subclass<T>()` where `T` is the
class to use as the TestSuite. For example:

In a C++ file:
Gadgets)`. The same can be done in Rust, by specifying a Rust wrapper around a
C++ class with the `#[gtest_suite]` macro. This macro is specified on the test
function, and comes after the `#[gtest]` macro. The macro takes an argument
which is the path to a Rust type that stands in for the C++ subclass of
`::testing::Test`.

To connect the C++ and Rust sides together:
1) On the C++ side, the class must subclass `testing::Test`, just as it would
for the `TEST_F()` macro.
2) Also on the C++ side, the implementation of the class (with name `ClassName`)
must include the use of the macro `RUST_GTEST_TEST_SUITE_FACTORY(ClassName)`,
which generates the factory function for Gtest.
3) On the Rust side, the C++-wrapper type must implement the unsafe
`rust_gtest_interop::TestSuite` trait. It should be implemented by using the
`#[extern_test_suite()]` macro, with the macro receiving as input the full
path of the C++ class which the Rust type is wrapping. For example
`#[extern_test_suite("some::ClassName")]`.

A full example:


```cpp
class ChosenClass: public testing::Test {};
// C++ header file for a TestSuite class.
namespace custom {

class CustomTestSuite: public testing::Test {};
CustomTestSuite();
}
```
```cpp
// C++ implementation file for a TestSuite class.
namespace custom {
CustomTestSuite::CustomTestSuite() = default;
RUST_GTEST_TEST_SUITE_FACTORY(CustomTestSuite);
/// This function can be used in #[gtest_suite].
extern "C" testing::Test* chosen_class_gtest_factory(void(*f)()) {
return rust_gtest_interop::rust_gtest_factory_for_subclass<ChosenClass>(f);
}
```

In Rust tests:
```rs
use rust_gtest_interop::*;
// Rust wrapper around the TestSuite class.
use rust_gtest_interop::prelude::*;

// Defines the Rust ffi::CustomTestSuite type that maps to the C++ class.
#[cxx::bridge]
mod ffi {
unsafe extern "C++" {
include!("path/to/custom_test_suite.h")
#[namespace="custom"]
type CustomTestSuite;
}
}
// Mark the CustomTestSuite type as being a Gtest TestSuite, which means it
// must subclass `testing::Test`.
#[extern_test_suite("custom::CustomTestSuite")]
unsafe impl rust_gtest_interop::TestSuite for ffi::CustomTestSuite {}
```

#[gtest(ChosenClassTest, Gadgets)]
#[gtest_suite(chosen_class_gtest_factory)]
fn test() {
// This test uses ChosenClass as its TestSuite.
```rs
// Rust unittests.
use rust_gtest_interop::prelude::*;

#[gtest(CustomTest, Gadgets)]
#[gtest_suite(ffi::CustomTestSuite)]
fn test(ts: Pin<&mut ffi::CustomTestSuite>) {
// This test uses CustomTestSuite as its TestSuite, and can access any exposed
// methods through its `ts` argument.
}
```

Then the `ChosenClassTest.Gadgets` test will run with `ChosenClass` as its
TestSuite class. Note that the C++ function must be marked `extern "C"` at this
time, until we can generate access to C++-mangled functions from Rust.
Then the `CustomTest.Gadgets` test will run with `CustomTestSuite` as its
TestSuite class. Since the cxx generator is used here, the rust file containing
the `#[cxx::bridge]` must also be added to the GN `cxx_bindings` variable (in
addition to `rs_sources`).

0 comments on commit 2f9c627

Please sign in to comment.