some random fixes

This commit is contained in:
2026-08-08 01:18:07 -05:00
parent 36fb359aa2
commit 128f9ab9d4
15 changed files with 1041 additions and 52 deletions
+2
View File
@@ -6,7 +6,9 @@
add_subdirectory(animation)
add_subdirectory(assert)
add_subdirectory(asset)
add_subdirectory(console)
add_subdirectory(error)
add_subdirectory(event)
add_subdirectory(thread)
add_subdirectory(display)
add_subdirectory(rpg)
+1
View File
@@ -6,5 +6,6 @@
include(dusktest)
# Tests
dusktest(test_easing.c)
dusktest(test_keyframe.c)
dusktest(test_animation.c)
+188
View File
@@ -0,0 +1,188 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "animation/easing.h"
#define EASING_TOLERANCE 0.0001f
static void test_easingLinear(void **state) {
assert_float_equal(easingLinear(0.0f), 0.0f, EASING_TOLERANCE);
assert_float_equal(easingLinear(0.25f), 0.25f, EASING_TOLERANCE);
assert_float_equal(easingLinear(0.5f), 0.5f, EASING_TOLERANCE);
assert_float_equal(easingLinear(0.75f), 0.75f, EASING_TOLERANCE);
assert_float_equal(easingLinear(1.0f), 1.0f, EASING_TOLERANCE);
}
static void test_easingInSine(void **state) {
assert_float_equal(easingInSine(0.0f), 0.0f, EASING_TOLERANCE);
assert_float_equal(easingInSine(0.25f), 0.076120f, EASING_TOLERANCE);
assert_float_equal(easingInSine(0.5f), 0.292893f, EASING_TOLERANCE);
assert_float_equal(easingInSine(0.75f), 0.617317f, EASING_TOLERANCE);
assert_float_equal(easingInSine(1.0f), 1.0f, EASING_TOLERANCE);
}
static void test_easingOutSine(void **state) {
assert_float_equal(easingOutSine(0.0f), 0.0f, EASING_TOLERANCE);
assert_float_equal(easingOutSine(0.25f), 0.382683f, EASING_TOLERANCE);
assert_float_equal(easingOutSine(0.5f), 0.707107f, EASING_TOLERANCE);
assert_float_equal(easingOutSine(0.75f), 0.923880f, EASING_TOLERANCE);
assert_float_equal(easingOutSine(1.0f), 1.0f, EASING_TOLERANCE);
}
static void test_easingInOutSine(void **state) {
assert_float_equal(easingInOutSine(0.0f), 0.0f, EASING_TOLERANCE);
assert_float_equal(easingInOutSine(0.25f), 0.146447f, EASING_TOLERANCE);
assert_float_equal(easingInOutSine(0.5f), 0.5f, EASING_TOLERANCE);
assert_float_equal(easingInOutSine(0.75f), 0.853553f, EASING_TOLERANCE);
assert_float_equal(easingInOutSine(1.0f), 1.0f, EASING_TOLERANCE);
}
static void test_easingInQuad(void **state) {
assert_float_equal(easingInQuad(0.0f), 0.0f, EASING_TOLERANCE);
assert_float_equal(easingInQuad(0.25f), 0.0625f, EASING_TOLERANCE);
assert_float_equal(easingInQuad(0.5f), 0.25f, EASING_TOLERANCE);
assert_float_equal(easingInQuad(0.75f), 0.5625f, EASING_TOLERANCE);
assert_float_equal(easingInQuad(1.0f), 1.0f, EASING_TOLERANCE);
}
static void test_easingOutQuad(void **state) {
assert_float_equal(easingOutQuad(0.0f), 0.0f, EASING_TOLERANCE);
assert_float_equal(easingOutQuad(0.25f), 0.4375f, EASING_TOLERANCE);
assert_float_equal(easingOutQuad(0.5f), 0.75f, EASING_TOLERANCE);
assert_float_equal(easingOutQuad(0.75f), 0.9375f, EASING_TOLERANCE);
assert_float_equal(easingOutQuad(1.0f), 1.0f, EASING_TOLERANCE);
}
static void test_easingInOutQuad(void **state) {
assert_float_equal(easingInOutQuad(0.0f), 0.0f, EASING_TOLERANCE);
assert_float_equal(easingInOutQuad(0.25f), 0.125f, EASING_TOLERANCE);
assert_float_equal(easingInOutQuad(0.5f), 0.5f, EASING_TOLERANCE);
assert_float_equal(easingInOutQuad(0.75f), 0.875f, EASING_TOLERANCE);
assert_float_equal(easingInOutQuad(1.0f), 1.0f, EASING_TOLERANCE);
}
static void test_easingInCubic(void **state) {
assert_float_equal(easingInCubic(0.0f), 0.0f, EASING_TOLERANCE);
assert_float_equal(easingInCubic(0.25f), 0.015625f, EASING_TOLERANCE);
assert_float_equal(easingInCubic(0.5f), 0.125f, EASING_TOLERANCE);
assert_float_equal(easingInCubic(0.75f), 0.421875f, EASING_TOLERANCE);
assert_float_equal(easingInCubic(1.0f), 1.0f, EASING_TOLERANCE);
}
static void test_easingOutCubic(void **state) {
assert_float_equal(easingOutCubic(0.0f), 0.0f, EASING_TOLERANCE);
assert_float_equal(easingOutCubic(0.25f), 0.578125f, EASING_TOLERANCE);
assert_float_equal(easingOutCubic(0.5f), 0.875f, EASING_TOLERANCE);
assert_float_equal(easingOutCubic(0.75f), 0.984375f, EASING_TOLERANCE);
assert_float_equal(easingOutCubic(1.0f), 1.0f, EASING_TOLERANCE);
}
static void test_easingInOutCubic(void **state) {
assert_float_equal(easingInOutCubic(0.0f), 0.0f, EASING_TOLERANCE);
assert_float_equal(easingInOutCubic(0.25f), 0.0625f, EASING_TOLERANCE);
assert_float_equal(easingInOutCubic(0.5f), 0.5f, EASING_TOLERANCE);
assert_float_equal(easingInOutCubic(0.75f), 0.9375f, EASING_TOLERANCE);
assert_float_equal(easingInOutCubic(1.0f), 1.0f, EASING_TOLERANCE);
}
static void test_easingInQuart(void **state) {
assert_float_equal(easingInQuart(0.0f), 0.0f, EASING_TOLERANCE);
assert_float_equal(easingInQuart(0.25f), 0.003906f, EASING_TOLERANCE);
assert_float_equal(easingInQuart(0.5f), 0.0625f, EASING_TOLERANCE);
assert_float_equal(easingInQuart(0.75f), 0.316406f, EASING_TOLERANCE);
assert_float_equal(easingInQuart(1.0f), 1.0f, EASING_TOLERANCE);
}
static void test_easingOutQuart(void **state) {
assert_float_equal(easingOutQuart(0.0f), 0.0f, EASING_TOLERANCE);
assert_float_equal(easingOutQuart(0.25f), 0.683594f, EASING_TOLERANCE);
assert_float_equal(easingOutQuart(0.5f), 0.9375f, EASING_TOLERANCE);
assert_float_equal(easingOutQuart(0.75f), 0.996094f, EASING_TOLERANCE);
assert_float_equal(easingOutQuart(1.0f), 1.0f, EASING_TOLERANCE);
}
static void test_easingInOutQuart(void **state) {
assert_float_equal(easingInOutQuart(0.0f), 0.0f, EASING_TOLERANCE);
assert_float_equal(easingInOutQuart(0.25f), 0.03125f, EASING_TOLERANCE);
assert_float_equal(easingInOutQuart(0.5f), 0.5f, EASING_TOLERANCE);
assert_float_equal(easingInOutQuart(0.75f), 0.96875f, EASING_TOLERANCE);
assert_float_equal(easingInOutQuart(1.0f), 1.0f, EASING_TOLERANCE);
}
static void test_easingInBack(void **state) {
// "In back" overshoots below 0 before rising to 1 - that's the point of it.
assert_float_equal(easingInBack(0.0f), 0.0f, EASING_TOLERANCE);
assert_float_equal(easingInBack(0.25f), -0.064137f, EASING_TOLERANCE);
assert_float_equal(easingInBack(0.5f), -0.087698f, EASING_TOLERANCE);
assert_float_equal(easingInBack(0.75f), 0.182590f, EASING_TOLERANCE);
assert_float_equal(easingInBack(1.0f), 1.0f, EASING_TOLERANCE);
}
static void test_easingOutBack(void **state) {
// "Out back" overshoots above 1 before settling at 1.
assert_float_equal(easingOutBack(0.0f), 0.0f, EASING_TOLERANCE);
assert_float_equal(easingOutBack(0.25f), 0.817410f, EASING_TOLERANCE);
assert_float_equal(easingOutBack(0.5f), 1.087697f, EASING_TOLERANCE);
assert_float_equal(easingOutBack(0.75f), 1.064137f, EASING_TOLERANCE);
assert_float_equal(easingOutBack(1.0f), 1.0f, EASING_TOLERANCE);
}
static void test_easingInOutBack(void **state) {
assert_float_equal(easingInOutBack(0.0f), 0.0f, EASING_TOLERANCE);
assert_float_equal(easingInOutBack(0.25f), -0.099682f, EASING_TOLERANCE);
assert_float_equal(easingInOutBack(0.5f), 0.5f, EASING_TOLERANCE);
assert_float_equal(easingInOutBack(0.75f), 1.099682f, EASING_TOLERANCE);
assert_float_equal(easingInOutBack(1.0f), 1.0f, EASING_TOLERANCE);
}
static void test_easingApplyDispatchesToEachType(void **state) {
assert_float_equal(easingApply(EASING_LINEAR, 0.5f), easingLinear(0.5f), EASING_TOLERANCE);
assert_float_equal(easingApply(EASING_IN_SINE, 0.5f), easingInSine(0.5f), EASING_TOLERANCE);
assert_float_equal(easingApply(EASING_OUT_SINE, 0.5f), easingOutSine(0.5f), EASING_TOLERANCE);
assert_float_equal(easingApply(EASING_IN_OUT_SINE, 0.5f), easingInOutSine(0.5f), EASING_TOLERANCE);
assert_float_equal(easingApply(EASING_IN_QUAD, 0.5f), easingInQuad(0.5f), EASING_TOLERANCE);
assert_float_equal(easingApply(EASING_OUT_QUAD, 0.5f), easingOutQuad(0.5f), EASING_TOLERANCE);
assert_float_equal(easingApply(EASING_IN_OUT_QUAD, 0.5f), easingInOutQuad(0.5f), EASING_TOLERANCE);
assert_float_equal(easingApply(EASING_IN_CUBIC, 0.5f), easingInCubic(0.5f), EASING_TOLERANCE);
assert_float_equal(easingApply(EASING_OUT_CUBIC, 0.5f), easingOutCubic(0.5f), EASING_TOLERANCE);
assert_float_equal(easingApply(EASING_IN_OUT_CUBIC, 0.5f), easingInOutCubic(0.5f), EASING_TOLERANCE);
assert_float_equal(easingApply(EASING_IN_QUART, 0.5f), easingInQuart(0.5f), EASING_TOLERANCE);
assert_float_equal(easingApply(EASING_OUT_QUART, 0.5f), easingOutQuart(0.5f), EASING_TOLERANCE);
assert_float_equal(easingApply(EASING_IN_OUT_QUART, 0.5f), easingInOutQuart(0.5f), EASING_TOLERANCE);
assert_float_equal(easingApply(EASING_IN_BACK, 0.5f), easingInBack(0.5f), EASING_TOLERANCE);
assert_float_equal(easingApply(EASING_OUT_BACK, 0.5f), easingOutBack(0.5f), EASING_TOLERANCE);
assert_float_equal(easingApply(EASING_IN_OUT_BACK, 0.5f), easingInOutBack(0.5f), EASING_TOLERANCE);
}
static void test_easingApplyInvalidTypeAsserts(void **state) {
expect_assert_failure(easingApply(EASING_COUNT, 0.5f));
}
int main(int argc, char **argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_easingLinear),
cmocka_unit_test(test_easingInSine),
cmocka_unit_test(test_easingOutSine),
cmocka_unit_test(test_easingInOutSine),
cmocka_unit_test(test_easingInQuad),
cmocka_unit_test(test_easingOutQuad),
cmocka_unit_test(test_easingInOutQuad),
cmocka_unit_test(test_easingInCubic),
cmocka_unit_test(test_easingOutCubic),
cmocka_unit_test(test_easingInOutCubic),
cmocka_unit_test(test_easingInQuart),
cmocka_unit_test(test_easingOutQuart),
cmocka_unit_test(test_easingInOutQuart),
cmocka_unit_test(test_easingInBack),
cmocka_unit_test(test_easingOutBack),
cmocka_unit_test(test_easingInOutBack),
cmocka_unit_test(test_easingApplyDispatchesToEachType),
cmocka_unit_test(test_easingApplyInvalidTypeAsserts),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+9
View File
@@ -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_console.c)
+168
View File
@@ -0,0 +1,168 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "console/console.h"
#include "thread/thread.h"
#include "util/string.h"
static void test_consoleInitDefaults(void **state) {
consoleInit();
assert_false(CONSOLE.visible);
for(int32_t i = 0; i < CONSOLE_HISTORY_MAX; i++) {
assert_string_equal(CONSOLE.line[i], "");
}
consoleDispose();
}
static void test_consolePrintAddsNewestAtEnd(void **state) {
consoleInit();
consolePrint("hello %d", 5);
assert_string_equal(CONSOLE.line[CONSOLE_HISTORY_MAX - 1], "hello 5");
consoleDispose();
}
static void test_consolePrintShiftsHistory(void **state) {
consoleInit();
consolePrint("one");
consolePrint("two");
consolePrint("three");
assert_string_equal(CONSOLE.line[CONSOLE_HISTORY_MAX - 1], "three");
assert_string_equal(CONSOLE.line[CONSOLE_HISTORY_MAX - 2], "two");
assert_string_equal(CONSOLE.line[CONSOLE_HISTORY_MAX - 3], "one");
consoleDispose();
}
static void test_consolePrintOverflowDropsOldest(void **state) {
consoleInit();
const int32_t total = CONSOLE_HISTORY_MAX + 2;
for(int32_t i = 0; i < total; i++) {
consolePrint("line-%d", i);
}
// The oldest two ("line-0", "line-1") must have been dropped.
for(int32_t i = 0; i < CONSOLE_HISTORY_MAX; i++) {
char_t expected[CONSOLE_LINE_MAX];
stringFormat(expected, CONSOLE_LINE_MAX, "line-%d", i + 2);
assert_string_equal(CONSOLE.line[i], expected);
}
consoleDispose();
}
// --- Thread-safety of consolePrint's shared history buffer ---
typedef struct {
int32_t threadIndex;
int32_t iterations;
} consoleprinter_data_t;
static void helper_consolePrinter(thread_t *thread) {
consoleprinter_data_t *data = (consoleprinter_data_t *)thread->data;
for(int32_t i = 0; i < data->iterations; i++) {
consolePrint("T%d-%03d", data->threadIndex, i);
}
}
// Parses a "T<thread>-<seq>" line, requiring the whole line to match.
// Returns false for an empty or malformed line.
static bool_t parsePrintedLine(
const char_t *line, int32_t *threadIndex, int32_t *seq
) {
int32_t consumed = 0;
int32_t matched = sscanf(line, "T%d-%d%n", threadIndex, seq, &consumed);
return matched == 2 && consumed == (int32_t)strlen(line);
}
#define CONSOLE_THREAD_COUNT 4
static void test_consolePrintConcurrentExactlyFillsHistory(void **state) {
// 4 threads x 4 prints = exactly CONSOLE_HISTORY_MAX, so every slot must
// end up holding one well-formed, uncorrupted message - none left empty,
// none torn/interleaved by a missing lock.
consoleInit();
const int32_t iterations = CONSOLE_HISTORY_MAX / CONSOLE_THREAD_COUNT;
consoleprinter_data_t data[CONSOLE_THREAD_COUNT];
thread_t threads[CONSOLE_THREAD_COUNT];
for(int32_t i = 0; i < CONSOLE_THREAD_COUNT; i++) {
data[i].threadIndex = i;
data[i].iterations = iterations;
threadInit(&threads[i], helper_consolePrinter);
threads[i].data = &data[i];
}
for(int32_t i = 0; i < CONSOLE_THREAD_COUNT; i++) threadStart(&threads[i]);
for(int32_t i = 0; i < CONSOLE_THREAD_COUNT; i++) threadStop(&threads[i]);
int32_t seenPerThread[CONSOLE_THREAD_COUNT] = { 0 };
for(int32_t i = 0; i < CONSOLE_HISTORY_MAX; i++) {
int32_t threadIndex, seq;
assert_true(parsePrintedLine(CONSOLE.line[i], &threadIndex, &seq));
assert_true(threadIndex >= 0 && threadIndex < CONSOLE_THREAD_COUNT);
assert_true(seq >= 0 && seq < iterations);
seenPerThread[threadIndex]++;
}
// Every message from every thread survived, since total prints == capacity.
for(int32_t i = 0; i < CONSOLE_THREAD_COUNT; i++) {
assert_int_equal(seenPerThread[i], iterations);
}
consoleDispose();
}
static void test_consolePrintConcurrentStressNoCorruption(void **state) {
// Far more prints than history capacity, from multiple threads at once.
// If printMutex didn't actually serialize the shift+copy in consolePrint,
// concurrent writers would tear each other's memoryMove/memoryCopy calls
// and this would surface as lines failing to parse as "T<n>-<seq>".
consoleInit();
const int32_t iterations = 500;
consoleprinter_data_t data[CONSOLE_THREAD_COUNT];
thread_t threads[CONSOLE_THREAD_COUNT];
for(int32_t i = 0; i < CONSOLE_THREAD_COUNT; i++) {
data[i].threadIndex = i;
data[i].iterations = iterations;
threadInit(&threads[i], helper_consolePrinter);
threads[i].data = &data[i];
}
for(int32_t i = 0; i < CONSOLE_THREAD_COUNT; i++) threadStart(&threads[i]);
for(int32_t i = 0; i < CONSOLE_THREAD_COUNT; i++) threadStop(&threads[i]);
for(int32_t i = 0; i < CONSOLE_HISTORY_MAX; i++) {
int32_t threadIndex, seq;
assert_true(parsePrintedLine(CONSOLE.line[i], &threadIndex, &seq));
assert_true(threadIndex >= 0 && threadIndex < CONSOLE_THREAD_COUNT);
assert_true(seq >= 0 && seq < iterations);
}
consoleDispose();
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_consoleInitDefaults),
cmocka_unit_test(test_consolePrintAddsNewestAtEnd),
cmocka_unit_test(test_consolePrintShiftsHistory),
cmocka_unit_test(test_consolePrintOverflowDropsOldest),
cmocka_unit_test(test_consolePrintConcurrentExactlyFillsHistory),
cmocka_unit_test(test_consolePrintConcurrentStressNoCorruption),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+184
View File
@@ -26,6 +26,19 @@ static errorret_t helper_chain(void) {
errorOk();
}
// Helper that throws with no format arguments (exercises the zero-arg
// ##__VA_ARGS__ branch of the errorThrow macro).
static errorret_t helper_throwNoArgs(void) {
errorThrow("Simple message with no arguments");
}
static const errorcode_t CUSTOM_ERROR_CODE = 7;
// Helper that throws with an explicit, non-default error code.
static errorret_t helper_throwWithCode(void) {
errorThrowWithCode(CUSTOM_ERROR_CODE, "Custom error %d", 99);
}
static void test_errorThrow(void **state) {
errorret_t ret = helper_throw();
@@ -84,6 +97,159 @@ static void test_errorChain(void **state) {
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_errorThrowNoFormatArguments(void **state) {
errorret_t ret = helper_throwNoArgs();
assert_int_not_equal(ret.code, ERROR_OK);
assert_string_equal(ret.state->message, "Simple message with no arguments");
errorCatch(ret);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_errorThrowWithCode(void **state) {
errorret_t ret = helper_throwWithCode();
// The explicit code must round-trip, not just "not OK".
assert_int_equal(ret.code, CUSTOM_ERROR_CODE);
assert_non_null(strstr(ret.state->message, "99"));
errorCatch(ret);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_errorChainOkRetvalPassesThroughUnchanged(void **state) {
// errorChainImpl's own early-return branch, called directly since the
// errorChain macro pre-filters on code != ERROR_OK before ever reaching it.
errorret_t ok = helper_ok();
errorret_t chained = errorChainImpl(ok, __FILE__, __func__, __LINE__);
assert_int_equal(chained.code, ERROR_OK);
assert_null(chained.state);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_errorPrintOkRetvalPassesThroughUnchanged(void **state) {
errorret_t ok = helper_ok();
errorret_t printed = errorPrint(ok);
assert_int_equal(printed.code, ERROR_OK);
assert_null(printed.state);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_errorPrintErrorRetvalPassesThroughUnchanged(void **state) {
errorret_t ret = helper_throw();
errorret_t printed = errorPrint(ret);
// errorPrint logs, but must not mutate or consume the error state.
assert_int_equal(printed.code, ret.code);
assert_ptr_equal(printed.state, ret.state);
assert_non_null(printed.state->message);
errorCatch(printed);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
// --- Negative / misuse paths ---
//
// These bypass the errorThrow/errorChain/errorOk macros to call the *Impl
// functions directly with invalid arguments, since correct macro usage can
// never produce these states (they always supply a valid &ERROR_STATE,
// __FILE__, __func__, __LINE__).
static void test_errorThrowImplNullStateAsserts(void **state) {
expect_assert_failure(
errorThrowImpl(NULL, ERROR_NOT_OK, __FILE__, __func__, __LINE__, "msg")
);
}
static void test_errorThrowImplOkCodeAsserts(void **state) {
errorstate_t local = { 0 };
expect_assert_failure(
errorThrowImpl(&local, ERROR_OK, __FILE__, __func__, __LINE__, "msg")
);
}
static void test_errorThrowImplNullFileAsserts(void **state) {
errorstate_t local = { 0 };
expect_assert_failure(
errorThrowImpl(&local, ERROR_NOT_OK, NULL, __func__, __LINE__, "msg")
);
}
static void test_errorThrowImplNullFunctionAsserts(void **state) {
errorstate_t local = { 0 };
expect_assert_failure(
errorThrowImpl(&local, ERROR_NOT_OK, __FILE__, NULL, __LINE__, "msg")
);
}
static void test_errorThrowImplNegativeLineAsserts(void **state) {
errorstate_t local = { 0 };
expect_assert_failure(
errorThrowImpl(&local, ERROR_NOT_OK, __FILE__, __func__, -1, "msg")
);
}
static void test_errorThrowImplNullMessageAsserts(void **state) {
errorstate_t local = { 0 };
expect_assert_failure(
errorThrowImpl(&local, ERROR_NOT_OK, __FILE__, __func__, __LINE__, NULL)
);
}
static void test_errorOkAssertsIfPreviousErrorNotCaught(void **state) {
// Simulate a caller forgetting to errorCatch a previous throw: leave
// ERROR_STATE.code non-OK, then errorOk() must assert rather than silently
// returning an OK that papers over the uncaught error.
assert_int_equal(ERROR_STATE.code, ERROR_OK);
ERROR_STATE.code = ERROR_NOT_OK;
expect_assert_failure(helper_ok());
// Nothing was actually allocated for this fake corruption - just restore
// the global so later tests see a clean state.
ERROR_STATE.code = ERROR_OK;
}
static void test_errorChainImplNullStateAsserts(void **state) {
errorret_t bad = { .code = ERROR_NOT_OK, .state = NULL };
expect_assert_failure(
errorChainImpl(bad, __FILE__, __func__, __LINE__)
);
}
static void test_errorChainImplNullMessageAsserts(void **state) {
errorstate_t local = { .code = ERROR_NOT_OK, .message = NULL, .lines = NULL };
errorret_t bad = { .code = ERROR_NOT_OK, .state = &local };
expect_assert_failure(
errorChainImpl(bad, __FILE__, __func__, __LINE__)
);
}
static void test_errorCatchNullStateAsserts(void **state) {
errorret_t bad = { .code = ERROR_NOT_OK, .state = NULL };
expect_assert_failure(errorCatch(bad));
}
static void test_errorCatchNullMessageAsserts(void **state) {
errorstate_t local = { .code = ERROR_NOT_OK, .message = NULL, .lines = NULL };
errorret_t bad = { .code = ERROR_NOT_OK, .state = &local };
expect_assert_failure(errorCatch(bad));
}
static void test_errorPrintNullStateAsserts(void **state) {
errorret_t bad = { .code = ERROR_NOT_OK, .state = NULL };
expect_assert_failure(errorPrint(bad));
}
static void test_errorPrintNullMessageAsserts(void **state) {
errorstate_t local = { .code = ERROR_NOT_OK, .message = NULL, .lines = NULL };
errorret_t bad = { .code = ERROR_NOT_OK, .state = &local };
expect_assert_failure(errorPrint(bad));
}
static void test_errorCatch_ok(void **state) {
// Catching an ok ret should be a no-op
errorret_t ret = helper_ok();
@@ -178,6 +344,24 @@ int main(void) {
cmocka_unit_test(test_errorOk),
cmocka_unit_test(test_errorIsOk),
cmocka_unit_test(test_errorChain),
cmocka_unit_test(test_errorThrowNoFormatArguments),
cmocka_unit_test(test_errorThrowWithCode),
cmocka_unit_test(test_errorChainOkRetvalPassesThroughUnchanged),
cmocka_unit_test(test_errorPrintOkRetvalPassesThroughUnchanged),
cmocka_unit_test(test_errorPrintErrorRetvalPassesThroughUnchanged),
cmocka_unit_test(test_errorThrowImplNullStateAsserts),
cmocka_unit_test(test_errorThrowImplOkCodeAsserts),
cmocka_unit_test(test_errorThrowImplNullFileAsserts),
cmocka_unit_test(test_errorThrowImplNullFunctionAsserts),
cmocka_unit_test(test_errorThrowImplNegativeLineAsserts),
cmocka_unit_test(test_errorThrowImplNullMessageAsserts),
cmocka_unit_test(test_errorOkAssertsIfPreviousErrorNotCaught),
cmocka_unit_test(test_errorChainImplNullStateAsserts),
cmocka_unit_test(test_errorChainImplNullMessageAsserts),
cmocka_unit_test(test_errorCatchNullStateAsserts),
cmocka_unit_test(test_errorCatchNullMessageAsserts),
cmocka_unit_test(test_errorPrintNullStateAsserts),
cmocka_unit_test(test_errorPrintNullMessageAsserts),
cmocka_unit_test(test_errorCatch_ok),
cmocka_unit_test(test_errorCatch_error),
cmocka_unit_test(test_error_thread_isolation),
+9
View File
@@ -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_event.c)
+443
View File
@@ -0,0 +1,443 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "event/event.h"
typedef struct {
int32_t callCount;
void *lastParams;
void *lastUser;
} eventrecord_t;
// Distinct function pointers are required per-subscriber - the same callback
// pointer cannot be subscribed twice, even with a different user pointer.
static void helper_recordA(void *params, void *user) {
eventrecord_t *record = (eventrecord_t *)user;
record->callCount++;
record->lastParams = params;
record->lastUser = user;
}
static void helper_recordB(void *params, void *user) {
eventrecord_t *record = (eventrecord_t *)user;
record->callCount++;
record->lastParams = params;
record->lastUser = user;
}
static void helper_recordC(void *params, void *user) {
eventrecord_t *record = (eventrecord_t *)user;
record->callCount++;
record->lastParams = params;
record->lastUser = user;
}
// Records via `params` instead of `user` - for cases exercising a NULL user
// (e.g. no users array), where dereferencing `user` would crash.
static void helper_recordViaParams(void *params, void *user) {
eventrecord_t *record = (eventrecord_t *)params;
record->callCount++;
record->lastParams = params;
record->lastUser = user;
}
#define EVENT_CAPACITY 4
// --- eventInit ---
static void test_eventInitSetsUpBackingArrays(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
memset(callbacks, 0xAA, sizeof(callbacks));
memset(users, 0xAA, sizeof(users));
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
assert_ptr_equal(event.callbacks, callbacks);
assert_ptr_equal(event.users, users);
assert_int_equal(event.size, EVENT_CAPACITY);
assert_int_equal(event.count, 0);
for(int32_t i = 0; i < EVENT_CAPACITY; i++) {
assert_null(callbacks[i]);
assert_null(users[i]);
}
}
static void test_eventInitAllowsNullUsersArray(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
event_t event;
eventInit(&event, callbacks, NULL, EVENT_CAPACITY);
assert_null(event.users);
}
static void test_eventInitResetClearsSubscribersOnly(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
eventrecord_t record = { 0 };
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, &record);
assert_int_equal(event.count, 1);
// Re-init (reset) with the same backing arrays.
eventInit(&event, callbacks, users, EVENT_CAPACITY);
assert_ptr_equal(event.callbacks, callbacks);
assert_int_equal(event.count, 0);
assert_null(callbacks[0]);
assert_null(users[0]);
}
static void test_eventInitNullEventAsserts(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
expect_assert_failure(eventInit(NULL, callbacks, NULL, EVENT_CAPACITY));
}
static void test_eventInitNullCallbacksAsserts(void **state) {
event_t event;
expect_assert_failure(eventInit(&event, NULL, NULL, EVENT_CAPACITY));
}
static void test_eventInitZeroSizeAsserts(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
event_t event;
expect_assert_failure(eventInit(&event, callbacks, NULL, 0));
}
// --- eventSubscribe ---
static void test_eventSubscribeAddsCallback(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
eventrecord_t record = { 0 };
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, &record);
assert_int_equal(event.count, 1);
assert_ptr_equal(event.callbacks[0], helper_recordA);
assert_ptr_equal(event.users[0], &record);
}
static void test_eventSubscribeMultiple(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
eventrecord_t recordA = { 0 }, recordB = { 0 }, recordC = { 0 };
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, &recordA);
eventSubscribe(&event, helper_recordB, &recordB);
eventSubscribe(&event, helper_recordC, &recordC);
assert_int_equal(event.count, 3);
}
static void test_eventSubscribeNullUserLeavesSlotNull(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, NULL);
assert_null(event.users[0]);
}
static void test_eventSubscribeNullUserAfterUnsubscribeStaysNull(void **state) {
// The vacated slot must actually be cleared, not left holding a stale
// user pointer from whatever previously occupied it.
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
eventrecord_t recordA = { 0 }, recordB = { 0 };
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, &recordA);
eventSubscribe(&event, helper_recordB, &recordB);
eventUnsubscribe(&event, helper_recordA);
eventSubscribe(&event, helper_recordC, NULL);
for(int32_t i = 0; i < event.count; i++) {
if(event.callbacks[i] == helper_recordC) {
assert_null(event.users[i]);
return;
}
}
fail_msg("helper_recordC was not found after subscribing.");
}
static void test_eventSubscribeUserWithoutUsersArrayAsserts(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
eventrecord_t record = { 0 };
event_t event;
eventInit(&event, callbacks, NULL, EVENT_CAPACITY);
expect_assert_failure(eventSubscribe(&event, helper_recordA, &record));
}
static void test_eventSubscribeDuplicateCallbackAsserts(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
eventrecord_t record = { 0 };
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, &record);
expect_assert_failure(eventSubscribe(&event, helper_recordA, &record));
}
static void test_eventSubscribeSameCallbackDifferentUserStillAsserts(void **state) {
// Documented as "the same (callback, user) pair may only be subscribed
// once", implying a different user should be fine - but the actual
// uniqueness check only looks at the callback pointer, so this asserts
// too. Captures actual behavior; flag if the doc/implementation should
// instead match on the (callback, user) pair.
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
eventrecord_t recordA = { 0 }, recordB = { 0 };
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, &recordA);
expect_assert_failure(eventSubscribe(&event, helper_recordA, &recordB));
}
static void test_eventSubscribeCapacityExceededAsserts(void **state) {
eventcallback_t callbacks[2];
void *users[2];
eventrecord_t recordA = { 0 }, recordB = { 0 }, recordC = { 0 };
event_t event;
eventInit(&event, callbacks, users, 2);
eventSubscribe(&event, helper_recordA, &recordA);
eventSubscribe(&event, helper_recordB, &recordB);
expect_assert_failure(eventSubscribe(&event, helper_recordC, &recordC));
}
static void test_eventSubscribeNullEventAsserts(void **state) {
expect_assert_failure(eventSubscribe(NULL, helper_recordA, NULL));
}
static void test_eventSubscribeNullCallbackAsserts(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
event_t event;
eventInit(&event, callbacks, NULL, EVENT_CAPACITY);
expect_assert_failure(eventSubscribe(&event, NULL, NULL));
}
// --- eventUnsubscribe ---
static void test_eventUnsubscribeMiddleSwapsLastIntoPlace(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
eventrecord_t recordA = { 0 }, recordB = { 0 }, recordC = { 0 };
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, &recordA);
eventSubscribe(&event, helper_recordB, &recordB);
eventSubscribe(&event, helper_recordC, &recordC);
eventUnsubscribe(&event, helper_recordB);
assert_int_equal(event.count, 2);
assert_ptr_equal(event.callbacks[0], helper_recordA);
// recordC (previously last) was swapped into the vacated middle slot.
assert_ptr_equal(event.callbacks[1], helper_recordC);
assert_ptr_equal(event.users[1], &recordC);
// The old tail slot is fully cleared.
assert_null(event.callbacks[2]);
assert_null(event.users[2]);
}
static void test_eventUnsubscribeLastElement(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
eventrecord_t recordA = { 0 }, recordB = { 0 };
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, &recordA);
eventSubscribe(&event, helper_recordB, &recordB);
eventUnsubscribe(&event, helper_recordB);
assert_int_equal(event.count, 1);
assert_ptr_equal(event.callbacks[0], helper_recordA);
assert_null(event.callbacks[1]);
}
static void test_eventUnsubscribeNotSubscribedIsNoop(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
eventrecord_t recordA = { 0 };
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, &recordA);
eventUnsubscribe(&event, helper_recordB);
assert_int_equal(event.count, 1);
assert_ptr_equal(event.callbacks[0], helper_recordA);
}
static void test_eventUnsubscribeThenResubscribeSucceeds(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
eventrecord_t recordA = { 0 };
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, &recordA);
eventUnsubscribe(&event, helper_recordA);
// Would have asserted (duplicate) had the removal not actually happened.
eventSubscribe(&event, helper_recordA, &recordA);
assert_int_equal(event.count, 1);
}
static void test_eventUnsubscribeWithNullUsersArrayIsSafe(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
event_t event;
eventInit(&event, callbacks, NULL, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, NULL);
eventUnsubscribe(&event, helper_recordA);
assert_int_equal(event.count, 0);
}
static void test_eventUnsubscribeNullEventAsserts(void **state) {
expect_assert_failure(eventUnsubscribe(NULL, helper_recordA));
}
static void test_eventUnsubscribeNullCallbackAsserts(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
event_t event;
eventInit(&event, callbacks, NULL, EVENT_CAPACITY);
expect_assert_failure(eventUnsubscribe(&event, NULL));
}
// --- eventInvoke ---
static void test_eventInvokeCallsAllSubscribersWithParamsAndUser(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
eventrecord_t recordA = { 0 }, recordB = { 0 };
int32_t params = 123;
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, &recordA);
eventSubscribe(&event, helper_recordB, &recordB);
eventInvoke(&event, &params);
assert_int_equal(recordA.callCount, 1);
assert_ptr_equal(recordA.lastParams, &params);
assert_ptr_equal(recordA.lastUser, &recordA);
assert_int_equal(recordB.callCount, 1);
assert_ptr_equal(recordB.lastParams, &params);
assert_ptr_equal(recordB.lastUser, &recordB);
}
static void test_eventInvokeWithNoSubscribersDoesNothing(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
// No subscribers exist, so there is nothing to observe beyond "no crash".
eventInvoke(&event, NULL);
}
static void test_eventInvokeWithNullUsersArrayPassesNull(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
eventrecord_t record = { 0 };
event_t event;
eventInit(&event, callbacks, NULL, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordViaParams, NULL);
eventInvoke(&event, &record);
assert_int_equal(record.callCount, 1);
assert_ptr_equal(record.lastParams, &record);
assert_null(record.lastUser);
}
static void test_eventInvokeSkipsUnsubscribedCallback(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
eventrecord_t recordA = { 0 }, recordB = { 0 };
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, &recordA);
eventSubscribe(&event, helper_recordB, &recordB);
eventUnsubscribe(&event, helper_recordA);
eventInvoke(&event, NULL);
assert_int_equal(recordA.callCount, 0);
assert_int_equal(recordB.callCount, 1);
}
static void test_eventInvokeNullEventAsserts(void **state) {
expect_assert_failure(eventInvoke(NULL, NULL));
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_eventInitSetsUpBackingArrays),
cmocka_unit_test(test_eventInitAllowsNullUsersArray),
cmocka_unit_test(test_eventInitResetClearsSubscribersOnly),
cmocka_unit_test(test_eventInitNullEventAsserts),
cmocka_unit_test(test_eventInitNullCallbacksAsserts),
cmocka_unit_test(test_eventInitZeroSizeAsserts),
cmocka_unit_test(test_eventSubscribeAddsCallback),
cmocka_unit_test(test_eventSubscribeMultiple),
cmocka_unit_test(test_eventSubscribeNullUserLeavesSlotNull),
cmocka_unit_test(test_eventSubscribeNullUserAfterUnsubscribeStaysNull),
cmocka_unit_test(test_eventSubscribeUserWithoutUsersArrayAsserts),
cmocka_unit_test(test_eventSubscribeDuplicateCallbackAsserts),
cmocka_unit_test(test_eventSubscribeSameCallbackDifferentUserStillAsserts),
cmocka_unit_test(test_eventSubscribeCapacityExceededAsserts),
cmocka_unit_test(test_eventSubscribeNullEventAsserts),
cmocka_unit_test(test_eventSubscribeNullCallbackAsserts),
cmocka_unit_test(test_eventUnsubscribeMiddleSwapsLastIntoPlace),
cmocka_unit_test(test_eventUnsubscribeLastElement),
cmocka_unit_test(test_eventUnsubscribeNotSubscribedIsNoop),
cmocka_unit_test(test_eventUnsubscribeThenResubscribeSucceeds),
cmocka_unit_test(test_eventUnsubscribeWithNullUsersArrayIsSafe),
cmocka_unit_test(test_eventUnsubscribeNullEventAsserts),
cmocka_unit_test(test_eventUnsubscribeNullCallbackAsserts),
cmocka_unit_test(test_eventInvokeCallsAllSubscribersWithParamsAndUser),
cmocka_unit_test(test_eventInvokeWithNoSubscribersDoesNothing),
cmocka_unit_test(test_eventInvokeWithNullUsersArrayPassesNull),
cmocka_unit_test(test_eventInvokeSkipsUnsubscribedCallback),
cmocka_unit_test(test_eventInvokeNullEventAsserts),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}