Simple UnitTest macros utility in C. I decided to make this to be able to more easily organize my tests and I emphasized on readability. Please tell me what you think and if this utility would perform well in big unit tests.
timer.h
/* Simple timer macros utility */
#ifndef CMC_TIMER_H
#define CMC_TIMER_H
#include <time.h>
typedef struct timer_s
{
clock_t start;
clock_t stop;
double result;
} timer_t;
#define TIMER_START(timer) \
timer.start = clock();
#define TIMER_STOP(timer) \
timer.stop = clock();
#define TIMER_CALC(timer) \
timer.result = (double)(((timer.stop - timer.start) * 1000.0) / CLOCKS_PER_SEC);
#endif /* CMC_TIMER_H */
test.h
/**
* Simple Unit Test Utility
*
* CMC_CREATE_UNIT
* Create a UnitTest
* Parameters:
* - UNAME : UnitTest name (const char *)
* - VERBOSE : Print PASSED / FAILED messages if true (bool)
* - BODY : Body containing all tests related to this UnitTest
*
* CMC_CREATE_TEST
* Create a Test inside a UnitTest.
* Parameters:
* - TNAME : Test name (const char *)
* - BODY : Block of code containing a specific test
* Inside the BODY use the following macros:
* - CMC_TEST_PASS
* Pass current test.
* - CMC_TEST_FAIL
* Fail current test.
* - CMC_TEST_PASS_ELSE_FAIL
* If given expression is true, pass current test, else fail.
* - CMC_TEST_ABORT
* Abort current unit test and displays error message. All tests
* after the abort won't be called.
* All of these macros can only be called once per test.
*
* CMC_TEST_COLOR
* Define this macro to allow colored output.
*/
#ifndef CMC_TEST_H
#define CMC_TEST_H
#include "timer.h"
#include <inttypes.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef CMC_TEST_COLOR
static const char *cmc_test_color[] = {"\x1b[32m", "\x1b[31m", "\x1b[35m"};
#endif
typedef struct cmc_test_info_s
{
uintmax_t total;
uintmax_t passed;
uintmax_t failed;
bool aborted;
bool verbose;
} cmc_test_info;
/* Logging function */
static void cmc_test_log(const char *unit_name, const char *current_test, bool aborted, bool passed)
{
if (aborted)
{
#ifdef CMC_TEST_COLOR
printf("UNIT_TEST %s %sABORTED\x1b[0m AT %s\n",
unit_name,
cmc_test_color[2],
current_test);
#else
printf("UNIT_TEST %s ABORTED at %s\n", unit_name, current_test);
#endif
}
else
{
#ifdef CMC_TEST_COLOR
printf("Test from %s %s%7s\x1b[0m %s\n",
unit_name,
passed ? cmc_test_color[0] : cmc_test_color[1],
passed ? "PASSED" : "FAILED",
current_test);
#else
printf("Test from %s %7s %s\n", unit_name, passed ? "PASSED" : "FAILED", current_test);
#endif
}
}
#define CMC_CREATE_UNIT(UNAME, VERBOSE, BODY) \
void UNAME(void) \
{ \
const char *unit_name = #UNAME; \
const char *current_test = NULL; \
\
cmc_test_info tinfo = {0}; \
timer_t timer = {0}; \
\
tinfo.verbose = VERBOSE; \
\
/* Tests */ \
TIMER_START(timer); \
BODY; \
TIMER_STOP(timer); \
TIMER_CALC(timer); \
\
unittest_abort: \
if (tinfo.aborted) \
{ \
cmc_test_log(unit_name, current_test, true, false); \
} \
\
printf("+--------------------------------------------------+\n"); \
printf("| Unit Test Report : %-30s|\n", unit_name); \
printf("| TOTAL : %10" PRIuMAX " |\n", tinfo.total); \
printf("| PASSED : %10" PRIuMAX " |\n", tinfo.passed); \
printf("| FAILED : %10" PRIuMAX " |\n", tinfo.failed); \
printf("| |\n"); \
printf("| Total Unit Test Runtime : %9.0f milliseconds |\n", timer.result); \
printf("+--------------------------------------------------+\n"); \
}
#define CMC_CREATE_TEST(TNAME, BODY) \
do \
{ \
current_test = #TNAME; \
\
tinfo.total += 1; \
\
BODY; \
\
} while (0);
#define CMC_TEST_PASS() \
do \
{ \
tinfo.passed += 1; \
if (tinfo.verbose) \
cmc_test_log(unit_name, current_test, false, true); \
\
} while (0)
#define CMC_TEST_FAIL() \
do \
{ \
tinfo.failed += 1; \
if (tinfo.verbose) \
cmc_test_log(unit_name, current_test, false, false); \
\
} while (0)
#define CMC_TEST_PASS_ELSE_FAIL(EXPRESSION) \
do \
{ \
if (EXPRESSION) \
CMC_TEST_PASS(); \
else \
CMC_TEST_FAIL(); \
\
} while (0)
#define CMC_TEST_ABORT() \
do \
{ \
\
tinfo.aborted = true; \
tinfo.total -= 1; \
goto unittest_abort; \
\
} while (0)
#endif /* CMC_TEST_H */
example.c
#include "test.h"
uintmax_t gcd(uintmax_t p, uintmax_t q)
{
if (p == 0)
{
return q;
}
if (q == 0)
{
return p;
}
uintmax_t r = p % q;
while (r != 0)
{
p = q;
q = r;
r = p % q;
}
return q;
}
CMC_CREATE_UNIT(gcd_test, true, {
CMC_CREATE_TEST(edge_cases, {
uintmax_t r1 = gcd(0, 0);
uintmax_t r2 = gcd(0, 9);
CMC_TEST_PASS_ELSE_FAIL(r1 == 0 && r2 == 9);
})
CMC_CREATE_TEST(test1, {
CMC_TEST_PASS_ELSE_FAIL(gcd(42, 56) == 14);
})
CMC_CREATE_TEST(test3, {
CMC_TEST_FAIL();
})
CMC_CREATE_TEST(test2, {
uintmax_t r = gcd(12, 0);
if (r == 12)
CMC_TEST_ABORT();
else
CMC_TEST_PASS();
})
CMC_CREATE_TEST(test4, {
uintmax_t r1 = gcd(461952, 116298);
uintmax_t r2 = gcd(7966496, 314080416);
CMC_TEST_PASS_ELSE_FAIL(r1 + r2 == 50);
})
})
int main(void)
{
gcd_test();
return 0;
}
This is part of the C Macro Collections library hosted here.
2 Answers 2
Statement-like macros should normally be wrapped in do
...while(0)
. They should also avoid multiple expansion of arguments. So instead of
#define TIMER_START(timer) \ timer.start = clock(); #define TIMER_STOP(timer) \ timer.stop = clock(); #define TIMER_CALC(timer) \ timer.result = (double)(((timer.stop - timer.start) * 1000.0) / CLOCKS_PER_SEC);
I'd recommend
#define TIMER_START(timer) \
do { (timer).start = clock(); } while (0)
#define TIMER_STOP(timer) \
do { (timer).stop = clock(); } while (0)
#define TIMER_CALC(timer) \
do { \
struct timer_s* t = &(timer); \
t->result = (double)(t->stop - t->start) \
* 1000.0 / CLOCKS_PER_SEC; \
} while (0)
or (more likely) a set of real functions instead.
(I fixed the cast that was in the wrong place - it's the result of the subtraction that's an integer value that may lose precision when promoted to double
for the multiplication).
typedef struct timer_s timer_t
collides with a POSIX reserved identifier, so probably worth avoiding.
Please don't embed terminal-specific codes like this:
static const char *cmc_test_color[] = {"\x1b[32m", "\x1b[31m", "\x1b[35m"};
That becomes an impenetrable mess (or worse) on terminal types other than the ones you've considered. It's better to adapt to the actual known terminal type, perhaps using a library such as termcap.
It seems strange that VERBOSE
is a compile-time setting, rather than a run-time parameter here:
#define CMC_CREATE_UNIT(UNAME, VERBOSE, BODY)
One of the statement-like macros has a stray semicolon after its definition:
#define CMC_CREATE_TEST(TNAME, BODY) \
{ \
... \
} while (0);
+= 1
is more idiomatically written as ++
:
++tinfo.total;
Similarly, write -= 1
using --
.
We'd like the unit-test program to exit with success status only if all tests succeeded. If any fail, we want to know (e.g. to stop the build at that point). I suggest creating the test function with an int
return type to support this, and ending with return tinfo.aborted || tinfo.failed
.
The behaviour when CMC_TEST_ABORT()
is used is strange. It's the only code that's setting tinfo.aborted
, so we could move the if (tinfo.aborted)
block directly into that code. OTOH, we shouldn't be printing the elapsed time in that case, as we've skipped the TIMER_STOP(timer);
and TIMER_CALC(timer);
lines.
Finally, I know it's not really up for review, but I couldn't resist making observations on the gcd()
function used for testing:
There's no need for a specific p==0
test - the flow without the test already does the right thing (but the q==0
test is required, as it's used as divisor in the %
operation).
We can reduce duplication, by moving p % q
into the test:
uintmax_t gcd(uintmax_t p, uintmax_t q)
{
if (q == 0) {
return p;
}
uintmax_t r;
while ((r = p % q) != 0) {
p = q;
q = r;
}
return q;
}
-
\$\begingroup\$ Never heard of
do {...} while(0)
in the context of macros. Learnt something new/old again. \$\endgroup\$AlexV– AlexV2019年06月26日 11:02:55 +00:00Commented Jun 26, 2019 at 11:02 -
\$\begingroup\$ Could you add an example of how to improve
static const char *cmc_test_color[] = {"\x1b[32m", "\x1b[31m", "\x1b[35m"};
with termcap? \$\endgroup\$alx - recommends codidact– alx - recommends codidact2019年06月26日 14:34:11 +00:00Commented Jun 26, 2019 at 14:34 -
\$\begingroup\$ That's not something I ever do from C (I'm generally happy with plain output). If you're having trouble using termcap/terminfo, then Stack Overflow may be a better place to get help. \$\endgroup\$Toby Speight– Toby Speight2019年06月26日 14:43:50 +00:00Commented Jun 26, 2019 at 14:43
-
\$\begingroup\$
typedef struct timer_s timer_t
collides with a reserved identifier (all typedef names ending in _t are reserved for future Standard Library use). I was going to call that one, but I didn't because I didn't find the text in the Standard. Could you quote the specific section, please? \$\endgroup\$alx - recommends codidact– alx - recommends codidact2019年06月26日 15:50:18 +00:00Commented Jun 26, 2019 at 15:50 -
1\$\begingroup\$ I was using CPPReference, which cites its sources as C99 section 7.26 and C11 section K.3.1.2. \$\endgroup\$Toby Speight– Toby Speight2019年06月26日 15:55:34 +00:00Commented Jun 26, 2019 at 15:55
- __func__
__func__
exists for a reason; use it instead of const char *unit_name = #UNAME;
:
cmc_test_log(__func__, current_test, true, false);
...
printf("| Unit Test Report : %-30s|\n", __func__);
- printf("%s", NULL);
Strictly speaking, that is Undefined Behaviour. glibc has a trick, and prints (null)
instead, but that trick is very unreliable, because if gcc decides to optimize printf
into puts
, then UB is invoked (read more: Adding newline character to printf() changes code behaviour on Stack Overflow).
So this code is probably invoking UB, given that all arguments are known at compile time, and the format string ends in \n
(current_test
was defined here: const char *current_test = NULL;
(Why const
and NULL
???)).
#ifdef CMC_TEST_COLOR
printf("UNIT_TEST %s %sABORTED\x1b[0m AT %s\n",
unit_name,
cmc_test_color[2],
current_test);
#else
- Differentiate typedef
ed identifiers from variable names
The easiest thing is to use _s
for struct
s (Or just don't typedef
at all) (not _t
; it is reserved by POSIX). You did it once, but forgot to do it in one of the typedef
s (cmc_test_info
).
Solution:
struct cmc_test_info_s {
uintmax_t total;
uintmax_t passed;
uintmax_t failed;
bool aborted;
bool verbose;
};
typedef struct cmc_test_info_s cmc_test_info_s;
- Function-like macros
Macros that behave like function calls should be named with lowercase to simulate functions, and help differentiate them of other macros that don't behave like functions:
#define timer_calc(timer) do \
{ \
struct timer_s *t_ = timer; \
double diff; \
\
diff = t_->stop - t_->start; \
t_->result = diff * 1000.0 / CLOCKS_PER_SEC; \
} while (0)
#define CMC_CREATE_UNIT(UNAME, verbose, BODY) \
void UNAME(void) \
{ \
...
\
}
Variables local to a macro should use names that are unlikely to be used in the calling function to avoid shadowing a variable (Imagine what would happen if the calling function called the macro this way: timer_calc(t_);
). The usual convention is to add a trailing underscore to names local to a macro.
- macros that depend on having a local variable with a magic name (source: Linux Kernel Coding Style)
#define FOO(val) bar(index, val)
might look like a good thing, but it’s confusing as hell when one reads the code and it’s prone to breakage from seemingly innocent changes.
Easy solution:
#define cmc_test_fail(tinfo, current_test) do \
{ \
struct cmc_test_info_s tinfo_ = tinfo; \
\
tinfo_.failed++; \
if (tinfo_.verbose) \
cmc_test_log(__func__, current_test, false, false); \
\
} while (0)
-
\$\begingroup\$ For "Why
const
andNULL
" -- why not? It's notchar* const
, which would be a bit odd but would be a reasonable way to giveNULL
a better, context-specific name. It'sconst char*
. \$\endgroup\$anon– anon2019年06月26日 18:04:16 +00:00Commented Jun 26, 2019 at 18:04 -
\$\begingroup\$ @NicHartley True, I interpreted it wrongly as
const char *const
. But the question was in the sense thatNULL
is not a validcurrent_test
value, and it would be better to give it some valid value, or leave it uninitialized, or ask the user to input a string as a variable. And because of that, I mixed some ideas :) \$\endgroup\$alx - recommends codidact– alx - recommends codidact2019年06月26日 18:45:54 +00:00Commented Jun 26, 2019 at 18:45