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