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>
This commit is contained in:
2026-07-31 07:32:35 -04:00
parent 55eb0334c4
commit 98a0a8562d
13 changed files with 4923 additions and 13 deletions

730
tests/test_collections.c Normal file
View File

@@ -0,0 +1,730 @@
/*
* List and tree additions -- src/collections.c, TODO.md section 3.6.
*
* The bare-node list functions, the tracked aksl_List container, and the binary
* search tree. The hash map and string buffer have their own files.
*
* Nodes here are stack arrays, which is the point: none of these functions
* allocates, so a caller drawing from a fixed pool can use all of them. The two
* *_free_all tests use aksl_malloc explicitly, because releasing is the one
* thing that has to know where the memory came from.
*/
#include "aksl_capture.h"
#define N 5
/* ---------------------------------------------------------------------- */
/* Helpers */
/* ---------------------------------------------------------------------- */
typedef struct VisitLog
{
int count;
aksl_ListNode *seen[16];
int break_at;
} VisitLog;
static void visitlog_init(VisitLog *log)
{
memset((void *)log, 0x00, sizeof(VisitLog));
log->break_at = -1;
}
static akerr_ErrorContext AKERR_NOIGNORE *record_visit(aksl_ListNode *node, void *data)
{
VisitLog *log = NULL;
int idx = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data");
log = (VisitLog *)data;
idx = log->count;
if ( idx < 16 ) {
log->seen[idx] = node;
}
log->count += 1;
if ( log->break_at == idx ) {
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop at visit %d", idx);
}
SUCCEED_RETURN(e);
}
/* Accepts the node whose data pointer equals `data`. */
static akerr_ErrorContext AKERR_NOIGNORE *match_data(aksl_ListNode *node, void *data, int *matched)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
FAIL_ZERO_RETURN(e, matched, AKERR_NULLPOINTER, "matched");
*matched = (node->data == data) ? 1 : 0;
SUCCEED_RETURN(e);
}
/* A predicate that fails, to prove the error comes back out of the search. */
static akerr_ErrorContext AKERR_NOIGNORE *failing_predicate(aksl_ListNode *node, void *data, int *matched)
{
PREPARE_ERROR(e);
(void)node;
(void)data;
(void)matched;
FAIL_RETURN(e, AKERR_VALUE, "predicate refused to answer");
}
/* Build node[0..n) into a chain and hand back the head. */
static void build_chain(aksl_ListNode *node, int n)
{
int i = 0;
for ( i = 0; i < n; i++ ) {
memset((void *)&node[i], 0x00, sizeof(aksl_ListNode));
}
for ( i = 1; i < n; i++ ) {
node[i - 1].next = &node[i];
node[i].prev = &node[i - 1];
}
}
/* ---------------------------------------------------------------------- */
/* Insertion */
/* ---------------------------------------------------------------------- */
static int test_prepend_moves_the_head(void)
{
aksl_ListNode node[3];
aksl_ListNode *head = NULL;
build_chain(node, 3);
head = &node[1];
node[1].prev = NULL;
AKSL_CHECK_OK(aksl_list_prepend(&head, &node[0]));
AKSL_CHECK(head == &node[0]);
AKSL_CHECK(node[0].next == &node[1]);
AKSL_CHECK(node[0].prev == NULL);
AKSL_CHECK(node[1].prev == &node[0]);
/* Prepending onto an empty list makes a one-node list. */
head = NULL;
memset((void *)&node[2], 0x00, sizeof(node[2]));
AKSL_CHECK_OK(aksl_list_prepend(&head, &node[2]));
AKSL_CHECK(head == &node[2]);
AKSL_CHECK(node[2].next == NULL);
AKSL_CHECK_STATUS(aksl_list_prepend(&head, &node[2]), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_list_prepend(NULL, &node[0]), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_prepend(&head, NULL), AKERR_NULLPOINTER);
return 0;
}
static int test_insert_after_and_before(void)
{
aksl_ListNode node[4];
aksl_ListNode *head = &node[0];
build_chain(node, 2);
memset((void *)&node[2], 0x00, sizeof(node[2]));
memset((void *)&node[3], 0x00, sizeof(node[3]));
/* node[0] -> node[2] -> node[1] */
AKSL_CHECK_OK(aksl_list_insert_after(&node[0], &node[2]));
AKSL_CHECK(node[0].next == &node[2]);
AKSL_CHECK(node[2].prev == &node[0]);
AKSL_CHECK(node[2].next == &node[1]);
AKSL_CHECK(node[1].prev == &node[2]);
/* Inserting before the head moves it, which is why head is required. */
AKSL_CHECK_OK(aksl_list_insert_before(&head, &node[0], &node[3]));
AKSL_CHECK(head == &node[3]);
AKSL_CHECK(node[3].next == &node[0]);
AKSL_CHECK(node[0].prev == &node[3]);
AKSL_CHECK_STATUS(aksl_list_insert_after(&node[0], &node[0]), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_list_insert_after(NULL, &node[0]), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_insert_after(&node[0], NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_insert_before(&head, &node[0], &node[0]), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_list_insert_before(NULL, &node[0], &node[1]), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_insert_before(&head, NULL, &node[1]), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_insert_before(&head, &node[0], NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Inspection */
/* ---------------------------------------------------------------------- */
static int test_length_counts_and_refuses_cycles(void)
{
aksl_ListNode node[N];
size_t n = 99;
/* An empty list is length 0, not an error. */
AKSL_CHECK_OK(aksl_list_length(NULL, &n));
AKSL_CHECK(n == 0);
build_chain(node, N);
AKSL_CHECK_OK(aksl_list_length(&node[0], &n));
AKSL_CHECK(n == N);
node[N - 1].next = &node[0];
AKSL_CHECK_STATUS(aksl_list_length(&node[0], &n), AKERR_CIRCULAR_REFERENCE);
AKSL_CHECK_STATUS(aksl_list_length(&node[0], NULL), AKERR_NULLPOINTER);
return 0;
}
static int test_find_returns_the_first_match_or_null(void)
{
aksl_ListNode node[N];
aksl_ListNode *found = (aksl_ListNode *)0x1;
int payload = 42;
int absent = 0;
build_chain(node, N);
node[2].data = &payload;
AKSL_CHECK_OK(aksl_list_find(&node[0], &match_data, &payload, &found));
AKSL_CHECK(found == &node[2]);
/* Nothing matches: NULL and success, not an error. */
AKSL_CHECK_OK(aksl_list_find(&node[0], &match_data, &absent, &found));
AKSL_CHECK(found == NULL);
/* An empty list finds nothing, equally without complaint. */
AKSL_CHECK_OK(aksl_list_find(NULL, &match_data, &payload, &found));
AKSL_CHECK(found == NULL);
/* A predicate that raises stops the search and propagates. */
AKSL_CHECK_STATUS_MSG_CONTAINS(
aksl_list_find(&node[0], &failing_predicate, NULL, &found),
AKERR_VALUE, "predicate refused");
AKSL_CHECK_STATUS(aksl_list_find(&node[0], NULL, NULL, &found), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_find(&node[0], &match_data, NULL, NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Rearranging */
/* ---------------------------------------------------------------------- */
static int test_reverse_flips_both_directions(void)
{
aksl_ListNode node[N];
aksl_ListNode *head = NULL;
aksl_ListNode *walk = NULL;
int i = 0;
build_chain(node, N);
head = &node[0];
AKSL_CHECK_OK(aksl_list_reverse(&head));
AKSL_CHECK(head == &node[N - 1]);
/* Forwards through the reversed list is backwards through the array. */
walk = head;
for ( i = N - 1; i >= 0; i-- ) {
AKSL_CHECK(walk == &node[i]);
walk = walk->next;
}
AKSL_CHECK(walk == NULL);
/* And the prev links were flipped too, not just the next ones. */
walk = &node[0];
for ( i = 0; i < N; i++ ) {
AKSL_CHECK(walk == &node[i]);
walk = walk->prev;
}
AKSL_CHECK(walk == NULL);
/* Reversing an empty list is a no-op, not an error. */
head = NULL;
AKSL_CHECK_OK(aksl_list_reverse(&head));
AKSL_CHECK(head == NULL);
AKSL_CHECK_STATUS(aksl_list_reverse(NULL), AKERR_NULLPOINTER);
return 0;
}
static int test_concat_joins_two_lists(void)
{
aksl_ListNode first[3];
aksl_ListNode second[2];
size_t n = 0;
build_chain(first, 3);
build_chain(second, 2);
AKSL_CHECK_OK(aksl_list_concat(&first[0], &second[0]));
AKSL_CHECK(first[2].next == &second[0]);
AKSL_CHECK(second[0].prev == &first[2]);
AKSL_CHECK_OK(aksl_list_length(&first[0], &n));
AKSL_CHECK(n == 5);
/* Concatenating with NULL is a no-op; with itself would make a cycle. */
AKSL_CHECK_OK(aksl_list_concat(&first[0], NULL));
AKSL_CHECK_STATUS(aksl_list_concat(&first[0], &first[0]), AKERR_VALUE);
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_list_concat(&first[0], &second[1]),
AKERR_VALUE, "already in the destination");
AKSL_CHECK_STATUS(aksl_list_concat(NULL, &second[0]), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Reverse iteration */
/* ---------------------------------------------------------------------- */
static int test_iterate_reverse_walks_back_to_the_head(void)
{
aksl_ListNode node[N];
VisitLog log;
int i = 0;
build_chain(node, N);
visitlog_init(&log);
AKSL_CHECK_OK(aksl_list_iterate_reverse(&node[N - 1], &record_visit, &log));
AKSL_CHECK(log.count == N);
for ( i = 0; i < N; i++ ) {
AKSL_CHECK(log.seen[i] == &node[N - 1 - i]);
}
/* The break works going this way too. */
visitlog_init(&log);
log.break_at = 1;
AKSL_CHECK_OK(aksl_list_iterate_reverse(&node[N - 1], &record_visit, &log));
AKSL_CHECK(log.count == 2);
/* And a cycle in the prev links is refused, as it is in the next links. */
node[0].prev = &node[N - 1];
visitlog_init(&log);
AKSL_CHECK_STATUS(aksl_list_iterate_reverse(&node[N - 1], &record_visit, &log),
AKERR_CIRCULAR_REFERENCE);
AKSL_CHECK_STATUS(aksl_list_iterate_reverse(NULL, &record_visit, &log), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_iterate_reverse(&node[0], NULL, &log), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Releasing */
/* ---------------------------------------------------------------------- */
static int test_free_all_releases_every_node(void)
{
aksl_ListNode *head = NULL;
aksl_ListNode *node = NULL;
int i = 0;
/* A heap list, since this is the one operation that has to own its memory. */
for ( i = 0; i < N; i++ ) {
AKSL_CHECK_OK(aksl_malloc(sizeof(aksl_ListNode), (void **)&node));
AKSL_CHECK_OK(aksl_list_node_init(node, NULL));
if ( head == NULL ) {
head = node;
} else {
AKSL_CHECK_OK(aksl_list_append(head, node));
}
}
AKSL_CHECK_OK(aksl_list_free_all(&head, NULL));
AKSL_CHECK(head == NULL);
/* An empty list frees cleanly, and a second call is a no-op rather than a
* double free, because the head was cleared. */
AKSL_CHECK_OK(aksl_list_free_all(&head, NULL));
AKSL_CHECK_STATUS(aksl_list_free_all(NULL, NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* The tracked container */
/* ---------------------------------------------------------------------- */
/*
* The reason the container exists: aksl_list_append walks to the tail every
* time, so building n nodes with it is O(n^2). push is O(1) and the length is a
* field rather than a walk.
*/
static int test_container_push_and_unshift(void)
{
aksl_List list;
aksl_ListNode node[N];
int i = 0;
AKSL_CHECK_OK(aksl_list_init(&list));
AKSL_CHECK(list.head == NULL && list.tail == NULL && list.length == 0);
for ( i = 0; i < N; i++ ) {
memset((void *)&node[i], 0x00, sizeof(node[i]));
AKSL_CHECK_OK(aksl_list_push(&list, &node[i]));
AKSL_CHECK(list.length == (size_t)(i + 1));
AKSL_CHECK(list.tail == &node[i]);
}
AKSL_CHECK(list.head == &node[0]);
AKSL_CHECK(node[0].prev == NULL);
AKSL_CHECK(node[N - 1].next == NULL);
AKSL_CHECK_OK(aksl_list_init(&list));
for ( i = 0; i < N; i++ ) {
memset((void *)&node[i], 0x00, sizeof(node[i]));
AKSL_CHECK_OK(aksl_list_unshift(&list, &node[i]));
AKSL_CHECK(list.head == &node[i]);
AKSL_CHECK(list.tail == &node[0]);
}
AKSL_CHECK(list.length == N);
AKSL_CHECK_STATUS(aksl_list_init(NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_push(NULL, &node[0]), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_push(&list, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_unshift(NULL, &node[0]), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_unshift(&list, NULL), AKERR_NULLPOINTER);
return 0;
}
/* Removing keeps head, tail and length describing the list. */
static int test_container_remove_maintains_the_endpoints(void)
{
aksl_List list;
aksl_ListNode node[3];
aksl_ListNode stranger;
int i = 0;
AKSL_CHECK_OK(aksl_list_init(&list));
for ( i = 0; i < 3; i++ ) {
memset((void *)&node[i], 0x00, sizeof(node[i]));
AKSL_CHECK_OK(aksl_list_push(&list, &node[i]));
}
memset((void *)&stranger, 0x00, sizeof(stranger));
/* Middle. */
AKSL_CHECK_OK(aksl_list_remove(&list, &node[1]));
AKSL_CHECK(list.length == 2);
AKSL_CHECK(list.head == &node[0] && list.tail == &node[2]);
AKSL_CHECK(node[0].next == &node[2] && node[2].prev == &node[0]);
/* Head. */
AKSL_CHECK_OK(aksl_list_remove(&list, &node[0]));
AKSL_CHECK(list.head == &node[2] && list.tail == &node[2]);
AKSL_CHECK(list.length == 1);
/* Last one out empties both endpoints. */
AKSL_CHECK_OK(aksl_list_remove(&list, &node[2]));
AKSL_CHECK(list.head == NULL && list.tail == NULL && list.length == 0);
/* A node that is not in this list is refused rather than corrupting it. */
AKSL_CHECK_OK(aksl_list_push(&list, &node[0]));
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_list_remove(&list, &stranger),
AKERR_VALUE, "not in this list");
AKSL_CHECK(list.length == 1);
AKSL_CHECK_STATUS(aksl_list_remove(NULL, &node[0]), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_remove(&list, NULL), AKERR_NULLPOINTER);
return 0;
}
static int test_container_clear(void)
{
aksl_List list;
aksl_ListNode *node = NULL;
int i = 0;
AKSL_CHECK_OK(aksl_list_init(&list));
for ( i = 0; i < N; i++ ) {
AKSL_CHECK_OK(aksl_malloc(sizeof(aksl_ListNode), (void **)&node));
AKSL_CHECK_OK(aksl_list_node_init(node, NULL));
AKSL_CHECK_OK(aksl_list_push(&list, node));
}
AKSL_CHECK(list.length == N);
AKSL_CHECK_OK(aksl_list_clear(&list, NULL));
AKSL_CHECK(list.head == NULL && list.tail == NULL && list.length == 0);
AKSL_CHECK_STATUS(aksl_list_clear(NULL, NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Binary search tree */
/* ---------------------------------------------------------------------- */
/* Compares the ints the leaf pointers point at. */
static akerr_ErrorContext AKERR_NOIGNORE *compare_ints(void *a, void *b, int *dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, a, AKERR_NULLPOINTER, "a");
FAIL_ZERO_RETURN(e, b, AKERR_NULLPOINTER, "b");
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dest");
*dest = *(int *)a - *(int *)b;
SUCCEED_RETURN(e);
}
typedef struct OrderLog
{
int count;
int seen[16];
} OrderLog;
static akerr_ErrorContext AKERR_NOIGNORE *record_leaf(aksl_TreeNode *node, void *data)
{
OrderLog *log = NULL;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data");
log = (OrderLog *)data;
if ( log->count < 16 ) {
log->seen[log->count] = *(int *)node->leaf;
}
log->count += 1;
SUCCEED_RETURN(e);
}
/*
* Insertion order 5 3 8 1 4 7 9 builds a tree whose in-order walk is sorted --
* which is the whole invariant a search tree exists to maintain, so asserting it
* is worth more than asserting any particular shape.
*/
static int test_tree_insert_orders_the_leaves(void)
{
static int values[7] = { 5, 3, 8, 1, 4, 7, 9 };
aksl_TreeNode node[7];
aksl_TreeNode *root = NULL;
OrderLog log;
int i = 0;
for ( i = 0; i < 7; i++ ) {
AKSL_CHECK_OK(aksl_tree_node_init(&node[i], &values[i]));
AKSL_CHECK_OK(aksl_tree_insert(&root, &node[i], &compare_ints));
}
AKSL_CHECK(root == &node[0]);
memset((void *)&log, 0x00, sizeof(log));
AKSL_CHECK_OK(aksl_tree_iterate(root, &record_leaf, NULL, NULL,
AKSL_TREE_SEARCH_DFS_INORDER, &log));
AKSL_CHECK(log.count == 7);
for ( i = 1; i < log.count; i++ ) {
AKSL_CHECK(log.seen[i - 1] < log.seen[i]);
}
AKSL_CHECK_STATUS(aksl_tree_insert(NULL, &node[0], &compare_ints), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_tree_insert(&root, NULL, &compare_ints), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_tree_insert(&root, &node[0], NULL), AKERR_NULLPOINTER);
return 0;
}
/*
* TODO.md 2.2.15: aksl_TreeNode.parent was declared and never touched by
* anything in the library. These are the functions that set it, and
* aksl_tree_remove is the one that needs it.
*/
static int test_tree_insert_sets_the_parent_links(void)
{
static int values[3] = { 5, 3, 8 };
aksl_TreeNode node[3];
aksl_TreeNode *root = NULL;
int i = 0;
for ( i = 0; i < 3; i++ ) {
AKSL_CHECK_OK(aksl_tree_node_init(&node[i], &values[i]));
AKSL_CHECK_OK(aksl_tree_insert(&root, &node[i], &compare_ints));
}
AKSL_CHECK(node[0].parent == NULL); /* the root */
AKSL_CHECK(node[1].parent == &node[0]);
AKSL_CHECK(node[2].parent == &node[0]);
AKSL_CHECK(node[0].left == &node[1]);
AKSL_CHECK(node[0].right == &node[2]);
return 0;
}
static int test_tree_find(void)
{
static int values[5] = { 5, 3, 8, 1, 9 };
int wanted = 8;
int absent = 6;
aksl_TreeNode node[5];
aksl_TreeNode *root = NULL;
aksl_TreeNode *found = (aksl_TreeNode *)0x1;
int i = 0;
for ( i = 0; i < 5; i++ ) {
AKSL_CHECK_OK(aksl_tree_node_init(&node[i], &values[i]));
AKSL_CHECK_OK(aksl_tree_insert(&root, &node[i], &compare_ints));
}
AKSL_CHECK_OK(aksl_tree_find(root, &wanted, &compare_ints, &found));
AKSL_CHECK(found == &node[2]);
/* Absent is NULL and success. */
AKSL_CHECK_OK(aksl_tree_find(root, &absent, &compare_ints, &found));
AKSL_CHECK(found == NULL);
/* An empty tree finds nothing, equally without complaint. */
AKSL_CHECK_OK(aksl_tree_find(NULL, &wanted, &compare_ints, &found));
AKSL_CHECK(found == NULL);
AKSL_CHECK_STATUS(aksl_tree_find(root, &wanted, NULL, &found), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_tree_find(root, &wanted, &compare_ints, NULL), AKERR_NULLPOINTER);
return 0;
}
/*
* All three removal cases, each checked by re-walking the tree and confirming it
* is still sorted and one node shorter. The two-child case is the interesting
* one: the in-order successor takes the node's place, which is the only value
* that keeps the ordering invariant on both sides.
*/
static int test_tree_remove_all_three_cases(void)
{
static int values[7] = { 5, 3, 8, 1, 4, 7, 9 };
aksl_TreeNode node[7];
aksl_TreeNode *root = NULL;
OrderLog log;
size_t count = 0;
int i = 0;
for ( i = 0; i < 7; i++ ) {
AKSL_CHECK_OK(aksl_tree_node_init(&node[i], &values[i]));
AKSL_CHECK_OK(aksl_tree_insert(&root, &node[i], &compare_ints));
}
/* Leaf: node[3] holds 1 and has no children. */
AKSL_CHECK_OK(aksl_tree_remove(&root, &node[3]));
AKSL_CHECK(node[3].parent == NULL && node[3].left == NULL && node[3].right == NULL);
AKSL_CHECK_OK(aksl_tree_count(root, &count));
AKSL_CHECK(count == 6);
/* One child: node[1] holds 3 and now has only its right child (4). */
AKSL_CHECK_OK(aksl_tree_remove(&root, &node[1]));
AKSL_CHECK_OK(aksl_tree_count(root, &count));
AKSL_CHECK(count == 5);
/* Two children: the root, 5, with 4 on the left and 8 on the right. */
AKSL_CHECK_OK(aksl_tree_remove(&root, &node[0]));
AKSL_CHECK_OK(aksl_tree_count(root, &count));
AKSL_CHECK(count == 4);
AKSL_CHECK(root != &node[0]);
AKSL_CHECK(root->parent == NULL);
/* Still sorted after all of that, which is the invariant that matters. */
memset((void *)&log, 0x00, sizeof(log));
AKSL_CHECK_OK(aksl_tree_iterate(root, &record_leaf, NULL, NULL,
AKSL_TREE_SEARCH_DFS_INORDER, &log));
AKSL_CHECK(log.count == 4);
for ( i = 1; i < log.count; i++ ) {
AKSL_CHECK(log.seen[i - 1] < log.seen[i]);
}
AKSL_CHECK_STATUS(aksl_tree_remove(NULL, &node[0]), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_tree_remove(&root, NULL), AKERR_NULLPOINTER);
return 0;
}
/* Removing the last node empties the tree rather than leaving a dangling root. */
static int test_tree_remove_the_only_node(void)
{
int value = 1;
aksl_TreeNode node;
aksl_TreeNode *root = NULL;
size_t count = 99;
AKSL_CHECK_OK(aksl_tree_node_init(&node, &value));
AKSL_CHECK_OK(aksl_tree_insert(&root, &node, &compare_ints));
AKSL_CHECK_OK(aksl_tree_remove(&root, &node));
AKSL_CHECK(root == NULL);
AKSL_CHECK_OK(aksl_tree_count(root, &count));
AKSL_CHECK(count == 0);
return 0;
}
static int test_tree_height_and_count(void)
{
static int values[7] = { 5, 3, 8, 1, 4, 7, 9 };
static int chain[4] = { 1, 2, 3, 4 };
aksl_TreeNode node[7];
aksl_TreeNode *root = NULL;
size_t count = 99;
int height = 99;
int i = 0;
/* Empty. */
AKSL_CHECK_OK(aksl_tree_height(NULL, &height));
AKSL_CHECK(height == 0);
AKSL_CHECK_OK(aksl_tree_count(NULL, &count));
AKSL_CHECK(count == 0);
/* Balanced by construction: 7 nodes, 3 levels. */
for ( i = 0; i < 7; i++ ) {
AKSL_CHECK_OK(aksl_tree_node_init(&node[i], &values[i]));
AKSL_CHECK_OK(aksl_tree_insert(&root, &node[i], &compare_ints));
}
AKSL_CHECK_OK(aksl_tree_count(root, &count));
AKSL_CHECK(count == 7);
AKSL_CHECK_OK(aksl_tree_height(root, &height));
AKSL_CHECK(height == 3);
/* Sorted input gives a degenerate chain: 4 nodes, 4 levels. This is a plain
* unbalanced BST and does not pretend otherwise. */
root = NULL;
for ( i = 0; i < 4; i++ ) {
AKSL_CHECK_OK(aksl_tree_node_init(&node[i], &chain[i]));
AKSL_CHECK_OK(aksl_tree_insert(&root, &node[i], &compare_ints));
}
AKSL_CHECK_OK(aksl_tree_height(root, &height));
AKSL_CHECK(height == 4);
AKSL_CHECK_STATUS(aksl_tree_height(root, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_tree_count(root, NULL), AKERR_NULLPOINTER);
return 0;
}
static int test_tree_free_all(void)
{
static int values[5] = { 5, 3, 8, 1, 9 };
aksl_TreeNode *root = NULL;
aksl_TreeNode *node = NULL;
int i = 0;
for ( i = 0; i < 5; i++ ) {
AKSL_CHECK_OK(aksl_malloc(sizeof(aksl_TreeNode), (void **)&node));
AKSL_CHECK_OK(aksl_tree_node_init(node, &values[i]));
AKSL_CHECK_OK(aksl_tree_insert(&root, node, &compare_ints));
}
AKSL_CHECK_OK(aksl_tree_free_all(&root, NULL));
AKSL_CHECK(root == NULL);
/* An empty tree frees cleanly and a second call is a no-op. */
AKSL_CHECK_OK(aksl_tree_free_all(&root, NULL));
AKSL_CHECK_STATUS(aksl_tree_free_all(NULL, NULL), AKERR_NULLPOINTER);
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_prepend_moves_the_head);
AKSL_RUN(failures, test_insert_after_and_before);
AKSL_RUN(failures, test_length_counts_and_refuses_cycles);
AKSL_RUN(failures, test_find_returns_the_first_match_or_null);
AKSL_RUN(failures, test_reverse_flips_both_directions);
AKSL_RUN(failures, test_concat_joins_two_lists);
AKSL_RUN(failures, test_iterate_reverse_walks_back_to_the_head);
AKSL_RUN(failures, test_free_all_releases_every_node);
AKSL_RUN(failures, test_container_push_and_unshift);
AKSL_RUN(failures, test_container_remove_maintains_the_endpoints);
AKSL_RUN(failures, test_container_clear);
AKSL_RUN(failures, test_tree_insert_orders_the_leaves);
AKSL_RUN(failures, test_tree_insert_sets_the_parent_links);
AKSL_RUN(failures, test_tree_find);
AKSL_RUN(failures, test_tree_remove_all_three_cases);
AKSL_RUN(failures, test_tree_remove_the_only_node);
AKSL_RUN(failures, test_tree_height_and_count);
AKSL_RUN(failures, test_tree_free_all);
AKSL_REPORT(failures);
}

417
tests/test_hashmap.c Normal file
View File

@@ -0,0 +1,417 @@
/*
* The fixed-capacity hash map and FNV-1a -- src/collections.c, TODO.md 3.6.
*
* "The single most obviously-missing data structure in the library", by the
* TODO's own account: akbasic needed one three times over -- variables,
* functions and labels -- and wrote ~130 lines of open-addressed table to get it.
*
* The behaviour worth testing hardest is the tombstone. Deleting a key that
* something else probed past has to leave a marker rather than an empty slot,
* because an empty slot ends a probe chain and would make the later key
* unreachable while it is still sitting there.
*/
#include "aksl_capture.h"
#define CAP 16
typedef struct SeenLog
{
int count;
char keys[CAP][AKSL_HASHMAP_MAX_KEY];
int break_at;
} SeenLog;
static akerr_ErrorContext AKERR_NOIGNORE *record_entry(const char *key, void *value, void *data)
{
SeenLog *log = NULL;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, key, AKERR_NULLPOINTER, "key");
FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data");
(void)value;
log = (SeenLog *)data;
if ( log->count < CAP ) {
snprintf(log->keys[log->count], AKSL_HASHMAP_MAX_KEY, "%s", key);
}
log->count += 1;
if ( log->break_at == log->count - 1 ) {
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop");
}
SUCCEED_RETURN(e);
}
/* ---------------------------------------------------------------------- */
/* FNV-1a */
/* ---------------------------------------------------------------------- */
/*
* The reference values are the canonical 32-bit FNV-1a ones: h = 2166136261,
* then h = (h XOR byte) * 16777619 for each byte. "a" and "foobar" are the
* vectors from the FNV reference material.
*/
static int test_fnv1a_known_answers(void)
{
uint32_t h = 0;
AKSL_CHECK_OK(aksl_strhash_fnv1a("", 0, &h));
AKSL_CHECK(h == 2166136261u);
AKSL_CHECK_OK(aksl_strhash_fnv1a("a", 1, &h));
AKSL_CHECK(h == 0xe40c292cu);
AKSL_CHECK_OK(aksl_strhash_fnv1a("foobar", 6, &h));
AKSL_CHECK(h == 0xbf9cf968u);
/* The NUL-terminated form agrees with the length-driven one. */
AKSL_CHECK_OK(aksl_strhash_fnv1a_str("foobar", &h));
AKSL_CHECK(h == 0xbf9cf968u);
AKSL_CHECK_STATUS(aksl_strhash_fnv1a(NULL, 0, &h), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strhash_fnv1a("a", 1, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strhash_fnv1a_str(NULL, &h), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strhash_fnv1a_str("a", NULL), AKERR_NULLPOINTER);
return 0;
}
/* High bytes are unsigned here for the same reason they are in djb2. */
static int test_fnv1a_high_bit_bytes_are_unsigned(void)
{
char buf[2] = { (char)0xff, (char)0xfe };
uint32_t h = 0;
uint32_t expected = 2166136261u;
expected = (expected ^ 0xffu) * 16777619u;
expected = (expected ^ 0xfeu) * 16777619u;
AKSL_CHECK_OK(aksl_strhash_fnv1a(buf, sizeof(buf), &h));
AKSL_CHECK(h == expected);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Put and get */
/* ---------------------------------------------------------------------- */
static int test_put_and_get(void)
{
aksl_HashEntry slots[CAP];
aksl_HashMap map;
int a = 1;
int b = 2;
void *value = NULL;
int found = 99;
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
AKSL_CHECK(map.count == 0);
AKSL_CHECK(map.capacity == CAP);
AKSL_CHECK_OK(aksl_hashmap_put(&map, "alpha", &a));
AKSL_CHECK_OK(aksl_hashmap_put(&map, "beta", &b));
AKSL_CHECK(map.count == 2);
AKSL_CHECK_OK(aksl_hashmap_get(&map, "alpha", &value, &found));
AKSL_CHECK(found == 1);
AKSL_CHECK(value == &a);
AKSL_CHECK_OK(aksl_hashmap_get(&map, "beta", &value, &found));
AKSL_CHECK(found == 1);
AKSL_CHECK(value == &b);
/* A key that is not there is found = 0 and success, not an error: looking
* something up and not finding it is the ordinary case for a symbol table. */
value = (void *)0x1;
AKSL_CHECK_OK(aksl_hashmap_get(&map, "gamma", &value, &found));
AKSL_CHECK(found == 0);
AKSL_CHECK(value == (void *)0x1); /* untouched when the key is absent */
/* NULL value out-param is allowed: sometimes you only want to know if it is there. */
AKSL_CHECK_OK(aksl_hashmap_get(&map, "alpha", NULL, &found));
AKSL_CHECK(found == 1);
return 0;
}
/* Putting an existing key replaces the value rather than adding a second entry. */
static int test_put_replaces_an_existing_key(void)
{
aksl_HashEntry slots[CAP];
aksl_HashMap map;
int a = 1;
int b = 2;
void *value = NULL;
int found = 0;
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
AKSL_CHECK_OK(aksl_hashmap_put(&map, "key", &a));
AKSL_CHECK_OK(aksl_hashmap_put(&map, "key", &b));
AKSL_CHECK(map.count == 1);
AKSL_CHECK_OK(aksl_hashmap_get(&map, "key", &value, &found));
AKSL_CHECK(found == 1);
AKSL_CHECK(value == &b);
return 0;
}
/* The map copies keys into its slots, so it owns them and cannot outlive them. */
static int test_the_map_owns_its_keys(void)
{
aksl_HashEntry slots[CAP];
aksl_HashMap map;
char key[16] = "transient";
int a = 1;
int found = 0;
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
AKSL_CHECK_OK(aksl_hashmap_put(&map, key, &a));
/* Scribble over the caller's copy of the key. */
memset(key, 'Z', sizeof(key) - 1);
AKSL_CHECK_OK(aksl_hashmap_get(&map, "transient", NULL, &found));
AKSL_CHECK(found == 1);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Bounds */
/* ---------------------------------------------------------------------- */
/*
* Full is an error naming the capacity, not a silent resize. A table that
* reallocates is a table whose entry pointers move underneath anything holding
* one; this one has a worst case you can state.
*/
static int test_a_full_map_refuses_rather_than_resizing(void)
{
aksl_HashEntry slots[4];
aksl_HashMap map;
char key[16];
int i = 0;
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4));
for ( i = 0; i < 4; i++ ) {
snprintf(key, sizeof(key), "k%d", i);
AKSL_CHECK_OK(aksl_hashmap_put(&map, key, NULL));
}
AKSL_CHECK(map.count == 4);
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_hashmap_put(&map, "overflow", NULL),
AKERR_OUTOFBOUNDS, "does not resize");
AKSL_CHECK(map.count == 4);
/* Everything already in there is still reachable and still correct. */
for ( i = 0; i < 4; i++ ) {
int found = 0;
snprintf(key, sizeof(key), "k%d", i);
AKSL_CHECK_OK(aksl_hashmap_get(&map, key, NULL, &found));
AKSL_CHECK(found == 1);
}
return 0;
}
/*
* A key too long for a slot is refused rather than truncated. Truncation would
* silently collide two different keys that share a prefix, which is the worst
* possible failure for a symbol table -- a lookup that returns the wrong thing.
*/
static int test_an_oversized_key_is_refused(void)
{
aksl_HashEntry slots[CAP];
aksl_HashMap map;
char longkey[AKSL_HASHMAP_MAX_KEY + 8];
int found = 0;
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
memset(longkey, 'k', sizeof(longkey) - 1);
longkey[sizeof(longkey) - 1] = '\0';
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_hashmap_put(&map, longkey, NULL),
AKERR_OUTOFBOUNDS, "AKSL_HASHMAP_MAX_KEY");
AKSL_CHECK_STATUS(aksl_hashmap_get(&map, longkey, NULL, &found), AKERR_OUTOFBOUNDS);
AKSL_CHECK_STATUS(aksl_hashmap_remove(&map, longkey, NULL), AKERR_OUTOFBOUNDS);
/* One byte short of the limit is fine: it is a limit, not an off-by-one. */
longkey[AKSL_HASHMAP_MAX_KEY - 1] = '\0';
AKSL_CHECK_OK(aksl_hashmap_put(&map, longkey, NULL));
AKSL_CHECK_OK(aksl_hashmap_get(&map, longkey, NULL, &found));
AKSL_CHECK(found == 1);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Removal and tombstones */
/* ---------------------------------------------------------------------- */
static int test_remove(void)
{
aksl_HashEntry slots[CAP];
aksl_HashMap map;
int a = 1;
int found = 99;
int removed = 99;
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
AKSL_CHECK_OK(aksl_hashmap_put(&map, "alpha", &a));
AKSL_CHECK_OK(aksl_hashmap_remove(&map, "alpha", &removed));
AKSL_CHECK(removed == 1);
AKSL_CHECK(map.count == 0);
AKSL_CHECK_OK(aksl_hashmap_get(&map, "alpha", NULL, &found));
AKSL_CHECK(found == 0);
/* Removing what is not there is success with removed = 0. */
AKSL_CHECK_OK(aksl_hashmap_remove(&map, "alpha", &removed));
AKSL_CHECK(removed == 0);
/* The out-param is optional. */
AKSL_CHECK_OK(aksl_hashmap_remove(&map, "nothing", NULL));
return 0;
}
/*
* The tombstone case, which is the one an open-addressed table gets wrong.
*
* A tiny capacity forces collisions, so several keys share a probe chain.
* Deleting one from the middle of that chain must not cut the keys behind it
* loose: if the slot were simply emptied, the probe for a later key would stop
* there and report the key missing while it was still sitting in the table.
*/
static int test_removal_does_not_break_a_probe_chain(void)
{
aksl_HashEntry slots[4];
aksl_HashMap map;
char key[16];
int found = 0;
int i = 0;
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4));
for ( i = 0; i < 4; i++ ) {
snprintf(key, sizeof(key), "k%d", i);
AKSL_CHECK_OK(aksl_hashmap_put(&map, key, NULL));
}
/* Take out each key in turn and confirm every remaining one is still found. */
for ( i = 0; i < 4; i++ ) {
int j = 0;
snprintf(key, sizeof(key), "k%d", i);
AKSL_CHECK_OK(aksl_hashmap_remove(&map, key, NULL));
for ( j = i + 1; j < 4; j++ ) {
snprintf(key, sizeof(key), "k%d", j);
AKSL_CHECK_OK(aksl_hashmap_get(&map, key, NULL, &found));
AKSL_CHECK(found == 1);
}
}
AKSL_CHECK(map.count == 0);
return 0;
}
/* A tombstone is reusable, so a table churned through repeatedly does not fill up. */
static int test_tombstones_are_reused(void)
{
aksl_HashEntry slots[4];
aksl_HashMap map;
char key[16];
int i = 0;
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4));
/* Far more insert/remove cycles than there are slots. */
for ( i = 0; i < 64; i++ ) {
snprintf(key, sizeof(key), "cycle%d", i);
AKSL_CHECK_OK(aksl_hashmap_put(&map, key, NULL));
AKSL_CHECK_OK(aksl_hashmap_remove(&map, key, NULL));
}
AKSL_CHECK(map.count == 0);
/* Still usable afterwards. */
AKSL_CHECK_OK(aksl_hashmap_put(&map, "final", NULL));
AKSL_CHECK(map.count == 1);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Iteration */
/* ---------------------------------------------------------------------- */
static int test_iterate_visits_every_live_entry(void)
{
aksl_HashEntry slots[CAP];
aksl_HashMap map;
SeenLog log;
char key[16];
int i = 0;
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
for ( i = 0; i < 5; i++ ) {
snprintf(key, sizeof(key), "k%d", i);
AKSL_CHECK_OK(aksl_hashmap_put(&map, key, NULL));
}
/* One removed, so there is a tombstone for the walk to skip. */
AKSL_CHECK_OK(aksl_hashmap_remove(&map, "k2", NULL));
memset((void *)&log, 0x00, sizeof(log));
log.break_at = -1;
AKSL_CHECK_OK(aksl_hashmap_iterate(&map, &record_entry, &log));
AKSL_CHECK(log.count == 4);
/* The break stops it, as everywhere else in this library. */
memset((void *)&log, 0x00, sizeof(log));
log.break_at = 1;
AKSL_CHECK_OK(aksl_hashmap_iterate(&map, &record_entry, &log));
AKSL_CHECK(log.count == 2);
return 0;
}
static int test_rejects_null_and_uninitialised(void)
{
aksl_HashEntry slots[CAP];
aksl_HashMap map;
aksl_HashMap empty;
SeenLog log;
int found = 0;
memset((void *)&empty, 0x00, sizeof(empty));
memset((void *)&log, 0x00, sizeof(log));
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
AKSL_CHECK_STATUS(aksl_hashmap_init(NULL, slots, CAP), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_init(&map, NULL, CAP), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_init(&map, slots, 0), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_hashmap_put(NULL, "k", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_put(&map, NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_get(NULL, "k", NULL, &found), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_get(&map, NULL, NULL, &found), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_get(&map, "k", NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_remove(NULL, "k", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_remove(&map, NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_iterate(NULL, &record_entry, &log), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_iterate(&map, NULL, &log), AKERR_NULLPOINTER);
/* A map that was never initialised is caught rather than dereferenced. */
AKSL_CHECK_STATUS(aksl_hashmap_put(&empty, "k", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_get(&empty, "k", NULL, &found), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_remove(&empty, "k", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_iterate(&empty, &record_entry, &log), AKERR_NULLPOINTER);
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_fnv1a_known_answers);
AKSL_RUN(failures, test_fnv1a_high_bit_bytes_are_unsigned);
AKSL_RUN(failures, test_put_and_get);
AKSL_RUN(failures, test_put_replaces_an_existing_key);
AKSL_RUN(failures, test_the_map_owns_its_keys);
AKSL_RUN(failures, test_a_full_map_refuses_rather_than_resizing);
AKSL_RUN(failures, test_an_oversized_key_is_refused);
AKSL_RUN(failures, test_remove);
AKSL_RUN(failures, test_removal_does_not_break_a_probe_chain);
AKSL_RUN(failures, test_tombstones_are_reused);
AKSL_RUN(failures, test_iterate_visits_every_live_entry);
AKSL_RUN(failures, test_rejects_null_and_uninitialised);
AKSL_REPORT(failures);
}

258
tests/test_strbuf.c Normal file
View File

@@ -0,0 +1,258 @@
/*
* 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);
}

610
tests/test_streamio.c Normal file
View File

@@ -0,0 +1,610 @@
/*
* 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;
}
/* ---------------------------------------------------------------------- */
/* 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_remove_and_rename);
AKSL_RUN(failures, test_mkstemp_and_mkdtemp);
AKSL_REPORT(failures);
}

448
tests/test_string.c Normal file
View File

@@ -0,0 +1,448 @@
/*
* String wrappers -- src/string.c, TODO.md section 3.1.
*
* The two contracts worth testing hardest are the ones that differ from libc:
* every copying function takes the destination size and treats truncation as an
* error that writes nothing, and every searching function treats "not found" as
* a successful answer of NULL.
*/
#include "aksl_capture.h"
#include <errno.h>
/* ---------------------------------------------------------------------- */
/* Length */
/* ---------------------------------------------------------------------- */
static int test_strlen_and_strnlen(void)
{
size_t n = 99;
AKSL_CHECK_OK(aksl_strlen("hello", &n));
AKSL_CHECK(n == 5);
AKSL_CHECK_OK(aksl_strlen("", &n));
AKSL_CHECK(n == 0);
/* strnlen stops at maxlen whether or not it found a terminator. */
AKSL_CHECK_OK(aksl_strnlen("hello", 3, &n));
AKSL_CHECK(n == 3);
AKSL_CHECK_OK(aksl_strnlen("hello", 99, &n));
AKSL_CHECK(n == 5);
AKSL_CHECK_STATUS(aksl_strlen(NULL, &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strlen("x", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strnlen(NULL, 1, &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strnlen("x", 1, NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Copying */
/* ---------------------------------------------------------------------- */
static int test_strcpy_copies_within_the_buffer(void)
{
char buf[8];
AKSL_CHECK_OK(aksl_strcpy(buf, sizeof(buf), "abc"));
AKSL_CHECK(strcmp(buf, "abc") == 0);
/* Exactly filling the buffer, terminator included, is not truncation. */
AKSL_CHECK_OK(aksl_strcpy(buf, sizeof(buf), "1234567"));
AKSL_CHECK(strcmp(buf, "1234567") == 0);
return 0;
}
/*
* The whole point of taking dstsize. akbasic writes this check by hand at ten
* sites: a length test, then strncpy, then an explicit NUL.
*/
static int test_strcpy_truncation_is_an_error_and_writes_nothing(void)
{
char buf[8] = "previous";
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strcpy(buf, sizeof(buf), "12345678"),
AKERR_OUTOFBOUNDS, "destination holds");
/*
* Empty, not a truncated prefix. A caller who ignores the status gets
* nothing, which is far easier to notice than a plausible-looking "1234567".
*/
AKSL_CHECK(buf[0] == '\0');
return 0;
}
/*
* strncpy(3) leaves the destination unterminated when the source is at least n
* bytes, and NUL-pads the whole remainder when it is shorter. This does neither.
*/
static int test_strncpy_always_terminates_and_never_pads(void)
{
char buf[16];
memset(buf, 'Z', sizeof(buf));
AKSL_CHECK_OK(aksl_strncpy(buf, sizeof(buf), "abcdef", 3));
AKSL_CHECK(strcmp(buf, "abc") == 0);
/* Not padded: everything past the terminator is untouched. */
AKSL_CHECK(buf[4] == 'Z');
/* n larger than the source just copies the source. */
AKSL_CHECK_OK(aksl_strncpy(buf, sizeof(buf), "abc", 99));
AKSL_CHECK(strcmp(buf, "abc") == 0);
/* n bytes that do not fit the destination is still an error. */
AKSL_CHECK_STATUS(aksl_strncpy(buf, 4, "abcdef", 6), AKERR_OUTOFBOUNDS);
AKSL_CHECK(buf[0] == '\0');
return 0;
}
static int test_strcat_appends_within_the_buffer(void)
{
char buf[16] = "ab";
AKSL_CHECK_OK(aksl_strcat(buf, sizeof(buf), "cd"));
AKSL_CHECK(strcmp(buf, "abcd") == 0);
AKSL_CHECK_OK(aksl_strcat(buf, sizeof(buf), ""));
AKSL_CHECK(strcmp(buf, "abcd") == 0);
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strcat(buf, sizeof(buf), "0123456789ab"),
AKERR_OUTOFBOUNDS, "in use plus");
/* The existing contents survive a refused append. */
AKSL_CHECK(strcmp(buf, "abcd") == 0);
return 0;
}
/* An unterminated destination is refused rather than walked off the end of. */
static int test_strcat_refuses_an_unterminated_destination(void)
{
char buf[4];
memset(buf, 'x', sizeof(buf));
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strcat(buf, sizeof(buf), "y"),
AKERR_VALUE, "not terminated");
return 0;
}
static int test_strncat_appends_at_most_n(void)
{
char buf[16] = "ab";
AKSL_CHECK_OK(aksl_strncat(buf, sizeof(buf), "cdef", 2));
AKSL_CHECK(strcmp(buf, "abcd") == 0);
AKSL_CHECK_STATUS(aksl_strncat(buf, 6, "xyz", 3), AKERR_OUTOFBOUNDS);
AKSL_CHECK(strcmp(buf, "abcd") == 0);
return 0;
}
static int test_copying_rejects_null_and_zero_size(void)
{
char buf[8] = "";
AKSL_CHECK_STATUS(aksl_strcpy(NULL, 8, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcpy(buf, sizeof(buf), NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcpy(buf, 0, "x"), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_strncpy(NULL, 8, "x", 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncpy(buf, sizeof(buf), NULL, 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncpy(buf, 0, "x", 1), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_strcat(NULL, 8, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcat(buf, sizeof(buf), NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcat(buf, 0, "x"), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_strncat(NULL, 8, "x", 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncat(buf, sizeof(buf), NULL, 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncat(buf, 0, "x", 1), AKERR_VALUE);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Duplication */
/* ---------------------------------------------------------------------- */
static int test_strdup_and_strndup(void)
{
char *copy = NULL;
AKSL_CHECK_OK(aksl_strdup("duplicate me", &copy));
AKSL_CHECK(copy != NULL);
AKSL_CHECK(strcmp(copy, "duplicate me") == 0);
AKSL_CHECK_OK(aksl_freep((void **)&copy));
AKSL_CHECK(copy == NULL);
AKSL_CHECK_OK(aksl_strndup("duplicate me", 9, &copy));
AKSL_CHECK(strcmp(copy, "duplicate") == 0);
AKSL_CHECK_OK(aksl_freep((void **)&copy));
/* n past the end of the string just copies the string. */
AKSL_CHECK_OK(aksl_strndup("ab", 99, &copy));
AKSL_CHECK(strcmp(copy, "ab") == 0);
AKSL_CHECK_OK(aksl_freep((void **)&copy));
AKSL_CHECK_STATUS(aksl_strdup(NULL, &copy), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strdup("x", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strndup(NULL, 1, &copy), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strndup("x", 1, NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Comparison */
/* ---------------------------------------------------------------------- */
static int test_comparisons(void)
{
int r = 99;
AKSL_CHECK_OK(aksl_strcmp("abc", "abc", &r));
AKSL_CHECK(r == 0);
AKSL_CHECK_OK(aksl_strcmp("abc", "abd", &r));
AKSL_CHECK(r < 0);
AKSL_CHECK_OK(aksl_strcmp("abd", "abc", &r));
AKSL_CHECK(r > 0);
AKSL_CHECK_OK(aksl_strncmp("abcXX", "abcYY", 3, &r));
AKSL_CHECK(r == 0);
AKSL_CHECK_OK(aksl_strncmp("abcXX", "abcYY", 4, &r));
AKSL_CHECK(r != 0);
/* The three case-folding sites akbasic writes out by hand. */
AKSL_CHECK_OK(aksl_strcasecmp("PRINT", "print", &r));
AKSL_CHECK(r == 0);
AKSL_CHECK_OK(aksl_strcasecmp("PRINT", "input", &r));
AKSL_CHECK(r != 0);
AKSL_CHECK_OK(aksl_strncasecmp("PRINTXX", "printYY", 5, &r));
AKSL_CHECK(r == 0);
AKSL_CHECK_OK(aksl_strcoll("abc", "abc", &r));
AKSL_CHECK(r == 0);
AKSL_CHECK_STATUS(aksl_strcmp(NULL, "b", &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcmp("a", NULL, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcmp("a", "b", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncmp(NULL, "b", 1, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncmp("a", NULL, 1, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncmp("a", "b", 1, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcasecmp(NULL, "b", &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcasecmp("a", NULL, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcasecmp("a", "b", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncasecmp(NULL, "b", 1, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncasecmp("a", NULL, 1, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncasecmp("a", "b", 1, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcoll(NULL, "b", &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcoll("a", NULL, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcoll("a", "b", NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Searching */
/* ---------------------------------------------------------------------- */
/*
* Every one of these treats absence as an answer. A library that raised on it
* would have every caller catching a non-error, and burning a pool slot to do
* so.
*/
static int test_searching_reports_absence_as_success(void)
{
const char *s = "the quick brown fox";
char *at = (char *)0x1;
size_t n = 99;
AKSL_CHECK_OK(aksl_strchr(s, 'q', &at));
AKSL_CHECK(at == s + 4);
AKSL_CHECK_OK(aksl_strchr(s, 'Z', &at));
AKSL_CHECK(at == NULL);
AKSL_CHECK_OK(aksl_strrchr(s, 'o', &at));
AKSL_CHECK(at == s + 17);
AKSL_CHECK_OK(aksl_strrchr(s, 'Z', &at));
AKSL_CHECK(at == NULL);
AKSL_CHECK_OK(aksl_strstr(s, "brown", &at));
AKSL_CHECK(at == s + 10);
AKSL_CHECK_OK(aksl_strstr(s, "purple", &at));
AKSL_CHECK(at == NULL);
AKSL_CHECK_OK(aksl_strpbrk(s, "xq", &at));
AKSL_CHECK(at == s + 4);
AKSL_CHECK_OK(aksl_strpbrk(s, "ZY", &at));
AKSL_CHECK(at == NULL);
AKSL_CHECK_OK(aksl_strspn("aaabbb", "a", &n));
AKSL_CHECK(n == 3);
AKSL_CHECK_OK(aksl_strcspn("aaabbb", "b", &n));
AKSL_CHECK(n == 3);
return 0;
}
/* The open-coded case-insensitive search, including its edge cases. */
static int test_strcasestr(void)
{
const char *s = "The Quick Brown Fox";
char *at = (char *)0x1;
AKSL_CHECK_OK(aksl_strcasestr(s, "quick", &at));
AKSL_CHECK(at == s + 4);
AKSL_CHECK_OK(aksl_strcasestr(s, "QUICK", &at));
AKSL_CHECK(at == s + 4);
AKSL_CHECK_OK(aksl_strcasestr(s, "purple", &at));
AKSL_CHECK(at == NULL);
/* An empty needle matches at the start, as strstr(3) has it. */
AKSL_CHECK_OK(aksl_strcasestr(s, "", &at));
AKSL_CHECK(at == s);
/* A needle longer than the haystack cannot match, and must not read past. */
AKSL_CHECK_OK(aksl_strcasestr("ab", "abcdef", &at));
AKSL_CHECK(at == NULL);
/* A match right at the end. */
AKSL_CHECK_OK(aksl_strcasestr(s, "fox", &at));
AKSL_CHECK(at == s + 16);
return 0;
}
static int test_searching_rejects_null_arguments(void)
{
char *at = NULL;
size_t n = 0;
AKSL_CHECK_STATUS(aksl_strchr(NULL, 'a', &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strchr("a", 'a', NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strrchr(NULL, 'a', &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strrchr("a", 'a', NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strstr(NULL, "a", &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strstr("a", NULL, &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strstr("a", "a", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcasestr(NULL, "a", &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcasestr("a", NULL, &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcasestr("a", "a", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strpbrk(NULL, "a", &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strpbrk("a", NULL, &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strpbrk("a", "a", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strspn(NULL, "a", &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strspn("a", NULL, &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strspn("a", "a", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcspn(NULL, "a", &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcspn("a", NULL, &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcspn("a", "a", NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Tokenising */
/* ---------------------------------------------------------------------- */
static int test_strtok_r_walks_the_tokens(void)
{
char input[] = "one,two,,three";
char *save = NULL;
char *tok = NULL;
int seen = 0;
AKSL_CHECK_OK(aksl_strtok_r(input, ",", &save, &tok));
AKSL_CHECK(tok != NULL && strcmp(tok, "one") == 0);
seen++;
while ( 1 ) {
AKSL_CHECK_OK(aksl_strtok_r(NULL, ",", &save, &tok));
if ( tok == NULL ) {
break;
}
seen++;
}
/* strtok_r collapses the empty field between the two commas. */
AKSL_CHECK(seen == 3);
AKSL_CHECK_STATUS(aksl_strtok_r(NULL, NULL, &save, &tok), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strtok_r(NULL, ",", NULL, &tok), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strtok_r(NULL, ",", &save, NULL), AKERR_NULLPOINTER);
return 0;
}
/* strsep keeps the empty fields, which is why it exists alongside strtok_r. */
static int test_strsep_keeps_empty_fields(void)
{
char input[] = "one,two,,three";
char *cursor = input;
char *tok = NULL;
int seen = 0;
while ( cursor != NULL ) {
AKSL_CHECK_OK(aksl_strsep(&cursor, ",", &tok));
if ( tok == NULL ) {
break;
}
seen++;
}
AKSL_CHECK(seen == 4);
AKSL_CHECK_STATUS(aksl_strsep(NULL, ",", &tok), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strsep(&cursor, NULL, &tok), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strsep(&cursor, ",", NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Status messages */
/* ---------------------------------------------------------------------- */
/*
* aksl_strerror knows this library's own statuses as well as errno values,
* which is the reason it is not a strerror_r wrapper: strerror_r could never
* name AKERR_NULLPOINTER.
*/
static int test_strerror_names_both_kinds_of_status(void)
{
char buf[128];
AKSL_CHECK_OK(aksl_strerror(ENOENT, buf, sizeof(buf)));
AKSL_CHECK(buf[0] != '\0');
AKSL_CHECK(strcmp(buf, "Unknown status 2") != 0);
AKSL_CHECK_OK(aksl_strerror(AKERR_NULLPOINTER, buf, sizeof(buf)));
AKSL_CHECK(buf[0] != '\0');
/* A status nothing has a name for is rendered as its number. */
AKSL_CHECK_OK(aksl_strerror(-98765, buf, sizeof(buf)));
AKSL_CHECK(strcmp(buf, "Unknown status -98765") == 0);
/* Too small a buffer is an error, and leaves nothing rather than a prefix. */
AKSL_CHECK_STATUS(aksl_strerror(ENOENT, buf, 2), AKERR_OUTOFBOUNDS);
AKSL_CHECK(buf[0] == '\0');
AKSL_CHECK_STATUS(aksl_strerror(ENOENT, NULL, 16), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strerror(ENOENT, buf, 0), AKERR_VALUE);
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_strlen_and_strnlen);
AKSL_RUN(failures, test_strcpy_copies_within_the_buffer);
AKSL_RUN(failures, test_strcpy_truncation_is_an_error_and_writes_nothing);
AKSL_RUN(failures, test_strncpy_always_terminates_and_never_pads);
AKSL_RUN(failures, test_strcat_appends_within_the_buffer);
AKSL_RUN(failures, test_strcat_refuses_an_unterminated_destination);
AKSL_RUN(failures, test_strncat_appends_at_most_n);
AKSL_RUN(failures, test_copying_rejects_null_and_zero_size);
AKSL_RUN(failures, test_strdup_and_strndup);
AKSL_RUN(failures, test_comparisons);
AKSL_RUN(failures, test_searching_reports_absence_as_success);
AKSL_RUN(failures, test_strcasestr);
AKSL_RUN(failures, test_searching_rejects_null_arguments);
AKSL_RUN(failures, test_strtok_r_walks_the_tokens);
AKSL_RUN(failures, test_strsep_keeps_empty_fields);
AKSL_RUN(failures, test_strerror_names_both_kinds_of_status);
AKSL_REPORT(failures);
}