project() now carries VERSION 0.1.0, and is the single place a version number
is spelled. It flows into generated version macros, the shared library's
VERSION/SOVERSION, the Version: field in akstdlib.pc, and a new
akstdlibConfigVersion.cmake. Before this @PROJECT_VERSION@ expanded to
nothing, so akstdlib.pc shipped an empty Version: and libakstdlib.so carried
no soname at all.
0.x on purpose: TODO.md section 2.1 still records four confirmed defects whose
fixes change documented behaviour, so the API is not being promised yet. While
the major version is 0 the soname carries MAJOR.MINOR -- 0.1 and 0.2 are
different ABIs -- and becomes MAJOR alone at 1.0. The if() in CMakeLists.txt
and the #if in tests/test_version.c encode that rule and are tested against
each other.
include/akstdlib_version.h.in is configured into the build tree as
akstdlib_version.h and installed beside akstdlib.h. It defines
AKSL_VERSION_MAJOR/MINOR/PATCH/STRING/NUMBER and AKSL_VERSION_SONAME.
AKSL_VERSION_NUMBER is computed rather than written as a literal, because a
literal 000100 is octal in C and would make 0.1.0 compare as 64;
test_version.c asserts it against the runtime components, so a rewrite to a
literal fails.
Those macros record what a caller was compiled against. aksl_version(),
aksl_version_string() and aksl_version_soname() report what actually loaded,
and AKSL_VERSION_CHECK() compares the two, raising AKERR_VALUE naming both.
It is a macro so that it expands at the caller's site and captures the
caller's numbers; the function compares them against the ones baked into the
library. Compatibility is "same soname", so patch is ignored -- a caller built
against 0.1.0 keeps working against 0.1.7.
Normally the soname catches a mismatch at load time and the check never fires.
It earns its keep when the soname is bypassed: a 0.2.0 build dropped in under
the 0.1 filename loads happily, and only the check notices.
write_basic_package_version_file() uses SameMinorVersion to mirror the soname,
falling back to ExactVersion below CMake 3.11 where that mode does not exist.
The fallback is stricter than the soname rule -- it pins the patch level too --
but never laxer, and wrongly refusing a good pairing beats wrongly accepting a
bad one.
Coverage of src/stdlib.c rose to 99.1% of lines (217/219), 45.1% of branches
and 25/25 functions. That puts branch coverage back over the old 45 gate, but
the gate stays at 40: 0.1 points of headroom is not a ratchet.
ctest 14/14, ASan+UBSan 14/14, coverage 16/16 at 90/40. Also verified out of
tree: SONAME libakstdlib.so.0.1 recorded in consumers, pkg-config
--modversion reporting 0.1.0, find_package(akstdlib 0.1) accepted with 0.2 and
1.0 refused, a patch-bumped 0.1.1 loading and passing the check, a 0.2.0
dropped in under the 0.1 filename caught by it, and an embedded
add_subdirectory build keeping its own version rather than the parent's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bump deps/libakerror 22 commits to 5ff8790 (1.0.0), which makes the
status-name table private, moves consumer status codes to a band starting
at AKERR_FIRST_CONSUMER_STATUS, enforces range ownership rather than
treating it as advisory, and gives the library an soname. See
deps/libakerror/UPGRADING.md.
src/stdlib.c needed no changes. This library defines no status codes of its
own -- it raises libakerror's AKERR_* codes and propagates errno, both
inside libakerror's reserved 0-255 band -- and it never referenced
AKERR_MAX_ERR_VALUE, __AKERR_ERROR_NAMES, AKERR_STATUS_RANGE_OK or
AKERR_STATUS_NAME_OK. What moved was everything around the code:
A -DAKSL_COVERAGE=ON build stopped configuring at all. libakerror
namespaces its `mutation` target when embedded but not its `coverage`
target, so it collided with ours. Shadow add_custom_target for the duration
of the add_subdirectory() call and rename the dependency's to
akerror_coverage, alongside the existing add_test shadow. Fix upstream and
delete the workaround; recorded in TODO.md.
Pin the 1.0.0 floor three ways, since no single one covers every
consumption path: an #error in akstdlib.h feature-testing
AKERR_FIRST_CONSUMER_STATUS, because libakerror publishes no version macro;
Requires: akerror >= 1.0.0 in akstdlib.pc, which also gets consumers
-lakerror transitively; and find_dependency(akerror) in
akstdlibConfig.cmake. The last was already broken before this bump -- the
template still carried its MyLibraryConfig placeholder with the dependency
commented out, so any external find_package(akstdlib) failed with a bare
"akerror::akerror not found" out of the generated targets file.
Branch coverage of src/stdlib.c fell from 51.0% to 44.3% with no source or
test change: the 1.0.0 PREPARE_ERROR/FAIL_* macros expand to more branches
at every call site, so 337/661 became 481/1087 -- 144 more branches covered,
426 more counted. Line coverage held at 99.0% (200/202) and function
coverage at 100% (21/21). Re-ratchet the CI branch gate 45 -> 40 rather than
chase branches that belong to libakerror's own suite.
tests/test_status_registry.c pins the contract that made the status-code
migration a no-op: libakstdlib reserves no consumer range, so an application
may allocate from AKERR_FIRST_CONSUMER_STATUS without coordinating with it,
and every status this library raises is inside the reserved band with a name
actually registered -- an unnamed one degrades to "Unknown Error" in every
later stack trace, which nothing else would notice. It exercises the new
ownership enforcement too, so the "reserves nothing" assertion cannot pass
vacuously.
ctest 13/13, ASan+UBSan 13/13, coverage 15/15 at 90/40, mutation 89.6%
(155/173, unchanged). Also verified out of tree: the #error fires as the
first diagnostic against a stale akerror.h, pkg-config refuses akerror
0.9.0, and an external find_package(akstdlib) consumer builds and runs
against a temp-prefix install.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every wrapper outside the list and tree code was untested. Six new test
files close that, following the plan already written in TODO.md 1.2-1.6:
test_stream.c fopen/fread/fwrite/fclose -- happy paths, the round
trip, AKERR_EOF on a short read, AKERR_IO on a stream
opened in the wrong mode, ENOENT, and the NULL guards
test_format.c printf/fprintf/sprintf -- text *and* count asserted
(stdout is pointed at a temp file to check aksl_printf),
all eight NULL guards, EBADF on a read-only stream, and
512 variadic calls in a loop as sanitizer cover for the
missing va_end
test_convert.c ato{i,l,ll,f} happy paths, negatives, leading
whitespace, NULL guards
test_path.c realpath on a file and on a symlink, both compared
against realpath(3) since TMPDIR may itself be a link;
ENOENT, ENOTDIR, NULL path
test_strhash.c djb2 known-answer vectors, len == 0, embedded NUL,
stability, NULL guards
test_convert_strict.c
known-failing (2.1.5): the AKERR_VALUE / ERANGE
contract the ato* family cannot express today
test_tree.c gains the BFS AKERR_NOT_IMPLEMENTED contract, NULL arguments,
and a callback error that is not AKERR_ITERATOR_BREAK propagating out.
Tests deliberately say nothing about behaviour TODO.md records as
defective -- unchecked ptr/mode/resolved_path, short transfers reported as
success, *count left at -1, the djb2 sign extension -- so the eventual fix
does not have to come with a test rewrite. Each failure case in
test_path.c passes a zeroed buffer, because the wrapper's own error path
formats resolved_path with %s (2.1.6).
aksl_capture.h gains aksl_temp_file() with an atexit unlink backstop.
Without it every test that fails before its own unlink leaves temp files
behind -- which is the normal case for a known-failing test, and happens
173 times over in a mutation run.
Coverage on src/stdlib.c: 52.0% -> 99.0% of lines (200/202), 23.6% ->
51.0% of branches, 8/21 -> 21/21 functions. The two uncovered lines are
both `} HANDLE(e, AKERR_ITERATOR_BREAK) {`, where the macro starts with
the `break;` of PROCESS's `case 0:` arm -- reachable only via a non-NULL
error context whose status is zero, the pathology 2.2.1 exists to remove.
Mutation score on src/stdlib.c: 46.8% -> 89.6% (155/173 killed). CI, the
pre-push hook and the docs ratchet from 40 to 80 accordingly, and the 18
survivors are grouped by cause in TODO.md and README.md. A new CI
coverage job gates at 90% lines / 45% branches.
Verified:
ctest --test-dir build # 12/12
ctest --test-dir build-asan # 12/12 under ASan + UBSan
ctest --test-dir build-coverage # 14/14, report attached
ctest --test-dir build -j8 --repeat until-fail:3
gcc -Wall -Wextra -c on all nine test files # no warnings
python3 scripts/mutation_test.py --target src/stdlib.c # 89.6%
No temp files left in /tmp after any of the above.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replace standalone message assertions with a helper that checks the returned akerr status first, then validates message content only as secondary context. Drop ambiguous memcpy message checks where both NULL guards use the same format string.
Cover deterministic TODO 1.1 memory-wrapper behavior with a new CTest target. Exercise malloc/free round trips, NULL argument handling, memset fill/no-op semantics, and memcpy copy/no-op/null-pointer paths.
Harden AKSL_CHECK_OK so success requires no returned error context, catching wrappers that accidentally raise status-0 errors from stale errno.
Co-authored-by: Codex GPT-5.5 Default <codex-gpt-5.5-default@openai.com>
The suite could not fail and did not run. test_linkedlist.c asserted nothing
at all -- it printed node names and returned 0 -- so every confirmed list
defect passed it. test_tree.c was red on every run because parms.steps was
never reset between its three searches, and four libakerror tests registered
but never built, reporting Not Run. CI, meanwhile, was not fetching the
submodule, so configure failed before reaching any of it.
- tests/aksl_capture.h: AKSL_CHECK (NDEBUG-proof), AKSL_CHECK_STATUS/_OK to
run an akerror-returning call, assert its status and release the context,
a capturing akerr_log_method, aksl_slots_in_use(), and an AKSL_RUN driver
that fails any test leaking an error-pool slot.
- test_linkedlist.c: rewritten as 15 assertion cases over the append,
iterate and pop behaviour that is correct today.
- test_tree.c: each search builds its own tree and params.
- Registration driven by AKSL_TESTS, plus AKSL_WILL_FAIL_TESTS (aborts by
design) and AKSL_KNOWN_FAILING_TESTS, which marks WILL_FAIL the three new
tests asserting correct behaviour for the confirmed defects in TODO.md
2.1.1-2.1.3. Fixing a defect flips its test to "unexpectedly passed",
which is the cue to promote it into AKSL_TESTS.
- add_test/set_tests_properties are shadowed across the libakerror
add_subdirectory call: CMake cannot un-register a test and
set_tests_properties cannot cross directory scopes. The dependency has
its own CI.
- AKSL_SANITIZE=ON builds library, tests and dependency with ASan+UBSan.
- scripts/mutation_test.py ported and retargeted at src/stdlib.c; wired to
a manual `mutation` target, not a CI gate until 1.1-1.9 exist.
- CI: checkout with submodules: recursive, and ctest --output-on-failure.
ctest is now 5/5 green in both the default and sanitizer builds.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>