98a0a8562dd5b18677cd5d8e8dfd2e7234d5abf4
6 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
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>
|
|||
|
82c47ed773
|
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> |
|||
|
54fb44cd2b
|
Document the test harnesses and add a pre-push hook
README.md was a build badge and nothing else. It now covers what the library is, how to build it, and how to run each of the three harnesses locally: the ctest suite, the AKSL_SANITIZE ASan/UBSan build, and mutation testing. The section on reading ctest output matters most: two of the three test lists are marked WILL_FAIL, so an all-green run does not mean the library is defect-free -- it means the known-good tests passed and the known-bad ones are still failing in the documented way. .githooks/pre-push runs the two fast harnesses (default build + ctest, then sanitizer build + ctest) before a push leaves the machine. It skips when there is nothing to test (branch deletions, empty pushes), fails the push with the captured output on any error, and builds under .git/aksl-prepush so it never disturbs your own build/. The slow mutation gate is opt-in via AKSL_HOOK_MUTATION=1, and git push --no-verify bypasses everything. Enable with: git config core.hooksPath .githooks Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
3e3ea41dc8
|
Add CI build
Some checks failed
libakstdlib CI Build / cmake_build (push) Failing after 2m41s
|