diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 0babd83..b819e95 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -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 diff --git a/AGENTS.md b/AGENTS.md index 760ed5a..3f8aff5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 3d64b93..6bda2fd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/README.md b/README.md index a7fd78c..25588cf 100644 --- a/README.md +++ b/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 diff --git a/TODO.md b/TODO.md index 99d16ce..a26d58f 100644 --- a/TODO.md +++ b/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 diff --git a/akstdlib.pc.in b/akstdlib.pc.in index 1e4db10..96def2a 100644 --- a/akstdlib.pc.in +++ b/akstdlib.pc.in @@ -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 \ No newline at end of file diff --git a/cmake/akstdlib.cmake.in b/cmake/akstdlib.cmake.in index 06ac718..3aec432 100644 --- a/cmake/akstdlib.cmake.in +++ b/cmake/akstdlib.cmake.in @@ -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") diff --git a/deps/libakerror b/deps/libakerror index 4fad0ce..5ff8790 160000 --- a/deps/libakerror +++ b/deps/libakerror @@ -1 +1 @@ -Subproject commit 4fad0cec595cca503ce863af2ded3bb7fe3a81d5 +Subproject commit 5ff87908e7b68ab2dc328b55ea89585272411ff9 diff --git a/include/akstdlib.h b/include/akstdlib.h index 37a1ae9..140cb8b 100644 --- a/include/akstdlib.h +++ b/include/akstdlib.h @@ -2,6 +2,25 @@ #define _AKSTDLIB_H_ #include + +/* + * 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 #include #include diff --git a/tests/test_status_registry.c b/tests/test_status_registry.c new file mode 100644 index 0000000..d9cac54 --- /dev/null +++ b/tests/test_status_registry.c @@ -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 + +/* + * 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); +}