From 08389a6d306570fc4575988e62a8397a5ff49bee Mon Sep 17 00:00:00 2001 From: Mosder Date: Fri, 31 Jul 2026 18:51:21 +0200 Subject: feat: created tester --- Makefile | 7 +- include/moslib/tester.h | 84 ++++++++++++++++++++- src/tester.c | 191 ++++++++++++++++++++++++++++++++++++++++++++++++ tests/main.c | 3 - tests/mem/alloc_tests.c | 37 ++++++++++ tests/mem/alloc_tests.h | 3 + tests/tests.c | 13 ++++ 7 files changed, 331 insertions(+), 7 deletions(-) delete mode 100644 tests/main.c create mode 100644 tests/mem/alloc_tests.c create mode 100644 tests/mem/alloc_tests.h create mode 100644 tests/tests.c diff --git a/Makefile b/Makefile index def9b73..194aa3b 100644 --- a/Makefile +++ b/Makefile @@ -26,9 +26,10 @@ structures: $(CC) $(CFLAGS) -c src/ds/array.c test: - $(CC) $(CFLAGS_TEST) tests/main.c -o run_tests - ./run_tests - rm run_tests + $(CC) $(CFLAGS_TEST) -c tests/mem/alloc_tests.c + $(CC) $(CFLAGS_TEST) *.o tests/tests.c -o run_tests + ./run_tests 2>/dev/null + rm run_tests *.o .PHONY: clean clean: diff --git a/include/moslib/tester.h b/include/moslib/tester.h index 0165257..9c3ea04 100644 --- a/include/moslib/tester.h +++ b/include/moslib/tester.h @@ -25,7 +25,6 @@ // Create tests. // Tests are of type TestFn (Test (*)()). // They should use test_assert functions to check correctness. -// // Example: // Test test_fn() { // int result1 = 1 + 2; @@ -35,6 +34,18 @@ // test_assert(result2 == 8, "3 + 5 should equal 8"); // } // +// You can also test if a function is supposed to call exit. +// You need to create a helper test and pass it into test_assert_exit in main test. +// Pass the helper test and expected exit code to it. +// Example: +// Test helper_test() { +// int param1, param2; +// int result = exit_failure_function(¶m1, ¶m2); +// } +// Test main_test() { +// test_assert_exit(helper_test, EXIT_FAILURE); +// } +// // ---------------------------------------------------------------------------------------------------- // // Add tests to test group: @@ -64,4 +75,75 @@ typedef struct Tester Tester; typedef struct TestGroup TestGroup; typedef Test (*TestFn)(); +// Initalize new tester +// You can skip this and just define it as NULL - it will initalize automatically +// +// Returns: +// pointer to the new tester +extern Tester *new_tester(); + +// Add new test group to the tester +// +// Arguments: +// tester +// tester to add the group to +// +// group_name +// name of the group to add +// +// Returns: +// pointer to the added group +#define add_test_group(tester, group_name) ((!(tester) ? (tester) = new_tester() : 0), add_test_group_fn((tester), (group_name))) + +// Assert that an expression is true +// Use in test functions to test correctness +// +// Arguments: +// expression +// expression to check - if it's true, test passes +// if it's false, test fails +// +// fail_message +// message to display when test fails +extern void test_assert(int expression, const char *fail_message); + +// Assert that function exits with given exit code +// +// Arguments: +// function +// function that is supposed to exit +// +// code +// expected exit code +#define test_assert_exit(function, code) test_assert_exit_fn((function), (code), #function); + +// Add test to a test group +// +// Arguments: +// group +// test group to add the test to +// +// test +// test to add to the group +#define add_test(group, test) add_test_fn((group), (test), #test); + +// Run defined tests +// +// Arguments: +// tester +// tester with tests to run +extern void run_tests(Tester *tester); + +// Free the tester +// +// Arguments: +// tester +// tester to free +extern void free_tester(Tester *tester); + +// Function prototypes for macros +extern TestGroup *add_test_group_fn(Tester *tester, const char *group_name); +extern void test_assert_exit_fn(TestFn function, int code, const char *name); +extern void add_test_fn(TestGroup *group, TestFn test, const char *name); + #endif // MOSLIB_TESTER_H diff --git a/src/tester.c b/src/tester.c index e69de29..361cfb9 100644 --- a/src/tester.c +++ b/src/tester.c @@ -0,0 +1,191 @@ +#include "moslib/tester.h" + +#include +#include +#include +#include +#include + +#include "moslib/mem/alloc.h" + +#define INIT_GROUP_CAP 8 +#define INIT_TEST_CAP 16 + +#define RED 91 +#define GREEN 92 +#define WHITE 97 + +#define RENDER_WIDTH 80 + +int failed = 0; + +#define print_color(template, ...) printf("\033[%dm" template "\033[0m", __VA_ARGS__) +#define nl printf("\n") + +void print_center(const char *text, int color, char padding_char) { + size_t post_char_count = (RENDER_WIDTH - strlen(text)) / 2; + size_t pre_char_count = RENDER_WIDTH - post_char_count - strlen(text); + + for (size_t i = 1; i < pre_char_count; i++) + printf("%c", padding_char); + + print_color(" %s ", color, text); + + for (size_t i = 1; i < post_char_count; i++) + printf("%c", padding_char); + + nl; +} + +typedef struct { + TestFn test; + const char *name; +} TestName; + +struct Tester { + TestGroup *groups; + size_t n_groups; + size_t group_cap; +}; + +struct TestGroup { + const char *name; + TestName *tests; + size_t n_tests; + size_t test_cap; +}; + +Tester *new_tester() { + Tester *tester = safe_malloc(sizeof(Tester)); + *tester = (Tester){ + .groups = safe_malloc(INIT_GROUP_CAP * sizeof(TestGroup)), + .n_groups = 0, + .group_cap = INIT_GROUP_CAP, + }; + return tester; +} + +TestGroup *add_test_group_fn(Tester *tester, const char *group_name) { + if (tester->n_groups >= tester->group_cap) { + tester->group_cap *= 2; + tester->groups = safe_realloc(tester->groups, tester->group_cap * sizeof(TestGroup)); + } + + TestGroup *group = tester->groups + tester->n_groups++; + // clang-format off + *group = (TestGroup){ + .name = group_name, + .tests = safe_malloc(INIT_TEST_CAP * sizeof(TestName)), + .n_tests = 0, + .test_cap = INIT_TEST_CAP + }; + // clang-format on + return group; +} + +void test_assert(int expression, const char *fail_message) { + if (!expression) { + failed = 1; + print_color("ASSERT FAILED: ", RED); + printf("%s\n", fail_message); + } +} + +jmp_buf jump_env; +int expected_code; +const char *fn_name; +void exit(int code) { + if (code != expected_code) { + failed = 1; + print_color("ASSERT FAILED: ", RED); + printf("exit code was %d, expected %d (in function %s)\n", code, expected_code, fn_name); + } + longjmp(jump_env, 1); +} + +void test_assert_exit_fn(TestFn function, int code, const char *name) { + expected_code = code; + fn_name = name; + if (setjmp(jump_env) == 0) { + function(); + failed = 1; + print_color("ASSERT FAILED: ", RED); + printf("exit was not called (in function %s)\n", name); + } +} + +void add_test_fn(TestGroup *group, TestFn test, const char *name) { + if (group->n_tests >= group->test_cap) { + group->test_cap *= 2; + group->tests = safe_realloc(group->tests, group->test_cap * sizeof(TestName)); + } + + group->tests[group->n_tests++] = (TestName){.test = test, .name = name}; +} + +void run_tests(Tester *tester) { + if (!tester) + return; + + size_t total_passed = 0; + size_t total_failed = 0; + char buf[80]; + + print_center("START", WHITE, '='); + + for (size_t i = 0; i < tester->n_groups; i++) { + nl; + TestGroup *group = tester->groups + i; + size_t tests_passed = 0; + size_t tests_failed = 0; + + print_center(group->name, WHITE, '-'); + + for (size_t j = 0; j < group->n_tests; j++) { + failed = 0; + printf("%s:\n", group->tests[j].name); + group->tests[j].test(); + if (failed) { + tests_failed += 1; + print_color("--- FAILED ---\n", RED); + } + else { + tests_passed += 1; + print_color("--- PASSED ---\n", GREEN); + } + } + + print_center("GROUP SUMMARY", WHITE, '-'); + + sprintf(buf, "%zu PASSED", tests_passed); + print_center(buf, GREEN, ' '); + sprintf(buf, "%zu FAILED", tests_failed); + print_center(buf, RED, ' '); + if (i < tester->n_groups - 1) { + nl; + print_center("TESTING", WHITE, '='); + } + + total_passed += tests_passed; + total_failed += tests_failed; + } + + nl; + print_center("TESTS SUMMARY", WHITE, '='); + sprintf(buf, "%zu PASSED", total_passed); + print_center(buf, GREEN, ' '); + sprintf(buf, "%zu FAILED", total_failed); + print_center(buf, RED, ' '); + print_center("END", WHITE, '='); +} + +void free_tester(Tester *tester) { + if (!tester) + return; + + for (size_t i = 0; i < tester->n_groups; i++) { + free(tester->groups[i].tests); + } + free(tester->groups); + free(tester); +} diff --git a/tests/main.c b/tests/main.c deleted file mode 100644 index 9b6bdc2..0000000 --- a/tests/main.c +++ /dev/null @@ -1,3 +0,0 @@ -int main(void) { - return 0; -} diff --git a/tests/mem/alloc_tests.c b/tests/mem/alloc_tests.c new file mode 100644 index 0000000..6eb2e9c --- /dev/null +++ b/tests/mem/alloc_tests.c @@ -0,0 +1,37 @@ +#include "alloc_tests.h" + +#include +#include +#include + +Test malloc_exit() { + safe_malloc(ULLONG_MAX); +} + +Test test_safe_malloc() { + void *p = safe_malloc(1024); + test_assert(p != NULL, "Should be able to malloc 1KiB"); + test_assert_exit(malloc_exit, EXIT_FAILURE); +} + +Test realloc_exit() { + void *p = NULL; + safe_realloc(p, ULLONG_MAX); +} + +Test test_safe_realloc() { + void *p = NULL; + p = safe_realloc(p, 1024); + test_assert(p != NULL, "Should be able to realloc NULL to 1KiB"); + + p = safe_realloc(p, 4096); + test_assert(p != NULL, "Should be able to realloc 1KiB to 4KiB"); + + test_assert_exit(realloc_exit, EXIT_FAILURE); +} + +void mem_alloc_tests(Tester *tester) { + TestGroup *group = add_test_group(tester, "moslib/mem/alloc.h"); + add_test(group, test_safe_malloc); + add_test(group, test_safe_realloc); +} diff --git a/tests/mem/alloc_tests.h b/tests/mem/alloc_tests.h new file mode 100644 index 0000000..aaf70a4 --- /dev/null +++ b/tests/mem/alloc_tests.h @@ -0,0 +1,3 @@ +#include + +extern void mem_alloc_tests(Tester *tester); diff --git a/tests/tests.c b/tests/tests.c new file mode 100644 index 0000000..63e84ca --- /dev/null +++ b/tests/tests.c @@ -0,0 +1,13 @@ +#include + +#include "mem/alloc_tests.h" + +int main(void) { + Tester *tester = new_tester(); + + mem_alloc_tests(tester); + + run_tests(tester); + free_tester(tester); + return 0; +} -- cgit v1.2.3