Commit Diff


commit - fcb7466a5ce845e0ca67043a78ba4dd6df61fbac
commit + b0aa2229dfdce21f3b35ca48fcdb99bcd9bc069b
blob - /dev/null
blob + 23e8cfaad8632bb2ad9adb72ec702222e957483d (mode 644)
--- /dev/null
+++ lang/c/test.h
@@ -0,0 +1,149 @@
+/* test.h - unit tests
+ * pesco 2020
+ *
+ * SYNOPSIS
+ *	#include "test.h"
+ *
+ *	TEST(function, ...);
+ *	RUNT(function, ...);
+ *
+ *	fail(fmt, ...);
+ *
+ *	check(expression);
+ *
+ * DESCRIPTION
+ *	The TEST() macro calls the given function as a unit test. Any
+ *	additional arguments will be passed to function, allowing parameterized
+ *	families of tests.
+ *
+ *	TEST() is meant to be called from main() and inspects argc/argv. The
+ *	command line should consist (only) of string prefixes that select the
+ *	corresponding tests. An empty command line selects all tests. Any
+ *	selected tests are passed to RUNT(). The RUNT() macro executes a test
+ *	unconditionally and prints the test result to standard output. It does
+ *	not depend on argc and argv being in scope.
+ *
+ *	Test functions may be defined with any return type, but the return
+ *	value is ignored. To signal failure of a test, the fail() macro should
+ *	be called with an appropriate error message, using printf(3)-style
+ *	arguments. Any other diagnostics should be printed to stderr as usual.
+ *
+ *	The check() macro tests a boolean condition. It calls fail() if the
+ *	given expression evaluates to false. The user should define custom
+ *	variants of check() as suitable.
+ *
+ *	The fail() macro does not abort the current test unless the X
+ *	environment variable is set (see below). Checks that always form a
+ *	precondition for the continuation of the test should use assert(3).
+ *	Meanwhile, RUNT() calls exit(3) after a failed test returns; see also
+ *	the K environment variable.
+ *
+ * ENVIRONMENT
+ *	The following variables serve as flags. They enable the stated behavior
+ *	whenever set, regardless of value.
+ *
+ *	V	Print tests that are skipped.
+ *
+ *	K	Do not exit after a failed test.
+ *
+ *	X	Call abort(3) immediately when fail() is called.
+ *
+ * EXAMPLES
+ *	The following shows a test program:
+ *
+ *		#include "test.h"
+ *
+ *		void foo(void)
+ *		{
+ *			check(1 == 1);
+ *		}
+ *
+ *		void bar(unsigned int mask)
+ *		{
+ *			check(0x01 & mask != 0);
+ *			check(0x10 & mask != 0);
+ *		}
+ *
+ *		int main(int argc, char **argv)
+ *		{
+ *			TEST(foo);
+ *			TEST(bar, 0x0f);
+ *			TEST(bar, 0xff);
+ *
+ *			return 0;
+ *		}
+ *
+ *	To run only the 'bar' family of tests:
+ *
+ *		$ ./test bar
+ *		bar(0xff):	OK
+ *		test.c:11: condition failed: (0x10 & mask) != 0
+ *		bar(0x0f):	FAIL
+ *
+ *	Running 'foo' and 'bar(0xff)':
+ *
+ *		$ ./test foo bar\(0xf
+ *		foo():	OK
+ *		bar(0xff):	OK
+ *
+ *	A list or count of available tests can be produced by combining verbose
+ *	mode with an impossible selection:
+ *
+ *		$ V=1 ./test -
+ *		foo() skipped
+ *		bar(0xff) skipped
+ *		bar(0x0f) skipped
+ *
+ *		$ V=1 ./test - | wc -l
+ *		3
+ */
+
+#ifndef TEST_H
+#define TEST_H
+
+#include <stdio.h>	/* printf, fprintf */
+#include <string.h>	/* strncmp */
+#include <stdlib.h>	/* exit, abort, getenv */
+
+static int failed_;
+
+#define TEST(X, ...) do {						\
+	const char *name = #X "(" #__VA_ARGS__ ")";			\
+	int i;								\
+									\
+	/* does name match any prefix given on the command line? */	\
+	for (i = 1; i < argc; i++)					\
+		if (strncmp(argv[i], name, strlen(argv[i])) == 0)	\
+			break;						\
+	/* run test if it matches an arg or if there are no args */	\
+	if (i < argc || argc == 1)					\
+		RUNT(X, __VA_ARGS__);					\
+	else if (getenv("V") != NULL)		/* V = verbose */	\
+		printf("%s skipped\n", name);				\
+} while (0)
+
+#define RUNT(X, ...) do {						\
+	const char *name = #X "(" #__VA_ARGS__ ")";			\
+									\
+	failed_ = 0;							\
+	(X)(__VA_ARGS__);						\
+	printf("%s:\t%s\n", name, failed_ ? "FAIL" : "OK");		\
+	if (failed_ && getenv("K") == NULL)	/* K = keep going */	\
+		exit(1);						\
+} while (0)
+
+#define check(EXP) do {							\
+	if (!(EXP))							\
+		fail("condition failed: %s", #EXP);			\
+} while (0)
+
+#define fail(...) do {							\
+	fprintf(stderr, "%s:%d: ", __FILE__, __LINE__);			\
+	fprintf(stderr, __VA_ARGS__);					\
+	fprintf(stderr, "\n");						\
+	if (getenv("X") != NULL)		/* X = abort */		\
+		abort();						\
+	failed_ = 1;							\
+} while (0)
+
+#endif /* TEST_H */