diff --git a/subprojects/test.h/README.md b/subprojects/test.h/README.md new file mode 100644 index 00000000..e6157fc1 --- /dev/null +++ b/subprojects/test.h/README.md @@ -0,0 +1,37 @@ +test.h +====== + +A very simple, light weight, header only C unit test framework. + +# Features + +* Easy to use, no dependencies, no setup needed. +* Keep test cases close to the code they test. +* Automatic registration of the test cases. + +# Usage +## Setup + +Just include the header + +```c +#include "test.h" +``` + +## Defining test cases + +```c +TEST_CASE(test_case_name) { + // Your code here + // ... + TEST_EQUAL(1, 0); // Fail +} +``` + +## Run the test cases + +Build your program with `-DUNIT_TEST`, then run your program with `./program --unittest`. + +# Hooks + +If you define a function `test_h_unittest_setup`, it will be called before any test cases are run. diff --git a/subprojects/test.h/examples/trivial_tests/Makefile b/subprojects/test.h/examples/trivial_tests/Makefile new file mode 100644 index 00000000..17cf3eab --- /dev/null +++ b/subprojects/test.h/examples/trivial_tests/Makefile @@ -0,0 +1,8 @@ +all: normal unittest + +# A normal binary, compiled without unit tests +normal: test_test1.c test_test2.c ../../test.h + gcc -O test_test1.c test_test2.c -I../../ -o $@ +# A binary to run unit tests +unittest: test_test1.c test_test2.c ../../test.h + gcc -g test_test1.c test_test2.c -DUNIT_TEST -I../../ -o $@ diff --git a/subprojects/test.h/examples/trivial_tests/test_test1.c b/subprojects/test.h/examples/trivial_tests/test_test1.c new file mode 100644 index 00000000..16d2b6fb --- /dev/null +++ b/subprojects/test.h/examples/trivial_tests/test_test1.c @@ -0,0 +1,15 @@ +#include +#include +#include "test.h" + +int main(int argc, char *const *argv) { + printf("Hello World!\n"); +} + +TEST_CASE(test1) { + TEST_EQUAL(1, 0); +} + +TEST_CASE(test2) { + TEST_TRUE(false); +} diff --git a/subprojects/test.h/examples/trivial_tests/test_test2.c b/subprojects/test.h/examples/trivial_tests/test_test2.c new file mode 100644 index 00000000..eb9ba136 --- /dev/null +++ b/subprojects/test.h/examples/trivial_tests/test_test2.c @@ -0,0 +1,5 @@ +#include +#include "test.h" + +TEST_CASE(test2) { +} diff --git a/subprojects/test.h/meson.build b/subprojects/test.h/meson.build new file mode 100644 index 00000000..042e5d5d --- /dev/null +++ b/subprojects/test.h/meson.build @@ -0,0 +1,2 @@ +project('test.h', 'c') +test_h_dep = declare_dependency(include_directories: include_directories('.')) diff --git a/subprojects/test.h/test.h b/subprojects/test.h/test.h new file mode 100644 index 00000000..b784a2d4 --- /dev/null +++ b/subprojects/test.h/test.h @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: MIT +#pragma once + +#ifdef UNIT_TEST + +#include +#include +#include +#include + +struct test_file_metadata; + +struct test_failure { + bool present; + const char *message; + const char *file; + int line; +}; + +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) \ + metadata->failure = (struct test_failure) { \ + .message = _message, .file = __FILE__, .line = __LINE__, .present = true, \ + } + +#define TEST_EQUAL(a, b) \ + do { \ + if ((a) != (b)) { \ + SET_FAILURE(#a " != " #b); \ + return; \ + } \ + } while (0) + +#define TEST_TRUE(a) \ + do { \ + if (!(a)) { \ + SET_FAILURE(#a " is not true"); \ + return; \ + } \ + } while (0) + +#define TEST_STREQUAL(a, b) \ + do { \ + if (strcmp(a, b) != 0) { \ + SET_FAILURE(#a " != " #b); \ + 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; \ + static struct test_case_metadata __test_h_meta_##_name = { \ + .name = #_name, \ + .fn = __test_h_##_name, \ + }; \ + static void __attribute__((constructor(101))) __test_h_##_name##_register(void) { \ + __test_h_meta_##_name.next = __test_h_file.tests; \ + __test_h_file.tests = &__test_h_meta_##_name; \ + if (!__test_h_file.registered) { \ + __test_h_file.name = __FILE__; \ + __test_h_file.next = test_file_head; \ + test_file_head = &__test_h_file; \ + __test_h_file.registered = true; \ + } \ + } \ + static void __test_h_##_name(struct test_case_metadata *metadata, \ + struct test_file_metadata *file_metadata) + +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; + FILE *cmdlinef = fopen("/proc/self/cmdline", "r"); + char *arg = NULL; + int arglen; + fscanf(cmdlinef, "%ms%n", &arg, &arglen); + fclose(cmdlinef); + 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); + 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_TRUE(a) (void)(a) +#define TEST_STREQUAL(a, b) \ + (void)(a); \ + (void)(b) + +#endif