Files
libakstdlib/tests/test_strto.c
Andrew Kesterson 55eb0334c4 Fix the six confirmed defects and close the API contract gaps
TODO.md section 2.1 recorded six defects reproduced against the built
library, and section 2.2 seventeen contract gaps. Both are closed. The
four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into
the tests for the things they test, and that list is now empty.

The defects:

  2.1.1  aksl_list_append conflated Floyd cycle detection with finding
         the tail, so `tail` tracked the node behind the midpoint. Any
         append to a list of 2+ nodes silently dropped everything after
         it. Two separate walks now: Floyd to prove the list is finite,
         then a plain walk to the end.
  2.1.2  aksl_list_iterate started visiting from Floyd's `slow` cursor,
         so the whole first half of the list -- head included -- was
         never passed to the callback. It starts at the head.
  2.1.3  AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame
         that raised it handled it and returned success, so the parent
         carried on into the sibling subtree. The recursion is split out
         and propagates the break; only the public entry swallows it.
  2.1.4  va_end now matches every va_start on every path.
  2.1.5  The ato* family had no error channel at all. Reimplemented over
         a new strto* family with errno cleared, an endptr check and a
         range check: AKERR_VALUE for junk, ERANGE for overflow.
  2.1.6  aksl_realpath never checked resolved_path, could not be told
         the buffer size, and formatted an unspecified buffer with %s on
         its own error path. It takes a length; aksl_realpath_alloc is
         the allocating form.

The contract gaps, in brief: errno is cleared before every wrapped call
and read back through a fallback so no error can carry status 0; fopen
validates pathname and mode; fread/fwrite report the transferred count
through a required out-param and no longer call a short transfer a
success; aksl_sprintf is gone in favour of aksl_snprintf, which treats
truncation as an error; the variadic wrappers carry format attributes;
djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded
and implements BFS, so lalloc/lfree are used rather than merely stored;
an unknown searchmode is AKERR_VALUE rather than silent success;
list_pop takes the head by reference; aksl_freep, the node initialisers
and extern "C" are new.

Build: -pg is out of the default build (it never reached the C compiler
anyway, and it is what produced the stray gmon.out), -Wall -Wextra are
in, and there is a .gitignore.

Tests: 11 binaries, all green under the normal and sanitizer builds.
Visit-order assertions replace the step counts that could not tell the
three depth-first orders apart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:14:11 -04:00

293 lines
10 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;
}
/* 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_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);
}