All checks were successful
Eighty-five comments cited section numbers -- 1.1, 2.2.6, 3.6 -- from a numbering the file had already abandoned before the move to the tracker. They label completed work, so the pointer was the only wrong part. The citation is removed and the sentence kept, which is what issue #27 recommended: these are labels, not references, and a label carrying a version-dependent pointer goes stale again at the next reorganisation. Where a pointer earns its place it names what actually holds the content now -- UPGRADING.md for the confirmed defects, libakerror #15 for the target namespacing, issue #7 for the mutation survivors. README.md and akstdlib.h sent readers to TODO.md for 'what is still open'; they name the tracker. Verified: cmake --build build && ctest --test-dir build, 19/19. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
341 lines
12 KiB
C
341 lines
12 KiB
C
/*
|
|
* The strto* family.
|
|
*
|
|
* 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);
|
|
}
|