Files
libakstdlib/tests/test_string.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

449 lines
16 KiB
C

/*
* String wrappers -- src/string.c.
*
* The two contracts worth testing hardest are the ones that differ from libc:
* every copying function takes the destination size and treats truncation as an
* error that writes nothing, and every searching function treats "not found" as
* a successful answer of NULL.
*/
#include "aksl_capture.h"
#include <errno.h>
/* ---------------------------------------------------------------------- */
/* Length */
/* ---------------------------------------------------------------------- */
static int test_strlen_and_strnlen(void)
{
size_t n = 99;
AKSL_CHECK_OK(aksl_strlen("hello", &n));
AKSL_CHECK(n == 5);
AKSL_CHECK_OK(aksl_strlen("", &n));
AKSL_CHECK(n == 0);
/* strnlen stops at maxlen whether or not it found a terminator. */
AKSL_CHECK_OK(aksl_strnlen("hello", 3, &n));
AKSL_CHECK(n == 3);
AKSL_CHECK_OK(aksl_strnlen("hello", 99, &n));
AKSL_CHECK(n == 5);
AKSL_CHECK_STATUS(aksl_strlen(NULL, &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strlen("x", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strnlen(NULL, 1, &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strnlen("x", 1, NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Copying */
/* ---------------------------------------------------------------------- */
static int test_strcpy_copies_within_the_buffer(void)
{
char buf[8];
AKSL_CHECK_OK(aksl_strcpy(buf, sizeof(buf), "abc"));
AKSL_CHECK(strcmp(buf, "abc") == 0);
/* Exactly filling the buffer, terminator included, is not truncation. */
AKSL_CHECK_OK(aksl_strcpy(buf, sizeof(buf), "1234567"));
AKSL_CHECK(strcmp(buf, "1234567") == 0);
return 0;
}
/*
* The whole point of taking dstsize. akbasic writes this check by hand at ten
* sites: a length test, then strncpy, then an explicit NUL.
*/
static int test_strcpy_truncation_is_an_error_and_writes_nothing(void)
{
char buf[8] = "previous";
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strcpy(buf, sizeof(buf), "12345678"),
AKERR_OUTOFBOUNDS, "destination holds");
/*
* Empty, not a truncated prefix. A caller who ignores the status gets
* nothing, which is far easier to notice than a plausible-looking "1234567".
*/
AKSL_CHECK(buf[0] == '\0');
return 0;
}
/*
* strncpy(3) leaves the destination unterminated when the source is at least n
* bytes, and NUL-pads the whole remainder when it is shorter. This does neither.
*/
static int test_strncpy_always_terminates_and_never_pads(void)
{
char buf[16];
memset(buf, 'Z', sizeof(buf));
AKSL_CHECK_OK(aksl_strncpy(buf, sizeof(buf), "abcdef", 3));
AKSL_CHECK(strcmp(buf, "abc") == 0);
/* Not padded: everything past the terminator is untouched. */
AKSL_CHECK(buf[4] == 'Z');
/* n larger than the source just copies the source. */
AKSL_CHECK_OK(aksl_strncpy(buf, sizeof(buf), "abc", 99));
AKSL_CHECK(strcmp(buf, "abc") == 0);
/* n bytes that do not fit the destination is still an error. */
AKSL_CHECK_STATUS(aksl_strncpy(buf, 4, "abcdef", 6), AKERR_OUTOFBOUNDS);
AKSL_CHECK(buf[0] == '\0');
return 0;
}
static int test_strcat_appends_within_the_buffer(void)
{
char buf[16] = "ab";
AKSL_CHECK_OK(aksl_strcat(buf, sizeof(buf), "cd"));
AKSL_CHECK(strcmp(buf, "abcd") == 0);
AKSL_CHECK_OK(aksl_strcat(buf, sizeof(buf), ""));
AKSL_CHECK(strcmp(buf, "abcd") == 0);
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strcat(buf, sizeof(buf), "0123456789ab"),
AKERR_OUTOFBOUNDS, "in use plus");
/* The existing contents survive a refused append. */
AKSL_CHECK(strcmp(buf, "abcd") == 0);
return 0;
}
/* An unterminated destination is refused rather than walked off the end of. */
static int test_strcat_refuses_an_unterminated_destination(void)
{
char buf[4];
memset(buf, 'x', sizeof(buf));
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strcat(buf, sizeof(buf), "y"),
AKERR_VALUE, "not terminated");
return 0;
}
static int test_strncat_appends_at_most_n(void)
{
char buf[16] = "ab";
AKSL_CHECK_OK(aksl_strncat(buf, sizeof(buf), "cdef", 2));
AKSL_CHECK(strcmp(buf, "abcd") == 0);
AKSL_CHECK_STATUS(aksl_strncat(buf, 6, "xyz", 3), AKERR_OUTOFBOUNDS);
AKSL_CHECK(strcmp(buf, "abcd") == 0);
return 0;
}
static int test_copying_rejects_null_and_zero_size(void)
{
char buf[8] = "";
AKSL_CHECK_STATUS(aksl_strcpy(NULL, 8, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcpy(buf, sizeof(buf), NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcpy(buf, 0, "x"), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_strncpy(NULL, 8, "x", 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncpy(buf, sizeof(buf), NULL, 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncpy(buf, 0, "x", 1), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_strcat(NULL, 8, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcat(buf, sizeof(buf), NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcat(buf, 0, "x"), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_strncat(NULL, 8, "x", 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncat(buf, sizeof(buf), NULL, 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncat(buf, 0, "x", 1), AKERR_VALUE);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Duplication */
/* ---------------------------------------------------------------------- */
static int test_strdup_and_strndup(void)
{
char *copy = NULL;
AKSL_CHECK_OK(aksl_strdup("duplicate me", &copy));
AKSL_CHECK(copy != NULL);
AKSL_CHECK(strcmp(copy, "duplicate me") == 0);
AKSL_CHECK_OK(aksl_freep((void **)&copy));
AKSL_CHECK(copy == NULL);
AKSL_CHECK_OK(aksl_strndup("duplicate me", 9, &copy));
AKSL_CHECK(strcmp(copy, "duplicate") == 0);
AKSL_CHECK_OK(aksl_freep((void **)&copy));
/* n past the end of the string just copies the string. */
AKSL_CHECK_OK(aksl_strndup("ab", 99, &copy));
AKSL_CHECK(strcmp(copy, "ab") == 0);
AKSL_CHECK_OK(aksl_freep((void **)&copy));
AKSL_CHECK_STATUS(aksl_strdup(NULL, &copy), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strdup("x", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strndup(NULL, 1, &copy), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strndup("x", 1, NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Comparison */
/* ---------------------------------------------------------------------- */
static int test_comparisons(void)
{
int r = 99;
AKSL_CHECK_OK(aksl_strcmp("abc", "abc", &r));
AKSL_CHECK(r == 0);
AKSL_CHECK_OK(aksl_strcmp("abc", "abd", &r));
AKSL_CHECK(r < 0);
AKSL_CHECK_OK(aksl_strcmp("abd", "abc", &r));
AKSL_CHECK(r > 0);
AKSL_CHECK_OK(aksl_strncmp("abcXX", "abcYY", 3, &r));
AKSL_CHECK(r == 0);
AKSL_CHECK_OK(aksl_strncmp("abcXX", "abcYY", 4, &r));
AKSL_CHECK(r != 0);
/* The three case-folding sites akbasic writes out by hand. */
AKSL_CHECK_OK(aksl_strcasecmp("PRINT", "print", &r));
AKSL_CHECK(r == 0);
AKSL_CHECK_OK(aksl_strcasecmp("PRINT", "input", &r));
AKSL_CHECK(r != 0);
AKSL_CHECK_OK(aksl_strncasecmp("PRINTXX", "printYY", 5, &r));
AKSL_CHECK(r == 0);
AKSL_CHECK_OK(aksl_strcoll("abc", "abc", &r));
AKSL_CHECK(r == 0);
AKSL_CHECK_STATUS(aksl_strcmp(NULL, "b", &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcmp("a", NULL, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcmp("a", "b", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncmp(NULL, "b", 1, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncmp("a", NULL, 1, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncmp("a", "b", 1, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcasecmp(NULL, "b", &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcasecmp("a", NULL, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcasecmp("a", "b", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncasecmp(NULL, "b", 1, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncasecmp("a", NULL, 1, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncasecmp("a", "b", 1, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcoll(NULL, "b", &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcoll("a", NULL, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcoll("a", "b", NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Searching */
/* ---------------------------------------------------------------------- */
/*
* Every one of these treats absence as an answer. A library that raised on it
* would have every caller catching a non-error, and burning a pool slot to do
* so.
*/
static int test_searching_reports_absence_as_success(void)
{
const char *s = "the quick brown fox";
char *at = (char *)0x1;
size_t n = 99;
AKSL_CHECK_OK(aksl_strchr(s, 'q', &at));
AKSL_CHECK(at == s + 4);
AKSL_CHECK_OK(aksl_strchr(s, 'Z', &at));
AKSL_CHECK(at == NULL);
AKSL_CHECK_OK(aksl_strrchr(s, 'o', &at));
AKSL_CHECK(at == s + 17);
AKSL_CHECK_OK(aksl_strrchr(s, 'Z', &at));
AKSL_CHECK(at == NULL);
AKSL_CHECK_OK(aksl_strstr(s, "brown", &at));
AKSL_CHECK(at == s + 10);
AKSL_CHECK_OK(aksl_strstr(s, "purple", &at));
AKSL_CHECK(at == NULL);
AKSL_CHECK_OK(aksl_strpbrk(s, "xq", &at));
AKSL_CHECK(at == s + 4);
AKSL_CHECK_OK(aksl_strpbrk(s, "ZY", &at));
AKSL_CHECK(at == NULL);
AKSL_CHECK_OK(aksl_strspn("aaabbb", "a", &n));
AKSL_CHECK(n == 3);
AKSL_CHECK_OK(aksl_strcspn("aaabbb", "b", &n));
AKSL_CHECK(n == 3);
return 0;
}
/* The open-coded case-insensitive search, including its edge cases. */
static int test_strcasestr(void)
{
const char *s = "The Quick Brown Fox";
char *at = (char *)0x1;
AKSL_CHECK_OK(aksl_strcasestr(s, "quick", &at));
AKSL_CHECK(at == s + 4);
AKSL_CHECK_OK(aksl_strcasestr(s, "QUICK", &at));
AKSL_CHECK(at == s + 4);
AKSL_CHECK_OK(aksl_strcasestr(s, "purple", &at));
AKSL_CHECK(at == NULL);
/* An empty needle matches at the start, as strstr(3) has it. */
AKSL_CHECK_OK(aksl_strcasestr(s, "", &at));
AKSL_CHECK(at == s);
/* A needle longer than the haystack cannot match, and must not read past. */
AKSL_CHECK_OK(aksl_strcasestr("ab", "abcdef", &at));
AKSL_CHECK(at == NULL);
/* A match right at the end. */
AKSL_CHECK_OK(aksl_strcasestr(s, "fox", &at));
AKSL_CHECK(at == s + 16);
return 0;
}
static int test_searching_rejects_null_arguments(void)
{
char *at = NULL;
size_t n = 0;
AKSL_CHECK_STATUS(aksl_strchr(NULL, 'a', &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strchr("a", 'a', NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strrchr(NULL, 'a', &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strrchr("a", 'a', NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strstr(NULL, "a", &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strstr("a", NULL, &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strstr("a", "a", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcasestr(NULL, "a", &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcasestr("a", NULL, &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcasestr("a", "a", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strpbrk(NULL, "a", &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strpbrk("a", NULL, &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strpbrk("a", "a", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strspn(NULL, "a", &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strspn("a", NULL, &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strspn("a", "a", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcspn(NULL, "a", &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcspn("a", NULL, &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcspn("a", "a", NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Tokenising */
/* ---------------------------------------------------------------------- */
static int test_strtok_r_walks_the_tokens(void)
{
char input[] = "one,two,,three";
char *save = NULL;
char *tok = NULL;
int seen = 0;
AKSL_CHECK_OK(aksl_strtok_r(input, ",", &save, &tok));
AKSL_CHECK(tok != NULL && strcmp(tok, "one") == 0);
seen++;
while ( 1 ) {
AKSL_CHECK_OK(aksl_strtok_r(NULL, ",", &save, &tok));
if ( tok == NULL ) {
break;
}
seen++;
}
/* strtok_r collapses the empty field between the two commas. */
AKSL_CHECK(seen == 3);
AKSL_CHECK_STATUS(aksl_strtok_r(NULL, NULL, &save, &tok), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strtok_r(NULL, ",", NULL, &tok), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strtok_r(NULL, ",", &save, NULL), AKERR_NULLPOINTER);
return 0;
}
/* strsep keeps the empty fields, which is why it exists alongside strtok_r. */
static int test_strsep_keeps_empty_fields(void)
{
char input[] = "one,two,,three";
char *cursor = input;
char *tok = NULL;
int seen = 0;
while ( cursor != NULL ) {
AKSL_CHECK_OK(aksl_strsep(&cursor, ",", &tok));
if ( tok == NULL ) {
break;
}
seen++;
}
AKSL_CHECK(seen == 4);
AKSL_CHECK_STATUS(aksl_strsep(NULL, ",", &tok), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strsep(&cursor, NULL, &tok), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strsep(&cursor, ",", NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Status messages */
/* ---------------------------------------------------------------------- */
/*
* aksl_strerror knows this library's own statuses as well as errno values,
* which is the reason it is not a strerror_r wrapper: strerror_r could never
* name AKERR_NULLPOINTER.
*/
static int test_strerror_names_both_kinds_of_status(void)
{
char buf[128];
AKSL_CHECK_OK(aksl_strerror(ENOENT, buf, sizeof(buf)));
AKSL_CHECK(buf[0] != '\0');
AKSL_CHECK(strcmp(buf, "Unknown status 2") != 0);
AKSL_CHECK_OK(aksl_strerror(AKERR_NULLPOINTER, buf, sizeof(buf)));
AKSL_CHECK(buf[0] != '\0');
/* A status nothing has a name for is rendered as its number. */
AKSL_CHECK_OK(aksl_strerror(-98765, buf, sizeof(buf)));
AKSL_CHECK(strcmp(buf, "Unknown status -98765") == 0);
/* Too small a buffer is an error, and leaves nothing rather than a prefix. */
AKSL_CHECK_STATUS(aksl_strerror(ENOENT, buf, 2), AKERR_OUTOFBOUNDS);
AKSL_CHECK(buf[0] == '\0');
AKSL_CHECK_STATUS(aksl_strerror(ENOENT, NULL, 16), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strerror(ENOENT, buf, 0), AKERR_VALUE);
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_strlen_and_strnlen);
AKSL_RUN(failures, test_strcpy_copies_within_the_buffer);
AKSL_RUN(failures, test_strcpy_truncation_is_an_error_and_writes_nothing);
AKSL_RUN(failures, test_strncpy_always_terminates_and_never_pads);
AKSL_RUN(failures, test_strcat_appends_within_the_buffer);
AKSL_RUN(failures, test_strcat_refuses_an_unterminated_destination);
AKSL_RUN(failures, test_strncat_appends_at_most_n);
AKSL_RUN(failures, test_copying_rejects_null_and_zero_size);
AKSL_RUN(failures, test_strdup_and_strndup);
AKSL_RUN(failures, test_comparisons);
AKSL_RUN(failures, test_searching_reports_absence_as_success);
AKSL_RUN(failures, test_strcasestr);
AKSL_RUN(failures, test_searching_rejects_null_arguments);
AKSL_RUN(failures, test_strtok_r_walks_the_tokens);
AKSL_RUN(failures, test_strsep_keeps_empty_fields);
AKSL_RUN(failures, test_strerror_names_both_kinds_of_status);
AKSL_REPORT(failures);
}