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>
This commit is contained in:
2026-07-30 22:20:54 -04:00
parent a37ba3fb89
commit 07c448508b
10 changed files with 393 additions and 18 deletions

View File

@@ -46,15 +46,27 @@ jobs:
# there is no lcov/gcovr to install here. # there is no lcov/gcovr to install here.
# #
# The gate is a ratchet, not a target: src/stdlib.c is at 99.0% of lines # The gate is a ratchet, not a target: src/stdlib.c is at 99.0% of lines
# and 51.0% of branches, so 90/45 fails on a real regression (a test # and 44.3% of branches, so 90/40 fails on a real regression (a test
# deleted, or new untested code added) without tripping over rounding. # deleted, or new untested code added) without tripping over rounding.
# The report is printed either way -- the uncovered lines it lists are the # The report is printed either way -- the uncovered lines it lists are the
# missing tests. # missing tests.
#
# 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 - name: coverage
run: | run: |
cmake -S . -B build-coverage -DAKSL_COVERAGE=ON \ cmake -S . -B build-coverage -DAKSL_COVERAGE=ON \
-DAKSL_COVERAGE_THRESHOLD=90 \ -DAKSL_COVERAGE_THRESHOLD=90 \
-DAKSL_COVERAGE_BRANCH_THRESHOLD=45 -DAKSL_COVERAGE_BRANCH_THRESHOLD=40
cmake --build build-coverage cmake --build build-coverage
ctest --test-dir build-coverage --output-on-failure ctest --test-dir build-coverage --output-on-failure
cat build-coverage/coverage-summary.txt cat build-coverage/coverage-summary.txt

View File

@@ -59,7 +59,7 @@ right list in `CMakeLists.txt`. `AKSL_TESTS` must exit zero.
`AKSL_KNOWN_FAILING_TESTS` assert documented defects from `TODO.md`; when one `AKSL_KNOWN_FAILING_TESTS` assert documented defects from `TODO.md`; when one
starts unexpectedly passing, move it into `AKSL_TESTS` with the fix. starts unexpectedly passing, move it into `AKSL_TESTS` with the fix.
`src/stdlib.c` is at 99.0% line coverage and CI gates it at 90 (line) / 45 `src/stdlib.c` is at 99.0% line coverage and CI gates it at 90 (line) / 40
(branch), so new code needs tests in the same commit. Run (branch), so new code needs tests in the same commit. Run
`cmake --build build-coverage --target coverage` and check the uncovered-line `cmake --build build-coverage --target coverage` and check the uncovered-line
listing before proposing a change. Tests for behaviour that `TODO.md` records as listing before proposing a change. Tests for behaviour that `TODO.md` records as

View File

@@ -96,6 +96,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)
@@ -180,6 +196,7 @@ set(AKSL_TESTS
linkedlist linkedlist
memory memory
path path
status_registry
stream stream
strhash strhash
tree tree

View File

@@ -25,6 +25,41 @@ 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.
### 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 four harnesses. The first three take seconds; the fourth takes about There are four harnesses. The first three take seconds; the fourth takes about
@@ -47,7 +82,9 @@ leaks a slot from libakerror's error pool.
One file per area of the API: `convert` (the `ato*` family), `format` (the One file per area of the API: `convert` (the `ato*` family), `format` (the
`printf` family), `stream` (`fopen`/`fread`/`fwrite`/`fclose`), `path` `printf` family), `stream` (`fopen`/`fread`/`fwrite`/`fclose`), `path`
(`aksl_realpath`), `strhash`, `memory`, `linkedlist` and `tree`. (`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`.
@@ -106,6 +143,14 @@ they work under `ctest -j` too:
The reset matters: gcov counters are cumulative, so without it each report would The reset matters: gcov counters are cumulative, so without it each report would
fold in every earlier run and overstate coverage. 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 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-summary.txt` (the same text report) and
`build-coverage/coverage.xml` (Cobertura, for CI publishers). The `coverage` `build-coverage/coverage.xml` (Cobertura, for CI publishers). The `coverage`
@@ -123,7 +168,7 @@ scripts/coverage.py --build build-coverage # report on disk
scripts/coverage.py --build build-coverage --summary-only # totals only 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 --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 --run-tests # reset, run ctest, report
scripts/coverage.py --build build-coverage --threshold 90 --branch-threshold 45 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. It needs nothing but Python 3 and gcc's own `gcov` — no lcov, gcovr or genhtml.
@@ -134,12 +179,12 @@ score:
```sh ```sh
cmake -S . -B build-coverage -DAKSL_COVERAGE=ON \ cmake -S . -B build-coverage -DAKSL_COVERAGE=ON \
-DAKSL_COVERAGE_THRESHOLD=90 -DAKSL_COVERAGE_BRANCH_THRESHOLD=45 -DAKSL_COVERAGE_THRESHOLD=90 -DAKSL_COVERAGE_BRANCH_THRESHOLD=40
``` ```
**Where it stands.** `src/stdlib.c` is at **99.0% of lines (200/202)**, **51.0% of **Where it stands.** `src/stdlib.c` is at **99.0% of lines (200/202)**, **44.3% of
branches** and **21/21 functions**, so 90/45 above is a ratchet with headroom branches (481/1087)** and **21/21 functions**, so 90/40 above is a ratchet with
rather than a target. The two uncovered lines are both headroom rather than a target. The two uncovered lines are both
`} HANDLE(e, AKERR_ITERATOR_BREAK) {` — in libakerror that macro begins with the `} 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 `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 callback returns a non-NULL error context whose status is *zero*. That is the
@@ -149,7 +194,11 @@ deliberately rather than pinned by a test.
Branch coverage sits far below line coverage because most branches in this file Branch coverage sits far below line coverage because most branches in this file
are inside the `FAIL_*`/`ATTEMPT`/`FINISH` macro expansions — pool exhaustion, are inside the `FAIL_*`/`ATTEMPT`/`FINISH` macro expansions — pool exhaustion,
stack-trace buffer limits, `akerr_valid_error_address` failures — and belong to stack-trace buffer limits, `akerr_valid_error_address` failures — and belong to
libakerror's own suite rather than to this one. 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) now score 44.3% (481/1087). The gate moved 45 → 40 to match; line and
function coverage did not move at all.
Two caveats. Coverage is measured at `-O0`, because the optimizer reorders lines 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 until per-line counts stop matching the source — so a coverage build is not the

43
TODO.md
View File

@@ -426,10 +426,45 @@ cue to move it into `AKSL_TESTS`.
- [ ] `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.
- [ ] **`project()` declares no `VERSION`**, so `@PROJECT_VERSION@` expands to nothing and
the installed `akstdlib.pc` ships an empty `Version:` field — a consumer cannot pin
this library the way `akstdlib.pc` now pins `akerror >= 1.0.0`. libakstdlib also has
no `SOVERSION`, so `libakstdlib.so` carries no soname even though its public header
re-exports libakerror's ABI and therefore breaks whenever libakerror's does. Give the
project a version and a `SOVERSION`, and install an `akstdlibConfigVersion.cmake` via
`write_basic_package_version_file()` so `find_package(akstdlib 1.2 REQUIRED)` can work.
(libakerror has the same `ConfigVersion` gap, which is why
`cmake/akstdlib.cmake.in` calls `find_dependency(akerror)` with no version.)
- [ ] **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

View File

@@ -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

View File

@@ -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")

View File

@@ -2,6 +2,25 @@
#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
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>

View 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);
}