Added first version of Slim Test, a minimalistic testing library for C.
authorStephan Soller <stephan.soller@helionweb.de>
Mon, 15 Feb 2016 18:59:44 +0000 (19:59 +0100)
committerStephan Soller <stephan.soller@helionweb.de>
Mon, 15 Feb 2016 18:59:44 +0000 (19:59 +0100)
.gitignore [new file with mode: 0644]
Makefile [new file with mode: 0644]
slim_test.h [new file with mode: 0644]
tests/slim_test_crashtest.c [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..2d247db
--- /dev/null
@@ -0,0 +1 @@
+tests/slim_test_crashtest
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
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 (file)
index 0000000..1f56de0
--- /dev/null
@@ -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 (file)
index 0000000..299bad2
--- /dev/null
@@ -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