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:
@@ -46,15 +46,27 @@ jobs:
|
||||
# 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
|
||||
# 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.
|
||||
# The report is printed either way -- the uncovered lines it lists are the
|
||||
# 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
|
||||
run: |
|
||||
cmake -S . -B build-coverage -DAKSL_COVERAGE=ON \
|
||||
-DAKSL_COVERAGE_THRESHOLD=90 \
|
||||
-DAKSL_COVERAGE_BRANCH_THRESHOLD=45
|
||||
-DAKSL_COVERAGE_BRANCH_THRESHOLD=40
|
||||
cmake --build build-coverage
|
||||
ctest --test-dir build-coverage --output-on-failure
|
||||
cat build-coverage/coverage-summary.txt
|
||||
|
||||
@@ -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
|
||||
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
|
||||
`cmake --build build-coverage --target coverage` and check the uncovered-line
|
||||
listing before proposing a change. Tests for behaviour that `TODO.md` records as
|
||||
|
||||
@@ -96,6 +96,22 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
endif()
|
||||
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)
|
||||
|
||||
set(AKSL_SUPPRESS_ADD_TEST FALSE)
|
||||
@@ -180,6 +196,7 @@ set(AKSL_TESTS
|
||||
linkedlist
|
||||
memory
|
||||
path
|
||||
status_registry
|
||||
stream
|
||||
strhash
|
||||
tree
|
||||
|
||||
63
README.md
63
README.md
@@ -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
|
||||
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
|
||||
|
||||
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
|
||||
`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
|
||||
`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
|
||||
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
|
||||
`build-coverage/coverage-summary.txt` (the same text report) and
|
||||
`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 --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 --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.
|
||||
@@ -134,12 +179,12 @@ score:
|
||||
|
||||
```sh
|
||||
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
|
||||
branches** and **21/21 functions**, so 90/45 above is a ratchet with headroom
|
||||
rather than a target. The two uncovered lines are both
|
||||
**Where it stands.** `src/stdlib.c` is at **99.0% of lines (200/202)**, **44.3% of
|
||||
branches (481/1087)** and **21/21 functions**, so 90/40 above is a ratchet with
|
||||
headroom rather than a target. The two uncovered lines are both
|
||||
`} 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
|
||||
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
|
||||
are inside the `FAIL_*`/`ATTEMPT`/`FINISH` macro expansions — pool exhaustion,
|
||||
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
|
||||
until per-line counts stop matching the source — so a coverage build is not the
|
||||
|
||||
43
TODO.md
43
TODO.md
@@ -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
|
||||
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.
|
||||
- [ ] **The `deps/libakerror` submodule is pinned 11 commits behind `main`** (pinned at
|
||||
`4fad0ce "Add gitea workflow"`; upstream is at `4212ff0`). The pin predates the
|
||||
refcount-leak fix, the stack-trace buffer-overflow fix, the `AKERR_MAX_ERR_VALUE`
|
||||
correction and the format-string fix. Bump it.
|
||||
- [x] **The `deps/libakerror` submodule was pinned 22 commits behind `main`** (pinned at
|
||||
`4fad0ce "Add gitea workflow"`). Bumped to `5ff8790`, which is libakerror **1.0.0** —
|
||||
the release that made the status-name table private, moved consumer status codes to a
|
||||
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
|
||||
`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
|
||||
|
||||
@@ -6,5 +6,11 @@ includedir=${exec_prefix}/include
|
||||
Name: akstdlib
|
||||
Description: C stdlib with akerror sanity wrappers
|
||||
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}/
|
||||
Libs: -L${libdir} -lakstdlib
|
||||
@@ -1,5 +1,17 @@
|
||||
# cmake/MyLibraryConfig.cmake.in
|
||||
include(CMakeFindDependencyMacro) # If your library has dependencies
|
||||
# find_dependency(AnotherDependency REQUIRED) # Example dependency
|
||||
# cmake/akstdlib.cmake.in -- installed as akstdlibConfig.cmake
|
||||
#
|
||||
# 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")
|
||||
|
||||
2
deps/libakerror
vendored
2
deps/libakerror
vendored
Submodule deps/libakerror updated: 4fad0cec59...5ff87908e7
@@ -2,6 +2,25 @@
|
||||
#define _AKSTDLIB_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 <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
225
tests/test_status_registry.c
Normal file
225
tests/test_status_registry.c
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user