Files
libakstdlib/tests/test_linkedlist.c
Logikoma (Codex GPT-5) 1b01aa3b42
Some checks failed
libakstdlib CI Build / cmake_build (push) Has been cancelled
libakstdlib CI Build / sanitizers (push) Has been cancelled
libakstdlib CI Build / coverage (push) Has been cancelled
libakstdlib CI Build / mutation_test (push) Has been cancelled
Remove stale TODO section references
2026-08-03 13:21:15 -04:00

644 lines
19 KiB
C

/*
* Linked list.
*
* 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, and aksl_list_iterate starts at the head rather than at
* whatever node Floyd's slow pointer happened to stop on.
*
* 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 */
/* ---------------------------------------------------------------------- */
/*
* 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.
*/
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;
}
/*
* The wrapper plan 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.
*/
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;
}
/*
* 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;
}
/*
* The wrapper plan'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);
}