From 07fb91b9571fc0add797cbcd0adcc8711401a2be Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Sat, 27 Dec 2025 12:00:21 -0800 Subject: Enough stuff to draw a cube --- test/test.c | 29 ++++--- test/test.h | 248 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 265 insertions(+), 12 deletions(-) create mode 100644 test/test.h (limited to 'test') diff --git a/test/test.c b/test/test.c index 668dec8..be3be93 100644 --- a/test/test.c +++ b/test/test.c @@ -1,3 +1,5 @@ +#include "test.h" + #include #include @@ -24,7 +26,7 @@ static bool WritePPM(int width, int height, const RGB* image, const char* path) return true; } -void ToRGB(int width, int height, const sgPixel* rgba, RGB* rgb) { +static void ToRGB(int width, int height, const sgPixel* rgba, RGB* rgb) { assert(rgba); assert(rgb); for (int y = 0; y < height; ++y) { @@ -71,18 +73,21 @@ static void TestTriangle(swgfx* gfx) { ToRGB(BufferWidth, BufferHeight, colour, rgb); WritePPM(BufferWidth, BufferHeight, rgb, "triangle.ppm"); + // TODO: Assert the contents. Turn this file into a unit test executable. + // Write a helper function that first writes the image to a file and then + // asserts its contents. } -int main() { - swgfx* gfx = 0; - if (!(gfx = sgNew())) { - fprintf(stderr, "Failed to create swgfx\n"); - return 1; +#define GFX_TEST(NAME, FUNC) \ + TEST_CASE(NAME) {\ + swgfx* gfx = nullptr;\ + if (!(gfx = sgNew())) {\ + SET_FAILURE("Failed to create swgfx\n", false);\ + }\ + FUNC(gfx);\ + sgDel(&gfx);\ } - - TestTriangle(gfx); - - sgDel(&gfx); - return 0; -} +GFX_TEST(triangle, TestTriangle) + +int main() { return 0; } diff --git a/test/test.h b/test/test.h new file mode 100644 index 0000000..cdd2f05 --- /dev/null +++ b/test/test.h @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: MIT +#pragma once + +#ifdef UNIT_TEST + +#include +#include +#include +#include + +#if defined(__DragonFly__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || \ + defined(__NetBSD__) || defined(__OpenBSD__) +#define USE_SYSCTL_FOR_ARGS 1 +// clang-format off +#include +#include +// clang-format on +#include // getpid +#endif + +struct test_file_metadata; + +struct test_failure { + bool present; + const char *message; + const char *file; + int line; + bool owned; +}; + +struct test_case_metadata { + void (*fn)(struct test_case_metadata *, struct test_file_metadata *); + struct test_failure failure; + const char *name; + struct test_case_metadata *next; +}; + +struct test_file_metadata { + bool registered; + const char *name; + struct test_file_metadata *next; + struct test_case_metadata *tests; +}; + +struct test_file_metadata __attribute__((weak)) * test_file_head; + +#define SET_FAILURE(_message, _owned) \ + metadata->failure = (struct test_failure) { \ + .present = true, \ + .message = _message, \ + .file = __FILE__, \ + .line = __LINE__, \ + .owned = _owned, \ + } + +#define TEST_EQUAL(a, b) \ + do { \ + if ((a) != (b)) { \ + SET_FAILURE(#a " != " #b, false); \ + return; \ + } \ + } while (0) + +#define TEST_NOTEQUAL(a, b) \ + do { \ + if ((a) == (b)) { \ + SET_FAILURE(#a " == " #b, false); \ + return; \ + } \ + } while (0) + +#define TEST_TRUE(a) \ + do { \ + if (!(a)) { \ + SET_FAILURE(#a " is not true", false); \ + return; \ + } \ + } while (0) + +#define TEST_STREQUAL(a, b) \ + do { \ + if (strcmp(a, b) != 0) { \ + const char *test_strequal__part2 = " != " #b; \ + size_t test_strequal__len = \ + strlen(a) + strlen(test_strequal__part2) + 3; \ + char *test_strequal__buf = malloc(test_strequal__len); \ + snprintf(test_strequal__buf, test_strequal__len, "\"%s\"%s", a, \ + test_strequal__part2); \ + SET_FAILURE(test_strequal__buf, true); \ + return; \ + } \ + } while (0) + +#define TEST_STRNEQUAL(a, b, len) \ + do { \ + if (strncmp(a, b, len) != 0) { \ + const char *test_strnequal__part2 = " != " #b; \ + size_t test_strnequal__len2 = \ + len + strlen(test_strnequal__part2) + 3; \ + char *test_strnequal__buf = malloc(test_strnequal__len2); \ + snprintf(test_strnequal__buf, test_strnequal__len2, \ + "\"%.*s\"%s", (int)len, a, test_strnequal__part2); \ + SET_FAILURE(test_strnequal__buf, true); \ + return; \ + } \ + } while (0) + +#define TEST_STREQUAL3(str, expected, len) \ + do { \ + if (len != strlen(expected) || strncmp(str, expected, len) != 0) { \ + const char *test_strequal3__part2 = " != " #expected; \ + size_t test_strequal3__len2 = \ + len + strlen(test_strequal3__part2) + 3; \ + char *test_strequal3__buf = malloc(test_strequal3__len2); \ + snprintf(test_strequal3__buf, test_strequal3__len2, \ + "\"%.*s\"%s", (int)len, str, test_strequal3__part2); \ + SET_FAILURE(test_strequal3__buf, true); \ + return; \ + } \ + } while (0) + +#define TEST_CASE(_name) \ + static void __test_h_##_name(struct test_case_metadata *, \ + struct test_file_metadata *); \ + static struct test_file_metadata __test_h_file##_name; \ + static struct test_case_metadata __test_h_meta_##_name = { \ + .fn = __test_h_##_name, \ + .failure = {}, \ + .name = #_name, \ + .next = 0, \ + }; \ + static void __attribute__((constructor(101))) __test_h_##_name##_register(void) { \ + __test_h_meta_##_name.next = __test_h_file##_name.tests; \ + __test_h_file##_name.tests = &__test_h_meta_##_name; \ + if (!__test_h_file##_name.registered) { \ + __test_h_file##_name.name = __FILE__; \ + __test_h_file##_name.next = test_file_head; \ + test_file_head = &__test_h_file##_name; \ + __test_h_file##_name.registered = true; \ + } \ + } \ + static void __test_h_##_name( \ + struct test_case_metadata *metadata __attribute__((unused)), \ + struct test_file_metadata *file_metadata __attribute__((unused))) + +extern void __attribute__((weak)) (*test_h_unittest_setup)(void); +/// Run defined tests, return true if all tests succeeds +/// @param[out] tests_run if not NULL, set to whether tests were run +static inline void __attribute__((constructor(102))) run_tests(void) { + bool should_run = false; +#ifdef USE_SYSCTL_FOR_ARGS + int mib[] = { + CTL_KERN, +#if defined(__NetBSD__) || defined(__OpenBSD__) + KERN_PROC_ARGS, + getpid(), + KERN_PROC_ARGV, +#else + KERN_PROC, + KERN_PROC_ARGS, + getpid(), +#endif + }; + char *arg = NULL; + size_t arglen; + sysctl(mib, sizeof(mib) / sizeof(mib[0]), NULL, &arglen, NULL, 0); + arg = malloc(arglen); + sysctl(mib, sizeof(mib) / sizeof(mib[0]), arg, &arglen, NULL, 0); +#else + FILE *cmdlinef = fopen("/proc/self/cmdline", "r"); + char *arg = NULL; + int arglen; + fscanf(cmdlinef, "%ms%n", &arg, &arglen); + fclose(cmdlinef); +#endif + for (char *pos = arg; pos < arg + arglen; pos += strlen(pos) + 1) { + if (strcmp(pos, "--unittest") == 0) { + should_run = true; + break; + } + } + free(arg); + + if (!should_run) { + return; + } + + if (&test_h_unittest_setup) { + test_h_unittest_setup(); + } + + struct test_file_metadata *i = test_file_head; + int failed = 0, success = 0; + while (i) { + fprintf(stderr, "Running tests from %s:\n", i->name); + struct test_case_metadata *j = i->tests; + while (j) { + fprintf(stderr, "\t%s ... ", j->name); + j->failure.present = false; + j->fn(j, i); + if (j->failure.present) { + fprintf(stderr, "failed (%s at %s:%d)\n", j->failure.message, + j->failure.file, j->failure.line); + if (j->failure.owned) { + free((char *)j->failure.message); + j->failure.message = NULL; + } + failed++; + } else { + fprintf(stderr, "passed\n"); + success++; + } + j = j->next; + } + fprintf(stderr, "\n"); + i = i->next; + } + int total = failed + success; + fprintf(stderr, "Test results: passed %d/%d, failed %d/%d\n", success, total, + failed, total); + exit(failed == 0 ? EXIT_SUCCESS : EXIT_FAILURE); +} + +#else + +#include + +#define TEST_CASE(name) static void __attribute__((unused)) __test_h_##name(void) + +#define TEST_EQUAL(a, b) \ + (void)(a); \ + (void)(b) +#define TEST_NOTEQUAL(a, b) \ + (void)(a); \ + (void)(b) +#define TEST_TRUE(a) (void)(a) +#define TEST_STREQUAL(a, b) \ + (void)(a); \ + (void)(b) +#define TEST_STRNEQUAL(a, b, len) \ + (void)(a); \ + (void)(b); \ + (void)(len) +#define TEST_STREQUAL3(str, expected, len) \ + (void)(str); \ + (void)(expected); \ + (void)(len) +#endif -- cgit v1.2.3