diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a0547a..2c90405 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,6 +81,11 @@ foreach(_test IN LISTS AKERR_TESTS) add_test(NAME ${_test} COMMAND test_${_test}) 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( ${AKERR_WILL_FAIL_TESTS} PROPERTIES WILL_FAIL TRUE diff --git a/tests/err_maxval.c b/tests/err_maxval.c index 0f3aaff..1dcdcaa 100644 --- a/tests/err_maxval.c +++ b/tests/err_maxval.c @@ -1,52 +1,92 @@ #include "akerror.h" #include "err_capture.h" +#include /* * 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 * +15 while AKERR_BADEXC is +17). * - * Guard the invariant: the AKERR_* range reserved above AKERR_LAST_ERRNO_VALUE - * must be larger than the number of AKERR_* codes defined, and every code must - * be individually indexable. + * Rather than hardcode the list of codes (which rots the moment someone adds a + * code), this test parses the generated akerror.h, discovers every + * #define AKERR_ (AKERR_LAST_ERRNO_VALUE + ) + * 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. */ -static const int akerr_codes[] = { - AKERR_NULLPOINTER, - 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, -}; +#ifndef AKERR_GENERATED_HEADER +#error "AKERR_GENERATED_HEADER (path to generated akerror.h) must be defined" +#endif 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. */ - AKERR_CHECK((AKERR_MAX_ERR_VALUE - AKERR_LAST_ERRNO_VALUE) > n); + /* #define AKERR_NAME (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 - * its name can be stored and retrieved. */ - for ( int i = 0; i < n; i++ ) { - AKERR_CHECK(akerr_codes[i] > AKERR_LAST_ERRNO_VALUE); - AKERR_CHECK(akerr_codes[i] <= AKERR_MAX_ERR_VALUE); + int highest_code = -1; /* highest offset among real error codes */ + char highest_name[64] = ""; + int max_err_value = -1; /* offset parsed from AKERR_MAX_ERR_VALUE */ + int code_count = 0; + + 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", - n, AKERR_MAX_ERR_VALUE - AKERR_LAST_ERRNO_VALUE); + /* We must have actually parsed both the codes and the ceiling. */ + 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; }