diff --git a/src/dusk/animation/animation.c b/src/dusk/animation/animation.c index 1660a4f0..65fa2b20 100644 --- a/src/dusk/animation/animation.c +++ b/src/dusk/animation/animation.c @@ -73,9 +73,13 @@ void animationUpdate( loop && pingpong, "Cannot set both ANIMATION_FLAG_LOOP and ANIMATION_FLAG_PINGPONG." ); - bool_t backward = pingpong - ? (anim->flags & ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD) != 0 - : (anim->flags & ANIMATION_FLAG_REVERSE) != 0; + + bool_t backward; + if(pingpong) { + backward = (anim->flags & ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD) != 0; + } else { + backward = (anim->flags & ANIMATION_FLAG_REVERSE) != 0; + } // Resolve boundary crossings one at a time, so a single large deltaTime // can correctly loop/pingpong across multiple boundaries in one call. @@ -92,9 +96,12 @@ void animationUpdate( remaining -= toBoundary; anim->time = backward ? 0.0f : anim->duration; - bool_t stopHere = backward - ? (anim->flags & ANIMATION_FLAG_STOP_BEGINNING) != 0 - : (anim->flags & ANIMATION_FLAG_STOP_END) != 0; + bool_t stopHere; + if(backward) { + stopHere = (anim->flags & ANIMATION_FLAG_STOP_BEGINNING) != 0; + } else { + stopHere = (anim->flags & ANIMATION_FLAG_STOP_END) != 0; + } if(stopHere) { justCompleted = true; diff --git a/src/dusk/console/console.c b/src/dusk/console/console.c index e0acabba..a600ac9f 100644 --- a/src/dusk/console/console.c +++ b/src/dusk/console/console.c @@ -18,10 +18,7 @@ console_t CONSOLE; void consoleInit(void) { memoryZero(&CONSOLE, sizeof(console_t)); CONSOLE.visible = false; - - #ifdef DUSK_CONSOLE_POSIX - threadMutexInit(&CONSOLE.printMutex); - #endif + threadMutexInit(&CONSOLE.printMutex); } void consolePrint(const char_t *message, ...) { @@ -32,20 +29,14 @@ void consolePrint(const char_t *message, ...) { int32_t len = stringFormatVA(buffer, CONSOLE_LINE_MAX, message, args); va_end(args); - #ifdef DUSK_CONSOLE_POSIX - threadMutexLock(&CONSOLE.printMutex); - #endif - + threadMutexLock(&CONSOLE.printMutex); memoryMove( CONSOLE.line[0], CONSOLE.line[1], (CONSOLE_HISTORY_MAX - 1) * CONSOLE_LINE_MAX ); memoryCopy(CONSOLE.line[CONSOLE_HISTORY_MAX - 1], buffer, len + 1); - - #ifdef DUSK_CONSOLE_POSIX - threadMutexUnlock(&CONSOLE.printMutex); - #endif + threadMutexUnlock(&CONSOLE.printMutex); logDebug("%s\n", buffer); } @@ -61,7 +52,5 @@ void consoleUpdate(void) { } void consoleDispose(void) { - #ifdef DUSK_CONSOLE_POSIX - threadMutexDispose(&CONSOLE.printMutex); - #endif + threadMutexDispose(&CONSOLE.printMutex); } \ No newline at end of file diff --git a/src/dusk/console/console.h b/src/dusk/console/console.h index 7ac6378b..d1f90a7d 100644 --- a/src/dusk/console/console.h +++ b/src/dusk/console/console.h @@ -6,24 +6,18 @@ */ #pragma once -#include "consoledefs.h" #include "error/error.h" #include "dusk.h" +#include "thread/thread.h" -#ifdef DUSK_CONSOLE_POSIX - #include "thread/thread.h" - #include - #include - #define CONSOLE_POSIX_POLL_RATE 75 -#endif +#define CONSOLE_LINE_MAX 512 +#define CONSOLE_HISTORY_MAX 16 +#define CONSOLE_EXEC_BUFFER_MAX 32 typedef struct { char_t line[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX]; bool_t visible; - - #ifdef DUSK_CONSOLE_POSIX - threadmutex_t printMutex; - #endif + threadmutex_t printMutex; } console_t; extern console_t CONSOLE; diff --git a/src/dusk/console/consoledefs.h b/src/dusk/console/consoledefs.h deleted file mode 100644 index 392dddcb..00000000 --- a/src/dusk/console/consoledefs.h +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once - -#define CONSOLE_LINE_MAX 512 -#define CONSOLE_HISTORY_MAX 16 -#define CONSOLE_EXEC_BUFFER_MAX 32 diff --git a/src/dusk/display/display.c b/src/dusk/display/display.c index 4e514306..98596d8e 100644 --- a/src/dusk/display/display.c +++ b/src/dusk/display/display.c @@ -33,13 +33,17 @@ errorret_t displayInit(void) { #ifdef displayPlatformInit errorChain(displayPlatformInit()); #endif + + // Set initial state errorChain(displaySetState((displaystate_t){ .flags = 0 })); + + // Init the fixed textures errorChain(textureInit( - &TEXTURE_WHITE, 4, 4, + &TEXTURE_WHITE, TEXTURE_FIXED_WIDTH, TEXTURE_FIXED_HEIGHT, TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_WHITE_PIXELS } )); errorChain(textureInit( - &TEXTURE_TEST, 4, 4, + &TEXTURE_TEST, TEXTURE_FIXED_WIDTH, TEXTURE_FIXED_HEIGHT, TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_TEST_PIXELS } )); @@ -50,14 +54,14 @@ errorret_t displayInit(void) { errorChain(planeInit()); errorChain(capsuleInit()); errorChain(triPrismInit()); - + + // Init the subsystems errorChain(frameBufferInitBackBuffer()); errorChain(spriteBatchInit()); errorChain(textInit()); errorChain(screenInit()); // Setup initial shader with default values - errorChain(shaderListInit()); errorOk(); diff --git a/src/dusk/display/texture/texture.c b/src/dusk/display/texture/texture.c index 5d911b42..2cfdff28 100644 --- a/src/dusk/display/texture/texture.c +++ b/src/dusk/display/texture/texture.c @@ -12,7 +12,7 @@ #include "display/display.h" texture_t TEXTURE_WHITE; -color_t TEXTURE_WHITE_PIXELS[4*4] = { +color_t TEXTURE_WHITE_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT] = { COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, @@ -20,7 +20,7 @@ color_t TEXTURE_WHITE_PIXELS[4*4] = { }; texture_t TEXTURE_TEST; -color_t TEXTURE_TEST_PIXELS[4*4] = { +color_t TEXTURE_TEST_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT] = { COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA, diff --git a/src/dusk/display/texture/texture.h b/src/dusk/display/texture/texture.h index c94f80f0..65df2d59 100644 --- a/src/dusk/display/texture/texture.h +++ b/src/dusk/display/texture/texture.h @@ -17,6 +17,9 @@ #error "textureDisposePlatform should not be defined." #endif +#define TEXTURE_FIXED_WIDTH 4 +#define TEXTURE_FIXED_HEIGHT 4 + typedef textureformatplatform_t textureformat_t; typedef textureplatform_t texture_t; @@ -29,9 +32,9 @@ typedef union texturedata_u { } texturedata_t; extern texture_t TEXTURE_WHITE; -extern color_t TEXTURE_WHITE_PIXELS[4*4]; +extern color_t TEXTURE_WHITE_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT]; extern texture_t TEXTURE_TEST; -extern color_t TEXTURE_TEST_PIXELS[4*4]; +extern color_t TEXTURE_TEST_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT]; /** * Initializes a texture. diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d5816717..309ad5e6 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -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) diff --git a/test/animation/CMakeLists.txt b/test/animation/CMakeLists.txt index 87e9eed1..aabb6fdf 100644 --- a/test/animation/CMakeLists.txt +++ b/test/animation/CMakeLists.txt @@ -6,5 +6,6 @@ include(dusktest) # Tests +dusktest(test_easing.c) dusktest(test_keyframe.c) dusktest(test_animation.c) diff --git a/test/animation/test_easing.c b/test/animation/test_easing.c new file mode 100644 index 00000000..8ed8c5c2 --- /dev/null +++ b/test/animation/test_easing.c @@ -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); +} diff --git a/test/console/CMakeLists.txt b/test/console/CMakeLists.txt new file mode 100644 index 00000000..a0a98903 --- /dev/null +++ b/test/console/CMakeLists.txt @@ -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) diff --git a/test/console/test_console.c b/test/console/test_console.c new file mode 100644 index 00000000..6d90f081 --- /dev/null +++ b/test/console/test_console.c @@ -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-" 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-". + 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); +} diff --git a/test/error/test_error.c b/test/error/test_error.c index aa7e7090..04ffa96f 100644 --- a/test/error/test_error.c +++ b/test/error/test_error.c @@ -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), diff --git a/test/event/CMakeLists.txt b/test/event/CMakeLists.txt new file mode 100644 index 00000000..8614a292 --- /dev/null +++ b/test/event/CMakeLists.txt @@ -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) diff --git a/test/event/test_event.c b/test/event/test_event.c new file mode 100644 index 00000000..350daeb7 --- /dev/null +++ b/test/event/test_event.c @@ -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, ¶ms); + + assert_int_equal(recordA.callCount, 1); + assert_ptr_equal(recordA.lastParams, ¶ms); + assert_ptr_equal(recordA.lastUser, &recordA); + + assert_int_equal(recordB.callCount, 1); + assert_ptr_equal(recordB.lastParams, ¶ms); + 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); +}