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

352 lines
11 KiB
C

/*
* Formatted-output wrappers: aksl_printf / aksl_fprintf / aksl_snprintf and
* their va_list forms.
*
* Formatted output, complete. Each happy path asserts both halves of the
* contract -- the byte count handed back through *count and the text that
* actually landed somewhere -- and every pointer argument is checked for its
* NULL guard.
*
* Readback goes through plain libc rather than aksl_fread so that a failure here
* points at the formatted-output wrapper under test and not at the stream
* wrappers, which tests/test_stream.c covers.
*
* aksl_sprintf is gone and aksl_snprintf takes its place, so the
* destination-overflow case that could not previously be written is here: it is
* AKERR_OUTOFBOUNDS, not the short success snprintf(3) would have reported.
*/
#include "aksl_capture.h"
#include <errno.h>
/* Read a whole file into buf and NUL-terminate. Returns bytes read, or -1. */
static long read_file(const char *path, char *buf, size_t n)
{
FILE *fp = fopen(path, "r");
size_t got = 0;
if ( fp == NULL ) {
return -1;
}
got = fread(buf, 1, n - 1, fp);
buf[got] = '\0';
if ( ferror(fp) ) {
fclose(fp);
return -1;
}
fclose(fp);
return (long)got;
}
static int test_snprintf_writes_text_and_count(void)
{
char buf[64];
int count = -1;
memset(buf, 0x00, sizeof(buf));
AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "%s=%d", "x", 7));
AKSL_CHECK(count == 3);
AKSL_CHECK(strcmp(buf, "x=7") == 0);
return 0;
}
static int test_snprintf_empty_format_writes_nothing(void)
{
char buf[8] = { 'z', 'z', 'z', 'z', 'z', 'z', 'z', 'z' };
int count = -1;
AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "%s", ""));
AKSL_CHECK(count == 0);
AKSL_CHECK(buf[0] == '\0');
return 0;
}
/*
* The case that could not be written while the wrapper was aksl_sprintf: output
* longer than the destination. snprintf(3) would truncate, NUL-terminate and
* report the length it *would* have written, leaving the caller to notice; here
* it is an error, and *count is 0 rather than the would-have-been length.
*/
static int test_snprintf_truncation_is_an_error(void)
{
char buf[8];
int count = -1;
memset(buf, 0x00, sizeof(buf));
AKSL_CHECK_STATUS_MSG_CONTAINS(
aksl_snprintf(&count, buf, sizeof(buf), "%s", "far too long for eight bytes"),
AKERR_OUTOFBOUNDS, "truncated");
AKSL_CHECK(count == 0);
return 0;
}
/* Exactly filling the buffer is not truncation; one byte more is. */
static int test_snprintf_boundary_is_exact(void)
{
char buf[8];
int count = -1;
/* 7 characters plus the NUL is exactly sizeof(buf). */
AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "%s", "1234567"));
AKSL_CHECK(count == 7);
AKSL_CHECK(strcmp(buf, "1234567") == 0);
count = -1;
AKSL_CHECK_STATUS(aksl_snprintf(&count, buf, sizeof(buf), "%s", "12345678"),
AKERR_OUTOFBOUNDS);
AKSL_CHECK(count == 0);
return 0;
}
static int test_snprintf_rejects_null_arguments_and_zero_size(void)
{
char buf[8];
int count = 0;
memset(buf, 0x00, sizeof(buf));
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(NULL, buf, sizeof(buf), "x"),
AKERR_NULLPOINTER, "count=");
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(&count, NULL, 8, "x"),
AKERR_NULLPOINTER, "str=");
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(&count, buf, sizeof(buf), NULL),
AKERR_NULLPOINTER, "format=");
/* size 0 leaves no room even for the terminator, so there is nothing to do. */
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(&count, buf, 0, "x"),
AKERR_VALUE, "size=0");
return 0;
}
static int test_fprintf_writes_to_stream(void)
{
char path[AKSL_TMP_MAX];
char readback[64];
FILE *fp = NULL;
int count = -1;
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
AKSL_CHECK_OK(aksl_fprintf(&count, fp, "%s %d", "value", 42));
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(count == 8);
AKSL_CHECK(read_file(path, readback, sizeof(readback)) == 8);
AKSL_CHECK(strcmp(readback, "value 42") == 0);
AKSL_CHECK(unlink(path) == 0);
return 0;
}
/*
* vfprintf on a stream opened "r" fails outright, so the wrapper reports the
* errno it saw (EBADF on glibc). *count is 0 afterwards, not vfprintf's -1:
* The wrapper plan recorded the negative count as a contract gap, and this is the
* assertion that closes it.
*/
static int test_fprintf_to_read_only_stream_reports_errno(void)
{
char path[AKSL_TMP_MAX];
FILE *fp = NULL;
int count = 99;
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fprintf(&count, fp, "%d", 1),
EBADF, "Short write");
AKSL_CHECK(count == 0);
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
return 0;
}
static int test_fprintf_rejects_null_arguments(void)
{
int count = 0;
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fprintf(NULL, stdout, "x"),
AKERR_NULLPOINTER, "count=");
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fprintf(&count, NULL, "x"),
AKERR_NULLPOINTER, "stream=");
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fprintf(&count, stdout, NULL),
AKERR_NULLPOINTER, "format=");
return 0;
}
/*
* aksl_printf writes to stdout, so stdout is pointed at a temp file for the
* duration of the call and then restored through a dup of the original
* descriptor. Nothing between the freopen and the dup2 may return early: an
* assertion there would leave stdout attached to the temp file for the rest of
* the run, and the test report itself would vanish.
*/
static int test_printf_writes_to_stdout(void)
{
char path[AKSL_TMP_MAX];
char readback[64];
akerr_ErrorContext *err = NULL;
int count = -1;
int saved = -1;
int restored = -1;
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
fflush(stdout);
saved = dup(fileno(stdout));
AKSL_CHECK(saved >= 0);
if ( freopen(path, "w", stdout) == NULL ) {
close(saved);
AKSL_CHECK(0);
}
err = aksl_printf(&count, "%s#%d", "out", 5);
fflush(stdout);
restored = dup2(saved, fileno(stdout));
close(saved);
clearerr(stdout);
AKSL_CHECK(restored >= 0);
AKSL_CHECK(aksl_take(err) == 0);
AKSL_CHECK(count == 5);
AKSL_CHECK(read_file(path, readback, sizeof(readback)) == 5);
AKSL_CHECK(strcmp(readback, "out#5") == 0);
AKSL_CHECK(unlink(path) == 0);
return 0;
}
static int test_printf_rejects_null_arguments(void)
{
int count = 0;
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_printf(NULL, "x"),
AKERR_NULLPOINTER, "count=");
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_printf(&count, NULL),
AKERR_NULLPOINTER, "format=");
return 0;
}
/*
* The allocating form. No fixed buffer means no truncation case, which is what
* makes it the right answer when the length is not knowable in advance and the
* result is too short-lived to want a whole aksl_StrBuf.
*/
static int test_asprintf_allocates_to_fit(void)
{
char *out = NULL;
int count = -1;
int i = 0;
AKSL_CHECK_OK(aksl_asprintf(&count, &out, "%s=%d", "key", 42));
AKSL_CHECK(out != NULL);
AKSL_CHECK(strcmp(out, "key=42") == 0);
AKSL_CHECK(count == 6);
AKSL_CHECK_OK(aksl_freep((void **)&out));
/* Far longer than any buffer a fixed-size wrapper would have used. */
AKSL_CHECK_OK(aksl_asprintf(&count, &out, "%01000d", 7));
AKSL_CHECK(count == 1000);
AKSL_CHECK(strlen(out) == 1000);
AKSL_CHECK_OK(aksl_freep((void **)&out));
/* An empty result is a valid empty string, not NULL. */
AKSL_CHECK_OK(aksl_asprintf(&count, &out, "%s", ""));
AKSL_CHECK(count == 0);
AKSL_CHECK(out != NULL && out[0] == '\0');
AKSL_CHECK_OK(aksl_freep((void **)&out));
/* Repeatedly, so a leak shows up under the sanitizer build. */
for ( i = 0; i < 256; i++ ) {
AKSL_CHECK_OK(aksl_asprintf(&count, &out, "iteration %d of %d", i, 256));
AKSL_CHECK((size_t)count == strlen(out));
AKSL_CHECK_OK(aksl_freep((void **)&out));
}
out = (char *)0x1;
AKSL_CHECK_STATUS(aksl_asprintf(NULL, &out, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_asprintf(&count, NULL, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_asprintf(&count, &out, NULL), AKERR_NULLPOINTER);
/* Cleared before the format is even looked at. */
AKSL_CHECK(out == NULL);
return 0;
}
/*
* The va_list forms are what the variadic ones are built on, and the wrapper contract
* wanted them exposed so consumers can write their own variadic wrappers. This
* is a consumer doing exactly that.
*/
static akerr_ErrorContext AKERR_NOIGNORE *consumer_wrapper(int *count, char *buf, size_t n,
const char *fmt, ...)
{
va_list args;
akerr_ErrorContext *raised = NULL;
va_start(args, fmt);
raised = aksl_vsnprintf(count, buf, n, fmt, args);
va_end(args);
return raised;
}
static int test_va_list_forms_are_usable_from_outside(void)
{
char buf[32];
int count = -1;
memset(buf, 0x00, sizeof(buf));
AKSL_CHECK_OK(consumer_wrapper(&count, buf, sizeof(buf), "%s/%d", "via", 3));
AKSL_CHECK(count == 5);
AKSL_CHECK(strcmp(buf, "via/3") == 0);
/* The error contract survives the extra layer intact. */
AKSL_CHECK_STATUS(consumer_wrapper(&count, buf, 4, "%s", "too long"),
AKERR_OUTOFBOUNDS);
AKSL_CHECK(count == 0);
return 0;
}
/*
* Regression cover for the missing va_end. Nothing here can
* assert on register-save state directly; the point is to run the variadic
* wrappers enough times, with enough arguments, that the sanitizer build has
* something to trip over.
*/
static int test_variadic_wrappers_survive_repeated_calls(void)
{
char buf[128];
int count = 0;
int i = 0;
for ( i = 0; i < 512; i++ ) {
AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "%d %s %ld %c %f",
i, "iteration", (long)i, 'x', (double)i));
AKSL_CHECK(count > 0);
AKSL_CHECK((size_t)count == strlen(buf));
}
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_snprintf_writes_text_and_count);
AKSL_RUN(failures, test_snprintf_empty_format_writes_nothing);
AKSL_RUN(failures, test_snprintf_truncation_is_an_error);
AKSL_RUN(failures, test_snprintf_boundary_is_exact);
AKSL_RUN(failures, test_snprintf_rejects_null_arguments_and_zero_size);
AKSL_RUN(failures, test_fprintf_writes_to_stream);
AKSL_RUN(failures, test_fprintf_to_read_only_stream_reports_errno);
AKSL_RUN(failures, test_fprintf_rejects_null_arguments);
AKSL_RUN(failures, test_printf_writes_to_stdout);
AKSL_RUN(failures, test_printf_rejects_null_arguments);
AKSL_RUN(failures, test_asprintf_allocates_to_fit);
AKSL_RUN(failures, test_va_list_forms_are_usable_from_outside);
AKSL_RUN(failures, test_variadic_wrappers_survive_repeated_calls);
AKSL_REPORT(failures);
}