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