Build a real test harness (TODO.md section 1.0)

The suite could not fail and did not run. test_linkedlist.c asserted nothing
at all -- it printed node names and returned 0 -- so every confirmed list
defect passed it. test_tree.c was red on every run because parms.steps was
never reset between its three searches, and four libakerror tests registered
but never built, reporting Not Run. CI, meanwhile, was not fetching the
submodule, so configure failed before reaching any of it.

- tests/aksl_capture.h: AKSL_CHECK (NDEBUG-proof), AKSL_CHECK_STATUS/_OK to
  run an akerror-returning call, assert its status and release the context,
  a capturing akerr_log_method, aksl_slots_in_use(), and an AKSL_RUN driver
  that fails any test leaking an error-pool slot.
- test_linkedlist.c: rewritten as 15 assertion cases over the append,
  iterate and pop behaviour that is correct today.
- test_tree.c: each search builds its own tree and params.
- Registration driven by AKSL_TESTS, plus AKSL_WILL_FAIL_TESTS (aborts by
  design) and AKSL_KNOWN_FAILING_TESTS, which marks WILL_FAIL the three new
  tests asserting correct behaviour for the confirmed defects in TODO.md
  2.1.1-2.1.3. Fixing a defect flips its test to "unexpectedly passed",
  which is the cue to promote it into AKSL_TESTS.
- add_test/set_tests_properties are shadowed across the libakerror
  add_subdirectory call: CMake cannot un-register a test and
  set_tests_properties cannot cross directory scopes. The dependency has
  its own CI.
- AKSL_SANITIZE=ON builds library, tests and dependency with ASan+UBSan.
- scripts/mutation_test.py ported and retargeted at src/stdlib.c; wired to
  a manual `mutation` target, not a CI gate until 1.1-1.9 exist.
- CI: checkout with submodules: recursive, and ctest --output-on-failure.

ctest is now 5/5 green in both the default and sanitizer builds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-28 12:39:54 -04:00
parent 5793f6a178
commit 3e296c3bff
10 changed files with 1888 additions and 130 deletions

View File

@@ -1,75 +1,319 @@
#include <stdio.h>
#include <akstdlib.h>
/*
* Linked-list behaviour that the library gets right today.
*
* The two confirmed list defects (TODO.md 2.1.1 aksl_list_append truncating the
* chain, and 2.1.2 aksl_list_iterate skipping the head) are asserted separately
* in tests/test_list_append_chain.c and tests/test_list_iterate_head.c, which
* are registered as known-failing. Everything in this file must pass.
*
* Note that the list-shape assertions here build their lists by hand rather
* than with aksl_list_append: append cannot be trusted to produce a chain
* longer than two nodes until 2.1.1 is fixed, and using it would make these
* tests fail for a reason that has nothing to do with what they are checking.
*/
#include "aksl_capture.h"
// This iterator does nothing but print the node names it is visiting
akerr_ErrorContext AKERR_NOIGNORE *myiter(aksl_ListNode *node, void *data)
#define MAX_VISITS 16
typedef struct VisitLog
{
int count = 0;
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, node->data, AKERR_NULLPOINTER, "node->data");
PASS(e, aksl_fprintf(&count, stderr, "Visiting node : %s\n", node->data));
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);
}
// This iterator function exits early once the index in `(int *)data` is reached
akerr_ErrorContext AKERR_NOIGNORE *myiter_earlyhalt(aksl_ListNode *node, void *data)
/* ---------------------------------------------------------------------- */
/* aksl_list_append */
/* ---------------------------------------------------------------------- */
static int test_append_single_node(void)
{
int *idx;
int count = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
FAIL_ZERO_RETURN(e, node->data, AKERR_NULLPOINTER, "node->data");
FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data");
idx = (int *)data;
if ( *idx == 1 ) {
// This exception is eaten by the iterator, we will never see it
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop");
}
*idx += 1;
SUCCEED_RETURN(e);
aksl_ListNode head;
aksl_ListNode tail;
memset((void *)&head, 0x00, sizeof(head));
memset((void *)&tail, 0x00, sizeof(tail));
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;
}
static int test_append_null_arguments(void)
{
aksl_ListNode node;
memset((void *)&node, 0x00, sizeof(node));
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;
memset((void *)&head, 0x00, sizeof(head));
memset((void *)&node, 0x00, sizeof(node));
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;
memset((void *)&a, 0x00, sizeof(a));
memset((void *)&b, 0x00, sizeof(b));
memset((void *)&node, 0x00, sizeof(node));
a.next = &b;
b.prev = &a;
b.next = &a;
AKSL_CHECK_STATUS(aksl_list_append(&a, &node), AKERR_CIRCULAR_REFERENCE);
return 0;
}
/* ---------------------------------------------------------------------- */
/* aksl_list_iterate */
/* ---------------------------------------------------------------------- */
static int test_iterate_null_arguments(void)
{
aksl_ListNode node;
VisitLog log;
memset((void *)&node, 0x00, sizeof(node));
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;
memset((void *)&node, 0x00, sizeof(node));
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;
}
static int test_iterate_detects_self_cycle(void)
{
aksl_ListNode head;
VisitLog log;
memset((void *)&head, 0x00, sizeof(head));
head.next = &head;
visitlog_init(&log);
AKSL_CHECK_STATUS(aksl_list_iterate(&head, &record_visit, &log),
AKERR_CIRCULAR_REFERENCE);
return 0;
}
static int test_iterate_detects_two_node_cycle(void)
{
aksl_ListNode a;
aksl_ListNode b;
VisitLog log;
memset((void *)&a, 0x00, sizeof(a));
memset((void *)&b, 0x00, sizeof(b));
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;
}
/* 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;
memset((void *)&node, 0x00, sizeof(node));
visitlog_init(&log);
log.fail_at = 0;
AKSL_CHECK_STATUS(aksl_list_iterate(&node, &record_visit, &log), AKERR_VALUE);
AKSL_CHECK_MSG_CONTAINS("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;
memset((void *)&node, 0x00, sizeof(node));
visitlog_init(&log);
log.break_at = 0;
AKSL_CHECK_OK(aksl_list_iterate(&node, &record_visit, &log));
AKSL_CHECK(log.count == 1);
return 0;
}
/* ---------------------------------------------------------------------- */
/* aksl_list_pop */
/* ---------------------------------------------------------------------- */
static int test_pop_middle_node(void)
{
aksl_ListNode a;
aksl_ListNode b;
aksl_ListNode c;
memset((void *)&a, 0x00, sizeof(a));
memset((void *)&b, 0x00, sizeof(b));
memset((void *)&c, 0x00, sizeof(c));
a.next = &b;
b.prev = &a;
b.next = &c;
c.prev = &b;
AKSL_CHECK_OK(aksl_list_pop(&b));
AKSL_CHECK(a.next == &c);
AKSL_CHECK(c.prev == &a);
AKSL_CHECK(b.next == NULL);
AKSL_CHECK(b.prev == NULL);
return 0;
}
static int test_pop_head_node(void)
{
aksl_ListNode a;
aksl_ListNode b;
memset((void *)&a, 0x00, sizeof(a));
memset((void *)&b, 0x00, sizeof(b));
a.next = &b;
b.prev = &a;
AKSL_CHECK_OK(aksl_list_pop(&a));
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;
memset((void *)&a, 0x00, sizeof(a));
memset((void *)&b, 0x00, sizeof(b));
a.next = &b;
b.prev = &a;
AKSL_CHECK_OK(aksl_list_pop(&b));
AKSL_CHECK(a.next == NULL);
AKSL_CHECK(a.prev == NULL);
AKSL_CHECK(b.next == NULL);
AKSL_CHECK(b.prev == NULL);
return 0;
}
static int test_pop_only_node(void)
{
aksl_ListNode a;
memset((void *)&a, 0x00, sizeof(a));
AKSL_CHECK_OK(aksl_list_pop(&a));
AKSL_CHECK(a.next == NULL);
AKSL_CHECK(a.prev == NULL);
return 0;
}
static int test_pop_null_argument(void)
{
AKSL_CHECK_STATUS(aksl_list_pop(NULL), AKERR_NULLPOINTER);
return 0;
}
int main(void)
{
PREPARE_ERROR(e);
int idx = 0;
int count = 0;
aksl_ListNode mylist;
aksl_ListNode node1;
aksl_ListNode node2;
int failures = 0;
ATTEMPT {
memset((void *)&mylist, 0x00, sizeof(aksl_ListNode));
memset((void *)&node1, 0x00, sizeof(aksl_ListNode));
memset((void *)&node2, 0x00, sizeof(aksl_ListNode));
akerr_init();
mylist.data = "Root";
node1.data = "Node 1";
CATCH(e, aksl_list_append(&mylist, &node1));
node2.data = "Node 2";
CATCH(e, aksl_list_append(&mylist, &node2));
AKSL_RUN(failures, test_append_single_node);
AKSL_RUN(failures, test_append_null_arguments);
AKSL_RUN(failures, test_append_detects_self_cycle);
AKSL_RUN(failures, test_append_detects_two_node_cycle);
// Iterate over all nodes in the list using the myiter() function
CATCH(e, aksl_list_iterate(&mylist, &myiter, NULL));
AKSL_RUN(failures, test_iterate_null_arguments);
AKSL_RUN(failures, test_iterate_single_node);
AKSL_RUN(failures, test_iterate_detects_self_cycle);
AKSL_RUN(failures, test_iterate_detects_two_node_cycle);
AKSL_RUN(failures, test_iterate_propagates_callback_error);
AKSL_RUN(failures, test_iterate_break_is_not_an_error);
// Iterate over up to the first 2 nodes in the list and then exit early
idx = 0;
CATCH(e, aksl_list_iterate(&mylist, &myiter_earlyhalt, &idx));
CATCH(e, aksl_fprintf(&count, stderr, "Iterator exited early at index %d\n", idx));
AKSL_RUN(failures, test_pop_middle_node);
AKSL_RUN(failures, test_pop_head_node);
AKSL_RUN(failures, test_pop_tail_node);
AKSL_RUN(failures, test_pop_only_node);
AKSL_RUN(failures, test_pop_null_argument);
// Break the list with a circular reference, and iterate it again
node2.next = &mylist;
CATCH(e, aksl_list_iterate(&mylist, &myiter, NULL));
} CLEANUP {
} PROCESS(e) {
} HANDLE(e, AKERR_CIRCULAR_REFERENCE) {
fprintf(stderr, "Circular reference error caught\n");
} FINISH_NORETURN(e);
return 0;
AKSL_REPORT(failures);
}