Files
libakstdlib/AGENTS.md

166 lines
7.9 KiB
Markdown
Raw Normal View History

2026-07-29 17:29:49 -04:00
# Repository Guidelines
## Project Structure & Module Organization
Version at 0.2.0: complete the wishlist, document it, gate the docs Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that file to hold outstanding items only. The API break gets a minor bump, because pre-1.0 the soname carries MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five signatures changed and the ato* contract with them; UPGRADING.md is new and lists every one, with the before/after for the cases the compiler cannot warn about. Section 3.1 is finished: reallocarray with the multiplication checked, aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf. Four functions on that list are deliberately absent rather than missing -- sprintf, strtok, setbuf and perror -- and TODO.md now says which and why, so nobody adds them thinking they were forgotten. Section 1.9, the cross-cutting tests: tests/test_pool.c drives every failure path AKERR_MAX_ARRAY_ERROR + 10 times and checks the pool after each round, because a wrapper that leaks a slot fails a hundred calls later in unrelated code. It also asserts that each error names the function and file it was raised from, which is what catches a FAIL that migrates into a helper during a refactor: status right, message right, origin quietly lying. tests/negative/ two sources that must FAIL to compile, built with -Werror and registered WILL_FAIL. AKERR_NOIGNORE and the format attributes are enforced by the compiler and by nothing else; drop either and every ordinary test still passes. Thread safety is answered rather than tested: the library is not thread-safe and cannot be made so from here, because libakerror's error pool is an unlocked process-global array. README.md says so plainly and TODO.md carries it as the item blocking any future pthread wrappers. Doxygen is configured and gated. All 147 public functions have @brief, a @param each, @throws per status and @return; EXTRACT_ALL is off and WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an undocumented entity. It ran to 0 warnings. The Doxyfile carries no version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from project(), so that stays the one place a version is written. CI now builds against the submodule it pins instead of also installing libakerror@main and never linking it, adds -Werror, and gains a sanitizer job. The pre-push hook matches, and runs the docs check too. Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The eight uncovered lines are each uncovered on purpose and TODO.md says which and why. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 08:00:16 -04:00
`libakstdlib` is a C shared library that wraps libc calls and data structures in
`libakerror` error contexts.
Public API declarations live in `include/akstdlib.h`, which is also where the
Doxygen documentation lives -- the header is the contract, the sources carry the
reasoning. The implementation is split by domain:
| File | Covers |
|---|---|
| `src/stdlib.c` | memory, formatted output, string-to-number, realpath, djb2, and the list/tree traversal entry points |
| `src/string.c` | the `string.h` surface |
| `src/stream.c` | `stdio.h` beyond open/read/write/close |
| `src/collections.c` | list and tree operations, hash map, string buffer, FNV-1a |
| `src/aksl_internal.h` | shared internals; not installed, not public |
Tests are one-file CTest executables under `tests/test_<name>.c`, with shared
helpers in `tests/aksl_capture.h`. `tests/negative/` holds sources that must
2026-07-31 08:05:09 -04:00
*fail* to compile, and `tests/consumer/` is a standalone project built against
the *installed* package rather than the build tree -- see the testing section
below. CMake package templates are
Version at 0.2.0: complete the wishlist, document it, gate the docs Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that file to hold outstanding items only. The API break gets a minor bump, because pre-1.0 the soname carries MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five signatures changed and the ato* contract with them; UPGRADING.md is new and lists every one, with the before/after for the cases the compiler cannot warn about. Section 3.1 is finished: reallocarray with the multiplication checked, aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf. Four functions on that list are deliberately absent rather than missing -- sprintf, strtok, setbuf and perror -- and TODO.md now says which and why, so nobody adds them thinking they were forgotten. Section 1.9, the cross-cutting tests: tests/test_pool.c drives every failure path AKERR_MAX_ARRAY_ERROR + 10 times and checks the pool after each round, because a wrapper that leaks a slot fails a hundred calls later in unrelated code. It also asserts that each error names the function and file it was raised from, which is what catches a FAIL that migrates into a helper during a refactor: status right, message right, origin quietly lying. tests/negative/ two sources that must FAIL to compile, built with -Werror and registered WILL_FAIL. AKERR_NOIGNORE and the format attributes are enforced by the compiler and by nothing else; drop either and every ordinary test still passes. Thread safety is answered rather than tested: the library is not thread-safe and cannot be made so from here, because libakerror's error pool is an unlocked process-global array. README.md says so plainly and TODO.md carries it as the item blocking any future pthread wrappers. Doxygen is configured and gated. All 147 public functions have @brief, a @param each, @throws per status and @return; EXTRACT_ALL is off and WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an undocumented entity. It ran to 0 warnings. The Doxyfile carries no version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from project(), so that stays the one place a version is written. CI now builds against the submodule it pins instead of also installing libakerror@main and never linking it, adds -Werror, and gains a sanitizer job. The pre-push hook matches, and runs the docs check too. Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The eight uncovered lines are each uncovered on purpose and TODO.md says which and why. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 08:00:16 -04:00
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/`.
2026-07-29 17:29:49 -04:00
## 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
```
Add code coverage to the CTest suite New AKSL_COVERAGE option instruments the library and its tests with --coverage -O0 and wires the report into the suite itself, so a plain ctest --test-dir build-coverage both runs the tests and produces coverage. Two CTest entries do the work, held in order by a CTest fixture rather than by declaration order so they also hold under ctest -j: coverage_reset (FIXTURES_SETUP) clears the .gcda counters before any test, since gcov counts are cumulative and would otherwise fold in earlier runs; coverage_report (FIXTURES_CLEANUP) aggregates gcov output afterwards. AKSL_COVERAGE_THRESHOLD / AKSL_COVERAGE_BRANCH_THRESHOLD gate the report, the same regression-ratchet idea as the mutation score. The `coverage` target builds, runs and prints in one step. scripts/coverage.py parses gcov's JSON output, aggregates line, branch and function counts across translation units, and lists every uncovered line and never-called function -- the actionable half, as with surviving mutants. Python stdlib plus gcc's own gcov only: no lcov, gcovr or genhtml. It also writes coverage-summary.txt (CTest hides the output of a passing test) and a Cobertura coverage.xml for CI publishers. Instrumentation is per target, so deps/libakerror stays out of the report. The mutation harness now ignores build*/ and gcov artifacts when copying the tree, so a coverage build does not slow it down. Baseline on src/stdlib.c: 52.0% of lines, 23.6% of branches, 8 of 21 functions. The uncovered functions are the untested wrappers the mutation survivors already point at (printf, ato*, stream, realpath, strhash). Verified: cmake -S . -B build-coverage -DAKSL_COVERAGE=ON cmake --build build-coverage --target coverage # 8/8, report printed ctest --test-dir build-coverage -j8 # fixture order holds cmake -S . -B build-coverage -DAKSL_COVERAGE=ON -DAKSL_COVERAGE_THRESHOLD=60 ctest --test-dir build-coverage --output-on-failure # gate fails as expected ctest --test-dir build --output-on-failure # 6/6, no .gcda emitted ctest --test-dir build-asan --output-on-failure # 6/6 scripts/mutation_test.py --target src/stdlib.c --list # 173 mutants, unchanged Totals match gcov itself: 51.98% of 202 lines, 23.60% of 661 branches. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 01:48:05 -04:00
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
```
2026-07-29 17:29:49 -04:00
Run `cmake --build build --target mutation` only when you need the slower
Version at 0.2.0: complete the wishlist, document it, gate the docs Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that file to hold outstanding items only. The API break gets a minor bump, because pre-1.0 the soname carries MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five signatures changed and the ato* contract with them; UPGRADING.md is new and lists every one, with the before/after for the cases the compiler cannot warn about. Section 3.1 is finished: reallocarray with the multiplication checked, aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf. Four functions on that list are deliberately absent rather than missing -- sprintf, strtok, setbuf and perror -- and TODO.md now says which and why, so nobody adds them thinking they were forgotten. Section 1.9, the cross-cutting tests: tests/test_pool.c drives every failure path AKERR_MAX_ARRAY_ERROR + 10 times and checks the pool after each round, because a wrapper that leaks a slot fails a hundred calls later in unrelated code. It also asserts that each error names the function and file it was raised from, which is what catches a FAIL that migrates into a helper during a refactor: status right, message right, origin quietly lying. tests/negative/ two sources that must FAIL to compile, built with -Werror and registered WILL_FAIL. AKERR_NOIGNORE and the format attributes are enforced by the compiler and by nothing else; drop either and every ordinary test still passes. Thread safety is answered rather than tested: the library is not thread-safe and cannot be made so from here, because libakerror's error pool is an unlocked process-global array. README.md says so plainly and TODO.md carries it as the item blocking any future pthread wrappers. Doxygen is configured and gated. All 147 public functions have @brief, a @param each, @throws per status and @return; EXTRACT_ALL is off and WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an undocumented entity. It ran to 0 warnings. The Doxyfile carries no version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from project(), so that stays the one place a version is written. CI now builds against the submodule it pins instead of also installing libakerror@main and never linking it, adds -Werror, and gains a sanitizer job. The pre-push hook matches, and runs the docs check too. Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The eight uncovered lines are each uncovered on purpose and TODO.md says which and why. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 08:00:16 -04:00
mutation harness, and `cmake --build build --target docs` to regenerate the API
documentation -- that one fails if any public function, parameter or return value
is undocumented, so it is a check as well as a generator.
`.githooks/pre-push` runs the default build, the sanitizer build and the
documentation check before a push; enable it with
`git config core.hooksPath .githooks`. `rebuild.sh` installs to
`/home/andrew/local` and removes the local build directory, so treat it as a
local convenience script.
2026-07-29 17:29:49 -04:00
## 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.
Version at 0.2.0: complete the wishlist, document it, gate the docs Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that file to hold outstanding items only. The API break gets a minor bump, because pre-1.0 the soname carries MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five signatures changed and the ato* contract with them; UPGRADING.md is new and lists every one, with the before/after for the cases the compiler cannot warn about. Section 3.1 is finished: reallocarray with the multiplication checked, aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf. Four functions on that list are deliberately absent rather than missing -- sprintf, strtok, setbuf and perror -- and TODO.md now says which and why, so nobody adds them thinking they were forgotten. Section 1.9, the cross-cutting tests: tests/test_pool.c drives every failure path AKERR_MAX_ARRAY_ERROR + 10 times and checks the pool after each round, because a wrapper that leaks a slot fails a hundred calls later in unrelated code. It also asserts that each error names the function and file it was raised from, which is what catches a FAIL that migrates into a helper during a refactor: status right, message right, origin quietly lying. tests/negative/ two sources that must FAIL to compile, built with -Werror and registered WILL_FAIL. AKERR_NOIGNORE and the format attributes are enforced by the compiler and by nothing else; drop either and every ordinary test still passes. Thread safety is answered rather than tested: the library is not thread-safe and cannot be made so from here, because libakerror's error pool is an unlocked process-global array. README.md says so plainly and TODO.md carries it as the item blocking any future pthread wrappers. Doxygen is configured and gated. All 147 public functions have @brief, a @param each, @throws per status and @return; EXTRACT_ALL is off and WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an undocumented entity. It ran to 0 warnings. The Doxyfile carries no version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from project(), so that stays the one place a version is written. CI now builds against the submodule it pins instead of also installing libakerror@main and never linking it, adds -Werror, and gains a sanitizer job. The pre-push hook matches, and runs the docs check too. Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The eight uncovered lines are each uncovered on purpose and TODO.md says which and why. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 08:00:16 -04:00
Four conventions hold across the whole library, and a new wrapper that breaks one
of them is wrong even if it compiles and passes:
- **Preserve the libc contract unless there is a compelling, documented reason
not to.** libc behaviour is the standard to meet. This library changes only
the error transport (to `akerror`) and, where libc returns a value, the result
shape (through a caller-provided destination pointer).
Version at 0.2.0: complete the wishlist, document it, gate the docs Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that file to hold outstanding items only. The API break gets a minor bump, because pre-1.0 the soname carries MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five signatures changed and the ato* contract with them; UPGRADING.md is new and lists every one, with the before/after for the cases the compiler cannot warn about. Section 3.1 is finished: reallocarray with the multiplication checked, aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf. Four functions on that list are deliberately absent rather than missing -- sprintf, strtok, setbuf and perror -- and TODO.md now says which and why, so nobody adds them thinking they were forgotten. Section 1.9, the cross-cutting tests: tests/test_pool.c drives every failure path AKERR_MAX_ARRAY_ERROR + 10 times and checks the pool after each round, because a wrapper that leaks a slot fails a hundred calls later in unrelated code. It also asserts that each error names the function and file it was raised from, which is what catches a FAIL that migrates into a helper during a refactor: status right, message right, origin quietly lying. tests/negative/ two sources that must FAIL to compile, built with -Werror and registered WILL_FAIL. AKERR_NOIGNORE and the format attributes are enforced by the compiler and by nothing else; drop either and every ordinary test still passes. Thread safety is answered rather than tested: the library is not thread-safe and cannot be made so from here, because libakerror's error pool is an unlocked process-global array. README.md says so plainly and TODO.md carries it as the item blocking any future pthread wrappers. Doxygen is configured and gated. All 147 public functions have @brief, a @param each, @throws per status and @return; EXTRACT_ALL is off and WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an undocumented entity. It ran to 0 warnings. The Doxyfile carries no version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from project(), so that stays the one place a version is written. CI now builds against the submodule it pins instead of also installing libakerror@main and never linking it, adds -Werror, and gains a sanitizer job. The pre-push hook matches, and runs the docs check too. Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The eight uncovered lines are each uncovered on purpose and TODO.md says which and why. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 08:00:16 -04:00
- **A NULL out-param is a caller error**, not "don't care".
- **Finding nothing is success** -- searching functions write NULL or zero and
return NULL.
- **Truncation is a failure.** Take the destination's size, raise
AKERR_OUTOFBOUNDS, and write nothing rather than a prefix.
- **Clear `errno` before the wrapped call** and read it back through
`AKSL_ERRNO_OR`, so no error can carry status 0 -- which every downstream
`DETECT` reads as success.
Every new public function needs a Doxygen block on its **declaration** with
`@brief`, a `@param` per parameter, a `@throws` per status it can raise, and
`@return`. The `docs` target fails otherwise.
The build is `-Wall -Wextra` and CI adds `-Werror`. `-Wpedantic` is deliberately
off: libakerror's `FAIL_*` macros trip "ISO C99 requires at least one argument
for the ..." on their own expansion, not on anything at the call site.
**Do not wrap a libc function that cannot fail and provides no failure or
operation-status result.** There is no `akerror` context to carry. A value such
as `umask()`'s previous mask is not an operation-status result, so `umask()` is
not a wrapper candidate.
2026-07-29 17:29:49 -04:00
## 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 defects that have an open issue; when one
starts unexpectedly passing, move it into `AKSL_TESTS` with the fix and close the
issue. Both of the
Version at 0.2.0: complete the wishlist, document it, gate the docs Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that file to hold outstanding items only. The API break gets a minor bump, because pre-1.0 the soname carries MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five signatures changed and the ato* contract with them; UPGRADING.md is new and lists every one, with the before/after for the cases the compiler cannot warn about. Section 3.1 is finished: reallocarray with the multiplication checked, aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf. Four functions on that list are deliberately absent rather than missing -- sprintf, strtok, setbuf and perror -- and TODO.md now says which and why, so nobody adds them thinking they were forgotten. Section 1.9, the cross-cutting tests: tests/test_pool.c drives every failure path AKERR_MAX_ARRAY_ERROR + 10 times and checks the pool after each round, because a wrapper that leaks a slot fails a hundred calls later in unrelated code. It also asserts that each error names the function and file it was raised from, which is what catches a FAIL that migrates into a helper during a refactor: status right, message right, origin quietly lying. tests/negative/ two sources that must FAIL to compile, built with -Werror and registered WILL_FAIL. AKERR_NOIGNORE and the format attributes are enforced by the compiler and by nothing else; drop either and every ordinary test still passes. Thread safety is answered rather than tested: the library is not thread-safe and cannot be made so from here, because libakerror's error pool is an unlocked process-global array. README.md says so plainly and TODO.md carries it as the item blocking any future pthread wrappers. Doxygen is configured and gated. All 147 public functions have @brief, a @param each, @throws per status and @return; EXTRACT_ALL is off and WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an undocumented entity. It ran to 0 warnings. The Doxyfile carries no version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from project(), so that stays the one place a version is written. CI now builds against the submodule it pins instead of also installing libakerror@main and never linking it, adds -Werror, and gains a sanitizer job. The pre-push hook matches, and runs the docs check too. Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The eight uncovered lines are each uncovered on purpose and TODO.md says which and why. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 08:00:16 -04:00
latter are currently empty -- all six confirmed defects are fixed -- but the
mechanism stays for the next one.
`tests/negative/` holds sources that must **fail** to compile. Each is an
`EXCLUDE_FROM_ALL` target built with `-Werror` and registered as a `WILL_FAIL`
CTest entry, so the test passes only when the compile fails. They cover the two
guarantees the compiler enforces and nothing else does: `AKERR_NOIGNORE` and the
format attributes. Drop either in a refactor and every ordinary test still
passes.
2026-07-29 17:29:49 -04:00
2026-07-31 08:05:09 -04:00
`tests/consumer/` is configured standalone against `CMAKE_PREFIX_PATH`, not as
part of this build, because being part of this build is what would let it pass
without testing anything. It covers the paths only an install has: the version
file, `find_dependency(akerror)` resolving, and the exported
`akstdlib::akstdlib` target. CI runs it after `cmake --install`.
Version at 0.2.0: complete the wishlist, document it, gate the docs Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that file to hold outstanding items only. The API break gets a minor bump, because pre-1.0 the soname carries MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five signatures changed and the ato* contract with them; UPGRADING.md is new and lists every one, with the before/after for the cases the compiler cannot warn about. Section 3.1 is finished: reallocarray with the multiplication checked, aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf. Four functions on that list are deliberately absent rather than missing -- sprintf, strtok, setbuf and perror -- and TODO.md now says which and why, so nobody adds them thinking they were forgotten. Section 1.9, the cross-cutting tests: tests/test_pool.c drives every failure path AKERR_MAX_ARRAY_ERROR + 10 times and checks the pool after each round, because a wrapper that leaks a slot fails a hundred calls later in unrelated code. It also asserts that each error names the function and file it was raised from, which is what catches a FAIL that migrates into a helper during a refactor: status right, message right, origin quietly lying. tests/negative/ two sources that must FAIL to compile, built with -Werror and registered WILL_FAIL. AKERR_NOIGNORE and the format attributes are enforced by the compiler and by nothing else; drop either and every ordinary test still passes. Thread safety is answered rather than tested: the library is not thread-safe and cannot be made so from here, because libakerror's error pool is an unlocked process-global array. README.md says so plainly and TODO.md carries it as the item blocking any future pthread wrappers. Doxygen is configured and gated. All 147 public functions have @brief, a @param each, @throws per status and @return; EXTRACT_ALL is off and WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an undocumented entity. It ran to 0 warnings. The Doxyfile carries no version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from project(), so that stays the one place a version is written. CI now builds against the submodule it pins instead of also installing libakerror@main and never linking it, adds -Werror, and gains a sanitizer job. The pre-push hook matches, and runs the docs check too. Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The eight uncovered lines are each uncovered on purpose and TODO.md says which and why. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 08:00:16 -04:00
Coverage is 99.5% of lines and 100% of functions across all four sources; CI
gates at 90 (line) / 40 (branch), so new code needs tests in the same commit. Run
Test the libc wrappers: 52% -> 99% line coverage 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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 02:07:08 -04:00
`cmake --build build-coverage --target coverage` and check the uncovered-line
listing before proposing a change. Tests for behaviour an open issue records as
Test the libc wrappers: 52% -> 99% line coverage 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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 02:07:08 -04:00
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.
2026-07-29 17:29:49 -04:00
## 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 issue it closes.
2026-07-29 17:29:49 -04:00
## Agent-Specific Instructions
**Outstanding work goes in the issue tracker, not in a file.** Open an issue at
<https://source.starfort.tech/andrew/libakstdlib/issues> — `tea issues create
--repo andrew/libakstdlib` — naming the file and line, the functional
consequence, and what closing it would touch. Label it by kind and blast radius
and leave `status::grooming` on it until its scope and approach are settled.
**Do not add outstanding items to `TODO.md`**: that file is the record of where
the library stands, what is deliberately not wrapped, and which uncovered lines
are uncoverable rather than untested. A description of work still to do goes
stale the moment somebody does it, which is why the two are separated.
**A defect in a dependency is filed against that dependency.** `libakerror` has
a tracker on the same forge, and three defects that cost this library a
workaround each sat in this repository's own notes for months without anybody
upstream being able to see them. Comment the workaround at its site with the
words "filed upstream" and delete it when the fix lands.
2026-07-29 17:29:49 -04:00
Do not modify generated build trees, profiling artifacts, or untracked scratch
files unless explicitly asked. Prefer small, test-backed changes and update
`README.md` when changing documented workflows.