Fix the six confirmed defects and close the API contract gaps
TODO.md section 2.1 recorded six defects reproduced against the built
library, and section 2.2 seventeen contract gaps. Both are closed. The
four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into
the tests for the things they test, and that list is now empty.
The defects:
2.1.1 aksl_list_append conflated Floyd cycle detection with finding
the tail, so `tail` tracked the node behind the midpoint. Any
append to a list of 2+ nodes silently dropped everything after
it. Two separate walks now: Floyd to prove the list is finite,
then a plain walk to the end.
2.1.2 aksl_list_iterate started visiting from Floyd's `slow` cursor,
so the whole first half of the list -- head included -- was
never passed to the callback. It starts at the head.
2.1.3 AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame
that raised it handled it and returned success, so the parent
carried on into the sibling subtree. The recursion is split out
and propagates the break; only the public entry swallows it.
2.1.4 va_end now matches every va_start on every path.
2.1.5 The ato* family had no error channel at all. Reimplemented over
a new strto* family with errno cleared, an endptr check and a
range check: AKERR_VALUE for junk, ERANGE for overflow.
2.1.6 aksl_realpath never checked resolved_path, could not be told
the buffer size, and formatted an unspecified buffer with %s on
its own error path. It takes a length; aksl_realpath_alloc is
the allocating form.
The contract gaps, in brief: errno is cleared before every wrapped call
and read back through a fallback so no error can carry status 0; fopen
validates pathname and mode; fread/fwrite report the transferred count
through a required out-param and no longer call a short transfer a
success; aksl_sprintf is gone in favour of aksl_snprintf, which treats
truncation as an error; the variadic wrappers carry format attributes;
djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded
and implements BFS, so lalloc/lfree are used rather than merely stored;
an unknown searchmode is AKERR_VALUE rather than silent success;
list_pop takes the head by reference; aksl_freep, the node initialisers
and extern "C" are new.
Build: -pg is out of the default build (it never reached the C compiler
anyway, and it is what produced the stray gmon.out), -Wall -Wextra are
in, and there is a .gitignore.
Tests: 11 binaries, all green under the normal and sanitizer builds.
Visit-order assertions replace the step counts that could not tell the
three depth-first orders apart.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,165 +1,572 @@
|
||||
/*
|
||||
* Depth-first tree search.
|
||||
* Tree traversal -- TODO.md section 1.8, complete.
|
||||
*
|
||||
* Previously this test shared one TreeSearchParams across all three searches
|
||||
* without resetting it, so `steps` accumulated (7, then 14, then 21) and the
|
||||
* second assertion failed -- `tree` was a red test on every run. Each search
|
||||
* now gets its own params.
|
||||
* The old version of this file counted steps, which cannot tell the three
|
||||
* depth-first orders apart because all three visit all seven nodes -- and could
|
||||
* not prove that AKERR_ITERATOR_BREAK stopped anything either, because it hid
|
||||
* its target in tree[6], the last node visited in every depth-first order. Every
|
||||
* traversal here records the node pointers it was handed, in order, and compares
|
||||
* against the expected sequence.
|
||||
*
|
||||
* Caveat worth knowing when reading the step counts below: the value is hidden
|
||||
* in tree[6], which is the last node visited in pre-, in- and post-order alike,
|
||||
* so "7 steps" holds for all three orders and does not actually distinguish
|
||||
* them -- nor does it prove that AKERR_ITERATOR_BREAK stopped anything (it does
|
||||
* not; see tests/test_tree_iterate_break.c and TODO.md 2.1.3). Visit-order
|
||||
* assertions that tell the three traversals apart are TODO.md section 1.8.
|
||||
*/
|
||||
|
||||
#include "aksl_capture.h"
|
||||
|
||||
#define MAX_LEAVES 7
|
||||
#define HIDDEN_VALUE ((void *)17336)
|
||||
|
||||
typedef struct TreeSearchParams
|
||||
{
|
||||
void *value;
|
||||
int steps;
|
||||
aksl_TreeNode *node;
|
||||
} TreeSearchParams;
|
||||
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *find_value(aksl_TreeNode *node, void *data)
|
||||
{
|
||||
TreeSearchParams *parms = NULL;
|
||||
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
|
||||
FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data");
|
||||
parms = (TreeSearchParams *)data;
|
||||
parms->steps += 1;
|
||||
if ( node->leaf == parms->value ) {
|
||||
parms->node = node;
|
||||
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop");
|
||||
}
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/*
|
||||
* Build the 3-level tree used by every case here, with the search value hidden
|
||||
* in the bottom-right leaf.
|
||||
* The 7-node tree used throughout:
|
||||
*
|
||||
* LEFT RIGHT
|
||||
* TREE[0]
|
||||
* +--------^^---------+
|
||||
* | |
|
||||
* TREE[1] TREE[2]
|
||||
* +---^^---+ +---^^---+
|
||||
* | | | |
|
||||
*TREE[3] TREE[4] TREE[5] TREE[6]
|
||||
* TREE[3] TREE[4] TREE[5] TREE[6]
|
||||
*
|
||||
* pre-order 0 1 3 4 2 5 6
|
||||
* in-order 3 1 4 0 5 2 6
|
||||
* post-order 3 4 1 5 6 2 0
|
||||
* BFS 0 1 2 3 4 5 6
|
||||
* BFS_RIGHT 0 2 1 6 5 4 3
|
||||
*/
|
||||
static void build_tree(aksl_TreeNode *tree, TreeSearchParams *parms)
|
||||
|
||||
#include "aksl_capture.h"
|
||||
|
||||
#define MAX_LEAVES 7
|
||||
#define MAX_VISITS 32
|
||||
|
||||
typedef struct VisitLog
|
||||
{
|
||||
memset((void *)tree, 0x00, sizeof(aksl_TreeNode) * MAX_LEAVES);
|
||||
int count;
|
||||
aksl_TreeNode *seen[MAX_VISITS];
|
||||
aksl_TreeNode *break_at; /* node to raise ITERATOR_BREAK on, or NULL */
|
||||
aksl_TreeNode *fail_at; /* node to raise AKERR_VALUE on, or NULL */
|
||||
} VisitLog;
|
||||
|
||||
static void visitlog_init(VisitLog *log)
|
||||
{
|
||||
memset((void *)log, 0x00, sizeof(VisitLog));
|
||||
}
|
||||
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *record_visit(aksl_TreeNode *node, void *data)
|
||||
{
|
||||
VisitLog *log = NULL;
|
||||
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
|
||||
FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data");
|
||||
log = (VisitLog *)data;
|
||||
if ( log->count < MAX_VISITS ) {
|
||||
log->seen[log->count] = node;
|
||||
}
|
||||
log->count += 1;
|
||||
if ( log->fail_at == node ) {
|
||||
FAIL_RETURN(e, AKERR_VALUE, "iterator failed at node %p", (void *)node);
|
||||
}
|
||||
if ( log->break_at == node ) {
|
||||
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop at node %p", (void *)node);
|
||||
}
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
static void build_tree(aksl_TreeNode *tree)
|
||||
{
|
||||
int i = 0;
|
||||
|
||||
for ( i = 0; i < MAX_LEAVES; i++ ) {
|
||||
memset((void *)&tree[i], 0x00, sizeof(aksl_TreeNode));
|
||||
}
|
||||
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];
|
||||
tree[6].leaf = HIDDEN_VALUE;
|
||||
|
||||
memset((void *)parms, 0x00, sizeof(TreeSearchParams));
|
||||
parms->value = HIDDEN_VALUE;
|
||||
}
|
||||
|
||||
static int search_finds_hidden_value(uint8_t searchmode)
|
||||
/* Assert that the visit log matches `expected`, which is a list of tree indices. */
|
||||
static int check_order(VisitLog *log, aksl_TreeNode *tree,
|
||||
const int *expected, int n)
|
||||
{
|
||||
aksl_TreeNode tree[MAX_LEAVES];
|
||||
TreeSearchParams parms;
|
||||
int i = 0;
|
||||
|
||||
build_tree(tree, &parms);
|
||||
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &find_value, NULL, NULL,
|
||||
searchmode, &parms, NULL));
|
||||
AKSL_CHECK(parms.node == &tree[6]);
|
||||
AKSL_CHECK(parms.steps == MAX_LEAVES);
|
||||
if ( log->count != n ) {
|
||||
fprintf(stderr, " CHECK FAILED: visited %d nodes, expected %d\n",
|
||||
log->count, n);
|
||||
return 1;
|
||||
}
|
||||
for ( i = 0; i < n; i++ ) {
|
||||
if ( log->seen[i] != &tree[expected[i]] ) {
|
||||
fprintf(stderr, " CHECK FAILED: visit %d was node %ld, expected %d\n",
|
||||
i, (long)(log->seen[i] - tree), expected[i]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_dfs_preorder(void)
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* aksl_tree_node_init */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static int test_node_init_zeroes_the_links(void)
|
||||
{
|
||||
return search_finds_hidden_value(AKSL_TREE_SEARCH_DFS_PREORDER);
|
||||
aksl_TreeNode node;
|
||||
int payload = 3;
|
||||
|
||||
memset((void *)&node, 0xff, sizeof(node));
|
||||
AKSL_CHECK_OK(aksl_tree_node_init(&node, &payload));
|
||||
AKSL_CHECK(node.parent == NULL);
|
||||
AKSL_CHECK(node.left == NULL);
|
||||
AKSL_CHECK(node.right == NULL);
|
||||
AKSL_CHECK(node.leaf == (void *)&payload);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_tree_node_init(NULL, NULL), AKERR_NULLPOINTER);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_dfs_inorder(void)
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Traversal orders */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static int test_preorder_visits_root_left_right(void)
|
||||
{
|
||||
return search_finds_hidden_value(AKSL_TREE_SEARCH_DFS_INORDER);
|
||||
aksl_TreeNode tree[MAX_LEAVES];
|
||||
VisitLog log;
|
||||
static const int expected[MAX_LEAVES] = { 0, 1, 3, 4, 2, 5, 6 };
|
||||
|
||||
build_tree(tree);
|
||||
visitlog_init(&log);
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_PREORDER, &log));
|
||||
AKSL_CHECK(check_order(&log, tree, expected, MAX_LEAVES) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_dfs_postorder(void)
|
||||
static int test_inorder_visits_left_root_right(void)
|
||||
{
|
||||
return search_finds_hidden_value(AKSL_TREE_SEARCH_DFS_POSTORDER);
|
||||
aksl_TreeNode tree[MAX_LEAVES];
|
||||
VisitLog log;
|
||||
static const int expected[MAX_LEAVES] = { 3, 1, 4, 0, 5, 2, 6 };
|
||||
|
||||
build_tree(tree);
|
||||
visitlog_init(&log);
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_INORDER, &log));
|
||||
AKSL_CHECK(check_order(&log, tree, expected, MAX_LEAVES) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_postorder_visits_left_right_root(void)
|
||||
{
|
||||
aksl_TreeNode tree[MAX_LEAVES];
|
||||
VisitLog log;
|
||||
static const int expected[MAX_LEAVES] = { 3, 4, 1, 5, 6, 2, 0 };
|
||||
|
||||
build_tree(tree);
|
||||
visitlog_init(&log);
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_POSTORDER, &log));
|
||||
AKSL_CHECK(check_order(&log, tree, expected, MAX_LEAVES) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* AKSL_TREE_SEARCH_DFS is documented as an alias for the pre-order mode. */
|
||||
static int test_dfs_is_an_alias_for_preorder(void)
|
||||
{
|
||||
aksl_TreeNode tree[MAX_LEAVES];
|
||||
VisitLog log;
|
||||
static const int expected[MAX_LEAVES] = { 0, 1, 3, 4, 2, 5, 6 };
|
||||
|
||||
build_tree(tree);
|
||||
visitlog_init(&log);
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS, &log));
|
||||
AKSL_CHECK(check_order(&log, tree, expected, MAX_LEAVES) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Both breadth-first modes are declared in the header but not implemented, and
|
||||
* say so through AKERR_NOT_IMPLEMENTED rather than by silently visiting nothing.
|
||||
* TODO.md 2.2.10 tracks implementing them; until then this is the contract.
|
||||
* BFS was AKERR_NOT_IMPLEMENTED, and the lalloc/lfree parameters that existed to
|
||||
* serve it were defaulted and then never called -- TODO.md 2.2.8 and 2.2.10.
|
||||
* Both modes work now, and the allocator test below proves the queue is real.
|
||||
*/
|
||||
static int bfs_reports_not_implemented(uint8_t searchmode)
|
||||
static int test_bfs_visits_level_by_level(void)
|
||||
{
|
||||
aksl_TreeNode tree[MAX_LEAVES];
|
||||
TreeSearchParams parms;
|
||||
VisitLog log;
|
||||
static const int expected[MAX_LEAVES] = { 0, 1, 2, 3, 4, 5, 6 };
|
||||
|
||||
build_tree(tree, &parms);
|
||||
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||
aksl_tree_iterate(&tree[0], &find_value, NULL, NULL, searchmode,
|
||||
&parms, NULL),
|
||||
AKERR_NOT_IMPLEMENTED, "Searchmode");
|
||||
AKSL_CHECK(parms.steps == 0);
|
||||
AKSL_CHECK(parms.node == NULL);
|
||||
build_tree(tree);
|
||||
visitlog_init(&log);
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_BFS, &log));
|
||||
AKSL_CHECK(check_order(&log, tree, expected, MAX_LEAVES) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_bfs_is_not_implemented(void)
|
||||
{
|
||||
return bfs_reports_not_implemented(AKSL_TREE_SEARCH_BFS);
|
||||
}
|
||||
|
||||
static int test_bfs_right_is_not_implemented(void)
|
||||
{
|
||||
return bfs_reports_not_implemented(AKSL_TREE_SEARCH_BFS_RIGHT);
|
||||
}
|
||||
|
||||
static int test_iterate_null_arguments(void)
|
||||
static int test_bfs_right_visits_right_child_first(void)
|
||||
{
|
||||
aksl_TreeNode tree[MAX_LEAVES];
|
||||
TreeSearchParams parms;
|
||||
VisitLog log;
|
||||
static const int expected[MAX_LEAVES] = { 0, 2, 1, 6, 5, 4, 3 };
|
||||
|
||||
build_tree(tree, &parms);
|
||||
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||
aksl_tree_iterate(NULL, &find_value, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_PREORDER, &parms, NULL),
|
||||
AKERR_NULLPOINTER, "root");
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||
aksl_tree_iterate(&tree[0], NULL, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_PREORDER, &parms, NULL),
|
||||
AKERR_NULLPOINTER, "iter");
|
||||
build_tree(tree);
|
||||
visitlog_init(&log);
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_BFS_RIGHT, &log));
|
||||
AKSL_CHECK(check_order(&log, tree, expected, MAX_LEAVES) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* A callback error that is not AKERR_ITERATOR_BREAK reaches the caller. */
|
||||
static int test_iterate_propagates_callback_error(void)
|
||||
/* AKSL_TREE_SEARCH_VISIT: this node and no further. */
|
||||
static int test_visit_mode_does_not_descend(void)
|
||||
{
|
||||
aksl_TreeNode tree[MAX_LEAVES];
|
||||
TreeSearchParams parms;
|
||||
VisitLog log;
|
||||
static const int expected[1] = { 0 };
|
||||
|
||||
build_tree(tree, &parms);
|
||||
/* find_value raises AKERR_NULLPOINTER when it is handed no data. */
|
||||
build_tree(tree);
|
||||
visitlog_init(&log);
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_VISIT, &log));
|
||||
AKSL_CHECK(check_order(&log, tree, expected, 1) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Custom allocator */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static int counting_alloc_calls = 0;
|
||||
static int counting_free_calls = 0;
|
||||
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *counting_alloc(size_t size, void **dest)
|
||||
{
|
||||
counting_alloc_calls += 1;
|
||||
return aksl_malloc(size, dest);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *counting_free(void *ptr)
|
||||
{
|
||||
counting_free_calls += 1;
|
||||
return aksl_free(ptr);
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO.md 1.8: "Custom lalloc/lfree are actually invoked -- currently they are
|
||||
* stored and never called". They are called now, once per node enqueued, and
|
||||
* every allocation is released. The depth-first modes allocate nothing at all,
|
||||
* which is the other half of the contract.
|
||||
*/
|
||||
static int test_custom_allocator_is_used_and_balanced(void)
|
||||
{
|
||||
aksl_TreeNode tree[MAX_LEAVES];
|
||||
VisitLog log;
|
||||
|
||||
build_tree(tree);
|
||||
visitlog_init(&log);
|
||||
counting_alloc_calls = 0;
|
||||
counting_free_calls = 0;
|
||||
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit,
|
||||
&counting_alloc, &counting_free,
|
||||
AKSL_TREE_SEARCH_BFS, &log));
|
||||
AKSL_CHECK(log.count == MAX_LEAVES);
|
||||
/* One queue entry per node visited, and every one of them released. */
|
||||
AKSL_CHECK(counting_alloc_calls == MAX_LEAVES);
|
||||
AKSL_CHECK(counting_free_calls == MAX_LEAVES);
|
||||
|
||||
/* Depth-first needs no queue, so it must not touch the allocator. */
|
||||
counting_alloc_calls = 0;
|
||||
counting_free_calls = 0;
|
||||
visitlog_init(&log);
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit,
|
||||
&counting_alloc, &counting_free,
|
||||
AKSL_TREE_SEARCH_DFS_PREORDER, &log));
|
||||
AKSL_CHECK(counting_alloc_calls == 0);
|
||||
AKSL_CHECK(counting_free_calls == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* A break part-way through a BFS still drains the queue it had already built. */
|
||||
static int test_break_during_bfs_releases_the_queue(void)
|
||||
{
|
||||
aksl_TreeNode tree[MAX_LEAVES];
|
||||
VisitLog log;
|
||||
|
||||
build_tree(tree);
|
||||
visitlog_init(&log);
|
||||
log.break_at = &tree[1];
|
||||
counting_alloc_calls = 0;
|
||||
counting_free_calls = 0;
|
||||
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit,
|
||||
&counting_alloc, &counting_free,
|
||||
AKSL_TREE_SEARCH_BFS, &log));
|
||||
/* 0 and 1 visited; the walk stops at 1 with 2 still sitting in the queue. */
|
||||
AKSL_CHECK(log.count == 2);
|
||||
AKSL_CHECK(counting_alloc_calls > 0);
|
||||
AKSL_CHECK(counting_alloc_calls == counting_free_calls);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Degenerate shapes */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static int test_single_node_tree_visits_once_in_every_order(void)
|
||||
{
|
||||
aksl_TreeNode node;
|
||||
VisitLog log;
|
||||
static const uint8_t modes[] = {
|
||||
AKSL_TREE_SEARCH_DFS_PREORDER,
|
||||
AKSL_TREE_SEARCH_DFS_INORDER,
|
||||
AKSL_TREE_SEARCH_DFS_POSTORDER,
|
||||
AKSL_TREE_SEARCH_BFS,
|
||||
AKSL_TREE_SEARCH_BFS_RIGHT,
|
||||
AKSL_TREE_SEARCH_VISIT,
|
||||
};
|
||||
size_t i = 0;
|
||||
|
||||
for ( i = 0; i < sizeof(modes) / sizeof(modes[0]); i++ ) {
|
||||
AKSL_CHECK_OK(aksl_tree_node_init(&node, NULL));
|
||||
visitlog_init(&log);
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&node, &record_visit, NULL, NULL,
|
||||
modes[i], &log));
|
||||
AKSL_CHECK(log.count == 1);
|
||||
AKSL_CHECK(log.seen[0] == &node);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Left-only and right-only chains of three nodes, no branching anywhere. */
|
||||
static int test_degenerate_chains(void)
|
||||
{
|
||||
aksl_TreeNode chain[3];
|
||||
VisitLog log;
|
||||
static const int forward[3] = { 0, 1, 2 };
|
||||
static const int backward[3] = { 2, 1, 0 };
|
||||
int i = 0;
|
||||
|
||||
/* Left-only: 0 -> 1 -> 2 */
|
||||
for ( i = 0; i < 3; i++ ) {
|
||||
AKSL_CHECK_OK(aksl_tree_node_init(&chain[i], NULL));
|
||||
}
|
||||
chain[0].left = &chain[1];
|
||||
chain[1].left = &chain[2];
|
||||
|
||||
visitlog_init(&log);
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_PREORDER, &log));
|
||||
AKSL_CHECK(check_order(&log, chain, forward, 3) == 0);
|
||||
|
||||
/* In-order down a left chain arrives at the deepest node first. */
|
||||
visitlog_init(&log);
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_INORDER, &log));
|
||||
AKSL_CHECK(check_order(&log, chain, backward, 3) == 0);
|
||||
|
||||
/* Right-only: the same shape, mirrored. */
|
||||
for ( i = 0; i < 3; i++ ) {
|
||||
AKSL_CHECK_OK(aksl_tree_node_init(&chain[i], NULL));
|
||||
}
|
||||
chain[0].right = &chain[1];
|
||||
chain[1].right = &chain[2];
|
||||
|
||||
visitlog_init(&log);
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_INORDER, &log));
|
||||
AKSL_CHECK(check_order(&log, chain, forward, 3) == 0);
|
||||
|
||||
visitlog_init(&log);
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_BFS, &log));
|
||||
AKSL_CHECK(check_order(&log, chain, forward, 3) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO.md 1.8 / 2.2.7: a chain deeper than the recursion can take. It used to
|
||||
* overflow the stack; it is AKERR_OUTOFBOUNDS now, and the message names the
|
||||
* documented limit. Built one node past the cap so the failure is the cap itself
|
||||
* and not some incidental shortfall. `static` because AKSL_TREE_MAX_DEPTH nodes
|
||||
* on the stack of a test function is not the point of the test.
|
||||
*/
|
||||
static int test_tree_deeper_than_the_cap_is_out_of_bounds(void)
|
||||
{
|
||||
static aksl_TreeNode chain[AKSL_TREE_MAX_DEPTH + 2];
|
||||
VisitLog log;
|
||||
int i = 0;
|
||||
|
||||
for ( i = 0; i < AKSL_TREE_MAX_DEPTH + 2; i++ ) {
|
||||
AKSL_CHECK_OK(aksl_tree_node_init(&chain[i], NULL));
|
||||
}
|
||||
for ( i = 0; i < AKSL_TREE_MAX_DEPTH + 1; i++ ) {
|
||||
chain[i].left = &chain[i + 1];
|
||||
}
|
||||
|
||||
visitlog_init(&log);
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||
aksl_tree_iterate(&tree[0], &find_value, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_PREORDER, NULL, NULL),
|
||||
AKERR_NULLPOINTER, "data");
|
||||
aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_PREORDER, &log),
|
||||
AKERR_OUTOFBOUNDS, "AKSL_TREE_MAX_DEPTH");
|
||||
|
||||
/* Exactly at the cap is fine -- a limit, not an off-by-one. */
|
||||
chain[AKSL_TREE_MAX_DEPTH - 1].left = NULL;
|
||||
visitlog_init(&log);
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_PREORDER, &log));
|
||||
AKSL_CHECK(log.count == AKSL_TREE_MAX_DEPTH);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* A child pointing back at an ancestor. This used to recurse until the process
|
||||
* died. The depth-first walk carries the ancestor chain and recognises the node
|
||||
* as its own forebear; the breadth-first walk has no ancestor chain to compare
|
||||
* against, so the same tree comes back as AKERR_OUTOFBOUNDS through the depth
|
||||
* cap instead -- a different status for the same shape, which the header says
|
||||
* out loud rather than leaving to be discovered.
|
||||
*/
|
||||
static int test_cyclic_tree_is_caught(void)
|
||||
{
|
||||
aksl_TreeNode tree[MAX_LEAVES];
|
||||
VisitLog log;
|
||||
|
||||
build_tree(tree);
|
||||
tree[3].left = &tree[0]; /* back to the root */
|
||||
|
||||
visitlog_init(&log);
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||
aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_PREORDER, &log),
|
||||
AKERR_CIRCULAR_REFERENCE, "own ancestor");
|
||||
|
||||
visitlog_init(&log);
|
||||
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_BFS, &log),
|
||||
AKERR_OUTOFBOUNDS);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Callback control flow */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* The defect that made ITERATOR_BREAK useless on a tree: the recursive frame
|
||||
* that raised the break handled it in its own PROCESS/HANDLE block and returned
|
||||
* success, so the parent frame's PASS saw nothing wrong and carried straight on
|
||||
* into the sibling subtree. All seven nodes were visited no matter where the
|
||||
* break was raised. TODO.md 2.1.3.
|
||||
*
|
||||
* One case per order, each breaking on a node that is *not* last in that order --
|
||||
* which is precisely what the old test could not do, because it hid its target
|
||||
* in tree[6], the final node in all three depth-first walks.
|
||||
*/
|
||||
static int test_break_aborts_the_whole_traversal(void)
|
||||
{
|
||||
aksl_TreeNode tree[MAX_LEAVES];
|
||||
VisitLog log;
|
||||
|
||||
build_tree(tree);
|
||||
|
||||
/* Pre-order 0 1 3 ... : breaking at 3 is the third visit. */
|
||||
visitlog_init(&log);
|
||||
log.break_at = &tree[3];
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_PREORDER, &log));
|
||||
AKSL_CHECK(log.count == 3);
|
||||
|
||||
/* In-order 3 1 4 ... : breaking at 4 is the third visit. */
|
||||
visitlog_init(&log);
|
||||
log.break_at = &tree[4];
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_INORDER, &log));
|
||||
AKSL_CHECK(log.count == 3);
|
||||
|
||||
/* Post-order 3 4 1 5 ... : breaking at 5 is the fourth visit. */
|
||||
visitlog_init(&log);
|
||||
log.break_at = &tree[5];
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_POSTORDER, &log));
|
||||
AKSL_CHECK(log.count == 4);
|
||||
|
||||
/* BFS 0 1 2 3 ... : breaking at 2 is the third visit. */
|
||||
visitlog_init(&log);
|
||||
log.break_at = &tree[2];
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_BFS, &log));
|
||||
AKSL_CHECK(log.count == 3);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Any other status propagates out with its message and its own status intact. */
|
||||
static int test_callback_error_propagates(void)
|
||||
{
|
||||
aksl_TreeNode tree[MAX_LEAVES];
|
||||
VisitLog log;
|
||||
|
||||
build_tree(tree);
|
||||
visitlog_init(&log);
|
||||
log.fail_at = &tree[3];
|
||||
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||
aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_PREORDER, &log),
|
||||
AKERR_VALUE, "iterator failed at node");
|
||||
AKSL_CHECK(log.count == 3);
|
||||
|
||||
/* And out of a BFS, where the queue has to be drained on the way past. */
|
||||
visitlog_init(&log);
|
||||
log.fail_at = &tree[2];
|
||||
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_BFS, &log),
|
||||
AKERR_VALUE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Argument validation */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static int test_null_arguments(void)
|
||||
{
|
||||
aksl_TreeNode tree[MAX_LEAVES];
|
||||
VisitLog log;
|
||||
|
||||
build_tree(tree);
|
||||
visitlog_init(&log);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_tree_iterate(NULL, &record_visit, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_PREORDER, &log),
|
||||
AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree[0], NULL, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_PREORDER, &log),
|
||||
AKERR_NULLPOINTER);
|
||||
AKSL_CHECK(log.count == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO.md 2.2.9: the switch had no default, so an unrecognised mode -- and
|
||||
* AKSL_TREE_SEARCH_VISIT, which the header documented but nothing implemented --
|
||||
* fell straight through to SUCCEED_RETURN having visited nothing at all. A
|
||||
* traversal that silently did not happen, reported as success.
|
||||
*/
|
||||
static int test_unknown_searchmode_is_a_value_error(void)
|
||||
{
|
||||
aksl_TreeNode tree[MAX_LEAVES];
|
||||
VisitLog log;
|
||||
|
||||
build_tree(tree);
|
||||
visitlog_init(&log);
|
||||
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||
aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL, 99, &log),
|
||||
AKERR_VALUE, "unknown searchmode");
|
||||
AKSL_CHECK(log.count == 0);
|
||||
|
||||
/* 6 is one past the last defined mode, and just as unacceptable. */
|
||||
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL, 6, &log),
|
||||
AKERR_VALUE);
|
||||
AKSL_CHECK(log.count == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -169,14 +576,29 @@ int main(void)
|
||||
|
||||
akerr_init();
|
||||
|
||||
AKSL_RUN(failures, test_dfs_preorder);
|
||||
AKSL_RUN(failures, test_dfs_inorder);
|
||||
AKSL_RUN(failures, test_dfs_postorder);
|
||||
AKSL_RUN(failures, test_node_init_zeroes_the_links);
|
||||
|
||||
AKSL_RUN(failures, test_bfs_is_not_implemented);
|
||||
AKSL_RUN(failures, test_bfs_right_is_not_implemented);
|
||||
AKSL_RUN(failures, test_iterate_null_arguments);
|
||||
AKSL_RUN(failures, test_iterate_propagates_callback_error);
|
||||
AKSL_RUN(failures, test_preorder_visits_root_left_right);
|
||||
AKSL_RUN(failures, test_inorder_visits_left_root_right);
|
||||
AKSL_RUN(failures, test_postorder_visits_left_right_root);
|
||||
AKSL_RUN(failures, test_dfs_is_an_alias_for_preorder);
|
||||
AKSL_RUN(failures, test_bfs_visits_level_by_level);
|
||||
AKSL_RUN(failures, test_bfs_right_visits_right_child_first);
|
||||
AKSL_RUN(failures, test_visit_mode_does_not_descend);
|
||||
|
||||
AKSL_RUN(failures, test_custom_allocator_is_used_and_balanced);
|
||||
AKSL_RUN(failures, test_break_during_bfs_releases_the_queue);
|
||||
|
||||
AKSL_RUN(failures, test_single_node_tree_visits_once_in_every_order);
|
||||
AKSL_RUN(failures, test_degenerate_chains);
|
||||
AKSL_RUN(failures, test_tree_deeper_than_the_cap_is_out_of_bounds);
|
||||
AKSL_RUN(failures, test_cyclic_tree_is_caught);
|
||||
|
||||
AKSL_RUN(failures, test_break_aborts_the_whole_traversal);
|
||||
AKSL_RUN(failures, test_callback_error_propagates);
|
||||
|
||||
AKSL_RUN(failures, test_null_arguments);
|
||||
AKSL_RUN(failures, test_unknown_searchmode_is_a_value_error);
|
||||
|
||||
AKSL_REPORT(failures);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user