--- /dev/null
+#define _POSIX_C_SOURCE 200809L
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>
+#include <unistd.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <regex.h>
+
+#define GREATER(x,y) ( ((x) > (y)) ? (x) : (y) )
+
+struct result {
+ int test; /* i.e. the number of the test */
+ char *name; /* name of the test, malloc */
+ int pass; /* if the test passed */
+ struct result *next; /* so we can make a linked list */
+};
+
+struct testrun {
+ char *name;
+ int version;
+ int plan;
+ int ran;
+ int pass;
+ int fail;
+ int skip;
+ int todo, todo_pass, todo_fail;
+ int bailout;
+ struct result *results;
+ struct result *last; /* last result, so we append rather than push */
+ int interactive;
+};
+
+int lookfor(char *needle, char *haystack, size_t n) {
+ return strncasecmp(needle, haystack, n) == 0;
+}
+
+int rmatch(const regex_t *preg, const char *string, regmatch_t pmatch[]) {
+ return regexec(preg, string, 4, pmatch, 0) == 0;
+}
+
+/* write n backspaces to stdout */
+void backup(int n) {
+ while (n--) putchar('\b');
+}
+
+void spaces(int n) {
+ while (n--) putchar(' ');
+}
+
+int runone(struct testrun *run) {
+ int pipefd[2];
+ pid_t cpid;
+ FILE *tap;
+ char *line;
+ ssize_t nread;
+ size_t len = 0;
+ int written = 0;
+
+ if (pipe(pipefd) == -1) {
+ perror("pipe");
+ exit(EXIT_FAILURE);
+ }
+
+ cpid = fork();
+ if (cpid == -1) {
+ perror("fork");
+ exit(EXIT_FAILURE);
+ }
+
+ if (cpid == 0) {
+ /* child */
+ close(pipefd[0]); /* close read end */
+ close(1); /* close stdout */
+ dup(pipefd[1]); /* set stdout to the write end of the pipe */
+ execlp(run->name, run->name, NULL);
+ /* only gets here if exec failed */
+ printf("Bail Out! exec %s failed: %s\n", run->name, strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+
+ close(pipefd[1]);
+ /* parent continue on */
+ tap = fdopen(pipefd[0], "r");
+
+ regex_t plan, diagnostic, bail, test, directive, version;
+ regmatch_t match[4];
+
+ regcomp(&plan, "^[[:digit:]]+\\.\\.([[:digit:]]+)", REG_EXTENDED);
+ regcomp(&diagnostic, "#[[:space:]]*(.+)", REG_EXTENDED);
+ regcomp(&bail, "^Bail Out![[:space:]]*(.+)", REG_EXTENDED);
+ regcomp(&version, "^TAP Version ([[:digit:]]+)", REG_EXTENDED);
+
+ regcomp(&test, "^(not )?ok([[:space:]]*([[:digit:]]+))?", REG_EXTENDED);
+ regcomp(&directive, "#[[:space:]]*([^[:space:]]+)([[:space:]]+(.+))?",
+ REG_EXTENDED|REG_ICASE);
+
+
+ /* description: ok \d+([^#]+) */
+ /* directive: #.+ */
+ /* todo: # todo */
+ /* skip: # skip */
+
+ /* bail out: ^Bail out! */
+ /* diagnostic: ^#.+ */
+ /* start of yaml: ^\s+--- */
+ /* end of yaml: ^\s+... */
+ /* between yaml lines must be indented */
+ /* anything else is an error */
+
+ while ((nread = getline(&line, &len, tap)) != -1) {
+ /* version: ^TAP version \d+ */
+
+ /* plan: ^\d+\.\.\d+ */
+ if (regexec(&plan, line, 2, match, 0) != REG_NOMATCH) {
+ run->plan = atoi(line+match[1].rm_so);
+ /* TODO plans can have a diag skip reason */
+ } else
+
+ /* ok: ^ok \d+ */
+ /* not ok: ^not ok \d+ */
+ if (rmatch(&test, line, match)) {
+ int test;
+ int todo = 0;
+ int skip = 0;
+ int pass = 0;
+
+ run->ran++;
+
+ if (run->interactive) {
+ backup(written);
+ written = printf("%d/%d", run->ran, run->plan);
+ fflush(stdout);
+ }
+
+
+ /* if the "not" match fails, the test passed */
+ pass = match[1].rm_so == -1;
+
+ /* test number */
+ test = atoi(line+match[3].rm_so);
+
+ /* directive */
+ if (rmatch(&directive, line, match)) {
+ todo = lookfor("todo", line+match[1].rm_so, 4);
+ skip = lookfor("skip", line+match[1].rm_so, 4);
+ }
+
+ /* check for diagnostics */
+ if (test != run->ran) {
+ fprintf(stderr, "expected test %d, got %d\n", run->ran+1, test);
+ }
+
+ /* pass, todo, or skip are all pass */
+ if (pass || todo || skip) {
+ run->pass++;
+ } else {
+ run->fail++;
+ }
+
+ if (todo && pass) {
+ run->todo_pass++;
+ }
+
+ if (skip) {
+ run->skip++;
+ }
+
+ if (!pass && !todo) {
+ struct result *r;
+ r = malloc(sizeof *r);
+
+ r->test = run->ran;
+ r->pass = 0;
+ if (run->last) {
+ run->last->next = r;
+ }
+ if (!run->results) {
+ run->results = r;
+ }
+ run->last = r;
+ /* push test number onto fail list */
+ }
+
+ }
+ else if (rmatch(&diagnostic, line, match)) {
+
+ }
+ else if (rmatch(&version, line, match)) {
+ run->version = atoi(line + match[1].rm_so);
+ }
+ else if (rmatch(&bail, line, match)) {
+ run->bailout = 1;
+ }
+ else {
+ printf("Unknown line: %s", line);
+ }
+
+ /* not ok: ^not ok \d+ */
+ /* description: ok \d+([^#]+) */
+ /* directive: #.+ */
+ /* todo: # todo */
+ /* skip: # skip */
+
+ /* bail out: ^Bail out! */
+ /* diagnostic: ^#.+ */
+ /* start of yaml: ^\s+--- */
+ /* end of yaml: ^\s+... */
+ /* between yaml lines must be indented */
+ /* anything else is an error */
+
+ }
+
+ regfree(&plan);
+ regfree(&test);
+
+ free(line);
+ fclose(tap);
+ wait(NULL);
+
+ if (run->ran < run->plan) {
+ run->fail += run->plan - run->ran;
+ }
+
+ if (run->interactive) {
+ backup(written);
+ spaces(written);
+ backup(written);
+ fflush(stdout);
+ }
+
+ return 0;
+}
+
+void json(struct testrun *total, struct testrun *runs, int nruns) {
+ if (total && runs && nruns) {
+ return;
+ }
+ return;
+}
+
+int main(int ac, char *av[]) {
+ int maxnamelen = 0;
+ ssize_t len;
+ int i, j;
+ struct testrun total = { 0 };
+ struct testrun run = { 0 };
+ struct testrun clear = { 0 };
+ struct testrun *runs;
+ int opt;
+
+ /*
+ * -j json output
+ * -p progress output
+ * -i interactive, regardless of tty
+ * -s summary text
+ * -d detail text
+ */
+
+ int interactive = 0, json = 0, summary = 1, progress = 1, detail = 0;
+
+ interactive = isatty(1);
+
+ /* TODO -jj turn off everything else? */
+ /* TODO -cC color escapes */
+ while ((opt = getopt(ac, av, "jJpPiIsSdD")) != -1) {
+ switch (opt) {
+ case 'j': json = 1; break;
+ case 'J': json = 0; break;
+ case 'p': progress = 1; break;
+ case 'P': progress = 0; break;
+ case 'i': interactive = 1; break;
+ case 'I': interactive = 0; break;
+ case 's': summary = 1; break;
+ case 'S': summary = 0; break;
+ case 'd': detail = 1; break;
+ case 'D': detail = 0; break;
+ default: /* '?' */
+ fprintf(stderr, "Usage: %s [-jpisd] file...\n",
+ av[0]);
+ exit(EXIT_FAILURE);
+ }
+ }
+
+ runs = malloc(ac * sizeof *runs);
+
+ for (i=optind; i<ac; i++) {
+ len = strlen(av[i]);
+ maxnamelen = GREATER(len, maxnamelen);
+ }
+
+ for (i=optind; i<ac; i++) {
+ run = clear;
+ run.name = av[i];
+ run.interactive = interactive;
+
+ if (progress) {
+ printf("%s ", run.name);
+ len = strlen(run.name);
+ for (j=len;j<maxnamelen+2;j++) {
+ putchar('.');
+ }
+ putchar(' ');
+ fflush(stdout);
+ }
+
+ runone(&run);
+ total.plan += run.plan;
+ total.ran += run.ran;
+ total.pass += run.pass;
+ total.fail += run.fail;
+ total.skip += run.skip;
+
+ if (progress) {
+ if (run.bailout) {
+ printf("Bail out!\n");
+ } else {
+ if (run.fail > 0 ) printf("not ");
+ printf("ok\n");
+ }
+ }
+
+ runs[i-optind] = run;
+ }
+
+ if (summary) {
+ printf("ran: %d/%d, pass: %d, fail: %d, skip: %d",
+ total.ran, total.plan, total.pass, total.fail,
+ total.skip
+ );
+ if (total.plan > 0) {
+ printf(", %.2f%% ok",
+ 100.0*(double)total.pass/(double)total.plan
+ );
+ }
+ printf("\n");
+
+ }
+
+ if (detail) {
+ printf("detail here\n");
+ }
+
+ if (json) {
+ printf("{ \"plan\": %d, \"ran\": %d, \"pass\": %d, \"fail\": %d }\n",
+ total.plan, total.ran, total.pass, total.fail
+ );
+ }
+
+#if 0
+ Files=8, Tests=508, 0 wallclock secs ( 0.08 usr + 0.01 sys = 0.09 CPU)
+Result: PASS
+#endif
+
+ return total.fail ? EXIT_FAILURE : EXIT_SUCCESS;
+}