Prepping for async
This commit is contained in:
@@ -4,6 +4,8 @@
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
add_subdirectory(assert)
|
||||
add_subdirectory(error)
|
||||
add_subdirectory(thread)
|
||||
add_subdirectory(display)
|
||||
# add_subdirectory(rpg)
|
||||
# add_subdirectory(item)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
include(dusktest)
|
||||
|
||||
# Tests
|
||||
dusktest(test_error.c)
|
||||
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "error/error.h"
|
||||
#include "thread/thread.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
// Helper that throws an error.
|
||||
static errorret_t helper_throw(void) {
|
||||
errorThrow("Test error %d", 42);
|
||||
}
|
||||
|
||||
// Helper that returns ok.
|
||||
static errorret_t helper_ok(void) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
// Helper that chains to helper_throw.
|
||||
static errorret_t helper_chain(void) {
|
||||
errorChain(helper_throw());
|
||||
errorOk();
|
||||
}
|
||||
|
||||
static void test_errorThrow(void **state) {
|
||||
errorret_t ret = helper_throw();
|
||||
|
||||
assert_int_not_equal(ret.code, ERROR_OK);
|
||||
assert_non_null(ret.state);
|
||||
assert_non_null(ret.state->message);
|
||||
assert_non_null(ret.state->lines);
|
||||
|
||||
// Message should contain our format argument
|
||||
assert_non_null(strstr(ret.state->message, "42"));
|
||||
|
||||
errorCatch(ret);
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_errorOk(void **state) {
|
||||
errorret_t ret = helper_ok();
|
||||
|
||||
assert_int_equal(ret.code, ERROR_OK);
|
||||
assert_null(ret.state);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_errorIsOk(void **state) {
|
||||
errorret_t ok = helper_ok();
|
||||
assert_true(errorIsOk(ok));
|
||||
assert_false(errorIsNotOk(ok));
|
||||
|
||||
errorret_t err = helper_throw();
|
||||
assert_false(errorIsOk(err));
|
||||
assert_true(errorIsNotOk(err));
|
||||
|
||||
errorCatch(err);
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_errorChain(void **state) {
|
||||
errorret_t ret = helper_chain();
|
||||
|
||||
// Error propagated up
|
||||
assert_int_not_equal(ret.code, ERROR_OK);
|
||||
assert_non_null(ret.state);
|
||||
assert_non_null(ret.state->lines);
|
||||
|
||||
// Lines should contain at least two stack entries
|
||||
int32_t count = 0;
|
||||
const char_t *p = ret.state->lines;
|
||||
while((p = strstr(p, " at ")) != NULL) {
|
||||
count++;
|
||||
p++;
|
||||
}
|
||||
assert_true(count >= 2);
|
||||
|
||||
errorCatch(ret);
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_errorCatch_ok(void **state) {
|
||||
// Catching an ok ret should be a no-op
|
||||
errorret_t ret = helper_ok();
|
||||
errorCatch(ret);
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_errorCatch_error(void **state) {
|
||||
errorret_t ret = helper_throw();
|
||||
assert_int_not_equal(ret.code, ERROR_OK);
|
||||
|
||||
errorCatch(ret);
|
||||
|
||||
// After catch the global state should be cleared
|
||||
assert_int_equal(ERROR_STATE.code, ERROR_OK);
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
// --- Threaded tests ---
|
||||
|
||||
typedef struct {
|
||||
errorcode_t capturedCode;
|
||||
bool_t messageHas42;
|
||||
} thread_test_data_t;
|
||||
|
||||
static void helper_thread_throw(thread_t *thread) {
|
||||
errorret_t ret = helper_throw();
|
||||
thread_test_data_t *data = (thread_test_data_t *)thread->data;
|
||||
data->capturedCode = ERROR_STATE.code;
|
||||
data->messageHas42 = (strstr(ret.state->message, "42") != NULL);
|
||||
errorCatch(ret);
|
||||
}
|
||||
|
||||
static void test_error_thread_isolation(void **state) {
|
||||
// Main thread state is clean before the test.
|
||||
assert_int_equal(ERROR_STATE.code, ERROR_OK);
|
||||
|
||||
thread_test_data_t data = { .capturedCode = ERROR_OK, .messageHas42 = false };
|
||||
|
||||
thread_t thread;
|
||||
threadInit(&thread, helper_thread_throw);
|
||||
thread.data = &data;
|
||||
threadStart(&thread);
|
||||
threadStop(&thread);
|
||||
|
||||
// Worker saw ERROR_NOT_OK in its own ERROR_STATE.
|
||||
assert_int_equal(data.capturedCode, ERROR_NOT_OK);
|
||||
assert_true(data.messageHas42);
|
||||
|
||||
// Main thread ERROR_STATE was not touched by the worker.
|
||||
assert_int_equal(ERROR_STATE.code, ERROR_OK);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
#define CONCURRENT_THREAD_COUNT 4
|
||||
|
||||
static thread_test_data_t concurrent_data[CONCURRENT_THREAD_COUNT];
|
||||
static thread_t concurrent_threads[CONCURRENT_THREAD_COUNT];
|
||||
|
||||
static void test_error_concurrent_throw(void **state) {
|
||||
for(int32_t i = 0; i < CONCURRENT_THREAD_COUNT; i++) {
|
||||
concurrent_data[i].capturedCode = ERROR_OK;
|
||||
concurrent_data[i].messageHas42 = false;
|
||||
threadInit(&concurrent_threads[i], helper_thread_throw);
|
||||
concurrent_threads[i].data = &concurrent_data[i];
|
||||
}
|
||||
|
||||
for(int32_t i = 0; i < CONCURRENT_THREAD_COUNT; i++) {
|
||||
threadStart(&concurrent_threads[i]);
|
||||
}
|
||||
|
||||
for(int32_t i = 0; i < CONCURRENT_THREAD_COUNT; i++) {
|
||||
threadStop(&concurrent_threads[i]);
|
||||
}
|
||||
|
||||
// Every worker must have seen its own independent error.
|
||||
for(int32_t i = 0; i < CONCURRENT_THREAD_COUNT; i++) {
|
||||
assert_int_equal(concurrent_data[i].capturedCode, ERROR_NOT_OK);
|
||||
assert_true(concurrent_data[i].messageHas42);
|
||||
}
|
||||
|
||||
// Main thread is still clean.
|
||||
assert_int_equal(ERROR_STATE.code, ERROR_OK);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_errorThrow),
|
||||
cmocka_unit_test(test_errorOk),
|
||||
cmocka_unit_test(test_errorIsOk),
|
||||
cmocka_unit_test(test_errorChain),
|
||||
cmocka_unit_test(test_errorCatch_ok),
|
||||
cmocka_unit_test(test_errorCatch_error),
|
||||
cmocka_unit_test(test_error_thread_isolation),
|
||||
cmocka_unit_test(test_error_concurrent_throw),
|
||||
};
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
include(dusktest)
|
||||
|
||||
# Tests
|
||||
dusktest(test_thread.c)
|
||||
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "thread/thread.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
static void helper_noop(thread_t *thread) {
|
||||
// intentionally empty: one-shot thread that exits immediately
|
||||
}
|
||||
|
||||
static void helper_loop(thread_t *thread) {
|
||||
while(!threadShouldStop(thread)) {}
|
||||
}
|
||||
|
||||
static void helper_write_data(thread_t *thread) {
|
||||
int32_t *value = (int32_t *)thread->data;
|
||||
*value = 42;
|
||||
}
|
||||
|
||||
// --- thread_t tests ---
|
||||
|
||||
static void test_threadInit(void **state) {
|
||||
thread_t thread;
|
||||
threadInit(&thread, helper_noop);
|
||||
|
||||
assert_int_equal(thread.state, THREAD_STATE_STOPPED);
|
||||
assert_ptr_equal(thread.callback, helper_noop);
|
||||
assert_null(thread.data);
|
||||
}
|
||||
|
||||
static void test_thread_start_stop(void **state) {
|
||||
thread_t thread;
|
||||
threadInit(&thread, helper_noop);
|
||||
threadStart(&thread);
|
||||
threadStop(&thread);
|
||||
|
||||
assert_int_equal(thread.state, THREAD_STATE_STOPPED);
|
||||
}
|
||||
|
||||
static void test_thread_should_stop(void **state) {
|
||||
// threadStop blocks until STOPPED — if threadShouldStop is broken the
|
||||
// looping callback never exits and this test hangs / times out.
|
||||
thread_t thread;
|
||||
threadInit(&thread, helper_loop);
|
||||
threadStart(&thread);
|
||||
threadStop(&thread);
|
||||
|
||||
assert_int_equal(thread.state, THREAD_STATE_STOPPED);
|
||||
}
|
||||
|
||||
static void test_thread_data(void **state) {
|
||||
int32_t value = 0;
|
||||
|
||||
thread_t thread;
|
||||
threadInit(&thread, helper_write_data);
|
||||
thread.data = &value;
|
||||
threadStart(&thread);
|
||||
threadStop(&thread);
|
||||
|
||||
// After threadStop the callback has definitely run.
|
||||
assert_int_equal(value, 42);
|
||||
}
|
||||
|
||||
static void test_thread_restart(void **state) {
|
||||
// A thread can be started, stopped, and started again.
|
||||
thread_t thread;
|
||||
threadInit(&thread, helper_noop);
|
||||
|
||||
threadStart(&thread);
|
||||
threadStop(&thread);
|
||||
assert_int_equal(thread.state, THREAD_STATE_STOPPED);
|
||||
|
||||
// Re-initialise so threadId / state are reset, then start again.
|
||||
threadInit(&thread, helper_noop);
|
||||
threadStart(&thread);
|
||||
threadStop(&thread);
|
||||
assert_int_equal(thread.state, THREAD_STATE_STOPPED);
|
||||
}
|
||||
|
||||
// --- threadmutex_t tests ---
|
||||
|
||||
static void test_threadMutex_lock_unlock(void **state) {
|
||||
threadmutex_t mutex;
|
||||
threadMutexInit(&mutex);
|
||||
|
||||
threadMutexLock(&mutex);
|
||||
threadMutexUnlock(&mutex);
|
||||
|
||||
threadMutexDispose(&mutex);
|
||||
}
|
||||
|
||||
// Shared data for try-lock test. Uses volatile phase to coordinate the two
|
||||
// threads without introducing a second mutex.
|
||||
typedef struct {
|
||||
threadmutex_t *target;
|
||||
volatile int32_t phase;
|
||||
bool_t resultWhileLocked;
|
||||
bool_t resultAfterUnlock;
|
||||
} trylock_data_t;
|
||||
|
||||
static void helper_trylock(thread_t *thread) {
|
||||
trylock_data_t *data = (trylock_data_t *)thread->data;
|
||||
|
||||
// Phase 1: main holds the lock — trylock must fail.
|
||||
while(data->phase != 1) {}
|
||||
data->resultWhileLocked = threadMutexTryLock(data->target);
|
||||
data->phase = 2;
|
||||
|
||||
// Phase 3: main released the lock — trylock must succeed.
|
||||
while(data->phase != 3) {}
|
||||
data->resultAfterUnlock = threadMutexTryLock(data->target);
|
||||
if(data->resultAfterUnlock) {
|
||||
threadMutexUnlock(data->target);
|
||||
}
|
||||
data->phase = 4;
|
||||
}
|
||||
|
||||
static void test_threadMutex_try_lock(void **state) {
|
||||
threadmutex_t mutex;
|
||||
threadMutexInit(&mutex);
|
||||
|
||||
trylock_data_t data = {
|
||||
.target = &mutex,
|
||||
.phase = 0,
|
||||
.resultWhileLocked = false,
|
||||
.resultAfterUnlock = false
|
||||
};
|
||||
|
||||
thread_t thread;
|
||||
threadInit(&thread, helper_trylock);
|
||||
thread.data = &data;
|
||||
threadStart(&thread);
|
||||
|
||||
// Hold the lock, then let the helper try.
|
||||
threadMutexLock(&mutex);
|
||||
data.phase = 1;
|
||||
while(data.phase != 2) {}
|
||||
assert_false(data.resultWhileLocked);
|
||||
|
||||
// Release, then let the helper try again.
|
||||
threadMutexUnlock(&mutex);
|
||||
data.phase = 3;
|
||||
while(data.phase != 4) {}
|
||||
assert_true(data.resultAfterUnlock);
|
||||
|
||||
threadStop(&thread);
|
||||
threadMutexDispose(&mutex);
|
||||
}
|
||||
|
||||
// Mutual-exclusion test: N threads each increment a shared counter M times
|
||||
// under a mutex. The final value must be exactly N*M.
|
||||
#define MUTEX_THREADS 4
|
||||
#define MUTEX_ITERATIONS 10000
|
||||
|
||||
typedef struct {
|
||||
threadmutex_t *mutex;
|
||||
int32_t *counter;
|
||||
} counter_data_t;
|
||||
|
||||
static counter_data_t counter_thread_data[MUTEX_THREADS];
|
||||
static thread_t counter_threads[MUTEX_THREADS];
|
||||
|
||||
static void helper_increment(thread_t *thread) {
|
||||
counter_data_t *data = (counter_data_t *)thread->data;
|
||||
for(int32_t i = 0; i < MUTEX_ITERATIONS; i++) {
|
||||
threadMutexLock(data->mutex);
|
||||
(*data->counter)++;
|
||||
threadMutexUnlock(data->mutex);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_threadMutex_mutual_exclusion(void **state) {
|
||||
threadmutex_t mutex;
|
||||
threadMutexInit(&mutex);
|
||||
int32_t counter = 0;
|
||||
|
||||
for(int32_t i = 0; i < MUTEX_THREADS; i++) {
|
||||
counter_thread_data[i].mutex = &mutex;
|
||||
counter_thread_data[i].counter = &counter;
|
||||
threadInit(&counter_threads[i], helper_increment);
|
||||
counter_threads[i].data = &counter_thread_data[i];
|
||||
}
|
||||
|
||||
for(int32_t i = 0; i < MUTEX_THREADS; i++) {
|
||||
threadStart(&counter_threads[i]);
|
||||
}
|
||||
|
||||
for(int32_t i = 0; i < MUTEX_THREADS; i++) {
|
||||
threadStop(&counter_threads[i]);
|
||||
}
|
||||
|
||||
assert_int_equal(counter, MUTEX_THREADS * MUTEX_ITERATIONS);
|
||||
|
||||
threadMutexDispose(&mutex);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_threadInit),
|
||||
cmocka_unit_test(test_thread_start_stop),
|
||||
cmocka_unit_test(test_thread_should_stop),
|
||||
cmocka_unit_test(test_thread_data),
|
||||
cmocka_unit_test(test_thread_restart),
|
||||
cmocka_unit_test(test_threadMutex_lock_unlock),
|
||||
cmocka_unit_test(test_threadMutex_try_lock),
|
||||
cmocka_unit_test(test_threadMutex_mutual_exclusion),
|
||||
};
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
Reference in New Issue
Block a user