Derive err_maxval's code set from the header, not a hardcoded list
The previous err_maxval hardcoded the list of AKERR_* codes, which silently rots the moment a code is added. Instead, parse the generated akerror.h at runtime: discover every "#define AKERR_<NAME> (AKERR_LAST_ERRNO_VALUE + N)", take the highest offset actually defined, and assert AKERR_MAX_ERR_VALUE covers it. A compile-time cross-check ties the parsed ceiling to the compiled macro so the test can't pass by reading a stale header. CMake injects the header path as AKERR_GENERATED_HEADER. Verified: the test fails (max_err_value >= highest_code) when pointed at a +15 header while AKERR_BADEXC is +17, and now also strengthens header mutation coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -81,6 +81,11 @@ foreach(_test IN LISTS AKERR_TESTS)
|
|||||||
add_test(NAME ${_test} COMMAND test_${_test})
|
add_test(NAME ${_test} COMMAND test_${_test})
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
|
# err_maxval parses the generated header to check AKERR_MAX_ERR_VALUE covers
|
||||||
|
# every AKERR_* code, so it needs the path to the header it was built against.
|
||||||
|
target_compile_definitions(test_err_maxval PRIVATE
|
||||||
|
AKERR_GENERATED_HEADER="${GENERATED_AKERROR_H}")
|
||||||
|
|
||||||
set_tests_properties(
|
set_tests_properties(
|
||||||
${AKERR_WILL_FAIL_TESTS}
|
${AKERR_WILL_FAIL_TESTS}
|
||||||
PROPERTIES WILL_FAIL TRUE
|
PROPERTIES WILL_FAIL TRUE
|
||||||
|
|||||||
@@ -1,52 +1,92 @@
|
|||||||
#include "akerror.h"
|
#include "akerror.h"
|
||||||
#include "err_capture.h"
|
#include "err_capture.h"
|
||||||
|
#include <regex.h>
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* AKERR_MAX_ERR_VALUE sizes the __AKERR_ERROR_NAMES table and is the upper bound
|
* AKERR_MAX_ERR_VALUE sizes the __AKERR_ERROR_NAMES table and is the upper bound
|
||||||
* akerr_name_for_status() will accept. If it is smaller than the highest AKERR_*
|
* akerr_name_for_status() accepts. If it is smaller than the highest AKERR_*
|
||||||
* code, those codes silently lose their names (this was a real bug: the max was
|
* code, those codes silently lose their names (this was a real bug: the max was
|
||||||
* +15 while AKERR_BADEXC is +17).
|
* +15 while AKERR_BADEXC is +17).
|
||||||
*
|
*
|
||||||
* Guard the invariant: the AKERR_* range reserved above AKERR_LAST_ERRNO_VALUE
|
* Rather than hardcode the list of codes (which rots the moment someone adds a
|
||||||
* must be larger than the number of AKERR_* codes defined, and every code must
|
* code), this test parses the generated akerror.h, discovers every
|
||||||
* be individually indexable.
|
* #define AKERR_<NAME> (AKERR_LAST_ERRNO_VALUE + <N>)
|
||||||
|
* finds the highest offset actually defined, and verifies AKERR_MAX_ERR_VALUE
|
||||||
|
* covers it. The path to the header the library was built from is injected by
|
||||||
|
* CMake as AKERR_GENERATED_HEADER.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* Every AKERR_* library error code. Keep in sync with akerror.h. */
|
#ifndef AKERR_GENERATED_HEADER
|
||||||
static const int akerr_codes[] = {
|
#error "AKERR_GENERATED_HEADER (path to generated akerror.h) must be defined"
|
||||||
AKERR_NULLPOINTER,
|
#endif
|
||||||
AKERR_OUTOFBOUNDS,
|
|
||||||
AKERR_API,
|
|
||||||
AKERR_ATTRIBUTE,
|
|
||||||
AKERR_TYPE,
|
|
||||||
AKERR_KEY,
|
|
||||||
AKERR_INDEX,
|
|
||||||
AKERR_FORMAT,
|
|
||||||
AKERR_IO,
|
|
||||||
AKERR_VALUE,
|
|
||||||
AKERR_RELATIONSHIP,
|
|
||||||
AKERR_EOF,
|
|
||||||
AKERR_CIRCULAR_REFERENCE,
|
|
||||||
AKERR_ITERATOR_BREAK,
|
|
||||||
AKERR_NOT_IMPLEMENTED,
|
|
||||||
AKERR_BADEXC,
|
|
||||||
};
|
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
int n = (int)(sizeof(akerr_codes) / sizeof(akerr_codes[0]));
|
FILE *fh = fopen(AKERR_GENERATED_HEADER, "r");
|
||||||
|
AKERR_CHECK(fh != NULL);
|
||||||
|
|
||||||
/* AKERR_MAX_ERR_VALUE must reserve more codes than are actually defined. */
|
/* #define AKERR_NAME (AKERR_LAST_ERRNO_VALUE + N) */
|
||||||
AKERR_CHECK((AKERR_MAX_ERR_VALUE - AKERR_LAST_ERRNO_VALUE) > n);
|
regex_t re;
|
||||||
|
const char *pattern =
|
||||||
|
"^[[:space:]]*#define[[:space:]]+(AKERR_[A-Za-z0-9_]+)[[:space:]]+"
|
||||||
|
"\\(AKERR_LAST_ERRNO_VALUE[[:space:]]*\\+[[:space:]]*([0-9]+)\\)";
|
||||||
|
AKERR_CHECK(regcomp(&re, pattern, REG_EXTENDED) == 0);
|
||||||
|
|
||||||
/* Stronger: every defined code must fall within the addressable range so
|
int highest_code = -1; /* highest offset among real error codes */
|
||||||
* its name can be stored and retrieved. */
|
char highest_name[64] = "";
|
||||||
for ( int i = 0; i < n; i++ ) {
|
int max_err_value = -1; /* offset parsed from AKERR_MAX_ERR_VALUE */
|
||||||
AKERR_CHECK(akerr_codes[i] > AKERR_LAST_ERRNO_VALUE);
|
int code_count = 0;
|
||||||
AKERR_CHECK(akerr_codes[i] <= AKERR_MAX_ERR_VALUE);
|
|
||||||
|
char line[4096];
|
||||||
|
regmatch_t m[3];
|
||||||
|
while ( fgets(line, sizeof(line), fh) != NULL ) {
|
||||||
|
if ( regexec(&re, line, 3, m, 0) != 0 ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
char name[64];
|
||||||
|
int nlen = (int)(m[1].rm_eo - m[1].rm_so);
|
||||||
|
if ( nlen >= (int)sizeof(name) ) {
|
||||||
|
nlen = (int)sizeof(name) - 1;
|
||||||
|
}
|
||||||
|
memcpy(name, line + m[1].rm_so, nlen);
|
||||||
|
name[nlen] = '\0';
|
||||||
|
|
||||||
|
char num[16];
|
||||||
|
int vlen = (int)(m[2].rm_eo - m[2].rm_so);
|
||||||
|
if ( vlen >= (int)sizeof(num) ) {
|
||||||
|
vlen = (int)sizeof(num) - 1;
|
||||||
|
}
|
||||||
|
memcpy(num, line + m[2].rm_so, vlen);
|
||||||
|
num[vlen] = '\0';
|
||||||
|
int offset = atoi(num);
|
||||||
|
|
||||||
|
if ( strcmp(name, "AKERR_MAX_ERR_VALUE") == 0 ) {
|
||||||
|
max_err_value = offset;
|
||||||
|
} else {
|
||||||
|
code_count++;
|
||||||
|
if ( offset > highest_code ) {
|
||||||
|
highest_code = offset;
|
||||||
|
snprintf(highest_name, sizeof(highest_name), "%s", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
regfree(&re);
|
||||||
|
fclose(fh);
|
||||||
|
|
||||||
fprintf(stderr, "err_maxval ok (%d codes, range %d)\n",
|
/* We must have actually parsed both the codes and the ceiling. */
|
||||||
n, AKERR_MAX_ERR_VALUE - AKERR_LAST_ERRNO_VALUE);
|
AKERR_CHECK(code_count > 0);
|
||||||
|
AKERR_CHECK(highest_code > 0);
|
||||||
|
AKERR_CHECK(max_err_value > 0);
|
||||||
|
|
||||||
|
/* Guard against parsing a different header than the one compiled in. */
|
||||||
|
AKERR_CHECK(max_err_value == (AKERR_MAX_ERR_VALUE - AKERR_LAST_ERRNO_VALUE));
|
||||||
|
|
||||||
|
/* The actual invariant: every defined code is indexable in the names table. */
|
||||||
|
AKERR_CHECK(max_err_value >= highest_code);
|
||||||
|
|
||||||
|
fprintf(stderr,
|
||||||
|
"err_maxval ok (%d codes parsed, highest %s at +%d, max +%d)\n",
|
||||||
|
code_count, highest_name, highest_code, max_err_value);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user