Add Clay 0.7.0 script
diff --git a/tests/clay b/tests/clay
new file mode 100755
index 0000000..cd3d42d
--- /dev/null
+++ b/tests/clay
@@ -0,0 +1,213 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from string import Template
+import re, fnmatch, os
+
+VERSION = "0.7.0"
+
+TEST_FUNC_REGEX = r"^(void\s+(test_%s__(\w+))\(\s*(void)?\s*\))\s*\{"
+
+TEMPLATE_MAIN = Template(
+r"""
+/*
+ * Clay v${version}
+ *
+ * This is an autogenerated file. Do not modify.
+ * To add new unit tests or suites, regenerate the whole
+ * file with `./clay`
+ */
+
+#define clay_print(...) ${clay_print}
+
+${clay_library}
+
+${extern_declarations}
+
+static const struct clay_func _all_callbacks[] = {
+    ${test_callbacks}
+};
+
+static const struct clay_suite _all_suites[] = {
+    ${test_suites}
+};
+
+static const char _suites_str[] = "${suites_str}";
+
+int main(int argc, char *argv[])
+{
+    return clay_test(
+        argc, argv, _suites_str,
+        _all_callbacks, ${cb_count},
+        _all_suites, ${suite_count}
+    );
+}
+""")
+
+TEMPLATE_SUITE = Template(
+r"""
+    {
+        "${clean_name}",
+        ${initialize},
+        ${cleanup},
+        ${cb_ptr}, ${cb_count}
+    }
+""")
+
+def main():
+    from optparse import OptionParser
+
+    parser = OptionParser()
+
+    parser.add_option('-c', '--clay-path', dest='clay_path')
+    parser.add_option('-o', '--output', dest='output')
+    parser.add_option('-v', '--report-to', dest='print_mode', default='stdout')
+
+    options, args = parser.parse_args()
+
+    for folder in args:
+        builder = ClayTestBuilder(folder,
+            clay_path = options.clay_path,
+            output = options.output,
+            print_mode = options.print_mode)
+
+        builder.render()
+
+
+class ClayTestBuilder:
+    def __init__(self, folder_name, output = None, clay_path = None, print_mode = 'stdout'):
+        self.declarations = []
+        self.callbacks = []
+        self.suites = []
+        self.suite_list = []
+
+        self.clay_path = clay_path
+        self.print_mode = print_mode
+        self.output = output or os.path.join(folder_name, "clay_main.c")
+        self.output_header = os.path.join(folder_name, "clay.h")
+
+        self.modules = ["clay.c", "clay_sandbox.c"]
+
+        print("Loading test suites...")
+
+        file_list = os.listdir(folder_name)
+        for fname in fnmatch.filter(file_list, "*.c"):
+            with open(fname) as f:
+                tcount = self._process_test_file(fname, f.read())
+                if tcount:
+                    print ("  %s: %d tests" % (fname, tcount))
+
+    def render(self):
+        template = TEMPLATE_MAIN.substitute(
+            version = VERSION,
+            clay_print = self._get_print_method(),
+            clay_library = self._get_library(),
+            extern_declarations = "\n".join(self.declarations),
+
+            suites_str = ", ".join(self.suite_list),
+
+            test_callbacks = ",\n\t".join(self.callbacks),
+            cb_count = len(self.callbacks),
+
+            test_suites = ",\n\t".join(self.suites),
+            suite_count = len(self.suites),
+        )
+
+        with open(self.output, "w") as out:
+            out.write(template)
+
+        with open(self.output_header, "w") as out:
+            out.write(self._load_file('clay.h'))
+
+        print ('Written test suite to "%s"' % self.output)
+        print ('Written header to "%s"' % self.output_header)
+
+    #####################################################
+    # Internal methods
+    #####################################################
+    def _get_print_method(self):
+        return {
+                'stdout' : 'printf(__VA_ARGS__)',
+                'stderr' : 'fprintf(stderr, __VA_ARGS__)',
+                'silent' : ''
+        }[self.print_mode]
+
+    def _load_file(self, filename):
+        if self.clay_path:
+            filename = os.path.join(self.clay_path, filename)
+            with open(filename) as cfile:
+                return cfile.read()
+
+        else:
+            import zlib, base64, sys
+            content = CLAY_FILES[filename]
+
+            if sys.version_info >= (3, 0):
+                content = bytearray(content, 'utf_8')
+                content = base64.b64decode(content)
+                content = zlib.decompress(content)
+                return str(content)
+            else:
+                content = base64.b64decode(content)
+                return zlib.decompress(content)
+
+    def _get_library(self):
+        return "\n".join(self._load_file(f) for f in self.modules)
+
+    def _parse_comment(self, comment):
+        comment = comment[2:-2]
+        comment = comment.splitlines()
+        comment = [line.strip() for line in comment]
+        comment = "\n".join(comment)
+
+        return comment
+
+    def _process_test_file(self, file_name, contents):
+        file_name = os.path.basename(file_name)
+        file_name, _ = os.path.splitext(file_name)
+
+        regex_string = TEST_FUNC_REGEX % file_name
+        regex = re.compile(regex_string, re.MULTILINE)
+
+        callbacks = []
+        initialize = cleanup = "{NULL, NULL, 0}"
+
+        for (declaration, symbol, short_name, _) in regex.findall(contents):
+            self.declarations.append("extern %s;" % declaration)
+            func_ptr = '{"%s", &%s, %d}' % (
+                short_name, symbol, len(self.suites)
+            )
+
+            if short_name == 'initialize':
+                initialize = func_ptr
+            elif short_name == 'cleanup':
+                cleanup = func_ptr
+            else:
+                callbacks.append(func_ptr)
+
+        if not callbacks:
+            return 0
+
+        suite = TEMPLATE_SUITE.substitute(
+            clean_name = file_name,
+            initialize = initialize,
+            cleanup = cleanup,
+            cb_ptr = "&_all_callbacks[%d]" % len(self.callbacks),
+            cb_count = len(callbacks)
+        ).strip()
+
+        self.callbacks += callbacks
+        self.suites.append(suite)
+        self.suite_list.append(file_name)
+
+        return len(callbacks)
+
+CLAY_FILES = {
+"clay.c" : r"""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""",
+"clay_sandbox.c" : r"""eJx1VG1P2zAQ/uz8iiPVSrKGETa0acr6YRJlqii0akFMopUVEgesJU6J3Wps8N93jkNeCquiJvE9d/c895IeT2KWAL0eX3z6aPUIvnDBYDb5fnk6nZ/TxWgG+8vlvtVjqWT/Axxqu4h5YllShYpHEN2HBdAoDR/pOlT3N8f+18+roDZzoSwu6TZMeUxVti5BTpQLqYzre33gWn8tIlWxiRRoR/wLLFIwtSkEOKk+cjTOg75ULgyH4EO/bxHiLOh4cTKeO1J9kIpmecxceHpCizZMLs7aBtf4hFHEpKziXdPpmQnoBtZzhzbKb1Muyd5ukoQVHkj+h1EFKRN3NXtTjlJZZd6GBY3yjVAwhONgB2MCMrHF4hTyZoUYDEPsy/MZCrI90E/lbWTuV4vRfDafno4nI9sC8oxFBlJl4vjS63SY8AScH0xdsmw90wKck+vp/MQ1lD0wSlwXU1Z1Pgrq3pIkL8DhSMkPgMO3RkkAgwF3S6Y7MhB8xxQ+OLUmvsKikpLJHh7qXNpLcbFhSNhYXk+HhpoUeiZEtH50XuqOJu+l6jp0izshzxZe9dj4O/2EKGWhoEmexqx4ewJbh7mkURbrruzWldhFDIcSDh9gab+TS9sOqp3RpgwOiqRtqApaBo3yLAtF/LIjRIp1gcwSpzKYucrrd9ereHhQcmx2Qj5KxbIa15a6zXkM5T5uhETjbf7b0WdGYbsGzdbuzr7xf+3d1Ed7URXytBxcu8TrBv4sf7YWV6+IbnU5A92NatLXuluM2pPp6wgYCDPhQOBDl7qJ3voGaegBHK1gb9j5fpmh6gIHAy2gjQrMGGEmPXkNGgZajNdIr1Nnv7AZ6zYp/UW5uJpMdkUYdMyLjnr/i++7mqz/Fj667+LfRjaLgOT/ATlX1I8=""",
+"clay.h" : r"""eJy9lF9PwjAUxZ/Zp7huLxtZCL4ikhgDkYT4IonxqSntnTTWdvaP4re33TA6HCG+8LTb2/b+zjnZlolKcayAkNvVzRNZzx/W5I6QJAtNofBPP8mEYtJzhKl1XIrNaDtLknctODBJPwmh1qJxeTIQygHTigsntCqTQaht6GypgWElJJbtERkoB7tojDYHPY6WGVHvZ8WLdqu95IRutHHF1W8NFh1hEqnydd508+F+WbTrYVFCW+iavnmM178NNxNevXWkDlZy3NWmhEgvugabnQJm1zAuQ0qL5WpOSKxWy/umShdesagXGJUSKho88wmkkP3MLOGy6CHHsyfJ06Pg+a5G5pBD1VHgdCPipIQ95hT/4rjzIMCgtZEsLCjtwBmPfeAumW2RvZwn93HRhz5v8L0azpD7+DD3xnYP8RhnjeG7bIMdpcdeow9qlFDP/5n72F4B7k18uIjBHTIfs5ykHf0Y/ixV8gUUh4yr"""
+}
+
+if __name__ == '__main__':
+    main()
+
diff --git a/tests/clay_main.c b/tests/clay_main.c
index 059b66e..738d79e 100644
--- a/tests/clay_main.c
+++ b/tests/clay_main.c
@@ -1,6 +1,6 @@
 
 /*
- * Clay v0.3.0
+ * Clay v0.7.0
  *
  * This is an autogenerated file. Do not modify.
  * To add new unit tests or suites, regenerate the whole