Godot Version
4.2.1.stable
Question
How do I set up unit tests for my GDExtension with doctest?
I’m trying to move computationally intensive workloads from GDScript into a GDExtension. As part of this task, I’m trying to add doctest for unit testing. However, I’m missing something, as I keep receiving an error when I execute the tests, and I’m not sure what I’m doing wrong.
I’ve set up a minimally viable project and have the doctest integration in a branch:
How to reproduce
- Clone MVP (
git clone https://github.com/cyberpuffin-digital/gdexample/
) - Checkout doctest branch (
cd gdexample; git checkout doctest
) - Build godot-cpp and gdextension (
make godot_cpp_linux gdextension_linux_debug
) - Execute tests (
make tests
)
Expected results
One test executed and passed: godot::GDExtension::is_prime(int)
Actual results
~/git/gdexample$ make tests
cmake -S ./test -B ./.cmake
-- The CXX compiler identification is GNU 11.4.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Found Git: /usr/bin/git (found version "2.34.1")
-- Godot CPP library found: ~/git/gdexample/godot-cpp/bin/libgodot-cpp.linux.template_debug.x86_64.a
-- GDExample library found: ~/git/gdexample/bin/linux/libgdexample.linux.template_debug.x86_64.so
-- GDExample Include Directories: ~/git/gdexample/test/../src
-- Configuring done
-- Generating done
-- Build files have been written to: ~/git/gdexample/.cmake
cmake --build ./.cmake --verbose
gmake[1]: Entering directory '~/git/gdexample/.cmake'
/usr/bin/cmake -S~/git/gdexample/test -B~/git/gdexample/.cmake --check-build-system CMakeFiles/Makefile.cmake 0
/usr/bin/cmake -E cmake_progress_start ~/git/gdexample/.cmake/CMakeFiles ~/git/gdexample/.cmake//CMakeFiles/progress.marks
/usr/bin/gmake -f CMakeFiles/Makefile2 all
gmake[2]: Entering directory '~/git/gdexample/.cmake'
/usr/bin/gmake -f CMakeFiles/doctest.dir/build.make CMakeFiles/doctest.dir/depend
gmake[3]: Entering directory '~/git/gdexample/.cmake'
cd ~/git/gdexample/.cmake && /usr/bin/cmake -E cmake_depends "Unix Makefiles" ~/git/gdexample/test ~/git/gdexample/test ~/git/gdexample/.cmake ~/git/gdexample/.cmake ~/git/gdexample/.cmake/CMakeFiles/doctest.dir/DependInfo.cmake --color=
gmake[3]: Leaving directory '~/git/gdexample/.cmake'
/usr/bin/gmake -f CMakeFiles/doctest.dir/build.make CMakeFiles/doctest.dir/build
gmake[3]: Entering directory '~/git/gdexample/.cmake'
[ 10%] Creating directories for 'doctest'
...
[ 80%] Completed 'doctest'
/usr/bin/cmake -E make_directory ~/git/gdexample/.cmake/CMakeFiles
/usr/bin/cmake -E touch ~/git/gdexample/.cmake/CMakeFiles/doctest-complete
/usr/bin/cmake -E touch ~/git/gdexample/.cmake/doctest/src/doctest-stamp/doctest-done
gmake[3]: Leaving directory '~/git/gdexample/.cmake'
[ 80%] Built target doctest
/usr/bin/gmake -f CMakeFiles/gdexample_test.dir/build.make CMakeFiles/gdexample_test.dir/depend
gmake[3]: Entering directory '~/git/gdexample/.cmake'
cd ~/git/gdexample/.cmake && /usr/bin/cmake -E cmake_depends "Unix Makefiles" ~/git/gdexample/test ~/git/gdexample/test ~/git/gdexample/.cmake ~/git/gdexample/.cmake ~/git/gdexample/.cmake/CMakeFiles/gdexample_test.dir/DependInfo.cmake --color=
gmake[3]: Leaving directory '~/git/gdexample/.cmake'
/usr/bin/gmake -f CMakeFiles/gdexample_test.dir/build.make CMakeFiles/gdexample_test.dir/build
gmake[3]: Entering directory '~/git/gdexample/.cmake'
[ 90%] Building CXX object CMakeFiles/gdexample_test.dir/src/test_gdexample.cpp.o
/usr/bin/c++ -I~/git/gdexample/.cmake/doctest/src/doctest/doctest -isystem ~/git/gdexample/test/../src -isystem ~/git/gdexample/test/../godot-cpp/gdextension -isystem ~/git/gdexample/test/../godot-cpp/gen/include -isystem ~/git/gdexample/test/../godot-cpp/include -MD -MT CMakeFiles/gdexample_test.dir/src/test_gdexample.cpp.o -MF CMakeFiles/gdexample_test.dir/src/test_gdexample.cpp.o.d -o CMakeFiles/gdexample_test.dir/src/test_gdexample.cpp.o -c ~/git/gdexample/test/src/test_gdexample.cpp
[100%] Linking CXX executable gdexample_test
/usr/bin/cmake -E cmake_link_script CMakeFiles/gdexample_test.dir/link.txt --verbose=1
/usr/bin/c++ CMakeFiles/gdexample_test.dir/src/test_gdexample.cpp.o -o gdexample_test -Wl,-rpath,~/git/gdexample/bin/linux ~/git/gdexample/bin/linux/libgdexample.linux.template_debug.x86_64.so ~/git/gdexample/godot-cpp/bin/libgodot-cpp.linux.template_debug.x86_64.a
/usr/bin/ld: CMakeFiles/gdexample_test.dir/src/test_gdexample.cpp.o: in function `DOCTEST_ANON_FUNC_14()':
test_gdexample.cpp:(.text+0x15e2f): undefined reference to `godot::GDExample::GDExample()'
/usr/bin/ld: test_gdexample.cpp:(.text+0x15eb3): undefined reference to `godot::GDExample::is_prime(int)'
/usr/bin/ld: test_gdexample.cpp:(.text+0x15fd0): undefined reference to `godot::GDExample::is_prime(int)'
/usr/bin/ld: test_gdexample.cpp:(.text+0x160ed): undefined reference to `godot::GDExample::is_prime(int)'
/usr/bin/ld: test_gdexample.cpp:(.text+0x1620a): undefined reference to `godot::GDExample::is_prime(int)'
/usr/bin/ld: test_gdexample.cpp:(.text+0x16327): undefined reference to `godot::GDExample::is_prime(int)'
/usr/bin/ld: test_gdexample.cpp:(.text+0x163cf): undefined reference to `godot::GDExample::~GDExample()'
/usr/bin/ld: test_gdexample.cpp:(.text+0x1668b): undefined reference to `godot::GDExample::~GDExample()'
/usr/bin/ld: CMakeFiles/gdexample_test.dir/src/test_gdexample.cpp.o: in function `DOCTEST_ANON_FUNC_16()':
test_gdexample.cpp:(.text+0x16744): undefined reference to `godot::GDExample::is_prime(int)'
/usr/bin/ld: test_gdexample.cpp:(.text+0x16861): undefined reference to `godot::GDExample::is_prime(int)'
/usr/bin/ld: test_gdexample.cpp:(.text+0x1697e): undefined reference to `godot::GDExample::is_prime(int)'
/usr/bin/ld: test_gdexample.cpp:(.text+0x16a9b): undefined reference to `godot::GDExample::is_prime(int)'
/usr/bin/ld: test_gdexample.cpp:(.text+0x16bb8): undefined reference to `godot::GDExample::is_prime(int)'
collect2: error: ld returned 1 exit status
gmake[3]: *** [CMakeFiles/gdexample_test.dir/build.make:99: gdexample_test] Error 1
gmake[3]: Leaving directory '~/git/gdexample/.cmake'
gmake[2]: *** [CMakeFiles/Makefile2:111: CMakeFiles/gdexample_test.dir/all] Error 2
gmake[2]: Leaving directory '~/git/gdexample/.cmake'
gmake[1]: *** [Makefile:91: all] Error 2
gmake[1]: Leaving directory '~/git/gdexample/.cmake'
make: *** [Makefile:83: tests] Error 2
CMakeLists.txt
cmake_minimum_required(VERSION 3.22.1)
project(gdexample_test VERSION 0.0.1 LANGUAGES CXX)
#########################
##### Setup Doctest #####
#########################
# Pull in doctest repository. Useful for pulling in documentation and updates.
include(ExternalProject)
find_package(Git REQUIRED)
ExternalProject_Add(
doctest
PREFIX ${CMAKE_BINARY_DIR}/doctest
GIT_REPOSITORY https://github.com/doctest/doctest.git
TIMEOUT 10
UPDATE_COMMAND ${GIT_EXECUTABLE} pull
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""
LOG_DOWNLOAD ON
)
# Expose required variable (DOCTEST_INCLUDE_DIR) to parent scope
ExternalProject_Get_Property(doctest source_dir)
set(DOCTEST_INCLUDE_DIR ${source_dir}/doctest CACHE INTERNAL "Path to include folder for doctest")
# Add doctest globally
include_directories(${DOCTEST_INCLUDE_DIR})
##############################
##### Find library files #####
##############################
set(gdexample_DIR ${CMAKE_SOURCE_DIR}/..)
set(godot_cpp_DIR ${CMAKE_SOURCE_DIR}/../godot-cpp)
if (UNIX)
if (CMAKE_SIZEOF_VOID_P EQUAL 4)
find_library(GodotCPP_LIBRARY
NAMES libgodot-cpp.linux.template_debug.x86_32.a
PATHS ${godot_cpp_DIR}/bin
)
find_library(GDExample_LIBRARY
NAMES libgdexample.linux.template_debug.x86_32.so
PATHS ${gdexample_DIR}/bin/linux
)
else()
find_library(GodotCPP_LIBRARY
NAMES libgodot-cpp.linux.template_debug.x86_64.a
PATHS ${godot_cpp_DIR}/bin
)
find_library(GDExample_LIBRARY
NAMES libgdexample.linux.template_debug.x86_64.so
PATHS ${gdexample_DIR}/bin/linux
)
endif()
elseif (WIN32)
if (CMAKE_SIZEOF_VOID_P EQUAL 4)
find_library(GodotCPP_LIBRARY
NAMES libgodot-cpp.windows.template_debug.x86_32.a
PATHS ${godot_cpp_DIR}/bin
)
find_library(GDExample_LIBRARY
NAMES libgdexample.windows.template_debug.x86_32.dll
PATHS ${gdexample_DIR}/bin/windows
)
else()
find_library(GodotCPP_LIBRARY
NAMES libgdexample.windows.template_debug.x86_64.a
PATHS ${godot_cpp_DIR}/bin
)
find_library(GDExample_LIBRARY
NAMES libgdexample.windows.template_debug.x86_64.dll
PATHS ${gdexample_DIR}/bin/windows
)
endif()
else()
message(FATAL_ERROR "Unsupported platform: ${CMAKE_SYSTEM_NAME}")
endif()
if (GodotCPP_LIBRARY)
message(STATUS "Godot CPP library found: ${GodotCPP_LIBRARY}")
else()
message(FATAL_ERROR "Godot CPP library not found. Build Godot CPP first.")
endif()
if (GDExample_LIBRARY)
message(STATUS "GDExample library found: ${GDExample_LIBRARY}")
else()
message(FATAL_ERROR "GDExample library was not found. Build GDExample first.")
endif()
###########################
##### Setup Godot CPP #####
###########################
add_library(Godot_CPP STATIC IMPORTED)
set_target_properties(Godot_CPP PROPERTIES IMPORTED_LOCATION ${GodotCPP_LIBRARY})
target_include_directories(Godot_CPP
INTERFACE ${godot_cpp_DIR}/gdextension/
INTERFACE ${godot_cpp_DIR}/gen/include
INTERFACE ${godot_cpp_DIR}/include
)
include_directories(${godot_cpp_dir})
###########################
##### Setup GDExample #####
###########################
add_library(GDExample SHARED IMPORTED)
set_target_properties(GDExample PROPERTIES IMPORTED_LOCATION ${GDExample_LIBRARY})
target_include_directories(GDExample INTERFACE ${gdexample_DIR}/src)
add_dependencies(GDExample Godot_CPP)
include_directories(${gdexample_DIR}/src)
get_target_property(GDEXAMPLE_INCLUDE_DIRS GDExample INTERFACE_INCLUDE_DIRECTORIES)
message(STATUS "GDExample Include Directories: ${GDEXAMPLE_INCLUDE_DIRS}")
#############################
##### Build test binary #####
#############################
add_executable(${PROJECT_NAME} ${CMAKE_SOURCE_DIR}/src/test_gdexample.cpp)
target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17)
target_link_libraries(${PROJECT_NAME}
PUBLIC GDExample
PUBLIC Godot_CPP
)
#####################
##### Run tests #####
#####################
add_custom_command(
TARGET ${PROJECT_NAME}
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E echo "Running tests..."
COMMAND ${CMAKE_CTEST_COMMAND} --build-config $<CONFIG> --output-on-failure -C $<CONFIG>
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
Implementation notes
- Doctest was chosen as the testing framework because (a) Godot engine seems to utilize doctest, and (b) it was a recommendation from the disord channel.
- The build system used for testing is CMake because (a) it’s the only build system in the doctest documentation, (b) I’m trying to learn the build systems better and I don’t know CMake very well.
- Tests are in the test subdirectory to keep them separate from the source code. I know doctest.h can be added directly to the code, but if I’m not mistaken, that will execute the test suite each time the extension gets loaded rather than by command.
- CMake is pulling the doctest repo because (a) the documentation gets copied for offline use and (b) so I don’t have to worry about doctest version tracking yet.
Other notes
nm
shows theis_prime
symbol on the built library:
~/git/gdexample$ nm demo/bin/linux/libgdexample.linux.template_debug.x86_64.so | grep -i is_prime
0000000000007280 t _ZN5godot9GDExample8is_primeEi