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

259 lines
8.5 KiB
C

/*
* The growable string buffer -- src/collections.c.
*
* The bounded formatting wrappers are the right answer when the destination is
* a fixed buffer and no answer at all when the length is not known in advance.
* This is that answer, and the properties worth holding onto are that it always
* grows enough, always stays NUL-terminated, and always says so when it cannot.
*/
#include "aksl_capture.h"
static int test_init_and_free(void)
{
aksl_StrBuf buf;
const char *s = NULL;
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
AKSL_CHECK(buf.data != NULL);
AKSL_CHECK(buf.length == 0);
/* A zero request is raised to the minimum rather than refused. */
AKSL_CHECK(buf.capacity >= 32);
/* Valid as a C string immediately, with no finalise step. */
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
AKSL_CHECK(strcmp(s, "") == 0);
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
AKSL_CHECK(buf.data == NULL);
AKSL_CHECK(buf.capacity == 0);
/* Freeing twice is an error rather than a double free, as aksl_freep arranges. */
AKSL_CHECK_STATUS(aksl_strbuf_free(&buf), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_init(NULL, 16), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_free(NULL), AKERR_NULLPOINTER);
return 0;
}
static int test_append_concatenates(void)
{
aksl_StrBuf buf;
const char *s = NULL;
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
AKSL_CHECK_OK(aksl_strbuf_append(&buf, "hello"));
AKSL_CHECK_OK(aksl_strbuf_append_char(&buf, ' '));
AKSL_CHECK_OK(aksl_strbuf_append(&buf, "world"));
AKSL_CHECK(buf.length == 11);
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
AKSL_CHECK(strcmp(s, "hello world") == 0);
/* Appending nothing is a no-op, not an error. */
AKSL_CHECK_OK(aksl_strbuf_append(&buf, ""));
AKSL_CHECK(buf.length == 11);
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
return 0;
}
/*
* Growth is the whole point. Appending far past the initial capacity has to
* reallocate, and everything written before the move has to survive it.
*/
static int test_growth_preserves_the_contents(void)
{
aksl_StrBuf buf;
const char *s = NULL;
size_t initial = 0;
int i = 0;
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 32));
initial = buf.capacity;
for ( i = 0; i < 1000; i++ ) {
AKSL_CHECK_OK(aksl_strbuf_append(&buf, "0123456789"));
}
AKSL_CHECK(buf.length == 10000);
AKSL_CHECK(buf.capacity > initial);
AKSL_CHECK(buf.capacity >= buf.length + 1);
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
AKSL_CHECK(strlen(s) == 10000);
/* Both ends, so a botched realloc shows up wherever it happened. */
AKSL_CHECK(strncmp(s, "0123456789", 10) == 0);
AKSL_CHECK(strcmp(s + 9990, "0123456789") == 0);
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
return 0;
}
/* Bytes rather than a string, so an embedded NUL can be appended deliberately. */
static int test_append_bytes_keeps_embedded_nuls(void)
{
aksl_StrBuf buf;
const char *s = NULL;
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
AKSL_CHECK_OK(aksl_strbuf_append_bytes(&buf, "ab\0cd", 5));
/* length counts all five; the C string view stops at the first NUL. */
AKSL_CHECK(buf.length == 5);
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
AKSL_CHECK(strlen(s) == 2);
AKSL_CHECK(memcmp(s, "ab\0cd", 5) == 0);
/* And the terminator is still there past the end. */
AKSL_CHECK(s[5] == '\0');
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
return 0;
}
/*
* Formatted append. vsnprintf is measured first and then written, so a long
* result grows the buffer rather than truncating -- which is the difference
* between this and aksl_snprintf into a fixed array.
*/
static int test_appendf_formats_and_grows(void)
{
aksl_StrBuf buf;
const char *s = NULL;
int i = 0;
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 8));
AKSL_CHECK_OK(aksl_strbuf_appendf(&buf, "%s=%d", "x", 7));
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
AKSL_CHECK(strcmp(s, "x=7") == 0);
/* Far longer than the 8 bytes it started with. */
AKSL_CHECK_OK(aksl_strbuf_appendf(&buf, " %s", "and a good deal more text than that"));
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
AKSL_CHECK(strcmp(s, "x=7 and a good deal more text than that") == 0);
/* Repeated formatted appends, to exercise the measure/write pair often. */
AKSL_CHECK_OK(aksl_strbuf_reset(&buf));
for ( i = 0; i < 200; i++ ) {
AKSL_CHECK_OK(aksl_strbuf_appendf(&buf, "[%03d]", i));
}
AKSL_CHECK(buf.length == 1000);
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
AKSL_CHECK(strncmp(s, "[000][001][002]", 15) == 0);
AKSL_CHECK(strcmp(s + 995, "[199]") == 0);
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
return 0;
}
/* An empty format produces nothing and is not an error. */
static int test_appendf_with_no_output(void)
{
aksl_StrBuf buf;
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
AKSL_CHECK_OK(aksl_strbuf_appendf(&buf, "%s", ""));
AKSL_CHECK(buf.length == 0);
AKSL_CHECK(buf.data[0] == '\0');
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
return 0;
}
/* reset empties without releasing, so the capacity is reused. */
static int test_reset_keeps_the_capacity(void)
{
aksl_StrBuf buf;
const char *s = NULL;
size_t grown = 0;
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
AKSL_CHECK_OK(aksl_strbuf_append(&buf, "something reasonably long to force a grow"));
grown = buf.capacity;
AKSL_CHECK_OK(aksl_strbuf_reset(&buf));
AKSL_CHECK(buf.length == 0);
AKSL_CHECK(buf.capacity == grown);
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
AKSL_CHECK(strcmp(s, "") == 0);
/* And it still works afterwards. */
AKSL_CHECK_OK(aksl_strbuf_append(&buf, "again"));
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
AKSL_CHECK(strcmp(s, "again") == 0);
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
return 0;
}
/*
* Every entry point checks that the buffer was initialised, so a zeroed
* aksl_StrBuf on the stack is an error rather than a NULL dereference.
*/
static int test_rejects_null_and_uninitialised(void)
{
aksl_StrBuf buf;
aksl_StrBuf zeroed;
const char *s = NULL;
memset((void *)&zeroed, 0x00, sizeof(zeroed));
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
AKSL_CHECK_STATUS(aksl_strbuf_append(NULL, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_append(&buf, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_append_bytes(NULL, "x", 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_append_bytes(&buf, NULL, 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_append_char(NULL, 'x'), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_appendf(NULL, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_appendf(&buf, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_reset(NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_cstr(NULL, &s), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_cstr(&buf, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_append(&zeroed, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_appendf(&zeroed, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_reset(&zeroed), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_cstr(&zeroed, &s), AKERR_NULLPOINTER);
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
return 0;
}
/*
* The reason this type exists, written the way a caller would: build a report of
* unknown length out of pieces, without anyone having to size a buffer first.
*/
static int test_builds_a_report(void)
{
aksl_StrBuf buf;
const char *s = NULL;
static const char *names[3] = { "alpha", "beta", "gamma" };
int i = 0;
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
AKSL_CHECK_OK(aksl_strbuf_append(&buf, "items:"));
for ( i = 0; i < 3; i++ ) {
AKSL_CHECK_OK(aksl_strbuf_appendf(&buf, " %s(%d)", names[i], i));
}
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
AKSL_CHECK(strcmp(s, "items: alpha(0) beta(1) gamma(2)") == 0);
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_init_and_free);
AKSL_RUN(failures, test_append_concatenates);
AKSL_RUN(failures, test_growth_preserves_the_contents);
AKSL_RUN(failures, test_append_bytes_keeps_embedded_nuls);
AKSL_RUN(failures, test_appendf_formats_and_grows);
AKSL_RUN(failures, test_appendf_with_no_output);
AKSL_RUN(failures, test_reset_keeps_the_capacity);
AKSL_RUN(failures, test_rejects_null_and_uninitialised);
AKSL_RUN(failures, test_builds_a_report);
AKSL_REPORT(failures);
}