util: add support for parametrized tests

munit supports test parameters (char* only) but the setup is a big
awkward/impossible in C to define statically. So lets simply pass the
parameters as one NULL-terminated string list and require names to be
prefixed with an @. During the test setup we can split that list up into
multiple parameters and pass those to the munit setup where they're
accessible via another wrapper macro:

MUNIT_TEST_WITH_PARAMS(test_foo, "@x", "1", "2", "@y", "100", "200") {
   const char *x = MUNIT_TEST_PARAM("@x");
   const char *y = MUNIT_TEST_PARAM("@y");
}

Part-of: <https://gitlab.freedesktop.org/libinput/libei/-/merge_requests/365>
This commit is contained in:
Peter Hutterer 2025-12-03 10:57:53 +10:00
parent d55da9466c
commit 2996a66b37
2 changed files with 64 additions and 1 deletions

View file

@ -56,10 +56,42 @@ munit_tests_run(int argc, char **argv)
_cleanup_free_ MunitTest *tests = calloc(count, sizeof(*tests)); _cleanup_free_ MunitTest *tests = calloc(count, sizeof(*tests));
size_t idx = 0; size_t idx = 0;
foreach_test(t) { foreach_test(t) {
size_t nparams = 0;
MunitParameterEnum *parameters = calloc(MUNIT_TEST_MAX_PARAMS, sizeof(*parameters));
const char *name = NULL;
char **values = NULL;
for (size_t i = 0; t->params[i]; i++) {
if (t->params[i][0] == '@') {
if (name != NULL) {
assert(nparams < MUNIT_TEST_MAX_PARAMS);
assert(strv_len(values) > 0);
/* Not stripping the @! */
parameters[nparams].name = xstrdup(name);
parameters[nparams].values = steal(&values);
++nparams;
}
name = t->params[i];
} else {
assert(i > 0); /* First one must be a name */
values = strv_append_strdup(values, t->params[i]);
}
}
if (name != NULL) {
assert(nparams < MUNIT_TEST_MAX_PARAMS);
assert(strv_len(values) > 0);
/* Not stripping the @! */
parameters[nparams].name = xstrdup(name);
parameters[nparams].values = steal(&values);
++nparams;
}
MunitTest test = { MunitTest test = {
.name = xaprintf("%s", t->name), .name = xaprintf("%s", t->name),
.test = t->func, .test = t->func,
.parameters = parameters,
}; };
tests[idx++] = test; tests[idx++] = test;
} }
@ -91,8 +123,10 @@ munit_tests_run(int argc, char **argv)
int rc = munit_suite_main(&suite, setup.userdata, setup.argc, setup.argv); int rc = munit_suite_main(&suite, setup.userdata, setup.argc, setup.argv);
for (idx = 0; idx < count; idx++) for (idx = 0; idx < count; idx++) {
free(tests[idx].name); free(tests[idx].name);
free(tests[idx].parameters);
}
return rc; return rc;
} }

View file

@ -41,6 +41,9 @@
#include <munit.h> #include <munit.h>
/* Random number, bumb if need be */
#define MUNIT_TEST_MAX_PARAMS 64
/** /**
* Put at the top of the file somewhere, declares the start/stop for the test section we need. * Put at the top of the file somewhere, declares the start/stop for the test section we need.
*/ */
@ -61,6 +64,7 @@ struct test_function {
const char *name; /* function name */ const char *name; /* function name */
const char *file; /* file name */ const char *file; /* file name */
munit_test_func_t func; /* test function */ munit_test_func_t func; /* test function */
const char *params[MUNIT_TEST_MAX_PARAMS];
} __attribute__((aligned(16))); } __attribute__((aligned(16)));
/** /**
@ -104,9 +108,34 @@ __attribute__((section("test_functions_section"))) = { \
.name = #_func, \ .name = #_func, \
.func = _func, \ .func = _func, \
.file = __FILE__, \ .file = __FILE__, \
.params = { NULL }, \
}; \ }; \
static MunitResult _func(const MunitParameter params[], void *user_data) static MunitResult _func(const MunitParameter params[], void *user_data)
/**
* Same as MUNIT test but takes a list of strings that define parameters
* and their values. Parameter names must be prefixed with @, for example:
* MUNIT_TEST_WITH_PARAMS(test_foo, "@x", "1", "2", "@y", "100", "200") {
* const char *x = MUNIT_TEST_PARAM("@x");
* const char *y = MUNIT_TEST_PARAM("@y");
* }
*/
#define MUNIT_TEST_WITH_PARAMS(_func, ...) \
static MunitResult _func(const MunitParameter params[], void *user_data); \
static const struct test_function _test_##_func \
__attribute__((used)) \
__attribute__((section("test_functions_section"))) = { \
.name = #_func, \
.func = _func, \
.file = __FILE__, \
.params = { __VA_ARGS__, NULL }, \
}; \
static MunitResult _func(const MunitParameter params[], void *user_data)
/* Retrieve the test parameter with the given name */
#define MUNIT_TEST_PARAM(name_) \
munit_parameters_get(params, name_)
/** /**
* Defines a struct global_setup_function in a custom ELF section that we can * Defines a struct global_setup_function in a custom ELF section that we can
* then find in munit_tests_run() to do setup based on argv and/or pass userdata * then find in munit_tests_run() to do setup based on argv and/or pass userdata