Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that
file to hold outstanding items only.
The API break gets a minor bump, because pre-1.0 the soname carries
MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five
signatures changed and the ato* contract with them; UPGRADING.md is new
and lists every one, with the before/after for the cases the compiler
cannot warn about.
Section 3.1 is finished: reallocarray with the multiplication checked,
aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf.
Four functions on that list are deliberately absent rather than missing
-- sprintf, strtok, setbuf and perror -- and TODO.md now says which and
why, so nobody adds them thinking they were forgotten.
Section 1.9, the cross-cutting tests:
tests/test_pool.c drives every failure path AKERR_MAX_ARRAY_ERROR
+ 10 times and checks the pool after each round,
because a wrapper that leaks a slot fails a
hundred calls later in unrelated code. It also
asserts that each error names the function and
file it was raised from, which is what catches a
FAIL that migrates into a helper during a
refactor: status right, message right, origin
quietly lying.
tests/negative/ two sources that must FAIL to compile, built with
-Werror and registered WILL_FAIL. AKERR_NOIGNORE
and the format attributes are enforced by the
compiler and by nothing else; drop either and
every ordinary test still passes.
Thread safety is answered rather than tested: the library is not
thread-safe and cannot be made so from here, because libakerror's error
pool is an unlocked process-global array. README.md says so plainly and
TODO.md carries it as the item blocking any future pthread wrappers.
Doxygen is configured and gated. All 147 public functions have @brief,
a @param each, @throws per status and @return; EXTRACT_ALL is off and
WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an
undocumented entity. It ran to 0 warnings. The Doxyfile carries no
version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from
project(), so that stays the one place a version is written.
CI now builds against the submodule it pins instead of also installing
libakerror@main and never linking it, adds -Werror, and gains a
sanitizer job. The pre-push hook matches, and runs the docs check too.
Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The
eight uncovered lines are each uncovered on purpose and TODO.md says
which and why.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
341 lines
12 KiB
C
341 lines
12 KiB
C
/*
|
|
* The strto* family -- TODO.md section 3.1.
|
|
*
|
|
* These are the real implementation behind the ato* wrappers and the thing
|
|
* akbasic had to hand-write for itself (its src/convert.c, ~60 lines) because
|
|
* the library would not do it. What they add over the ato* forms is a base and
|
|
* an endptr: pass a non-NULL endptr to parse a number off the front of a longer
|
|
* string and find out where it stopped, and the trailing-junk check goes away
|
|
* because the caller has taken responsibility for what follows.
|
|
*
|
|
* tests/test_convert.c covers what the two families share; this file is the part
|
|
* that is specific to having a base and an endptr.
|
|
*/
|
|
|
|
#include "aksl_capture.h"
|
|
|
|
#include <errno.h>
|
|
#include <limits.h>
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Base handling */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
static int test_base_is_honoured(void)
|
|
{
|
|
long out = 0;
|
|
|
|
AKSL_CHECK_OK(aksl_strtol("ff", NULL, 16, &out));
|
|
AKSL_CHECK(out == 255);
|
|
AKSL_CHECK_OK(aksl_strtol("1010", NULL, 2, &out));
|
|
AKSL_CHECK(out == 10);
|
|
AKSL_CHECK_OK(aksl_strtol("777", NULL, 8, &out));
|
|
AKSL_CHECK(out == 511);
|
|
AKSL_CHECK_OK(aksl_strtol("z", NULL, 36, &out));
|
|
AKSL_CHECK(out == 35);
|
|
return 0;
|
|
}
|
|
|
|
/* Base 0 auto-detects the 0x and 0 prefixes, which is the counterpart to
|
|
* tests/test_convert.c's test that aksl_atoi refuses "0x10". */
|
|
static int test_base_zero_detects_the_prefix(void)
|
|
{
|
|
long out = 0;
|
|
|
|
AKSL_CHECK_OK(aksl_strtol("0x10", NULL, 0, &out));
|
|
AKSL_CHECK(out == 16);
|
|
AKSL_CHECK_OK(aksl_strtol("010", NULL, 0, &out));
|
|
AKSL_CHECK(out == 8);
|
|
AKSL_CHECK_OK(aksl_strtol("10", NULL, 0, &out));
|
|
AKSL_CHECK(out == 10);
|
|
return 0;
|
|
}
|
|
|
|
/* A digit that does not belong to the base is where the number ends. */
|
|
static int test_digits_outside_the_base_end_the_number(void)
|
|
{
|
|
long out = 0;
|
|
char *end = NULL;
|
|
|
|
/* "9" is not an octal digit, so with a NULL endptr it is trailing junk. */
|
|
AKSL_CHECK_STATUS(aksl_strtol("129", NULL, 8, &out), AKERR_VALUE);
|
|
/* With an endptr the caller gets the partial parse and the stopping point. */
|
|
AKSL_CHECK_OK(aksl_strtol("129", &end, 8, &out));
|
|
AKSL_CHECK(out == 10); /* 012 octal */
|
|
AKSL_CHECK(end != NULL && *end == '9');
|
|
return 0;
|
|
}
|
|
|
|
/* An invalid base is EINVAL from strtol(3) rather than a silent answer. */
|
|
static int test_invalid_base_is_rejected(void)
|
|
{
|
|
long out = 99;
|
|
|
|
AKSL_CHECK_STATUS(aksl_strtol("10", NULL, 37, &out), EINVAL);
|
|
AKSL_CHECK(out == 0);
|
|
AKSL_CHECK_STATUS(aksl_strtol("10", NULL, 1, &out), EINVAL);
|
|
AKSL_CHECK(out == 0);
|
|
return 0;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* endptr */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
/*
|
|
* With an endptr, trailing text is the caller's business and not an error. This
|
|
* is the shape a tokenizer wants: parse a number, carry on from where it ended.
|
|
*/
|
|
static int test_endptr_reports_where_parsing_stopped(void)
|
|
{
|
|
const char *input = "42 rest of the line";
|
|
char *end = NULL;
|
|
long out = 0;
|
|
|
|
AKSL_CHECK_OK(aksl_strtol(input, &end, 10, &out));
|
|
AKSL_CHECK(out == 42);
|
|
AKSL_CHECK(end == input + 2);
|
|
AKSL_CHECK(strcmp(end, " rest of the line") == 0);
|
|
return 0;
|
|
}
|
|
|
|
/* Consuming nothing is still an error, endptr or not: there was no number. */
|
|
static int test_no_digits_is_an_error_even_with_an_endptr(void)
|
|
{
|
|
char *end = (char *)0x1;
|
|
long out = 99;
|
|
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strtol("nope", &end, 10, &out),
|
|
AKERR_VALUE, "no digits");
|
|
AKSL_CHECK(out == 0);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Every form writes through endptr, not just the two the other tests happen to
|
|
* use. Each type has its own function body, so "strtol handles endptr" says
|
|
* nothing whatsoever about strtoull.
|
|
*/
|
|
static int test_every_form_writes_the_endptr(void)
|
|
{
|
|
const char *input = "42rest";
|
|
char *end = NULL;
|
|
long lout = 0;
|
|
long long llout = 0;
|
|
unsigned long ulout = 0;
|
|
unsigned long long ullout = 0;
|
|
double dout = 0.0;
|
|
float fout = 0.0f;
|
|
long double ldout = 0.0L;
|
|
|
|
end = NULL;
|
|
AKSL_CHECK_OK(aksl_strtol(input, &end, 10, &lout));
|
|
AKSL_CHECK(lout == 42 && strcmp(end, "rest") == 0);
|
|
|
|
end = NULL;
|
|
AKSL_CHECK_OK(aksl_strtoll(input, &end, 10, &llout));
|
|
AKSL_CHECK(llout == 42 && strcmp(end, "rest") == 0);
|
|
|
|
end = NULL;
|
|
AKSL_CHECK_OK(aksl_strtoul(input, &end, 10, &ulout));
|
|
AKSL_CHECK(ulout == 42 && strcmp(end, "rest") == 0);
|
|
|
|
end = NULL;
|
|
AKSL_CHECK_OK(aksl_strtoull(input, &end, 10, &ullout));
|
|
AKSL_CHECK(ullout == 42 && strcmp(end, "rest") == 0);
|
|
|
|
end = NULL;
|
|
AKSL_CHECK_OK(aksl_strtod(input, &end, &dout));
|
|
AKSL_CHECK(dout == 42.0 && strcmp(end, "rest") == 0);
|
|
|
|
end = NULL;
|
|
AKSL_CHECK_OK(aksl_strtof(input, &end, &fout));
|
|
AKSL_CHECK(fout == 42.0f && strcmp(end, "rest") == 0);
|
|
|
|
end = NULL;
|
|
AKSL_CHECK_OK(aksl_strtold(input, &end, &ldout));
|
|
AKSL_CHECK(ldout == 42.0L && strcmp(end, "rest") == 0);
|
|
return 0;
|
|
}
|
|
|
|
/* Successive calls chained through endptr walk a whole list of numbers. */
|
|
static int test_endptr_chains_across_a_list(void)
|
|
{
|
|
const char *input = "1,2,3";
|
|
char *cursor = (char *)input;
|
|
long out = 0;
|
|
long total = 0;
|
|
int seen = 0;
|
|
|
|
while ( *cursor != '\0' ) {
|
|
AKSL_CHECK_OK(aksl_strtol(cursor, &cursor, 10, &out));
|
|
total += out;
|
|
seen += 1;
|
|
if ( *cursor == ',' ) {
|
|
cursor++;
|
|
}
|
|
}
|
|
AKSL_CHECK(seen == 3);
|
|
AKSL_CHECK(total == 6);
|
|
return 0;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Ranges and signs */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
static int test_range_errors_per_type(void)
|
|
{
|
|
long lout = 99;
|
|
long long llout = 99;
|
|
unsigned long ulout = 99;
|
|
unsigned long long ullout = 99;
|
|
char buf[64];
|
|
|
|
snprintf(buf, sizeof(buf), "%lld9", (long long)LLONG_MAX);
|
|
AKSL_CHECK_STATUS(aksl_strtol(buf, NULL, 10, &lout), ERANGE);
|
|
AKSL_CHECK(lout == 0);
|
|
AKSL_CHECK_STATUS(aksl_strtoll(buf, NULL, 10, &llout), ERANGE);
|
|
AKSL_CHECK(llout == 0);
|
|
|
|
snprintf(buf, sizeof(buf), "%llu9", (unsigned long long)ULLONG_MAX);
|
|
AKSL_CHECK_STATUS(aksl_strtoul(buf, NULL, 10, &ulout), ERANGE);
|
|
AKSL_CHECK(ulout == 0);
|
|
AKSL_CHECK_STATUS(aksl_strtoull(buf, NULL, 10, &ullout), ERANGE);
|
|
AKSL_CHECK(ullout == 0);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* strtoul(3) and strtoull(3) accept a leading '-' and hand back the negation
|
|
* wrapped into the unsigned range, so "-1" parses as ULONG_MAX and reports
|
|
* nothing at all. That is exactly the silent failure this library exists to
|
|
* surface, so the sign is refused before the call.
|
|
*/
|
|
static int test_unsigned_forms_refuse_a_negative(void)
|
|
{
|
|
unsigned long ulout = 99;
|
|
unsigned long long ullout = 99;
|
|
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strtoul("-1", NULL, 10, &ulout),
|
|
AKERR_VALUE, "negative");
|
|
AKSL_CHECK(ulout == 0);
|
|
/* Including behind leading whitespace, which strtoul skips. */
|
|
AKSL_CHECK_STATUS(aksl_strtoul(" -1", NULL, 10, &ulout), AKERR_VALUE);
|
|
AKSL_CHECK_STATUS(aksl_strtoull("-1", NULL, 10, &ullout), AKERR_VALUE);
|
|
AKSL_CHECK(ullout == 0);
|
|
|
|
/* A leading '+' is fine: it is a sign, not a negation. */
|
|
AKSL_CHECK_OK(aksl_strtoul("+7", NULL, 10, &ulout));
|
|
AKSL_CHECK(ulout == 7);
|
|
return 0;
|
|
}
|
|
|
|
static int test_unsigned_boundaries_round_trip(void)
|
|
{
|
|
unsigned long ulout = 0;
|
|
unsigned long long ullout = 0;
|
|
char buf[64];
|
|
|
|
snprintf(buf, sizeof(buf), "%lu", ULONG_MAX);
|
|
AKSL_CHECK_OK(aksl_strtoul(buf, NULL, 10, &ulout));
|
|
AKSL_CHECK(ulout == ULONG_MAX);
|
|
|
|
snprintf(buf, sizeof(buf), "%llu", ULLONG_MAX);
|
|
AKSL_CHECK_OK(aksl_strtoull(buf, NULL, 10, &ullout));
|
|
AKSL_CHECK(ullout == ULLONG_MAX);
|
|
return 0;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Floating point */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
static int test_floating_point_forms(void)
|
|
{
|
|
double dout = 0.0;
|
|
float fout = 0.0f;
|
|
long double ldout = 0.0L;
|
|
char *end = NULL;
|
|
|
|
AKSL_CHECK_OK(aksl_strtod("2.5", NULL, &dout));
|
|
AKSL_CHECK(dout == 2.5);
|
|
AKSL_CHECK_OK(aksl_strtof("2.5", NULL, &fout));
|
|
AKSL_CHECK(fout == 2.5f);
|
|
AKSL_CHECK_OK(aksl_strtold("2.5", NULL, &ldout));
|
|
AKSL_CHECK(ldout == 2.5L);
|
|
|
|
AKSL_CHECK_OK(aksl_strtod("2.5rest", &end, &dout));
|
|
AKSL_CHECK(dout == 2.5);
|
|
AKSL_CHECK(strcmp(end, "rest") == 0);
|
|
|
|
AKSL_CHECK_STATUS(aksl_strtod("2.5rest", NULL, &dout), AKERR_VALUE);
|
|
AKSL_CHECK_STATUS(aksl_strtof("nope", NULL, &fout), AKERR_VALUE);
|
|
AKSL_CHECK_STATUS(aksl_strtold("nope", NULL, &ldout), AKERR_VALUE);
|
|
|
|
/* float has a much smaller range than double, and says so. */
|
|
AKSL_CHECK_STATUS(aksl_strtof("1e300", NULL, &fout), ERANGE);
|
|
AKSL_CHECK(fout == 0.0f);
|
|
AKSL_CHECK_OK(aksl_strtod("1e300", NULL, &dout));
|
|
AKSL_CHECK(dout > 0.0);
|
|
return 0;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Argument validation */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
static int test_every_form_rejects_null_arguments(void)
|
|
{
|
|
long lout = 0;
|
|
long long llout = 0;
|
|
unsigned long ulout = 0;
|
|
unsigned long long ullout = 0;
|
|
double dout = 0.0;
|
|
float fout = 0.0f;
|
|
long double ldout = 0.0L;
|
|
|
|
AKSL_CHECK_STATUS(aksl_strtol(NULL, NULL, 10, &lout), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtol("1", NULL, 10, NULL), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtoll(NULL, NULL, 10, &llout), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtoll("1", NULL, 10, NULL), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtoul(NULL, NULL, 10, &ulout), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtoul("1", NULL, 10, NULL), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtoull(NULL, NULL, 10, &ullout), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtoull("1", NULL, 10, NULL), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtod(NULL, NULL, &dout), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtod("1", NULL, NULL), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtof(NULL, NULL, &fout), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtof("1", NULL, NULL), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtold(NULL, NULL, &ldout), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtold("1", NULL, NULL), AKERR_NULLPOINTER);
|
|
return 0;
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
int failures = 0;
|
|
|
|
akerr_init();
|
|
|
|
AKSL_RUN(failures, test_base_is_honoured);
|
|
AKSL_RUN(failures, test_base_zero_detects_the_prefix);
|
|
AKSL_RUN(failures, test_digits_outside_the_base_end_the_number);
|
|
AKSL_RUN(failures, test_invalid_base_is_rejected);
|
|
|
|
AKSL_RUN(failures, test_endptr_reports_where_parsing_stopped);
|
|
AKSL_RUN(failures, test_every_form_writes_the_endptr);
|
|
AKSL_RUN(failures, test_no_digits_is_an_error_even_with_an_endptr);
|
|
AKSL_RUN(failures, test_endptr_chains_across_a_list);
|
|
|
|
AKSL_RUN(failures, test_range_errors_per_type);
|
|
AKSL_RUN(failures, test_unsigned_forms_refuse_a_negative);
|
|
AKSL_RUN(failures, test_unsigned_boundaries_round_trip);
|
|
|
|
AKSL_RUN(failures, test_floating_point_forms);
|
|
|
|
AKSL_RUN(failures, test_every_form_rejects_null_arguments);
|
|
|
|
AKSL_REPORT(failures);
|
|
}
|