First part, compiling the code#
Integrating GoogleTest into a project that mixes native C++ with managed C++/CLR can get messy, especially when it comes to test detection in Visual Studio’s Test Explorer and to linker errors, and recently I ran into both of them at the same time.
Diagnosing the issue#
Tests not appearing in Test Explorer#
At first my native C++ GoogleTest unit tests were visible in Visual Studio’s Test Explorer, but as soon as I enabled C++/CLR they disappeared. It became clear that the test adapter had trouble finding GoogleTest tests once managed code was part of the project.
Linker errors and an unexpected Catch2 library#
On top of that, the linker started to complain with errors like these:
error LNK2038: mismatch detected for '_ITERATOR_DEBUG_LEVEL': value '0' doesn't match value '2'
error LNK2038: mismatch detected for 'RuntimeLibrary': value 'MD_DynamicRelease' doesn't match value 'MDd_DynamicDebug'You get these when Visual Studio tries to link libraries built with conflicting configurations (Debug against Release). The odd part was that Catch2 ended up in the link even though I had never linked it myself. That’s vcpkg’s AutoLink feature, which scans for libraries and pulls in the ones that match the configuration, and it made the whole thing a lot harder to untangle.
Solution#
In the end I found out that two additional configuration files were enough to fix it.
1. packages.config#
Adding this file explicitly specified the correct GoogleTest version and restored test detection in Visual Studio’s Test Explorer:
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.googletest.v140.windesktop.msvcstl.static.rt-dyn" version="1.8.1.7" targetFramework="native" />
</packages>2. vcpkg.json#
This file declares the dependencies for vcpkg explicitly. I’m not sure it’s necessary, but it for sure helped in my dev environment.
{
"name": "my-project",
"version-string": "0.1.0",
"dependencies": [
"gtest"
]
}Conclusion#
It looks like getting GoogleTest into a mixed C++ and C++/CLR project brings its own set of problems, and in my case two extra files, packages.config and vcpkg.json, were what it took to solve them.
Second part, the great SEH exception mystery#
The first part was about compiling and linking, and this one is about the runtime. Out of nowhere, SEH (Structured Exception Handling) exceptions started to fire the moment the tests ran, which kept even the simplest of them from executing.
What is the Structured Exception Handling (SEH)
Initial checks#
My first move was to confirm that the Visual Studio debugger was set to Mixed mode (Native + Managed). After that I used breakpoints to narrow the problem down, and eventually it turned out that things went wrong whenever my native tests called into managed code.
The test class has a SetUp() method, so I set a breakpoint there.
class UltimateTest : public ::testing::Test
{
protected:
void SetUp() override
{
}
};Nothing broke there, so I removed all the tests and tried the simplest test known to everyone:
TEST(BasicTest, Test_Simple) {
EXPECT_EQ(42, 42);
}No problems with that one, so I added another test based on my UltimateTest fixture:
TEST_F(UltimateTest, Test_Simple_Fixture) {
EXPECT_EQ(42, 42);
}Still no issues, and I moved on to a test that calls into a library written in managed C++:
TEST_F(UltimateTest, Test_Call_Managed) {
double result = ManagedMethods::Calc(ManagedMethods::CALCULATE_THIS_THING, 1.0);
EXPECT_NEAR(result, 3.14159, 1e-5);
}This one wouldn’t even stop on the first line, because the SEH exception arrived before the code could even think about running. By then I was almost sure that the root of the problem wasn’t the adapter or the libraries but something in the project itself. Maybe the dynamic libraries weren’t loading properly, or maybe they weren’t loading at all.
Resolving assembly loading issues#
To find out, I took over assembly loading myself by overriding SetUp() in my test fixture:
void SetUp() override {
System::AppDomain::CurrentDomain->AssemblyResolve += gcnew System::ResolveEventHandler(&UltimateTest::OnAssemblyResolve);
System::Reflection::Assembly::Load("SpecialManagedLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null");
}
static System::Reflection::Assembly^ OnAssemblyResolve(System::Object^ sender, System::ResolveEventArgs^ args) {
System::String^ assemblyPath = System::IO::Path::Combine(
System::IO::Path::GetDirectoryName(System::Reflection::Assembly::GetExecutingAssembly()->Location),
args->Name->Split(',')[0] + ".dll");
if (System::IO::File::Exists(assemblyPath)) {
return System::Reflection::Assembly::LoadFrom(assemblyPath);
} else {
throw gcnew System::IO::FileNotFoundException("Assembly not found: " + assemblyPath);
}
}In the output logs I could see assemblies loading and unloading again right away, together with the "Assembly resolve error:" message, and that pointed to missing dependencies. Making sure that all of the dependent libraries got copied to the output directory finally fixed it. It took me a while to figure that out.
Fiddling with GoogleTest adapters#
While I was chasing this problem I switched the GoogleTest adapter three times:
Microsoft.googletest.v140.windesktop.msvcstl.static.rt-dyn.1.8.1.7Microsoft.googletest.v140.windesktop.msvcstl.dynamic.rt-dyn.1.8.1.7Microsoft.googletest.v140.windesktop.msvcstl.static.rt-static.1.8.1.7
Only static.rt-dyn worked with my project. To be honest I was grasping at straws and changing everything I could, since I had no clue what was behind the SEH chaos.
For a moment I even considered building my own version of GoogleTest from the full sources, just to have the comfort of seeing absolutely everything in the debugger. I ended up pulling version 1.8.1.7 into a separate directory, which gave me a local reference at least. In the end the culprits were mismatched runtime libraries, implicit linking and a few missing dependencies.