project() now carries VERSION 0.1.0, and is the single place a version number is spelled. It flows into generated version macros, the shared library's VERSION/SOVERSION, the Version: field in akstdlib.pc, and a new akstdlibConfigVersion.cmake. Before this @PROJECT_VERSION@ expanded to nothing, so akstdlib.pc shipped an empty Version: and libakstdlib.so carried no soname at all. 0.x on purpose: TODO.md section 2.1 still records four confirmed defects whose fixes change documented behaviour, so the API is not being promised yet. While the major version is 0 the soname carries MAJOR.MINOR -- 0.1 and 0.2 are different ABIs -- and becomes MAJOR alone at 1.0. The if() in CMakeLists.txt and the #if in tests/test_version.c encode that rule and are tested against each other. include/akstdlib_version.h.in is configured into the build tree as akstdlib_version.h and installed beside akstdlib.h. It defines AKSL_VERSION_MAJOR/MINOR/PATCH/STRING/NUMBER and AKSL_VERSION_SONAME. AKSL_VERSION_NUMBER is computed rather than written as a literal, because a literal 000100 is octal in C and would make 0.1.0 compare as 64; test_version.c asserts it against the runtime components, so a rewrite to a literal fails. Those macros record what a caller was compiled against. aksl_version(), aksl_version_string() and aksl_version_soname() report what actually loaded, and AKSL_VERSION_CHECK() compares the two, raising AKERR_VALUE naming both. It is a macro so that it expands at the caller's site and captures the caller's numbers; the function compares them against the ones baked into the library. Compatibility is "same soname", so patch is ignored -- a caller built against 0.1.0 keeps working against 0.1.7. Normally the soname catches a mismatch at load time and the check never fires. It earns its keep when the soname is bypassed: a 0.2.0 build dropped in under the 0.1 filename loads happily, and only the check notices. write_basic_package_version_file() uses SameMinorVersion to mirror the soname, falling back to ExactVersion below CMake 3.11 where that mode does not exist. The fallback is stricter than the soname rule -- it pins the patch level too -- but never laxer, and wrongly refusing a good pairing beats wrongly accepting a bad one. Coverage of src/stdlib.c rose to 99.1% of lines (217/219), 45.1% of branches and 25/25 functions. That puts branch coverage back over the old 45 gate, but the gate stays at 40: 0.1 points of headroom is not a ratchet. ctest 14/14, ASan+UBSan 14/14, coverage 16/16 at 90/40. Also verified out of tree: SONAME libakstdlib.so.0.1 recorded in consumers, pkg-config --modversion reporting 0.1.0, find_package(akstdlib 0.1) accepted with 0.2 and 1.0 refused, a patch-bumped 0.1.1 loading and passing the check, a 0.2.0 dropped in under the 0.1 filename caught by it, and an embedded add_subdirectory build keeping its own version rather than the parent's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
416 lines
18 KiB
CMake
416 lines
18 KiB
CMake
cmake_minimum_required(VERSION 3.10)
|
|
# The single source of truth for the version. It flows from here into
|
|
# include/akstdlib_version.h (generated from the .in template next to it), the
|
|
# shared library's VERSION/SOVERSION, the Version: field in akstdlib.pc, and
|
|
# akstdlibConfigVersion.cmake. Nothing else should spell a version number.
|
|
#
|
|
# 0.x on purpose: TODO.md section 2.1 still records four confirmed defects whose
|
|
# fixes change documented behaviour (aksl_atoi's contract, aksl_realpath's
|
|
# signature), so this library is not promising a stable API yet.
|
|
project(akstdlib VERSION 0.1.0 LANGUAGES C)
|
|
|
|
# The granularity at which the ABI is allowed to break, and therefore the
|
|
# soname: libakstdlib.so.0.1. Pre-1.0 that is MAJOR.MINOR, because a 0.x library
|
|
# makes no compatibility promise across a minor bump. At 1.0 this becomes
|
|
# ${PROJECT_VERSION_MAJOR} alone -- change it here, and AKSL_VERSION_SONAME in
|
|
# the header template follows automatically because it is configured from this.
|
|
if(PROJECT_VERSION_MAJOR EQUAL 0)
|
|
set(AKSL_SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
|
|
else()
|
|
set(AKSL_SOVERSION "${PROJECT_VERSION_MAJOR}")
|
|
endif()
|
|
|
|
set(AKSL_GENERATED_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated/include")
|
|
set(AKSL_VERSION_HEADER "${AKSL_GENERATED_INCLUDE_DIR}/akstdlib_version.h")
|
|
configure_file(
|
|
"${CMAKE_CURRENT_SOURCE_DIR}/include/akstdlib_version.h.in"
|
|
"${AKSL_VERSION_HEADER}"
|
|
@ONLY
|
|
)
|
|
|
|
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()
|
|
|
|
# Coverage build, off by default:
|
|
# cmake -S . -B build-coverage -DAKSL_COVERAGE=ON
|
|
# cmake --build build-coverage --target coverage
|
|
# Instrumentation is applied per target below (the library and the test
|
|
# binaries) rather than through CMAKE_C_FLAGS, so deps/libakerror is left
|
|
# uninstrumented -- it has its own suite, and its .gcda would only be noise in
|
|
# this project's report.
|
|
option(AKSL_COVERAGE "Build the library and its tests with gcov instrumentation" OFF)
|
|
# Minimum total line / branch coverage for the `coverage_report` CTest entry
|
|
# that a -DAKSL_COVERAGE=ON build adds. 0 disables the gate and reports only.
|
|
set(AKSL_COVERAGE_THRESHOLD 0 CACHE STRING
|
|
"Fail the coverage_report test below this total line coverage percentage")
|
|
set(AKSL_COVERAGE_BRANCH_THRESHOLD 0 CACHE STRING
|
|
"Fail the coverage_report test below this total branch coverage percentage")
|
|
|
|
if(AKSL_COVERAGE)
|
|
if(NOT CMAKE_C_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$")
|
|
message(FATAL_ERROR
|
|
"AKSL_COVERAGE=ON needs a gcov-compatible compiler; "
|
|
"CMAKE_C_COMPILER_ID is ${CMAKE_C_COMPILER_ID}")
|
|
endif()
|
|
# -O0 because the optimizer folds and reorders lines until per-line counts
|
|
# stop matching the source. -fprofile-abs-path makes gcov record absolute
|
|
# source paths, which is what lets the report be read from any directory.
|
|
set(AKSL_COVERAGE_COMPILE_FLAGS --coverage -O0 -g)
|
|
if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
|
|
list(APPEND AKSL_COVERAGE_COMPILE_FLAGS -fprofile-abs-path)
|
|
endif()
|
|
message(STATUS "AKSL_COVERAGE=ON: instrumenting akstdlib and its tests for gcov")
|
|
endif()
|
|
|
|
# Add gcov instrumentation to one target. No-op unless AKSL_COVERAGE is set.
|
|
function(aksl_target_coverage target)
|
|
if(NOT AKSL_COVERAGE)
|
|
return()
|
|
endif()
|
|
target_compile_options(${target} PRIVATE ${AKSL_COVERAGE_COMPILE_FLAGS})
|
|
if(CMAKE_VERSION VERSION_LESS 3.13)
|
|
set_property(TARGET ${target} APPEND_STRING PROPERTY LINK_FLAGS " --coverage")
|
|
else()
|
|
target_link_options(${target} PRIVATE --coverage)
|
|
endif()
|
|
endfunction()
|
|
|
|
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()
|
|
|
|
# libakerror namespaces its `mutation` target when it is embedded but not its
|
|
# `coverage` target (deps/libakerror/CMakeLists.txt:172 vs :189), so a
|
|
# -DAKSL_COVERAGE=ON build hits "another target with the same name already
|
|
# exists" against the `coverage` target this project adds, and fails to
|
|
# configure at all. Rename the dependency's on the way past rather than
|
|
# dropping it: its coverage script drives its own instrumented build tree, so
|
|
# `cmake --build build-coverage --target akerror_coverage` still does the right
|
|
# thing. Remove this once the dependency namespaces it upstream -- see TODO.md.
|
|
function(add_custom_target _name)
|
|
if(AKSL_SUPPRESS_ADD_TEST AND _name STREQUAL "coverage")
|
|
_add_custom_target(akerror_coverage ${ARGN})
|
|
else()
|
|
_add_custom_target(${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).
|
|
# The generated directory carries akstdlib_version.h, which akstdlib.h includes;
|
|
# it is PUBLIC because consumers building against the build tree need it too. On
|
|
# install both headers land side by side in ${CMAKE_INSTALL_INCLUDEDIR}, so the
|
|
# install interface needs no second entry.
|
|
target_include_directories(akstdlib PUBLIC
|
|
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
|
$<BUILD_INTERFACE:${AKSL_GENERATED_INCLUDE_DIR}>
|
|
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/>
|
|
)
|
|
|
|
# VERSION gives the real file (libakstdlib.so.0.1.0); SOVERSION gives the symlink
|
|
# and the ELF soname recorded in every consumer (libakstdlib.so.0.1), so a
|
|
# consumer built against 0.1 will not silently load an ABI-incompatible 0.2.
|
|
set_target_properties(akstdlib PROPERTIES
|
|
VERSION ${PROJECT_VERSION}
|
|
SOVERSION ${AKSL_SOVERSION}
|
|
)
|
|
|
|
target_link_libraries(akstdlib PUBLIC akerror::akerror)
|
|
|
|
aksl_target_coverage(akstdlib)
|
|
|
|
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 "${AKSL_VERSION_HEADER}" 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}
|
|
)
|
|
|
|
# Without this, find_package(akstdlib 0.1 REQUIRED) is refused for want of a
|
|
# version file no matter what is installed -- which is exactly the gap that
|
|
# stops cmake/akstdlib.cmake.in from requesting a version of akerror.
|
|
#
|
|
# SameMinorVersion mirrors the soname: pre-1.0, 0.1 and 0.2 are different ABIs.
|
|
# It arrived in CMake 3.11 and this project declares 3.10, so fall back to
|
|
# ExactVersion on older CMake. That is stricter than the soname rule -- it pins
|
|
# the patch level too, so a 0.1.0 request refuses a compatible 0.1.1 -- but it is
|
|
# never laxer, and wrongly refusing a good pairing beats wrongly accepting a bad
|
|
# one. Drop the branch when the minimum moves past 3.11.
|
|
if(CMAKE_VERSION VERSION_LESS 3.11)
|
|
set(AKSL_VERSION_COMPATIBILITY ExactVersion)
|
|
else()
|
|
set(AKSL_VERSION_COMPATIBILITY SameMinorVersion)
|
|
endif()
|
|
|
|
write_basic_package_version_file(
|
|
"${CMAKE_CURRENT_BINARY_DIR}/akstdlibConfigVersion.cmake"
|
|
VERSION ${PROJECT_VERSION}
|
|
COMPATIBILITY ${AKSL_VERSION_COMPATIBILITY}
|
|
)
|
|
|
|
install(FILES
|
|
"${CMAKE_CURRENT_BINARY_DIR}/akstdlibConfig.cmake"
|
|
"${CMAKE_CURRENT_BINARY_DIR}/akstdlibConfigVersion.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
|
|
convert
|
|
format
|
|
linkedlist
|
|
memory
|
|
path
|
|
status_registry
|
|
stream
|
|
strhash
|
|
tree
|
|
version
|
|
)
|
|
|
|
set(AKSL_WILL_FAIL_TESTS
|
|
)
|
|
|
|
set(AKSL_KNOWN_FAILING_TESTS
|
|
convert_strict # TODO.md 2.1.5 -- ato* cannot report a bad conversion
|
|
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)
|
|
# The test binaries are instrumented too. The default report filters them out
|
|
# (only src/ and include/ are shown), but `scripts/coverage.py --include
|
|
# tests` then answers a different question: whether every test case and
|
|
# helper branch in tests/ actually runs, which is how a test function that was
|
|
# written but never wired into AKSL_RUN shows up.
|
|
aksl_target_coverage(test_${_test})
|
|
add_test(NAME ${_test} COMMAND test_${_test})
|
|
list(APPEND AKSL_TEST_TARGETS 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
|
|
)
|
|
|
|
# Both the coverage report and the mutation harness are Python scripts.
|
|
find_package(Python3 COMPONENTS Interpreter)
|
|
|
|
# Code coverage. A -DAKSL_COVERAGE=ON build wires the report into the suite
|
|
# itself, so `ctest --test-dir build-coverage` both runs the tests and prints
|
|
# what they touched:
|
|
#
|
|
# coverage_reset deletes the accumulated .gcda counters. gcov counts are
|
|
# cumulative, so without this every report would fold in
|
|
# earlier runs and overstate coverage. FIXTURES_SETUP makes
|
|
# CTest run it before any test that needs the fixture, even
|
|
# under `ctest -j`.
|
|
# coverage_report aggregates gcov output and prints the summary plus the
|
|
# uncovered lines. FIXTURES_CLEANUP makes CTest run it after
|
|
# the last test in the fixture, which is exactly when the
|
|
# counters are complete.
|
|
#
|
|
# The report is a plain report unless AKSL_COVERAGE_THRESHOLD is set, in which
|
|
# case coverage_report fails below that percentage. Same ratchet idea as the
|
|
# mutation threshold: gate on the number you have, raise it as tests land.
|
|
#
|
|
# CTest hides the output of a passing test, so coverage_report also writes
|
|
# <build>/coverage-summary.txt and <build>/coverage.xml (Cobertura). The
|
|
# `coverage` target below builds, runs and prints in one step.
|
|
if(AKSL_COVERAGE AND Python3_FOUND)
|
|
set(AKSL_COVERAGE_ARGS
|
|
${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/scripts/coverage.py
|
|
--build ${CMAKE_CURRENT_BINARY_DIR}
|
|
--source-root ${CMAKE_CURRENT_SOURCE_DIR})
|
|
|
|
add_test(NAME coverage_reset COMMAND ${AKSL_COVERAGE_ARGS} --zero)
|
|
add_test(NAME coverage_report COMMAND ${AKSL_COVERAGE_ARGS}
|
|
--threshold ${AKSL_COVERAGE_THRESHOLD}
|
|
--branch-threshold ${AKSL_COVERAGE_BRANCH_THRESHOLD}
|
|
--output ${CMAKE_CURRENT_BINARY_DIR}/coverage-summary.txt
|
|
--cobertura ${CMAKE_CURRENT_BINARY_DIR}/coverage.xml)
|
|
|
|
set_tests_properties(coverage_reset PROPERTIES FIXTURES_SETUP AKSL_GCDA)
|
|
set_tests_properties(coverage_report PROPERTIES FIXTURES_CLEANUP AKSL_GCDA)
|
|
set_tests_properties(
|
|
${AKSL_TESTS} ${AKSL_WILL_FAIL_TESTS} ${AKSL_KNOWN_FAILING_TESTS}
|
|
PROPERTIES FIXTURES_REQUIRED AKSL_GCDA
|
|
)
|
|
# gcov has to be spawned once per .gcda, so give the report more room than the
|
|
# 30s the test binaries get.
|
|
set_tests_properties(coverage_reset coverage_report PROPERTIES TIMEOUT 300)
|
|
|
|
# Namespaced when embedded in another project, for the same reason the mutation
|
|
# target below is: a sibling dependency may well ship a `coverage` target too.
|
|
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
|
set(AKSL_COVERAGE_TARGET coverage)
|
|
else()
|
|
set(AKSL_COVERAGE_TARGET akstdlib_coverage)
|
|
endif()
|
|
|
|
# Convenience entry point that also builds the test binaries first. It runs
|
|
# the suite rather than the script directly, because the two fixture tests
|
|
# above already reset the counters and produce the report -- and CTest pulls a
|
|
# required fixture back in even when it is filtered out, so there is no way to
|
|
# run the tests without them. The second command re-reads the same counters to
|
|
# print the report that CTest suppressed for the passing coverage_report test
|
|
# (it only spawns gcov again, it does not re-run anything).
|
|
add_custom_target(${AKSL_COVERAGE_TARGET}
|
|
COMMAND ctest --test-dir ${CMAKE_CURRENT_BINARY_DIR} --output-on-failure
|
|
COMMAND ${AKSL_COVERAGE_ARGS}
|
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
|
USES_TERMINAL
|
|
COMMENT "Running the test suite under gcov and reporting coverage"
|
|
)
|
|
add_dependencies(${AKSL_COVERAGE_TARGET} ${AKSL_TEST_TARGETS})
|
|
elseif(AKSL_COVERAGE)
|
|
message(WARNING "AKSL_COVERAGE=ON but Python3 was not found: "
|
|
"instrumenting the build, but the coverage report "
|
|
"(scripts/coverage.py) will not be wired into CTest")
|
|
endif()
|
|
|
|
# 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
|
|
# When embedded in another project, use a namespaced target to avoid collisions
|
|
# with mutation targets provided by sibling dependencies.
|
|
# 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.
|
|
if(Python3_FOUND)
|
|
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
|
set(AKSL_MUTATION_TARGET mutation)
|
|
else()
|
|
set(AKSL_MUTATION_TARGET akstdlib_mutation)
|
|
endif()
|
|
add_custom_target(${AKSL_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 the library, expects tests to fail)"
|
|
)
|
|
endif()
|
|
|
|
|
|
# pkgconfig
|