372 lines
12 KiB
C
372 lines
12 KiB
C
/**
|
|
* 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);
|
|
}
|