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>
660 lines
22 KiB
C
660 lines
22 KiB
C
/*
|
|
* 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;
|
|
}
|
|
|
|
/*
|
|
* 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;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* 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);
|
|
|
|
AKSL_RUN(failures, test_scanf_reads_stdin);
|
|
|
|
AKSL_RUN(failures, test_remove_and_rename);
|
|
AKSL_RUN(failures, test_mkstemp_and_mkdtemp);
|
|
|
|
AKSL_REPORT(failures);
|
|
}
|