Files
libakstdlib/tests/test_streamio.c

660 lines
22 KiB
C
Raw Normal View History

Wrap the strings, streams and collections the wishlist asked for TODO.md section 3.1's high-priority list and section 3.6's data structures. This is the surface akbasic went without: it makes 10 calls into this library and 116 to raw libc, and 69 of those 116 are strlen, strcmp, strncpy and strstr. Three new translation units, because src/stdlib.c covering four times what it did would stop being readable: src/string.c lengths, bounded copy and concatenation, duplication, comparison including the case-insensitive forms, searching, the reentrant tokenisers, and a status message that knows this library's own statuses as well as errno's. src/stream.c positioning, flushing and buffering, character and line I/O, stream state, freopen/fdopen/tmpfile, formatted input, and the file operations. src/collections.c the list functions that were missing, a head/tail container so append is O(1), a binary search tree, FNV-1a, a fixed-capacity hash map and a growable string buffer. Two conventions run through all of it. The copying functions take the destination size even where the libc function they are named for does not, because strcpy(3) cannot be called safely without it, and truncation is an error that writes nothing rather than a plausible prefix -- this is the idiom akbasic writes out by hand at ten sites. The searching functions treat "not found" as a successful answer of NULL, because absent is an answer and raising on it would make every caller handle a non-error. The hash map is akbasic's src/symtab.c generalised: open-addressed with linear probing over a caller-supplied slot array, keys copied into fixed slots so the map owns them, tombstones on delete so a removal cannot cut a probe chain, and a refusal rather than a resize when full. Tests: 16 binaries, green under the normal and sanitizer builds. The scanf wrappers take the number of conversions the caller expects, since comparing scanf(3)'s return against that by hand is the check everyone eventually forgets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:32:35 -04:00
/*
* Stream wrappers beyond open/read/write/close -- src/stream.c, TODO.md 3.1.
*
* tests/test_stream.c covers fopen/fread/fwrite/fclose. This one covers
* positioning, flushing, character and line I/O, stream state, and formatted
* input.
*
* The recurring assertion is the one the section exists for: where stdio folds
* "no more data" and "something broke" into a single sentinel -- EOF from
* fgetc, NULL from fgets, -1L from ftell -- the wrapper separates them, so
* AKERR_EOF ends a read loop and anything else is a fault.
*/
#include "aksl_capture.h"
#include <errno.h>
#include <fcntl.h>
/* A temp file containing exactly `text`, opened for reading. */
static int open_with_text(const char *text, char *path, size_t pathlen, FILE **fp)
{
FILE *out = NULL;
if ( aksl_temp_file(path, pathlen) != 0 ) {
return 1;
}
out = fopen(path, "w");
if ( out == NULL ) {
return 1;
}
if ( text[0] != '\0' && fputs(text, out) == EOF ) {
fclose(out);
return 1;
}
if ( fclose(out) != 0 ) {
return 1;
}
*fp = fopen(path, "r");
return (*fp == NULL) ? 1 : 0;
}
/* ---------------------------------------------------------------------- */
/* Positioning */
/* ---------------------------------------------------------------------- */
static int test_seek_and_tell(void)
{
char path[AKSL_TMP_MAX];
FILE *fp = NULL;
long pos = -1;
int c = 0;
AKSL_CHECK(open_with_text("0123456789", path, sizeof(path), &fp) == 0);
AKSL_CHECK_OK(aksl_ftell(fp, &pos));
AKSL_CHECK(pos == 0);
AKSL_CHECK_OK(aksl_fseek(fp, 4, SEEK_SET));
AKSL_CHECK_OK(aksl_ftell(fp, &pos));
AKSL_CHECK(pos == 4);
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
AKSL_CHECK(c == '4');
AKSL_CHECK_OK(aksl_fseek(fp, -1, SEEK_END));
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
AKSL_CHECK(c == '9');
/* rewind is the fseek whose failure rewind(3) throws away. */
AKSL_CHECK_OK(aksl_rewind(fp));
AKSL_CHECK_OK(aksl_ftell(fp, &pos));
AKSL_CHECK(pos == 0);
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
return 0;
}
static int test_seeko_and_tello(void)
{
char path[AKSL_TMP_MAX];
FILE *fp = NULL;
off_t pos = -1;
AKSL_CHECK(open_with_text("0123456789", path, sizeof(path), &fp) == 0);
AKSL_CHECK_OK(aksl_fseeko(fp, 6, SEEK_SET));
AKSL_CHECK_OK(aksl_ftello(fp, &pos));
AKSL_CHECK(pos == 6);
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
return 0;
}
static int test_getpos_and_setpos(void)
{
char path[AKSL_TMP_MAX];
FILE *fp = NULL;
fpos_t saved;
int c = 0;
AKSL_CHECK(open_with_text("abcdef", path, sizeof(path), &fp) == 0);
AKSL_CHECK_OK(aksl_fseek(fp, 2, SEEK_SET));
AKSL_CHECK_OK(aksl_fgetpos(fp, &saved));
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
AKSL_CHECK(c == 'c');
AKSL_CHECK_OK(aksl_fsetpos(fp, &saved));
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
AKSL_CHECK(c == 'c');
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
return 0;
}
static int test_positioning_rejects_null(void)
{
long pos = 0;
off_t opos = 0;
fpos_t fpos;
AKSL_CHECK_STATUS(aksl_fseek(NULL, 0, SEEK_SET), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_ftell(NULL, &pos), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_ftell(stdin, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_rewind(NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fseeko(NULL, 0, SEEK_SET), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_ftello(NULL, &opos), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_ftello(stdin, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fgetpos(NULL, &fpos), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fgetpos(stdin, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fsetpos(NULL, &fpos), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fsetpos(stdin, NULL), AKERR_NULLPOINTER);
return 0;
}
/* Seeking a pipe is ESPIPE, which is the failure rewind(3) would have hidden. */
static int test_seek_on_a_pipe_reports_espipe(void)
{
FILE *fp = popen("echo hello", "r");
long pos = 0;
AKSL_CHECK(fp != NULL);
AKSL_CHECK_STATUS(aksl_fseek(fp, 0, SEEK_SET), ESPIPE);
AKSL_CHECK_STATUS(aksl_ftell(fp, &pos), ESPIPE);
AKSL_CHECK_STATUS(aksl_rewind(fp), ESPIPE);
pclose(fp);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Character and line I/O */
/* ---------------------------------------------------------------------- */
/*
* fgetc(3) returns EOF for both the end of the file and a read error. Split
* apart, a read loop needs no ferror/feof dance at the bottom of it.
*/
static int test_fgetc_separates_eof_from_error(void)
{
char path[AKSL_TMP_MAX];
FILE *fp = NULL;
int c = 0;
int seen = 0;
AKSL_CHECK(open_with_text("ab", path, sizeof(path), &fp) == 0);
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
AKSL_CHECK(c == 'a');
seen++;
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
AKSL_CHECK(c == 'b');
seen++;
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fgetc(fp, &c), AKERR_EOF, "end of stream");
AKSL_CHECK(seen == 2);
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
/* A stream with no read permission is the error half of the same sentinel. */
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
AKSL_CHECK_STATUS(aksl_fgetc(fp, &c), EBADF);
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
AKSL_CHECK_STATUS(aksl_fgetc(NULL, &c), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fgetc(stdin, NULL), AKERR_NULLPOINTER);
return 0;
}
static int test_fputc_and_ungetc(void)
{
char path[AKSL_TMP_MAX];
FILE *fp = NULL;
int c = 0;
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
AKSL_CHECK_OK(aksl_fputc('x', fp));
AKSL_CHECK_OK(aksl_fputc('y', fp));
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
AKSL_CHECK(c == 'x');
/* Put it back and read it again. */
AKSL_CHECK_OK(aksl_ungetc(c, fp));
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
AKSL_CHECK(c == 'x');
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
AKSL_CHECK_STATUS(aksl_fputc('x', NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_ungetc('x', NULL), AKERR_NULLPOINTER);
return 0;
}
static int test_fgets_and_fputs(void)
{
char path[AKSL_TMP_MAX];
char line[64];
FILE *fp = NULL;
size_t len = 0;
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
AKSL_CHECK_OK(aksl_fputs("first\n", fp));
AKSL_CHECK_OK(aksl_fputs("second\n", fp));
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
AKSL_CHECK_OK(aksl_fgets(line, sizeof(line), fp, &len));
AKSL_CHECK(strcmp(line, "first\n") == 0);
AKSL_CHECK(len == 6);
AKSL_CHECK_OK(aksl_fgets(line, sizeof(line), fp, &len));
AKSL_CHECK(strcmp(line, "second\n") == 0);
/* The end of the input is AKERR_EOF, which is how the loop terminates. */
AKSL_CHECK_STATUS(aksl_fgets(line, sizeof(line), fp, &len), AKERR_EOF);
AKSL_CHECK(len == 0);
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
return 0;
}
/*
* A line longer than the buffer is a short read, not an error -- that is what
* fgets does and what a caller reading fixed chunks wants. The reported length
* is how the caller notices: a full buffer with no newline is exactly this case.
*/
static int test_fgets_reports_a_long_line_as_a_short_read(void)
{
char path[AKSL_TMP_MAX];
char line[4];
FILE *fp = NULL;
size_t len = 0;
AKSL_CHECK(open_with_text("abcdefgh\n", path, sizeof(path), &fp) == 0);
AKSL_CHECK_OK(aksl_fgets(line, sizeof(line), fp, &len));
AKSL_CHECK(len == 3);
AKSL_CHECK(strcmp(line, "abc") == 0);
AKSL_CHECK(line[len - 1] != '\n');
/* The rest of the line is still there. */
AKSL_CHECK_OK(aksl_fgets(line, sizeof(line), fp, &len));
AKSL_CHECK(strcmp(line, "def") == 0);
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
AKSL_CHECK_STATUS(aksl_fgets(NULL, 4, stdin, &len), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fgets(line, 4, NULL, &len), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fgets(line, 4, stdin, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fgets(line, 0, stdin, &len), AKERR_VALUE);
return 0;
}
/*
* getline grows the buffer, so the caller starts with NULL/0 and frees once at
* the end -- not once per line. The reported length is what distinguishes an
* embedded NUL from the end of the line; strlen cannot.
*/
static int test_getline_grows_its_buffer(void)
{
char path[AKSL_TMP_MAX];
char *line = NULL;
size_t cap = 0;
size_t len = 0;
FILE *fp = NULL;
int lines = 0;
AKSL_CHECK(open_with_text("short\na much longer line than the first one\n",
path, sizeof(path), &fp) == 0);
while ( 1 ) {
akerr_ErrorContext *raised = aksl_getline(&line, &cap, fp, &len);
int status = aksl_take(raised);
if ( status == AKERR_EOF ) {
break;
}
AKSL_CHECK(status == 0);
AKSL_CHECK(len > 0);
AKSL_CHECK(line != NULL);
lines++;
}
AKSL_CHECK(lines == 2);
/* One buffer for the whole file, grown as needed. */
AKSL_CHECK(cap >= 42);
AKSL_CHECK_OK(aksl_freep((void **)&line));
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
return 0;
}
static int test_getdelim_splits_on_any_byte(void)
{
char path[AKSL_TMP_MAX];
char *field = NULL;
size_t cap = 0;
size_t len = 0;
FILE *fp = NULL;
int fields = 0;
AKSL_CHECK(open_with_text("a:b:c", path, sizeof(path), &fp) == 0);
while ( 1 ) {
akerr_ErrorContext *raised = aksl_getdelim(&field, &cap, ':', fp, &len);
int status = aksl_take(raised);
if ( status == AKERR_EOF ) {
break;
}
AKSL_CHECK(status == 0);
fields++;
}
AKSL_CHECK(fields == 3);
AKSL_CHECK_OK(aksl_freep((void **)&field));
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
AKSL_CHECK_STATUS(aksl_getline(NULL, &cap, stdin, &len), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_getline(&field, NULL, stdin, &len), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_getline(&field, &cap, NULL, &len), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_getline(&field, &cap, stdin, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_getdelim(NULL, &cap, ':', stdin, &len), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_getdelim(&field, &cap, ':', stdin, NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Stream state */
/* ---------------------------------------------------------------------- */
static int test_stream_state(void)
{
char path[AKSL_TMP_MAX];
FILE *fp = NULL;
int flag = 99;
int fd = -1;
int c = 0;
AKSL_CHECK(open_with_text("a", path, sizeof(path), &fp) == 0);
AKSL_CHECK_OK(aksl_feof(fp, &flag));
AKSL_CHECK(flag == 0);
AKSL_CHECK_OK(aksl_ferror(fp, &flag));
AKSL_CHECK(flag == 0);
AKSL_CHECK_OK(aksl_fileno(fp, &fd));
AKSL_CHECK(fd >= 0);
/* Read past the end to set the EOF indicator, then clear it. */
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
AKSL_CHECK_STATUS(aksl_fgetc(fp, &c), AKERR_EOF);
AKSL_CHECK_OK(aksl_feof(fp, &flag));
AKSL_CHECK(flag != 0);
AKSL_CHECK_OK(aksl_clearerr(fp));
AKSL_CHECK_OK(aksl_feof(fp, &flag));
AKSL_CHECK(flag == 0);
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
AKSL_CHECK_STATUS(aksl_feof(NULL, &flag), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_feof(stdin, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_ferror(NULL, &flag), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_ferror(stdin, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_clearerr(NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fileno(NULL, &fd), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fileno(stdin, NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Flushing, buffering and opening */
/* ---------------------------------------------------------------------- */
static int test_fflush_and_setvbuf(void)
{
char path[AKSL_TMP_MAX];
char buffer[BUFSIZ];
FILE *fp = NULL;
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
AKSL_CHECK_OK(aksl_setvbuf(fp, buffer, _IOFBF, sizeof(buffer)));
AKSL_CHECK_OK(aksl_fputs("buffered", fp));
AKSL_CHECK_OK(aksl_fflush(fp));
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
/* NULL means "every output stream" and is not an error, unlike everywhere else. */
AKSL_CHECK_OK(aksl_fflush(NULL));
AKSL_CHECK_STATUS(aksl_setvbuf(NULL, NULL, _IONBF, 0), AKERR_NULLPOINTER);
return 0;
}
static int test_tmpfile_fdopen_and_freopen(void)
{
char path[AKSL_TMP_MAX];
FILE *fp = NULL;
FILE *reopened = NULL;
int fd = -1;
size_t moved = 0;
/* tmpfile: no name, gone when it is closed. */
AKSL_CHECK_OK(aksl_tmpfile(&fp));
AKSL_CHECK(fp != NULL);
AKSL_CHECK_OK(aksl_fwrite("x", 1, 1, fp, &moved));
AKSL_CHECK_OK(aksl_fclose(fp));
/* fdopen: wrap a descriptor this test owns. */
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
fd = open(path, O_RDONLY);
AKSL_CHECK(fd >= 0);
AKSL_CHECK_OK(aksl_fdopen(fd, "r", &fp));
AKSL_CHECK_OK(aksl_fclose(fp)); /* closes fd too */
/* freopen: point an existing stream at a different file. */
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
AKSL_CHECK_OK(aksl_freopen(path, "r", fp, &reopened));
AKSL_CHECK(reopened != NULL);
AKSL_CHECK_OK(aksl_fclose(reopened));
AKSL_CHECK(unlink(path) == 0);
AKSL_CHECK_STATUS(aksl_tmpfile(NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fdopen(-1, "r", &fp), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_fdopen(0, NULL, &fp), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fdopen(0, "r", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_freopen("/tmp", "r", NULL, &fp), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_freopen("/tmp", NULL, stdin, &fp), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_freopen("/tmp", "r", stdin, NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Formatted input */
/* ---------------------------------------------------------------------- */
/*
* The reason the scanf wrappers take an expected count: scanf(3) returns how
* many conversions succeeded, and comparing that against the number written in
* the format string is a check every caller has to do by hand and eventually
* forgets. Forgetting leaves the unassigned arguments holding whatever they
* held before.
*/
static int test_sscanf_enforces_the_expected_count(void)
{
int a = 0;
int b = 0;
int assigned = 0;
AKSL_CHECK_OK(aksl_sscanf("12 34", "%d %d", 2, &assigned, &a, &b));
AKSL_CHECK(assigned == 2);
AKSL_CHECK(a == 12 && b == 34);
/* One conversion short: an error rather than a stale second variable. */
a = 0;
b = 999;
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_sscanf("12 xy", "%d %d", 2, &assigned, &a, &b),
AKERR_VALUE, "1 of 2 conversions");
AKSL_CHECK(assigned == 1);
AKSL_CHECK(b == 999);
/* expected 0 opts out and hands the count back for the caller to judge. */
AKSL_CHECK_OK(aksl_sscanf("12 xy", "%d %d", 0, &assigned, &a, &b));
AKSL_CHECK(assigned == 1);
/* Nothing matched at all. */
AKSL_CHECK_STATUS(aksl_sscanf("", "%d", 1, &assigned, &a), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_sscanf(NULL, "%d", 1, &assigned, &a), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_sscanf("1", NULL, 1, &assigned), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_sscanf("1", "%d", 1, NULL, &a), AKERR_NULLPOINTER);
return 0;
}
static int test_fscanf_reads_from_a_stream(void)
{
char path[AKSL_TMP_MAX];
FILE *fp = NULL;
int a = 0;
int b = 0;
int assigned = 0;
AKSL_CHECK(open_with_text("7 8\n", path, sizeof(path), &fp) == 0);
AKSL_CHECK_OK(aksl_fscanf(fp, "%d %d", 2, &assigned, &a, &b));
AKSL_CHECK(assigned == 2);
AKSL_CHECK(a == 7 && b == 8);
/* Nothing left: the end of the stream, not a conversion failure. */
AKSL_CHECK_STATUS(aksl_fscanf(fp, "%d", 1, &assigned, &a), AKERR_EOF);
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
AKSL_CHECK_STATUS(aksl_fscanf(NULL, "%d", 1, &assigned, &a), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fscanf(stdin, NULL, 1, &assigned), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fscanf(stdin, "%d", 1, NULL, &a), AKERR_NULLPOINTER);
return 0;
}
Version at 0.2.0: complete the wishlist, document it, gate the docs Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that file to hold outstanding items only. The API break gets a minor bump, because pre-1.0 the soname carries MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five signatures changed and the ato* contract with them; UPGRADING.md is new and lists every one, with the before/after for the cases the compiler cannot warn about. Section 3.1 is finished: reallocarray with the multiplication checked, aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf. Four functions on that list are deliberately absent rather than missing -- sprintf, strtok, setbuf and perror -- and TODO.md now says which and why, so nobody adds them thinking they were forgotten. Section 1.9, the cross-cutting tests: tests/test_pool.c drives every failure path AKERR_MAX_ARRAY_ERROR + 10 times and checks the pool after each round, because a wrapper that leaks a slot fails a hundred calls later in unrelated code. It also asserts that each error names the function and file it was raised from, which is what catches a FAIL that migrates into a helper during a refactor: status right, message right, origin quietly lying. tests/negative/ two sources that must FAIL to compile, built with -Werror and registered WILL_FAIL. AKERR_NOIGNORE and the format attributes are enforced by the compiler and by nothing else; drop either and every ordinary test still passes. Thread safety is answered rather than tested: the library is not thread-safe and cannot be made so from here, because libakerror's error pool is an unlocked process-global array. README.md says so plainly and TODO.md carries it as the item blocking any future pthread wrappers. Doxygen is configured and gated. All 147 public functions have @brief, a @param each, @throws per status and @return; EXTRACT_ALL is off and WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an undocumented entity. It ran to 0 warnings. The Doxyfile carries no version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from project(), so that stays the one place a version is written. CI now builds against the submodule it pins instead of also installing libakerror@main and never linking it, adds -Werror, and gains a sanitizer job. The pre-push hook matches, and runs the docs check too. Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The eight uncovered lines are each uncovered on purpose and TODO.md says which and why. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:00:16 -04:00
/*
* aksl_scanf reads stdin, so stdin is pointed at a temp file for the duration of
* the call and restored through a dup of the original descriptor -- the same
* dance tests/test_format.c does for aksl_printf and stdout, and for the same
* reason: nothing between the freopen and the dup2 may return early.
*/
static int test_scanf_reads_stdin(void)
{
char path[AKSL_TMP_MAX];
FILE *fp = NULL;
akerr_ErrorContext *err = NULL;
int a = 0;
int b = 0;
int assigned = 0;
int saved = -1;
int restored = -1;
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
fp = fopen(path, "w");
AKSL_CHECK(fp != NULL);
AKSL_CHECK(fputs("11 22\n", fp) != EOF);
AKSL_CHECK(fclose(fp) == 0);
saved = dup(fileno(stdin));
AKSL_CHECK(saved >= 0);
if ( freopen(path, "r", stdin) == NULL ) {
close(saved);
AKSL_CHECK(0);
}
err = aksl_scanf("%d %d", 2, &assigned, &a, &b);
restored = dup2(saved, fileno(stdin));
close(saved);
clearerr(stdin);
AKSL_CHECK(restored >= 0);
AKSL_CHECK(aksl_take(err) == 0);
AKSL_CHECK(assigned == 2);
AKSL_CHECK(a == 11 && b == 22);
AKSL_CHECK(unlink(path) == 0);
AKSL_CHECK_STATUS(aksl_scanf(NULL, 1, &assigned), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_scanf("%d", 1, NULL, &a), AKERR_NULLPOINTER);
return 0;
}
Wrap the strings, streams and collections the wishlist asked for TODO.md section 3.1's high-priority list and section 3.6's data structures. This is the surface akbasic went without: it makes 10 calls into this library and 116 to raw libc, and 69 of those 116 are strlen, strcmp, strncpy and strstr. Three new translation units, because src/stdlib.c covering four times what it did would stop being readable: src/string.c lengths, bounded copy and concatenation, duplication, comparison including the case-insensitive forms, searching, the reentrant tokenisers, and a status message that knows this library's own statuses as well as errno's. src/stream.c positioning, flushing and buffering, character and line I/O, stream state, freopen/fdopen/tmpfile, formatted input, and the file operations. src/collections.c the list functions that were missing, a head/tail container so append is O(1), a binary search tree, FNV-1a, a fixed-capacity hash map and a growable string buffer. Two conventions run through all of it. The copying functions take the destination size even where the libc function they are named for does not, because strcpy(3) cannot be called safely without it, and truncation is an error that writes nothing rather than a plausible prefix -- this is the idiom akbasic writes out by hand at ten sites. The searching functions treat "not found" as a successful answer of NULL, because absent is an answer and raising on it would make every caller handle a non-error. The hash map is akbasic's src/symtab.c generalised: open-addressed with linear probing over a caller-supplied slot array, keys copied into fixed slots so the map owns them, tombstones on delete so a removal cannot cut a probe chain, and a refusal rather than a resize when full. Tests: 16 binaries, green under the normal and sanitizer builds. The scanf wrappers take the number of conversions the caller expects, since comparing scanf(3)'s return against that by hand is the check everyone eventually forgets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:32:35 -04:00
/* ---------------------------------------------------------------------- */
/* Files */
/* ---------------------------------------------------------------------- */
static int test_remove_and_rename(void)
{
char path[AKSL_TMP_MAX];
char moved[AKSL_TMP_MAX + 8];
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
AKSL_CHECK((size_t)snprintf(moved, sizeof(moved), "%s.moved", path) < sizeof(moved));
AKSL_CHECK_OK(aksl_rename(path, moved));
AKSL_CHECK(access(path, F_OK) != 0);
AKSL_CHECK(access(moved, F_OK) == 0);
AKSL_CHECK_OK(aksl_remove(moved));
AKSL_CHECK(access(moved, F_OK) != 0);
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_remove("/nonexistent/aksl/file"),
ENOENT, "/nonexistent/aksl/file");
AKSL_CHECK_STATUS(aksl_remove(NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_rename(NULL, "b"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_rename("a", NULL), AKERR_NULLPOINTER);
return 0;
}
/*
* mkstemp and mkdtemp rewrite the template in place, so a template that is not
* a writable buffer of the right shape is a crash waiting to happen. The shape
* is checked here rather than left to the kernel.
*/
static int test_mkstemp_and_mkdtemp(void)
{
char file_template[] = "/tmp/aksl_streamio_XXXXXX";
char dir_template[] = "/tmp/aksl_streamio_dir_XXXXXX";
char bad[] = "/tmp/aksl_no_placeholder";
int fd = -1;
AKSL_CHECK_OK(aksl_mkstemp(file_template, &fd));
AKSL_CHECK(fd >= 0);
AKSL_CHECK(strcmp(file_template, "/tmp/aksl_streamio_XXXXXX") != 0);
AKSL_CHECK(close(fd) == 0);
AKSL_CHECK_OK(aksl_remove(file_template));
AKSL_CHECK_OK(aksl_mkdtemp(dir_template));
AKSL_CHECK(access(dir_template, F_OK) == 0);
AKSL_CHECK(rmdir(dir_template) == 0);
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_mkstemp(bad, &fd), AKERR_VALUE, "six literal X");
AKSL_CHECK(fd == -1);
AKSL_CHECK_STATUS(aksl_mkdtemp(bad), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_mkstemp(NULL, &fd), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_mkstemp(file_template, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_mkdtemp(NULL), AKERR_NULLPOINTER);
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_seek_and_tell);
AKSL_RUN(failures, test_seeko_and_tello);
AKSL_RUN(failures, test_getpos_and_setpos);
AKSL_RUN(failures, test_positioning_rejects_null);
AKSL_RUN(failures, test_seek_on_a_pipe_reports_espipe);
AKSL_RUN(failures, test_fgetc_separates_eof_from_error);
AKSL_RUN(failures, test_fputc_and_ungetc);
AKSL_RUN(failures, test_fgets_and_fputs);
AKSL_RUN(failures, test_fgets_reports_a_long_line_as_a_short_read);
AKSL_RUN(failures, test_getline_grows_its_buffer);
AKSL_RUN(failures, test_getdelim_splits_on_any_byte);
AKSL_RUN(failures, test_stream_state);
AKSL_RUN(failures, test_fflush_and_setvbuf);
AKSL_RUN(failures, test_tmpfile_fdopen_and_freopen);
AKSL_RUN(failures, test_sscanf_enforces_the_expected_count);
AKSL_RUN(failures, test_fscanf_reads_from_a_stream);
Version at 0.2.0: complete the wishlist, document it, gate the docs Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that file to hold outstanding items only. The API break gets a minor bump, because pre-1.0 the soname carries MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five signatures changed and the ato* contract with them; UPGRADING.md is new and lists every one, with the before/after for the cases the compiler cannot warn about. Section 3.1 is finished: reallocarray with the multiplication checked, aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf. Four functions on that list are deliberately absent rather than missing -- sprintf, strtok, setbuf and perror -- and TODO.md now says which and why, so nobody adds them thinking they were forgotten. Section 1.9, the cross-cutting tests: tests/test_pool.c drives every failure path AKERR_MAX_ARRAY_ERROR + 10 times and checks the pool after each round, because a wrapper that leaks a slot fails a hundred calls later in unrelated code. It also asserts that each error names the function and file it was raised from, which is what catches a FAIL that migrates into a helper during a refactor: status right, message right, origin quietly lying. tests/negative/ two sources that must FAIL to compile, built with -Werror and registered WILL_FAIL. AKERR_NOIGNORE and the format attributes are enforced by the compiler and by nothing else; drop either and every ordinary test still passes. Thread safety is answered rather than tested: the library is not thread-safe and cannot be made so from here, because libakerror's error pool is an unlocked process-global array. README.md says so plainly and TODO.md carries it as the item blocking any future pthread wrappers. Doxygen is configured and gated. All 147 public functions have @brief, a @param each, @throws per status and @return; EXTRACT_ALL is off and WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an undocumented entity. It ran to 0 warnings. The Doxyfile carries no version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from project(), so that stays the one place a version is written. CI now builds against the submodule it pins instead of also installing libakerror@main and never linking it, adds -Werror, and gains a sanitizer job. The pre-push hook matches, and runs the docs check too. Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The eight uncovered lines are each uncovered on purpose and TODO.md says which and why. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:00:16 -04:00
AKSL_RUN(failures, test_scanf_reads_stdin);
Wrap the strings, streams and collections the wishlist asked for TODO.md section 3.1's high-priority list and section 3.6's data structures. This is the surface akbasic went without: it makes 10 calls into this library and 116 to raw libc, and 69 of those 116 are strlen, strcmp, strncpy and strstr. Three new translation units, because src/stdlib.c covering four times what it did would stop being readable: src/string.c lengths, bounded copy and concatenation, duplication, comparison including the case-insensitive forms, searching, the reentrant tokenisers, and a status message that knows this library's own statuses as well as errno's. src/stream.c positioning, flushing and buffering, character and line I/O, stream state, freopen/fdopen/tmpfile, formatted input, and the file operations. src/collections.c the list functions that were missing, a head/tail container so append is O(1), a binary search tree, FNV-1a, a fixed-capacity hash map and a growable string buffer. Two conventions run through all of it. The copying functions take the destination size even where the libc function they are named for does not, because strcpy(3) cannot be called safely without it, and truncation is an error that writes nothing rather than a plausible prefix -- this is the idiom akbasic writes out by hand at ten sites. The searching functions treat "not found" as a successful answer of NULL, because absent is an answer and raising on it would make every caller handle a non-error. The hash map is akbasic's src/symtab.c generalised: open-addressed with linear probing over a caller-supplied slot array, keys copied into fixed slots so the map owns them, tombstones on delete so a removal cannot cut a probe chain, and a refusal rather than a resize when full. Tests: 16 binaries, green under the normal and sanitizer builds. The scanf wrappers take the number of conversions the caller expects, since comparing scanf(3)'s return against that by hand is the check everyone eventually forgets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:32:35 -04:00
AKSL_RUN(failures, test_remove_and_rename);
AKSL_RUN(failures, test_mkstemp_and_mkdtemp);
AKSL_REPORT(failures);
}