Files
dusk/test/thread/test_thread.c
T
2026-06-01 10:57:40 -05:00

217 lines
5.5 KiB
C

/**
* 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);
}