Fix the six confirmed defects and close the API contract gaps
TODO.md section 2.1 recorded six defects reproduced against the built
library, and section 2.2 seventeen contract gaps. Both are closed. The
four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into
the tests for the things they test, and that list is now empty.
The defects:
2.1.1 aksl_list_append conflated Floyd cycle detection with finding
the tail, so `tail` tracked the node behind the midpoint. Any
append to a list of 2+ nodes silently dropped everything after
it. Two separate walks now: Floyd to prove the list is finite,
then a plain walk to the end.
2.1.2 aksl_list_iterate started visiting from Floyd's `slow` cursor,
so the whole first half of the list -- head included -- was
never passed to the callback. It starts at the head.
2.1.3 AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame
that raised it handled it and returned success, so the parent
carried on into the sibling subtree. The recursion is split out
and propagates the break; only the public entry swallows it.
2.1.4 va_end now matches every va_start on every path.
2.1.5 The ato* family had no error channel at all. Reimplemented over
a new strto* family with errno cleared, an endptr check and a
range check: AKERR_VALUE for junk, ERANGE for overflow.
2.1.6 aksl_realpath never checked resolved_path, could not be told
the buffer size, and formatted an unspecified buffer with %s on
its own error path. It takes a length; aksl_realpath_alloc is
the allocating form.
The contract gaps, in brief: errno is cleared before every wrapped call
and read back through a fallback so no error can carry status 0; fopen
validates pathname and mode; fread/fwrite report the transferred count
through a required out-param and no longer call a short transfer a
success; aksl_sprintf is gone in favour of aksl_snprintf, which treats
truncation as an error; the variadic wrappers carry format attributes;
djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded
and implements BFS, so lalloc/lfree are used rather than merely stored;
an unknown searchmode is AKERR_VALUE rather than silent success;
list_pop takes the head by reference; aksl_freep, the node initialisers
and extern "C" are new.
Build: -pg is out of the default build (it never reached the C compiler
anyway, and it is what produced the stray gmon.out), -Wall -Wextra are
in, and there is a .gitignore.
Tests: 11 binaries, all green under the normal and sanitizer builds.
Visit-order assertions replace the step counts that could not tell the
three depth-first orders apart.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,19 @@
|
||||
/*
|
||||
* Formatted-output wrappers: aksl_printf / aksl_fprintf / aksl_sprintf.
|
||||
* Formatted-output wrappers: aksl_printf / aksl_fprintf / aksl_snprintf and
|
||||
* their va_list forms.
|
||||
*
|
||||
* TODO.md section 1.3. 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.
|
||||
* TODO.md section 1.3, now 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.
|
||||
*
|
||||
* Not covered: the destination-overflow case, because aksl_sprintf wraps the
|
||||
* unbounded vsprintf and there is no bounded entry point to test yet
|
||||
* (TODO.md 2.2.4).
|
||||
* aksl_sprintf is gone (TODO.md 2.2.4) 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"
|
||||
@@ -37,41 +39,81 @@ static long read_file(const char *path, char *buf, size_t n)
|
||||
return (long)got;
|
||||
}
|
||||
|
||||
static int test_sprintf_writes_text_and_count(void)
|
||||
static int test_snprintf_writes_text_and_count(void)
|
||||
{
|
||||
char buf[64];
|
||||
int count = -1;
|
||||
|
||||
memset(buf, 0x00, sizeof(buf));
|
||||
AKSL_CHECK_OK(aksl_sprintf(&count, buf, "%s=%d", "x", 7));
|
||||
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_sprintf_empty_format_writes_nothing(void)
|
||||
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_sprintf(&count, buf, "%s", ""));
|
||||
AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "%s", ""));
|
||||
AKSL_CHECK(count == 0);
|
||||
AKSL_CHECK(buf[0] == '\0');
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_sprintf_rejects_null_arguments(void)
|
||||
/*
|
||||
* 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_sprintf(NULL, buf, "x"),
|
||||
AKERR_NULLPOINTER, "count=");
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_sprintf(&count, NULL, "x"),
|
||||
AKERR_NULLPOINTER, "str=");
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_sprintf(&count, buf, NULL),
|
||||
AKERR_NULLPOINTER, "format=");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -96,19 +138,21 @@ static int test_fprintf_writes_to_stream(void)
|
||||
|
||||
/*
|
||||
* vfprintf on a stream opened "r" fails outright, so the wrapper reports the
|
||||
* errno it saw (EBADF on glibc). *count is left holding -1 in this case, which
|
||||
* TODO.md 1.3 flags as a contract gap -- the status is the assertion here.
|
||||
* errno it saw (EBADF on glibc). *count is 0 afterwards, not vfprintf's -1:
|
||||
* TODO.md 1.3 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 = 0;
|
||||
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");
|
||||
EBADF, "Short write");
|
||||
AKSL_CHECK(count == 0);
|
||||
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||
AKSL_CHECK(unlink(path) == 0);
|
||||
return 0;
|
||||
@@ -119,11 +163,11 @@ static int test_fprintf_rejects_null_arguments(void)
|
||||
int count = 0;
|
||||
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fprintf(NULL, stdout, "x"),
|
||||
AKERR_NULLPOINTER, "count=");
|
||||
AKERR_NULLPOINTER, "count=");
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fprintf(&count, NULL, "x"),
|
||||
AKERR_NULLPOINTER, "stream=");
|
||||
AKERR_NULLPOINTER, "stream=");
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fprintf(&count, stdout, NULL),
|
||||
AKERR_NULLPOINTER, "format=");
|
||||
AKERR_NULLPOINTER, "format=");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -174,9 +218,43 @@ static int test_printf_rejects_null_arguments(void)
|
||||
int count = 0;
|
||||
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_printf(NULL, "x"),
|
||||
AKERR_NULLPOINTER, "count=");
|
||||
AKERR_NULLPOINTER, "count=");
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_printf(&count, NULL),
|
||||
AKERR_NULLPOINTER, "format=");
|
||||
AKERR_NULLPOINTER, "format=");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* The va_list forms are what the variadic ones are built on, and TODO.md 3.1
|
||||
* 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;
|
||||
}
|
||||
|
||||
@@ -193,8 +271,8 @@ static int test_variadic_wrappers_survive_repeated_calls(void)
|
||||
int i = 0;
|
||||
|
||||
for ( i = 0; i < 512; i++ ) {
|
||||
AKSL_CHECK_OK(aksl_sprintf(&count, buf, "%d %s %ld %c %f",
|
||||
i, "iteration", (long)i, 'x', (double)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));
|
||||
}
|
||||
@@ -207,9 +285,11 @@ int main(void)
|
||||
|
||||
akerr_init();
|
||||
|
||||
AKSL_RUN(failures, test_sprintf_writes_text_and_count);
|
||||
AKSL_RUN(failures, test_sprintf_empty_format_writes_nothing);
|
||||
AKSL_RUN(failures, test_sprintf_rejects_null_arguments);
|
||||
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);
|
||||
@@ -218,6 +298,7 @@ int main(void)
|
||||
AKSL_RUN(failures, test_printf_writes_to_stdout);
|
||||
AKSL_RUN(failures, test_printf_rejects_null_arguments);
|
||||
|
||||
AKSL_RUN(failures, test_va_list_forms_are_usable_from_outside);
|
||||
AKSL_RUN(failures, test_variadic_wrappers_survive_repeated_calls);
|
||||
|
||||
AKSL_REPORT(failures);
|
||||
|
||||
Reference in New Issue
Block a user