Files
libakgl/CMakeLists.txt
Andrew Kesterson 22162db2da Add physics, heap, json_helpers, game, and actor test suites
Raise line coverage from 39.6 to 61.8 percent with four new suites and an
extension to the actor suite, and register every suite through a single
CMake list so a new test file cannot be left out of the coverage fixture.
Give the test targets a build-tree RPATH and prepend the build tree to
LD_LIBRARY_PATH for CTest, so a developer with a previously installed
libakgl.so exercises the library that was just compiled.

Fix six defects the new tests exposed:

- akgl_physics_simulate read self->gravity_time before its NULL check, so a
  NULL backend crashed instead of reporting AKERR_NULLPOINTER.
- akgl_game_save transposed CLEANUP and PROCESS, which placed the fclose
  inside the PROCESS switch. An ordinary save never flushed or closed its
  stream and produced an empty file.
- akgl_game_save_actors wrote each name table terminator from the address of
  a single char, emitting stack contents into the save file and a sentinel
  the loader could not recognize.
- akgl_game_load_objectnamemap used CATCH directly inside while(1), where the
  break leaves the loop rather than propagating, so a truncated name table
  loaded as a successful game.
- akgl_Actor_cmhf_up_on and _down_on dereferenced actor->basechar with no
  NULL check, unlike their left and right counterparts.
- akgl_actor_logic_movement checked actor twice instead of checking
  actor->basechar before dereferencing it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:03:21 -04:00

311 lines
11 KiB
CMake

cmake_minimum_required(VERSION 3.10)
project(akgl LANGUAGES C)
include(CTest)
option(AKGL_COVERAGE "Instrument libakgl and generate coverage reports with CTest" OFF)
if(AKGL_COVERAGE)
if(NOT CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
message(FATAL_ERROR "AKGL_COVERAGE requires GCC or Clang")
endif()
find_program(GCOVR_EXECUTABLE gcovr REQUIRED)
endif()
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
# Vendored projects own their test suites. Suppress their CTest registration
# while embedded so the top-level suite contains only targets built here.
set(AKGL_SUPPRESS_DEPENDENCY_TESTS TRUE)
function(add_test)
if(NOT AKGL_SUPPRESS_DEPENDENCY_TESTS)
_add_test(${ARGV})
endif()
endfunction()
function(set_tests_properties)
if(NOT AKGL_SUPPRESS_DEPENDENCY_TESTS)
_set_tests_properties(${ARGV})
endif()
endfunction()
set(JANSSON_WITHOUT_TESTS ON CACHE BOOL "Do not build vendored Jansson tests" FORCE)
set(JANSSON_EXAMPLES OFF CACHE BOOL "Do not build vendored Jansson examples" FORCE)
set(JANSSON_BUILD_DOCS OFF CACHE BOOL "Do not build vendored Jansson docs" FORCE)
add_subdirectory(deps/jansson EXCLUDE_FROM_ALL)
add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
add_subdirectory(deps/libakstdlib EXCLUDE_FROM_ALL)
add_subdirectory(deps/SDL EXCLUDE_FROM_ALL)
add_subdirectory(deps/SDL_image EXCLUDE_FROM_ALL)
add_subdirectory(deps/SDL_mixer EXCLUDE_FROM_ALL)
add_subdirectory(deps/SDL_ttf EXCLUDE_FROM_ALL)
# Reserve error-name table entries for libakgl-specific status codes.
target_compile_definitions(akerror PUBLIC
AKERR_MAX_ERR_VALUE=256
)
set(AKGL_SUPPRESS_DEPENDENCY_TESTS FALSE)
else()
find_package(PkgConfig REQUIRED)
if(NOT TARGET SDL3::SDL3)
find_package(SDL3 REQUIRED)
endif()
if(NOT TARGET SDL3_image::SDL3_image)
find_package(SDL3_image REQUIRED)
endif()
if(NOT TARGET SDL3_mixer::SDL3_mixer)
find_package(SDL3_mixer REQUIRED)
endif()
if(NOT TARGET SDL3_ttf::SDL3_ttf)
find_package(SDL3_ttf REQUIRED)
endif()
if(NOT TARGET akerror::akerror)
find_package(akerror REQUIRED)
endif()
if(NOT TARGET akstdlib::akstdlib)
find_package(akstdlib REQUIRED)
endif()
if(NOT TARGET jansson::jansson)
find_package(jansson)
endif()
endif()
set(GAMECONTROLLERDB_H "include/akgl/SDL_GameControllerDB.h")
set(prefix ${CMAKE_INSTALL_PREFIX})
set(exec_prefix "\${prefix}")
set(libdir "\${exec_prefix}/lib")
set(includedir "\${prefix}/include")
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/akgl.pc.in ${CMAKE_CURRENT_BINARY_DIR}/akgl.pc @ONLY)
# Tests use both relative paths and SDL_GetBasePath(), so stage fixtures beside
# test executables in every out-of-tree build.
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/tests/assets"
DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
add_custom_command(
OUTPUT ${GAMECONTROLLERDB_H}
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/mkcontrollermappings.sh ${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "Generating controller mappings ..."
)
# Add include directories
include_directories(${SDL3_INCLUDE_DIRS})
add_library(akgl SHARED
deps/semver/semver.c
src/actor.c
src/actor_state_string_names.c
src/text.c
src/assets.c
src/character.c
src/draw.c
src/game.c
src/controller.c
src/heap.c
src/json_helpers.c
src/registry.c
src/renderer.c
src/physics.c
src/sprite.c
src/staticstring.c
src/tilemap.c
src/util.c
${GAMECONTROLLERDB_H}
)
add_library(akgl::akgl ALIAS akgl)
add_executable(charviewer util/charviewer.c)
add_executable(test_semver_unit deps/semver/semver_unit.c)
# Every suite here is a standalone C program named tests/<name>.c, built as
# test_<name> and registered with CTest under <name>.
set(AKGL_TEST_SUITES
actor
bitmasks
character
game
heap
json_helpers
physics
registry
sprite
staticstring
tilemap
util
)
foreach(suite IN LISTS AKGL_TEST_SUITES)
add_executable(test_${suite} tests/${suite}.c)
add_test(NAME ${suite} COMMAND test_${suite})
endforeach()
add_test(NAME semver_unit COMMAND test_semver_unit)
set_tests_properties(
${AKGL_TEST_SUITES} semver_unit
PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests" TIMEOUT 30
)
# Specify include directories for the library's headers (if applicable)
target_include_directories(akgl PUBLIC
include/
deps/semver/
)
if(AKGL_COVERAGE)
target_compile_options(akgl PRIVATE --coverage -O0 -g)
target_link_options(akgl PRIVATE --coverage)
set(AKGL_COVERAGE_DIR "${CMAKE_CURRENT_BINARY_DIR}/coverage")
file(MAKE_DIRECTORY "${AKGL_COVERAGE_DIR}")
add_test(
NAME coverage_reset
COMMAND ${GCOVR_EXECUTABLE}
--root "${CMAKE_CURRENT_SOURCE_DIR}"
--object-directory "${CMAKE_CURRENT_BINARY_DIR}"
--delete
)
set_tests_properties(coverage_reset PROPERTIES FIXTURES_SETUP akgl_coverage)
# Any suite missing from this list runs outside the fixture and has its
# counters discarded by coverage_reset.
set_tests_properties(
${AKGL_TEST_SUITES} semver_unit
PROPERTIES FIXTURES_REQUIRED akgl_coverage
)
add_test(
NAME coverage_report
COMMAND ${GCOVR_EXECUTABLE}
--root "${CMAKE_CURRENT_SOURCE_DIR}"
--object-directory "${CMAKE_CURRENT_BINARY_DIR}"
--filter "${CMAKE_CURRENT_SOURCE_DIR}/src/"
--xml-pretty
--xml "${AKGL_COVERAGE_DIR}/coverage.xml"
--html-details "${AKGL_COVERAGE_DIR}/index.html"
)
set_tests_properties(
coverage_report
PROPERTIES
FIXTURES_CLEANUP akgl_coverage
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
)
endif()
target_link_libraries(akgl
PUBLIC
SDL3::SDL3
SDL3_image::SDL3_image
SDL3_mixer::SDL3_mixer
SDL3_ttf::SDL3_ttf
akstdlib::akstdlib
akerror::akerror
jansson::jansson
)
foreach(suite IN LISTS AKGL_TEST_SUITES)
target_link_libraries(test_${suite} PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
target_include_directories(test_${suite} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/tests")
endforeach()
target_link_libraries(charviewer PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
# When the vendored SDL satellite libraries are built in-tree they land in per-
# project subdirectories that are not on the loader's default search path, so a
# freshly built test aborts before main() with "cannot open shared object file".
# Bake those directories into the build-tree RPATH. Installed builds resolve the
# same libraries through find_package and need no help.
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
set(AKGL_VENDORED_RPATH
"$<TARGET_FILE_DIR:SDL3::SDL3>"
"$<TARGET_FILE_DIR:SDL3_image::SDL3_image>"
"$<TARGET_FILE_DIR:SDL3_ttf::SDL3_ttf>"
"$<TARGET_FILE_DIR:SDL3_mixer::SDL3_mixer>"
"$<TARGET_FILE_DIR:akerror::akerror>"
"$<TARGET_FILE_DIR:akstdlib::akstdlib>"
)
foreach(suite IN LISTS AKGL_TEST_SUITES)
set_target_properties(test_${suite} PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}")
endforeach()
set_target_properties(charviewer akgl PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}")
# RPATH alone is not enough: LD_LIBRARY_PATH is searched first, so a developer
# who has previously run rebuild.sh has an installed libakgl.so ahead of the
# one under test. Prepend the build tree for the CTest run so the suite always
# exercises what was just compiled.
set(AKGL_TEST_LIBPATH
"${CMAKE_CURRENT_BINARY_DIR}"
"${CMAKE_CURRENT_BINARY_DIR}/deps/SDL"
"${CMAKE_CURRENT_BINARY_DIR}/deps/SDL_image"
"${CMAKE_CURRENT_BINARY_DIR}/deps/SDL_ttf"
"${CMAKE_CURRENT_BINARY_DIR}/deps/SDL_mixer"
"${CMAKE_CURRENT_BINARY_DIR}/deps/libakerror"
"${CMAKE_CURRENT_BINARY_DIR}/deps/libakstdlib"
)
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.22")
set(AKGL_TEST_ENV_MOD "")
foreach(dir IN LISTS AKGL_TEST_LIBPATH)
list(APPEND AKGL_TEST_ENV_MOD "LD_LIBRARY_PATH=path_list_prepend:${dir}")
endforeach()
set_tests_properties(
${AKGL_TEST_SUITES}
PROPERTIES ENVIRONMENT_MODIFICATION "${AKGL_TEST_ENV_MOD}"
)
else()
string(REPLACE ";" ":" AKGL_TEST_LIBPATH_JOINED "${AKGL_TEST_LIBPATH}")
set_tests_properties(
${AKGL_TEST_SUITES}
PROPERTIES ENVIRONMENT "LD_LIBRARY_PATH=${AKGL_TEST_LIBPATH_JOINED}:$ENV{LD_LIBRARY_PATH}"
)
endif()
endif()
# Mutation testing copies the repository to scratch space, applies one small
# source change at a time, and verifies that the passing tests detect it. The
# intentionally failing character test is excluded by the harness.
find_package(Python3 COMPONENTS Interpreter)
if(Python3_FOUND)
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
set(AKGL_MUTATION_TARGET mutation)
else()
set(AKGL_MUTATION_TARGET akgl_mutation)
endif()
add_custom_target(${AKGL_MUTATION_TARGET}
COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/scripts/mutation_test.py
--source-root ${CMAKE_CURRENT_SOURCE_DIR}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
USES_TERMINAL
COMMENT "Running mutation tests (breaks a scratch copy, expects tests to fail)"
)
endif()
set(main_lib_dest "lib/akgl-${MY_LIBRARY_VERSION}")
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akgl.pc DESTINATION "lib/pkgconfig/")
install(TARGETS akgl DESTINATION "lib/")
install(FILES "deps/semver/semver.h" DESTINATION "include/")
install(FILES "include/akgl/actor.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/types.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/text.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/assets.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/character.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/error.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/draw.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/game.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/controller.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/heap.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/iterator.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/json_helpers.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/renderer.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/physics.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/registry.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/sprite.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/staticstring.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/tilemap.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/util.h" DESTINATION "include/akgl/")
install(FILES ${GAMECONTROLLERDB_H} DESTINATION "include/akgl/")