Compare commits
11 Commits
feature/te
...
fd71bcc67b
| Author | SHA1 | Date | |
|---|---|---|---|
|
fd71bcc67b
|
|||
|
95e5002512
|
|||
|
07c448508b
|
|||
|
a37ba3fb89
|
|||
|
437da2960b
|
|||
|
82c47ed773
|
|||
|
a87cbfb26d
|
|||
|
566004afd6
|
|||
|
11c04923f8
|
|||
|
01734f511b
|
|||
| 8003239116 |
@@ -31,6 +31,52 @@ jobs:
|
|||||||
ctest --test-dir build --output-on-failure
|
ctest --test-dir build --output-on-failure
|
||||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
- name: dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y cmake gcc python3
|
||||||
|
# scripts/coverage.py needs nothing but python3 and gcc's own gcov, so
|
||||||
|
# there is no lcov/gcovr to install here.
|
||||||
|
#
|
||||||
|
# The gate is a ratchet, not a target: src/stdlib.c is at 99.1% of lines
|
||||||
|
# and 45.1% of branches, so 90/40 fails on a real regression (a test
|
||||||
|
# deleted, or new untested code added) without tripping over rounding.
|
||||||
|
# The report is printed either way -- the uncovered lines it lists are the
|
||||||
|
# missing tests.
|
||||||
|
#
|
||||||
|
# Branch coverage is back over 45 (the aksl_version_* functions are fully
|
||||||
|
# covered, which lifted it from 44.3%), but the gate stays at 40: 0.1
|
||||||
|
# points of headroom is not a ratchet, it is a coin flip on the next
|
||||||
|
# rounding change.
|
||||||
|
#
|
||||||
|
# The branch gate was 45 against 51.0% until the libakerror 1.0.0 bump.
|
||||||
|
# Nothing about this library's tests changed: line coverage held at
|
||||||
|
# 99.0% (200/202) and function coverage at 100% (21/21), but the branch
|
||||||
|
# denominator went from 661 to 1087 because the 1.0.0 PREPARE_ERROR /
|
||||||
|
# FAIL_* macros expand to more branches at every call site in
|
||||||
|
# src/stdlib.c, and most of the added branches are not reachable from the
|
||||||
|
# way this library calls them. 337/661 became 481/1087 -- 144 more
|
||||||
|
# branches covered, 426 more branches counted. Chasing them here would be
|
||||||
|
# testing libakerror's macros, which is libakerror's mutation suite's job
|
||||||
|
# (macros expand at the call site, so coverage cannot see them properly
|
||||||
|
# from either side). Re-ratcheted rather than papered over.
|
||||||
|
- name: coverage
|
||||||
|
run: |
|
||||||
|
cmake -S . -B build-coverage -DAKSL_COVERAGE=ON \
|
||||||
|
-DAKSL_COVERAGE_THRESHOLD=90 \
|
||||||
|
-DAKSL_COVERAGE_BRANCH_THRESHOLD=40
|
||||||
|
cmake --build build-coverage
|
||||||
|
ctest --test-dir build-coverage --output-on-failure
|
||||||
|
cat build-coverage/coverage-summary.txt
|
||||||
|
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||||
|
|
||||||
mutation_test:
|
mutation_test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -48,19 +94,18 @@ jobs:
|
|||||||
# confirm the suite fails. Gated on src/stdlib.c (fast, deterministic);
|
# confirm the suite fails. Gated on src/stdlib.c (fast, deterministic);
|
||||||
# run the full default target locally for the macro header as well.
|
# run the full default target locally for the macro header as well.
|
||||||
#
|
#
|
||||||
# The threshold is a ratchet, not a quality bar. The score on the section
|
# The threshold is a ratchet, not a quality bar. The score is 89.6%
|
||||||
# 1.0 suite is 46.8% (81/173 killed): the list and tree functions are
|
# (155/173 killed) now that TODO.md sections 1.2-1.6 have tests; it was
|
||||||
# covered, and everything else in src/stdlib.c -- the printf family, the
|
# 46.8% when only the list and tree functions were covered. 80 leaves
|
||||||
# ato* family, realpath, the memory and stream wrappers -- has no tests
|
# headroom for the runner while still failing on a real regression (tests
|
||||||
# yet, so its mutants survive. 40 leaves headroom for the runner while
|
# deleted, or new untested code added). The 18 survivors are listed in the
|
||||||
# still failing on a real regression (tests deleted, or new untested code
|
# published report -- each one is a missing assertion.
|
||||||
# added). Raise it as TODO.md sections 1.1-1.9 land.
|
|
||||||
- name: mutation testing
|
- name: mutation testing
|
||||||
run: |
|
run: |
|
||||||
python3 scripts/mutation_test.py \
|
python3 scripts/mutation_test.py \
|
||||||
--target src/stdlib.c \
|
--target src/stdlib.c \
|
||||||
--junit mutation-junit.xml \
|
--junit mutation-junit.xml \
|
||||||
--threshold 40
|
--threshold 80
|
||||||
# Publish even when the threshold gate fails, so survivors are visible --
|
# Publish even when the threshold gate fails, so survivors are visible --
|
||||||
# each one is a missing test. Display-only (fail_on_failure: false); the
|
# each one is a missing test. Display-only (fail_on_failure: false); the
|
||||||
# --threshold above is the gate. annotate_only avoids the Checks API 404
|
# --threshold above is the gate. annotate_only avoids the Checks API 404
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ set -u
|
|||||||
|
|
||||||
# Keep in sync with the --threshold in .gitea/workflows/ci.yaml, so a push that
|
# Keep in sync with the --threshold in .gitea/workflows/ci.yaml, so a push that
|
||||||
# would fail CI fails here first.
|
# would fail CI fails here first.
|
||||||
MUTATION_THRESHOLD="${AKSL_MUTATION_THRESHOLD:-40}"
|
MUTATION_THRESHOLD="${AKSL_MUTATION_THRESHOLD:-80}"
|
||||||
|
|
||||||
ZERO_SHA=0000000000000000000000000000000000000000
|
ZERO_SHA=0000000000000000000000000000000000000000
|
||||||
|
|
||||||
|
|||||||
82
AGENTS.md
Normal file
82
AGENTS.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# Repository Guidelines
|
||||||
|
|
||||||
|
## Project Structure & Module Organization
|
||||||
|
|
||||||
|
`libakstdlib` is a C shared library that wraps libc calls and small data
|
||||||
|
structures in `libakerror` error contexts. Public API declarations live in
|
||||||
|
`include/akstdlib.h`; implementation lives in `src/stdlib.c`. Tests are
|
||||||
|
one-file CTest executables under `tests/test_<name>.c`, with shared test helpers
|
||||||
|
in `tests/aksl_capture.h`. CMake package templates are in `cmake/` and
|
||||||
|
`akstdlib.pc.in`. The vendored dependency is `deps/libakerror`; update it as a
|
||||||
|
submodule rather than editing generated files under `build/`, `build-asan/` or
|
||||||
|
`build-coverage/`.
|
||||||
|
|
||||||
|
## Build, Test, and Development Commands
|
||||||
|
|
||||||
|
Initialize dependencies before a fresh build:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git submodule update --init --recursive
|
||||||
|
cmake -S . -B build
|
||||||
|
cmake --build build
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the normal suite with `ctest --test-dir build --output-on-failure`. Use the
|
||||||
|
instrumented build for memory and undefined-behavior checks:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake -S . -B build-asan -DAKSL_SANITIZE=ON
|
||||||
|
cmake --build build-asan
|
||||||
|
ctest --test-dir build-asan --output-on-failure
|
||||||
|
```
|
||||||
|
|
||||||
|
For coverage, configure a third tree; the report is part of that suite (the
|
||||||
|
`coverage_reset` / `coverage_report` CTest entries) and also lands in
|
||||||
|
`build-coverage/coverage-summary.txt`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake -S . -B build-coverage -DAKSL_COVERAGE=ON
|
||||||
|
cmake --build build-coverage --target coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `cmake --build build --target mutation` only when you need the slower
|
||||||
|
mutation harness. `rebuild.sh` installs to `/home/andrew/local` and removes the
|
||||||
|
local build directory, so treat it as a local convenience script.
|
||||||
|
|
||||||
|
## Coding Style & Naming Conventions
|
||||||
|
|
||||||
|
Use C with 4-space indentation; existing files sometimes use tabs for continued
|
||||||
|
statements, so match the surrounding block. Public symbols use the `aksl_`
|
||||||
|
prefix, structs use `aksl_<Name>`, and tests use `test_<feature>.c` plus static
|
||||||
|
`test_<case>` functions. Preserve the `akerr_ErrorContext AKERR_NOIGNORE *`
|
||||||
|
return convention and the `PREPARE_ERROR` / `FAIL_*` / `SUCCEED_RETURN` pattern.
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
|
||||||
|
Add a new test by creating `tests/test_mything.c` and adding `mything` to the
|
||||||
|
right list in `CMakeLists.txt`. `AKSL_TESTS` must exit zero.
|
||||||
|
`AKSL_WILL_FAIL_TESTS` are deliberate abort/contract tests.
|
||||||
|
`AKSL_KNOWN_FAILING_TESTS` assert documented defects from `TODO.md`; when one
|
||||||
|
starts unexpectedly passing, move it into `AKSL_TESTS` with the fix.
|
||||||
|
|
||||||
|
`src/stdlib.c` is at 99.1% line coverage and CI gates it at 90 (line) / 40
|
||||||
|
(branch), so new code needs tests in the same commit. Run
|
||||||
|
`cmake --build build-coverage --target coverage` and check the uncovered-line
|
||||||
|
listing before proposing a change. Tests for behaviour that `TODO.md` records as
|
||||||
|
defective belong in `AKSL_KNOWN_FAILING_TESTS` asserting the *correct* contract —
|
||||||
|
do not pin current-but-wrong behaviour in `AKSL_TESTS`, since that turns the
|
||||||
|
eventual fix into a test failure.
|
||||||
|
|
||||||
|
## Commit & Pull Request Guidelines
|
||||||
|
|
||||||
|
Recent commits use short imperative summaries, for example `Add memory wrapper
|
||||||
|
tests` and `Make error-status assertions authoritative`. Keep commits focused
|
||||||
|
and include tests with behavior changes. Pull requests should describe the
|
||||||
|
changed API or behavior, list the CTest/sanitizer/mutation commands run, and
|
||||||
|
link the relevant `TODO.md` item or issue when fixing a known defect.
|
||||||
|
|
||||||
|
## Agent-Specific Instructions
|
||||||
|
|
||||||
|
Do not modify generated build trees, profiling artifacts, or untracked scratch
|
||||||
|
files unless explicitly asked. Prefer small, test-backed changes and update
|
||||||
|
`README.md` or `TODO.md` when changing documented workflows or known failures.
|
||||||
232
CMakeLists.txt
232
CMakeLists.txt
@@ -1,5 +1,32 @@
|
|||||||
cmake_minimum_required(VERSION 3.10)
|
cmake_minimum_required(VERSION 3.10)
|
||||||
project(akstdlib LANGUAGES C)
|
# 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_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -ggdb -pg")
|
||||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -g -ggdb -pg")
|
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -g -ggdb -pg")
|
||||||
@@ -20,6 +47,50 @@ if(AKSL_SANITIZE)
|
|||||||
message(STATUS "AKSL_SANITIZE=ON: building with ASan + UBSan")
|
message(STATUS "AKSL_SANITIZE=ON: building with ASan + UBSan")
|
||||||
endif()
|
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)
|
if(TARGET akerror::akerror)
|
||||||
message(STATUS "FOUND akerror::akerror")
|
message(STATUS "FOUND akerror::akerror")
|
||||||
else()
|
else()
|
||||||
@@ -52,6 +123,22 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
|||||||
endif()
|
endif()
|
||||||
endfunction()
|
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)
|
add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
|
||||||
|
|
||||||
set(AKSL_SUPPRESS_ADD_TEST FALSE)
|
set(AKSL_SUPPRESS_ADD_TEST FALSE)
|
||||||
@@ -75,14 +162,29 @@ add_library(akstdlib SHARED
|
|||||||
|
|
||||||
add_library(akstdlib::akstdlib ALIAS akstdlib)
|
add_library(akstdlib::akstdlib ALIAS akstdlib)
|
||||||
|
|
||||||
# Specify include directories for the library's headers (if applicable)
|
# 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
|
target_include_directories(akstdlib PUBLIC
|
||||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||||
|
$<BUILD_INTERFACE:${AKSL_GENERATED_INCLUDE_DIR}>
|
||||||
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/>
|
$<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)
|
target_link_libraries(akstdlib PUBLIC akerror::akerror)
|
||||||
|
|
||||||
|
aksl_target_coverage(akstdlib)
|
||||||
|
|
||||||
set(main_lib_dest "lib/my_library-${MY_LIBRARY_VERSION}")
|
set(main_lib_dest "lib/my_library-${MY_LIBRARY_VERSION}")
|
||||||
install(TARGETS akstdlib
|
install(TARGETS akstdlib
|
||||||
EXPORT akstdlibTargets
|
EXPORT akstdlibTargets
|
||||||
@@ -93,6 +195,7 @@ install(TARGETS akstdlib
|
|||||||
)
|
)
|
||||||
|
|
||||||
install(FILES "include/akstdlib.h" DESTINATION "include/")
|
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(FILES ${CMAKE_CURRENT_BINARY_DIR}/akstdlib.pc DESTINATION "lib/pkgconfig/")
|
||||||
|
|
||||||
|
|
||||||
@@ -108,8 +211,31 @@ configure_package_config_file(
|
|||||||
INSTALL_DESTINATION ${akstdlib_install_cmakedir}
|
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
|
install(FILES
|
||||||
"${CMAKE_CURRENT_BINARY_DIR}/akstdlibConfig.cmake"
|
"${CMAKE_CURRENT_BINARY_DIR}/akstdlibConfig.cmake"
|
||||||
|
"${CMAKE_CURRENT_BINARY_DIR}/akstdlibConfigVersion.cmake"
|
||||||
DESTINATION ${akstdlib_install_cmakedir}
|
DESTINATION ${akstdlib_install_cmakedir}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -129,14 +255,23 @@ install(FILES
|
|||||||
# "unexpectedly passed" -- that is the cue to move
|
# "unexpectedly passed" -- that is the cue to move
|
||||||
# it up into AKSL_TESTS.
|
# it up into AKSL_TESTS.
|
||||||
set(AKSL_TESTS
|
set(AKSL_TESTS
|
||||||
|
convert
|
||||||
|
format
|
||||||
linkedlist
|
linkedlist
|
||||||
|
memory
|
||||||
|
path
|
||||||
|
status_registry
|
||||||
|
stream
|
||||||
|
strhash
|
||||||
tree
|
tree
|
||||||
|
version
|
||||||
)
|
)
|
||||||
|
|
||||||
set(AKSL_WILL_FAIL_TESTS
|
set(AKSL_WILL_FAIL_TESTS
|
||||||
)
|
)
|
||||||
|
|
||||||
set(AKSL_KNOWN_FAILING_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_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
|
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
|
tree_iterate_break # TODO.md 2.1.3 -- ITERATOR_BREAK does not stop a walk
|
||||||
@@ -146,7 +281,14 @@ foreach(_test IN LISTS AKSL_TESTS AKSL_WILL_FAIL_TESTS AKSL_KNOWN_FAILING_TESTS)
|
|||||||
add_executable(test_${_test} tests/test_${_test}.c)
|
add_executable(test_${_test} tests/test_${_test}.c)
|
||||||
target_include_directories(test_${_test} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests)
|
target_include_directories(test_${_test} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests)
|
||||||
target_link_libraries(test_${_test} PRIVATE akstdlib)
|
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})
|
add_test(NAME ${_test} COMMAND test_${_test})
|
||||||
|
list(APPEND AKSL_TEST_TARGETS test_${_test})
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
if(AKSL_WILL_FAIL_TESTS OR AKSL_KNOWN_FAILING_TESTS)
|
if(AKSL_WILL_FAIL_TESTS OR AKSL_KNOWN_FAILING_TESTS)
|
||||||
@@ -167,17 +309,99 @@ set_tests_properties(
|
|||||||
PROPERTIES TIMEOUT 30
|
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
|
# 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
|
# 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
|
# re-runs the whole suite once per mutant, so it is a manual target rather than
|
||||||
# a CTest test:
|
# a CTest test:
|
||||||
# cmake --build build --target mutation
|
# 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
|
# 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
|
# narrower, faster src/stdlib.c set with a --threshold gate; see
|
||||||
# .gitea/workflows/ci.yaml.
|
# .gitea/workflows/ci.yaml.
|
||||||
find_package(Python3 COMPONENTS Interpreter)
|
|
||||||
if(Python3_FOUND)
|
if(Python3_FOUND)
|
||||||
add_custom_target(mutation
|
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}
|
COMMAND ${Python3_EXECUTABLE}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/scripts/mutation_test.py
|
${CMAKE_CURRENT_SOURCE_DIR}/scripts/mutation_test.py
|
||||||
--source-root ${CMAKE_CURRENT_SOURCE_DIR}
|
--source-root ${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
|||||||
226
README.md
226
README.md
@@ -25,9 +25,108 @@ A top-level build compiles the vendored `deps/libakerror`. When `libakstdlib` is
|
|||||||
consumed as a subproject, it uses whatever `akerror::akerror` target or installed
|
consumed as a subproject, it uses whatever `akerror::akerror` target or installed
|
||||||
package the parent provides instead.
|
package the parent provides instead.
|
||||||
|
|
||||||
|
### This library's own version
|
||||||
|
|
||||||
|
`libakstdlib` is at **0.1.0**. The version lives in exactly one place — the
|
||||||
|
`project()` call in `CMakeLists.txt` — and flows from there into everything
|
||||||
|
else, so a bump is a one-line edit:
|
||||||
|
|
||||||
|
| Artifact | Value at 0.1.0 | From |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `AKSL_VERSION_MAJOR` / `_MINOR` / `_PATCH` | `0` / `1` / `0` | `include/akstdlib_version.h.in` |
|
||||||
|
| `AKSL_VERSION_STRING` | `"0.1.0"` | same |
|
||||||
|
| `AKSL_VERSION_NUMBER` | `100` | same |
|
||||||
|
| `AKSL_VERSION_SONAME` | `"0.1"` | same |
|
||||||
|
| shared library | `libakstdlib.so.0.1.0`, soname `libakstdlib.so.0.1` | `VERSION` / `SOVERSION` |
|
||||||
|
| `pkg-config --modversion akstdlib` | `0.1.0` | `akstdlib.pc` |
|
||||||
|
| `find_package(akstdlib 0.1)` | accepted; `0.2` and `1.0` refused | `akstdlibConfigVersion.cmake` |
|
||||||
|
|
||||||
|
`akstdlib_version.h` is **generated** — that is why there is no such file in the
|
||||||
|
source tree, only the `.in` template beside `akstdlib.h`. Don't hand-edit the
|
||||||
|
copy in your build directory; change the template or `project()`.
|
||||||
|
|
||||||
|
It is `0.x` deliberately. `TODO.md` §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 the loader will not substitute one for the other. At 1.0
|
||||||
|
the soname becomes `MAJOR` alone — the `if(PROJECT_VERSION_MAJOR EQUAL 0)` in
|
||||||
|
`CMakeLists.txt` and the matching `#if` in `tests/test_version.c` are the two
|
||||||
|
places that encode this, and they are tested against each other.
|
||||||
|
|
||||||
|
`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.
|
||||||
|
|
||||||
|
#### Compiled-against vs. loaded
|
||||||
|
|
||||||
|
The macros above record what a caller was **compiled** against. What it actually
|
||||||
|
**loaded** is a different question, and the two can disagree:
|
||||||
|
|
||||||
|
```c
|
||||||
|
int major, minor, patch;
|
||||||
|
akerr_ErrorContext *e = aksl_version(&major, &minor, &patch); /* the loaded .so */
|
||||||
|
const char *v = aksl_version_string(); /* likewise */
|
||||||
|
|
||||||
|
e = AKSL_VERSION_CHECK(); /* compares the two; AKERR_VALUE on a mismatch */
|
||||||
|
```
|
||||||
|
|
||||||
|
`AKSL_VERSION_CHECK()` is a macro on purpose: it expands at *your* call site, so
|
||||||
|
it captures the `AKSL_VERSION_*` you were built with and passes them to a
|
||||||
|
function that compares against the values baked into the library. Calling
|
||||||
|
`aksl_version_check()` with hand-written numbers defeats the whole mechanism.
|
||||||
|
|
||||||
|
Compatibility is defined as "same soname", so pre-1.0 both major and minor must
|
||||||
|
match and the patch level is ignored — a caller built against 0.1.0 keeps working
|
||||||
|
against 0.1.7, which is exactly the promise the shared soname makes.
|
||||||
|
|
||||||
|
In normal use the soname catches the mismatch first, at load time, and the check
|
||||||
|
never fires. It earns its keep when the soname is bypassed: a hand-install that
|
||||||
|
drops a 0.2.0 build in under the 0.1 filename, or a package that strips
|
||||||
|
versioning. Then the loader is happy and only the check notices:
|
||||||
|
|
||||||
|
```
|
||||||
|
compiled against : 0.1.0 (soname 0.1)
|
||||||
|
loaded : 0.2.0 (0.2.0)
|
||||||
|
MISMATCH DETECTED: compiled against libakstdlib 0.1.0, loaded 0.2.0 (soname 0.2)
|
||||||
|
```
|
||||||
|
|
||||||
|
### The libakerror version floor
|
||||||
|
|
||||||
|
**libakerror 1.0.0 or newer is required.** That release made the status-name
|
||||||
|
table private, moved consumer status codes into a band starting at
|
||||||
|
`AKERR_FIRST_CONSUMER_STATUS` (256), made range ownership enforced rather than
|
||||||
|
advisory, and gave the library an soname — see
|
||||||
|
`deps/libakerror/UPGRADING.md`. It is a source *and* ABI break, so pairing this
|
||||||
|
header with an older `akerror.h` is not a compile problem you can work around;
|
||||||
|
the pairing is simply invalid.
|
||||||
|
|
||||||
|
Three things enforce the floor, because no single one covers every way the
|
||||||
|
library gets consumed:
|
||||||
|
|
||||||
|
| Mechanism | Where | Catches |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `#error` on a missing `AKERR_FIRST_CONSUMER_STATUS` | `include/akstdlib.h` | a stale `akerror.h` earlier on the include path, at the first diagnostic rather than as a pile of errors inside `src/stdlib.c` |
|
||||||
|
| `Requires: akerror >= 1.0.0` | `akstdlib.pc.in` | a pkg-config consumer, which also now gets `-lakerror` transitively |
|
||||||
|
| `find_dependency(akerror)` | `cmake/akstdlib.cmake.in` | a `find_package(akstdlib)` consumer, which previously failed with a bare *"akerror::akerror not found"* out of the generated targets file |
|
||||||
|
|
||||||
|
The header guard feature-tests rather than version-tests because libakerror
|
||||||
|
publishes no version macro; `AKERR_FIRST_CONSUMER_STATUS` is the symbol 1.0.0
|
||||||
|
introduced, so its absence is what "older than 1.0.0" actually looks like. The
|
||||||
|
CMake path requests no version for the same kind of reason: libakerror installs
|
||||||
|
no `akerrorConfigVersion.cmake`, so `find_dependency(akerror 1.0.0)` would be
|
||||||
|
refused for want of a version file no matter which akerror is installed.
|
||||||
|
|
||||||
|
**libakstdlib defines no status codes of its own.** It raises libakerror's
|
||||||
|
`AKERR_*` codes and propagates the host's `errno` values, all of which live in
|
||||||
|
libakerror's reserved `0`–`255` band, so it reserves no range and an application
|
||||||
|
is free to allocate from `AKERR_FIRST_CONSUMER_STATUS` without coordinating with
|
||||||
|
it. `tests/test_status_registry.c` pins that, along with the requirement that
|
||||||
|
every status this library raises actually has a name registered — an unnamed one
|
||||||
|
degrades to `"Unknown Error"` in every later stack trace, which nothing else
|
||||||
|
would notice.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
There are three harnesses. The first two take seconds; the third takes about
|
There are four harnesses. The first three take seconds; the fourth takes about
|
||||||
half an hour.
|
half an hour.
|
||||||
|
|
||||||
### 1. The test suite
|
### 1. The test suite
|
||||||
@@ -41,8 +140,15 @@ ctest --test-dir build --output-on-failure
|
|||||||
Tests live one per file in `tests/test_<name>.c` and share the helpers in
|
Tests live one per file in `tests/test_<name>.c` and share the helpers in
|
||||||
`tests/aksl_capture.h` — `AKSL_CHECK()` for plain assertions (unlike `assert()`
|
`tests/aksl_capture.h` — `AKSL_CHECK()` for plain assertions (unlike `assert()`
|
||||||
it survives `-DNDEBUG`), `AKSL_CHECK_STATUS(call, expected)` to run a wrapper and
|
it survives `-DNDEBUG`), `AKSL_CHECK_STATUS(call, expected)` to run a wrapper and
|
||||||
assert on the status it returns, and an `AKSL_RUN()` driver that additionally
|
assert on the status it returns, `aksl_temp_file()` for tests that need a real
|
||||||
fails any test which leaks a slot from libakerror's error pool.
|
file to work on, and an `AKSL_RUN()` driver that additionally fails any test which
|
||||||
|
leaks a slot from libakerror's error pool.
|
||||||
|
|
||||||
|
One file per area of the API: `convert` (the `ato*` family), `format` (the
|
||||||
|
`printf` family), `stream` (`fopen`/`fread`/`fwrite`/`fclose`), `path`
|
||||||
|
(`aksl_realpath`), `strhash`, `memory`, `linkedlist`, `tree`, and
|
||||||
|
`status_registry` (this library's side of the libakerror status-registry
|
||||||
|
contract — see "The libakerror version floor" above).
|
||||||
|
|
||||||
To add a test, drop `tests/test_mything.c` in place and add `mything` to
|
To add a test, drop `tests/test_mything.c` in place and add `mything` to
|
||||||
`AKSL_TESTS` in `CMakeLists.txt`.
|
`AKSL_TESTS` in `CMakeLists.txt`.
|
||||||
@@ -80,7 +186,94 @@ misbehave under instrumentation — the uninitialised `%s` in `aksl_realpath`, t
|
|||||||
unbounded `vsprintf` behind `aksl_sprintf`, the missing `va_end` in the `printf`
|
unbounded `vsprintf` behind `aksl_sprintf`, the missing `va_end` in the `printf`
|
||||||
family — so new tests for those should be run this way.
|
family — so new tests for those should be run this way.
|
||||||
|
|
||||||
### 3. Mutation testing
|
### 3. Code coverage
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake -S . -B build-coverage -DAKSL_COVERAGE=ON
|
||||||
|
cmake --build build-coverage --target coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
`-DAKSL_COVERAGE=ON` compiles the library and the tests with `--coverage -O0`,
|
||||||
|
and wires the report into the suite itself, so a plain
|
||||||
|
`ctest --test-dir build-coverage` also produces it. Two extra CTest entries
|
||||||
|
appear, held in place by a CTest fixture rather than by declaration order, so
|
||||||
|
they work under `ctest -j` too:
|
||||||
|
|
||||||
|
| Test | When | Does |
|
||||||
|
|---|---|---|
|
||||||
|
| `coverage_reset` | before every other test | deletes the accumulated `.gcda` counters |
|
||||||
|
| `coverage_report` | after every other test | aggregates `gcov` output, prints the summary, applies the threshold gate |
|
||||||
|
|
||||||
|
The reset matters: gcov counters are cumulative, so without it each report would
|
||||||
|
fold in every earlier run and overstate coverage.
|
||||||
|
|
||||||
|
`coverage` here is this project's target. libakerror ships a `coverage` target of
|
||||||
|
its own and, unlike its `mutation` target, does not namespace it when embedded,
|
||||||
|
so a top-level `-DAKSL_COVERAGE=ON` build would collide on the name and fail to
|
||||||
|
configure at all. `CMakeLists.txt` renames the dependency's to `akerror_coverage`
|
||||||
|
on the way past — it drives its own instrumented build tree, so
|
||||||
|
`cmake --build build-coverage --target akerror_coverage` still works. The
|
||||||
|
workaround goes away when libakerror namespaces it upstream; see `TODO.md` §2.3.
|
||||||
|
|
||||||
|
CTest hides the output of a passing test, so `coverage_report` also writes
|
||||||
|
`build-coverage/coverage-summary.txt` (the same text report) and
|
||||||
|
`build-coverage/coverage.xml` (Cobertura, for CI publishers). The `coverage`
|
||||||
|
target above prints the report to the terminal for you; otherwise read the file
|
||||||
|
or use `ctest --test-dir build-coverage -V -R coverage_report`.
|
||||||
|
|
||||||
|
The report lists per-file line, branch and function coverage, then every
|
||||||
|
uncovered line and every function the suite never called — that listing is the
|
||||||
|
actionable part, the same way surviving mutants are for the harness below.
|
||||||
|
|
||||||
|
Drive the script directly for anything narrower:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scripts/coverage.py --build build-coverage # report on disk counters
|
||||||
|
scripts/coverage.py --build build-coverage --summary-only # totals only
|
||||||
|
scripts/coverage.py --build build-coverage --include tests # coverage of the tests themselves
|
||||||
|
scripts/coverage.py --build build-coverage --run-tests # reset, run ctest, report
|
||||||
|
scripts/coverage.py --build build-coverage --threshold 90 --branch-threshold 40
|
||||||
|
```
|
||||||
|
|
||||||
|
It needs nothing but Python 3 and gcc's own `gcov` — no lcov, gcovr or genhtml.
|
||||||
|
|
||||||
|
To gate on coverage, set the threshold at configure time; `coverage_report` then
|
||||||
|
fails below it, and the same regression-ratchet logic applies as for the mutation
|
||||||
|
score:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake -S . -B build-coverage -DAKSL_COVERAGE=ON \
|
||||||
|
-DAKSL_COVERAGE_THRESHOLD=90 -DAKSL_COVERAGE_BRANCH_THRESHOLD=40
|
||||||
|
```
|
||||||
|
|
||||||
|
**Where it stands.** `src/stdlib.c` is at **99.1% of lines (217/219)**, **45.1% of
|
||||||
|
branches (519/1151)** and **25/25 functions**, so 90/40 above is a ratchet with
|
||||||
|
headroom rather than a target. The two uncovered lines are both
|
||||||
|
`} HANDLE(e, AKERR_ITERATOR_BREAK) {` — in libakerror that macro begins with the
|
||||||
|
`break;` belonging to `PROCESS`'s `case 0:` arm, which is only reachable when a
|
||||||
|
callback returns a non-NULL error context whose status is *zero*. That is the
|
||||||
|
pathological case §2.2.1 of `TODO.md` exists to remove, so it is left uncovered
|
||||||
|
deliberately rather than pinned by a test.
|
||||||
|
|
||||||
|
Branch coverage sits far below line coverage because most branches in this file
|
||||||
|
are inside the `FAIL_*`/`ATTEMPT`/`FINISH` macro expansions — pool exhaustion,
|
||||||
|
stack-trace buffer limits, `akerr_valid_error_address` failures — and belong to
|
||||||
|
libakerror's own suite rather than to this one. The libakerror 1.0.0 bump made
|
||||||
|
that gap wider without changing a line of this library: the branch denominator
|
||||||
|
went from 661 to 1087 as those macros grew, so the same tests that scored 51.0%
|
||||||
|
(337/661) dropped to 44.3% (481/1087). The gate moved 45 → 40 to match; line and
|
||||||
|
function coverage did not move at all. Adding the `aksl_version_*` family and its
|
||||||
|
tests brought the figure back to 45.1% (519/1151), but the gate stays at 40 —
|
||||||
|
0.1 points of headroom is not a ratchet.
|
||||||
|
|
||||||
|
Two caveats. Coverage is measured at `-O0`, because the optimizer reorders lines
|
||||||
|
until per-line counts stop matching the source — so a coverage build is not the
|
||||||
|
build to profile. And gcov flushes its counters at normal process exit, which an
|
||||||
|
`AKSL_WILL_FAIL_TESTS` entry that aborts by design never reaches: such a test
|
||||||
|
contributes no coverage data at all, so lines only it reaches are reported as
|
||||||
|
uncovered.
|
||||||
|
|
||||||
|
### 4. Mutation testing
|
||||||
|
|
||||||
The suite tells you the library works. Mutation testing tells you the *suite*
|
The suite tells you the library works. Mutation testing tells you the *suite*
|
||||||
works: it breaks the library in small ways, one at a time, and checks that the
|
works: it breaks the library in small ways, one at a time, and checks that the
|
||||||
@@ -96,7 +289,7 @@ or drive the script directly for a faster or narrower run:
|
|||||||
scripts/mutation_test.py --target src/stdlib.c # C source only
|
scripts/mutation_test.py --target src/stdlib.c # C source only
|
||||||
scripts/mutation_test.py --target src/stdlib.c --list # enumerate, build nothing
|
scripts/mutation_test.py --target src/stdlib.c --list # enumerate, build nothing
|
||||||
scripts/mutation_test.py --target src/stdlib.c --max-mutants 20
|
scripts/mutation_test.py --target src/stdlib.c --max-mutants 20
|
||||||
scripts/mutation_test.py --target src/stdlib.c --threshold 40
|
scripts/mutation_test.py --target src/stdlib.c --threshold 80
|
||||||
```
|
```
|
||||||
|
|
||||||
A mutant that makes the tests fail is *killed* (good); one the tests still pass
|
A mutant that makes the tests fail is *killed* (good); one the tests still pass
|
||||||
@@ -105,10 +298,23 @@ run prints every survivor with `file:line` and the exact edit. The harness never
|
|||||||
touches your working tree — it copies the repo to a scratch directory and mutates
|
touches your working tree — it copies the repo to a scratch directory and mutates
|
||||||
the copy.
|
the copy.
|
||||||
|
|
||||||
CI runs the `src/stdlib.c` set with `--threshold 40`. That is a regression
|
CI runs the `src/stdlib.c` set with `--threshold 80`. That is a regression ratchet
|
||||||
ratchet rather than a quality bar: the current score is 46.8%, and the survivors
|
rather than a quality bar: the current score is **89.6% (155/173 killed)**, up
|
||||||
are concentrated in the wrappers that have no tests yet. Raise the threshold as
|
from 46.8% before the wrapper tests landed. Raise the threshold as the remaining
|
||||||
coverage lands.
|
survivors are turned into assertions.
|
||||||
|
|
||||||
|
The 18 survivors cluster in three places, and each names a real gap rather than a
|
||||||
|
test-harness artifact:
|
||||||
|
|
||||||
|
- **Statements whose absence nothing observes** — deleting `free(ptr)`,
|
||||||
|
`obj->next = NULL`, or a `SUCCEED_RETURN` leaves behaviour the suite does not
|
||||||
|
look at (a leak, a stale pointer, a success that was already NULL).
|
||||||
|
- **The `aksl_list_append` cycle/tail walk** (`tail = slow`, `slow = slow->next`,
|
||||||
|
`tail = fast`) — the function is broken in exactly this area (`TODO.md` §2.1.1),
|
||||||
|
so its known-failing test cannot pin the internals yet.
|
||||||
|
- **`lalloc`/`lfree` defaulting in `aksl_tree_iterate`** — dead parameters
|
||||||
|
(§2.2.8): they are defaulted and then never called, so inverting the guard
|
||||||
|
changes nothing observable.
|
||||||
|
|
||||||
## The pre-push hook
|
## The pre-push hook
|
||||||
|
|
||||||
@@ -128,5 +334,5 @@ AKSL_HOOK_MUTATION=1 git push # also run the mutation gate (slow)
|
|||||||
git push --no-verify # skip the hook entirely
|
git push --no-verify # skip the hook entirely
|
||||||
```
|
```
|
||||||
|
|
||||||
Other knobs: `AKSL_MUTATION_THRESHOLD` (default 40, keep it in step with
|
Other knobs: `AKSL_MUTATION_THRESHOLD` (default 80, keep it in step with
|
||||||
`.gitea/workflows/ci.yaml`) and `AKSL_HOOK_BUILD_DIR`.
|
`.gitea/workflows/ci.yaml`) and `AKSL_HOOK_BUILD_DIR`.
|
||||||
|
|||||||
300
TODO.md
300
TODO.md
@@ -57,21 +57,20 @@ against `tests/aksl_capture.h` and added to the `AKSL_TESTS` list.
|
|||||||
- [x] **Port `scripts/mutation_test.py` from libakerror.** Retargeted at `src/stdlib.c` and
|
- [x] **Port `scripts/mutation_test.py` from libakerror.** Retargeted at `src/stdlib.c` and
|
||||||
`include/akstdlib.h` (178 mutants) and exposed as
|
`include/akstdlib.h` (178 mutants) and exposed as
|
||||||
`cmake --build build --target mutation`. CI runs the narrower `src/stdlib.c` set (173
|
`cmake --build build --target mutation`. CI runs the narrower `src/stdlib.c` set (173
|
||||||
mutants) as its own `mutation_test` job with `--threshold 40` and a JUnit report.
|
mutants) as its own `mutation_test` job with `--threshold 80` and a JUnit report.
|
||||||
**Current score: 46.8%** — 81 killed, 92 survived. The survivors are concentrated
|
**Current score: 89.6%** — 155 killed, 18 survived. It was 46.8% (81 killed, 92
|
||||||
exactly where §1.1–1.9 have yet to be written:
|
survived) before §1.2–1.6 were written; the remaining survivors are:
|
||||||
|
|
||||||
| surviving mutants | function |
|
| surviving mutants | where | why |
|
||||||
|---|---|
|
|---|---|---|
|
||||||
| 10 | `aksl_tree_iterate` |
|
| 5 | deleted statements whose absence nothing observes (`free(ptr)`, `obj->next = NULL`, three `SUCCEED_RETURN`s) | needs an assertion on the side effect, not on the status |
|
||||||
| 6 each | `aksl_fread`, `aksl_fwrite`, `aksl_fprintf`, `aksl_sprintf` |
|
| 4 | `aksl_list_append`'s cycle/tail walk | the function is broken here (§2.1.1); its known-failing test cannot pin the internals yet |
|
||||||
| 5 | `aksl_printf` |
|
| 4 | `lalloc`/`lfree` defaulting in `aksl_tree_iterate` | dead parameters (§2.2.8) — defaulted, then never called |
|
||||||
| 4 each | `aksl_memcpy`, `aksl_realpath`, `aksl_strhash_djb2`, `aksl_list_append` |
|
| 3 | `*count == -1` in the `printf` family | `*count` on the error path is itself a contract gap (§1.3) |
|
||||||
| 3 each | `aksl_malloc`, `aksl_free`, `aksl_memset`, `aksl_fopen`, `aksl_fclose`, `aksl_atoi`, `aksl_atol`, `aksl_atoll`, `aksl_atof` |
|
| 2 | `feof` in `aksl_fwrite`, `break` after the BFS `FAIL_RETURN` | unreachable in practice |
|
||||||
| 1 | `aksl_list_iterate` |
|
|
||||||
|
|
||||||
The threshold is a regression ratchet, not a quality bar; raise it as sections below
|
The threshold is a regression ratchet, not a quality bar; raise it as those survivors
|
||||||
land. Each surviving mutant is a concrete missing test — `mutation-junit.xml` lists
|
become assertions. Each one is a concrete missing test — `mutation-junit.xml` lists
|
||||||
them with `file:line` and the exact edit.
|
them with `file:line` and the exact edit.
|
||||||
- [x] **Cap every test with a CTest `TIMEOUT`.** Found while measuring the above: a mutant
|
- [x] **Cap every test with a CTest `TIMEOUT`.** Found while measuring the above: a mutant
|
||||||
that deletes `fast = fast->next->next` turns `aksl_list_iterate` into an infinite
|
that deletes `fast = fast->next->next` turns `aksl_list_iterate` into an infinite
|
||||||
@@ -89,94 +88,112 @@ against `tests/aksl_capture.h` and added to the `AKSL_TESTS` list.
|
|||||||
|
|
||||||
### 1.1 Memory wrappers
|
### 1.1 Memory wrappers
|
||||||
|
|
||||||
- [ ] `aksl_malloc` — happy path returns non-NULL and writes through `dst`.
|
- [x] `aksl_malloc` — happy path returns non-NULL and writes through `dst`.
|
||||||
- [ ] `aksl_malloc(n, NULL)` → `AKERR_NULLPOINTER`, message content asserted.
|
- [x] `aksl_malloc(n, NULL)` → `AKERR_NULLPOINTER`, message content asserted.
|
||||||
- [ ] `aksl_malloc(0, &p)` — pin down the contract. Today the result depends on whether the
|
- [ ] `aksl_malloc(0, &p)` — pin down the contract. Today the result depends on whether the
|
||||||
platform's `malloc(0)` returns NULL; if it does, the wrapper reports `errno`, which may
|
platform's `malloc(0)` returns NULL; if it does, the wrapper reports `errno`, which may
|
||||||
be `0`. See §2.2.1.
|
be `0`. See §2.2.1.
|
||||||
- [ ] `aksl_malloc(SIZE_MAX, &p)` → allocation failure surfaces `ENOMEM`, and `*dst` is left
|
- [ ] `aksl_malloc(SIZE_MAX, &p)` → allocation failure surfaces `ENOMEM`, and `*dst` is left
|
||||||
NULL rather than garbage.
|
NULL rather than garbage.
|
||||||
- [ ] `aksl_free(NULL)` → `AKERR_NULLPOINTER` (assert the documented behaviour, since
|
- [x] `aksl_free(NULL)` → `AKERR_NULLPOINTER` (assert the documented behaviour, since
|
||||||
`free(NULL)` is legal C and callers will be surprised).
|
`free(NULL)` is legal C and callers will be surprised).
|
||||||
- [ ] `aksl_free` happy path, and a malloc→free round trip under ASan.
|
- [x] `aksl_free` happy path, and a malloc→free round trip under ASan.
|
||||||
- [ ] `aksl_memset` — happy path fills the buffer; `n == 0` is a no-op; `s == NULL` →
|
- [x] `aksl_memset` — happy path fills the buffer; `n == 0` is a no-op; `s == NULL` →
|
||||||
`AKERR_NULLPOINTER`.
|
`AKERR_NULLPOINTER`.
|
||||||
- [ ] `aksl_memcpy` — happy path copies; `d == NULL` and `s == NULL` each →
|
- [x] `aksl_memcpy` — happy path copies; `d == NULL` and `s == NULL` each →
|
||||||
`AKERR_NULLPOINTER`; `n == 0` with valid pointers succeeds.
|
`AKERR_NULLPOINTER`; `n == 0` with valid pointers succeeds.
|
||||||
- [ ] `aksl_memcpy` with overlapping regions — decide and test whether this is rejected
|
- [ ] `aksl_memcpy` with overlapping regions — decide and test whether this is rejected
|
||||||
(`AKERR_VALUE`) or documented as UB like `memcpy`.
|
(`AKERR_VALUE`) or documented as UB like `memcpy`.
|
||||||
|
|
||||||
### 1.2 File I/O
|
### 1.2 File I/O
|
||||||
|
|
||||||
- [ ] `aksl_fopen` happy path on a temp file; `*fp` is written.
|
Covered by `tests/test_stream.c`.
|
||||||
- [ ] `aksl_fopen("/nonexistent/path", "r", &fp)` → `ENOENT` propagated as the status, with
|
|
||||||
|
- [x] `aksl_fopen` happy path on a temp file; `*fp` is written.
|
||||||
|
- [x] `aksl_fopen("/nonexistent/path", "r", &fp)` → `ENOENT` propagated as the status, with
|
||||||
the pathname in the message.
|
the pathname in the message.
|
||||||
- [ ] `aksl_fopen` on a mode-denied path (e.g. `/proc/1/mem`, or a `chmod 000` temp file) →
|
- [ ] `aksl_fopen` on a mode-denied path (e.g. `/proc/1/mem`, or a `chmod 000` temp file) →
|
||||||
`EACCES`.
|
`EACCES`.
|
||||||
- [ ] `aksl_fopen(path, mode, NULL)` → `AKERR_NULLPOINTER`.
|
- [x] `aksl_fopen(path, mode, NULL)` → `AKERR_NULLPOINTER`.
|
||||||
- [ ] `aksl_fopen(NULL, "r", &fp)` and `aksl_fopen(path, NULL, &fp)` → currently unchecked,
|
- [ ] `aksl_fopen(NULL, "r", &fp)` and `aksl_fopen(path, NULL, &fp)` → currently unchecked,
|
||||||
see §2.2.2. Test once the guards exist.
|
see §2.2.2. Test once the guards exist.
|
||||||
- [ ] `aksl_fread` full read; short read at EOF → `AKERR_EOF`; read from a write-only stream
|
- [x] `aksl_fread` full read; short read at EOF → `AKERR_EOF`; read from a write-only stream
|
||||||
→ `AKERR_IO`; `fp == NULL` → `AKERR_NULLPOINTER`; `ptr == NULL` → should be
|
→ `AKERR_IO`; `fp == NULL` → `AKERR_NULLPOINTER`. `ptr == NULL` is still unchecked and
|
||||||
`AKERR_NULLPOINTER` (see §2.2.3).
|
untested (see §2.2.3).
|
||||||
- [ ] `aksl_fread` **partial read that is neither EOF nor error** — assert whatever the fixed
|
- [ ] `aksl_fread` **partial read that is neither EOF nor error** — assert whatever the fixed
|
||||||
contract is; today this silently returns success and the caller cannot tell how many
|
contract is; today this silently returns success and the caller cannot tell how many
|
||||||
members were read.
|
members were read.
|
||||||
- [ ] `aksl_fwrite` happy path; write to a read-only stream → `AKERR_IO`; write to a full
|
- [x] `aksl_fwrite` happy path; write to a read-only stream → `AKERR_IO`; `fp == NULL` →
|
||||||
device (`/dev/full`) → `ENOSPC`/`AKERR_IO`; `fp == NULL` → `AKERR_NULLPOINTER`.
|
`AKERR_NULLPOINTER`. The full-device (`/dev/full`) case is still open.
|
||||||
- [ ] `aksl_fclose` happy path; `NULL` → `AKERR_NULLPOINTER`; double-close is caught or
|
- [x] `aksl_fclose` happy path; `NULL` → `AKERR_NULLPOINTER`. Double-close is still
|
||||||
documented as UB.
|
undecided, so it is neither caught nor tested.
|
||||||
- [ ] `aksl_fclose` on a stream whose buffered flush fails (`/dev/full`) → non-zero `fclose`
|
- [ ] `aksl_fclose` on a stream whose buffered flush fails (`/dev/full`) → non-zero `fclose`
|
||||||
surfaces `errno`.
|
surfaces `errno`.
|
||||||
- [ ] Round-trip test: `fopen` → `fwrite` → `fclose` → `fopen` → `fread` → compare bytes.
|
- [x] Round-trip test: `fopen` → `fwrite` → `fclose` → `fopen` → `fread` → compare bytes.
|
||||||
|
|
||||||
### 1.3 Formatted output
|
### 1.3 Formatted output
|
||||||
|
|
||||||
- [ ] `aksl_printf` / `aksl_fprintf` / `aksl_sprintf` happy paths, asserting both the byte
|
Covered by `tests/test_format.c`.
|
||||||
count written through `count` and the produced text.
|
|
||||||
- [ ] Each of the three with every pointer argument NULL in turn → `AKERR_NULLPOINTER`.
|
- [x] `aksl_printf` / `aksl_fprintf` / `aksl_sprintf` happy paths, asserting both the byte
|
||||||
- [ ] `aksl_fprintf` to a closed / read-only stream → error path, and confirm `*count` is not
|
count written through `count` and the produced text. (`aksl_printf` is checked by
|
||||||
left holding `-1` as if it were a valid length.
|
pointing `stdout` at a temp file for the duration of the call.)
|
||||||
|
- [x] Each of the three with every pointer argument NULL in turn → `AKERR_NULLPOINTER`.
|
||||||
|
- [x] `aksl_fprintf` to a read-only stream → the `errno` it saw (`EBADF`) with "Short write"
|
||||||
|
in the message. `*count` is still left holding `-1`; the test asserts the status only,
|
||||||
|
so it will not have to change when that is fixed.
|
||||||
- [ ] `aksl_sprintf` with a format that overflows the destination — currently unbounded
|
- [ ] `aksl_sprintf` with a format that overflows the destination — currently unbounded
|
||||||
(§2.2.4). Test the `aksl_snprintf` replacement once it exists.
|
(§2.2.4). Test the `aksl_snprintf` replacement once it exists.
|
||||||
- [ ] Regression test for the missing `va_end` (§2.1.4) — a test that calls each variadic
|
- [x] Regression test for the missing `va_end` (§2.1.4) — 512 variadic calls in a loop, which
|
||||||
wrapper many times in a loop, run under valgrind/ASan.
|
the sanitizer build also runs.
|
||||||
- [ ] Format-string/arg mismatch is caught at compile time once
|
- [ ] Format-string/arg mismatch is caught at compile time once
|
||||||
`__attribute__((format(printf, ...)))` is added (§2.2.5) — a negative compile test.
|
`__attribute__((format(printf, ...)))` is added (§2.2.5) — a negative compile test.
|
||||||
|
|
||||||
### 1.4 String → number
|
### 1.4 String → number
|
||||||
|
|
||||||
- [ ] `aksl_atoi` / `atol` / `atoll` / `atof` happy paths, including negative values and
|
Happy paths and NULL guards are covered by `tests/test_convert.c`; the strict-conversion
|
||||||
|
contract is asserted by `tests/test_convert_strict.c`, which is registered as a known
|
||||||
|
failure until §2.1.5 is fixed.
|
||||||
|
|
||||||
|
- [x] `aksl_atoi` / `atol` / `atoll` / `atof` happy paths, including negative values and
|
||||||
leading whitespace.
|
leading whitespace.
|
||||||
- [ ] NULL `nptr` and NULL `dest` for each → `AKERR_NULLPOINTER`.
|
- [x] NULL `nptr` and NULL `dest` for each → `AKERR_NULLPOINTER`.
|
||||||
- [ ] **Non-numeric input** (`"not a number"`) — **[CONFIRMED]** currently returns *success*
|
- [x] **Non-numeric input** (`"not a number"`) — **[CONFIRMED]** currently returns *success*
|
||||||
with `*dest == 0`. Test the `AKERR_VALUE` behaviour once §2.1.5 is fixed.
|
with `*dest == 0`. `AKERR_VALUE` is asserted by the known-failing test.
|
||||||
- [ ] **Overflow** (`"99999999999999999999"`) — **[CONFIRMED]** currently returns success with
|
- [x] **Overflow** (`"99999999999999999999"`) — **[CONFIRMED]** currently returns success with
|
||||||
a garbage value (`-1` on this box). Test for `ERANGE`.
|
a garbage value (`-1` on this box). `ERANGE` is asserted by the known-failing test.
|
||||||
- [ ] Empty string, `" "`, `"12abc"` (trailing junk), `"0x10"`, `"inf"`/`"nan"` for `atof`.
|
- [x] Empty string, `" "` and `"12abc"` (trailing junk) → `AKERR_VALUE`, in the known-failing
|
||||||
|
test. `"0x10"` and `"inf"`/`"nan"` for `atof` are still open.
|
||||||
- [ ] `LONG_MIN`/`LONG_MAX`/`LLONG_MIN`/`LLONG_MAX` boundary strings round-trip exactly.
|
- [ ] `LONG_MIN`/`LONG_MAX`/`LLONG_MIN`/`LLONG_MAX` boundary strings round-trip exactly.
|
||||||
|
|
||||||
### 1.5 `aksl_realpath`
|
### 1.5 `aksl_realpath`
|
||||||
|
|
||||||
- [ ] Happy path on an existing file and on a symlink chain; result matches `realpath(3)`.
|
Covered by `tests/test_path.c`. Every failure case there passes a zeroed `resolved_path`,
|
||||||
- [ ] Non-existent path → `ENOENT`.
|
because the wrapper's own error path formats that buffer with `%s` (§2.1.6).
|
||||||
- [ ] A path component that is not a directory → `ENOTDIR`.
|
|
||||||
|
- [x] Happy path on an existing file and on a symlink chain; result matches `realpath(3)`.
|
||||||
|
- [x] Non-existent path → `ENOENT`.
|
||||||
|
- [x] A path component that is not a directory → `ENOTDIR`.
|
||||||
- [ ] Symlink loop → `ELOOP`.
|
- [ ] Symlink loop → `ELOOP`.
|
||||||
- [ ] `path == NULL` → `AKERR_NULLPOINTER`.
|
- [x] `path == NULL` → `AKERR_NULLPOINTER`.
|
||||||
- [ ] `resolved_path == NULL` — currently unchecked and leaks (§2.1.6). Test once fixed.
|
- [ ] `resolved_path == NULL` — currently unchecked and leaks (§2.1.6). Test once fixed.
|
||||||
- [ ] A failure case where `resolved_path` is an *uninitialised* buffer — this is the crash
|
- [ ] A failure case where `resolved_path` is an *uninitialised* buffer — this is the crash
|
||||||
case in §2.1.6; run it under ASan/MSan.
|
case in §2.1.6; run it under ASan/MSan.
|
||||||
|
|
||||||
### 1.6 `aksl_strhash_djb2`
|
### 1.6 `aksl_strhash_djb2`
|
||||||
|
|
||||||
- [ ] Known-answer vectors: `djb2("")` == 5381; a handful of fixed strings with their
|
Covered by `tests/test_strhash.c`.
|
||||||
|
|
||||||
|
- [x] Known-answer vectors: `djb2("")` == 5381; a handful of fixed strings with their
|
||||||
pre-computed 32-bit values.
|
pre-computed 32-bit values.
|
||||||
- [ ] `len == 0` returns 5381 regardless of `str` contents.
|
- [x] `len == 0` returns 5381 regardless of `str` contents.
|
||||||
- [ ] NULL `str` and NULL `hashval` → `AKERR_NULLPOINTER`.
|
- [x] NULL `str` and NULL `hashval` → `AKERR_NULLPOINTER`.
|
||||||
- [ ] **High-bit bytes** (`"\xff\xfe"`) — pins down the sign-extension bug in §2.2.6; the
|
- [ ] **High-bit bytes** (`"\xff\xfe"`) — pins down the sign-extension bug in §2.2.6; the
|
||||||
expected value must be the `unsigned char` one.
|
expected value must be the `unsigned char` one (5868578; the wrapper returns the
|
||||||
- [ ] Embedded NUL bytes are hashed (the function is length-driven, not NUL-driven).
|
sign-extended 5859874 today). Every vector in the test file is 7-bit ASCII precisely so
|
||||||
- [ ] Same input → same output across two calls (no hidden state).
|
that it says nothing about this case either way.
|
||||||
|
- [x] Embedded NUL bytes are hashed (the function is length-driven, not NUL-driven).
|
||||||
|
- [x] Same input → same output across two calls (no hidden state).
|
||||||
|
|
||||||
### 1.7 Linked list
|
### 1.7 Linked list
|
||||||
|
|
||||||
@@ -220,8 +237,11 @@ against `tests/aksl_capture.h` and added to the `AKSL_TESTS` list.
|
|||||||
visits 0, 1, 3, so breaking at `tree[3]` must stop the walk at **3** visits, and today
|
visits 0, 1, 3, so breaking at `tree[3]` must stop the walk at **3** visits, and today
|
||||||
it runs on to all 7. Add the equivalent for in-order (3, 1, 4, 0, 5, 2, 6 → breaking at
|
it runs on to all 7. Add the equivalent for in-order (3, 1, 4, 0, 5, 2, 6 → breaking at
|
||||||
`tree[4]` gives 3) and post-order (3, 4, 1, 5, 6, 2, 0 → breaking at `tree[5]` gives 4).
|
`tree[4]` gives 3) and post-order (3, 4, 1, 5, 6, 2, 0 → breaking at `tree[5]` gives 4).
|
||||||
- [ ] `AKSL_TREE_SEARCH_BFS` and `AKSL_TREE_SEARCH_BFS_RIGHT` → `AKERR_NOT_IMPLEMENTED`
|
- [x] `AKSL_TREE_SEARCH_BFS` and `AKSL_TREE_SEARCH_BFS_RIGHT` → `AKERR_NOT_IMPLEMENTED`
|
||||||
today; replace with real order assertions once implemented (§3 / §2.2.9).
|
today, asserted in `tests/test_tree.c` along with the callback never being called;
|
||||||
|
replace with real order assertions once implemented (§3 / §2.2.9).
|
||||||
|
- [x] `aksl_tree_iterate` NULL `root` / NULL `iter` → `AKERR_NULLPOINTER`, and a callback error
|
||||||
|
that is *not* `AKERR_ITERATOR_BREAK` propagates out to the caller.
|
||||||
- [ ] **Unknown `searchmode`** (e.g. `99`) — **[CONFIRMED]** currently returns *success*
|
- [ ] **Unknown `searchmode`** (e.g. `99`) — **[CONFIRMED]** currently returns *success*
|
||||||
having visited nothing. Should be `AKERR_VALUE`.
|
having visited nothing. Should be `AKERR_VALUE`.
|
||||||
- [ ] **`AKSL_TREE_SEARCH_VISIT`** (defined in the header, `5`) — **[CONFIRMED]** falls into
|
- [ ] **`AKSL_TREE_SEARCH_VISIT`** (defined in the header, `5`) — **[CONFIRMED]** falls into
|
||||||
@@ -257,11 +277,12 @@ against `tests/aksl_capture.h` and added to the `AKSL_TESTS` list.
|
|||||||
|
|
||||||
### 2.1 Confirmed defects (reproduced against the built library)
|
### 2.1 Confirmed defects (reproduced against the built library)
|
||||||
|
|
||||||
The first three now have a failing test apiece, registered in `AKSL_KNOWN_FAILING_TESTS`
|
Four of these now have a failing test apiece, registered in `AKSL_KNOWN_FAILING_TESTS`
|
||||||
and therefore marked `WILL_FAIL` so the suite stays green while the gap stays visible:
|
and therefore marked `WILL_FAIL` so the suite stays green while the gap stays visible:
|
||||||
`tests/test_list_append_chain.c`, `tests/test_list_iterate_head.c` and
|
`tests/test_list_append_chain.c`, `tests/test_list_iterate_head.c`,
|
||||||
`tests/test_tree_iterate_break.c`. When one of these defects is fixed, CTest reports that
|
`tests/test_tree_iterate_break.c` and `tests/test_convert_strict.c`. When one of these
|
||||||
test as failed with *unexpectedly passed* — that is the cue to move it into `AKSL_TESTS`.
|
defects is fixed, CTest reports that test as failed with *unexpectedly passed* — that is the
|
||||||
|
cue to move it into `AKSL_TESTS`.
|
||||||
|
|
||||||
1. **`aksl_list_append` does not find the tail — it silently truncates the list.**
|
1. **`aksl_list_append` does not find the tail — it silently truncates the list.**
|
||||||
`src/stdlib.c:194`. The function conflates Floyd cycle detection with tail-finding:
|
`src/stdlib.c:194`. The function conflates Floyd cycle detection with tail-finding:
|
||||||
@@ -297,6 +318,7 @@ test as failed with *unexpectedly passed* — that is the cue to move it into `A
|
|||||||
`endptr` check for "no digits consumed" and "trailing junk", and a range check —
|
`endptr` check for "no digits consumed" and "trailing junk", and a range check —
|
||||||
raising `AKERR_VALUE` and `ERANGE` respectively. Keep the `atoi`-compatible names but
|
raising `AKERR_VALUE` and `ERANGE` respectively. Keep the `atoi`-compatible names but
|
||||||
document the stricter contract, or add `aksl_strtol`-family wrappers alongside.
|
document the stricter contract, or add `aksl_strtol`-family wrappers alongside.
|
||||||
|
`tests/test_convert_strict.c` asserts that contract and is registered as a known failure.
|
||||||
|
|
||||||
6. **`aksl_realpath` mishandles `resolved_path`.** `src/stdlib.c:171`.
|
6. **`aksl_realpath` mishandles `resolved_path`.** `src/stdlib.c:171`.
|
||||||
- `resolved_path` is never NULL-checked. `realpath(path, NULL)` is valid and mallocs a
|
- `resolved_path` is never NULL-checked. `realpath(path, NULL)` is valid and mallocs a
|
||||||
@@ -384,14 +406,19 @@ test as failed with *unexpectedly passed* — that is the cue to move it into `A
|
|||||||
|
|
||||||
15. **`aksl_TreeNode.parent` is declared but never set or read** by any library function.
|
15. **`aksl_TreeNode.parent` is declared but never set or read** by any library function.
|
||||||
|
|
||||||
16. **Header hygiene**: no `extern "C" { }` guard for C++ consumers; no version macros
|
16. **Header hygiene**: no `extern "C" { }` guard for C++ consumers; `akstdlib.h` pulls in
|
||||||
(`AKSL_VERSION_MAJOR`…); `akstdlib.h` pulls in `stdio.h`/`stdlib.h`/`string.h`/`stdint.h`
|
`stdio.h`/`stdlib.h`/`string.h`/`stdint.h` into every consumer's namespace. The version
|
||||||
into every consumer's namespace.
|
macros this item also asked for are **done**: `AKSL_VERSION_MAJOR`/`_MINOR`/`_PATCH`/
|
||||||
|
`_STRING`/`_NUMBER`/`_SONAME` are generated into `akstdlib_version.h` from
|
||||||
|
`project(akstdlib VERSION …)`, with `aksl_version()`/`aksl_version_string()`/
|
||||||
|
`aksl_version_soname()` and `AKSL_VERSION_CHECK()` reporting the *loaded* library so a
|
||||||
|
header/`.so` mismatch is nameable. See `tests/test_version.c` and `README.md`.
|
||||||
|
|
||||||
17. **Doxygen coverage is 2 functions out of 20.** A `Doxyfile` exists but only
|
17. **Doxygen coverage is 2 functions out of 24.** A `Doxyfile` exists but only
|
||||||
`aksl_tree_iterate` and `aksl_list_iterate` have doc comments. Every public function
|
`aksl_tree_iterate` and `aksl_list_iterate` have doc comments. Every public function
|
||||||
needs `@param`/`@throws`/`@return`, especially the ones whose contract deviates from libc
|
needs `@param`/`@throws`/`@return`, especially the ones whose contract deviates from libc
|
||||||
(`aksl_free(NULL)` is an error; `aksl_atoi` will become strict).
|
(`aksl_free(NULL)` is an error; `aksl_atoi` will become strict). The four
|
||||||
|
`aksl_version_*` entry points have prose comments in the header but no Doxygen tags.
|
||||||
|
|
||||||
### 2.3 Build, CI and repository
|
### 2.3 Build, CI and repository
|
||||||
|
|
||||||
@@ -404,10 +431,56 @@ test as failed with *unexpectedly passed* — that is the cue to move it into `A
|
|||||||
- [ ] `src/stdlib.c` triggers **"ISO C99 requires at least one argument for the `...`"** on
|
- [ ] `src/stdlib.c` triggers **"ISO C99 requires at least one argument for the `...`"** on
|
||||||
roughly 20 lines under `-Wpedantic`, because `FAIL_*` is called with a bare message and
|
roughly 20 lines under `-Wpedantic`, because `FAIL_*` is called with a bare message and
|
||||||
no varargs. Either always pass an argument, or add a zero-arg-safe form in libakerror.
|
no varargs. Either always pass an argument, or add a zero-arg-safe form in libakerror.
|
||||||
- [ ] **The `deps/libakerror` submodule is pinned 11 commits behind `main`** (pinned at
|
- [x] **The `deps/libakerror` submodule was pinned 22 commits behind `main`** (pinned at
|
||||||
`4fad0ce "Add gitea workflow"`; upstream is at `4212ff0`). The pin predates the
|
`4fad0ce "Add gitea workflow"`). Bumped to `5ff8790`, which is libakerror **1.0.0** —
|
||||||
refcount-leak fix, the stack-trace buffer-overflow fix, the `AKERR_MAX_ERR_VALUE`
|
the release that made the status-name table private, moved consumer status codes to a
|
||||||
correction and the format-string fix. Bump it.
|
band starting at `AKERR_FIRST_CONSUMER_STATUS` (256), made range ownership enforced,
|
||||||
|
and gave the library an soname. See `deps/libakerror/UPGRADING.md`. The bump also
|
||||||
|
brings the refcount-leak fix, the stack-trace buffer-overflow fix and the
|
||||||
|
format-string fix. Nothing in this library's sources used the removed
|
||||||
|
`AKERR_MAX_ERR_VALUE`, `__AKERR_ERROR_NAMES`, `AKERR_STATUS_RANGE_OK` or
|
||||||
|
`AKERR_STATUS_NAME_OK`, so the source change was confined to the build, the packaging
|
||||||
|
and one compile-time guard. What it did move is recorded in the three items below.
|
||||||
|
- [ ] **libakerror does not namespace its `coverage` target when embedded**, only its
|
||||||
|
`mutation` target (`deps/libakerror/CMakeLists.txt:172` vs `:189`). A
|
||||||
|
`-DAKSL_COVERAGE=ON` top-level build therefore failed to configure at all —
|
||||||
|
*"another target with the same name already exists"* — the moment the dependency
|
||||||
|
gained a coverage target. Worked around in `CMakeLists.txt` by shadowing
|
||||||
|
`add_custom_target` for the duration of the `add_subdirectory()` call and renaming the
|
||||||
|
dependency's to `akerror_coverage`, alongside the existing `add_test` shadow. **Fix
|
||||||
|
upstream and delete the workaround**: libakerror should apply the same
|
||||||
|
`CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR` test to `coverage` that it
|
||||||
|
already applies to `mutation`.
|
||||||
|
- [ ] **Branch coverage of `src/stdlib.c` fell from 51.0% to 44.3% on the 1.0.0 bump**, with
|
||||||
|
no change to this library's sources or tests. Line coverage held at 99.0% (200/202)
|
||||||
|
and function coverage at 100% (21/21); the branch *denominator* went from 661 to 1087
|
||||||
|
because the 1.0.0 `PREPARE_ERROR`/`FAIL_*` macros expand to more branches at every
|
||||||
|
call site. 337/661 became 481/1087 — 144 more branches covered, 426 more counted. The
|
||||||
|
CI branch gate was re-ratcheted 45 → 40 to match. Most of the added branches are
|
||||||
|
unreachable from the way this library calls the macros, so raising the number here
|
||||||
|
would mean testing libakerror's macros rather than this library; that belongs to
|
||||||
|
libakerror's mutation suite, since macros expand at the call site and coverage cannot
|
||||||
|
see them properly from either side. Revisit if the gap ever hides a real regression.
|
||||||
|
- [x] **`project()` declared no `VERSION`**, so `@PROJECT_VERSION@` expanded to nothing, the
|
||||||
|
installed `akstdlib.pc` shipped an empty `Version:` field, and `libakstdlib.so` carried
|
||||||
|
no soname. Now `project(akstdlib VERSION 0.1.0)`, with `SOVERSION` `0.1`
|
||||||
|
(`MAJOR.MINOR` while major is 0, `MAJOR` from 1.0 — the `if()` in `CMakeLists.txt` and
|
||||||
|
the matching `#if` in `tests/test_version.c` encode the rule and are tested against
|
||||||
|
each other), an `akstdlibConfigVersion.cmake` at `SameMinorVersion`, and version macros
|
||||||
|
generated into `akstdlib_version.h`. Verified: `find_package(akstdlib 0.1)` is accepted
|
||||||
|
while `0.2` and `1.0` are refused, and a 0.2.0 build dropped in under the 0.1 filename
|
||||||
|
is caught by `AKSL_VERSION_CHECK()`.
|
||||||
|
- [ ] **libakerror still installs no `akerrorConfigVersion.cmake`**, so
|
||||||
|
`cmake/akstdlib.cmake.in` has to call `find_dependency(akerror)` with no version — a
|
||||||
|
request for one would be refused for want of a version file regardless of what is
|
||||||
|
installed. libakstdlib now does this correctly and libakerror does not; fix it there
|
||||||
|
with the same `write_basic_package_version_file()` call, then add the `1.0.0` floor to
|
||||||
|
the `find_dependency` here. Until then the floor rests on `akstdlib.pc`'s `Requires:`
|
||||||
|
and the `#error` guard in `akstdlib.h`.
|
||||||
|
- [ ] **`aksl_version_check()` ignores its `patch` argument** (`src/stdlib.c`, the `(void)patch`),
|
||||||
|
which is correct for the current "same soname" rule but means the parameter exists only
|
||||||
|
so the error message can name the caller's full version. If a future rule needs patch
|
||||||
|
to participate, that is the line to change.
|
||||||
- [ ] **CI does not build against the submodule it pins.** `.gitea/workflows/ci.yaml` clones
|
- [ ] **CI does not build against the submodule it pins.** `.gitea/workflows/ci.yaml` clones
|
||||||
`libakerror@main` and installs it, while the build it then runs is top-level and so
|
`libakerror@main` and installs it, while the build it then runs is top-level and so
|
||||||
compiles `deps/libakerror` at the pinned commit — two different libakerror versions
|
compiles `deps/libakerror` at the pinned commit — two different libakerror versions
|
||||||
@@ -551,3 +624,92 @@ Not libc wrappers, but the existing list/tree API is visibly incomplete:
|
|||||||
NUL-terminated `aksl_strhash_djb2_str` convenience form.
|
NUL-terminated `aksl_strhash_djb2_str` convenience form.
|
||||||
- [ ] Growable buffer / string-builder type, to make the `snprintf` and `strcat` wrappers
|
- [ ] Growable buffer / string-builder type, to make the `snprintf` and `strcat` wrappers
|
||||||
pleasant to use.
|
pleasant to use.
|
||||||
|
|
||||||
|
## 4. Evidence from the first full consumer
|
||||||
|
|
||||||
|
`akbasic` (`source.starfort.tech/andrew/akbasic`) is a ~6,300-line C interpreter built on this
|
||||||
|
library and `libakerror`. It is the first consumer to exercise the whole surface rather than a
|
||||||
|
corner of it, so what it *could not* use is worth recording: it prioritizes the wishlist in §3
|
||||||
|
by what a real port actually reaches for, and it confirms which §2 entries have teeth.
|
||||||
|
|
||||||
|
**The headline number.** Across `src/`, akbasic makes **10 calls into this library** and
|
||||||
|
**116 calls to raw libc**:
|
||||||
|
|
||||||
|
| `aksl_*` used | Count |
|
||||||
|
|---|---|
|
||||||
|
| `aksl_fopen` / `aksl_fclose` | 6 |
|
||||||
|
| `aksl_fprintf` | 2 |
|
||||||
|
| `aksl_fwrite` | 1 |
|
||||||
|
| `aksl_strhash_djb2` | 1 |
|
||||||
|
|
||||||
|
| Raw libc it had to use instead | Count | Wanted from §3 |
|
||||||
|
|---|---|---|
|
||||||
|
| `snprintf` | 28 | §3.1 bounded string formatting |
|
||||||
|
| `strlen` | 37 | §3.1 strings |
|
||||||
|
| `strcmp` | 16 | §3.1 strings |
|
||||||
|
| `strncpy` | 15 | §3.1 strings |
|
||||||
|
| `memcpy` / `memset` | 16 | wrapped, but see §2.2.11 |
|
||||||
|
| `strtoll` / `strtod` | 2 | §3.1 string → number |
|
||||||
|
| `fgets` | 2 | §3.1 stream I/O |
|
||||||
|
| `strstr` | 1 | §3.1 strings |
|
||||||
|
|
||||||
|
A library whose value proposition is "turn silent libc failures into error contexts" is being
|
||||||
|
bypassed 92% of the time by the consumer most committed to it. Every one of those calls is a
|
||||||
|
place where a failure goes unreported.
|
||||||
|
|
||||||
|
### 4.1 What the port had to write for itself
|
||||||
|
|
||||||
|
1. **A strict `strtoll`/`strtod` wrapper** (`akbasic/src/convert.c`, ~60 lines). This is §2.1.5
|
||||||
|
biting, and it is not cosmetic: the Go reference this port reproduces checks `strconv`'s
|
||||||
|
error at four sites and turns each into a BASIC error. Routing them through `aksl_atoi`
|
||||||
|
would have converted four *diagnosable errors* into *wrong answers* — `VAL("garbage")`
|
||||||
|
silently returning `0.0` rather than raising, and a malformed line number becoming line 0.
|
||||||
|
akbasic's own `TODO.md` §1.9 formally bans the `aksl_ato*` family for this reason. **The
|
||||||
|
fix wanted is exactly §3.1's `strtol` family plus the endptr/`errno`/range contract §2.1.5
|
||||||
|
describes**; when it lands, `src/convert.c` gets deleted.
|
||||||
|
|
||||||
|
2. **A fixed-capacity string-keyed hash table** (`akbasic/src/symtab.c`, ~130 lines). §3.6
|
||||||
|
already calls for a "hash map built on `aksl_strhash_djb2`". This is that, three times over:
|
||||||
|
the interpreter needs one for variables, one for functions and one for labels per scope. It
|
||||||
|
is open-addressed with linear probing over a caller-supplied slot count, refuses rather than
|
||||||
|
resizes when full, and is the single most obviously-missing data structure in the library.
|
||||||
|
Worth lifting more or less verbatim if you want a starting point.
|
||||||
|
|
||||||
|
3. **The bounded-copy-with-truncation-as-error idiom, ten times.** `strncpy` followed by an
|
||||||
|
explicit NUL and a preceding length check appears at ten sites across six files. §3.1 lists
|
||||||
|
`strncpy` "with truncation reported as an error rather than silently accepted, which is the
|
||||||
|
whole value proposition here" — agreed, and the repetition is the evidence.
|
||||||
|
|
||||||
|
4. **Uppercase folding for case-insensitive lookup, three times.** Verb and function names are
|
||||||
|
case-insensitive in BASIC while variable names are not, so the port folds case before every
|
||||||
|
keyword lookup: by hand at `src/environment.c:197` and `src/parser_commands.c:113`, and
|
||||||
|
through `<ctype.h>`'s `toupper` in `src/verbs.c`. §3.1's `strcasecmp`/`strncasecmp` would
|
||||||
|
remove all three.
|
||||||
|
|
||||||
|
### 4.2 Confirmed with impact
|
||||||
|
|
||||||
|
- **§2.2.4 (`aksl_sprintf` is unbounded)** — akbasic never calls it, deliberately. Its 28
|
||||||
|
`snprintf` calls all write into fixed buffers whose size is the whole point, and there is no
|
||||||
|
`aksl_snprintf` to route them through. This is the single highest-value item in §3.1 for a
|
||||||
|
consumer of this shape.
|
||||||
|
- **§2.2.2 (`aksl_fopen` does not validate `pathname`/`mode`)** — real, and reached from user
|
||||||
|
input. akbasic's `DLOAD`/`DSAVE` take a filename from a BASIC string expression, so an empty
|
||||||
|
or unevaluated one is reachable from a running program; `akbasic_cmd_dload` validates the
|
||||||
|
name itself before handing it over, with a comment pointing here.
|
||||||
|
- **§2.2.6 (`aksl_strhash_djb2` sign-extends high bytes)** — benign for akbasic, which only
|
||||||
|
ever hashes 7-bit ASCII identifiers, and noted in its §1.3 as a hazard for anyone who later
|
||||||
|
keys a table on a filename or a string literal. Still worth fixing; the wrong answer is
|
||||||
|
platform-dependent, which is the bad kind of benign.
|
||||||
|
- **§2.1.4 (`va_end` is never called)** — akbasic's stdio text sink is built on `aksl_fprintf`
|
||||||
|
and therefore runs this UB on every line of program output. Nothing has misbehaved on
|
||||||
|
x86-64 glibc, which is exactly what makes it worth fixing before something does.
|
||||||
|
|
||||||
|
### 4.3 Not needed, and why
|
||||||
|
|
||||||
|
Recorded so the wishlist does not get prioritized purely by this consumer's shape. akbasic
|
||||||
|
uses **no** allocator (§3.1 memory), **no** lists or trees (§3.6), and **no** `aksl_realpath`:
|
||||||
|
it draws everything from fixed pools by design, and the `ak*` no-dynamic-allocation rule means
|
||||||
|
`calloc`/`realloc` would go unused here even if they existed. A consumer that does allocate
|
||||||
|
would weight §3.1's memory section far higher. The list and tree defects in §2.1.1–3 were
|
||||||
|
avoided rather than survived — akbasic's symbol tables are open-addressed precisely because
|
||||||
|
`aksl_list_append` truncates.
|
||||||
|
|||||||
@@ -6,5 +6,11 @@ includedir=${exec_prefix}/include
|
|||||||
Name: akstdlib
|
Name: akstdlib
|
||||||
Description: C stdlib with akerror sanity wrappers
|
Description: C stdlib with akerror sanity wrappers
|
||||||
Version: @PROJECT_VERSION@
|
Version: @PROJECT_VERSION@
|
||||||
|
# akerror is a public dependency, not a private one: akstdlib.h includes
|
||||||
|
# akerror.h and every entry point returns an akerr_ErrorContext *, so a consumer
|
||||||
|
# compiles against both headers and links both libraries. 1.0.0 is the floor --
|
||||||
|
# it is the release that made the status registry private and gave the library an
|
||||||
|
# soname, and akstdlib.h refuses to compile against anything older.
|
||||||
|
Requires: akerror >= 1.0.0
|
||||||
Cflags: -I${includedir}/
|
Cflags: -I${includedir}/
|
||||||
Libs: -L${libdir} -lakstdlib
|
Libs: -L${libdir} -lakstdlib
|
||||||
@@ -1,5 +1,17 @@
|
|||||||
# cmake/MyLibraryConfig.cmake.in
|
# cmake/akstdlib.cmake.in -- installed as akstdlibConfig.cmake
|
||||||
include(CMakeFindDependencyMacro) # If your library has dependencies
|
#
|
||||||
# find_dependency(AnotherDependency REQUIRED) # Example dependency
|
# akstdlibTargets.cmake names akerror::akerror in the INTERFACE_LINK_LIBRARIES of
|
||||||
|
# the imported akstdlib target, because akstdlib links it PUBLIC. Without the
|
||||||
|
# find_dependency below, a consumer that has not already found akerror itself
|
||||||
|
# gets a bare "target not found" out of the generated targets file rather than a
|
||||||
|
# message naming the dependency it is missing.
|
||||||
|
#
|
||||||
|
# No version is requested here. libakerror installs no akerrorConfigVersion.cmake,
|
||||||
|
# so find_dependency(akerror 1.0.0) would be refused for want of a version file
|
||||||
|
# regardless of which akerror is actually installed. The 1.0.0 floor is carried by
|
||||||
|
# the `Requires: akerror >= 1.0.0` line in akstdlib.pc and enforced at compile
|
||||||
|
# time by the #error guard at the top of akstdlib.h.
|
||||||
|
include(CMakeFindDependencyMacro)
|
||||||
|
find_dependency(akerror)
|
||||||
|
|
||||||
include("${CMAKE_CURRENT_LIST_DIR}/akstdlibTargets.cmake")
|
include("${CMAKE_CURRENT_LIST_DIR}/akstdlibTargets.cmake")
|
||||||
|
|||||||
2
deps/libakerror
vendored
2
deps/libakerror
vendored
Submodule deps/libakerror updated: 4fad0cec59...5ff87908e7
@@ -2,6 +2,33 @@
|
|||||||
#define _AKSTDLIB_H_
|
#define _AKSTDLIB_H_
|
||||||
|
|
||||||
#include <akerror.h>
|
#include <akerror.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* libakerror 1.0.0 is the floor. That release moved the status-name table into a
|
||||||
|
* private registry -- AKERR_MAX_ERR_VALUE and __AKERR_ERROR_NAMES are gone, the
|
||||||
|
* registry entry points raise akerr_ErrorContext * instead of returning int, and
|
||||||
|
* the library gained an soname -- so a translation unit that pairs this header
|
||||||
|
* with a pre-1.0.0 akerror.h is an ABI mismatch, not just a compile problem.
|
||||||
|
*
|
||||||
|
* libakerror publishes no version macro, so this feature-tests on
|
||||||
|
* AKERR_FIRST_CONSUMER_STATUS, which that release introduced, rather than on a
|
||||||
|
* version number that does not exist. Without the guard a stale installed header
|
||||||
|
* fails much further in, as a pile of unrelated errors inside src/stdlib.c.
|
||||||
|
*
|
||||||
|
* See deps/libakerror/UPGRADING.md.
|
||||||
|
*/
|
||||||
|
#ifndef AKERR_FIRST_CONSUMER_STATUS
|
||||||
|
#error "libakstdlib requires libakerror >= 1.0.0: the akerror.h on the include path predates the status registry. Rebuild and reinstall libakerror."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
* AKSL_VERSION_MAJOR/MINOR/PATCH/STRING/NUMBER and AKSL_VERSION_SONAME.
|
||||||
|
* Generated by CMake from include/akstdlib_version.h.in, which is why there is
|
||||||
|
* no such file in the source tree -- it is configured into the build tree and
|
||||||
|
* installed alongside this header.
|
||||||
|
*/
|
||||||
|
#include <akstdlib_version.h>
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -33,6 +60,42 @@ typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_TreeNodeIterator)(aksl_TreeNod
|
|||||||
typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_AllocFunc)(size_t size, void **dest);
|
typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_AllocFunc)(size_t size, void **dest);
|
||||||
typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_FreeFunc)(void *ptr);
|
typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_FreeFunc)(void *ptr);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Version of the shared library actually loaded, as opposed to the
|
||||||
|
* AKSL_VERSION_* macros above, which record what the caller was *compiled*
|
||||||
|
* against. The two differ exactly when a stale libakstdlib.so is on the loader
|
||||||
|
* path, which is the failure these entry points exist to name.
|
||||||
|
*
|
||||||
|
* All three pointers are required; this library treats a NULL out-param as a
|
||||||
|
* caller error rather than as "don't care" (so does aksl_free).
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_version(int *major, int *minor, int *patch);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The loaded library's version as "MAJOR.MINOR.PATCH", and its soname. Neither
|
||||||
|
* can fail -- they return pointers to string literals in the library's own
|
||||||
|
* .rodata -- so they return the string directly rather than an error context,
|
||||||
|
* the same exception akerr_name_for_status() makes for the same reason.
|
||||||
|
*/
|
||||||
|
const char *aksl_version_string(void);
|
||||||
|
const char *aksl_version_soname(void);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Compare the caller's compiled-in version against the loaded library's.
|
||||||
|
* Compatibility is defined as "same soname", so pre-1.0 both major and minor
|
||||||
|
* must match and patch is ignored; from 1.0 only major will matter. Raises
|
||||||
|
* AKERR_VALUE naming both versions on a mismatch.
|
||||||
|
*
|
||||||
|
* Call it through AKSL_VERSION_CHECK() below rather than directly. The macro
|
||||||
|
* expands at *your* call site, so it captures the AKSL_VERSION_* the caller was
|
||||||
|
* built with; the function compares them against the values baked into the
|
||||||
|
* library. Passing the numbers by hand defeats the entire mechanism.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_version_check(int major, int minor, int patch);
|
||||||
|
|
||||||
|
#define AKSL_VERSION_CHECK() \
|
||||||
|
aksl_version_check(AKSL_VERSION_MAJOR, AKSL_VERSION_MINOR, AKSL_VERSION_PATCH)
|
||||||
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_fopen(char *pathname, char *mode, FILE **fp);
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_fopen(char *pathname, char *mode, FILE **fp);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_fwrite(void *ptr, size_t size, size_t nmemb, FILE *fp);
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_fwrite(void *ptr, size_t size, size_t nmemb, FILE *fp);
|
||||||
|
|||||||
37
include/akstdlib_version.h.in
Normal file
37
include/akstdlib_version.h.in
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#ifndef _AKSTDLIB_VERSION_H_
|
||||||
|
#define _AKSTDLIB_VERSION_H_
|
||||||
|
|
||||||
|
/*
|
||||||
|
* GENERATED FILE -- do not edit.
|
||||||
|
*
|
||||||
|
* Configured from include/akstdlib_version.h.in by CMake. The version itself
|
||||||
|
* lives in exactly one place, the project() call in CMakeLists.txt, and flows
|
||||||
|
* from there into these macros, the shared library's SOVERSION, the Version:
|
||||||
|
* field in akstdlib.pc, and akstdlibConfigVersion.cmake. To bump the version,
|
||||||
|
* edit project(); to change what the macros look like, edit this template.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define AKSL_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
|
||||||
|
#define AKSL_VERSION_MINOR @PROJECT_VERSION_MINOR@
|
||||||
|
#define AKSL_VERSION_PATCH @PROJECT_VERSION_PATCH@
|
||||||
|
#define AKSL_VERSION_STRING "@PROJECT_VERSION@"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A single ordered integer, for `#if AKSL_VERSION_NUMBER >= ...` compares.
|
||||||
|
* Computed rather than written out as a literal on purpose: a literal like
|
||||||
|
* 000100 is octal in C, which would silently make version 0.1.0 compare as 64.
|
||||||
|
* Each component gets two decimal digits, so this is correct up to x.99.99.
|
||||||
|
*/
|
||||||
|
#define AKSL_VERSION_NUMBER \
|
||||||
|
((AKSL_VERSION_MAJOR * 10000) + (AKSL_VERSION_MINOR * 100) + AKSL_VERSION_PATCH)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The soname of the library these headers describe -- "0.1" for libakstdlib.so.0.1.
|
||||||
|
* This is the granularity at which the ABI is allowed to break, and it is what
|
||||||
|
* aksl_version_check() compares: pre-1.0 a minor bump is a break, so the soname
|
||||||
|
* carries MAJOR.MINOR. From 1.0 it becomes MAJOR alone, and the SOVERSION
|
||||||
|
* expression in CMakeLists.txt and this macro change together.
|
||||||
|
*/
|
||||||
|
#define AKSL_VERSION_SONAME "@AKSL_SOVERSION@"
|
||||||
|
|
||||||
|
#endif // _AKSTDLIB_VERSION_H_
|
||||||
469
scripts/coverage.py
Normal file
469
scripts/coverage.py
Normal file
@@ -0,0 +1,469 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Code coverage harness for libakstdlib.
|
||||||
|
|
||||||
|
Reports which lines, branches and functions of the library the CTest suite
|
||||||
|
actually executed. Coverage answers "what did the tests touch"; the mutation
|
||||||
|
harness in scripts/mutation_test.py answers the stronger question "would the
|
||||||
|
tests notice if it broke". They are complementary: a line can be covered and
|
||||||
|
still have no assertion behind it, so an uncovered line is definitely untested
|
||||||
|
while a covered one is only maybe tested.
|
||||||
|
|
||||||
|
Requires a build configured with -DAKSL_COVERAGE=ON, which compiles the library
|
||||||
|
and its tests with --coverage. That emits a .gcno next to every object at build
|
||||||
|
time and a .gcda next to it at run time; this script feeds those to gcov and
|
||||||
|
aggregates its JSON output.
|
||||||
|
|
||||||
|
There are no third-party dependencies (Python stdlib + gcc's own gcov), so this
|
||||||
|
works anywhere the project already builds -- no lcov, gcovr or genhtml needed.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
scripts/coverage.py --build build-coverage [options]
|
||||||
|
|
||||||
|
--build DIR build tree to read .gcda from (default: build-coverage)
|
||||||
|
--source-root DIR repo root (default: parent of this script's dir)
|
||||||
|
--gcov PROG gcov binary to use (default: $GCOV or gcov)
|
||||||
|
--include PREFIX only report sources under this repo-relative prefix;
|
||||||
|
repeatable. Default: src, include
|
||||||
|
--exclude PREFIX drop sources under this repo-relative prefix;
|
||||||
|
repeatable. Default: tests, deps, build
|
||||||
|
--zero delete every .gcda in the build tree, then exit. Run
|
||||||
|
before the suite so counters do not accumulate across
|
||||||
|
runs.
|
||||||
|
--run-tests run ctest in the build tree first (implies --zero)
|
||||||
|
--threshold PCT exit non-zero if total line coverage < PCT (default 0)
|
||||||
|
--branch-threshold PCT
|
||||||
|
exit non-zero if total branch coverage < PCT
|
||||||
|
--summary-only omit the per-file uncovered-line listing
|
||||||
|
--output PATH also write the text report to PATH (CTest hides the
|
||||||
|
output of a passing test, so the in-suite report lands
|
||||||
|
in <build>/coverage-summary.txt this way)
|
||||||
|
--cobertura PATH also write a Cobertura XML report (for CI publishers)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import xml.sax.saxutils as saxutils
|
||||||
|
|
||||||
|
DEFAULT_INCLUDE = ["src", "include"]
|
||||||
|
DEFAULT_EXCLUDE = ["tests", "deps", "build"]
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# gcov invocation and parsing
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
class FileCoverage:
|
||||||
|
"""Accumulated coverage for one source file, keyed by line number.
|
||||||
|
|
||||||
|
A single source file can be represented in several .gcda files -- a header
|
||||||
|
compiled into every test binary, or a .c file linked more than once -- so
|
||||||
|
counts are summed across all of them rather than overwritten.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, path):
|
||||||
|
self.path = path # repo-relative
|
||||||
|
self.lines = {} # line number -> execution count
|
||||||
|
self.branches = {} # line number -> list of branch counts
|
||||||
|
self.functions = {} # function name -> execution count
|
||||||
|
|
||||||
|
def add_line(self, lineno, count):
|
||||||
|
self.lines[lineno] = self.lines.get(lineno, 0) + count
|
||||||
|
|
||||||
|
def add_branches(self, lineno, counts):
|
||||||
|
cur = self.branches.setdefault(lineno, [0] * len(counts))
|
||||||
|
# Defensive: different translation units should agree on the branch
|
||||||
|
# count for a line, but never let a mismatch raise.
|
||||||
|
if len(cur) != len(counts):
|
||||||
|
if len(counts) > len(cur):
|
||||||
|
cur.extend([0] * (len(counts) - len(cur)))
|
||||||
|
counts = counts + [0] * (len(cur) - len(counts))
|
||||||
|
for i, c in enumerate(counts):
|
||||||
|
cur[i] += c
|
||||||
|
|
||||||
|
def add_function(self, name, count):
|
||||||
|
self.functions[name] = self.functions.get(name, 0) + count
|
||||||
|
|
||||||
|
# -- derived totals ---------------------------------------------------- #
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lines_total(self):
|
||||||
|
return len(self.lines)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lines_covered(self):
|
||||||
|
return sum(1 for c in self.lines.values() if c > 0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def branches_total(self):
|
||||||
|
return sum(len(b) for b in self.branches.values())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def branches_covered(self):
|
||||||
|
return sum(1 for b in self.branches.values() for c in b if c > 0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def functions_total(self):
|
||||||
|
return len(self.functions)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def functions_covered(self):
|
||||||
|
return sum(1 for c in self.functions.values() if c > 0)
|
||||||
|
|
||||||
|
def uncovered_lines(self):
|
||||||
|
return sorted(n for n, c in self.lines.items() if c == 0)
|
||||||
|
|
||||||
|
def uncovered_functions(self):
|
||||||
|
return sorted(n for n, c in self.functions.items() if c == 0)
|
||||||
|
|
||||||
|
|
||||||
|
def pct(covered, total):
|
||||||
|
"""Coverage percentage. A file with nothing instrumented counts as 100%,
|
||||||
|
matching gcov/lcov: there is nothing there to leave untested."""
|
||||||
|
return 100.0 * covered / total if total else 100.0
|
||||||
|
|
||||||
|
|
||||||
|
def find_gcda(build):
|
||||||
|
return sorted(glob.glob(os.path.join(build, "**", "*.gcda"), recursive=True))
|
||||||
|
|
||||||
|
|
||||||
|
def run_gcov(gcov, gcda):
|
||||||
|
"""Run gcov on one .gcda and return the parsed JSON, or None on failure.
|
||||||
|
|
||||||
|
gcov resolves the matching .gcno relative to the .gcda, so it is invoked
|
||||||
|
from the .gcda's directory with a bare filename.
|
||||||
|
"""
|
||||||
|
workdir = os.path.dirname(gcda) or "."
|
||||||
|
cmd = [gcov, "--json-format", "--stdout", "--branch-probabilities",
|
||||||
|
os.path.basename(gcda)]
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(cmd, cwd=workdir, stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE)
|
||||||
|
except OSError as exc:
|
||||||
|
sys.stderr.write(f"warning: cannot run {gcov}: {exc}\n")
|
||||||
|
return None
|
||||||
|
if proc.returncode != 0:
|
||||||
|
sys.stderr.write(f"warning: gcov failed on {gcda}:\n"
|
||||||
|
f"{proc.stderr.decode(errors='replace')}")
|
||||||
|
return None
|
||||||
|
text = proc.stdout.decode(errors="replace").strip()
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return json.loads(text)
|
||||||
|
except ValueError as exc:
|
||||||
|
sys.stderr.write(f"warning: unparseable gcov JSON for {gcda}: {exc}\n")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def rel_to_root(path, cwd, root):
|
||||||
|
"""Resolve a path from gcov's JSON to a repo-relative path, or None if it
|
||||||
|
falls outside the repo (libc headers, the toolchain, an out-of-tree dep)."""
|
||||||
|
if not os.path.isabs(path):
|
||||||
|
path = os.path.join(cwd, path)
|
||||||
|
path = os.path.realpath(path)
|
||||||
|
try:
|
||||||
|
rel = os.path.relpath(path, root)
|
||||||
|
except ValueError: # different drive (Windows)
|
||||||
|
return None
|
||||||
|
if rel.startswith(os.pardir):
|
||||||
|
return None
|
||||||
|
return rel.replace(os.sep, "/")
|
||||||
|
|
||||||
|
|
||||||
|
def selected(rel, includes, excludes):
|
||||||
|
"""True if a repo-relative path passes the include/exclude prefix filters.
|
||||||
|
Excludes win, so `--include src --exclude src/generated` behaves sanely."""
|
||||||
|
def under(prefix):
|
||||||
|
return rel == prefix or rel.startswith(prefix.rstrip("/") + "/")
|
||||||
|
if any(under(e) for e in excludes):
|
||||||
|
return False
|
||||||
|
if not includes:
|
||||||
|
return True
|
||||||
|
return any(under(i) for i in includes)
|
||||||
|
|
||||||
|
|
||||||
|
def collect(build, root, gcov, includes, excludes):
|
||||||
|
"""Aggregate coverage for every selected source file in the build tree."""
|
||||||
|
files = {}
|
||||||
|
gcda_files = find_gcda(build)
|
||||||
|
for gcda in gcda_files:
|
||||||
|
data = run_gcov(gcov, gcda)
|
||||||
|
if not data:
|
||||||
|
continue
|
||||||
|
cwd = data.get("current_working_directory") or os.path.dirname(gcda)
|
||||||
|
for entry in data.get("files", []):
|
||||||
|
rel = rel_to_root(entry.get("file", ""), cwd, root)
|
||||||
|
if rel is None or not selected(rel, includes, excludes):
|
||||||
|
continue
|
||||||
|
fc = files.setdefault(rel, FileCoverage(rel))
|
||||||
|
for line in entry.get("lines", []):
|
||||||
|
lineno = line.get("line_number")
|
||||||
|
if lineno is None:
|
||||||
|
continue
|
||||||
|
fc.add_line(lineno, line.get("count", 0))
|
||||||
|
branches = line.get("branches") or []
|
||||||
|
if branches:
|
||||||
|
fc.add_branches(lineno, [b.get("count", 0) for b in branches])
|
||||||
|
for fn in entry.get("functions", []):
|
||||||
|
name = fn.get("demangled_name") or fn.get("name")
|
||||||
|
if name:
|
||||||
|
fc.add_function(name, fn.get("execution_count", 0))
|
||||||
|
return files, len(gcda_files)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Reporting
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
def line_ranges(numbers):
|
||||||
|
"""Compress [3,4,5,9,11,12] into ['3-5', '9', '11-12'] for readability."""
|
||||||
|
out = []
|
||||||
|
start = prev = None
|
||||||
|
for n in numbers:
|
||||||
|
if start is None:
|
||||||
|
start = prev = n
|
||||||
|
elif n == prev + 1:
|
||||||
|
prev = n
|
||||||
|
else:
|
||||||
|
out.append(str(start) if start == prev else f"{start}-{prev}")
|
||||||
|
start = prev = n
|
||||||
|
if start is not None:
|
||||||
|
out.append(str(start) if start == prev else f"{start}-{prev}")
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def report(files, summary_only):
|
||||||
|
"""Render the text report. Returns (totals dict, report text)."""
|
||||||
|
names = sorted(files)
|
||||||
|
width = max([len(n) for n in names] + [len("TOTAL")])
|
||||||
|
out = []
|
||||||
|
|
||||||
|
header = (f" {'file'.ljust(width)} {'lines':>15} {'branches':>15} "
|
||||||
|
f"{'functions':>15}")
|
||||||
|
out.append("=" * len(header))
|
||||||
|
out.append("CODE COVERAGE SUMMARY")
|
||||||
|
out.append("=" * len(header))
|
||||||
|
out.append(header)
|
||||||
|
out.append(" " + "-" * (len(header) - 2))
|
||||||
|
|
||||||
|
def row(name, lc, lt, bc, bt, fc_, ft):
|
||||||
|
cell = lambda c, t: f"{pct(c, t):5.1f}% {c:4d}/{t:<4d}"
|
||||||
|
out.append(f" {name.ljust(width)} {cell(lc, lt):>15} "
|
||||||
|
f"{cell(bc, bt):>15} {cell(fc_, ft):>15}")
|
||||||
|
|
||||||
|
tot = [0, 0, 0, 0, 0, 0]
|
||||||
|
for name in names:
|
||||||
|
f = files[name]
|
||||||
|
vals = (f.lines_covered, f.lines_total, f.branches_covered,
|
||||||
|
f.branches_total, f.functions_covered, f.functions_total)
|
||||||
|
row(name, *vals)
|
||||||
|
tot = [t + v for t, v in zip(tot, vals)]
|
||||||
|
|
||||||
|
out.append(" " + "-" * (len(header) - 2))
|
||||||
|
row("TOTAL", *tot)
|
||||||
|
|
||||||
|
if not summary_only:
|
||||||
|
for name in names:
|
||||||
|
f = files[name]
|
||||||
|
gaps = f.uncovered_lines()
|
||||||
|
missing_fns = f.uncovered_functions()
|
||||||
|
if not gaps and not missing_fns:
|
||||||
|
continue
|
||||||
|
out.append(f"\n{name}: uncovered (each one is a missing test)")
|
||||||
|
if missing_fns:
|
||||||
|
out.append(" functions never called:")
|
||||||
|
for fn in missing_fns:
|
||||||
|
out.append(f" {fn}")
|
||||||
|
if gaps:
|
||||||
|
out.append(f" lines ({len(gaps)}): "
|
||||||
|
+ ", ".join(line_ranges(gaps)))
|
||||||
|
|
||||||
|
text = "\n".join(out) + "\n"
|
||||||
|
sys.stdout.write(text)
|
||||||
|
return text, {
|
||||||
|
"lines_covered": tot[0], "lines_total": tot[1],
|
||||||
|
"branches_covered": tot[2], "branches_total": tot[3],
|
||||||
|
"functions_covered": tot[4], "functions_total": tot[5],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def write_cobertura(path, files, root, totals):
|
||||||
|
"""Emit a minimal Cobertura report. One <class> per source file, which is
|
||||||
|
what CI coverage publishers expect for C."""
|
||||||
|
esc = saxutils.quoteattr
|
||||||
|
lr = pct(totals["lines_covered"], totals["lines_total"]) / 100.0
|
||||||
|
br = pct(totals["branches_covered"], totals["branches_total"]) / 100.0
|
||||||
|
out = ['<?xml version="1.0" encoding="UTF-8"?>',
|
||||||
|
f'<coverage line-rate="{lr:.4f}" branch-rate="{br:.4f}" '
|
||||||
|
f'lines-covered="{totals["lines_covered"]}" '
|
||||||
|
f'lines-valid="{totals["lines_total"]}" '
|
||||||
|
f'branches-covered="{totals["branches_covered"]}" '
|
||||||
|
f'branches-valid="{totals["branches_total"]}" '
|
||||||
|
'complexity="0" version="0" timestamp="0">',
|
||||||
|
' <sources>', f' <source>{saxutils.escape(root)}</source>',
|
||||||
|
' </sources>', ' <packages>',
|
||||||
|
' <package name="akstdlib" '
|
||||||
|
f'line-rate="{lr:.4f}" branch-rate="{br:.4f}" complexity="0">',
|
||||||
|
' <classes>']
|
||||||
|
for name in sorted(files):
|
||||||
|
f = files[name]
|
||||||
|
flr = pct(f.lines_covered, f.lines_total) / 100.0
|
||||||
|
fbr = pct(f.branches_covered, f.branches_total) / 100.0
|
||||||
|
cls = os.path.basename(name)
|
||||||
|
out.append(f' <class name={esc(cls)} filename={esc(name)} '
|
||||||
|
f'line-rate="{flr:.4f}" branch-rate="{fbr:.4f}" '
|
||||||
|
'complexity="0">')
|
||||||
|
out.append(' <methods/>')
|
||||||
|
out.append(' <lines>')
|
||||||
|
for lineno in sorted(f.lines):
|
||||||
|
count = f.lines[lineno]
|
||||||
|
brs = f.branches.get(lineno) or []
|
||||||
|
if brs:
|
||||||
|
taken = sum(1 for c in brs if c > 0)
|
||||||
|
cond = (f' branch="true" condition-coverage='
|
||||||
|
f'{esc(f"{pct(taken, len(brs)):.0f}% ({taken}/{len(brs)})")}')
|
||||||
|
else:
|
||||||
|
cond = ' branch="false"'
|
||||||
|
out.append(f' <line number="{lineno}" '
|
||||||
|
f'hits="{count}"{cond}/>')
|
||||||
|
out.append(' </lines>')
|
||||||
|
out.append(' </class>')
|
||||||
|
out += [' </classes>', ' </package>', ' </packages>',
|
||||||
|
'</coverage>']
|
||||||
|
with open(path, "w") as fh:
|
||||||
|
fh.write("\n".join(out) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Driver
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
def zero_counters(build):
|
||||||
|
"""Delete accumulated .gcda. gcov counters are cumulative across runs, so
|
||||||
|
without this a report mixes in every earlier suite run (and any test binary
|
||||||
|
run by hand), which quietly overstates coverage."""
|
||||||
|
removed = 0
|
||||||
|
for gcda in find_gcda(build):
|
||||||
|
try:
|
||||||
|
os.remove(gcda)
|
||||||
|
removed += 1
|
||||||
|
except OSError as exc:
|
||||||
|
sys.stderr.write(f"warning: cannot remove {gcda}: {exc}\n")
|
||||||
|
return removed
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(line_buffering=True)
|
||||||
|
except (AttributeError, ValueError):
|
||||||
|
pass
|
||||||
|
here = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
default_root = os.path.dirname(here)
|
||||||
|
|
||||||
|
ap = argparse.ArgumentParser(description="Code coverage for libakstdlib")
|
||||||
|
ap.add_argument("--build", default="build-coverage")
|
||||||
|
ap.add_argument("--source-root", default=default_root)
|
||||||
|
ap.add_argument("--gcov", default=os.environ.get("GCOV", "gcov"))
|
||||||
|
ap.add_argument("--include", action="append", default=None)
|
||||||
|
ap.add_argument("--exclude", action="append", default=None)
|
||||||
|
ap.add_argument("--zero", action="store_true")
|
||||||
|
ap.add_argument("--run-tests", action="store_true")
|
||||||
|
ap.add_argument("--threshold", type=float, default=0.0)
|
||||||
|
ap.add_argument("--branch-threshold", type=float, default=0.0)
|
||||||
|
ap.add_argument("--summary-only", action="store_true")
|
||||||
|
ap.add_argument("--output", default=None,
|
||||||
|
help="also write the text report to this path")
|
||||||
|
ap.add_argument("--cobertura", default=None)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
root = os.path.realpath(args.source_root)
|
||||||
|
build = os.path.abspath(args.build)
|
||||||
|
includes = args.include if args.include is not None else DEFAULT_INCLUDE
|
||||||
|
if args.exclude is not None:
|
||||||
|
excludes = args.exclude
|
||||||
|
else:
|
||||||
|
# Excludes win over includes, so a *default* exclude must not silently
|
||||||
|
# cancel an explicit --include: `--include tests` has to report tests/
|
||||||
|
# even though tests/ is excluded by default.
|
||||||
|
excludes = [e for e in DEFAULT_EXCLUDE
|
||||||
|
if not any(selected(e, [i], []) for i in (args.include or []))]
|
||||||
|
|
||||||
|
if not os.path.isdir(build):
|
||||||
|
sys.stderr.write(f"error: no such build directory: {build}\n"
|
||||||
|
"Configure one with:\n"
|
||||||
|
" cmake -S . -B build-coverage -DAKSL_COVERAGE=ON\n"
|
||||||
|
" cmake --build build-coverage\n")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
if args.zero and not args.run_tests:
|
||||||
|
n = zero_counters(build)
|
||||||
|
print(f"Cleared {n} .gcda counter file(s) in {build}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.run_tests:
|
||||||
|
n = zero_counters(build)
|
||||||
|
print(f"Cleared {n} .gcda counter file(s) in {build}")
|
||||||
|
print("Running ctest ...")
|
||||||
|
rc = subprocess.call(["ctest", "--test-dir", build,
|
||||||
|
"--output-on-failure"])
|
||||||
|
if rc != 0:
|
||||||
|
# Report anyway: partial coverage from a red suite is still useful,
|
||||||
|
# and the ctest output above already shows what failed.
|
||||||
|
sys.stderr.write(f"warning: ctest exited {rc}; "
|
||||||
|
"reporting coverage from a failing suite\n")
|
||||||
|
print()
|
||||||
|
|
||||||
|
files, n_gcda = collect(build, root, args.gcov, includes, excludes)
|
||||||
|
if not n_gcda:
|
||||||
|
sys.stderr.write(
|
||||||
|
f"error: no .gcda files under {build}.\n"
|
||||||
|
"Either the build was not configured with -DAKSL_COVERAGE=ON, or\n"
|
||||||
|
"the test suite has not been run yet (try --run-tests).\n")
|
||||||
|
return 2
|
||||||
|
if not files:
|
||||||
|
sys.stderr.write(
|
||||||
|
f"error: {n_gcda} .gcda file(s) found, but none of the covered "
|
||||||
|
"sources matched\nthe include/exclude filters "
|
||||||
|
f"(include={includes}, exclude={excludes}).\n")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
text, totals = report(files, args.summary_only)
|
||||||
|
|
||||||
|
# CTest swallows the output of a passing test, so also drop the report on
|
||||||
|
# disk: that is what makes the in-suite coverage_report entry useful without
|
||||||
|
# having to re-run ctest under -V.
|
||||||
|
if args.output:
|
||||||
|
path = os.path.abspath(args.output)
|
||||||
|
try:
|
||||||
|
with open(path, "w") as fh:
|
||||||
|
fh.write(text)
|
||||||
|
print(f"\nReport written to: {path}")
|
||||||
|
except OSError as exc:
|
||||||
|
sys.stderr.write(f"warning: cannot write {path}: {exc}\n")
|
||||||
|
|
||||||
|
if args.cobertura:
|
||||||
|
path = os.path.abspath(args.cobertura)
|
||||||
|
write_cobertura(path, files, root, totals)
|
||||||
|
print(f"\nCobertura report written to: {path}")
|
||||||
|
|
||||||
|
line_pct = pct(totals["lines_covered"], totals["lines_total"])
|
||||||
|
branch_pct = pct(totals["branches_covered"], totals["branches_total"])
|
||||||
|
failed = False
|
||||||
|
if args.threshold > 0 and line_pct < args.threshold:
|
||||||
|
print(f"\nFAIL: line coverage {line_pct:.1f}% < "
|
||||||
|
f"threshold {args.threshold:.1f}%")
|
||||||
|
failed = True
|
||||||
|
if args.branch_threshold > 0 and branch_pct < args.branch_threshold:
|
||||||
|
print(f"\nFAIL: branch coverage {branch_pct:.1f}% < "
|
||||||
|
f"threshold {args.branch_threshold:.1f}%")
|
||||||
|
failed = True
|
||||||
|
return 1 if failed else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -277,8 +277,11 @@ def write_junit(path, records, targets):
|
|||||||
|
|
||||||
|
|
||||||
def copy_tree(src, dst):
|
def copy_tree(src, dst):
|
||||||
ignore = shutil.ignore_patterns("build", ".git", "*.o", "*.so", "*~",
|
# "build*" covers build/, build-asan/ and build-coverage/: the harness
|
||||||
"#*#", "*.iso", "*.png")
|
# configures its own tree inside the copy, so copying those is pure IO --
|
||||||
|
# and a coverage tree drags along every .gcno/.gcda as well.
|
||||||
|
ignore = shutil.ignore_patterns("build*", ".git", "*.o", "*.so", "*~",
|
||||||
|
"#*#", "*.iso", "*.png", "*.gcno", "*.gcda")
|
||||||
shutil.copytree(src, dst, ignore=ignore, symlinks=True)
|
shutil.copytree(src, dst, ignore=ignore, symlinks=True)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
51
src/stdlib.c
51
src/stdlib.c
@@ -6,6 +6,57 @@
|
|||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Version of the library itself. These read the AKSL_VERSION_* macros as they
|
||||||
|
* were when *this translation unit* was compiled, which is what makes them the
|
||||||
|
* loaded library's version rather than the caller's: the caller's copy of the
|
||||||
|
* same macros comes from whatever akstdlib_version.h it compiled against.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_version(int *major, int *minor, int *patch)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, major, AKERR_NULLPOINTER, "major=%p, minor=%p, patch=%p",
|
||||||
|
(void *)major, (void *)minor, (void *)patch);
|
||||||
|
FAIL_ZERO_RETURN(e, minor, AKERR_NULLPOINTER, "major=%p, minor=%p, patch=%p",
|
||||||
|
(void *)major, (void *)minor, (void *)patch);
|
||||||
|
FAIL_ZERO_RETURN(e, patch, AKERR_NULLPOINTER, "major=%p, minor=%p, patch=%p",
|
||||||
|
(void *)major, (void *)minor, (void *)patch);
|
||||||
|
*major = AKSL_VERSION_MAJOR;
|
||||||
|
*minor = AKSL_VERSION_MINOR;
|
||||||
|
*patch = AKSL_VERSION_PATCH;
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *aksl_version_string(void)
|
||||||
|
{
|
||||||
|
return AKSL_VERSION_STRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *aksl_version_soname(void)
|
||||||
|
{
|
||||||
|
return AKSL_VERSION_SONAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Compatibility is "same soname". Pre-1.0 that is MAJOR.MINOR, so both are
|
||||||
|
* compared and patch is deliberately ignored; from 1.0 the minor comparison
|
||||||
|
* comes out, in step with the SOVERSION expression in CMakeLists.txt. The
|
||||||
|
* caller's numbers arrive as arguments because AKSL_VERSION_CHECK() expanded
|
||||||
|
* them at the caller's site -- see the macro in akstdlib.h.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_version_check(int major, int minor, int patch)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
(void)patch;
|
||||||
|
FAIL_NONZERO_RETURN(e,
|
||||||
|
(major != AKSL_VERSION_MAJOR || minor != AKSL_VERSION_MINOR),
|
||||||
|
AKERR_VALUE,
|
||||||
|
"compiled against libakstdlib %d.%d.%d, loaded %s (soname %s)",
|
||||||
|
major, minor, patch,
|
||||||
|
AKSL_VERSION_STRING, AKSL_VERSION_SONAME);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_malloc(size_t size, void **dst)
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_malloc(size_t size, void **dst)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(e);
|
PREPARE_ERROR(e);
|
||||||
|
|||||||
@@ -18,7 +18,9 @@
|
|||||||
* AKSL_CHECK_STATUS() run an akerror-returning expression, assert on the
|
* AKSL_CHECK_STATUS() run an akerror-returning expression, assert on the
|
||||||
* status it came back with, and release the context
|
* status it came back with, and release the context
|
||||||
* so the error pool does not leak.
|
* so the error pool does not leak.
|
||||||
* AKSL_CHECK_OK() the status == 0 (success) case of the above.
|
* AKSL_CHECK_STATUS_MSG_CONTAINS()
|
||||||
|
* assert status first, then message content.
|
||||||
|
* AKSL_CHECK_OK() assert a call returned no error context at all.
|
||||||
* aksl_last_status/... the status, message and function name of the most
|
* aksl_last_status/... the status, message and function name of the most
|
||||||
* recent context taken by AKSL_CHECK_STATUS.
|
* recent context taken by AKSL_CHECK_STATUS.
|
||||||
* aksl_capture_install() swap in a capturing akerr_log_method so a test can
|
* aksl_capture_install() swap in a capturing akerr_log_method so a test can
|
||||||
@@ -26,6 +28,8 @@
|
|||||||
* unhandled-error output.
|
* unhandled-error output.
|
||||||
* aksl_slots_in_use() how many slots are currently checked out of
|
* aksl_slots_in_use() how many slots are currently checked out of
|
||||||
* AKERR_ARRAY_ERROR, for pool-leak assertions.
|
* AKERR_ARRAY_ERROR, for pool-leak assertions.
|
||||||
|
* aksl_temp_file() create an empty temp file for the stream, formatted
|
||||||
|
* output and path tests to work on.
|
||||||
* AKSL_RUN() run one test function and tally the result.
|
* AKSL_RUN() run one test function and tally the result.
|
||||||
*
|
*
|
||||||
* Tests are written as a set of `static int test_xxx(void)` functions that
|
* Tests are written as a set of `static int test_xxx(void)` functions that
|
||||||
@@ -35,7 +39,9 @@
|
|||||||
#include <akstdlib.h>
|
#include <akstdlib.h>
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------- */
|
/* ---------------------------------------------------------------------- */
|
||||||
/* Log capture */
|
/* Log capture */
|
||||||
@@ -93,6 +99,68 @@ static int __attribute__((unused)) aksl_slots_in_use(void)
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Temp files */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
#define AKSL_TMP_MAX 256
|
||||||
|
#define AKSL_TMP_TRACKED 32
|
||||||
|
|
||||||
|
static char aksl_tmp_paths[AKSL_TMP_TRACKED][AKSL_TMP_MAX];
|
||||||
|
static int aksl_tmp_count = 0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Unlink every path aksl_temp_file() handed out. Registered with atexit, so the
|
||||||
|
* files go away even when a test returns early on a failed assertion -- which is
|
||||||
|
* the normal case for a known-failing test and for every mutant the mutation
|
||||||
|
* harness builds. Paths a test already unlinked simply fail here, harmlessly.
|
||||||
|
*/
|
||||||
|
static void aksl_temp_cleanup(void)
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for ( i = 0; i < aksl_tmp_count; i++ ) {
|
||||||
|
unlink(aksl_tmp_paths[i]);
|
||||||
|
}
|
||||||
|
aksl_tmp_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create an empty temp file under $TMPDIR (or /tmp) and write its path into buf.
|
||||||
|
* Returns 0 on success, non-zero on failure.
|
||||||
|
*
|
||||||
|
* mkstemp both names and creates the file, so a test that opens the path for
|
||||||
|
* reading is never racing another process for the name. Tests should still
|
||||||
|
* unlink what they create -- asserting on it catches a wrapper that removed or
|
||||||
|
* renamed the file -- but aksl_temp_cleanup() is the backstop.
|
||||||
|
*/
|
||||||
|
static int __attribute__((unused)) aksl_temp_file(char *buf, size_t n)
|
||||||
|
{
|
||||||
|
const char *dir = getenv("TMPDIR");
|
||||||
|
int fd = -1;
|
||||||
|
|
||||||
|
if ( dir == NULL || dir[0] == '\0' ) {
|
||||||
|
dir = "/tmp";
|
||||||
|
}
|
||||||
|
if ( (size_t)snprintf(buf, n, "%s/aksl_test_XXXXXX", dir) >= n ) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
fd = mkstemp(buf);
|
||||||
|
if ( fd < 0 ) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
close(fd);
|
||||||
|
|
||||||
|
if ( aksl_tmp_count < AKSL_TMP_TRACKED ) {
|
||||||
|
if ( aksl_tmp_count == 0 && atexit(&aksl_temp_cleanup) != 0 ) {
|
||||||
|
return 0; /* tracking is best-effort; the file itself is fine */
|
||||||
|
}
|
||||||
|
snprintf(aksl_tmp_paths[aksl_tmp_count], AKSL_TMP_MAX, "%s", buf);
|
||||||
|
aksl_tmp_count++;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------- */
|
/* ---------------------------------------------------------------------- */
|
||||||
/* Taking ownership of a returned error context */
|
/* Taking ownership of a returned error context */
|
||||||
/* ---------------------------------------------------------------------- */
|
/* ---------------------------------------------------------------------- */
|
||||||
@@ -106,8 +174,9 @@ static char aksl_last_function[AKERR_MAX_ERROR_FNAME_LENGTH];
|
|||||||
/*
|
/*
|
||||||
* Record the status/message/function of a returned context, release it back to
|
* Record the status/message/function of a returned context, release it back to
|
||||||
* the pool, and hand back the status. A NULL context means success, which is
|
* the pool, and hand back the status. A NULL context means success, which is
|
||||||
* status 0. Every akstdlib call in a test should go through this (or through
|
* status 0. Failure-path assertions should go through this so that no test
|
||||||
* AKSL_CHECK_STATUS, which wraps it) so that no test leaks a pool slot.
|
* leaks a pool slot. Success-path assertions use AKSL_CHECK_OK, which also
|
||||||
|
* verifies that no bogus status-0 context was returned.
|
||||||
*/
|
*/
|
||||||
static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e)
|
static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e)
|
||||||
{
|
{
|
||||||
@@ -168,7 +237,30 @@ static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e)
|
|||||||
} \
|
} \
|
||||||
} while ( 0 )
|
} while ( 0 )
|
||||||
|
|
||||||
#define AKSL_CHECK_OK(__expr) AKSL_CHECK_STATUS(__expr, 0)
|
/*
|
||||||
|
* Success is represented by a NULL error context, not just status 0. This catches
|
||||||
|
* wrappers that incorrectly raise an error using a stale errno value of 0.
|
||||||
|
*/
|
||||||
|
#define AKSL_CHECK_OK(__expr) \
|
||||||
|
do { \
|
||||||
|
akerr_ErrorContext *__e = (__expr); \
|
||||||
|
if ( __e != NULL ) { \
|
||||||
|
int __st = aksl_take(__e); \
|
||||||
|
fprintf(stderr, \
|
||||||
|
" CHECK FAILED: %s\n" \
|
||||||
|
" returned error context with status %d (%s) \"%s\"\n" \
|
||||||
|
" expected no error context\n" \
|
||||||
|
" at %s:%d\n", \
|
||||||
|
#__expr, \
|
||||||
|
__st, akerr_name_for_status(__st, NULL), \
|
||||||
|
aksl_last_message, \
|
||||||
|
__FILE__, __LINE__); \
|
||||||
|
return 1; \
|
||||||
|
} \
|
||||||
|
aksl_last_status = 0; \
|
||||||
|
aksl_last_message[0] = '\0'; \
|
||||||
|
aksl_last_function[0] = '\0'; \
|
||||||
|
} while ( 0 )
|
||||||
|
|
||||||
#define AKSL_CHECK_CONTAINS(needle) \
|
#define AKSL_CHECK_CONTAINS(needle) \
|
||||||
AKSL_CHECK(strstr(aksl_capture_buf, (needle)) != NULL)
|
AKSL_CHECK(strstr(aksl_capture_buf, (needle)) != NULL)
|
||||||
@@ -176,9 +268,42 @@ static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e)
|
|||||||
#define AKSL_CHECK_NOT_CONTAINS(needle) \
|
#define AKSL_CHECK_NOT_CONTAINS(needle) \
|
||||||
AKSL_CHECK(strstr(aksl_capture_buf, (needle)) == NULL)
|
AKSL_CHECK(strstr(aksl_capture_buf, (needle)) == NULL)
|
||||||
|
|
||||||
/* Assert on the message of the context most recently taken. */
|
/*
|
||||||
#define AKSL_CHECK_MSG_CONTAINS(needle) \
|
* Message checks are secondary: the returned akerr status is the contract. Keep
|
||||||
AKSL_CHECK(strstr(aksl_last_message, (needle)) != NULL)
|
* the status and message assertions coupled so tests cannot pass on text alone.
|
||||||
|
*/
|
||||||
|
#define AKSL_CHECK_STATUS_MSG_CONTAINS(__expr, __expected, __needle) \
|
||||||
|
do { \
|
||||||
|
int __st = aksl_take(__expr); \
|
||||||
|
if ( __st != (__expected) ) { \
|
||||||
|
fprintf(stderr, \
|
||||||
|
" CHECK FAILED: %s\n" \
|
||||||
|
" got %d (%s) \"%s\"\n" \
|
||||||
|
" expected %d (%s)\n" \
|
||||||
|
" at %s:%d\n", \
|
||||||
|
#__expr, \
|
||||||
|
__st, akerr_name_for_status(__st, NULL), \
|
||||||
|
aksl_last_message, \
|
||||||
|
(__expected), \
|
||||||
|
akerr_name_for_status((__expected), NULL), \
|
||||||
|
__FILE__, __LINE__); \
|
||||||
|
return 1; \
|
||||||
|
} \
|
||||||
|
if ( strstr(aksl_last_message, (__needle)) == NULL ) { \
|
||||||
|
fprintf(stderr, \
|
||||||
|
" CHECK FAILED: message from %s\n" \
|
||||||
|
" got \"%s\"\n" \
|
||||||
|
" expected substring \"%s\"\n" \
|
||||||
|
" status %d (%s)\n" \
|
||||||
|
" at %s:%d\n", \
|
||||||
|
#__expr, \
|
||||||
|
aksl_last_message, \
|
||||||
|
(__needle), \
|
||||||
|
__st, akerr_name_for_status(__st, NULL), \
|
||||||
|
__FILE__, __LINE__); \
|
||||||
|
return 1; \
|
||||||
|
} \
|
||||||
|
} while ( 0 )
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------- */
|
/* ---------------------------------------------------------------------- */
|
||||||
/* Test driver */
|
/* Test driver */
|
||||||
|
|||||||
128
tests/test_convert.c
Normal file
128
tests/test_convert.c
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* String -> number wrappers: aksl_atoi / atol / atoll / atof.
|
||||||
|
*
|
||||||
|
* TODO.md section 1.4. This file covers the parts of the contract that are
|
||||||
|
* stable today: the happy paths (including negatives and leading whitespace)
|
||||||
|
* and the NULL guards.
|
||||||
|
*
|
||||||
|
* It deliberately says nothing about non-numeric input, trailing junk or
|
||||||
|
* overflow. Those all return *success* today (TODO.md 2.1.5) and the correct
|
||||||
|
* behaviour is asserted by tests/test_convert_strict.c, which is registered as
|
||||||
|
* a known failure. Pinning the current lax behaviour here as well would mean
|
||||||
|
* this file starts failing on the day the defect is fixed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "aksl_capture.h"
|
||||||
|
|
||||||
|
static int test_atoi_converts_positive(void)
|
||||||
|
{
|
||||||
|
int out = -1;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_atoi("1234", &out));
|
||||||
|
AKSL_CHECK(out == 1234);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_atoi_converts_negative(void)
|
||||||
|
{
|
||||||
|
int out = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_atoi("-42", &out));
|
||||||
|
AKSL_CHECK(out == -42);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_atoi_skips_leading_whitespace(void)
|
||||||
|
{
|
||||||
|
int out = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_atoi(" \t 7", &out));
|
||||||
|
AKSL_CHECK(out == 7);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_atoi_rejects_null_arguments(void)
|
||||||
|
{
|
||||||
|
int out = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_atoi(NULL, &out),
|
||||||
|
AKERR_NULLPOINTER, "nptr=");
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_atoi("1", NULL),
|
||||||
|
AKERR_NULLPOINTER, "dest=");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_atol_converts_and_rejects_null(void)
|
||||||
|
{
|
||||||
|
long out = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_atol("2147483648", &out));
|
||||||
|
AKSL_CHECK(out == 2147483648L);
|
||||||
|
AKSL_CHECK_OK(aksl_atol("-2147483648", &out));
|
||||||
|
AKSL_CHECK(out == -2147483648L);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_atol(NULL, &out), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_atol("1", NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_atoll_converts_and_rejects_null(void)
|
||||||
|
{
|
||||||
|
long long out = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_atoll("9007199254740993", &out));
|
||||||
|
AKSL_CHECK(out == 9007199254740993LL);
|
||||||
|
AKSL_CHECK_OK(aksl_atoll("-9007199254740993", &out));
|
||||||
|
AKSL_CHECK(out == -9007199254740993LL);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_atoll(NULL, &out), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_atoll("1", NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_atof_converts_and_rejects_null(void)
|
||||||
|
{
|
||||||
|
double out = 0.0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_atof("2.5", &out));
|
||||||
|
AKSL_CHECK(out == 2.5);
|
||||||
|
AKSL_CHECK_OK(aksl_atof(" -0.125", &out));
|
||||||
|
AKSL_CHECK(out == -0.125);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_atof(NULL, &out), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_atof("1", NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The wrappers hold no state: the same input converts the same way twice. */
|
||||||
|
static int test_conversions_are_repeatable(void)
|
||||||
|
{
|
||||||
|
int first = 0;
|
||||||
|
int second = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_atoi("321", &first));
|
||||||
|
AKSL_CHECK_OK(aksl_atoi("321", &second));
|
||||||
|
AKSL_CHECK(first == second);
|
||||||
|
AKSL_CHECK(first == 321);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
akerr_init();
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_atoi_converts_positive);
|
||||||
|
AKSL_RUN(failures, test_atoi_converts_negative);
|
||||||
|
AKSL_RUN(failures, test_atoi_skips_leading_whitespace);
|
||||||
|
AKSL_RUN(failures, test_atoi_rejects_null_arguments);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_atol_converts_and_rejects_null);
|
||||||
|
AKSL_RUN(failures, test_atoll_converts_and_rejects_null);
|
||||||
|
AKSL_RUN(failures, test_atof_converts_and_rejects_null);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_conversions_are_repeatable);
|
||||||
|
|
||||||
|
AKSL_REPORT(failures);
|
||||||
|
}
|
||||||
75
tests/test_convert_strict.c
Normal file
75
tests/test_convert_strict.c
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* KNOWN FAILING -- TODO.md section 2.1.5
|
||||||
|
*
|
||||||
|
* The ato* wrappers cannot report a conversion failure. atoi(3) and friends have
|
||||||
|
* no error channel at all: "not a number" converts to 0 and an overflowing
|
||||||
|
* literal converts to a wrapped value, and in both cases the wrapper hands back
|
||||||
|
* success. A library whose entire purpose is turning silent libc failures into
|
||||||
|
* error contexts should not be the one place a bad conversion passes unnoticed.
|
||||||
|
*
|
||||||
|
* This test asserts the contract the fix should provide -- reimplemented over
|
||||||
|
* strtol/strtoll/strtod with errno cleared, an endptr check for "no digits
|
||||||
|
* consumed" and "trailing junk", and a range check:
|
||||||
|
*
|
||||||
|
* no digits consumed / trailing junk -> AKERR_VALUE
|
||||||
|
* value out of range -> ERANGE
|
||||||
|
*
|
||||||
|
* Registered in AKSL_KNOWN_FAILING_TESTS (WILL_FAIL). When the wrappers get a
|
||||||
|
* real error channel, CTest reports this as unexpectedly passing -- move it into
|
||||||
|
* AKSL_TESTS then, and fold the cases into tests/test_convert.c.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "aksl_capture.h"
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
static int test_non_numeric_input_is_a_value_error(void)
|
||||||
|
{
|
||||||
|
int out = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_atoi("not a number", &out), AKERR_VALUE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_empty_input_is_a_value_error(void)
|
||||||
|
{
|
||||||
|
int out = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_atoi("", &out), AKERR_VALUE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_atoi(" ", &out), AKERR_VALUE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_trailing_junk_is_a_value_error(void)
|
||||||
|
{
|
||||||
|
int out = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_atoi("12abc", &out), AKERR_VALUE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_overflow_is_erange(void)
|
||||||
|
{
|
||||||
|
int out = 0;
|
||||||
|
long lout = 0;
|
||||||
|
long long llout = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_atoi("99999999999999999999", &out), ERANGE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_atol("99999999999999999999999999", &lout), ERANGE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_atoll("99999999999999999999999999", &llout), ERANGE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
akerr_init();
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_non_numeric_input_is_a_value_error);
|
||||||
|
AKSL_RUN(failures, test_empty_input_is_a_value_error);
|
||||||
|
AKSL_RUN(failures, test_trailing_junk_is_a_value_error);
|
||||||
|
AKSL_RUN(failures, test_overflow_is_erange);
|
||||||
|
|
||||||
|
AKSL_REPORT(failures);
|
||||||
|
}
|
||||||
224
tests/test_format.c
Normal file
224
tests/test_format.c
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
/*
|
||||||
|
* Formatted-output wrappers: aksl_printf / aksl_fprintf / aksl_sprintf.
|
||||||
|
*
|
||||||
|
* TODO.md section 1.3. Each happy path asserts both halves of the contract --
|
||||||
|
* the byte count handed back through *count and the text that actually landed
|
||||||
|
* somewhere -- and every pointer argument is checked for its NULL guard.
|
||||||
|
*
|
||||||
|
* Readback goes through plain libc rather than aksl_fread so that a failure here
|
||||||
|
* points at the formatted-output wrapper under test and not at the stream
|
||||||
|
* wrappers, which tests/test_stream.c covers.
|
||||||
|
*
|
||||||
|
* Not covered: the destination-overflow case, because aksl_sprintf wraps the
|
||||||
|
* unbounded vsprintf and there is no bounded entry point to test yet
|
||||||
|
* (TODO.md 2.2.4).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "aksl_capture.h"
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
/* Read a whole file into buf and NUL-terminate. Returns bytes read, or -1. */
|
||||||
|
static long read_file(const char *path, char *buf, size_t n)
|
||||||
|
{
|
||||||
|
FILE *fp = fopen(path, "r");
|
||||||
|
size_t got = 0;
|
||||||
|
|
||||||
|
if ( fp == NULL ) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
got = fread(buf, 1, n - 1, fp);
|
||||||
|
buf[got] = '\0';
|
||||||
|
if ( ferror(fp) ) {
|
||||||
|
fclose(fp);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
fclose(fp);
|
||||||
|
return (long)got;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_sprintf_writes_text_and_count(void)
|
||||||
|
{
|
||||||
|
char buf[64];
|
||||||
|
int count = -1;
|
||||||
|
|
||||||
|
memset(buf, 0x00, sizeof(buf));
|
||||||
|
AKSL_CHECK_OK(aksl_sprintf(&count, buf, "%s=%d", "x", 7));
|
||||||
|
AKSL_CHECK(count == 3);
|
||||||
|
AKSL_CHECK(strcmp(buf, "x=7") == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_sprintf_empty_format_writes_nothing(void)
|
||||||
|
{
|
||||||
|
char buf[8] = { 'z', 'z', 'z', 'z', 'z', 'z', 'z', 'z' };
|
||||||
|
int count = -1;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_sprintf(&count, buf, "%s", ""));
|
||||||
|
AKSL_CHECK(count == 0);
|
||||||
|
AKSL_CHECK(buf[0] == '\0');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_sprintf_rejects_null_arguments(void)
|
||||||
|
{
|
||||||
|
char buf[8];
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
memset(buf, 0x00, sizeof(buf));
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_sprintf(NULL, buf, "x"),
|
||||||
|
AKERR_NULLPOINTER, "count=");
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_sprintf(&count, NULL, "x"),
|
||||||
|
AKERR_NULLPOINTER, "str=");
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_sprintf(&count, buf, NULL),
|
||||||
|
AKERR_NULLPOINTER, "format=");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_fprintf_writes_to_stream(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
char readback[64];
|
||||||
|
FILE *fp = NULL;
|
||||||
|
int count = -1;
|
||||||
|
|
||||||
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
|
||||||
|
AKSL_CHECK_OK(aksl_fprintf(&count, fp, "%s %d", "value", 42));
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
|
||||||
|
AKSL_CHECK(count == 8);
|
||||||
|
AKSL_CHECK(read_file(path, readback, sizeof(readback)) == 8);
|
||||||
|
AKSL_CHECK(strcmp(readback, "value 42") == 0);
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* vfprintf on a stream opened "r" fails outright, so the wrapper reports the
|
||||||
|
* errno it saw (EBADF on glibc). *count is left holding -1 in this case, which
|
||||||
|
* TODO.md 1.3 flags as a contract gap -- the status is the assertion here.
|
||||||
|
*/
|
||||||
|
static int test_fprintf_to_read_only_stream_reports_errno(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
FILE *fp = NULL;
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fprintf(&count, fp, "%d", 1),
|
||||||
|
EBADF, "Short write");
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_fprintf_rejects_null_arguments(void)
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fprintf(NULL, stdout, "x"),
|
||||||
|
AKERR_NULLPOINTER, "count=");
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fprintf(&count, NULL, "x"),
|
||||||
|
AKERR_NULLPOINTER, "stream=");
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fprintf(&count, stdout, NULL),
|
||||||
|
AKERR_NULLPOINTER, "format=");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* aksl_printf writes to stdout, so stdout is pointed at a temp file for the
|
||||||
|
* duration of the call and then restored through a dup of the original
|
||||||
|
* descriptor. Nothing between the freopen and the dup2 may return early: an
|
||||||
|
* assertion there would leave stdout attached to the temp file for the rest of
|
||||||
|
* the run, and the test report itself would vanish.
|
||||||
|
*/
|
||||||
|
static int test_printf_writes_to_stdout(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
char readback[64];
|
||||||
|
akerr_ErrorContext *err = NULL;
|
||||||
|
int count = -1;
|
||||||
|
int saved = -1;
|
||||||
|
int restored = -1;
|
||||||
|
|
||||||
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||||
|
|
||||||
|
fflush(stdout);
|
||||||
|
saved = dup(fileno(stdout));
|
||||||
|
AKSL_CHECK(saved >= 0);
|
||||||
|
if ( freopen(path, "w", stdout) == NULL ) {
|
||||||
|
close(saved);
|
||||||
|
AKSL_CHECK(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
err = aksl_printf(&count, "%s#%d", "out", 5);
|
||||||
|
|
||||||
|
fflush(stdout);
|
||||||
|
restored = dup2(saved, fileno(stdout));
|
||||||
|
close(saved);
|
||||||
|
clearerr(stdout);
|
||||||
|
|
||||||
|
AKSL_CHECK(restored >= 0);
|
||||||
|
AKSL_CHECK(aksl_take(err) == 0);
|
||||||
|
AKSL_CHECK(count == 5);
|
||||||
|
AKSL_CHECK(read_file(path, readback, sizeof(readback)) == 5);
|
||||||
|
AKSL_CHECK(strcmp(readback, "out#5") == 0);
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_printf_rejects_null_arguments(void)
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_printf(NULL, "x"),
|
||||||
|
AKERR_NULLPOINTER, "count=");
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_printf(&count, NULL),
|
||||||
|
AKERR_NULLPOINTER, "format=");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Regression cover for the missing va_end (TODO.md 2.1.4). Nothing here can
|
||||||
|
* assert on register-save state directly; the point is to run the variadic
|
||||||
|
* wrappers enough times, with enough arguments, that the sanitizer build has
|
||||||
|
* something to trip over.
|
||||||
|
*/
|
||||||
|
static int test_variadic_wrappers_survive_repeated_calls(void)
|
||||||
|
{
|
||||||
|
char buf[128];
|
||||||
|
int count = 0;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for ( i = 0; i < 512; i++ ) {
|
||||||
|
AKSL_CHECK_OK(aksl_sprintf(&count, buf, "%d %s %ld %c %f",
|
||||||
|
i, "iteration", (long)i, 'x', (double)i));
|
||||||
|
AKSL_CHECK(count > 0);
|
||||||
|
AKSL_CHECK((size_t)count == strlen(buf));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
akerr_init();
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_sprintf_writes_text_and_count);
|
||||||
|
AKSL_RUN(failures, test_sprintf_empty_format_writes_nothing);
|
||||||
|
AKSL_RUN(failures, test_sprintf_rejects_null_arguments);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_fprintf_writes_to_stream);
|
||||||
|
AKSL_RUN(failures, test_fprintf_to_read_only_stream_reports_errno);
|
||||||
|
AKSL_RUN(failures, test_fprintf_rejects_null_arguments);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_printf_writes_to_stdout);
|
||||||
|
AKSL_RUN(failures, test_printf_rejects_null_arguments);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_variadic_wrappers_survive_repeated_calls);
|
||||||
|
|
||||||
|
AKSL_REPORT(failures);
|
||||||
|
}
|
||||||
@@ -190,8 +190,9 @@ static int test_iterate_propagates_callback_error(void)
|
|||||||
visitlog_init(&log);
|
visitlog_init(&log);
|
||||||
log.fail_at = 0;
|
log.fail_at = 0;
|
||||||
|
|
||||||
AKSL_CHECK_STATUS(aksl_list_iterate(&node, &record_visit, &log), AKERR_VALUE);
|
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||||
AKSL_CHECK_MSG_CONTAINS("iterator failed at visit 0");
|
aksl_list_iterate(&node, &record_visit, &log),
|
||||||
|
AKERR_VALUE, "iterator failed at visit 0");
|
||||||
AKSL_CHECK(log.count == 1);
|
AKSL_CHECK(log.count == 1);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
150
tests/test_memory.c
Normal file
150
tests/test_memory.c
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
/*
|
||||||
|
* Memory-wrapper behaviour that is deterministic today.
|
||||||
|
*
|
||||||
|
* TODO.md section 1.1 also calls out malloc(0), malloc(SIZE_MAX), and
|
||||||
|
* overlapping memcpy. Those are intentionally not pinned here yet: malloc(0)
|
||||||
|
* is platform-dependent under the current wrapper, huge allocations need
|
||||||
|
* sanitizer-safe handling, and overlapping memcpy is still an API-contract
|
||||||
|
* decision.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "aksl_capture.h"
|
||||||
|
|
||||||
|
static int test_malloc_writes_nonnull_pointer(void)
|
||||||
|
{
|
||||||
|
void *ptr = NULL;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_malloc(32, &ptr));
|
||||||
|
AKSL_CHECK(ptr != NULL);
|
||||||
|
AKSL_CHECK_OK(aksl_free(ptr));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_malloc_rejects_null_destination(void)
|
||||||
|
{
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_malloc(32, NULL),
|
||||||
|
AKERR_NULLPOINTER, "NULL");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_free_rejects_null_pointer(void)
|
||||||
|
{
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_free(NULL),
|
||||||
|
AKERR_NULLPOINTER, "NULL");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_malloc_free_round_trip(void)
|
||||||
|
{
|
||||||
|
unsigned char *buf = NULL;
|
||||||
|
size_t i = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_malloc(64, (void **)&buf));
|
||||||
|
AKSL_CHECK(buf != NULL);
|
||||||
|
for ( i = 0; i < 64; i++ ) {
|
||||||
|
buf[i] = (unsigned char)i;
|
||||||
|
}
|
||||||
|
for ( i = 0; i < 64; i++ ) {
|
||||||
|
AKSL_CHECK(buf[i] == (unsigned char)i);
|
||||||
|
}
|
||||||
|
AKSL_CHECK_OK(aksl_free(buf));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_memset_fills_buffer(void)
|
||||||
|
{
|
||||||
|
unsigned char buf[8];
|
||||||
|
size_t i = 0;
|
||||||
|
|
||||||
|
memset((void *)buf, 0x00, sizeof(buf));
|
||||||
|
AKSL_CHECK_OK(aksl_memset(buf, 0x5a, sizeof(buf)));
|
||||||
|
for ( i = 0; i < sizeof(buf); i++ ) {
|
||||||
|
AKSL_CHECK(buf[i] == 0x5a);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_memset_zero_length_is_noop(void)
|
||||||
|
{
|
||||||
|
unsigned char buf[4] = { 1, 2, 3, 4 };
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_memset(buf, 0xff, 0));
|
||||||
|
AKSL_CHECK(buf[0] == 1);
|
||||||
|
AKSL_CHECK(buf[1] == 2);
|
||||||
|
AKSL_CHECK(buf[2] == 3);
|
||||||
|
AKSL_CHECK(buf[3] == 4);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_memset_rejects_null_buffer(void)
|
||||||
|
{
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_memset(NULL, 0x00, 4),
|
||||||
|
AKERR_NULLPOINTER, "s=");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_memcpy_copies_bytes(void)
|
||||||
|
{
|
||||||
|
unsigned char src[6] = { 0, 1, 2, 3, 4, 5 };
|
||||||
|
unsigned char dst[6] = { 9, 9, 9, 9, 9, 9 };
|
||||||
|
size_t i = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_memcpy(dst, src, sizeof(src)));
|
||||||
|
for ( i = 0; i < sizeof(src); i++ ) {
|
||||||
|
AKSL_CHECK(dst[i] == src[i]);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_memcpy_zero_length_is_noop(void)
|
||||||
|
{
|
||||||
|
unsigned char src[3] = { 1, 2, 3 };
|
||||||
|
unsigned char dst[3] = { 4, 5, 6 };
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_memcpy(dst, src, 0));
|
||||||
|
AKSL_CHECK(dst[0] == 4);
|
||||||
|
AKSL_CHECK(dst[1] == 5);
|
||||||
|
AKSL_CHECK(dst[2] == 6);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_memcpy_rejects_null_destination(void)
|
||||||
|
{
|
||||||
|
unsigned char src[1] = { 1 };
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_memcpy(NULL, src, sizeof(src)),
|
||||||
|
AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_memcpy_rejects_null_source(void)
|
||||||
|
{
|
||||||
|
unsigned char dst[1] = { 0 };
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_memcpy(dst, NULL, sizeof(dst)),
|
||||||
|
AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
akerr_init();
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_malloc_writes_nonnull_pointer);
|
||||||
|
AKSL_RUN(failures, test_malloc_rejects_null_destination);
|
||||||
|
AKSL_RUN(failures, test_free_rejects_null_pointer);
|
||||||
|
AKSL_RUN(failures, test_malloc_free_round_trip);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_memset_fills_buffer);
|
||||||
|
AKSL_RUN(failures, test_memset_zero_length_is_noop);
|
||||||
|
AKSL_RUN(failures, test_memset_rejects_null_buffer);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_memcpy_copies_bytes);
|
||||||
|
AKSL_RUN(failures, test_memcpy_zero_length_is_noop);
|
||||||
|
AKSL_RUN(failures, test_memcpy_rejects_null_destination);
|
||||||
|
AKSL_RUN(failures, test_memcpy_rejects_null_source);
|
||||||
|
|
||||||
|
AKSL_REPORT(failures);
|
||||||
|
}
|
||||||
116
tests/test_path.c
Normal file
116
tests/test_path.c
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
* aksl_realpath -- TODO.md section 1.5.
|
||||||
|
*
|
||||||
|
* The happy paths compare against realpath(3) itself rather than against a
|
||||||
|
* hard-coded string, because $TMPDIR may itself be a symlink (/tmp -> /private/tmp
|
||||||
|
* and friends) and the resolved answer is what the platform says it is.
|
||||||
|
*
|
||||||
|
* Every failure case here passes a *zeroed* resolved_path buffer. That is
|
||||||
|
* deliberate: on failure the wrapper formats resolved_path with %s while
|
||||||
|
* realpath(3) leaves the buffer unspecified (TODO.md 2.1.6), so a test that
|
||||||
|
* passed an uninitialised buffer would be reading uninitialised memory in the
|
||||||
|
* library's own error path. The uninitialised-buffer crash is the defect's own
|
||||||
|
* test to write, not something these should trip over incidentally.
|
||||||
|
*
|
||||||
|
* Also not covered: resolved_path == NULL, which is unchecked today and leaks
|
||||||
|
* the buffer realpath(3) allocates (2.1.6).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "aksl_capture.h"
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <limits.h>
|
||||||
|
|
||||||
|
static int test_resolves_an_existing_file(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
char resolved[PATH_MAX];
|
||||||
|
char expected[PATH_MAX];
|
||||||
|
|
||||||
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||||
|
memset(resolved, 0x00, sizeof(resolved));
|
||||||
|
AKSL_CHECK(realpath(path, expected) != NULL);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_realpath(path, resolved));
|
||||||
|
AKSL_CHECK(strcmp(resolved, expected) == 0);
|
||||||
|
AKSL_CHECK(resolved[0] == '/');
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A symlink resolves to its target, not to itself. */
|
||||||
|
static int test_resolves_a_symlink_to_its_target(void)
|
||||||
|
{
|
||||||
|
char target[AKSL_TMP_MAX];
|
||||||
|
char link[AKSL_TMP_MAX];
|
||||||
|
char resolved[PATH_MAX];
|
||||||
|
char expected[PATH_MAX];
|
||||||
|
|
||||||
|
AKSL_CHECK(aksl_temp_file(target, sizeof(target)) == 0);
|
||||||
|
AKSL_CHECK(aksl_temp_file(link, sizeof(link)) == 0);
|
||||||
|
/* mkstemp created the link path as a regular file; symlink needs it gone. */
|
||||||
|
AKSL_CHECK(unlink(link) == 0);
|
||||||
|
AKSL_CHECK(symlink(target, link) == 0);
|
||||||
|
|
||||||
|
memset(resolved, 0x00, sizeof(resolved));
|
||||||
|
AKSL_CHECK(realpath(target, expected) != NULL);
|
||||||
|
AKSL_CHECK_OK(aksl_realpath(link, resolved));
|
||||||
|
AKSL_CHECK(strcmp(resolved, expected) == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK(unlink(link) == 0);
|
||||||
|
AKSL_CHECK(unlink(target) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_missing_path_reports_enoent(void)
|
||||||
|
{
|
||||||
|
char resolved[PATH_MAX];
|
||||||
|
|
||||||
|
memset(resolved, 0x00, sizeof(resolved));
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||||
|
aksl_realpath("/nonexistent/aksl/path", resolved),
|
||||||
|
ENOENT, "/nonexistent/aksl/path");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A regular file used as a directory component is ENOTDIR, not ENOENT. */
|
||||||
|
static int test_non_directory_component_reports_enotdir(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
char child[AKSL_TMP_MAX + 8];
|
||||||
|
char resolved[PATH_MAX];
|
||||||
|
|
||||||
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||||
|
AKSL_CHECK((size_t)snprintf(child, sizeof(child), "%s/child", path)
|
||||||
|
< sizeof(child));
|
||||||
|
|
||||||
|
memset(resolved, 0x00, sizeof(resolved));
|
||||||
|
AKSL_CHECK_STATUS(aksl_realpath(child, resolved), ENOTDIR);
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_rejects_null_path(void)
|
||||||
|
{
|
||||||
|
char resolved[PATH_MAX];
|
||||||
|
|
||||||
|
memset(resolved, 0x00, sizeof(resolved));
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_realpath(NULL, resolved),
|
||||||
|
AKERR_NULLPOINTER, "path=");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
akerr_init();
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_resolves_an_existing_file);
|
||||||
|
AKSL_RUN(failures, test_resolves_a_symlink_to_its_target);
|
||||||
|
AKSL_RUN(failures, test_missing_path_reports_enoent);
|
||||||
|
AKSL_RUN(failures, test_non_directory_component_reports_enotdir);
|
||||||
|
AKSL_RUN(failures, test_rejects_null_path);
|
||||||
|
|
||||||
|
AKSL_REPORT(failures);
|
||||||
|
}
|
||||||
225
tests/test_status_registry.c
Normal file
225
tests/test_status_registry.c
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
/*
|
||||||
|
* libakstdlib's side of the libakerror 1.0.0 status registry contract.
|
||||||
|
*
|
||||||
|
* See deps/libakerror/UPGRADING.md. That release made the status-name table
|
||||||
|
* private, moved consumer status codes to a reserved band starting at
|
||||||
|
* AKERR_FIRST_CONSUMER_STATUS, and made ownership of a range enforced rather
|
||||||
|
* than advisory. None of it broke this library's build, because libakstdlib
|
||||||
|
* defines no status codes of its own -- it raises libakerror's AKERR_* codes and
|
||||||
|
* propagates the host's errno values, both of which live in libakerror's own
|
||||||
|
* 0-255 band.
|
||||||
|
*
|
||||||
|
* That "defines none of its own" is now a contract worth pinning rather than an
|
||||||
|
* accident, because it is what lets libakstdlib sit in a process alongside any
|
||||||
|
* other libakerror consumer without a range negotiation. These tests assert it,
|
||||||
|
* and assert that the registry the codes are looked up in is actually populated
|
||||||
|
* -- a status whose name fails to register degrades to "Unknown Error" in every
|
||||||
|
* later stack trace, which is a silent loss of debuggability rather than a
|
||||||
|
* failure anyone would notice.
|
||||||
|
*
|
||||||
|
* If libakstdlib ever does define its own status codes, this file is where the
|
||||||
|
* reservation gets asserted: replace test_reserves_no_consumer_range with one
|
||||||
|
* that reserves the library's declared range and confirms its names registered.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "aksl_capture.h"
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Every status libakstdlib raises, and where from. All of them belong to
|
||||||
|
* libakerror or to the host's errno space; none is defined here.
|
||||||
|
*/
|
||||||
|
static const struct {
|
||||||
|
int status;
|
||||||
|
const char *symbol;
|
||||||
|
const char *raised_by;
|
||||||
|
} aksl_raised_statuses[] = {
|
||||||
|
{ AKERR_NULLPOINTER, "AKERR_NULLPOINTER", "every NULL parameter guard" },
|
||||||
|
{ AKERR_IO, "AKERR_IO", "aksl_fread / aksl_fwrite short count" },
|
||||||
|
{ AKERR_EOF, "AKERR_EOF", "aksl_fread at end of stream" },
|
||||||
|
{ AKERR_ITERATOR_BREAK, "AKERR_ITERATOR_BREAK", "aksl_list_iterate / aksl_tree_iterate" },
|
||||||
|
{ AKERR_CIRCULAR_REFERENCE, "AKERR_CIRCULAR_REFERENCE", "aksl_list_append cycle guard" },
|
||||||
|
{ AKERR_NOT_IMPLEMENTED, "AKERR_NOT_IMPLEMENTED", "unsupported tree search modes" },
|
||||||
|
{ ENOENT, "ENOENT", "errno propagated by aksl_fopen" },
|
||||||
|
};
|
||||||
|
|
||||||
|
#define AKSL_RAISED_STATUS_COUNT \
|
||||||
|
((int)(sizeof(aksl_raised_statuses) / sizeof(aksl_raised_statuses[0])))
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Ranges this file claims out of the consumer band. A reservation is
|
||||||
|
* process-global and there is no way to give one back, so each test function
|
||||||
|
* gets its own slice and no two overlap.
|
||||||
|
*
|
||||||
|
* +0 .. +15 test_reserves_no_consumer_range
|
||||||
|
* +16 .. +31 test_range_ownership_is_enforced
|
||||||
|
* +32 .. +47 test_naming_a_foreign_status_is_refused
|
||||||
|
*/
|
||||||
|
#define AKSL_TEST_RANGE_FREE (AKERR_FIRST_CONSUMER_STATUS + 0)
|
||||||
|
#define AKSL_TEST_RANGE_OWNED (AKERR_FIRST_CONSUMER_STATUS + 16)
|
||||||
|
#define AKSL_TEST_RANGE_FOREIGN (AKERR_FIRST_CONSUMER_STATUS + 32)
|
||||||
|
#define AKSL_TEST_RANGE_SIZE 16
|
||||||
|
|
||||||
|
#define AKSL_OWNER_A "aksl-test-a"
|
||||||
|
#define AKSL_OWNER_B "aksl-test-b"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The band boundary this library's "raises nothing of its own" claim rests on.
|
||||||
|
* Asserted rather than assumed: if libakerror ever moves the boundary, the
|
||||||
|
* status-band assertion below stops meaning what it says.
|
||||||
|
*/
|
||||||
|
static int test_consumer_band_starts_at_256(void)
|
||||||
|
{
|
||||||
|
AKSL_CHECK(AKERR_FIRST_CONSUMER_STATUS == 256);
|
||||||
|
AKSL_CHECK(AKERR_RESERVED_STATUS_COUNT == 256);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Every status libakstdlib raises falls inside libakerror's reserved band, so it
|
||||||
|
* cannot collide with a consumer that allocates from 256 up.
|
||||||
|
*/
|
||||||
|
static int test_raised_statuses_are_in_the_reserved_band(void)
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for ( i = 0; i < AKSL_RAISED_STATUS_COUNT; i++ ) {
|
||||||
|
if ( aksl_raised_statuses[i].status >= AKERR_FIRST_CONSUMER_STATUS ) {
|
||||||
|
fprintf(stderr,
|
||||||
|
" CHECK FAILED: %s (%d) is outside libakerror's reserved band\n"
|
||||||
|
" raised by %s\n"
|
||||||
|
" at %s:%d\n",
|
||||||
|
aksl_raised_statuses[i].symbol,
|
||||||
|
aksl_raised_statuses[i].status,
|
||||||
|
aksl_raised_statuses[i].raised_by,
|
||||||
|
__FILE__, __LINE__);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The registry actually holds a name for each of them. This is the check that
|
||||||
|
* catches a build whose name table is too small: registration failures are
|
||||||
|
* reported at akerr_init() time, but the visible symptom afterwards is only that
|
||||||
|
* stack traces read "Unknown Error", which no other test would notice.
|
||||||
|
*/
|
||||||
|
static int test_raised_statuses_have_registered_names(void)
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
char *name = NULL;
|
||||||
|
|
||||||
|
for ( i = 0; i < AKSL_RAISED_STATUS_COUNT; i++ ) {
|
||||||
|
name = akerr_name_for_status(aksl_raised_statuses[i].status, NULL);
|
||||||
|
if ( name == NULL || strcmp(name, "Unknown Error") == 0 ) {
|
||||||
|
fprintf(stderr,
|
||||||
|
" CHECK FAILED: %s (%d) has no registered name\n"
|
||||||
|
" raised by %s\n"
|
||||||
|
" reads back as \"%s\"\n"
|
||||||
|
" at %s:%d\n",
|
||||||
|
aksl_raised_statuses[i].symbol,
|
||||||
|
aksl_raised_statuses[i].status,
|
||||||
|
aksl_raised_statuses[i].raised_by,
|
||||||
|
name == NULL ? "(NULL)" : name,
|
||||||
|
__FILE__, __LINE__);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* libakstdlib reserves nothing in the consumer band, so an application is free
|
||||||
|
* to allocate from AKERR_FIRST_CONSUMER_STATUS without coordinating with it.
|
||||||
|
* The library is driven first so that anything it might do lazily has happened
|
||||||
|
* before the range is claimed.
|
||||||
|
*/
|
||||||
|
static int test_reserves_no_consumer_range(void)
|
||||||
|
{
|
||||||
|
void *p = NULL;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_malloc(16, &p));
|
||||||
|
AKSL_CHECK_OK(aksl_free(p));
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(akerr_reserve_status_range(AKSL_TEST_RANGE_FREE,
|
||||||
|
AKSL_TEST_RANGE_SIZE,
|
||||||
|
AKSL_OWNER_A));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Ownership is enforced, which is what makes the assertion above meaningful --
|
||||||
|
* a reservation that succeeded against an already-claimed range would prove
|
||||||
|
* nothing. libakerror's own 0-255 band is refused for the same reason.
|
||||||
|
*/
|
||||||
|
static int test_range_ownership_is_enforced(void)
|
||||||
|
{
|
||||||
|
AKSL_CHECK_OK(akerr_reserve_status_range(AKSL_TEST_RANGE_OWNED,
|
||||||
|
AKSL_TEST_RANGE_SIZE,
|
||||||
|
AKSL_OWNER_A));
|
||||||
|
|
||||||
|
/* An identical reservation by the same owner is a documented no-op. */
|
||||||
|
AKSL_CHECK_OK(akerr_reserve_status_range(AKSL_TEST_RANGE_OWNED,
|
||||||
|
AKSL_TEST_RANGE_SIZE,
|
||||||
|
AKSL_OWNER_A));
|
||||||
|
|
||||||
|
/* The same range under a different owner is not. */
|
||||||
|
AKSL_CHECK_STATUS(akerr_reserve_status_range(AKSL_TEST_RANGE_OWNED,
|
||||||
|
AKSL_TEST_RANGE_SIZE,
|
||||||
|
AKSL_OWNER_B),
|
||||||
|
AKERR_STATUS_RANGE_OVERLAP);
|
||||||
|
|
||||||
|
/* Nor is any part of libakerror's own band. */
|
||||||
|
AKSL_CHECK_STATUS(akerr_reserve_status_range(AKERR_NULLPOINTER, 1, AKSL_OWNER_B),
|
||||||
|
AKERR_STATUS_RANGE_OVERLAP);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Naming is enforced against the reservation too: a status nobody reserved and a
|
||||||
|
* status somebody else reserved are both refused, with different codes.
|
||||||
|
*/
|
||||||
|
static int test_naming_a_foreign_status_is_refused(void)
|
||||||
|
{
|
||||||
|
AKSL_CHECK_STATUS(akerr_register_status_name(AKSL_OWNER_A,
|
||||||
|
AKSL_TEST_RANGE_FOREIGN,
|
||||||
|
"Unreserved"),
|
||||||
|
AKERR_STATUS_NAME_UNRESERVED);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(akerr_reserve_status_range(AKSL_TEST_RANGE_FOREIGN,
|
||||||
|
AKSL_TEST_RANGE_SIZE,
|
||||||
|
AKSL_OWNER_A));
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(akerr_register_status_name(AKSL_OWNER_A,
|
||||||
|
AKSL_TEST_RANGE_FOREIGN,
|
||||||
|
"Mine"));
|
||||||
|
AKSL_CHECK(strcmp(akerr_name_for_status(AKSL_TEST_RANGE_FOREIGN, NULL),
|
||||||
|
"Mine") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(akerr_register_status_name(AKSL_OWNER_B,
|
||||||
|
AKSL_TEST_RANGE_FOREIGN,
|
||||||
|
"Theirs"),
|
||||||
|
AKERR_STATUS_NAME_FOREIGN);
|
||||||
|
/* The refused registration left the owner's name in place. */
|
||||||
|
AKSL_CHECK(strcmp(akerr_name_for_status(AKSL_TEST_RANGE_FOREIGN, NULL),
|
||||||
|
"Mine") == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
akerr_init();
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_consumer_band_starts_at_256);
|
||||||
|
AKSL_RUN(failures, test_raised_statuses_are_in_the_reserved_band);
|
||||||
|
AKSL_RUN(failures, test_raised_statuses_have_registered_names);
|
||||||
|
AKSL_RUN(failures, test_reserves_no_consumer_range);
|
||||||
|
AKSL_RUN(failures, test_range_ownership_is_enforced);
|
||||||
|
AKSL_RUN(failures, test_naming_a_foreign_status_is_refused);
|
||||||
|
|
||||||
|
AKSL_REPORT(failures);
|
||||||
|
}
|
||||||
188
tests/test_stream.c
Normal file
188
tests/test_stream.c
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
/*
|
||||||
|
* Stream wrappers: aksl_fopen / aksl_fread / aksl_fwrite / aksl_fclose.
|
||||||
|
*
|
||||||
|
* TODO.md section 1.2. Covered here: the happy paths, the round trip, the NULL
|
||||||
|
* guards that exist, and the two error statuses the wrappers can actually
|
||||||
|
* produce today -- AKERR_EOF from a short read and AKERR_IO from a stream whose
|
||||||
|
* error indicator is set.
|
||||||
|
*
|
||||||
|
* Not covered, because the behaviour is a documented gap rather than a
|
||||||
|
* contract: aksl_fopen(NULL, ...) and aksl_fopen(path, NULL, ...) are unchecked
|
||||||
|
* (2.2.2), aksl_fread/aksl_fwrite never check ptr and report a short transfer
|
||||||
|
* that is neither EOF nor error as complete success (2.2.3).
|
||||||
|
*
|
||||||
|
* Temp files come from aksl_temp_file() and are unlinked by the test that made
|
||||||
|
* them, so a failing test leaves nothing behind but the file it was mid-way
|
||||||
|
* through.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "aksl_capture.h"
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
static int test_fopen_writes_stream_pointer(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
FILE *fp = NULL;
|
||||||
|
|
||||||
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
|
||||||
|
AKSL_CHECK(fp != NULL);
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_fopen_reports_missing_path(void)
|
||||||
|
{
|
||||||
|
FILE *fp = NULL;
|
||||||
|
|
||||||
|
/* The pathname belongs in the message; the status is the errno fopen saw. */
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||||
|
aksl_fopen("/nonexistent/aksl/stream", "r", &fp),
|
||||||
|
ENOENT, "/nonexistent/aksl/stream");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_fopen_rejects_null_stream_out(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
|
||||||
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fopen(path, "r", NULL),
|
||||||
|
AKERR_NULLPOINTER, "NULL");
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* fopen -> fwrite -> fclose -> fopen -> fread -> compare, all through the wrappers. */
|
||||||
|
static int test_write_read_round_trip(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
char payload[] = "libakstdlib round trip";
|
||||||
|
char readback[sizeof(payload)];
|
||||||
|
FILE *fp = NULL;
|
||||||
|
|
||||||
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
|
||||||
|
AKSL_CHECK_OK(aksl_fwrite(payload, 1, sizeof(payload), fp));
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
|
||||||
|
fp = NULL;
|
||||||
|
memset(readback, 0x00, sizeof(readback));
|
||||||
|
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
|
||||||
|
AKSL_CHECK_OK(aksl_fread(readback, 1, sizeof(readback), fp));
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
|
||||||
|
AKSL_CHECK(memcmp(payload, readback, sizeof(payload)) == 0);
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Asking for more members than the file holds sets feof, which is AKERR_EOF. */
|
||||||
|
static int test_fread_short_read_is_eof(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
char buf[32];
|
||||||
|
FILE *fp = NULL;
|
||||||
|
|
||||||
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
|
||||||
|
AKSL_CHECK_OK(aksl_fwrite("abcd", 1, 4, fp));
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
|
||||||
|
fp = NULL;
|
||||||
|
memset(buf, 0x00, sizeof(buf));
|
||||||
|
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), fp),
|
||||||
|
AKERR_EOF, "EOF");
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
|
||||||
|
/* The bytes that did arrive are still in the buffer. */
|
||||||
|
AKSL_CHECK(memcmp(buf, "abcd", 4) == 0);
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A stream opened "w" has no read permission, so fread sets the error indicator
|
||||||
|
* rather than the EOF one: AKERR_IO, not AKERR_EOF.
|
||||||
|
*/
|
||||||
|
static int test_fread_from_write_only_stream_is_io_error(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
char buf[4];
|
||||||
|
FILE *fp = NULL;
|
||||||
|
|
||||||
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
|
||||||
|
memset(buf, 0x00, sizeof(buf));
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), fp),
|
||||||
|
AKERR_IO, "Error reading file");
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_fread_rejects_null_stream(void)
|
||||||
|
{
|
||||||
|
char buf[4];
|
||||||
|
|
||||||
|
memset(buf, 0x00, sizeof(buf));
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), NULL),
|
||||||
|
AKERR_NULLPOINTER, "NULL");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mirror image of the fread case: a "r" stream cannot be written to. */
|
||||||
|
static int test_fwrite_to_read_only_stream_is_io_error(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
FILE *fp = NULL;
|
||||||
|
|
||||||
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
|
||||||
|
AKSL_CHECK_STATUS(aksl_fwrite("xy", 1, 2, fp), AKERR_IO);
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_fwrite_rejects_null_stream(void)
|
||||||
|
{
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fwrite("xy", 1, 2, NULL),
|
||||||
|
AKERR_NULLPOINTER, "NULL");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_fclose_rejects_null_stream(void)
|
||||||
|
{
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fclose(NULL),
|
||||||
|
AKERR_NULLPOINTER, "NULL");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
akerr_init();
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_fopen_writes_stream_pointer);
|
||||||
|
AKSL_RUN(failures, test_fopen_reports_missing_path);
|
||||||
|
AKSL_RUN(failures, test_fopen_rejects_null_stream_out);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_write_read_round_trip);
|
||||||
|
AKSL_RUN(failures, test_fread_short_read_is_eof);
|
||||||
|
AKSL_RUN(failures, test_fread_from_write_only_stream_is_io_error);
|
||||||
|
AKSL_RUN(failures, test_fread_rejects_null_stream);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_fwrite_to_read_only_stream_is_io_error);
|
||||||
|
AKSL_RUN(failures, test_fwrite_rejects_null_stream);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_fclose_rejects_null_stream);
|
||||||
|
|
||||||
|
AKSL_REPORT(failures);
|
||||||
|
}
|
||||||
103
tests/test_strhash.c
Normal file
103
tests/test_strhash.c
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* aksl_strhash_djb2 -- TODO.md section 1.6.
|
||||||
|
*
|
||||||
|
* The expected values are the canonical djb2 ones: h = 5381, then
|
||||||
|
* h = h * 33 + byte for each of len bytes, truncated to 32 bits. They were
|
||||||
|
* computed independently of this implementation.
|
||||||
|
*
|
||||||
|
* Every vector here is 7-bit ASCII, where signed and unsigned char agree. The
|
||||||
|
* high-bit case ("\xff\xfe") is the sign-extension defect in TODO.md 2.2.6 and
|
||||||
|
* is left for a test that can be registered as a known failure.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "aksl_capture.h"
|
||||||
|
|
||||||
|
static int test_empty_string_is_the_djb2_seed(void)
|
||||||
|
{
|
||||||
|
uint32_t h = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strhash_djb2("", 0, &h));
|
||||||
|
AKSL_CHECK(h == 5381);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* len drives the loop, so a zero length ignores the contents entirely. */
|
||||||
|
static int test_zero_length_ignores_the_buffer(void)
|
||||||
|
{
|
||||||
|
char buf[] = "ignored";
|
||||||
|
uint32_t h = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strhash_djb2(buf, 0, &h));
|
||||||
|
AKSL_CHECK(h == 5381);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_known_answer_vectors(void)
|
||||||
|
{
|
||||||
|
char hello[] = "hello";
|
||||||
|
char libname[] = "libakstdlib";
|
||||||
|
uint32_t h = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strhash_djb2(hello, 5, &h));
|
||||||
|
AKSL_CHECK(h == 261238937u);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strhash_djb2(libname, 11, &h));
|
||||||
|
AKSL_CHECK(h == 884285482u);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The function is length-driven, not NUL-driven: an embedded NUL is hashed. */
|
||||||
|
static int test_embedded_nul_is_hashed(void)
|
||||||
|
{
|
||||||
|
char buf[3] = { 'a', '\0', 'b' };
|
||||||
|
uint32_t whole = 0;
|
||||||
|
uint32_t prefix = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strhash_djb2(buf, sizeof(buf), &whole));
|
||||||
|
AKSL_CHECK(whole == 193482728u);
|
||||||
|
|
||||||
|
/* Stopping at the NUL would give the one-byte hash instead. */
|
||||||
|
AKSL_CHECK_OK(aksl_strhash_djb2(buf, 1, &prefix));
|
||||||
|
AKSL_CHECK(prefix != whole);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_hash_is_stable_across_calls(void)
|
||||||
|
{
|
||||||
|
char buf[] = "repeatable";
|
||||||
|
uint32_t first = 0;
|
||||||
|
uint32_t second = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strhash_djb2(buf, sizeof(buf) - 1, &first));
|
||||||
|
AKSL_CHECK_OK(aksl_strhash_djb2(buf, sizeof(buf) - 1, &second));
|
||||||
|
AKSL_CHECK(first == second);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_rejects_null_arguments(void)
|
||||||
|
{
|
||||||
|
char buf[] = "x";
|
||||||
|
uint32_t h = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strhash_djb2(NULL, 1, &h),
|
||||||
|
AKERR_NULLPOINTER, "str");
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strhash_djb2(buf, 1, NULL),
|
||||||
|
AKERR_NULLPOINTER, "hashval");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
akerr_init();
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_empty_string_is_the_djb2_seed);
|
||||||
|
AKSL_RUN(failures, test_zero_length_ignores_the_buffer);
|
||||||
|
AKSL_RUN(failures, test_known_answer_vectors);
|
||||||
|
AKSL_RUN(failures, test_embedded_nul_is_hashed);
|
||||||
|
AKSL_RUN(failures, test_hash_is_stable_across_calls);
|
||||||
|
AKSL_RUN(failures, test_rejects_null_arguments);
|
||||||
|
|
||||||
|
AKSL_REPORT(failures);
|
||||||
|
}
|
||||||
@@ -99,6 +99,70 @@ static int test_dfs_postorder(void)
|
|||||||
return search_finds_hidden_value(AKSL_TREE_SEARCH_DFS_POSTORDER);
|
return search_finds_hidden_value(AKSL_TREE_SEARCH_DFS_POSTORDER);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Both breadth-first modes are declared in the header but not implemented, and
|
||||||
|
* say so through AKERR_NOT_IMPLEMENTED rather than by silently visiting nothing.
|
||||||
|
* TODO.md 2.2.10 tracks implementing them; until then this is the contract.
|
||||||
|
*/
|
||||||
|
static int bfs_reports_not_implemented(uint8_t searchmode)
|
||||||
|
{
|
||||||
|
aksl_TreeNode tree[MAX_LEAVES];
|
||||||
|
TreeSearchParams parms;
|
||||||
|
|
||||||
|
build_tree(tree, &parms);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||||
|
aksl_tree_iterate(&tree[0], &find_value, NULL, NULL, searchmode,
|
||||||
|
&parms, NULL),
|
||||||
|
AKERR_NOT_IMPLEMENTED, "Searchmode");
|
||||||
|
AKSL_CHECK(parms.steps == 0);
|
||||||
|
AKSL_CHECK(parms.node == NULL);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_bfs_is_not_implemented(void)
|
||||||
|
{
|
||||||
|
return bfs_reports_not_implemented(AKSL_TREE_SEARCH_BFS);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_bfs_right_is_not_implemented(void)
|
||||||
|
{
|
||||||
|
return bfs_reports_not_implemented(AKSL_TREE_SEARCH_BFS_RIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_iterate_null_arguments(void)
|
||||||
|
{
|
||||||
|
aksl_TreeNode tree[MAX_LEAVES];
|
||||||
|
TreeSearchParams parms;
|
||||||
|
|
||||||
|
build_tree(tree, &parms);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||||
|
aksl_tree_iterate(NULL, &find_value, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_PREORDER, &parms, NULL),
|
||||||
|
AKERR_NULLPOINTER, "root");
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||||
|
aksl_tree_iterate(&tree[0], NULL, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_PREORDER, &parms, NULL),
|
||||||
|
AKERR_NULLPOINTER, "iter");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A callback error that is not AKERR_ITERATOR_BREAK reaches the caller. */
|
||||||
|
static int test_iterate_propagates_callback_error(void)
|
||||||
|
{
|
||||||
|
aksl_TreeNode tree[MAX_LEAVES];
|
||||||
|
TreeSearchParams parms;
|
||||||
|
|
||||||
|
build_tree(tree, &parms);
|
||||||
|
/* find_value raises AKERR_NULLPOINTER when it is handed no data. */
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||||
|
aksl_tree_iterate(&tree[0], &find_value, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_PREORDER, NULL, NULL),
|
||||||
|
AKERR_NULLPOINTER, "data");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
int failures = 0;
|
int failures = 0;
|
||||||
@@ -109,5 +173,10 @@ int main(void)
|
|||||||
AKSL_RUN(failures, test_dfs_inorder);
|
AKSL_RUN(failures, test_dfs_inorder);
|
||||||
AKSL_RUN(failures, test_dfs_postorder);
|
AKSL_RUN(failures, test_dfs_postorder);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_bfs_is_not_implemented);
|
||||||
|
AKSL_RUN(failures, test_bfs_right_is_not_implemented);
|
||||||
|
AKSL_RUN(failures, test_iterate_null_arguments);
|
||||||
|
AKSL_RUN(failures, test_iterate_propagates_callback_error);
|
||||||
|
|
||||||
AKSL_REPORT(failures);
|
AKSL_REPORT(failures);
|
||||||
}
|
}
|
||||||
|
|||||||
177
tests/test_version.c
Normal file
177
tests/test_version.c
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
/*
|
||||||
|
* Version reporting -- TODO.md section 2.2.16.
|
||||||
|
*
|
||||||
|
* There are two versions in play and the whole point of this API is that they
|
||||||
|
* are allowed to differ:
|
||||||
|
*
|
||||||
|
* the AKSL_VERSION_* macros what the caller was COMPILED against, baked
|
||||||
|
* into the caller's object file from whatever
|
||||||
|
* akstdlib_version.h was on its include path
|
||||||
|
* aksl_version() and friends what is actually LOADED, baked into
|
||||||
|
* libakstdlib.so when the library was built
|
||||||
|
*
|
||||||
|
* In this test they necessarily agree, because the test binary and the library
|
||||||
|
* are built from one tree in one configure. So the assertions below split into
|
||||||
|
* two kinds: the ones that check the two sides agree (which would catch a build
|
||||||
|
* that compiled the library and its tests against different generated headers),
|
||||||
|
* and the ones that drive aksl_version_check() with deliberately wrong numbers
|
||||||
|
* to prove it actually refuses a mismatch rather than always returning success.
|
||||||
|
* The second kind is what makes the first kind worth anything.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "aksl_capture.h"
|
||||||
|
|
||||||
|
static int test_version_string_matches_its_components(void)
|
||||||
|
{
|
||||||
|
char expected[64];
|
||||||
|
|
||||||
|
snprintf(expected, sizeof(expected), "%d.%d.%d",
|
||||||
|
AKSL_VERSION_MAJOR, AKSL_VERSION_MINOR, AKSL_VERSION_PATCH);
|
||||||
|
AKSL_CHECK(strcmp(AKSL_VERSION_STRING, expected) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* AKSL_VERSION_NUMBER must be ordinary decimal arithmetic. Written out as a
|
||||||
|
* literal it would be tempting to spell 0.1.0 as 000100, which C reads as
|
||||||
|
* octal 64 -- so this compares the compile-time macro against the same figure
|
||||||
|
* computed at runtime from the loaded library's own components. A literal that
|
||||||
|
* went octal fails here; the current computed form cannot.
|
||||||
|
*/
|
||||||
|
static int test_version_number_is_decimal_and_ordered(void)
|
||||||
|
{
|
||||||
|
int major = -1;
|
||||||
|
int minor = -1;
|
||||||
|
int patch = -1;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_version(&major, &minor, &patch));
|
||||||
|
AKSL_CHECK(AKSL_VERSION_NUMBER == (major * 10000) + (minor * 100) + patch);
|
||||||
|
|
||||||
|
/* Usable in the preprocessor, which is the reason it exists at all. */
|
||||||
|
#if AKSL_VERSION_NUMBER < 0
|
||||||
|
AKSL_CHECK(0 && "AKSL_VERSION_NUMBER is negative");
|
||||||
|
#endif
|
||||||
|
AKSL_CHECK(AKSL_VERSION_NUMBER >= 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header and shared library came out of the same configure. */
|
||||||
|
static int test_loaded_version_matches_the_header(void)
|
||||||
|
{
|
||||||
|
int major = -1;
|
||||||
|
int minor = -1;
|
||||||
|
int patch = -1;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_version(&major, &minor, &patch));
|
||||||
|
AKSL_CHECK(major == AKSL_VERSION_MAJOR);
|
||||||
|
AKSL_CHECK(minor == AKSL_VERSION_MINOR);
|
||||||
|
AKSL_CHECK(patch == AKSL_VERSION_PATCH);
|
||||||
|
|
||||||
|
AKSL_CHECK(strcmp(aksl_version_string(), AKSL_VERSION_STRING) == 0);
|
||||||
|
AKSL_CHECK(strcmp(aksl_version_soname(), AKSL_VERSION_SONAME) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The soname is the ABI-break granularity, and it mirrors the SOVERSION
|
||||||
|
* expression in CMakeLists.txt: MAJOR.MINOR while major is 0, MAJOR alone after
|
||||||
|
* that. If one of the two ever changes without the other, this fails.
|
||||||
|
*/
|
||||||
|
static int test_soname_matches_the_documented_rule(void)
|
||||||
|
{
|
||||||
|
char expected[64];
|
||||||
|
|
||||||
|
#if AKSL_VERSION_MAJOR == 0
|
||||||
|
snprintf(expected, sizeof(expected), "%d.%d",
|
||||||
|
AKSL_VERSION_MAJOR, AKSL_VERSION_MINOR);
|
||||||
|
#else
|
||||||
|
snprintf(expected, sizeof(expected), "%d", AKSL_VERSION_MAJOR);
|
||||||
|
#endif
|
||||||
|
AKSL_CHECK(strcmp(aksl_version_soname(), expected) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_version_check_accepts_a_matching_build(void)
|
||||||
|
{
|
||||||
|
AKSL_CHECK_OK(AKSL_VERSION_CHECK());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The refusals. Without these the success above proves nothing -- a
|
||||||
|
* aksl_version_check() that returned NULL unconditionally would pass it.
|
||||||
|
*/
|
||||||
|
static int test_version_check_refuses_a_mismatch(void)
|
||||||
|
{
|
||||||
|
AKSL_CHECK_STATUS(aksl_version_check(AKSL_VERSION_MAJOR + 1,
|
||||||
|
AKSL_VERSION_MINOR,
|
||||||
|
AKSL_VERSION_PATCH),
|
||||||
|
AKERR_VALUE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_version_check(AKSL_VERSION_MAJOR,
|
||||||
|
AKSL_VERSION_MINOR + 1,
|
||||||
|
AKSL_VERSION_PATCH),
|
||||||
|
AKERR_VALUE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Patch is deliberately not part of the comparison: a patch bump is an ABI
|
||||||
|
* promise, so a caller built against 0.1.0 must keep working against 0.1.7.
|
||||||
|
*/
|
||||||
|
static int test_version_check_ignores_the_patch_level(void)
|
||||||
|
{
|
||||||
|
AKSL_CHECK_OK(aksl_version_check(AKSL_VERSION_MAJOR,
|
||||||
|
AKSL_VERSION_MINOR,
|
||||||
|
AKSL_VERSION_PATCH + 9));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The error has to say which two versions disagree, or it is not actionable. */
|
||||||
|
static int test_mismatch_message_names_both_versions(void)
|
||||||
|
{
|
||||||
|
char compiled[64];
|
||||||
|
|
||||||
|
snprintf(compiled, sizeof(compiled), "%d.%d.%d",
|
||||||
|
AKSL_VERSION_MAJOR + 1, AKSL_VERSION_MINOR, AKSL_VERSION_PATCH);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_version_check(AKSL_VERSION_MAJOR + 1,
|
||||||
|
AKSL_VERSION_MINOR,
|
||||||
|
AKSL_VERSION_PATCH),
|
||||||
|
AKERR_VALUE, compiled);
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_version_check(AKSL_VERSION_MAJOR + 1,
|
||||||
|
AKSL_VERSION_MINOR,
|
||||||
|
AKSL_VERSION_PATCH),
|
||||||
|
AKERR_VALUE, AKSL_VERSION_STRING);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_rejects_null_arguments(void)
|
||||||
|
{
|
||||||
|
int major = 0;
|
||||||
|
int minor = 0;
|
||||||
|
int patch = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_version(NULL, &minor, &patch), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_version(&major, NULL, &patch), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_version(&major, &minor, NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
akerr_init();
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_version_string_matches_its_components);
|
||||||
|
AKSL_RUN(failures, test_version_number_is_decimal_and_ordered);
|
||||||
|
AKSL_RUN(failures, test_loaded_version_matches_the_header);
|
||||||
|
AKSL_RUN(failures, test_soname_matches_the_documented_rule);
|
||||||
|
AKSL_RUN(failures, test_version_check_accepts_a_matching_build);
|
||||||
|
AKSL_RUN(failures, test_version_check_refuses_a_mismatch);
|
||||||
|
AKSL_RUN(failures, test_version_check_ignores_the_patch_level);
|
||||||
|
AKSL_RUN(failures, test_mismatch_message_names_both_versions);
|
||||||
|
AKSL_RUN(failures, test_rejects_null_arguments);
|
||||||
|
|
||||||
|
AKSL_REPORT(failures);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user