Files
containers/tests/vector_container.c
T
2026-08-17 04:16:46 -05:00

635 lines
24 KiB
C

#define _DEFAULT_SOURCE /* strsignal() is only declared under this with -std=c11 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include "malunal/container.h"
#include "malunal/containers/vector.h"
#include "malunal/allocators.h"
/* ------------------------------------------------------------------ */
/* Minimal assertion + test-registration harness (no framework) */
/* ------------------------------------------------------------------ */
#define CHECK(cond, msg) \
do { \
if (!(cond)) { \
fprintf(stderr, " assertion failed: %s (%s:%d)\n", \
(msg), __FILE__, __LINE__); \
_exit(1); /* fail fast within the child: state is unreliable */ \
} \
} while (0)
#define CHECK_EQ_SZ(actual, expected, msg) \
do { \
malunal_size_t _a = (malunal_size_t)(actual); \
malunal_size_t _e = (malunal_size_t)(expected); \
if (_a != _e) { \
fprintf(stderr, \
" assertion failed: %s (%s:%d) got=%llu want=%llu\n", \
(msg), __FILE__, __LINE__, \
(unsigned long long)_a, (unsigned long long)_e); \
_exit(1); \
} \
} while (0)
#define CHECK_EQ_INT(actual, expected, msg) \
do { \
int _a = (int)(actual); \
int _e = (int)(expected); \
if (_a != _e) { \
fprintf(stderr, " assertion failed: %s (%s:%d) got=%d want=%d\n", \
(msg), __FILE__, __LINE__, _a, _e); \
_exit(1); \
} \
} while (0)
typedef void (*test_fn_t)(void);
typedef struct {
const char* name;
test_fn_t fn;
} test_case_t;
#define MAX_TESTS 64
static test_case_t g_tests[MAX_TESTS];
static int g_test_count = 0;
static void register_test(const char* name, test_fn_t fn) {
g_tests[g_test_count].name = name;
g_tests[g_test_count].fn = fn;
g_test_count++;
}
#define TEST(name) \
static void name(void); \
static void name##_register(void) __attribute__((constructor)); \
static void name##_register(void) { register_test(#name, name); } \
static void name(void)
/* Runs a single test in a forked child so a crash can't take out the
* rest of the suite. Returns 1 for pass, 0 for fail/crash. */
static int run_isolated(test_fn_t fn) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 0;
}
if (pid == 0) {
/* child */
fn();
_exit(0); /* reached only if no CHECK failed */
}
int status = 0;
waitpid(pid, &status, 0);
if (WIFEXITED(status) && WEXITSTATUS(status) == 0)
return 1;
if (WIFSIGNALED(status)) {
int sig = WTERMSIG(status);
fprintf(stderr, " CRASHED: terminated by signal %d (%s)\n", sig, strsignal(sig));
}
return 0;
}
/* ------------------------------------------------------------------ */
/* Shared setup helper */
/* ------------------------------------------------------------------ */
/* Initializes a fresh vector-as-container with a libc-backed allocator.
* `allocator_out` is filled in because callers need to keep the
* allocator alive (by value) for the container's lifetime. */
static container_mptr_t setup_container(
vector_t* vector,
allocator_t* allocator_out,
malunal_size_t stride,
malunal_size_t capacity
) {
*vector = vector_container();
*allocator_out = libc_allocator();
container_mptr_t container = (container_mptr_t)vector;
container_exception_t rc = container_initialize(
container, allocator_out, stride, capacity);
CHECK_EQ_INT(rc, CONTAINER_ERROR_SUCCESS, "container_initialize should succeed");
return container;
}
/* ------------------------------------------------------------------ */
/* vector_container() -- construction */
/* ------------------------------------------------------------------ */
TEST(construct_then_uninitialized_accessors_are_zero) {
vector_t vector = vector_container();
container_iptr_t container = (container_iptr_t)&vector;
malunal_size_t value = 123; /* sentinel so we know it was actually written */
container_exception_t rc = container_stride(container, &value);
CHECK_EQ_INT(rc, CONTAINER_ERROR_SUCCESS, "container_stride should succeed pre-initialize");
CHECK_EQ_SZ(value, 0, "stride should start at 0");
rc = container_count(container, &value);
CHECK_EQ_INT(rc, CONTAINER_ERROR_SUCCESS, "container_count should succeed pre-initialize");
CHECK_EQ_SZ(value, 0, "count should start at 0");
rc = container_capacity(container, &value);
CHECK_EQ_INT(rc, CONTAINER_ERROR_SUCCESS, "container_capacity should succeed pre-initialize");
CHECK_EQ_SZ(value, 0, "capacity should start at 0");
}
TEST(vector_t_and_container_t_are_same_size) {
CHECK_EQ_SZ(sizeof(vector_t), sizeof(container_t), "vector_t must match container_t size");
}
/* ------------------------------------------------------------------ */
/* container_initialize / container_finalize */
/* ------------------------------------------------------------------ */
TEST(initialize_allocates_backing_storage) {
vector_t vector = vector_container();
allocator_t libc_alloc = libc_allocator();
allocator_mptr_t allocator = &libc_alloc;
malunal_size_t value = 0;
container_mptr_t container = (container_mptr_t)&vector;
container_exception_t error = container_initialize(
container, allocator, sizeof(int), 4);
CHECK(error == CONTAINER_ERROR_SUCCESS, "Container failed to initialize");
error = container_stride(container, &value);
CHECK(error == CONTAINER_ERROR_SUCCESS, "Container failed to provide stride");
CHECK(value == sizeof(int), "Stride should match requested element size");
error = container_count(container, &value);
CHECK(error == CONTAINER_ERROR_SUCCESS, "Container failed to provide count");
CHECK(value == 0, "Count should be 0 right after initialize");
error = container_capacity(container, &value);
CHECK(error == CONTAINER_ERROR_SUCCESS, "Container failed to provide capacity");
CHECK(value == 4, "Capacity should match requested capacity");
error = container_finalize(container);
CHECK(error == CONTAINER_ERROR_SUCCESS, "Container failed to finalize");
}
TEST(allocator_accessor_returns_the_allocator_used) {
vector_t vector = vector_container();
allocator_t libc_alloc = libc_allocator();
container_mptr_t container = (container_mptr_t)&vector;
container_initialize(container, &libc_alloc, sizeof(int), 4);
allocator_mptr_t got = NULL;
container_exception_t rc = container_allocator(container, &got);
CHECK_EQ_INT(rc, CONTAINER_ERROR_SUCCESS, "container_allocator should succeed");
CHECK(got == &libc_alloc, "container_allocator should return the allocator passed to initialize");
container_finalize(container);
}
/* ------------------------------------------------------------------ */
/* container_append + vector_get */
/* ------------------------------------------------------------------ */
TEST(append_then_get_round_trips_value) {
vector_t vector = vector_container();
allocator_t libc_alloc = libc_allocator();
container_mptr_t container = setup_container(&vector, &libc_alloc, sizeof(int), 4);
int value = 42;
container_exception_t rc = container_append(container, &value);
CHECK_EQ_INT(rc, CONTAINER_ERROR_SUCCESS, "container_append should succeed");
int out = 0;
rc = vector_get(&vector, 0, &out);
CHECK_EQ_INT(rc, CONTAINER_ERROR_SUCCESS, "vector_get should succeed for freshly appended element");
CHECK_EQ_INT(out, 42, "vector_get should return the value that was appended");
container_finalize(container);
}
TEST(append_increments_count) {
vector_t vector = vector_container();
allocator_t libc_alloc = libc_allocator();
container_mptr_t container = setup_container(&vector, &libc_alloc, sizeof(int), 4);
int a = 1, b = 2;
container_append(container, &a);
container_append(container, &b);
malunal_size_t count = 0;
container_count(container, &count);
CHECK_EQ_SZ(count, 2, "count should track number of appends");
int out0 = 0, out1 = 0;
vector_get(&vector, 0, &out0);
vector_get(&vector, 1, &out1);
CHECK_EQ_INT(out0, 1, "first appended element preserved");
CHECK_EQ_INT(out1, 2, "second appended element preserved");
container_finalize(container);
}
TEST(append_past_capacity_grows_and_preserves_data) {
vector_t vector = vector_container();
allocator_t libc_alloc = libc_allocator();
container_mptr_t container = setup_container(&vector, &libc_alloc, sizeof(int), 2);
int vals[5] = { 10, 20, 30, 40, 50 };
for (int i = 0; i < 5; i++) {
container_exception_t rc = container_append(container, &vals[i]);
CHECK_EQ_INT(rc, CONTAINER_ERROR_SUCCESS, "append should succeed while growing");
}
malunal_size_t capacity = 0, count = 0;
container_capacity(container, &capacity);
container_count(container, &count);
CHECK(capacity >= 5, "capacity should have grown to fit 5 elements");
CHECK_EQ_SZ(count, 5, "count should be 5 after 5 appends");
for (int i = 0; i < 5; i++) {
int out = 0;
vector_get(&vector, i, &out);
CHECK_EQ_INT(out, vals[i], "element should survive a growth reallocation");
}
container_finalize(container);
}
/* ------------------------------------------------------------------ */
/* container_clear */
/* ------------------------------------------------------------------ */
TEST(clear_resets_count_but_keeps_capacity) {
vector_t vector = vector_container();
allocator_t libc_alloc = libc_allocator();
container_mptr_t container = setup_container(&vector, &libc_alloc, sizeof(int), 4);
int a = 1, b = 2;
container_append(container, &a);
container_append(container, &b);
container_exception_t rc = container_clear(container);
CHECK_EQ_INT(rc, CONTAINER_ERROR_SUCCESS, "container_clear should succeed");
malunal_size_t count = 99, capacity = 0;
container_count(container, &count);
container_capacity(container, &capacity);
CHECK_EQ_SZ(count, 0, "clear should reset count to 0");
CHECK_EQ_SZ(capacity, 4, "clear should not release/shrink capacity");
container_finalize(container);
}
/* ------------------------------------------------------------------ */
/* vector_set */
/* ------------------------------------------------------------------ */
TEST(set_overwrites_existing_element) {
vector_t vector = vector_container();
allocator_t libc_alloc = libc_allocator();
container_mptr_t container = setup_container(&vector, &libc_alloc, sizeof(int), 4);
int a = 1, b = 2, replacement = 99;
container_append(container, &a);
container_append(container, &b);
container_exception_t rc = vector_set(&vector, 0, &replacement);
CHECK_EQ_INT(rc, CONTAINER_ERROR_SUCCESS, "vector_set should succeed for an in-range index");
int out = 0;
vector_get(&vector, 0, &out);
CHECK_EQ_INT(out, 99, "vector_set should overwrite the element at the given index");
container_finalize(container);
}
TEST(get_set_report_out_of_bounds_at_count) {
/* Per container.h/vector.h, get/set should fail once the index
* reaches or exceeds the number of *live* elements. Capacity may
* still have room, but reading/writing an unset slot is out of
* bounds. */
vector_t vector = vector_container();
allocator_t libc_alloc = libc_allocator();
container_mptr_t container = setup_container(&vector, &libc_alloc, sizeof(int), 4);
int a = 1;
container_append(container, &a); /* count == 1, capacity == 4 */
int out = 0;
container_exception_t rc = vector_get(&vector, 1, &out);
CHECK_EQ_INT(rc, CONTAINER_ERROR_OUT_OF_BOUNDS,
"get(index == count) should be out of bounds, not read uninitialized capacity");
container_finalize(container);
}
TEST(get_set_report_out_of_bounds_at_capacity) {
/* index == capacity is one past the last valid slot. */
vector_t vector = vector_container();
allocator_t libc_alloc = libc_allocator();
container_mptr_t container = setup_container(&vector, &libc_alloc, sizeof(int), 4);
int a = 1;
container_append(container, &a);
int out = 0;
container_exception_t rc = vector_get(&vector, 4, &out);
CHECK_EQ_INT(rc, CONTAINER_ERROR_OUT_OF_BOUNDS,
"get(index == capacity) should be out of bounds");
container_finalize(container);
}
/* ------------------------------------------------------------------ */
/* vector_index_of */
/* ------------------------------------------------------------------ */
TEST(index_of_finds_present_element) {
vector_t vector = vector_container();
allocator_t libc_alloc = libc_allocator();
container_mptr_t container = setup_container(&vector, &libc_alloc, sizeof(int), 4);
int a = 5, b = 6, c = 7;
container_append(container, &a);
container_append(container, &b);
container_append(container, &c);
malunal_size_t idx = (malunal_size_t)-1;
int needle = 6;
container_exception_t rc = vector_index_of(&vector, &needle, &idx);
CHECK_EQ_INT(rc, CONTAINER_ERROR_SUCCESS, "index_of should succeed when element is present");
CHECK_EQ_SZ(idx, 1, "index_of should report the correct index of the matching element");
container_finalize(container);
}
TEST(index_of_reports_missing_element) {
vector_t vector = vector_container();
allocator_t libc_alloc = libc_allocator();
container_mptr_t container = setup_container(&vector, &libc_alloc, sizeof(int), 4);
int a = 5;
container_append(container, &a);
malunal_size_t idx = 0;
int needle = 999;
vector_index_of(&vector, &needle, &idx);
/* Documented contract: outidx should be set to a not-found sentinel
* (-1) when the element isn't found. */
CHECK_EQ_SZ(idx, (malunal_size_t)-1, "index_of should report a not-found sentinel");
container_finalize(container);
}
TEST(index_of_actually_compares_against_the_search_element) {
/* vector_index_of_def_impl's loop does
* memcmp(buffer, buffer + stride, stride)
* i.e. it compares each element to its *neighbor*, and never touches
* the `element` argument at all. So it doesn't search for the given
* value -- it finds the first position holding two equal adjacent
* elements, whatever the caller was actually looking for. This test
* makes that failure mode concrete and unambiguous: [1, 1, 2],
* searching for 2, should report index 2 -- not index 0. */
vector_t vector = vector_container();
allocator_t libc_alloc = libc_allocator();
container_mptr_t container = setup_container(&vector, &libc_alloc, sizeof(int), 4);
int a = 1, b = 1, c = 2;
container_append(container, &a);
container_append(container, &b);
container_append(container, &c); /* [1, 1, 2] */
malunal_size_t idx = 0;
int needle = 2;
vector_index_of(&vector, &needle, &idx);
CHECK_EQ_SZ(idx, 2, "index_of should find the index of the *searched* value, not an adjacent duplicate pair");
container_finalize(container);
}
/* ------------------------------------------------------------------ */
/* vector_insert_at */
/* ------------------------------------------------------------------ */
TEST(insert_at_end_appends_value) {
vector_t vector = vector_container();
allocator_t libc_alloc = libc_allocator();
container_mptr_t container = setup_container(&vector, &libc_alloc, sizeof(int), 4);
int a = 1, b = 2, c = 3;
container_append(container, &a);
container_append(container, &b);
container_exception_t rc = vector_insert_at(&vector, 2, &c);
CHECK_EQ_INT(rc, CONTAINER_ERROR_SUCCESS, "insert_at(count) should succeed");
int out = 0;
vector_get(&vector, 2, &out);
CHECK_EQ_INT(out, 3, "inserted value should be readable back at the target index");
container_finalize(container);
}
TEST(insert_at_middle_shifts_later_elements) {
vector_t vector = vector_container();
allocator_t libc_alloc = libc_allocator();
container_mptr_t container = setup_container(&vector, &libc_alloc, sizeof(int), 8);
int a = 1, b = 3, mid = 2;
container_append(container, &a);
container_append(container, &b); /* [1, 3] */
container_exception_t rc = vector_insert_at(&vector, 1, &mid); /* -> [1, 2, 3] */
CHECK_EQ_INT(rc, CONTAINER_ERROR_SUCCESS, "insert_at(1) should succeed");
int out0 = 0, out1 = 0, out2 = 0;
vector_get(&vector, 0, &out0);
vector_get(&vector, 1, &out1);
vector_get(&vector, 2, &out2);
CHECK_EQ_INT(out0, 1, "element before insertion point unchanged");
CHECK_EQ_INT(out1, 2, "inserted element lands at requested index");
CHECK_EQ_INT(out2, 3, "element after insertion point shifted right");
container_finalize(container);
}
/* ------------------------------------------------------------------ */
/* vector_remove_at */
/* ------------------------------------------------------------------ */
TEST(remove_at_last_leaves_earlier_elements_untouched) {
vector_t vector = vector_container();
allocator_t libc_alloc = libc_allocator();
container_mptr_t container = setup_container(&vector, &libc_alloc, sizeof(int), 8);
int a = 1, b = 2, c = 3;
container_append(container, &a);
container_append(container, &b);
container_append(container, &c); /* [1, 2, 3] */
container_exception_t rc = vector_remove_at(&vector, 2); /* remove the '3' */
CHECK_EQ_INT(rc, CONTAINER_ERROR_SUCCESS, "remove_at(last) should succeed");
malunal_size_t count = 0;
container_count(container, &count);
CHECK_EQ_SZ(count, 2, "count should drop by one after removal");
int out0 = 0, out1 = 0;
vector_get(&vector, 0, &out0);
vector_get(&vector, 1, &out1);
CHECK_EQ_INT(out0, 1, "element 0 should be unaffected by removing the last element");
CHECK_EQ_INT(out1, 2, "element 1 should be unaffected by removing the last element");
container_finalize(container);
}
TEST(remove_at_middle_shifts_later_elements_left) {
vector_t vector = vector_container();
allocator_t libc_alloc = libc_allocator();
container_mptr_t container = setup_container(&vector, &libc_alloc, sizeof(int), 8);
int a = 1, b = 2, c = 3;
container_append(container, &a);
container_append(container, &b);
container_append(container, &c); /* [1, 2, 3] */
container_exception_t rc = vector_remove_at(&vector, 0); /* -> [2, 3] */
CHECK_EQ_INT(rc, CONTAINER_ERROR_SUCCESS, "remove_at(0) should succeed");
int out0 = 0, out1 = 0;
vector_get(&vector, 0, &out0);
vector_get(&vector, 1, &out1);
CHECK_EQ_INT(out0, 2, "elements after the removed index should shift left");
CHECK_EQ_INT(out1, 3, "elements after the removed index should shift left");
container_finalize(container);
}
TEST(remove_at_on_empty_vector_is_out_of_bounds) {
vector_t vector = vector_container();
allocator_t libc_alloc = libc_allocator();
container_mptr_t container = setup_container(&vector, &libc_alloc, sizeof(int), 4);
container_exception_t rc = vector_remove_at(&vector, 0);
CHECK_EQ_INT(rc, CONTAINER_ERROR_OUT_OF_BOUNDS,
"remove_at on an empty vector should be rejected as out of bounds");
container_finalize(container);
}
/* ------------------------------------------------------------------ */
/* container_remove */
/* ------------------------------------------------------------------ */
TEST(container_remove_removes_the_first_matching_element) {
vector_t vector = vector_container();
allocator_t libc_alloc = libc_allocator();
container_mptr_t container = setup_container(&vector, &libc_alloc, sizeof(int), 8);
int a = 10, b = 20, c = 30;
container_append(container, &a);
container_append(container, &b);
container_append(container, &c); /* [10, 20, 30] */
container_exception_t rc = container_remove(container, &a); /* remove the leading '10' */
CHECK_EQ_INT(rc, CONTAINER_ERROR_SUCCESS, "container_remove should succeed for a present element");
malunal_size_t count = 0;
container_count(container, &count);
CHECK_EQ_SZ(count, 2, "count should drop by one after container_remove");
int out0 = 0;
vector_get(&vector, 0, &out0);
CHECK_EQ_INT(out0, 20, "removing the first element should shift the rest left");
container_finalize(container);
}
TEST(container_remove_of_absent_element_is_success) {
/* container.h: "if an element does not exist in the container, the
* most common response should be CONTAINER_ERROR_SUCCESS." */
vector_t vector = vector_container();
allocator_t libc_alloc = libc_allocator();
container_mptr_t container = setup_container(&vector, &libc_alloc, sizeof(int), 4);
int a = 1;
container_append(container, &a);
int missing = 404;
container_exception_t rc = container_remove(container, &missing);
CHECK_EQ_INT(rc, CONTAINER_ERROR_SUCCESS,
"removing an absent element should report success, per the documented contract");
container_finalize(container);
}
/* ------------------------------------------------------------------ */
/* container_contains */
/* ------------------------------------------------------------------ */
TEST(container_contains_finds_a_present_element) {
vector_t vector = vector_container();
allocator_t libc_alloc = libc_allocator();
container_mptr_t container = setup_container(&vector, &libc_alloc, sizeof(int), 4);
int a = 1, b = 2;
container_append(container, &a);
container_append(container, &b);
container_exception_t rc = container_contains(container, &b);
CHECK_EQ_INT(rc, CONTAINER_ERROR_SUCCESS, "contains should succeed for a present element");
container_finalize(container);
}
TEST(container_contains_rejects_an_absent_element) {
/* vector_contains_impl does `index > 0 ? SUCCESS : FAILURE`. But
* vector_index_of's not-found sentinel is (malunal_size_t)-1, i.e.
* SIZE_MAX, which is also > 0 -- so an absent element is reported as
* *found*. (Combined with index_of's separate bug of comparing
* adjacent elements instead of the search value, this vector has no
* adjacent duplicates, so index_of legitimately reports "not
* found" via the -1 sentinel here -- and contains still says yes.) */
vector_t vector = vector_container();
allocator_t libc_alloc = libc_allocator();
container_mptr_t container = setup_container(&vector, &libc_alloc, sizeof(int), 4);
int a = 1, b = 2;
container_append(container, &a);
container_append(container, &b); /* [1, 2], no adjacent duplicates */
int missing = 999;
container_exception_t rc = container_contains(container, &missing);
CHECK_EQ_INT(rc, CONTAINER_ERROR_FAILURE, "contains should report failure for an absent element");
container_finalize(container);
}
/* ------------------------------------------------------------------ */
/* runner */
/* ------------------------------------------------------------------ */
int main(void) {
int passed = 0;
printf("running %d tests\n\n", g_test_count);
for (int i = 0; i < g_test_count; i++) {
printf("[ RUN ] %s\n", g_tests[i].name);
int ok = run_isolated(g_tests[i].fn);
printf("[%s] %s\n\n", ok ? " PASS " : " FAIL ", g_tests[i].name);
passed += ok;
}
printf("---------------------------------------------\n");
printf("%d / %d tests passed\n", passed, g_test_count);
return passed == g_test_count ? 0 : 1;
}