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() 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 $ $ ) 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_.c, built into the executable # test_ and registered as the CTest test . 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 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 # /coverage-summary.txt and /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) # 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(coverage 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(coverage ${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