Files
libakstdlib/tests/test_convert.c
Tachikoma 55d986c631
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 5m53s
libakstdlib CI Build / sanitizers (push) Successful in 2m55s
libakstdlib CI Build / coverage (push) Successful in 2m48s
libakstdlib CI Build / mutation_test (push) Successful in 12m39s
Drop the TODO.md section numbers from 26 files
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>
2026-08-02 22:01:27 -04:00

325 lines
9.7 KiB
C

/*
* String -> number wrappers: aksl_atoi / atol / atoll / atof.
*
* Numeric conversion, complete. The cases that used to live in
* tests/test_convert_strict.c -- registered as a known failure because the ato*
* family had no error channel at all (2.1.5) -- are folded back in here now that
* they pass: non-numeric input, empty input, trailing junk and overflow are all
* errors rather than silent wrong answers.
*
* The strto* family these are built on is tested in tests/test_strto.c.
*/
#include "aksl_capture.h"
#include <errno.h>
#include <limits.h>
static int test_atoi_converts_positive(void)
{
int out = -1;
AKSL_CHECK_OK(aksl_atoi("1234", &out));
AKSL_CHECK(out == 1234);
return 0;
}
static int test_atoi_converts_negative(void)
{
int out = 0;
AKSL_CHECK_OK(aksl_atoi("-42", &out));
AKSL_CHECK(out == -42);
return 0;
}
static int test_atoi_skips_leading_whitespace(void)
{
int out = 0;
AKSL_CHECK_OK(aksl_atoi(" \t 7", &out));
AKSL_CHECK(out == 7);
return 0;
}
static int test_atoi_rejects_null_arguments(void)
{
int out = 0;
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_atoi(NULL, &out),
AKERR_NULLPOINTER, "nptr=");
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_atoi("1", NULL),
AKERR_NULLPOINTER, "dest=");
return 0;
}
/*
* The whole reason 2.1.5 mattered. atoi("not a number") returned success and 0,
* so a caller could not tell a parse failure from a legitimate zero -- and
* akbasic had to write ~60 lines of its own strtoll/strtod wrapper to avoid
* turning four diagnosable BASIC errors into four wrong answers.
*/
static int test_non_numeric_input_is_a_value_error(void)
{
int out = 99;
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_atoi("not a number", &out),
AKERR_VALUE, "no digits");
/* *dest is 0 on every failure path, never left holding a partial answer. */
AKSL_CHECK(out == 0);
return 0;
}
static int test_empty_input_is_a_value_error(void)
{
int out = 99;
AKSL_CHECK_STATUS(aksl_atoi("", &out), AKERR_VALUE);
AKSL_CHECK(out == 0);
AKSL_CHECK_STATUS(aksl_atoi(" ", &out), AKERR_VALUE);
AKSL_CHECK(out == 0);
return 0;
}
static int test_trailing_junk_is_a_value_error(void)
{
int out = 99;
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_atoi("12abc", &out),
AKERR_VALUE, "trailing junk");
AKSL_CHECK(out == 0);
/* Trailing whitespace counts as junk too: the whole string must be a number. */
AKSL_CHECK_STATUS(aksl_atoi("12 ", &out), AKERR_VALUE);
return 0;
}
/*
* "0x10" through the ato* forms is base 10, so parsing stops at the 'x' and the
* rest is trailing junk. The wrapper plan left this open; the answer is that the
* prefix-honouring parse is aksl_strtol(nptr, NULL, 0, &dest), and tests/
* test_strto.c holds that half.
*/
static int test_hex_literal_is_rejected_by_the_base_10_forms(void)
{
int out = 99;
AKSL_CHECK_STATUS(aksl_atoi("0x10", &out), AKERR_VALUE);
AKSL_CHECK(out == 0);
return 0;
}
static int test_overflow_is_erange(void)
{
int out = 99;
long lout = 99;
long long llout = 99;
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_atoi("99999999999999999999", &out),
ERANGE, "out of range");
AKSL_CHECK(out == 0);
AKSL_CHECK_STATUS(aksl_atol("99999999999999999999999999", &lout), ERANGE);
AKSL_CHECK(lout == 0);
AKSL_CHECK_STATUS(aksl_atoll("99999999999999999999999999", &llout), ERANGE);
AKSL_CHECK(llout == 0);
return 0;
}
/*
* aksl_atoi narrows from long to int, so a value that fits a long and not an int
* is ERANGE from the wrapper rather than from strtol. On a platform where long
* and int are the same width there is nothing to narrow and strtol has already
* caught it, so the case is skipped rather than asserted into a false pass.
*/
static int test_atoi_narrows_to_int_range(void)
{
#if LONG_MAX > INT_MAX
int out = 99;
char buf[64];
snprintf(buf, sizeof(buf), "%ld", (long)INT_MAX + 1);
AKSL_CHECK_STATUS(aksl_atoi(buf, &out), ERANGE);
AKSL_CHECK(out == 0);
snprintf(buf, sizeof(buf), "%ld", (long)INT_MIN - 1);
AKSL_CHECK_STATUS(aksl_atoi(buf, &out), ERANGE);
AKSL_CHECK(out == 0);
#else
fprintf(stderr, " (skipped: long and int are the same width here)\n");
#endif
return 0;
}
/* The type boundaries round-trip exactly rather than nearly. */
static int test_type_boundaries_round_trip(void)
{
char buf[64];
int iout = 0;
long lout = 0;
long long llout = 0;
snprintf(buf, sizeof(buf), "%d", INT_MAX);
AKSL_CHECK_OK(aksl_atoi(buf, &iout));
AKSL_CHECK(iout == INT_MAX);
snprintf(buf, sizeof(buf), "%d", INT_MIN);
AKSL_CHECK_OK(aksl_atoi(buf, &iout));
AKSL_CHECK(iout == INT_MIN);
snprintf(buf, sizeof(buf), "%ld", LONG_MAX);
AKSL_CHECK_OK(aksl_atol(buf, &lout));
AKSL_CHECK(lout == LONG_MAX);
snprintf(buf, sizeof(buf), "%ld", LONG_MIN);
AKSL_CHECK_OK(aksl_atol(buf, &lout));
AKSL_CHECK(lout == LONG_MIN);
snprintf(buf, sizeof(buf), "%lld", LLONG_MAX);
AKSL_CHECK_OK(aksl_atoll(buf, &llout));
AKSL_CHECK(llout == LLONG_MAX);
snprintf(buf, sizeof(buf), "%lld", LLONG_MIN);
AKSL_CHECK_OK(aksl_atoll(buf, &llout));
AKSL_CHECK(llout == LLONG_MIN);
return 0;
}
static int test_atol_converts_and_rejects_null(void)
{
long out = 0;
AKSL_CHECK_OK(aksl_atol("2147483648", &out));
AKSL_CHECK(out == 2147483648L);
AKSL_CHECK_OK(aksl_atol("-2147483648", &out));
AKSL_CHECK(out == -2147483648L);
AKSL_CHECK_STATUS(aksl_atol(NULL, &out), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_atol("1", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_atol("nope", &out), AKERR_VALUE);
return 0;
}
static int test_atoll_converts_and_rejects_null(void)
{
long long out = 0;
AKSL_CHECK_OK(aksl_atoll("9007199254740993", &out));
AKSL_CHECK(out == 9007199254740993LL);
AKSL_CHECK_OK(aksl_atoll("-9007199254740993", &out));
AKSL_CHECK(out == -9007199254740993LL);
AKSL_CHECK_STATUS(aksl_atoll(NULL, &out), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_atoll("1", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_atoll("nope", &out), AKERR_VALUE);
return 0;
}
static int test_atof_converts_and_rejects_null(void)
{
double out = 0.0;
AKSL_CHECK_OK(aksl_atof("2.5", &out));
AKSL_CHECK(out == 2.5);
AKSL_CHECK_OK(aksl_atof(" -0.125", &out));
AKSL_CHECK(out == -0.125);
AKSL_CHECK_OK(aksl_atof("1e3", &out));
AKSL_CHECK(out == 1000.0);
AKSL_CHECK_STATUS(aksl_atof(NULL, &out), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_atof("1", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_atof("nope", &out), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_atof("1.5xyz", &out), AKERR_VALUE);
return 0;
}
/*
* The wrapper plan left "inf" and "nan" open. They are accepted, because strtod(3)
* accepts them and because they are exact round-trips rather than approximations
* of something else -- a caller who did not want them has a domain check to make
* that this library cannot make for it.
*/
static int test_atof_accepts_infinity_and_nan(void)
{
double out = 0.0;
AKSL_CHECK_OK(aksl_atof("inf", &out));
AKSL_CHECK(out > 0.0 && (out * 2.0) == out);
AKSL_CHECK_OK(aksl_atof("-INFINITY", &out));
AKSL_CHECK(out < 0.0 && (out * 2.0) == out);
AKSL_CHECK_OK(aksl_atof("nan", &out));
/* The only value that is not equal to itself. */
AKSL_CHECK(out != out);
return 0;
}
/* Overflow and underflow both come back as ERANGE, which is strtod's only signal. */
static int test_atof_range_errors(void)
{
double out = 1.0;
AKSL_CHECK_STATUS(aksl_atof("1e400", &out), ERANGE);
AKSL_CHECK(out == 0.0);
AKSL_CHECK_STATUS(aksl_atof("1e-400", &out), ERANGE);
AKSL_CHECK(out == 0.0);
return 0;
}
/* The wrappers hold no state: the same input converts the same way twice. */
static int test_conversions_are_repeatable(void)
{
int first = 0;
int second = 0;
AKSL_CHECK_OK(aksl_atoi("321", &first));
AKSL_CHECK_OK(aksl_atoi("321", &second));
AKSL_CHECK(first == second);
AKSL_CHECK(first == 321);
return 0;
}
/*
* errno is cleared before each wrapped call, so a failure left over from an
* earlier unrelated call cannot be reported as this one's. Set errno to
* something conspicuous first and check that a successful conversion still
* succeeds -- the old code could report a stale errno as its own status.
*/
static int test_stale_errno_does_not_leak_into_the_result(void)
{
int out = 0;
errno = ERANGE;
AKSL_CHECK_OK(aksl_atoi("5", &out));
AKSL_CHECK(out == 5);
errno = ENOMEM;
AKSL_CHECK_STATUS(aksl_atoi("junk", &out), AKERR_VALUE);
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_atoi_converts_positive);
AKSL_RUN(failures, test_atoi_converts_negative);
AKSL_RUN(failures, test_atoi_skips_leading_whitespace);
AKSL_RUN(failures, test_atoi_rejects_null_arguments);
AKSL_RUN(failures, test_non_numeric_input_is_a_value_error);
AKSL_RUN(failures, test_empty_input_is_a_value_error);
AKSL_RUN(failures, test_trailing_junk_is_a_value_error);
AKSL_RUN(failures, test_hex_literal_is_rejected_by_the_base_10_forms);
AKSL_RUN(failures, test_overflow_is_erange);
AKSL_RUN(failures, test_atoi_narrows_to_int_range);
AKSL_RUN(failures, test_type_boundaries_round_trip);
AKSL_RUN(failures, test_atol_converts_and_rejects_null);
AKSL_RUN(failures, test_atoll_converts_and_rejects_null);
AKSL_RUN(failures, test_atof_converts_and_rejects_null);
AKSL_RUN(failures, test_atof_accepts_infinity_and_nan);
AKSL_RUN(failures, test_atof_range_errors);
AKSL_RUN(failures, test_conversions_are_repeatable);
AKSL_RUN(failures, test_stale_errno_does_not_leak_into_the_result);
AKSL_REPORT(failures);
}