259 lines
8.5 KiB
C
259 lines
8.5 KiB
C
|
|
/*
|
||
|
|
* The growable string buffer -- src/collections.c, TODO.md 3.6.
|
||
|
|
*
|
||
|
|
* 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);
|
||
|
|
}
|