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