/* * Linked list -- TODO.md section 1.7, complete. * * The two confirmed list defects are fixed, so the tests that used to live in * tests/test_list_append_chain.c and tests/test_list_iterate_head.c are folded * back in here: aksl_list_append builds the whole chain rather than truncating * it at the midpoint (2.1.1), and aksl_list_iterate starts at the head rather * than at whatever node Floyd's slow pointer happened to stop on (2.1.2). * * The chain assertions build their lists with aksl_list_append now, which is the * point -- they could not, while append was the thing under suspicion. */ #include "aksl_capture.h" #define MAX_VISITS 16 #define CHAIN_LEN 5 typedef struct VisitLog { int count; aksl_ListNode *seen[MAX_VISITS]; int break_at; /* visit index to raise ITERATOR_BREAK on, or -1 */ int fail_at; /* visit index to raise AKERR_VALUE on, or -1 */ } VisitLog; static void visitlog_init(VisitLog *log) { memset((void *)log, 0x00, sizeof(VisitLog)); log->break_at = -1; log->fail_at = -1; } static akerr_ErrorContext AKERR_NOIGNORE *record_visit(aksl_ListNode *node, void *data) { VisitLog *log = NULL; int idx = 0; PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node"); FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data"); log = (VisitLog *)data; idx = log->count; if ( idx < MAX_VISITS ) { log->seen[idx] = node; } log->count += 1; if ( log->fail_at == idx ) { FAIL_RETURN(e, AKERR_VALUE, "iterator failed at visit %d", idx); } if ( log->break_at == idx ) { FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop at visit %d", idx); } SUCCEED_RETURN(e); } /* ---------------------------------------------------------------------- */ /* aksl_list_node_init */ /* ---------------------------------------------------------------------- */ /* * TODO.md 2.2.14: every caller used to have to remember to memset a node before * its first use, and a stack node that skipped it walked straight into garbage. */ static int test_node_init_zeroes_the_links(void) { aksl_ListNode node; int payload = 7; memset((void *)&node, 0xff, sizeof(node)); AKSL_CHECK_OK(aksl_list_node_init(&node, &payload)); AKSL_CHECK(node.next == NULL); AKSL_CHECK(node.prev == NULL); AKSL_CHECK(node.data == (void *)&payload); AKSL_CHECK_OK(aksl_list_node_init(&node, NULL)); AKSL_CHECK(node.data == NULL); AKSL_CHECK_STATUS(aksl_list_node_init(NULL, NULL), AKERR_NULLPOINTER); return 0; } /* ---------------------------------------------------------------------- */ /* aksl_list_append */ /* ---------------------------------------------------------------------- */ static int test_append_single_node(void) { aksl_ListNode head; aksl_ListNode tail; AKSL_CHECK_OK(aksl_list_node_init(&head, NULL)); AKSL_CHECK_OK(aksl_list_node_init(&tail, NULL)); AKSL_CHECK_OK(aksl_list_append(&head, &tail)); AKSL_CHECK(head.next == &tail); AKSL_CHECK(head.prev == NULL); AKSL_CHECK(tail.prev == &head); AKSL_CHECK(tail.next == NULL); return 0; } /* * The defect that made this library's list unusable: `tail` was assigned from * Floyd's `slow` cursor *before* slow advanced, so it tracked the node behind * the midpoint rather than the last node. Appending n1..n4 to n0 produced the * chain "n0 -> n4" and silently dropped n1, n2 and n3. TODO.md 2.1.1. */ static int test_append_builds_the_whole_chain(void) { aksl_ListNode node[CHAIN_LEN]; aksl_ListNode *walk = NULL; int i = 0; for ( i = 0; i < CHAIN_LEN; i++ ) { AKSL_CHECK_OK(aksl_list_node_init(&node[i], NULL)); } for ( i = 1; i < CHAIN_LEN; i++ ) { AKSL_CHECK_OK(aksl_list_append(&node[0], &node[i])); } /* Forward links, head to tail. */ walk = &node[0]; for ( i = 0; i < CHAIN_LEN; i++ ) { AKSL_CHECK(walk == &node[i]); walk = walk->next; } AKSL_CHECK(walk == NULL); /* Back links, tail to head. */ walk = &node[CHAIN_LEN - 1]; for ( i = CHAIN_LEN - 1; i >= 0; i-- ) { AKSL_CHECK(walk == &node[i]); walk = walk->prev; } AKSL_CHECK(walk == NULL); return 0; } /* append points obj->prev at the real tail and terminates the chain at obj. */ static int test_append_links_prev_to_the_real_tail(void) { aksl_ListNode node[4]; int i = 0; for ( i = 0; i < 4; i++ ) { AKSL_CHECK_OK(aksl_list_node_init(&node[i], NULL)); } for ( i = 1; i < 4; i++ ) { AKSL_CHECK_OK(aksl_list_append(&node[0], &node[i])); AKSL_CHECK(node[i].prev == &node[i - 1]); AKSL_CHECK(node[i].next == NULL); AKSL_CHECK(node[i - 1].next == &node[i]); } return 0; } static int test_append_null_arguments(void) { aksl_ListNode node; AKSL_CHECK_OK(aksl_list_node_init(&node, NULL)); AKSL_CHECK_STATUS(aksl_list_append(NULL, &node), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_list_append(&node, NULL), AKERR_NULLPOINTER); return 0; } static int test_append_detects_self_cycle(void) { aksl_ListNode head; aksl_ListNode node; AKSL_CHECK_OK(aksl_list_node_init(&head, NULL)); AKSL_CHECK_OK(aksl_list_node_init(&node, NULL)); head.next = &head; AKSL_CHECK_STATUS(aksl_list_append(&head, &node), AKERR_CIRCULAR_REFERENCE); return 0; } static int test_append_detects_two_node_cycle(void) { aksl_ListNode a; aksl_ListNode b; aksl_ListNode node; AKSL_CHECK_OK(aksl_list_node_init(&a, NULL)); AKSL_CHECK_OK(aksl_list_node_init(&b, NULL)); AKSL_CHECK_OK(aksl_list_node_init(&node, NULL)); a.next = &b; b.prev = &a; b.next = &a; AKSL_CHECK_STATUS(aksl_list_append(&a, &node), AKERR_CIRCULAR_REFERENCE); return 0; } /* A cycle that does not include the head: a -> b -> c -> b. */ static int test_append_detects_cycle_below_the_head(void) { aksl_ListNode a; aksl_ListNode b; aksl_ListNode c; aksl_ListNode node; AKSL_CHECK_OK(aksl_list_node_init(&a, NULL)); AKSL_CHECK_OK(aksl_list_node_init(&b, NULL)); AKSL_CHECK_OK(aksl_list_node_init(&c, NULL)); AKSL_CHECK_OK(aksl_list_node_init(&node, NULL)); a.next = &b; b.prev = &a; b.next = &c; c.prev = &b; c.next = &b; AKSL_CHECK_STATUS(aksl_list_append(&a, &node), AKERR_CIRCULAR_REFERENCE); return 0; } /* * TODO.md 1.7 asked for the aliasing contract to be defined. It is refusal: * relinking a node that is already in the list would orphan everything between * its old position and the tail, so the tail walk -- which happens anyway -- * doubles as the check. */ static int test_append_refuses_a_node_already_in_the_list(void) { aksl_ListNode node[3]; int i = 0; for ( i = 0; i < 3; i++ ) { AKSL_CHECK_OK(aksl_list_node_init(&node[i], NULL)); } AKSL_CHECK_OK(aksl_list_append(&node[0], &node[1])); AKSL_CHECK_OK(aksl_list_append(&node[0], &node[2])); AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_list_append(&node[0], &node[1]), AKERR_VALUE, "already in this list"); AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_list_append(&node[0], &node[0]), AKERR_VALUE, "already the head"); /* The list is untouched by the refusal. */ AKSL_CHECK(node[0].next == &node[1]); AKSL_CHECK(node[1].next == &node[2]); AKSL_CHECK(node[2].next == NULL); return 0; } /* ---------------------------------------------------------------------- */ /* aksl_list_iterate */ /* ---------------------------------------------------------------------- */ static int test_iterate_null_arguments(void) { aksl_ListNode node; VisitLog log; AKSL_CHECK_OK(aksl_list_node_init(&node, NULL)); visitlog_init(&log); AKSL_CHECK_STATUS(aksl_list_iterate(NULL, &record_visit, &log), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_list_iterate(&node, NULL, &log), AKERR_NULLPOINTER); AKSL_CHECK(log.count == 0); return 0; } static int test_iterate_single_node(void) { aksl_ListNode node; VisitLog log; AKSL_CHECK_OK(aksl_list_node_init(&node, NULL)); visitlog_init(&log); AKSL_CHECK_OK(aksl_list_iterate(&node, &record_visit, &log)); AKSL_CHECK(log.count == 1); AKSL_CHECK(log.seen[0] == &node); return 0; } /* * Every node, exactly once, in order, starting at the head. The cycle check * leaves Floyd's `slow` cursor at the list midpoint, and the visiting loop used * to start from there -- so the whole first half of the list, head included, was * never passed to the callback at all. TODO.md 2.1.2. */ static int test_iterate_visits_every_node_from_the_head(void) { aksl_ListNode node[CHAIN_LEN]; VisitLog log; int i = 0; for ( i = 0; i < CHAIN_LEN; i++ ) { AKSL_CHECK_OK(aksl_list_node_init(&node[i], NULL)); } for ( i = 1; i < CHAIN_LEN; i++ ) { AKSL_CHECK_OK(aksl_list_append(&node[0], &node[i])); } visitlog_init(&log); AKSL_CHECK_OK(aksl_list_iterate(&node[0], &record_visit, &log)); AKSL_CHECK(log.count == CHAIN_LEN); for ( i = 0; i < CHAIN_LEN; i++ ) { AKSL_CHECK(log.seen[i] == &node[i]); } return 0; } /* An even-length list too: Floyd's midpoint lands differently, the answer does not. */ static int test_iterate_visits_every_node_of_an_even_list(void) { aksl_ListNode node[4]; VisitLog log; int i = 0; for ( i = 0; i < 4; i++ ) { AKSL_CHECK_OK(aksl_list_node_init(&node[i], NULL)); } for ( i = 1; i < 4; i++ ) { AKSL_CHECK_OK(aksl_list_append(&node[0], &node[i])); } visitlog_init(&log); AKSL_CHECK_OK(aksl_list_iterate(&node[0], &record_visit, &log)); AKSL_CHECK(log.count == 4); for ( i = 0; i < 4; i++ ) { AKSL_CHECK(log.seen[i] == &node[i]); } return 0; } static int test_iterate_detects_self_cycle(void) { aksl_ListNode head; VisitLog log; AKSL_CHECK_OK(aksl_list_node_init(&head, NULL)); head.next = &head; visitlog_init(&log); AKSL_CHECK_STATUS(aksl_list_iterate(&head, &record_visit, &log), AKERR_CIRCULAR_REFERENCE); AKSL_CHECK(log.count == 0); return 0; } static int test_iterate_detects_two_node_cycle(void) { aksl_ListNode a; aksl_ListNode b; VisitLog log; AKSL_CHECK_OK(aksl_list_node_init(&a, NULL)); AKSL_CHECK_OK(aksl_list_node_init(&b, NULL)); a.next = &b; b.prev = &a; b.next = &a; visitlog_init(&log); AKSL_CHECK_STATUS(aksl_list_iterate(&a, &record_visit, &log), AKERR_CIRCULAR_REFERENCE); return 0; } /* Tail pointing back into the middle: a -> b -> c -> b. */ static int test_iterate_detects_tail_to_middle_cycle(void) { aksl_ListNode a; aksl_ListNode b; aksl_ListNode c; VisitLog log; AKSL_CHECK_OK(aksl_list_node_init(&a, NULL)); AKSL_CHECK_OK(aksl_list_node_init(&b, NULL)); AKSL_CHECK_OK(aksl_list_node_init(&c, NULL)); a.next = &b; b.prev = &a; b.next = &c; c.prev = &b; c.next = &b; visitlog_init(&log); AKSL_CHECK_STATUS(aksl_list_iterate(&a, &record_visit, &log), AKERR_CIRCULAR_REFERENCE); return 0; } /* An error other than ITERATOR_BREAK must come back out of the iteration with * its status and message intact. */ static int test_iterate_propagates_callback_error(void) { aksl_ListNode node; VisitLog log; AKSL_CHECK_OK(aksl_list_node_init(&node, NULL)); visitlog_init(&log); log.fail_at = 0; AKSL_CHECK_STATUS_MSG_CONTAINS( aksl_list_iterate(&node, &record_visit, &log), AKERR_VALUE, "iterator failed at visit 0"); AKSL_CHECK(log.count == 1); return 0; } /* ITERATOR_BREAK is a control signal, not a failure: the caller sees success. */ static int test_iterate_break_is_not_an_error(void) { aksl_ListNode node; VisitLog log; AKSL_CHECK_OK(aksl_list_node_init(&node, NULL)); visitlog_init(&log); log.break_at = 0; AKSL_CHECK_OK(aksl_list_iterate(&node, &record_visit, &log)); AKSL_CHECK(log.count == 1); return 0; } /* * And the count is what proves it stopped early rather than merely finishing. * A break on the second of five nodes must leave three nodes unvisited. */ static int test_iterate_break_stops_at_that_node(void) { aksl_ListNode node[CHAIN_LEN]; VisitLog log; int i = 0; for ( i = 0; i < CHAIN_LEN; i++ ) { AKSL_CHECK_OK(aksl_list_node_init(&node[i], NULL)); } for ( i = 1; i < CHAIN_LEN; i++ ) { AKSL_CHECK_OK(aksl_list_append(&node[0], &node[i])); } visitlog_init(&log); log.break_at = 1; AKSL_CHECK_OK(aksl_list_iterate(&node[0], &record_visit, &log)); AKSL_CHECK(log.count == 2); AKSL_CHECK(log.seen[0] == &node[0]); AKSL_CHECK(log.seen[1] == &node[1]); return 0; } /* ---------------------------------------------------------------------- */ /* aksl_list_pop */ /* ---------------------------------------------------------------------- */ static int test_pop_middle_node(void) { aksl_ListNode a; aksl_ListNode b; aksl_ListNode c; aksl_ListNode *head = &a; AKSL_CHECK_OK(aksl_list_node_init(&a, NULL)); AKSL_CHECK_OK(aksl_list_node_init(&b, NULL)); AKSL_CHECK_OK(aksl_list_node_init(&c, NULL)); AKSL_CHECK_OK(aksl_list_append(&a, &b)); AKSL_CHECK_OK(aksl_list_append(&a, &c)); AKSL_CHECK_OK(aksl_list_pop(&head, &b)); AKSL_CHECK(head == &a); AKSL_CHECK(a.next == &c); AKSL_CHECK(c.prev == &a); AKSL_CHECK(b.next == NULL); AKSL_CHECK(b.prev == NULL); return 0; } /* * TODO.md 2.2.12: popping the head used to leave the caller's own head pointer * aimed at a node that was no longer in the list, with no way to learn the new * one. That is what the head out-param is for, and this is the assertion. */ static int test_pop_head_node_moves_the_head(void) { aksl_ListNode a; aksl_ListNode b; aksl_ListNode *head = &a; AKSL_CHECK_OK(aksl_list_node_init(&a, NULL)); AKSL_CHECK_OK(aksl_list_node_init(&b, NULL)); AKSL_CHECK_OK(aksl_list_append(&a, &b)); AKSL_CHECK_OK(aksl_list_pop(&head, &a)); AKSL_CHECK(head == &b); AKSL_CHECK(b.prev == NULL); AKSL_CHECK(b.next == NULL); AKSL_CHECK(a.next == NULL); AKSL_CHECK(a.prev == NULL); return 0; } static int test_pop_tail_node(void) { aksl_ListNode a; aksl_ListNode b; aksl_ListNode *head = &a; AKSL_CHECK_OK(aksl_list_node_init(&a, NULL)); AKSL_CHECK_OK(aksl_list_node_init(&b, NULL)); AKSL_CHECK_OK(aksl_list_append(&a, &b)); AKSL_CHECK_OK(aksl_list_pop(&head, &b)); AKSL_CHECK(head == &a); AKSL_CHECK(a.next == NULL); AKSL_CHECK(a.prev == NULL); AKSL_CHECK(b.next == NULL); AKSL_CHECK(b.prev == NULL); return 0; } /* Popping the only node empties the list, and the head becomes NULL. */ static int test_pop_only_node_empties_the_list(void) { aksl_ListNode a; aksl_ListNode *head = &a; AKSL_CHECK_OK(aksl_list_node_init(&a, NULL)); AKSL_CHECK_OK(aksl_list_pop(&head, &a)); AKSL_CHECK(head == NULL); AKSL_CHECK(a.next == NULL); AKSL_CHECK(a.prev == NULL); return 0; } static int test_pop_null_arguments(void) { aksl_ListNode a; aksl_ListNode *head = &a; AKSL_CHECK_OK(aksl_list_node_init(&a, NULL)); AKSL_CHECK_STATUS(aksl_list_pop(NULL, &a), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_list_pop(&head, NULL), AKERR_NULLPOINTER); return 0; } /* The list is still walkable after a pop, and the popped node is gone from it. */ static int test_pop_then_iterate(void) { aksl_ListNode node[4]; aksl_ListNode *head = &node[0]; VisitLog log; int i = 0; for ( i = 0; i < 4; i++ ) { AKSL_CHECK_OK(aksl_list_node_init(&node[i], NULL)); } for ( i = 1; i < 4; i++ ) { AKSL_CHECK_OK(aksl_list_append(&node[0], &node[i])); } AKSL_CHECK_OK(aksl_list_pop(&head, &node[2])); visitlog_init(&log); AKSL_CHECK_OK(aksl_list_iterate(head, &record_visit, &log)); AKSL_CHECK(log.count == 3); AKSL_CHECK(log.seen[0] == &node[0]); AKSL_CHECK(log.seen[1] == &node[1]); AKSL_CHECK(log.seen[2] == &node[3]); /* And again, this time taking the head out. */ AKSL_CHECK_OK(aksl_list_pop(&head, &node[0])); visitlog_init(&log); AKSL_CHECK_OK(aksl_list_iterate(head, &record_visit, &log)); AKSL_CHECK(log.count == 2); AKSL_CHECK(log.seen[0] == &node[1]); AKSL_CHECK(log.seen[1] == &node[3]); return 0; } /* * TODO.md 1.7's pool-accounting case. AKSL_RUN already asserts that each test * leaves the pool as it found it; this one drives enough failures in a row to * exhaust the pool several times over, which is where a wrapper that raises an * error and forgets to release it shows up as an outright exhaustion rather than * as a slow leak. */ static int test_error_pool_survives_a_long_run_of_failures(void) { aksl_ListNode a; aksl_ListNode b; aksl_ListNode *head = &a; VisitLog log; int i = 0; AKSL_CHECK_OK(aksl_list_node_init(&a, NULL)); AKSL_CHECK_OK(aksl_list_node_init(&b, NULL)); a.next = &a; for ( i = 0; i < AKERR_MAX_ARRAY_ERROR + 10; i++ ) { AKSL_CHECK_STATUS(aksl_list_append(&a, &b), AKERR_CIRCULAR_REFERENCE); AKSL_CHECK_STATUS(aksl_list_append(NULL, &b), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_list_pop(&head, NULL), AKERR_NULLPOINTER); visitlog_init(&log); AKSL_CHECK_STATUS(aksl_list_iterate(&a, &record_visit, &log), AKERR_CIRCULAR_REFERENCE); AKSL_CHECK(aksl_slots_in_use() == 0); } return 0; } int main(void) { int failures = 0; akerr_init(); AKSL_RUN(failures, test_node_init_zeroes_the_links); AKSL_RUN(failures, test_append_single_node); AKSL_RUN(failures, test_append_builds_the_whole_chain); AKSL_RUN(failures, test_append_links_prev_to_the_real_tail); AKSL_RUN(failures, test_append_null_arguments); AKSL_RUN(failures, test_append_detects_self_cycle); AKSL_RUN(failures, test_append_detects_two_node_cycle); AKSL_RUN(failures, test_append_detects_cycle_below_the_head); AKSL_RUN(failures, test_append_refuses_a_node_already_in_the_list); AKSL_RUN(failures, test_iterate_null_arguments); AKSL_RUN(failures, test_iterate_single_node); AKSL_RUN(failures, test_iterate_visits_every_node_from_the_head); AKSL_RUN(failures, test_iterate_visits_every_node_of_an_even_list); AKSL_RUN(failures, test_iterate_detects_self_cycle); AKSL_RUN(failures, test_iterate_detects_two_node_cycle); AKSL_RUN(failures, test_iterate_detects_tail_to_middle_cycle); AKSL_RUN(failures, test_iterate_propagates_callback_error); AKSL_RUN(failures, test_iterate_break_is_not_an_error); AKSL_RUN(failures, test_iterate_break_stops_at_that_node); AKSL_RUN(failures, test_pop_middle_node); AKSL_RUN(failures, test_pop_head_node_moves_the_head); AKSL_RUN(failures, test_pop_tail_node); AKSL_RUN(failures, test_pop_only_node_empties_the_list); AKSL_RUN(failures, test_pop_null_arguments); AKSL_RUN(failures, test_pop_then_iterate); AKSL_RUN(failures, test_error_pool_survives_a_long_run_of_failures); AKSL_REPORT(failures); }