Files
libakstdlib/tests/test_strto.c
Andrew Kesterson 125eeb2109 Version at 0.2.0: complete the wishlist, document it, gate the docs
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>
2026-07-31 08:00:16 -04:00

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);
}