56230dd340
- util/ref.c: refUnlock's assert(count >= 0) on an unsigned count was tautological, so a double-unlock silently underflowed to UINT32_MAX instead of asserting. Now asserts count > 0 before decrementing. - duskpsp/time/timepsp.c: timeGetRealTimeZonePSP returned hours while every other platform (and timeepoch.c's math) expects seconds. - thread.c: threadHandler reset threadId outside the mutex, after signaling STOPPED, letting a caller's immediate threadStart() race threadStartRequest()'s "thread id not 0" assert. threadId is now reset inside the same locked section. - tools/color.py: whole-number CSV channels (0, 1) produced invalid C float literals (0f, 1f) for the generated COLOR_*_3F/_4F macros. Route through float() so they always stringify with a decimal point. Also adds .claude/code-check.md: a full review pass over every subsystem in src/dusk/, with the above (plus several other findings not yet acted on) written up in detail. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
218 lines
5.7 KiB
C
218 lines
5.7 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 on the same
|
|
// thread_t -- without re-calling threadInit() -- because threadId is
|
|
// reset under stateMutex before threadHandler() signals STOPPED, so
|
|
// threadStop() returning guarantees threadStartRequest()'s "thread id
|
|
// not 0" assert won't race against it. Looping a few times gives any
|
|
// regression of that ordering repeated chances to hit the window.
|
|
thread_t thread;
|
|
threadInit(&thread, helper_noop);
|
|
|
|
for(int32_t i = 0; i < 20; i++) {
|
|
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);
|
|
}
|