Version at 0.2.0: complete the wishlist, document it, gate the docs
Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that
file to hold outstanding items only.
The API break gets a minor bump, because pre-1.0 the soname carries
MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five
signatures changed and the ato* contract with them; UPGRADING.md is new
and lists every one, with the before/after for the cases the compiler
cannot warn about.
Section 3.1 is finished: reallocarray with the multiplication checked,
aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf.
Four functions on that list are deliberately absent rather than missing
-- sprintf, strtok, setbuf and perror -- and TODO.md now says which and
why, so nobody adds them thinking they were forgotten.
Section 1.9, the cross-cutting tests:
tests/test_pool.c drives every failure path AKERR_MAX_ARRAY_ERROR
+ 10 times and checks the pool after each round,
because a wrapper that leaks a slot fails a
hundred calls later in unrelated code. It also
asserts that each error names the function and
file it was raised from, which is what catches a
FAIL that migrates into a helper during a
refactor: status right, message right, origin
quietly lying.
tests/negative/ two sources that must FAIL to compile, built with
-Werror and registered WILL_FAIL. AKERR_NOIGNORE
and the format attributes are enforced by the
compiler and by nothing else; drop either and
every ordinary test still passes.
Thread safety is answered rather than tested: the library is not
thread-safe and cannot be made so from here, because libakerror's error
pool is an unlocked process-global array. README.md says so plainly and
TODO.md carries it as the item blocking any future pthread wrappers.
Doxygen is configured and gated. All 147 public functions have @brief,
a @param each, @throws per status and @return; EXTRACT_ALL is off and
WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an
undocumented entity. It ran to 0 warnings. The Doxyfile carries no
version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from
project(), so that stays the one place a version is written.
CI now builds against the submodule it pins instead of also installing
libakerror@main and never linking it, adds -Werror, and gains a
sanitizer job. The pre-push hook matches, and runs the docs check too.
Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The
eight uncovered lines are each uncovered on purpose and TODO.md says
which and why.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -170,6 +170,15 @@ static char aksl_last_message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH];
|
||||
/* Sized to match akerr_ErrorContext.function, which akerror declares with
|
||||
* AKERR_MAX_ERROR_FNAME_LENGTH rather than AKERR_MAX_ERROR_FUNCTION_LENGTH. */
|
||||
static char aksl_last_function[AKERR_MAX_ERROR_FNAME_LENGTH];
|
||||
/*
|
||||
* The source location the error was raised from. Captured so that
|
||||
* tests/test_pool.c can assert an error came from the wrapper it names rather
|
||||
* than from somewhere further down -- an error that reports the wrong origin is
|
||||
* worse than useless when the only debugging you get is a log file after the
|
||||
* fact, which is the stated reason this library exists.
|
||||
*/
|
||||
static char aksl_last_file[AKERR_MAX_ERROR_FNAME_LENGTH];
|
||||
static int aksl_last_line = 0;
|
||||
|
||||
/*
|
||||
* Record the status/message/function of a returned context, release it back to
|
||||
@@ -184,6 +193,8 @@ static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e)
|
||||
|
||||
aksl_last_message[0] = '\0';
|
||||
aksl_last_function[0] = '\0';
|
||||
aksl_last_file[0] = '\0';
|
||||
aksl_last_line = 0;
|
||||
if ( e == NULL ) {
|
||||
aksl_last_status = 0;
|
||||
return 0;
|
||||
@@ -191,6 +202,8 @@ static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e)
|
||||
aksl_last_status = e->status;
|
||||
snprintf(aksl_last_message, sizeof(aksl_last_message), "%s", e->message);
|
||||
snprintf(aksl_last_function, sizeof(aksl_last_function), "%s", e->function);
|
||||
snprintf(aksl_last_file, sizeof(aksl_last_file), "%s", e->fname);
|
||||
aksl_last_line = e->lineno;
|
||||
/*
|
||||
* akerr_release_error is marked warn_unused_result, and a (void) cast does
|
||||
* not silence that in GCC, so the result is assigned and discarded.
|
||||
@@ -260,6 +273,8 @@ static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e)
|
||||
aksl_last_status = 0; \
|
||||
aksl_last_message[0] = '\0'; \
|
||||
aksl_last_function[0] = '\0'; \
|
||||
aksl_last_file[0] = '\0'; \
|
||||
aksl_last_line = 0; \
|
||||
} while ( 0 )
|
||||
|
||||
#define AKSL_CHECK_CONTAINS(needle) \
|
||||
|
||||
26
tests/negative/format_mismatch.c
Normal file
26
tests/negative/format_mismatch.c
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* NEGATIVE COMPILE TEST -- TODO.md sections 1.3 and 2.2.5.
|
||||
*
|
||||
* This file must NOT compile. It is built by the CTest entry
|
||||
* `negative_format_mismatch` with -Werror, and that test is marked WILL_FAIL.
|
||||
*
|
||||
* What it asserts: the format attributes on the variadic wrappers are attached
|
||||
* and effective. Going through a wrapper is exactly how a caller loses the
|
||||
* compile-time format checking it would have had calling printf(3) directly --
|
||||
* printf("%d", "str") is caught and an unattributed wrapper's equivalent is not.
|
||||
* AKSL_PRINTF_FORMAT is what restores it, and this is what proves it is there.
|
||||
*/
|
||||
|
||||
#include <akstdlib.h>
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char buf[64];
|
||||
int count = 0;
|
||||
akerr_ErrorContext *raised = NULL;
|
||||
|
||||
/* %d against a string. This is the line that must not build. */
|
||||
raised = aksl_snprintf(&count, buf, sizeof(buf), "%d", "not an int");
|
||||
|
||||
return raised == NULL ? 0 : 1;
|
||||
}
|
||||
26
tests/negative/noignore.c
Normal file
26
tests/negative/noignore.c
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* NEGATIVE COMPILE TEST -- TODO.md section 1.9.
|
||||
*
|
||||
* This file must NOT compile. It is built by the CTest entry `negative_noignore`
|
||||
* with -Werror, and that test is marked WILL_FAIL, so a successful build is a
|
||||
* test failure.
|
||||
*
|
||||
* What it asserts: AKERR_NOIGNORE is actually attached and actually effective.
|
||||
* Every entry point in this library returns an error context the caller must
|
||||
* look at, and the whole design rests on discarding one being hard rather than
|
||||
* merely inadvisable. AKERR_NOIGNORE expands to warn_unused_result, which does
|
||||
* nothing at all if it is dropped from a declaration during a refactor -- and
|
||||
* nothing about the resulting library would look wrong.
|
||||
*/
|
||||
|
||||
#include <akstdlib.h>
|
||||
|
||||
int main(void)
|
||||
{
|
||||
void *ptr = NULL;
|
||||
|
||||
/* Discarding the returned context. This is the line that must not build. */
|
||||
aksl_malloc(32, &ptr);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -149,6 +149,29 @@ static int test_insert_after_and_before(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Inserting before a node that is *not* the head, which relinks the node in
|
||||
* front of it as well -- a different path from inserting before the head, where
|
||||
* there is no such node.
|
||||
*/
|
||||
static int test_insert_before_a_middle_node(void)
|
||||
{
|
||||
aksl_ListNode node[3];
|
||||
aksl_ListNode fresh;
|
||||
aksl_ListNode *head = &node[0];
|
||||
|
||||
build_chain(node, 3);
|
||||
memset((void *)&fresh, 0x00, sizeof(fresh));
|
||||
|
||||
AKSL_CHECK_OK(aksl_list_insert_before(&head, &node[2], &fresh));
|
||||
AKSL_CHECK(head == &node[0]); /* unchanged, unlike the head case */
|
||||
AKSL_CHECK(node[1].next == &fresh);
|
||||
AKSL_CHECK(fresh.prev == &node[1]);
|
||||
AKSL_CHECK(fresh.next == &node[2]);
|
||||
AKSL_CHECK(node[2].prev == &fresh);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Inspection */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
@@ -177,6 +200,7 @@ static int test_find_returns_the_first_match_or_null(void)
|
||||
{
|
||||
aksl_ListNode node[N];
|
||||
aksl_ListNode *found = (aksl_ListNode *)0x1;
|
||||
aksl_ListNode *cyclic = NULL;
|
||||
int payload = 42;
|
||||
int absent = 0;
|
||||
|
||||
@@ -199,6 +223,19 @@ static int test_find_returns_the_first_match_or_null(void)
|
||||
aksl_list_find(&node[0], &failing_predicate, NULL, &found),
|
||||
AKERR_VALUE, "predicate refused");
|
||||
|
||||
/*
|
||||
* A cyclic list is refused before the walk starts rather than searched
|
||||
* forever. Every whole-list function shares one bounded tail walk, so this
|
||||
* covers the bound for find, reverse, concat and free_all alike.
|
||||
*/
|
||||
node[N - 1].next = &node[0];
|
||||
cyclic = &node[0];
|
||||
AKSL_CHECK_STATUS(aksl_list_find(&node[0], &match_data, &payload, &found),
|
||||
AKERR_CIRCULAR_REFERENCE);
|
||||
AKSL_CHECK_STATUS(aksl_list_reverse(&cyclic), AKERR_CIRCULAR_REFERENCE);
|
||||
AKSL_CHECK_STATUS(aksl_list_concat(&node[0], &node[2]), AKERR_CIRCULAR_REFERENCE);
|
||||
node[N - 1].next = NULL;
|
||||
|
||||
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;
|
||||
@@ -336,6 +373,257 @@ static int test_free_all_releases_every_node(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* A free function that fails on one node in the middle.
|
||||
*
|
||||
* Both *_free_all functions go out of their way to keep the first error and
|
||||
* carry on rather than returning immediately, because abandoning the walk would
|
||||
* leak everything after the node that failed -- turning one bad free into a leak
|
||||
* of the whole remaining structure. This is the test that says so.
|
||||
*/
|
||||
/* Defined with the tree tests below; declared here because the tree free-all
|
||||
* case belongs beside the list one rather than beside its comparator. */
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *compare_ints(void *a, void *b, int *dest);
|
||||
|
||||
static int failing_free_countdown = 0;
|
||||
static int failing_free_calls = 0;
|
||||
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *free_that_fails_once(void *ptr)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
failing_free_calls += 1;
|
||||
if ( failing_free_calls == failing_free_countdown ) {
|
||||
/* Refuse, but still release the memory: the point is the error path,
|
||||
* not a deliberate leak for the sanitizer to find. */
|
||||
free(ptr);
|
||||
FAIL_RETURN(e, AKERR_VALUE, "free refused on call %d", failing_free_calls);
|
||||
}
|
||||
free(ptr);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/*
|
||||
* And one that refuses every time, so more than one error is raised during a
|
||||
* single walk. Only the first is kept and handed back; the rest have to be
|
||||
* released, or a walk over n nodes with a broken free would consume n pool slots
|
||||
* and exhaust the pool -- the failure mode the whole pool-accounting section of
|
||||
* TODO.md 1.9 exists to catch.
|
||||
*/
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *free_that_always_fails(void *ptr)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
failing_free_calls += 1;
|
||||
free(ptr);
|
||||
FAIL_RETURN(e, AKERR_VALUE, "free refused on call %d", failing_free_calls);
|
||||
}
|
||||
|
||||
/*
|
||||
* And one that refuses from a given call onwards, which is what it takes to fail
|
||||
* repeatedly *inside* a drain: the dequeues that come first have to succeed, or
|
||||
* the walk stops with a real error before it ever reaches the break.
|
||||
*/
|
||||
static int failing_free_from = 0;
|
||||
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *free_that_fails_from(void *ptr)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
failing_free_calls += 1;
|
||||
free(ptr);
|
||||
if ( failing_free_calls >= failing_free_from ) {
|
||||
FAIL_RETURN(e, AKERR_VALUE, "free refused on call %d", failing_free_calls);
|
||||
}
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
static int test_free_all_releases_the_errors_it_does_not_return(void)
|
||||
{
|
||||
aksl_ListNode *head = NULL;
|
||||
aksl_ListNode *node = NULL;
|
||||
aksl_TreeNode *root = NULL;
|
||||
aksl_TreeNode *tnode = NULL;
|
||||
static int values[N] = { 5, 3, 8, 1, 9 };
|
||||
int i = 0;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
failing_free_calls = 0;
|
||||
/* The first of N errors comes back; the other N-1 must not leak. */
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_list_free_all(&head, &free_that_always_fails),
|
||||
AKERR_VALUE, "free refused on call 1");
|
||||
AKSL_CHECK(failing_free_calls == N);
|
||||
AKSL_CHECK(aksl_slots_in_use() == 0);
|
||||
|
||||
for ( i = 0; i < N; i++ ) {
|
||||
AKSL_CHECK_OK(aksl_malloc(sizeof(aksl_TreeNode), (void **)&tnode));
|
||||
AKSL_CHECK_OK(aksl_tree_node_init(tnode, &values[i]));
|
||||
AKSL_CHECK_OK(aksl_tree_insert(&root, tnode, &compare_ints));
|
||||
}
|
||||
|
||||
failing_free_calls = 0;
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_tree_free_all(&root, &free_that_always_fails),
|
||||
AKERR_VALUE, "free refused on call 1");
|
||||
AKSL_CHECK(failing_free_calls == N);
|
||||
AKSL_CHECK(aksl_slots_in_use() == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_list_free_all_reports_the_first_failure_but_frees_everything(void)
|
||||
{
|
||||
aksl_ListNode *head = NULL;
|
||||
aksl_ListNode *node = NULL;
|
||||
int i = 0;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
failing_free_calls = 0;
|
||||
failing_free_countdown = 2; /* fail on the second of five */
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_list_free_all(&head, &free_that_fails_once),
|
||||
AKERR_VALUE, "free refused on call 2");
|
||||
/* Every node was still visited: the walk did not stop at the failure. */
|
||||
AKSL_CHECK(failing_free_calls == N);
|
||||
AKSL_CHECK(head == NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_tree_free_all_reports_the_first_failure_but_frees_everything(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));
|
||||
}
|
||||
|
||||
failing_free_calls = 0;
|
||||
failing_free_countdown = 2;
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_tree_free_all(&root, &free_that_fails_once),
|
||||
AKERR_VALUE, "free refused on call 2");
|
||||
AKSL_CHECK(failing_free_calls == 5);
|
||||
AKSL_CHECK(root == NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* The same idea for the breadth-first traversal queue: a failing lfree during
|
||||
* the drain must not stop the drain, and must not mask the error that got the
|
||||
* walk there in the first place. The traversal itself still succeeds -- the
|
||||
* drain failure is logged and dropped, because losing the caller's real error to
|
||||
* report a bookkeeping one would be the worse trade.
|
||||
*/
|
||||
static aksl_TreeNode *break_on_node = NULL;
|
||||
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *break_at(aksl_TreeNode *node, void *data)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
(void)data;
|
||||
if ( node == break_on_node ) {
|
||||
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop here");
|
||||
}
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *plain_alloc(size_t size, void **dest)
|
||||
{
|
||||
return aksl_malloc(size, dest);
|
||||
}
|
||||
|
||||
static int test_bfs_queue_drain_survives_a_failing_free(void)
|
||||
{
|
||||
aksl_TreeNode tree[3];
|
||||
int i = 0;
|
||||
|
||||
for ( i = 0; i < 3; i++ ) {
|
||||
AKSL_CHECK_OK(aksl_tree_node_init(&tree[i], NULL));
|
||||
}
|
||||
tree[0].left = &tree[1];
|
||||
tree[0].right = &tree[2];
|
||||
|
||||
/*
|
||||
* The lfree calls, in order:
|
||||
* 1 the root's queue entry, released as it is dequeued
|
||||
* 2 the left child's entry, likewise
|
||||
* -- the callback breaks on the left child, leaving the right child queued
|
||||
* 3 the right child's entry, released by the drain in CLEANUP
|
||||
*
|
||||
* Failing on call 3 is therefore a failure inside the drain. The traversal
|
||||
* still succeeds: the break is not an error, and losing that answer in order
|
||||
* to report a bookkeeping failure would be the worse trade -- so the drain
|
||||
* error is logged and dropped, which is what this asserts.
|
||||
*/
|
||||
break_on_node = &tree[1];
|
||||
failing_free_calls = 0;
|
||||
failing_free_countdown = 3;
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &break_at,
|
||||
&plain_alloc, &free_that_fails_once,
|
||||
AKSL_TREE_SEARCH_BFS, NULL));
|
||||
AKSL_CHECK(failing_free_calls == 3);
|
||||
|
||||
/* And the pool is intact afterwards: the dropped context was released. */
|
||||
AKSL_CHECK(aksl_slots_in_use() == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* More than one failure inside a single drain, which is the case where the drain
|
||||
* has to release the errors it is not keeping. A seven-node tree broken on the
|
||||
* third visit leaves two entries queued:
|
||||
*
|
||||
* dequeue 0 (free 1), enqueue 1 and 2
|
||||
* dequeue 1 (free 2), enqueue 3 and 4 queue: 2 3 4
|
||||
* dequeue 2 (free 3), callback breaks queue: 3 4
|
||||
* drain frees 3 and 4 (free 4, free 5)
|
||||
*
|
||||
* With a free that refuses every time, the drain raises twice and must return at
|
||||
* most one context to be dropped -- keeping both would leak a pool slot per
|
||||
* queued node, which over a large tree exhausts the pool outright.
|
||||
*/
|
||||
static int test_bfs_queue_drain_releases_repeated_failures(void)
|
||||
{
|
||||
aksl_TreeNode tree[7];
|
||||
int i = 0;
|
||||
|
||||
for ( i = 0; i < 7; i++ ) {
|
||||
AKSL_CHECK_OK(aksl_tree_node_init(&tree[i], NULL));
|
||||
}
|
||||
tree[0].left = &tree[1];
|
||||
tree[0].right = &tree[2];
|
||||
tree[1].left = &tree[3];
|
||||
tree[1].right = &tree[4];
|
||||
tree[2].left = &tree[5];
|
||||
tree[2].right = &tree[6];
|
||||
|
||||
break_on_node = &tree[2];
|
||||
failing_free_calls = 0;
|
||||
failing_free_from = 4; /* the first drain call */
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &break_at,
|
||||
&plain_alloc, &free_that_fails_from,
|
||||
AKSL_TREE_SEARCH_BFS, NULL));
|
||||
/* Three dequeues plus two drained entries. */
|
||||
AKSL_CHECK(failing_free_calls == 5);
|
||||
AKSL_CHECK(aksl_slots_in_use() == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* The tracked container */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
@@ -619,6 +907,99 @@ static int test_tree_remove_all_three_cases(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* The mirror images of the cases above: a node that is its parent's *right*
|
||||
* child, and a node whose only child is on the left. Both take different
|
||||
* branches through tree_replace, and neither is reached by the test above.
|
||||
*/
|
||||
static int test_tree_remove_mirrored_shapes(void)
|
||||
{
|
||||
static int values[4] = { 5, 8, 7, 6 };
|
||||
aksl_TreeNode node[4];
|
||||
aksl_TreeNode *root = NULL;
|
||||
aksl_TreeNode *found = NULL;
|
||||
int missing = 8;
|
||||
size_t count = 0;
|
||||
int i = 0;
|
||||
|
||||
/*
|
||||
* 5 node[0]
|
||||
* \
|
||||
* 8 node[1], a right child
|
||||
* /
|
||||
* 7 node[2], whose only child is on the left
|
||||
* /
|
||||
* 6 node[3]
|
||||
*/
|
||||
for ( i = 0; i < 4; 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].right == &node[1]);
|
||||
AKSL_CHECK(node[1].left == &node[2]);
|
||||
AKSL_CHECK(node[2].left == &node[3]);
|
||||
|
||||
/* A right child with a single left child underneath it. */
|
||||
AKSL_CHECK_OK(aksl_tree_remove(&root, &node[1]));
|
||||
AKSL_CHECK_OK(aksl_tree_count(root, &count));
|
||||
AKSL_CHECK(count == 3);
|
||||
AKSL_CHECK_OK(aksl_tree_find(root, &missing, &compare_ints, &found));
|
||||
AKSL_CHECK(found == NULL);
|
||||
/* 7 took its place as the root's right child. */
|
||||
AKSL_CHECK(node[0].right == &node[2]);
|
||||
AKSL_CHECK(node[2].parent == &node[0]);
|
||||
|
||||
/* And now a node whose only child is on the left. */
|
||||
AKSL_CHECK_OK(aksl_tree_remove(&root, &node[2]));
|
||||
AKSL_CHECK_OK(aksl_tree_count(root, &count));
|
||||
AKSL_CHECK(count == 2);
|
||||
AKSL_CHECK(node[0].right == &node[3]);
|
||||
AKSL_CHECK(node[3].parent == &node[0]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* The two-children case where the successor is not the removed node's own right
|
||||
* child, so the successor has to be lifted out of its position first. The other
|
||||
* removal test happens to hit the adjacent-successor path.
|
||||
*/
|
||||
static int test_tree_remove_with_a_distant_successor(void)
|
||||
{
|
||||
static int values[5] = { 5, 3, 9, 7, 8 };
|
||||
aksl_TreeNode node[5];
|
||||
aksl_TreeNode *root = NULL;
|
||||
OrderLog log;
|
||||
int i = 0;
|
||||
|
||||
/*
|
||||
* 5 node[0]
|
||||
* / \
|
||||
* 3 9 node[1], node[2]
|
||||
* /
|
||||
* 7 node[3] -- the in-order successor of 5, two levels down
|
||||
* \
|
||||
* 8 node[4]
|
||||
*/
|
||||
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_remove(&root, &node[0]));
|
||||
/* 7 becomes the root, and its own right child (8) is not lost. */
|
||||
AKSL_CHECK(root == &node[3]);
|
||||
AKSL_CHECK(root->parent == NULL);
|
||||
|
||||
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]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Removing the last node empties the tree rather than leaving a dangling root. */
|
||||
static int test_tree_remove_the_only_node(void)
|
||||
{
|
||||
@@ -707,12 +1088,18 @@ int main(void)
|
||||
|
||||
AKSL_RUN(failures, test_prepend_moves_the_head);
|
||||
AKSL_RUN(failures, test_insert_after_and_before);
|
||||
AKSL_RUN(failures, test_insert_before_a_middle_node);
|
||||
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_free_all_releases_the_errors_it_does_not_return);
|
||||
AKSL_RUN(failures, test_list_free_all_reports_the_first_failure_but_frees_everything);
|
||||
AKSL_RUN(failures, test_tree_free_all_reports_the_first_failure_but_frees_everything);
|
||||
AKSL_RUN(failures, test_bfs_queue_drain_survives_a_failing_free);
|
||||
AKSL_RUN(failures, test_bfs_queue_drain_releases_repeated_failures);
|
||||
|
||||
AKSL_RUN(failures, test_container_push_and_unshift);
|
||||
AKSL_RUN(failures, test_container_remove_maintains_the_endpoints);
|
||||
@@ -722,6 +1109,8 @@ int main(void)
|
||||
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_mirrored_shapes);
|
||||
AKSL_RUN(failures, test_tree_remove_with_a_distant_successor);
|
||||
AKSL_RUN(failures, test_tree_remove_the_only_node);
|
||||
AKSL_RUN(failures, test_tree_height_and_count);
|
||||
AKSL_RUN(failures, test_tree_free_all);
|
||||
|
||||
@@ -224,6 +224,51 @@ static int test_printf_rejects_null_arguments(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* The allocating form. No fixed buffer means no truncation case, which is what
|
||||
* makes it the right answer when the length is not knowable in advance and the
|
||||
* result is too short-lived to want a whole aksl_StrBuf.
|
||||
*/
|
||||
static int test_asprintf_allocates_to_fit(void)
|
||||
{
|
||||
char *out = NULL;
|
||||
int count = -1;
|
||||
int i = 0;
|
||||
|
||||
AKSL_CHECK_OK(aksl_asprintf(&count, &out, "%s=%d", "key", 42));
|
||||
AKSL_CHECK(out != NULL);
|
||||
AKSL_CHECK(strcmp(out, "key=42") == 0);
|
||||
AKSL_CHECK(count == 6);
|
||||
AKSL_CHECK_OK(aksl_freep((void **)&out));
|
||||
|
||||
/* Far longer than any buffer a fixed-size wrapper would have used. */
|
||||
AKSL_CHECK_OK(aksl_asprintf(&count, &out, "%01000d", 7));
|
||||
AKSL_CHECK(count == 1000);
|
||||
AKSL_CHECK(strlen(out) == 1000);
|
||||
AKSL_CHECK_OK(aksl_freep((void **)&out));
|
||||
|
||||
/* An empty result is a valid empty string, not NULL. */
|
||||
AKSL_CHECK_OK(aksl_asprintf(&count, &out, "%s", ""));
|
||||
AKSL_CHECK(count == 0);
|
||||
AKSL_CHECK(out != NULL && out[0] == '\0');
|
||||
AKSL_CHECK_OK(aksl_freep((void **)&out));
|
||||
|
||||
/* Repeatedly, so a leak shows up under the sanitizer build. */
|
||||
for ( i = 0; i < 256; i++ ) {
|
||||
AKSL_CHECK_OK(aksl_asprintf(&count, &out, "iteration %d of %d", i, 256));
|
||||
AKSL_CHECK((size_t)count == strlen(out));
|
||||
AKSL_CHECK_OK(aksl_freep((void **)&out));
|
||||
}
|
||||
|
||||
out = (char *)0x1;
|
||||
AKSL_CHECK_STATUS(aksl_asprintf(NULL, &out, "x"), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_asprintf(&count, NULL, "x"), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_asprintf(&count, &out, NULL), AKERR_NULLPOINTER);
|
||||
/* Cleared before the format is even looked at. */
|
||||
AKSL_CHECK(out == NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* The va_list forms are what the variadic ones are built on, and TODO.md 3.1
|
||||
* wanted them exposed so consumers can write their own variadic wrappers. This
|
||||
@@ -298,6 +343,7 @@ int main(void)
|
||||
AKSL_RUN(failures, test_printf_writes_to_stdout);
|
||||
AKSL_RUN(failures, test_printf_rejects_null_arguments);
|
||||
|
||||
AKSL_RUN(failures, test_asprintf_allocates_to_fit);
|
||||
AKSL_RUN(failures, test_va_list_forms_are_usable_from_outside);
|
||||
AKSL_RUN(failures, test_variadic_wrappers_survive_repeated_calls);
|
||||
|
||||
|
||||
@@ -121,6 +121,84 @@ static int test_realloc_grows_and_preserves_the_old_block_on_failure(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* reallocarray's whole reason for existing: `realloc(p, n * size)` is a heap
|
||||
* overflow waiting for an n large enough to wrap, and the multiplication is the
|
||||
* place a caller does not think to look.
|
||||
*/
|
||||
static int test_reallocarray_checks_the_multiplication(void)
|
||||
{
|
||||
void *ptr = NULL;
|
||||
unsigned char *buf = NULL;
|
||||
size_t i = 0;
|
||||
|
||||
AKSL_CHECK_OK(aksl_malloc(4, &ptr));
|
||||
AKSL_CHECK_OK(aksl_reallocarray(&ptr, 16, 8));
|
||||
buf = (unsigned char *)ptr;
|
||||
for ( i = 0; i < 128; i++ ) {
|
||||
buf[i] = (unsigned char)i;
|
||||
}
|
||||
AKSL_CHECK_OK(aksl_free(ptr));
|
||||
|
||||
ptr = NULL;
|
||||
/* SIZE_MAX/4 members of 8 bytes wraps; without the check this would be a
|
||||
* plausible-looking small allocation followed by a very large write. */
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_reallocarray(&ptr, SIZE_MAX / 4, 8),
|
||||
AKERR_OUTOFBOUNDS, "overflows size_t");
|
||||
AKSL_CHECK(ptr == NULL);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_reallocarray(NULL, 1, 1), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_reallocarray(&ptr, 0, 1), AKERR_VALUE);
|
||||
AKSL_CHECK_STATUS(aksl_reallocarray(&ptr, 1, 0), AKERR_VALUE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* aligned_alloc(3) requires size to be a multiple of the alignment and the
|
||||
* alignment to be a power of two. Violating either is undefined behaviour that
|
||||
* usually just returns NULL, so both are checked and reported as what they are.
|
||||
*/
|
||||
static int test_aligned_alloc_checks_its_preconditions(void)
|
||||
{
|
||||
void *ptr = (void *)0x1;
|
||||
|
||||
AKSL_CHECK_OK(aksl_aligned_alloc(64, 128, &ptr));
|
||||
AKSL_CHECK(ptr != NULL);
|
||||
AKSL_CHECK(((uintptr_t)ptr % 64) == 0);
|
||||
AKSL_CHECK_OK(aksl_free(ptr));
|
||||
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_aligned_alloc(24, 48, &ptr),
|
||||
AKERR_VALUE, "not a power of two");
|
||||
AKSL_CHECK(ptr == NULL);
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_aligned_alloc(64, 100, &ptr),
|
||||
AKERR_VALUE, "not a multiple of alignment");
|
||||
AKSL_CHECK_STATUS(aksl_aligned_alloc(0, 64, &ptr), AKERR_VALUE);
|
||||
AKSL_CHECK_STATUS(aksl_aligned_alloc(64, 0, &ptr), AKERR_VALUE);
|
||||
AKSL_CHECK_STATUS(aksl_aligned_alloc(64, 128, NULL), AKERR_NULLPOINTER);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* posix_memalign(3) breaks the errno convention: it returns the error number and
|
||||
* leaves errno alone. The wrapper reports it as a status like everything else.
|
||||
*/
|
||||
static int test_posix_memalign(void)
|
||||
{
|
||||
void *ptr = (void *)0x1;
|
||||
|
||||
AKSL_CHECK_OK(aksl_posix_memalign(&ptr, sizeof(void *) * 2, 100));
|
||||
AKSL_CHECK(ptr != NULL);
|
||||
AKSL_CHECK(((uintptr_t)ptr % (sizeof(void *) * 2)) == 0);
|
||||
AKSL_CHECK_OK(aksl_free(ptr));
|
||||
|
||||
/* Not a multiple of sizeof(void *), which posix_memalign refuses. */
|
||||
AKSL_CHECK_STATUS(aksl_posix_memalign(&ptr, 3, 100), EINVAL);
|
||||
AKSL_CHECK(ptr == NULL);
|
||||
AKSL_CHECK_STATUS(aksl_posix_memalign(NULL, 16, 100), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_posix_memalign(&ptr, 16, 0), AKERR_VALUE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_free_rejects_null_pointer(void)
|
||||
{
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_free(NULL),
|
||||
@@ -324,6 +402,9 @@ int main(void)
|
||||
AKSL_RUN(failures, test_malloc_free_round_trip);
|
||||
|
||||
AKSL_RUN(failures, test_calloc_zeroes_and_guards);
|
||||
AKSL_RUN(failures, test_reallocarray_checks_the_multiplication);
|
||||
AKSL_RUN(failures, test_aligned_alloc_checks_its_preconditions);
|
||||
AKSL_RUN(failures, test_posix_memalign);
|
||||
AKSL_RUN(failures, test_realloc_grows_and_preserves_the_old_block_on_failure);
|
||||
|
||||
AKSL_RUN(failures, test_free_rejects_null_pointer);
|
||||
|
||||
410
tests/test_pool.c
Normal file
410
tests/test_pool.c
Normal file
@@ -0,0 +1,410 @@
|
||||
/*
|
||||
* Cross-cutting properties every wrapper has to have -- TODO.md section 1.9.
|
||||
*
|
||||
* Two of them, and neither is about what any individual function computes:
|
||||
*
|
||||
* Error-pool accounting. libakerror hands out error contexts from a fixed
|
||||
* process-global array of AKERR_MAX_ARRAY_ERROR slots. A wrapper that raises
|
||||
* an error and forgets to release it does not fail visibly -- it fails
|
||||
* AKERR_MAX_ARRAY_ERROR calls later, in whatever unrelated code happens to ask
|
||||
* for a slot next, by which time the leak is nowhere near the symptom. So
|
||||
* every failure path here is driven AKERR_MAX_ARRAY_ERROR + 10 times, which is
|
||||
* past exhaustion several times over, with the pool checked after each round.
|
||||
*
|
||||
* Stack-trace content. An error that reports the wrong origin is worse than
|
||||
* useless when the only debugging you get is a log file after the fact, which
|
||||
* is the stated reason this library exists at all. Each assertion here names
|
||||
* the function the error must say it came from and the source file it must
|
||||
* name, so a FAIL that migrates into a helper is caught.
|
||||
*
|
||||
* AKSL_RUN already checks the pool after every test in every file. This one
|
||||
* checks it inside the loop as well, because a leak of one slot per call needs
|
||||
* more than one call to show up.
|
||||
*/
|
||||
|
||||
#include "aksl_capture.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
|
||||
/* Rounds enough to exhaust the pool several times over. */
|
||||
#define ROUNDS (AKERR_MAX_ARRAY_ERROR + 10)
|
||||
|
||||
/*
|
||||
* Assert the last error came from `func` in a file whose name ends in `file`.
|
||||
* The recorded fname is whatever __FILE__ expanded to at the FAIL site, which is
|
||||
* an absolute path under CMake, so this matches on the tail rather than the
|
||||
* whole thing.
|
||||
*/
|
||||
static int came_from(const char *func, const char *file)
|
||||
{
|
||||
size_t flen = strlen(file);
|
||||
size_t rlen = strlen(aksl_last_file);
|
||||
|
||||
if ( strcmp(aksl_last_function, func) != 0 ) {
|
||||
fprintf(stderr, " CHECK FAILED: error says it came from \"%s\", expected \"%s\"\n",
|
||||
aksl_last_function, func);
|
||||
return 1;
|
||||
}
|
||||
if ( rlen < flen || strcmp(aksl_last_file + (rlen - flen), file) != 0 ) {
|
||||
fprintf(stderr, " CHECK FAILED: error names file \"%s\", expected one ending \"%s\"\n",
|
||||
aksl_last_file, file);
|
||||
return 1;
|
||||
}
|
||||
if ( aksl_last_line <= 0 ) {
|
||||
fprintf(stderr, " CHECK FAILED: error records line %d\n", aksl_last_line);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Pool accounting */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static int test_memory_wrappers_do_not_leak_slots(void)
|
||||
{
|
||||
void *ptr = NULL;
|
||||
int i = 0;
|
||||
|
||||
for ( i = 0; i < ROUNDS; i++ ) {
|
||||
AKSL_CHECK_STATUS(aksl_malloc(32, NULL), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_malloc(0, &ptr), AKERR_VALUE);
|
||||
AKSL_CHECK_STATUS(aksl_calloc(0, 1, &ptr), AKERR_VALUE);
|
||||
AKSL_CHECK_STATUS(aksl_realloc(NULL, 8), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_free(NULL), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_freep(NULL), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_memset(NULL, 0, 1), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_memcpy(NULL, "a", 1), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_memmove(NULL, "a", 1), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_memcmp(NULL, "a", 1, NULL), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_memchr(NULL, 'a', 1, NULL), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK(aksl_slots_in_use() == 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_stream_wrappers_do_not_leak_slots(void)
|
||||
{
|
||||
FILE *fp = NULL;
|
||||
size_t moved = 0;
|
||||
int i = 0;
|
||||
|
||||
for ( i = 0; i < ROUNDS; i++ ) {
|
||||
AKSL_CHECK_STATUS(aksl_fopen(NULL, "r", &fp), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_fopen("/nonexistent/aksl/x", "r", &fp), ENOENT);
|
||||
AKSL_CHECK_STATUS(aksl_fread(NULL, 1, 1, stdin, &moved), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_fwrite(NULL, 1, 1, stdout, &moved), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_fclose(NULL), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_fseek(NULL, 0, SEEK_SET), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_ftell(NULL, NULL), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_fgetc(NULL, NULL), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_fputs(NULL, stdout), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_remove("/nonexistent/aksl/x"), ENOENT);
|
||||
AKSL_CHECK(aksl_slots_in_use() == 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_format_wrappers_do_not_leak_slots(void)
|
||||
{
|
||||
char buf[16];
|
||||
int count = 0;
|
||||
int i = 0;
|
||||
|
||||
for ( i = 0; i < ROUNDS; i++ ) {
|
||||
AKSL_CHECK_STATUS(aksl_printf(NULL, "x"), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_fprintf(NULL, stdout, "x"), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_snprintf(NULL, buf, sizeof(buf), "x"), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_snprintf(&count, buf, 4, "%s", "far too long"),
|
||||
AKERR_OUTOFBOUNDS);
|
||||
AKSL_CHECK(aksl_slots_in_use() == 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_conversion_wrappers_do_not_leak_slots(void)
|
||||
{
|
||||
int iout = 0;
|
||||
long lout = 0;
|
||||
unsigned long ulout = 0;
|
||||
double dout = 0.0;
|
||||
int i = 0;
|
||||
|
||||
for ( i = 0; i < ROUNDS; i++ ) {
|
||||
AKSL_CHECK_STATUS(aksl_atoi(NULL, &iout), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_atoi("junk", &iout), AKERR_VALUE);
|
||||
AKSL_CHECK_STATUS(aksl_atoi("99999999999999999999", &iout), ERANGE);
|
||||
AKSL_CHECK_STATUS(aksl_strtol("junk", NULL, 10, &lout), AKERR_VALUE);
|
||||
AKSL_CHECK_STATUS(aksl_strtoul("-1", NULL, 10, &ulout), AKERR_VALUE);
|
||||
AKSL_CHECK_STATUS(aksl_strtod("junk", NULL, &dout), AKERR_VALUE);
|
||||
AKSL_CHECK(aksl_slots_in_use() == 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_string_wrappers_do_not_leak_slots(void)
|
||||
{
|
||||
char buf[8];
|
||||
char *at = NULL;
|
||||
size_t n = 0;
|
||||
int r = 0;
|
||||
int i = 0;
|
||||
|
||||
for ( i = 0; i < ROUNDS; i++ ) {
|
||||
AKSL_CHECK_STATUS(aksl_strlen(NULL, &n), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_strcpy(buf, sizeof(buf), "far too long for eight"),
|
||||
AKERR_OUTOFBOUNDS);
|
||||
AKSL_CHECK_STATUS(aksl_strcat(buf, 0, "x"), AKERR_VALUE);
|
||||
AKSL_CHECK_STATUS(aksl_strdup(NULL, &at), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_strcmp(NULL, "b", &r), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_strchr(NULL, 'a', &at), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_strerror(0, buf, 0), AKERR_VALUE);
|
||||
AKSL_CHECK(aksl_slots_in_use() == 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_collection_wrappers_do_not_leak_slots(void)
|
||||
{
|
||||
aksl_ListNode node;
|
||||
aksl_ListNode *head = NULL;
|
||||
aksl_List list;
|
||||
aksl_TreeNode tree;
|
||||
aksl_HashMap map;
|
||||
aksl_HashEntry slots[4];
|
||||
aksl_StrBuf strbuf;
|
||||
size_t n = 0;
|
||||
int i = 0;
|
||||
|
||||
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
|
||||
AKSL_CHECK_OK(aksl_list_init(&list));
|
||||
AKSL_CHECK_OK(aksl_tree_node_init(&tree, NULL));
|
||||
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4));
|
||||
memset((void *)&strbuf, 0x00, sizeof(strbuf));
|
||||
|
||||
for ( i = 0; i < ROUNDS; i++ ) {
|
||||
AKSL_CHECK_STATUS(aksl_list_append(NULL, &node), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_list_pop(&head, NULL), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_list_prepend(NULL, &node), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_list_length(&node, NULL), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_list_remove(&list, &node), AKERR_VALUE);
|
||||
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree, NULL, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_PREORDER, NULL),
|
||||
AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_tree_count(&tree, NULL), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_hashmap_get(&map, "absent", NULL, NULL), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_strbuf_append(&strbuf, "x"), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_strhash_djb2(NULL, 0, NULL), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK(aksl_slots_in_use() == 0);
|
||||
}
|
||||
(void)n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* A traversal that fails part-way through has the most opportunities to leak: an
|
||||
* error is raised inside a callback, propagated up through several frames, and
|
||||
* handled or not at the top. Drive that repeatedly too.
|
||||
*/
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *always_fails(aksl_TreeNode *node, void *data)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
(void)node;
|
||||
(void)data;
|
||||
FAIL_RETURN(e, AKERR_VALUE, "callback always fails");
|
||||
}
|
||||
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *always_breaks(aksl_TreeNode *node, void *data)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
(void)node;
|
||||
(void)data;
|
||||
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "callback always breaks");
|
||||
}
|
||||
|
||||
static int test_traversal_failures_do_not_leak_slots(void)
|
||||
{
|
||||
aksl_TreeNode tree[3];
|
||||
int i = 0;
|
||||
|
||||
for ( i = 0; i < 3; i++ ) {
|
||||
AKSL_CHECK_OK(aksl_tree_node_init(&tree[i], NULL));
|
||||
}
|
||||
tree[0].left = &tree[1];
|
||||
tree[0].right = &tree[2];
|
||||
|
||||
for ( i = 0; i < ROUNDS; i++ ) {
|
||||
/* Propagated out of the recursion. */
|
||||
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree[0], &always_fails, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_PREORDER, NULL),
|
||||
AKERR_VALUE);
|
||||
/* Swallowed at the top -- the released-not-returned path. */
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &always_breaks, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_PREORDER, NULL));
|
||||
/* And through the breadth-first walk, which also drains a queue. */
|
||||
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree[0], &always_fails, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_BFS, NULL),
|
||||
AKERR_VALUE);
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &always_breaks, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_BFS, NULL));
|
||||
AKSL_CHECK(aksl_slots_in_use() == 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Stack-trace content */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* Each of these asserts that the error names the function that raised it and
|
||||
* the file that function lives in. This is what catches a FAIL that gets moved
|
||||
* into a shared helper during a refactor: the status stays right, the message
|
||||
* stays right, and the origin quietly starts pointing at the wrong place.
|
||||
*/
|
||||
static int test_errors_name_their_origin_in_stdlib(void)
|
||||
{
|
||||
void *ptr = NULL;
|
||||
int count = 0;
|
||||
char resolved[PATH_MAX];
|
||||
uint32_t h = 0;
|
||||
aksl_ListNode node;
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_malloc(32, NULL), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK(came_from("aksl_malloc", "src/stdlib.c") == 0);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_free(NULL), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK(came_from("aksl_free", "src/stdlib.c") == 0);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_memcpy(NULL, "a", 1), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK(came_from("aksl_memcpy", "src/stdlib.c") == 0);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_fopen(NULL, "r", (FILE **)&ptr), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK(came_from("aksl_fopen", "src/stdlib.c") == 0);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_fclose(NULL), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK(came_from("aksl_fclose", "src/stdlib.c") == 0);
|
||||
|
||||
/*
|
||||
* aksl_printf forwards to aksl_vprintf, so the error legitimately comes
|
||||
* from the latter -- the variadic form is a three-line wrapper with no FAIL
|
||||
* of its own. Asserting the real origin rather than the one a reader might
|
||||
* expect is the point of recording it.
|
||||
*/
|
||||
AKSL_CHECK_STATUS(aksl_printf(NULL, "x"), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK(came_from("aksl_vprintf", "src/stdlib.c") == 0);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_snprintf(&count, NULL, 8, "x"), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK(came_from("aksl_vsnprintf", "src/stdlib.c") == 0);
|
||||
|
||||
/* Likewise, the ato* forms are calls into the strto* ones. */
|
||||
AKSL_CHECK_STATUS(aksl_atol("junk", (long *)&ptr), AKERR_VALUE);
|
||||
AKSL_CHECK(came_from("aksl_strtol", "src/stdlib.c") == 0);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_realpath(NULL, resolved, sizeof(resolved)), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK(came_from("aksl_realpath", "src/stdlib.c") == 0);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_strhash_djb2(NULL, 0, &h), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK(came_from("aksl_strhash_djb2", "src/stdlib.c") == 0);
|
||||
|
||||
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
|
||||
AKSL_CHECK_STATUS(aksl_list_append(NULL, &node), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK(came_from("aksl_list_append", "src/stdlib.c") == 0);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_tree_iterate(NULL, NULL, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_PREORDER, NULL),
|
||||
AKERR_NULLPOINTER);
|
||||
AKSL_CHECK(came_from("aksl_tree_iterate", "src/stdlib.c") == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_errors_name_their_origin_in_string_and_stream(void)
|
||||
{
|
||||
char buf[8];
|
||||
size_t n = 0;
|
||||
long pos = 0;
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_strlen(NULL, &n), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK(came_from("aksl_strlen", "src/string.c") == 0);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_strcpy(buf, sizeof(buf), "far too long for eight"),
|
||||
AKERR_OUTOFBOUNDS);
|
||||
AKSL_CHECK(came_from("aksl_strcpy", "src/string.c") == 0);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_strdup(NULL, NULL), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK(came_from("aksl_strdup", "src/string.c") == 0);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_strerror(0, buf, 0), AKERR_VALUE);
|
||||
AKSL_CHECK(came_from("aksl_strerror", "src/string.c") == 0);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_fseek(NULL, 0, SEEK_SET), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK(came_from("aksl_fseek", "src/stream.c") == 0);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_ftell(NULL, &pos), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK(came_from("aksl_ftell", "src/stream.c") == 0);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_getline(NULL, NULL, NULL, NULL), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK(came_from("aksl_getline", "src/stream.c") == 0);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_mkdtemp("no-placeholder"), AKERR_VALUE);
|
||||
AKSL_CHECK(came_from("aksl_mkdtemp", "src/stream.c") == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_errors_name_their_origin_in_collections(void)
|
||||
{
|
||||
aksl_ListNode node;
|
||||
aksl_List list;
|
||||
aksl_HashMap map;
|
||||
aksl_HashEntry slots[4];
|
||||
aksl_StrBuf strbuf;
|
||||
char longkey[AKSL_HASHMAP_MAX_KEY + 4];
|
||||
|
||||
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
|
||||
AKSL_CHECK_OK(aksl_list_init(&list));
|
||||
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4));
|
||||
memset((void *)&strbuf, 0x00, sizeof(strbuf));
|
||||
memset(longkey, 'k', sizeof(longkey) - 1);
|
||||
longkey[sizeof(longkey) - 1] = '\0';
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_list_prepend(NULL, &node), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK(came_from("aksl_list_prepend", "src/collections.c") == 0);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_list_remove(&list, &node), AKERR_VALUE);
|
||||
AKSL_CHECK(came_from("aksl_list_remove", "src/collections.c") == 0);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_tree_insert(NULL, NULL, NULL), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK(came_from("aksl_tree_insert", "src/collections.c") == 0);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_hashmap_put(&map, longkey, NULL), AKERR_OUTOFBOUNDS);
|
||||
AKSL_CHECK(came_from("aksl_hashmap_put", "src/collections.c") == 0);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_strhash_fnv1a(NULL, 0, NULL), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK(came_from("aksl_strhash_fnv1a", "src/collections.c") == 0);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_strbuf_reset(&strbuf), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK(came_from("aksl_strbuf_reset", "src/collections.c") == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int failures = 0;
|
||||
|
||||
akerr_init();
|
||||
|
||||
AKSL_RUN(failures, test_memory_wrappers_do_not_leak_slots);
|
||||
AKSL_RUN(failures, test_stream_wrappers_do_not_leak_slots);
|
||||
AKSL_RUN(failures, test_format_wrappers_do_not_leak_slots);
|
||||
AKSL_RUN(failures, test_conversion_wrappers_do_not_leak_slots);
|
||||
AKSL_RUN(failures, test_string_wrappers_do_not_leak_slots);
|
||||
AKSL_RUN(failures, test_collection_wrappers_do_not_leak_slots);
|
||||
AKSL_RUN(failures, test_traversal_failures_do_not_leak_slots);
|
||||
|
||||
AKSL_RUN(failures, test_errors_name_their_origin_in_stdlib);
|
||||
AKSL_RUN(failures, test_errors_name_their_origin_in_string_and_stream);
|
||||
AKSL_RUN(failures, test_errors_name_their_origin_in_collections);
|
||||
|
||||
AKSL_REPORT(failures);
|
||||
}
|
||||
@@ -520,6 +520,53 @@ static int test_fscanf_reads_from_a_stream(void)
|
||||
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 */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
@@ -603,6 +650,8 @@ int main(void)
|
||||
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);
|
||||
|
||||
|
||||
@@ -111,6 +111,53 @@ static int test_no_digits_is_an_error_even_with_an_endptr(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Every form writes through endptr, not just the two the other tests happen to
|
||||
* use. Each type has its own function body, so "strtol handles endptr" says
|
||||
* nothing whatsoever about strtoull.
|
||||
*/
|
||||
static int test_every_form_writes_the_endptr(void)
|
||||
{
|
||||
const char *input = "42rest";
|
||||
char *end = NULL;
|
||||
long lout = 0;
|
||||
long long llout = 0;
|
||||
unsigned long ulout = 0;
|
||||
unsigned long long ullout = 0;
|
||||
double dout = 0.0;
|
||||
float fout = 0.0f;
|
||||
long double ldout = 0.0L;
|
||||
|
||||
end = NULL;
|
||||
AKSL_CHECK_OK(aksl_strtol(input, &end, 10, &lout));
|
||||
AKSL_CHECK(lout == 42 && strcmp(end, "rest") == 0);
|
||||
|
||||
end = NULL;
|
||||
AKSL_CHECK_OK(aksl_strtoll(input, &end, 10, &llout));
|
||||
AKSL_CHECK(llout == 42 && strcmp(end, "rest") == 0);
|
||||
|
||||
end = NULL;
|
||||
AKSL_CHECK_OK(aksl_strtoul(input, &end, 10, &ulout));
|
||||
AKSL_CHECK(ulout == 42 && strcmp(end, "rest") == 0);
|
||||
|
||||
end = NULL;
|
||||
AKSL_CHECK_OK(aksl_strtoull(input, &end, 10, &ullout));
|
||||
AKSL_CHECK(ullout == 42 && strcmp(end, "rest") == 0);
|
||||
|
||||
end = NULL;
|
||||
AKSL_CHECK_OK(aksl_strtod(input, &end, &dout));
|
||||
AKSL_CHECK(dout == 42.0 && strcmp(end, "rest") == 0);
|
||||
|
||||
end = NULL;
|
||||
AKSL_CHECK_OK(aksl_strtof(input, &end, &fout));
|
||||
AKSL_CHECK(fout == 42.0f && strcmp(end, "rest") == 0);
|
||||
|
||||
end = NULL;
|
||||
AKSL_CHECK_OK(aksl_strtold(input, &end, &ldout));
|
||||
AKSL_CHECK(ldout == 42.0L && strcmp(end, "rest") == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Successive calls chained through endptr walk a whole list of numbers. */
|
||||
static int test_endptr_chains_across_a_list(void)
|
||||
{
|
||||
@@ -277,6 +324,7 @@ int main(void)
|
||||
AKSL_RUN(failures, test_invalid_base_is_rejected);
|
||||
|
||||
AKSL_RUN(failures, test_endptr_reports_where_parsing_stopped);
|
||||
AKSL_RUN(failures, test_every_form_writes_the_endptr);
|
||||
AKSL_RUN(failures, test_no_digits_is_an_error_even_with_an_endptr);
|
||||
AKSL_RUN(failures, test_endptr_chains_across_a_list);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user