For a unit-testing library that I'm writing, rexo, I would like to implement an automatic test registration mechanism compatible with both C99 and C++11.
Automatic test registration usually goes along the lines of:
- providing macros for the users to define test suites and test cases.
- having the macros instantiate file-level structures that contain the data needed to fully describe their respective test suites/cases.
- having some logic that can somehow discover these structure instances at run-time.
I've got most of this sorted out but one bit: providing a nice interface for defining additional data to be attached to each test suite/case.
The (non-public) data structure to attach looks like this:
struct rx__data {
const char *name;
int value;
rx_run_fn run;
};
I managed to get a RX__MAKE_DATA()
macro working with a designated initializer syntax, as follows:
/* https://github.com/swansontec/map-macro ----------------------------------- */
#define EVAL0(...) __VA_ARGS__
#define EVAL1(...) EVAL0(EVAL0(EVAL0(__VA_ARGS__)))
#define EVAL2(...) EVAL1(EVAL1(EVAL1(__VA_ARGS__)))
#define EVAL3(...) EVAL2(EVAL2(EVAL2(__VA_ARGS__)))
#define EVAL4(...) EVAL3(EVAL3(EVAL3(__VA_ARGS__)))
#define EVAL(...) EVAL4(EVAL4(EVAL4(__VA_ARGS__)))
#define MAP_END(...)
#define MAP_OUT
#define MAP_GET_END2() 0, MAP_END
#define MAP_GET_END1(...) MAP_GET_END2
#define MAP_GET_END(...) MAP_GET_END1
#define MAP_NEXT0(test, next, ...) next MAP_OUT
#define MAP_NEXT1(test, next) MAP_NEXT0(test, next, 0)
#define MAP_NEXT(test, next) MAP_NEXT1(MAP_GET_END test, next)
#define MAP0(f, x, peek, ...) f(x) MAP_NEXT(peek, MAP1)(f, peek, __VA_ARGS__)
#define MAP1(f, x, peek, ...) f(x) MAP_NEXT(peek, MAP0)(f, peek, __VA_ARGS__)
#define MAP(f, ...) EVAL(MAP1(f, __VA_ARGS__, ()()(), ()()(), ()()(), 0))
/* -------------------------------------------------------------------------- */
typedef int (*rx_run_fn)();
struct rx__data {
const char *name;
int value;
rx_run_fn run;
};
int run() { return 999; }
#ifdef __cplusplus
#define RX__WRAP_ASSIGNMENT(x) out x;
#define RX__MAKE_DATA(...) \
[]() -> struct rx__data { \
struct rx__data out = {}; \
MAP(RX__WRAP_ASSIGNMENT, __VA_ARGS__); \
return out; \
}()
#else
#define RX__MAKE_DATA(...) { __VA_ARGS__ }
#endif
static const struct rx__data foo
= RX__MAKE_DATA(.name = "abc", .value = 123, .run = run);
It's all good except that, since the rx__data
struct can be attached to both test suites and test cases, I'd like to have a mechanism that allows me to know if a data member has been explicitely set or not by the user. This way, I can infer the final data to apply to a test case by:
- retrieving the data to inherit from the parent test suite.
- overriding only the members from the test suite that were explicitely set onto the test case.
For example
RX_TEST_SUITE(my_suite, .name = "abc", .value = 123, .run = run);
RX_TEST_CASE(my_suite, my_case, .value = 666)
{
...
}
would result in ‘my_case’ having the data {.name = "abc", .value = 666, .run = run}
attached to it.
For this to work, I thought of adding a boolean value for each field, to keep track of what has been explicitely defined or not by the user:
typedef int (*rx_run_fn)();
struct rx__data {
const char *name;
int value;
rx_run_fn run;
int name_defined;
int value_defined;
int run_defined;
};
int run() { return 999; }
#ifdef __cplusplus
#define RX__ARG(field, value) out.field = value; out.field##_defined = 1
#define RX__MAKE_DATA(...) \
[]() -> struct rx__data { \
struct rx__data out = {}; \
__VA_ARGS__; \
return out; \
}();
#else
#define RX__ARG(field, value) .field = value, .field##_defined = 1
#define RX__MAKE_DATA(...) { __VA_ARGS__ }
#endif
#define RX_NAME_ARG(x) RX__ARG(name, x)
#define RX_VALUE_ARG(x) RX__ARG(value, x)
#define RX_RUN_ARG(x) RX__ARG(run, x)
static const struct rx__data foo
= RX__MAKE_DATA(RX_NAME_ARG("abc"), RX_VALUE_ARG(123), RX_RUN_ARG(run));
And it's all working great here again, except that the user now has to set the arguments using macros instead of the previous designated initializer syntax.
So the questions is: how can I keep track of these user-defined struct
members while preserving the designated initializer syntax?
Note: if possible, I'd really like to have a robust way of detecting if a member was defined, so no in-band indicators—that is, no ”if this member has this magic value, then it's likely that is wasn't explicitely set”.
Aucun commentaire:
Enregistrer un commentaire