7940276f879cd3813cec58647fe9c0085218ec8d
9 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
125eeb2109
|
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>
|
|||
|
fd71bcc67b
|
Record what the first full consumer had to work around
akbasic is a ~6,300-line C interpreter built on this library. It is the first
consumer to exercise the whole surface rather than a corner of it, so what it
could not use is worth writing down: it prioritizes the section 3 wishlist by
what a real port actually reaches for, and it says which section 2 entries have
teeth.
The number that matters: across src/, akbasic makes 10 calls into this library
and 116 to raw libc. A library whose value proposition is turning silent libc
failures into error contexts is being bypassed 92% of the time by the consumer
most committed to it.
Four things it had to write for itself, each of which maps onto an existing
unchecked box. A strict strtoll/strtod wrapper, because 2.1.5 means aksl_atoi
would have turned four diagnosable errors into wrong answers -- VAL("garbage")
answering 0.0 instead of raising. A fixed-capacity string-keyed hash table,
which is 3.6's "hash map built on aksl_strhash_djb2", needed three times over
for variables, functions and labels. The bounded-copy-with-truncation-as-error
idiom at ten sites across six files. And case folding at three sites.
Also confirms 2.2.4 (aksl_sprintf unbounded -- akbasic deliberately never calls
it, and has 28 snprintf sites with no aksl_snprintf to route them through),
2.2.2 (aksl_fopen validation, reached from user input via DLOAD/DSAVE), 2.2.6
(the djb2 sign extension, benign here only because BASIC identifiers are ASCII)
and 2.1.4 (the missing va_end, which akbasic's text sink runs on every line of
program output).
Section 4.3 records what akbasic did *not* need, so the wishlist does not get
reordered purely by one consumer's shape: it allocates nothing and uses no lists
or trees, so a consumer that does allocate would weight 3.1's memory section far
higher.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
|
95e5002512
|
Version the library at 0.1.0
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> |
|||
|
07c448508b
|
Upgrade to libakerror 1.0.0
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> |
|||
|
437da2960b
|
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>
|
|||
| 8003239116 |
Add memory wrapper tests
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> |
|||
|
80205d5c4f
|
Fix miscounted break-visit expectation in TODO.md 1.8
The bullet said breaking at tree[3] in pre-order should assert 4 visits. Pre-order is 0, 1, 3, 4, 2, 5, 6, so the correct count is 3 -- which is what tests/test_tree_iterate_break.c already asserts. The TODO was written before that test existed and had the number wrong, not the test. Also point the bullet at the existing pre-order test, and give the in-order and post-order traversals with their break points so the remaining work does not need the count re-derived. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
68009ea0e3
|
Add mutation testing to the Gitea workflow
A separate mutation_test job runs scripts/mutation_test.py over src/stdlib.c with a --threshold gate and publishes a JUnit report, mirroring libakerror's CI. Checkout needs submodules: recursive -- the harness copies the repo and builds the copy top-level, so deps/libakerror has to be there. Measured score on the section 1.0 suite is 46.8% (81/173 killed). The gate is set at 40: a regression ratchet, not a quality bar. Survivors cluster in the wrappers that have no tests yet (printf family, ato* family, realpath, the memory and stream wrappers); the list and tree code that section 1.0 does cover leaves only 5 survivors between them. Also caps every test with TIMEOUT 30. Measuring the score turned this up: a mutant deleting `fast = fast->next->next` makes aksl_list_iterate spin forever, so ctest never returned, the harness killed ctest at its own 120s timeout, and the orphaned test binary kept burning a core while holding the pipe open -- the run wedged and needed killing by hand. With a per-test timeout ctest reaps its own child, those mutants are killed in 30s, and nothing is left behind. It guards the real suite against the same shape of bug too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
3e296c3bff
|
Build a real test harness (TODO.md section 1.0)
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> |