/* * Tree traversal -- TODO.md section 1.8, complete. * * 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. * * The 7-node tree used throughout: * * TREE[0] * +--------^^---------+ * | | * TREE[1] TREE[2] * +---^^---+ +---^^---+ * | | | | * 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 */ #include "aksl_capture.h" #define MAX_LEAVES 7 #define MAX_VISITS 32 typedef struct VisitLog { 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]; } /* 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) { int i = 0; 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; } /* ---------------------------------------------------------------------- */ /* aksl_tree_node_init */ /* ---------------------------------------------------------------------- */ static int test_node_init_zeroes_the_links(void) { 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; } /* ---------------------------------------------------------------------- */ /* Traversal orders */ /* ---------------------------------------------------------------------- */ static int test_preorder_visits_root_left_right(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_PREORDER, &log)); AKSL_CHECK(check_order(&log, tree, expected, MAX_LEAVES) == 0); return 0; } static int test_inorder_visits_left_root_right(void) { 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; } /* * 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 test_bfs_visits_level_by_level(void) { aksl_TreeNode tree[MAX_LEAVES]; VisitLog log; static const int expected[MAX_LEAVES] = { 0, 1, 2, 3, 4, 5, 6 }; 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_right_visits_right_child_first(void) { aksl_TreeNode tree[MAX_LEAVES]; VisitLog log; static const int expected[MAX_LEAVES] = { 0, 2, 1, 6, 5, 4, 3 }; 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; } /* AKSL_TREE_SEARCH_VISIT: this node and no further. */ static int test_visit_mode_does_not_descend(void) { aksl_TreeNode tree[MAX_LEAVES]; VisitLog log; static const int expected[1] = { 0 }; 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(&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); /* * And again leaning right. The recursion counts depth separately for each * child, so a chain that only ever goes left says nothing about whether the * right-hand descent counts at all -- mutation testing found exactly that: * changing the right child's `depth + 1` to `depth + 0` survived the whole * suite, because nothing here had ever recursed right more than three deep. */ 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].right = &chain[i + 1]; } visitlog_init(&log); AKSL_CHECK_STATUS(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL, AKSL_TREE_SEARCH_DFS_PREORDER, &log), AKERR_OUTOFBOUNDS); /* Both orders that descend right, so in-order and post-order count too. */ visitlog_init(&log); AKSL_CHECK_STATUS(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL, AKSL_TREE_SEARCH_DFS_INORDER, &log), AKERR_OUTOFBOUNDS); visitlog_init(&log); AKSL_CHECK_STATUS(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL, AKSL_TREE_SEARCH_DFS_POSTORDER, &log), AKERR_OUTOFBOUNDS); /* And breadth-first, whose depth is carried on the queue entry instead. */ visitlog_init(&log); AKSL_CHECK_STATUS(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL, AKSL_TREE_SEARCH_BFS, &log), AKERR_OUTOFBOUNDS); 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; } int main(void) { int failures = 0; akerr_init(); AKSL_RUN(failures, test_node_init_zeroes_the_links); 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); }