From 8872c78a5f0faa4b78af3e2cce0d7ba6db326f5d Mon Sep 17 00:00:00 2001
From: Stephan Soller <stephan.soller@helionweb.de>
Date: Mon, 15 Feb 2016 19:59:44 +0100
Subject: [PATCH] Added first version of Slim Test, a minimalistic testing
 library for C.

---
 .gitignore                  |   1 +
 Makefile                    |  17 ++++
 slim_test.h                 | 198 ++++++++++++++++++++++++++++++++++++
 tests/slim_test_crashtest.c |  71 +++++++++++++
 4 files changed, 287 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 Makefile
 create mode 100644 slim_test.h
 create mode 100644 tests/slim_test_crashtest.c

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2d247db
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+tests/slim_test_crashtest
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..cb49323
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,17 @@
+# Setup implicit rule to build object files
+CC  = gcc
+CFLAGS = -std=c99 -Werror -Wall -Wextra
+
+
+# Tests are created by implicit rules
+TESTS = $(patsubst %.c,%,$(wildcard tests/*test.c))
+all: $(TESTS)
+
+# Individula test dependencies, tests are build by implicit rules
+tests/slim_test_crashtest.c: slim_test.h
+
+
+# Clean all files in the .gitignore list, ensures that the ignore file
+# is properly maintained.
+clean:
+	xargs --arg-file .gitignore --verbose rm -fr
\ No newline at end of file
diff --git a/slim_test.h b/slim_test.h
new file mode 100644
index 0000000..1f56de0
--- /dev/null
+++ b/slim_test.h
@@ -0,0 +1,198 @@
+/**
+
+Slim Test v1.0
+By Stephan Soller <stephan.soller@helionweb.de>
+Licensed under the MIT license
+
+This is an stb style single header file library to write test cases.
+Define SLIM_TEST_IMPLEMENTATION before you include this file in *one* C file to
+create the implementation.
+
+
+USAGE
+
+	#define SLIM_TEST_IMPLEMENTATION
+	#include "slim_test.h"
+	
+	void test_case_a() {
+		int x = 7;
+		st_check_int(x, 7);
+	}
+	
+	void test_case_b() {
+		const char* text = "Hello Test!";
+		st_check_str(text, "Hello World!");
+	}
+	
+	int main() {
+		st_run(test_case_a);
+		st_run(test_case_b);
+		return st_show_report();
+	}
+
+This will output:
+
+	.F
+	- test_case_b failed in tests/slim_test_example.c:11
+	  text == "Hello World!" failed, got "Hello Test!", expected "Hello World!"
+	1 tests failed, 1 tests passed, 1 checks passed
+
+
+QUICK NOTES
+
+- Each test case is a void function. If a check fails that test case is aborted
+  but the others will still be run.
+- Each test case has to be called using the `st_run()` function.
+- `st_show_report()` shows some statistics and returns the number of failed test
+  cases.
+- The checks are macros that return from the test case when the check failed.
+  You can add your own check via the `st_check_msg()` macro like this:
+  
+  	#define st_check_even(actual) st_check_msg( (actual) % 2 == 0, #actual " is odd, expected it to be even" )
+  
+  The message understands `printf()` format specifiers like %s and %f. Just like
+  with `printf()` the values to inserted there are added as extra arguments after
+  the message.
+- Check messages can only be up to 1024 bytes long. If you need anything longer
+  you can define ST_MAX_MESSAGE_SIZE with the size you need before you include
+  this header.
+
+
+VERSION HISTORY
+
+v1.0  2016-02-14  Initial release
+
+**/
+
+#ifndef SLIM_TEST_INCLUDED
+#define SLIM_TEST_INCLUDED
+
+#include <string.h>
+#include <math.h>
+
+typedef void (*st_test_func_t)();
+int  st_run(st_test_func_t test_case);
+int  st_show_report();
+void st_failed(const char* func, const char* file, const int line, const char *message, ...);
+
+size_t st_tests_run = 0, st_tests_failed = 0, st_checks_passed = 0;
+
+#define st_check(expr)                             if( expr                                       ){ st_checks_passed++; }else{ return st_failed(__func__, __FILE__, __LINE__, #expr); }
+#define st_check_msg(expr, message, ...)           if( expr                                       ){ st_checks_passed++; }else{ return st_failed(__func__, __FILE__, __LINE__, message, ##__VA_ARGS__); }
+#define st_check_str(actual, expected)             if( strcmp((actual), (expected)) == 0          ){ st_checks_passed++; }else{ return st_failed(__func__, __FILE__, __LINE__, #actual " == " #expected " failed, got \"%s\", expected \"%s\"", (actual), (expected)); }
+#define st_check_strn(actual, expected, size)      if( strncmp((actual), (expected), (size)) == 0 ){ st_checks_passed++; }else{ return st_failed(__func__, __FILE__, __LINE__, #actual " == " #expected " failed, got \"%.*s\", expected \"%.*s\"", (int)(size), (actual), (int)(size), (expected)); }
+#define st_check_int(actual, expected)             if( (actual) == (expected)                     ){ st_checks_passed++; }else{ return st_failed(__func__, __FILE__, __LINE__, #actual " == " #expected " failed, got %d, expected %d", (actual), (expected)); }
+#define st_check_float(actual, expected, epsilon)  if( fabs((actual) - (expected)) < (epsilon)    ){ st_checks_passed++; }else{ return st_failed(__func__, __FILE__, __LINE__, #actual " == " #expected " failed, got %f, expected %f (epsilon %f)", (actual), (expected), (epsilon)); }
+#define st_check_not_null(actual)                  if( (actual) != NULL                           ){ st_checks_passed++; }else{ return st_failed(__func__, __FILE__, __LINE__, #actual " is NULL but should not be NULL"); }
+#define st_check_null(actual)                      if( (actual) == NULL                           ){ st_checks_passed++; }else{ return st_failed(__func__, __FILE__, __LINE__, #actual " should be NULL, got %p", (actual)); }
+
+#ifndef ST_MAX_MESSAGE_SIZE
+#define ST_MAX_MESSAGE_SIZE 1024
+#endif
+
+#endif // SLIM_TEST_INCLUDED
+
+
+#ifdef SLIM_TEST_IMPLEMENTATION
+
+#include <stdio.h>
+#include <stdarg.h>
+#include <stdlib.h>
+
+/**
+ * A single linked list of error messages. For each failed check `st_failed()`
+ * appends a message at the end of the list. It's only used internally so it's
+ * not part of the public header.
+ */
+typedef struct st_report_item st_report_item_t, *st_report_item_p;
+struct st_report_item {
+    char *msg;
+    st_report_item_p next;
+};
+st_report_item_p st__report_items = NULL;
+
+
+/**
+ * Records a message for a failed test case. You can use printf() format
+ * specifiers in the `message` parameter.
+ * 
+ * The entire message can only be up to ST_MAX_MESSAGE_SIZE bytes long.
+ * 
+ * Implementation notes:
+ * 
+ * Would be easier with vasprintf() and asprintf() but this requires _GNU_SOURCE.
+ * Not a good idea in a single header file library to pull in that feature test
+ * macro.
+ */
+void st_failed(const char* func, const char* file, const int line, const char *message, ...) {
+	st_tests_failed++;
+	
+	// Allocate a new report item for the message and insert it at the end of the linked list
+	st_report_item_p item = malloc(sizeof(st_report_item_t));
+	memset(item, 0, sizeof(st_report_item_t));
+	if (st__report_items == NULL) {
+		st__report_items = item;
+	} else {
+		st_report_item_p curr = st__report_items;
+		while(curr->next != NULL)
+			curr = curr->next;
+		curr->next = item;
+	}
+	
+	// Allocate message buffer in item
+	size_t msg_buffer_size = ST_MAX_MESSAGE_SIZE, msg_buffer_filled = 0;
+	item->msg = malloc(msg_buffer_size);
+	
+	// Put error message into buffer
+	msg_buffer_filled += snprintf(item->msg, msg_buffer_size, "- %s failed in %s:%d\n  ", func, file, line);
+	
+	va_list args;
+	va_start(args, message);
+	msg_buffer_filled += vsnprintf(item->msg + msg_buffer_filled, msg_buffer_size - msg_buffer_filled, message, args);
+	va_end(args);
+	
+	msg_buffer_filled += snprintf(item->msg + msg_buffer_filled, msg_buffer_size - msg_buffer_filled, "\n");
+	
+	// Write zero terminator to make sure that printing this message doesn't
+	// read beyond the allocated buffer.
+	item->msg[msg_buffer_size - 1] = '\0';
+}
+
+
+/**
+ * Runs one test case function. Prints a "." and returns 1 (true) if the test
+ * passed or prints an "F" and returns 0 (false) if it failed.
+ */
+int st_run(st_test_func_t test_case) {
+	st_tests_run++;
+	size_t test_failed_before_run = st_tests_failed;
+	test_case();
+	
+	if (st_tests_failed > test_failed_before_run) {
+		fprintf(stderr, "F");
+		fflush(stderr);
+		return 0;
+	}
+	
+	fprintf(stderr, ".");
+	fflush(stderr);
+	return 1;
+}
+
+/**
+ * Shows the messages of all failed test cases as well as a small summary.
+ * Returns the number of failed test cases.
+ */
+int st_show_report() {
+	fprintf(stderr, "\n");
+	
+	for(st_report_item_p item = st__report_items; item != NULL; item = item->next)
+		fputs(item->msg, stderr);
+	
+	fprintf(stderr, "\x1b[%sm", (st_tests_failed > 0) ? "31" : "32");
+	fprintf(stderr, "%zu tests failed, %zu tests passed, %zu checks passed\n", st_tests_failed, st_tests_run - st_tests_failed, st_checks_passed);
+	fprintf(stderr, "\x1b[0m");
+	return st_tests_failed;
+}
+
+#endif // SLIM_TEST_IMPLEMENTATION
\ No newline at end of file
diff --git a/tests/slim_test_crashtest.c b/tests/slim_test_crashtest.c
new file mode 100644
index 0000000..299bad2
--- /dev/null
+++ b/tests/slim_test_crashtest.c
@@ -0,0 +1,71 @@
+/**
+
+This file contains code to see if Slim Test behaves as it should. All test cases
+here are expected to fail at the last check. Not nice but adding an extra
+check_failed() would complicate matters more than it's worth it.
+
+**/
+
+#define SLIM_TEST_IMPLEMENTATION
+#include "../slim_test.h"
+
+
+void test_check() {
+	st_check(1 == 0);
+}
+
+void test_check_msg() {
+	st_check_msg(1 == 1, "broken!");
+	st_check_msg(1 == 0, "broken! expected %d", 7);
+}
+
+void test_check_str() {
+	st_check_str("foo", "bar");
+}
+
+void test_check_strn() {
+	st_check_strn("foo1", "foo2", 3);
+	st_check_strn("fox1", "foo2", 3);
+}
+
+void test_check_int() {
+	int value = 7;
+	st_check_int(value, 7);
+	st_check_int(value, 8);
+}
+
+void test_check_float() {
+	float value = 3.141;
+	st_check_float(value, 3.141, 0.001);
+	st_check_float(value, 3.5, 0.001);
+}
+
+void test_check_not_null() {
+	int value = 7;
+	void* p = &value;
+	st_check_not_null(p);
+	
+	p = NULL;
+	st_check_not_null(p);
+}
+
+void test_check_null() {
+	void* p = NULL;
+	st_check_null(p);
+	
+	int value = 7;
+	p = &value;
+	st_check_null(p);
+}
+
+int main() {
+	st_run(test_check);
+	st_run(test_check_msg);
+	st_run(test_check_str);
+	st_run(test_check_strn);
+	st_run(test_check_int);
+	st_run(test_check_float);
+	st_run(test_check_not_null);
+	st_run(test_check_null);
+	return st_show_report();
+}
\ No newline at end of file
-- 
2.20.1