[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[PATCH RFC] Implement vector test functions


Signed-off-by: Michał Kępień <michal@xxxxxxx>
---
Hi,

While porting BIND unit tests to cmocka, we often found ourselves in the
need of calling the same test function over and over again with a
different input each time.  I was looking for a way to do it elegantly,
i.e. in a way that would allow us to:

  - define the desired set of unit tests as a vector of input data for
    a common test function rather than as a set of separate CMUnitTest
    structures, each of which would call the same test function with a
    different input,

  - avoid copy-pasting boilerplate setup/test/teardown code as much as
    possible,

  - ensure a test function failure for any given input will not prevent
    subsequent inputs from being checked, i.e. ensure all input data
    will be checked against the test function and that the resulting
    unit test set will contain as many elements as the vector of input
    data.

I could not come up with a solution that would satisfy all of the above
requirements using the currently available API, so I tried extending it
so that it suits my needs.

This patch is a preliminary version of what I would consider a solution
for the above use case.  Shall you be interested in adopting it, I will
be happy to polish it further according to your guidance; I just thought
it does not make sense to tweak the code (and its documentation) to
perfection at this point, when I have no idea whether there would be any
interest in merging it.

The example in example/vector_test.c attempts to show what kind of
improvements in test definitions this patch allows: the example program
will effectively perform the same set of tests no matter whether it is
compiled with or without the USE_VECTOR macro being defined.  However,
employing "vector test functions" (which is what I tentatively dubbed
these new additions) allows eliminating boilerplate statements from each
test_fn_*() call and (IMHO) defining input data in a more declarative
manner, thanks to the use of designated initializers.  It also allows
further inputs for the test function to be added more easily to the test
suite by eliminating the need to add extra boilerplate code for each new
input.

The functions added by this patch allow vectors of arbitrary structures
to be passed to the test function.  The price of enabling that is lack
of type safety and resorting to pointer arithmetic.

The functions added by this patch are not thread-safe (but then neither
is cmocka): static variables are used for iterating over the vector of
input data.  This mostly stems from the desire to keep the current ABI
unchanged.

I will be glad to hear your thoughts about this idea.  If this is
something you would be interested in adopting, please let me know how
can I improve it to make it mergeable.

Thanks!

 example/CMakeLists.txt |  7 ++++
 example/vector_test.c  | 85 ++++++++++++++++++++++++++++++++++++++++++
 include/cmocka.h       | 27 ++++++++++++++
 src/cmocka.c           | 85 ++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 204 insertions(+)
 create mode 100644 example/vector_test.c

diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt
index 8e80ee2..fa4f6a0 100644
--- a/example/CMakeLists.txt
+++ b/example/CMakeLists.txt
@@ -38,6 +38,13 @@ add_cmocka_test(simple_test
                 LINK_LIBRARIES ${CMOCKA_SHARED_LIBRARY})
 add_cmocka_test_environment(simple_test)
 
+### Vector test example
+add_cmocka_test(vector_test
+                SOURCES vector_test.c
+                COMPILE_OPTIONS ${DEFAULT_C_COMPILE_FLAGS} -DUSE_VECTOR
+                LINK_LIBRARIES ${CMOCKA_SHARED_LIBRARY})
+add_cmocka_test_environment(vector_test)
+
 ### Allocate module test
 add_cmocka_test(allocate_module_test
                 SOURCES allocate_module.c allocate_module_test.c
diff --git a/example/vector_test.c b/example/vector_test.c
new file mode 100644
index 0000000..c062e42
--- /dev/null
+++ b/example/vector_test.c
@@ -0,0 +1,85 @@
+#include <stdarg.h>
+#include <stddef.h>
+#include <setjmp.h>
+#include <cmocka.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+
+static int
+set_refval_to_42(void **state) {
+	int *refval = malloc(sizeof(int));
+
+	if (refval == NULL)
+		return -1;
+
+	*refval = 42;
+	*state = refval;
+
+	return 0;
+}
+
+static int
+free_refval(void **state) {
+	free(*state);
+
+	return 0;
+}
+
+#ifndef USE_VECTOR
+static void
+test_fn_0(void **state) {
+	const int *refval = *state;
+	assert_int_not_equal(*refval, 0);
+}
+
+static void
+test_fn_21(void **state) {
+	const int *refval = *state;
+	assert_int_not_equal(*refval, 21);
+}
+
+static void
+test_fn_42(void **state) {
+	const int *refval = *state;
+	assert_int_equal(*refval, 42);
+}
+#else
+typedef struct {
+	const char *name;
+	int val;
+	int equal_to_refval;
+} test_fn_vector;
+
+static void
+test_fn(void **state, const void *elem) {
+	const int *refval = *state;
+	const test_fn_vector *vector = (const test_fn_vector *)elem;
+
+	if (vector->equal_to_refval)
+		assert_int_equal(*refval, vector->val);
+	else
+		assert_int_not_equal(*refval, vector->val);
+}
+#endif
+
+int main(void) {
+#ifndef USE_VECTOR
+	const struct CMUnitTest tests[] = {
+		cmocka_unit_test(test_fn_0),
+		cmocka_unit_test(test_fn_21),
+		cmocka_unit_test(test_fn_42),
+	};
+
+	return cmocka_run_group_tests(tests, set_refval_to_42, free_refval);
+#else
+	const test_fn_vector tests[] = {
+		{ .name = "0",  .val = 0,  .equal_to_refval = 0 },
+		{ .name = "21", .val = 21, .equal_to_refval = 0 },
+		{ .name = "42", .val = 42, .equal_to_refval = 1 },
+	};
+
+	return cmocka_run_vector_tests(test_fn, set_refval_to_42, free_refval,
+				       tests);
+#endif
+}
diff --git a/include/cmocka.h b/include/cmocka.h
index e6861c8..6f2d616 100644
--- a/include/cmocka.h
+++ b/include/cmocka.h
@@ -1836,6 +1836,23 @@ int cmocka_run_group_tests_name(const char *group_name,
         _cmocka_run_group_tests(group_name, group_tests, sizeof(group_tests) / sizeof((group_tests)[0]), group_setup, group_teardown)
 #endif
 
+#ifdef DOXYGEN
+/**
+ * XXX
+ */
+int cmocka_run_vector_tests(const char *group_name,
+			    CMUnitTestVectorFunction vector_func,
+			    CMFixtureFunction group_setup,
+			    CMFixtureFunction group_teardown,
+			    const void *vector,
+			    const size_t vector_size,
+			    const size_t elem_size);
+#else
+# define cmocka_run_vector_tests(f, group_setup, group_teardown, vector) \
+	_cmocka_run_vector_tests(#f, f, group_setup, group_teardown, vector, \
+	sizeof(vector) / sizeof(vector[0]), sizeof(vector[0]))
+#endif
+
 /** @} */
 
 /**
@@ -2082,6 +2099,9 @@ typedef struct GroupTest {
 /* Function prototype for test functions. */
 typedef void (*CMUnitTestFunction)(void **state);
 
+/* Function prototype for vector test functions. */
+typedef void (*CMUnitTestVectorFunction)(void **state, const void *elem);
+
 /* Function prototype for setup and teardown functions. */
 typedef int (*CMFixtureFunction)(void **state);
 
@@ -2253,6 +2273,13 @@ int _cmocka_run_group_tests(const char *group_name,
                             const size_t num_tests,
                             CMFixtureFunction group_setup,
                             CMFixtureFunction group_teardown);
+int _cmocka_run_vector_tests(const char *group_name,
+			     CMUnitTestVectorFunction vector_func,
+			     CMFixtureFunction group_setup,
+			     CMFixtureFunction group_teardown,
+			     const void *vector,
+			     const size_t vector_size,
+			     const size_t elem_size);
 
 /* Standard output and error print methods. */
 void print_message(const char* const format, ...) CMOCKA_PRINTF_ATTRIBUTE(1, 2);
diff --git a/src/cmocka.c b/src/cmocka.c
index b21fe15..ae1baca 100644
--- a/src/cmocka.c
+++ b/src/cmocka.c
@@ -312,6 +312,14 @@ static enum cm_message_output global_msg_output = CM_OUTPUT_STDOUT;
 
 static const char *global_test_filter_pattern;
 
+typedef struct {
+	const char *name;
+	unsigned char data[];
+} CMVectorElem;
+
+static CMUnitTestVectorFunction current_vector_func;
+static const CMVectorElem **current_vector, **current_vector_elem;
+
 #ifndef _WIN32
 /* Signals caught by exception_handler(). */
 static const int exception_signals[] = {
@@ -1827,6 +1835,13 @@ static void *libc_malloc(size_t size)
 #define malloc test_malloc
 }
 
+static void *libc_calloc(size_t nmemb, size_t size)
+{
+#undef malloc
+    return calloc(nmemb, size);
+#define malloc test_malloc
+}
+
 static void libc_free(void *ptr)
 {
 #undef free
@@ -3039,6 +3054,76 @@ int _cmocka_run_group_tests(const char *group_name,
     return total_failed + total_errors;
 }
 
+static int
+alloc_test_name(char **dst, const char *group, const char *name)
+{
+	size_t name_size = strlen(group) + strlen(name) + 2;
+
+	*dst = libc_malloc(name_size);
+	if (*dst == NULL) {
+		return 1;
+	}
+
+	snprintf(*dst, name_size, "%s_%s", group, name);
+	return 0;
+}
+
+static void
+_cmocka_vector_call(void **state) {
+	current_vector_func(state, *current_vector_elem++);
+}
+
+int
+_cmocka_run_vector_tests(const char *group_name,
+			 CMUnitTestVectorFunction vector_func,
+			 CMFixtureFunction group_setup,
+			 CMFixtureFunction group_teardown,
+			 const void *vector,
+			 const size_t vector_size,
+			 const size_t elem_size)
+{
+	struct CMUnitTest *tests;
+	char **test_names;
+	int result;
+	size_t i;
+
+	current_vector = libc_calloc(vector_size, sizeof(*current_vector));
+	tests = libc_calloc(vector_size, sizeof(*tests));
+	test_names = libc_calloc(vector_size, sizeof(*test_names));
+	if (current_vector == NULL || tests == NULL || test_names == NULL) {
+		return -2;
+	}
+
+	for (i = 0; i < vector_size; i++) {
+		const unsigned char *base = (const unsigned char *)vector;
+		const void *elem_addr = base + (i * elem_size);
+		const CMVectorElem *elem = (const CMVectorElem *)elem_addr;
+
+		if (alloc_test_name(&test_names[i], group_name, elem->name)) {
+			return -2;
+		}
+
+		tests[i].name = test_names[i];
+		tests[i].test_func = _cmocka_vector_call;
+		current_vector[i] = elem;
+	}
+
+	current_vector_func = vector_func;
+	current_vector_elem = &current_vector[0];
+	result = _cmocka_run_group_tests(group_name, tests, vector_size,
+					 group_setup, group_teardown);
+	current_vector_func = NULL;
+
+	for (i = 0; i < vector_size; i++) {
+		libc_free(test_names[i]);
+	}
+	libc_free(test_names);
+	libc_free(tests);
+	libc_free(current_vector);
+
+	return (result);
+}
+
 /****************************************************************************
  * DEPRECATED TEST RUNNER
  ****************************************************************************/
-- 
2.19.1


Follow-Ups:
Re: [PATCH RFC] Implement vector test functionsAndreas Schneider <asn@xxxxxxxxxxxxxx>