Bump deps/libakerror 22 commits to 5ff8790 (1.0.0), which makes the status-name table private, moves consumer status codes to a band starting at AKERR_FIRST_CONSUMER_STATUS, enforces range ownership rather than treating it as advisory, and gives the library an soname. See deps/libakerror/UPGRADING.md. src/stdlib.c needed no changes. This library defines no status codes of its own -- it raises libakerror's AKERR_* codes and propagates errno, both inside libakerror's reserved 0-255 band -- and it never referenced AKERR_MAX_ERR_VALUE, __AKERR_ERROR_NAMES, AKERR_STATUS_RANGE_OK or AKERR_STATUS_NAME_OK. What moved was everything around the code: A -DAKSL_COVERAGE=ON build stopped configuring at all. libakerror namespaces its `mutation` target when embedded but not its `coverage` target, so it collided with ours. Shadow add_custom_target for the duration of the add_subdirectory() call and rename the dependency's to akerror_coverage, alongside the existing add_test shadow. Fix upstream and delete the workaround; recorded in TODO.md. Pin the 1.0.0 floor three ways, since no single one covers every consumption path: an #error in akstdlib.h feature-testing AKERR_FIRST_CONSUMER_STATUS, because libakerror publishes no version macro; Requires: akerror >= 1.0.0 in akstdlib.pc, which also gets consumers -lakerror transitively; and find_dependency(akerror) in akstdlibConfig.cmake. The last was already broken before this bump -- the template still carried its MyLibraryConfig placeholder with the dependency commented out, so any external find_package(akstdlib) failed with a bare "akerror::akerror not found" out of the generated targets file. Branch coverage of src/stdlib.c fell from 51.0% to 44.3% with no source or test change: the 1.0.0 PREPARE_ERROR/FAIL_* macros expand to more branches at every call site, so 337/661 became 481/1087 -- 144 more branches covered, 426 more counted. Line coverage held at 99.0% (200/202) and function coverage at 100% (21/21). Re-ratchet the CI branch gate 45 -> 40 rather than chase branches that belong to libakerror's own suite. tests/test_status_registry.c pins the contract that made the status-code migration a no-op: libakstdlib reserves no consumer range, so an application may allocate from AKERR_FIRST_CONSUMER_STATUS without coordinating with it, and every status this library raises is inside the reserved band with a name actually registered -- an unnamed one degrades to "Unknown Error" in every later stack trace, which nothing else would notice. It exercises the new ownership enforcement too, so the "reserves nothing" assertion cannot pass vacuously. ctest 13/13, ASan+UBSan 13/13, coverage 15/15 at 90/40, mutation 89.6% (155/173, unchanged). Also verified out of tree: the #error fires as the first diagnostic against a stale akerror.h, pkg-config refuses akerror 0.9.0, and an external find_package(akstdlib) consumer builds and runs against a temp-prefix install. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
351 lines
15 KiB
CMake
351 lines
15 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()
|
|
|
|
# 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)
|
|
target_include_directories(akstdlib PUBLIC
|
|
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
|
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/>
|
|
)
|
|
|
|
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 ${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
|
|
convert
|
|
format
|
|
linkedlist
|
|
memory
|
|
path
|
|
status_registry
|
|
stream
|
|
strhash
|
|
tree
|
|
)
|
|
|
|
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
|