/** * Copyright (c) 2026 Dominic Masters * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ #include "dusktest.h" #include "error/error.h" #include "thread/thread.h" #include "util/memory.h" // Helper that throws an error. static errorret_t helper_throw(void) { errorThrow("Test error %d", 42); } // Helper that returns ok. static errorret_t helper_ok(void) { errorOk(); } // Helper that chains to helper_throw. static errorret_t helper_chain(void) { errorChain(helper_throw()); 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(); assert_int_not_equal(ret.code, ERROR_OK); assert_non_null(ret.state); assert_non_null(ret.state->message); assert_non_null(ret.state->lines); // Message should contain our format argument assert_non_null(strstr(ret.state->message, "42")); errorCatch(ret); assert_int_equal(memoryGetAllocatedCount(), 0); } static void test_errorOk(void **state) { errorret_t ret = helper_ok(); assert_int_equal(ret.code, ERROR_OK); assert_null(ret.state); assert_int_equal(memoryGetAllocatedCount(), 0); } static void test_errorIsOk(void **state) { errorret_t ok = helper_ok(); assert_true(errorIsOk(ok)); assert_false(errorIsNotOk(ok)); errorret_t err = helper_throw(); assert_false(errorIsOk(err)); assert_true(errorIsNotOk(err)); errorCatch(err); assert_int_equal(memoryGetAllocatedCount(), 0); } static void test_errorChain(void **state) { errorret_t ret = helper_chain(); // Error propagated up assert_int_not_equal(ret.code, ERROR_OK); assert_non_null(ret.state); assert_non_null(ret.state->lines); // Lines should contain at least two stack entries int32_t count = 0; const char_t *p = ret.state->lines; while((p = strstr(p, " at ")) != NULL) { count++; p++; } assert_true(count >= 2); errorCatch(ret); 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(); errorCatch(ret); assert_int_equal(memoryGetAllocatedCount(), 0); } static void test_errorCatch_error(void **state) { errorret_t ret = helper_throw(); assert_int_not_equal(ret.code, ERROR_OK); errorCatch(ret); // After catch the global state should be cleared assert_int_equal(ERROR_STATE.code, ERROR_OK); assert_int_equal(memoryGetAllocatedCount(), 0); } // --- Threaded tests --- typedef struct { errorcode_t capturedCode; bool_t messageHas42; } thread_test_data_t; static void helper_thread_throw(thread_t *thread) { errorret_t ret = helper_throw(); thread_test_data_t *data = (thread_test_data_t *)thread->data; data->capturedCode = ERROR_STATE.code; data->messageHas42 = (strstr(ret.state->message, "42") != NULL); errorCatch(ret); } static void test_error_thread_isolation(void **state) { // Main thread state is clean before the test. assert_int_equal(ERROR_STATE.code, ERROR_OK); thread_test_data_t data = { .capturedCode = ERROR_OK, .messageHas42 = false }; thread_t thread; threadInit(&thread, helper_thread_throw); thread.data = &data; threadStart(&thread); threadStop(&thread); // Worker saw ERROR_NOT_OK in its own ERROR_STATE. assert_int_equal(data.capturedCode, ERROR_NOT_OK); assert_true(data.messageHas42); // Main thread ERROR_STATE was not touched by the worker. assert_int_equal(ERROR_STATE.code, ERROR_OK); assert_int_equal(memoryGetAllocatedCount(), 0); } #define CONCURRENT_THREAD_COUNT 4 static thread_test_data_t concurrent_data[CONCURRENT_THREAD_COUNT]; static thread_t concurrent_threads[CONCURRENT_THREAD_COUNT]; static void test_error_concurrent_throw(void **state) { for(int32_t i = 0; i < CONCURRENT_THREAD_COUNT; i++) { concurrent_data[i].capturedCode = ERROR_OK; concurrent_data[i].messageHas42 = false; threadInit(&concurrent_threads[i], helper_thread_throw); concurrent_threads[i].data = &concurrent_data[i]; } for(int32_t i = 0; i < CONCURRENT_THREAD_COUNT; i++) { threadStart(&concurrent_threads[i]); } for(int32_t i = 0; i < CONCURRENT_THREAD_COUNT; i++) { threadStop(&concurrent_threads[i]); } // Every worker must have seen its own independent error. for(int32_t i = 0; i < CONCURRENT_THREAD_COUNT; i++) { assert_int_equal(concurrent_data[i].capturedCode, ERROR_NOT_OK); assert_true(concurrent_data[i].messageHas42); } // Main thread is still clean. assert_int_equal(ERROR_STATE.code, ERROR_OK); assert_int_equal(memoryGetAllocatedCount(), 0); } int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_errorThrow), 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), cmocka_unit_test(test_error_concurrent_throw), }; return cmocka_run_group_tests(tests, NULL, NULL); }