From 2e3d66d728bf622b5e54381d9a9f1ba36301417e Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Tue, 12 Mar 2019 23:27:00 +0000 Subject: [PATCH 01/13] Initial commit --- README.md | 47 ++++++++++++++++++++++ examples/trivial_tests/Makefile | 8 ++++ examples/trivial_tests/test_test1.c | 13 +++++++ examples/trivial_tests/test_test2.c | 6 +++ test.h | 60 +++++++++++++++++++++++++++++ 5 files changed, 134 insertions(+) create mode 100644 README.md create mode 100644 examples/trivial_tests/Makefile create mode 100644 examples/trivial_tests/test_test1.c create mode 100644 examples/trivial_tests/test_test2.c create mode 100644 test.h diff --git a/README.md b/README.md new file mode 100644 index 00000000..94ecf546 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +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. + +## Setup + +Just include the header + +```c +#include "test.h" +``` + +To enable the test cases in your code, compile your program with `-DUNIT_TEST`. + +## Defining test cases + +```c +TEST_CASE(test_case_name) { + // Your code here + // ... + SHOULD_EQUAL(1, 0); // Fail +} +``` + +## Run the test cases + +In your main function, call `run_tests`: + +```c +int main() { + // necessary setup code + // ... +#ifdef UNIT_TEST + run_tests(); + // cleanup +#endif + // usual code +} + +``` diff --git a/examples/trivial_tests/Makefile b/examples/trivial_tests/Makefile new file mode 100644 index 00000000..e3f1b6ba --- /dev/null +++ b/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 test_test1.c test_test2.c -I../../ -o $@ +# A binary to run unit tests +unittest: test_test1.c test_test2.c ../../test.h + gcc test_test1.c test_test2.c -DUNIT_TEST -I../../ -o $@ diff --git a/examples/trivial_tests/test_test1.c b/examples/trivial_tests/test_test1.c new file mode 100644 index 00000000..38fccf4c --- /dev/null +++ b/examples/trivial_tests/test_test1.c @@ -0,0 +1,13 @@ +#include +#include "test.h" + +int main() { +#ifdef UNIT_TEST + run_tests(); +#endif +} + +TEST_CASE(test1) { + printf("test1\n"); + SHOULD_EQUAL(1, 0); +} diff --git a/examples/trivial_tests/test_test2.c b/examples/trivial_tests/test_test2.c new file mode 100644 index 00000000..a73de465 --- /dev/null +++ b/examples/trivial_tests/test_test2.c @@ -0,0 +1,6 @@ +#include +#include "test.h" + +TEST_CASE(test2) { + printf("test2\n"); +} diff --git a/test.h b/test.h new file mode 100644 index 00000000..bfec4ba7 --- /dev/null +++ b/test.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +#pragma once + +#ifdef UNIT_TEST + +#include +#include +#include + +struct test_case_metadata { + void (*fn)(struct test_case_metadata *); + bool success; + struct test_case_metadata *next; +}; + +struct test_case_metadata __attribute__((weak)) *test_case_head; + +static inline void report_failure(const char *message, const char *file, int line) { + fprintf(stderr, "Test failed, %s at %s:%d\n", message, file, line); +} + +#define SHOULD_EQUAL(a, b) do { \ + if ((a) != (b)) { \ + report_failure(#a " != " #b, __FILE__, __LINE__); \ + metadata->success = false; \ + return; \ + } \ +} while(0) + +#define TEST_CASE(name) \ +static void __test_h_##name(struct test_case_metadata *); \ +static void __attribute__((constructor)) __test_h_##name##_register(void) { \ + struct test_case_metadata *t = malloc(sizeof(*t)); \ + t->fn = __test_h_##name; \ + t->next = test_case_head; \ + test_case_head = t; \ +} \ +static void __test_h_##name(struct test_case_metadata *metadata) + +static inline void run_tests(void) { + struct test_case_metadata *i = test_case_head; + int failed = 0; + while(i) { + i->success = true; + i->fn(i); + if (!i->success) { + failed++; + } + i = i->next; + } +} + +#else + +#define TEST_CASE(name) \ +static void __attribute__((unused)) __test_h_##name(void) + +#define SHOULD_EQUAL(a, b) + +#endif From d0afc3eeb3708b71a9712d7b3a812d9176933d3c Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Wed, 13 Mar 2019 21:05:12 +0000 Subject: [PATCH 02/13] Print more information for test cases --- examples/trivial_tests/Makefile | 4 +- examples/trivial_tests/test_test1.c | 8 +- examples/trivial_tests/test_test2.c | 1 - test.h | 122 ++++++++++++++++++++-------- 4 files changed, 96 insertions(+), 39 deletions(-) diff --git a/examples/trivial_tests/Makefile b/examples/trivial_tests/Makefile index e3f1b6ba..9320a846 100644 --- a/examples/trivial_tests/Makefile +++ b/examples/trivial_tests/Makefile @@ -2,7 +2,7 @@ all: normal unittest # A normal binary, compiled without unit tests normal: test_test1.c test_test2.c ../../test.h - gcc test_test1.c test_test2.c -I../../ -o $@ + gcc -g test_test1.c test_test2.c -I../../ -o $@ # A binary to run unit tests unittest: test_test1.c test_test2.c ../../test.h - gcc test_test1.c test_test2.c -DUNIT_TEST -I../../ -o $@ + gcc -g test_test1.c test_test2.c -DUNIT_TEST -I../../ -o $@ diff --git a/examples/trivial_tests/test_test1.c b/examples/trivial_tests/test_test1.c index 38fccf4c..8454e9d4 100644 --- a/examples/trivial_tests/test_test1.c +++ b/examples/trivial_tests/test_test1.c @@ -1,4 +1,5 @@ #include +#include #include "test.h" int main() { @@ -8,6 +9,9 @@ int main() { } TEST_CASE(test1) { - printf("test1\n"); - SHOULD_EQUAL(1, 0); + TEST_EQUAL(1, 0); +} + +TEST_CASE(test2) { + TEST_TRUE(false); } diff --git a/examples/trivial_tests/test_test2.c b/examples/trivial_tests/test_test2.c index a73de465..eb9ba136 100644 --- a/examples/trivial_tests/test_test2.c +++ b/examples/trivial_tests/test_test2.c @@ -2,5 +2,4 @@ #include "test.h" TEST_CASE(test2) { - printf("test2\n"); } diff --git a/test.h b/test.h index bfec4ba7..37f835e9 100644 --- a/test.h +++ b/test.h @@ -3,58 +3,112 @@ #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 *); - bool success; + void (*fn)(struct test_case_metadata *, struct test_file_metadata *); + struct test_failure failure; + const char *name; struct test_case_metadata *next; }; -struct test_case_metadata __attribute__((weak)) *test_case_head; +struct test_file_metadata { + bool registered; + const char *name; + struct test_file_metadata *next; + struct test_case_metadata *tests; +}; -static inline void report_failure(const char *message, const char *file, int line) { - fprintf(stderr, "Test failed, %s at %s:%d\n", message, file, line); -} +struct test_file_metadata __attribute__((weak)) * test_file_head; -#define SHOULD_EQUAL(a, b) do { \ - if ((a) != (b)) { \ - report_failure(#a " != " #b, __FILE__, __LINE__); \ - metadata->success = false; \ - return; \ - } \ -} while(0) +#define SET_FAILURE(_message) \ + metadata->failure = (struct test_failure) { \ + .message = _message, .file = __FILE__, .line = __LINE__, .present = true, \ + } -#define TEST_CASE(name) \ -static void __test_h_##name(struct test_case_metadata *); \ -static void __attribute__((constructor)) __test_h_##name##_register(void) { \ - struct test_case_metadata *t = malloc(sizeof(*t)); \ - t->fn = __test_h_##name; \ - t->next = test_case_head; \ - test_case_head = t; \ -} \ -static void __test_h_##name(struct test_case_metadata *metadata) +#define TEST_EQUAL(a, b) \ + do { \ + if ((a) != (b)) { \ + SET_FAILURE(#a " != " #b); \ + return; \ + } \ + } while (0) -static inline void run_tests(void) { - struct test_case_metadata *i = test_case_head; - int failed = 0; - while(i) { - i->success = true; - i->fn(i); - if (!i->success) { - failed++; +#define TEST_TRUE(a) \ + do { \ + if (!(a)) { \ + SET_FAILURE(#a " is not 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; \ + static struct test_case_metadata __test_h_meta_##_name = { \ + .name = #_name, \ + .fn = __test_h_##_name, \ + }; \ + static void __attribute__((constructor)) __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) + +/// Run defined tests, return true if all tests succeeds +static inline bool run_tests(void) { + 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); + return failed == 0; } #else -#define TEST_CASE(name) \ -static void __attribute__((unused)) __test_h_##name(void) +#define TEST_CASE(name) static void __attribute__((unused)) __test_h_##name(void) -#define SHOULD_EQUAL(a, b) +#define TEST_EQUAL(a, b) +#define TEST_TRUE(a) #endif From 115d38f7b65e947ef0265f6a7a10966d9ce9fe71 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Wed, 13 Mar 2019 21:08:01 +0000 Subject: [PATCH 03/13] Add stub run_tests For when unit testing is disabled --- test.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test.h b/test.h index 37f835e9..b20f2e08 100644 --- a/test.h +++ b/test.h @@ -75,7 +75,7 @@ struct test_file_metadata __attribute__((weak)) * test_file_head; struct test_file_metadata *file_metadata) /// Run defined tests, return true if all tests succeeds -static inline bool run_tests(void) { +static inline bool __attribute__((unused)) run_tests(void) { struct test_file_metadata *i = test_file_head; int failed = 0, success = 0; while (i) { @@ -111,4 +111,7 @@ static inline bool run_tests(void) { #define TEST_EQUAL(a, b) #define TEST_TRUE(a) +static inline bool __attribute__((unused)) run_tests(void) { + return true; +} #endif From 5f15ab6878aad87ccee8cc3c776a8c0e1d3590c9 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Wed, 13 Mar 2019 21:08:53 +0000 Subject: [PATCH 04/13] Update README --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 94ecf546..10fdbcc7 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ To enable the test cases in your code, compile your program with `-DUNIT_TEST`. TEST_CASE(test_case_name) { // Your code here // ... - SHOULD_EQUAL(1, 0); // Fail + TEST_EQUAL(1, 0); // Fail } ``` @@ -37,10 +37,11 @@ In your main function, call `run_tests`: int main() { // necessary setup code // ... -#ifdef UNIT_TEST - run_tests(); + if (!run_tests()) { + // test failed + abort(); + } // cleanup -#endif // usual code } From e4e40a8aabfb488f98c43c3300894bd69b466343 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Wed, 13 Mar 2019 21:10:11 +0000 Subject: [PATCH 05/13] Fix missing stdbool.h include --- examples/trivial_tests/Makefile | 2 +- examples/trivial_tests/test_test1.c | 2 -- test.h | 2 ++ 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/trivial_tests/Makefile b/examples/trivial_tests/Makefile index 9320a846..17cf3eab 100644 --- a/examples/trivial_tests/Makefile +++ b/examples/trivial_tests/Makefile @@ -2,7 +2,7 @@ all: normal unittest # A normal binary, compiled without unit tests normal: test_test1.c test_test2.c ../../test.h - gcc -g test_test1.c test_test2.c -I../../ -o $@ + 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/examples/trivial_tests/test_test1.c b/examples/trivial_tests/test_test1.c index 8454e9d4..fcbf4f76 100644 --- a/examples/trivial_tests/test_test1.c +++ b/examples/trivial_tests/test_test1.c @@ -3,9 +3,7 @@ #include "test.h" int main() { -#ifdef UNIT_TEST run_tests(); -#endif } TEST_CASE(test1) { diff --git a/test.h b/test.h index b20f2e08..16b635b7 100644 --- a/test.h +++ b/test.h @@ -106,6 +106,8 @@ static inline bool __attribute__((unused)) run_tests(void) { #else +#include + #define TEST_CASE(name) static void __attribute__((unused)) __test_h_##name(void) #define TEST_EQUAL(a, b) From 09083afa1e7d021273842365dd79f14bfcc9f21e Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Wed, 13 Mar 2019 21:18:08 +0000 Subject: [PATCH 06/13] Parse command line arguments --- examples/trivial_tests/test_test1.c | 4 ++-- test.h | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/examples/trivial_tests/test_test1.c b/examples/trivial_tests/test_test1.c index fcbf4f76..bdf7024f 100644 --- a/examples/trivial_tests/test_test1.c +++ b/examples/trivial_tests/test_test1.c @@ -2,8 +2,8 @@ #include #include "test.h" -int main() { - run_tests(); +int main(int argc, char *const *argv) { + run_tests(argc, argv); } TEST_CASE(test1) { diff --git a/test.h b/test.h index 16b635b7..5c0882ed 100644 --- a/test.h +++ b/test.h @@ -6,6 +6,7 @@ #include #include #include +#include struct test_file_metadata; @@ -75,7 +76,18 @@ struct test_file_metadata __attribute__((weak)) * test_file_head; struct test_file_metadata *file_metadata) /// Run defined tests, return true if all tests succeeds -static inline bool __attribute__((unused)) run_tests(void) { +static inline bool __attribute__((unused)) run_tests(int argc, char *const *argv) { + bool should_run = false; + for (int i = 0; i < argc; i++) { + if (strcmp(argv[i], "--unittest") == 0) { + should_run = true; + break; + } + } + if (!should_run) { + return true; + } + struct test_file_metadata *i = test_file_head; int failed = 0, success = 0; while (i) { @@ -113,7 +125,7 @@ static inline bool __attribute__((unused)) run_tests(void) { #define TEST_EQUAL(a, b) #define TEST_TRUE(a) -static inline bool __attribute__((unused)) run_tests(void) { +static inline bool __attribute__((unused)) run_tests(int argc, char *const *argv) { return true; } #endif From 63a0c61f630f34f155e7f80c3796d0af1770b5c8 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Wed, 13 Mar 2019 21:21:27 +0000 Subject: [PATCH 07/13] Update README --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 10fdbcc7..8616e7e4 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,10 @@ TEST_CASE(test_case_name) { In your main function, call `run_tests`: ```c -int main() { +int main(int argc, char *const *argv) { // necessary setup code // ... - if (!run_tests()) { + if (!run_tests(argc, argv)) { // test failed abort(); } @@ -46,3 +46,5 @@ int main() { } ``` + +Then, run your program with `./program --unittest`. From 7034883fbcf35c54bba1ee34edc0fb85a231b421 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Mon, 18 Mar 2019 21:46:23 +0000 Subject: [PATCH 08/13] Add a meson.build to test.h Signed-off-by: Yuxuan Shui --- meson.build | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 meson.build diff --git a/meson.build b/meson.build new file mode 100644 index 00000000..042e5d5d --- /dev/null +++ b/meson.build @@ -0,0 +1,2 @@ +project('test.h', 'c') +test_h_dep = declare_dependency(include_directories: include_directories('.')) From d9e628fb536e51cd0a1493d1eb396495fe3d4ff0 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Mon, 18 Mar 2019 21:52:30 +0000 Subject: [PATCH 09/13] test.h: fix unused variable warning when not enabled Signed-off-by: Yuxuan Shui --- test.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test.h b/test.h index 5c0882ed..a44385d2 100644 --- a/test.h +++ b/test.h @@ -122,8 +122,8 @@ static inline bool __attribute__((unused)) run_tests(int argc, char *const *argv #define TEST_CASE(name) static void __attribute__((unused)) __test_h_##name(void) -#define TEST_EQUAL(a, b) -#define TEST_TRUE(a) +#define TEST_EQUAL(a, b) (void)(a); (void)(b) +#define TEST_TRUE(a) (void)(a) static inline bool __attribute__((unused)) run_tests(int argc, char *const *argv) { return true; From 3e9bc97858219face4544f1087c5cc10874a938e Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Mon, 18 Mar 2019 21:57:10 +0000 Subject: [PATCH 10/13] test.h: add out parameter to return whether tests were run Signed-off-by: Yuxuan Shui --- examples/trivial_tests/test_test1.c | 2 +- test.h | 20 +++++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/examples/trivial_tests/test_test1.c b/examples/trivial_tests/test_test1.c index bdf7024f..a2da9fe8 100644 --- a/examples/trivial_tests/test_test1.c +++ b/examples/trivial_tests/test_test1.c @@ -3,7 +3,7 @@ #include "test.h" int main(int argc, char *const *argv) { - run_tests(argc, argv); + run_tests(argc, argv, NULL); } TEST_CASE(test1) { diff --git a/test.h b/test.h index a44385d2..d469ef1b 100644 --- a/test.h +++ b/test.h @@ -76,8 +76,13 @@ struct test_file_metadata __attribute__((weak)) * test_file_head; struct test_file_metadata *file_metadata) /// Run defined tests, return true if all tests succeeds -static inline bool __attribute__((unused)) run_tests(int argc, char *const *argv) { +/// @param[out] tests_run if not NULL, set to whether tests were run +static inline bool __attribute__((unused)) +run_tests(int argc, char *const *argv, bool *tests_run) { bool should_run = false; + if (tests_run) { + *tests_run = false; + } for (int i = 0; i < argc; i++) { if (strcmp(argv[i], "--unittest") == 0) { should_run = true; @@ -88,6 +93,9 @@ static inline bool __attribute__((unused)) run_tests(int argc, char *const *argv return true; } + if (tests_run) { + *tests_run = true; + } struct test_file_metadata *i = test_file_head; int failed = 0, success = 0; while (i) { @@ -122,10 +130,16 @@ static inline bool __attribute__((unused)) run_tests(int argc, char *const *argv #define TEST_CASE(name) static void __attribute__((unused)) __test_h_##name(void) -#define TEST_EQUAL(a, b) (void)(a); (void)(b) +#define TEST_EQUAL(a, b) \ + (void)(a); \ + (void)(b) #define TEST_TRUE(a) (void)(a) -static inline bool __attribute__((unused)) run_tests(int argc, char *const *argv) { +static inline bool __attribute__((unused)) +run_tests(int argc, char *const *argv, bool *tests_run) { + if (tests_run) { + *tests_run = false; + } return true; } #endif From 9e662148eb6190c2b2b1446f651276ef679ab499 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Mon, 18 Mar 2019 22:28:38 +0000 Subject: [PATCH 11/13] test.h: add TEST_STREQUAL Signed-off-by: Yuxuan Shui --- test.h | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test.h b/test.h index d469ef1b..99c7db35 100644 --- a/test.h +++ b/test.h @@ -54,6 +54,14 @@ struct test_file_metadata __attribute__((weak)) * test_file_head; } \ } 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 *); \ @@ -134,6 +142,9 @@ run_tests(int argc, char *const *argv, bool *tests_run) { (void)(a); \ (void)(b) #define TEST_TRUE(a) (void)(a) +#define TEST_STREQUAL(a, b) \ + (void)(a); \ + (void)(b) static inline bool __attribute__((unused)) run_tests(int argc, char *const *argv, bool *tests_run) { From cf253e12f3dec3fcee379ca9190575b4b6ab94f3 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Mon, 18 Mar 2019 22:41:00 +0000 Subject: [PATCH 12/13] test.h: Update README Signed-off-by: Yuxuan Shui --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8616e7e4..0ba77ea5 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ In your main function, call `run_tests`: int main(int argc, char *const *argv) { // necessary setup code // ... - if (!run_tests(argc, argv)) { + if (!run_tests(argc, argv, NULL)) { // test failed abort(); } From a84877df68873f80ff3620f4993619b35b21f758 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Thu, 21 Mar 2019 20:11:20 +0000 Subject: [PATCH 13/13] Run run_tests automatically Make unittest facility less visible. Signed-off-by: Yuxuan Shui --- README.md | 23 ++------- examples/trivial_tests/test_test1.c | 2 +- test.h | 74 ++++++++++++++--------------- 3 files changed, 42 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 0ba77ea5..e6157fc1 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,13 @@ test.h A very simple, light weight, header only C unit test framework. -## Features +# 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 @@ -17,8 +18,6 @@ Just include the header #include "test.h" ``` -To enable the test cases in your code, compile your program with `-DUNIT_TEST`. - ## Defining test cases ```c @@ -31,20 +30,8 @@ TEST_CASE(test_case_name) { ## Run the test cases -In your main function, call `run_tests`: +Build your program with `-DUNIT_TEST`, then run your program with `./program --unittest`. -```c -int main(int argc, char *const *argv) { - // necessary setup code - // ... - if (!run_tests(argc, argv, NULL)) { - // test failed - abort(); - } - // cleanup - // usual code -} +# Hooks -``` - -Then, run your program with `./program --unittest`. +If you define a function `test_h_unittest_setup`, it will be called before any test cases are run. diff --git a/examples/trivial_tests/test_test1.c b/examples/trivial_tests/test_test1.c index a2da9fe8..16d2b6fb 100644 --- a/examples/trivial_tests/test_test1.c +++ b/examples/trivial_tests/test_test1.c @@ -3,7 +3,7 @@ #include "test.h" int main(int argc, char *const *argv) { - run_tests(argc, argv, NULL); + printf("Hello World!\n"); } TEST_CASE(test1) { diff --git a/test.h b/test.h index 99c7db35..b784a2d4 100644 --- a/test.h +++ b/test.h @@ -60,50 +60,55 @@ struct test_file_metadata __attribute__((weak)) * test_file_head; SET_FAILURE(#a " != " #b); \ return; \ } \ - } while(0) + } 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)) __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, \ +#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 bool __attribute__((unused)) -run_tests(int argc, char *const *argv, bool *tests_run) { +static inline void __attribute__((constructor(102))) run_tests(void) { bool should_run = false; - if (tests_run) { - *tests_run = false; - } - for (int i = 0; i < argc; i++) { - if (strcmp(argv[i], "--unittest") == 0) { + 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 true; + return; } - if (tests_run) { - *tests_run = true; + if (&test_h_unittest_setup) { + test_h_unittest_setup(); } + struct test_file_metadata *i = test_file_head; int failed = 0, success = 0; while (i) { @@ -129,7 +134,7 @@ run_tests(int argc, char *const *argv, bool *tests_run) { int total = failed + success; fprintf(stderr, "Test results: passed %d/%d, failed %d/%d\n", success, total, failed, total); - return failed == 0; + exit(failed == 0 ? EXIT_SUCCESS : EXIT_FAILURE); } #else @@ -146,11 +151,4 @@ run_tests(int argc, char *const *argv, bool *tests_run) { (void)(a); \ (void)(b) -static inline bool __attribute__((unused)) -run_tests(int argc, char *const *argv, bool *tests_run) { - if (tests_run) { - *tests_run = false; - } - return true; -} #endif