Files
libakstdlib/CMakeLists.txt
Andrew Kesterson 68009ea0e3 Add mutation testing to the Gitea workflow
A separate mutation_test job runs scripts/mutation_test.py over src/stdlib.c
with a --threshold gate and publishes a JUnit report, mirroring libakerror's
CI. Checkout needs submodules: recursive -- the harness copies the repo and
builds the copy top-level, so deps/libakerror has to be there.

Measured score on the section 1.0 suite is 46.8% (81/173 killed). The gate is
set at 40: a regression ratchet, not a quality bar. Survivors cluster in the
wrappers that have no tests yet (printf family, ato* family, realpath, the
memory and stream wrappers); the list and tree code that section 1.0 does
cover leaves only 5 survivors between them.

Also caps every test with TIMEOUT 30. Measuring the score turned this up: a
mutant deleting `fast = fast->next->next` makes aksl_list_iterate spin
forever, so ctest never returned, the harness killed ctest at its own 120s
timeout, and the orphaned test binary kept burning a core while holding the
pipe open -- the run wedged and needed killing by hand. With a per-test
timeout ctest reaps its own child, those mutants are killed in 30s, and
nothing is left behind. It guards the real suite against the same shape of
bug too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 10:35:46 -04:00

192 lines
7.3 KiB
CMake

cmake_minimum_required(VERSION 3.10)
project(akstdlib LANGUAGES C)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -ggdb -pg")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -g -ggdb -pg")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -g -ggdb -pg")
# Sanitizer build, off by default:
# cmake -S . -B build-asan -DAKSL_SANITIZE=ON && ctest --test-dir build-asan
# Set before the dependency is added so libakerror is instrumented too --
# several of the defects in TODO.md section 2 (the uninitialised %s in
# aksl_realpath, the unbounded vsprintf in aksl_sprintf, the missing va_end in
# the printf family) only show up under ASan/UBSan.
option(AKSL_SANITIZE "Build the library and its tests with ASan + UBSan" OFF)
if(AKSL_SANITIZE)
set(AKSL_SANITIZE_FLAGS "-fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${AKSL_SANITIZE_FLAGS}")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${AKSL_SANITIZE_FLAGS}")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${AKSL_SANITIZE_FLAGS}")
message(STATUS "AKSL_SANITIZE=ON: building with ASan + UBSan")
endif()
if(TARGET akerror::akerror)
message(STATUS "FOUND akerror::akerror")
else()
message(STATUS "MISSING akerror::akerror")
endif()
include(CTest)
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
# libakerror registers its own CTest entries unconditionally, and because we
# pull it in EXCLUDE_FROM_ALL its test binaries are never built -- so every
# one of them used to show up in our suite as "Not Run" and fail. CMake has no
# way to un-register a test, and set_tests_properties cannot reach across
# directory scopes, so add_test() is shadowed for the duration of the
# add_subdirectory() call. The dependency has its own CI; this project's suite
# should only contain this project's tests.
set(AKSL_SUPPRESS_ADD_TEST TRUE)
function(add_test)
if(NOT AKSL_SUPPRESS_ADD_TEST)
_add_test(${ARGV})
endif()
endfunction()
# libakerror also marks two of those tests WILL_FAIL, which would now be
# setting properties on tests that no longer exist, so suppress that too.
function(set_tests_properties)
if(NOT AKSL_SUPPRESS_ADD_TEST)
_set_tests_properties(${ARGV})
endif()
endfunction()
add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
set(AKSL_SUPPRESS_ADD_TEST FALSE)
else()
if(NOT TARGET akerror::akerror)
find_package(PkgConfig REQUIRED)
find_package(akerror REQUIRED)
endif()
endif()
set(akstdlib_install_cmakedir "${CMAKE_INSTALL_LIBDIR}/cmake/akstdlib")
set(prefix ${CMAKE_INSTALL_PREFIX})
set(exec_prefix "\${prefix}")
set(libdir "\${exec_prefix}/lib")
set(includedir "\${prefix}/include")
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/akstdlib.pc.in ${CMAKE_CURRENT_BINARY_DIR}/akstdlib.pc @ONLY)
add_library(akstdlib SHARED
src/stdlib.c
)
add_library(akstdlib::akstdlib ALIAS akstdlib)
# Specify include directories for the library's headers (if applicable)
target_include_directories(akstdlib PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/>
)
target_link_libraries(akstdlib PUBLIC akerror::akerror)
set(main_lib_dest "lib/my_library-${MY_LIBRARY_VERSION}")
install(TARGETS akstdlib
EXPORT akstdlibTargets
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
install(FILES "include/akstdlib.h" DESTINATION "include/")
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akstdlib.pc DESTINATION "lib/pkgconfig/")
install(EXPORT akstdlibTargets
FILE akstdlibTargets.cmake
NAMESPACE akstdlib::
DESTINATION ${akstdlib_install_cmakedir}
)
configure_package_config_file(
cmake/akstdlib.cmake.in
"${CMAKE_CURRENT_BINARY_DIR}/akstdlibConfig.cmake"
INSTALL_DESTINATION ${akstdlib_install_cmakedir}
)
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/akstdlibConfig.cmake"
DESTINATION ${akstdlib_install_cmakedir}
)
# Each test is one source file tests/test_<name>.c, built into the executable
# test_<name> and registered as the CTest test <name>. Shared helpers live in
# tests/aksl_capture.h.
#
# AKSL_TESTS must exit 0.
# AKSL_WILL_FAIL_TESTS expected to abort by design (an unhandled error
# reaching FINISH_NORETURN, a deliberate contract
# violation), so a non-zero exit is a pass.
# AKSL_KNOWN_FAILING_TESTS assert the *correct* behaviour of a confirmed
# defect from TODO.md section 2.1. They fail until
# the defect is fixed, and are marked WILL_FAIL so
# the suite stays green and the gap stays visible.
# When one is fixed CTest reports it as failed with
# "unexpectedly passed" -- that is the cue to move
# it up into AKSL_TESTS.
set(AKSL_TESTS
linkedlist
tree
)
set(AKSL_WILL_FAIL_TESTS
)
set(AKSL_KNOWN_FAILING_TESTS
list_append_chain # TODO.md 2.1.1 -- append truncates lists of 2+ nodes
list_iterate_head # TODO.md 2.1.2 -- iterate starts at the midpoint
tree_iterate_break # TODO.md 2.1.3 -- ITERATOR_BREAK does not stop a walk
)
foreach(_test IN LISTS AKSL_TESTS AKSL_WILL_FAIL_TESTS AKSL_KNOWN_FAILING_TESTS)
add_executable(test_${_test} tests/test_${_test}.c)
target_include_directories(test_${_test} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests)
target_link_libraries(test_${_test} PRIVATE akstdlib)
add_test(NAME ${_test} COMMAND test_${_test})
endforeach()
if(AKSL_WILL_FAIL_TESTS OR AKSL_KNOWN_FAILING_TESTS)
set_tests_properties(
${AKSL_WILL_FAIL_TESTS} ${AKSL_KNOWN_FAILING_TESTS}
PROPERTIES WILL_FAIL TRUE
)
endif()
# Cap every test. The list and tree code is full of loops whose termination
# depends on a single condition, so a plausible bug -- or a mutant from the
# target below -- turns a test into an infinite loop. Without this, ctest waits
# forever; the mutation harness then kills ctest at its own timeout and the
# spinning test binary is left orphaned, holding the pipe open and burning a
# core. The whole suite runs in well under a second, so 30s is pure headroom.
set_tests_properties(
${AKSL_TESTS} ${AKSL_WILL_FAIL_TESTS} ${AKSL_KNOWN_FAILING_TESTS}
PROPERTIES TIMEOUT 30
)
# Mutation testing: break the library in small ways and confirm the test suite
# notices. This is a meta-check on the tests themselves, and it rebuilds and
# re-runs the whole suite once per mutant, so it is a manual target rather than
# a CTest test:
# cmake --build build --target mutation
# This target covers both src/stdlib.c and include/akstdlib.h. CI runs the
# narrower, faster src/stdlib.c set with a --threshold gate; see
# .gitea/workflows/ci.yaml.
find_package(Python3 COMPONENTS Interpreter)
if(Python3_FOUND)
add_custom_target(mutation
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 the library, expects tests to fail)"
)
endif()
# pkgconfig