How to write a death test in gtest for Rust APIs


#1

I ran into a problem when I try writing a death test in gtest for my Rust API. The Rust API simply calls an assert!(false). I used ASSERT_ANY_THROW(....) to catch the thrown exceptions when hitting the assert!(false) in the test:

TEST(gtest_rust, rust_death) {
    ASSERT_ANY_THROW(boom());
}

The problem is that TEST(...) { ASSERT_ANY_THROW(...); } cannot be called twice.

// This test is passed successfully!
TEST(gtest_rust, rust_death) {
    ASSERT_ANY_THROW(boom());
}

// But this test will stop in the middle!
TEST(gtest_rust, rust_death_again) {
    ASSERT_ANY_THROW(boom());
}

The second TEST(...) { ASSERT_ANY_THROW(...); } will just stop in the middle of the test:

[==========] Running 4 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 4 tests from gtest_rust
[ RUN      ] gtest_rust.c_death
[       OK ] gtest_rust.c_death (1 ms)
[ RUN      ] gtest_rust.c_death_again
[       OK ] gtest_rust.c_death_again (1 ms)
[ RUN      ] gtest_rust.rust_death
thread '<unnamed>' panicked at 'assertion failed: false', demo.rs:11:5
note: Run with `RUST_BACKTRACE=1` for a backtrace.
[       OK ] gtest_rust.rust_death (0 ms)
[ RUN      ] gtest_rust.rust_death_again
thread '<unnamed>' panicked at 'assertion failed: false', demo.rs:11:5
stack backtrace:
   0:        0x106dc5333 - std::sys::unix::backtrace::tracing::imp::unwind_backtrace::h4a2f945c17ba2811
   1:        0x106dbcbfc - std::sys_common::backtrace::_print::ha9b171449ed45096
   2:        0x106dc1d41 - std::panicking::default_hook::{{closure}}::hfb91a8b1e6ba36f6
   3:        0x106dc1a3d - std::panicking::default_hook::h345fdd8c0726903f
   4:        0x106dc24ce - std::panicking::rust_panic_with_hook::h60b2c7e9825136db
   5:        0x106d9d047 - std::panicking::begin_panic::h3129e92ff7069e5a
   6:        0x106d9ca53 - boom
   7:        0x106d98de3 - _ZN32gtest_rust_rust_death_again_Test8TestBodyEv
   8:        0x106e83d1d - _ZN7testing8internal38HandleSehExceptionsInMethodIfSupportedINS_4TestEvEET0_PT_MS4_FS3_vEPKc
   9:        0x106e24afa - _ZN7testing8internal35HandleExceptionsInMethodIfSupportedINS_4TestEvEET0_PT_MS4_FS3_vEPKc
  10:        0x106e24a32 - _ZN7testing4Test3RunEv
  11:        0x106e26b9e - _ZN7testing8TestInfo3RunEv
  12:        0x106e2857b - _ZN7testing8TestCase3RunEv
  13:        0x106e42b38 - _ZN7testing8internal12UnitTestImpl11RunAllTestsEv
  14:        0x106e875dd - _ZN7testing8internal38HandleSehExceptionsInMethodIfSupportedINS0_12UnitTestImplEbEET0_PT_MS4_FS3_vEPKc
  15:        0x106e4253a - _ZN7testing8internal35HandleExceptionsInMethodIfSupportedINS0_12UnitTestImplEbEET0_PT_MS4_FS3_vEPKc
  16:        0x106e423fb - _ZN7testing8UnitTest3RunEv
  17:        0x106e06aa0 - _Z13RUN_ALL_TESTSv
  18:        0x106e06a85 - main
thread panicked while panicking. aborting.
run.sh: line 11: 10400 Illegal instruction: 4  ./test_death

I know I can use the internal testing framework in cargo and adding #[test], #[should_panic] to do that job. However, in my real usecase, I have an abstract interface in C for different implementations on different platforms. Most of the platform-dependent implementations are written in C/C++, but one is written in Rust. Therefore, I like to write a death test that works like what #[should_panic] test does, but it can be called via my abstract interface. The gtest’s death test is a good option for me.

The following is the whole code:

#include <cassert>
#include "gtest/gtest.h"

#if defined(__cplusplus)
extern "C" {
#endif

// Call `assert!(false)`
extern void boom(); // exposed from libdemo.a

#if defined(__cplusplus)
}
#endif

void bang() { assert(false); }

TEST(gtest_rust, c_death) {
    ASSERT_DEATH(bang(), "");
}

// It's ok to call `bang()` twice.
TEST(gtest_rust, c_death_again) {
    ASSERT_DEATH(bang(), "");
}

TEST(gtest_rust, rust_death) {
    ASSERT_ANY_THROW(boom());
}

// This test will stop in the middle
// if `boom()` is called twice.
TEST(gtest_rust, rust_death_again) {
    ASSERT_ANY_THROW(boom());
}

Does anyone know how to write a proper death test for boom above ?


#2

I’m not familiar with google’s c++ test suite, but I’m pretty sure I saw somewhere that internally when a panic! is called, it actually spawns a c++ exception, so is there something that can work with a c++ exception?


#3

I think ASSERT_ANY_THROW is used to verify an exception is thrown.

One interesting question is the following code:

void panic() { throw SIGILL; }

TEST(gtest_rust, c_throw_an_exception) {
    ASSERT_ANY_THROW(panic());
}

TEST(gtest_rust, c_throw_an_exception_again) {
    ASSERT_ANY_THROW(panic());
}

works fine, but the code below:

#if defined(__cplusplus)
extern "C" {
#endif

// Call `assert!(false)` in the Rust code
extern void boom();

#if defined(__cplusplus)
}
#endif

TEST(gtest_rust, rust_death) {
    ASSERT_ANY_THROW(boom());
}

// This test will stop in the middle when `boom()` is called again!
TEST(gtest_rust, rust_death_again) {
    ASSERT_ANY_THROW(boom());
}

stops in the middle while running the second test(rust_death_again).


#4

Unfortunately that was as far as my knowledge goes with rust panics in c++…


#5

I just want to make sure I understand what you’re trying to do here. You have some C/C++ code that calls an extern "c" Rust function. The Rust function panics, and you want to catch that panic in C/C++?

If that’s what you want, it’s not possible. Allowing a panic to unwind across an FFI boundary is undefined behavior. What you should do instead is use catch_unwind in your Rust function and return an error value of some sort (or just return an error directly), then check for the error in C++ and potentially throw a C++ exception.


#6

I just want to make sure I understand what you’re trying to do here. You have some C/C++ code that calls an extern "c" Rust function. The Rust function panics, and you want to catch that panic in C/C++?

Yes, I want to know if the Rust function panics or not.

If that’s what you want, it’s not possible. Allowing a panic to unwind across an FFI boundary is undefined behavior. What you should do instead is use catch_unwind in your Rust function and return an error value of some sort (or just return an error directly), then check for the error in C++ and potentially throw a C++ exception.

Thanks for the info! If the panic in the Rust function cannot be caught by try { } catch(...) {}, catch_unwind might be the only way to verify if the Rust function panics or not.