Providing multiple dll PATHs to CTest on Windows

Hello,

I’m attempting to discover GTest unit tests and execute the tests with CTest in Windows for a multi-level library project. I have started at the bottom (lowest level library) to simplify things for the moment.

My issue, which I have “solved”, is for the unit test executable to find the relevant dependency DLLs on Windows when built as shared libraries.

The way my build is structured right now, if the top level project is built as shared libraries, then all dependencies that it builds are also shared. I would prefer to build the dependencies statically and avoid this problem for unit testing, but my attempts to do that and still test the top level project as a DLL failed.

I am able to execute CTest successfully if I add the following to my test project CMakeLists.txt:

target_link_libraries(${PROJECT_NAME} PUBLIC my_library_under_test) #this library links to my_library_two in its build
target_link_libraries(${PROJECT_NAME} PRIVATE GTest::gtest_main GTest::gtest GTest::gmock_main GTest::gmock)

include(GoogleTest)
gtest_add_tests(TARGET ${PROJECT_NAME} TEST_LIST allTests)

if(WIN32)
    # Ensure the required DLLs can be found at runtime
    set_tests_properties(
            ${allTests}
            PROPERTIES ENVIRONMENT "PATH=$<SHELL_PATH:$<TARGET_FILE_DIR:gtest>>$<SEMICOLON>$<SHELL_PATH:$<TARGET_FILE_DIR:gmock>>$<SEMICOLON>$<SHELL_PATH:$<TARGET_FILE_DIR:my_library_two>>$<SEMICOLON>$<SHELL_PATH:$<TARGET_FILE_DIR:my_library_under_test>>$<SEMICOLON>$ENV{PATH}"
    )
endif()

And this all works fine, but because it is a generator expression I cannot figure out how to compose it in pieces, and I get a massively long line. This problem explodes with additional dependencies for the project, so I can only assume I am doing this the wrong way.

So, my question is two fold:

  1. What is the correct way to do this?
    OR
  2. Would it be better to build the test executables statically, and if so, how do I enforce that even for builds that are otherwise creating shared libraries?

To clarify, my CMakeLists.txt for the project allows the library to be built static or dynamic depending on a command line flag to CMake (-DBUILD_SHARED_LIBS=NO/YES). So I can’t really stop someone from building my project with shared libs turned on and then attempting to execute the unit tests (which would fail if not for my poor solution above).

Thanks!

I’ve been working on this some more, and I’ve decided on doing the following. If any of this is a bad idea, please let me know.

I’ve gone with option #2, and figured out how to temporarily enable static builds for dependencies using the following pattern:

# .... rest of file
add_executable(${PROJECT_NAME} ${TEST_SOURCE_FILES})

# Force google test to build statically
set(${PROJECT_NAME}_TEMP_GTEST_BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS})
set(BUILD_SHARED_LIBS NO)
include(FetchContent)
FetchContent_Declare(
        googletest
        URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip
)
FetchContent_MakeAvailable(googletest)
target_link_libraries(${PROJECT_NAME} PRIVATE GTest::gtest_main GTest::gtest GTest::gmock_main GTest::gmock)
set(BUILD_SHARED_LIBS ${PROJECT_NAME}_TEMP_GTEST_BUILD_SHARED_LIBS})
# Restore shared library configuration from temporary variable

# ... repeat for other dependencies if needed

I did this in both my test cmake file, for its own dependencies (just gtest), as well as in the CMakeLists.txt of the project that is being tested by it (which calls add_subdirectory on this Tests/ directory).

And then, at the bottom of my test project CMakeLists.txt, I add the following, which is now limited to exactly one additional PATH environment variable addition since the rest of the deps are built statically now. Since my project under test is a shared library in this case, I wanted that to remain a DLL, so I add that output folder to the PATH for CTest and that’s all that is required.

# Restore shared library configuration from temporary variable

target_link_libraries(${PROJECT_NAME} PUBLIC my_library_under_test)

include(GoogleTest)
gtest_add_tests(TARGET ${PROJECT_NAME} TEST_LIST allTests)

if(WIN32)
    # Ensure the required DLLs can be found at runtime
    set_tests_properties(
            ${allTests}
            PROPERTIES ENVIRONMENT "PATH=$<SHELL_PATH:$<TARGET_FILE_DIR:my_library_under_test>>$<SEMICOLON>$ENV{PATH}"
    )
endif()

Have you tried to use the new TARGET_RUNTIME_DLL_DIRS generator expression ?
You could use that to generate a bat file which then actually runs your executables.
https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html#genex:TARGET_RUNTIME_DLL_DIRS

Other things that may be of interest here:

  • the ENVIRONMENT_MODIFICATION test property to append (or prepend) items to PATH
  • The block command to avoid those _TEMP_*_BUILD_SHARED_LIBS variables
1 Like

Good tips. Thank you Ben!
I ended up putting the dependency grab into a cmake module and pulling it in with fetch content. It’s a custom function now.
That being said, I think your suggestions will still prove useful, particularly for the test environment.

Thanks for the suggestion. I want to avoid additional complexity just for ruining windows tests, if possible. I will take a look at this tomorrow at work, though.