]> git.xonotic.org Git - xonotic/gmqcc.git/blob - test.cpp
Use std::vector for static_names
[xonotic/gmqcc.git] / test.cpp
1 #include <vector>
2
3 #include <stdlib.h>
4 #include <string.h>
5 #include <stdio.h>
6
7 #include <sys/types.h>
8 #include <sys/wait.h>
9 #include <sys/stat.h>
10
11 #include <dirent.h>
12 #include <unistd.h>
13
14 #include "gmqcc.h"
15
16 static const char *task_bins[] = {
17     "./gmqcc",
18     "./qcvm"
19 };
20
21 struct popen_t {
22     FILE *handles[3];
23     int pipes[3];
24     int stderr_fd;
25     int stdout_fd;
26     int pid;
27 };
28
29 static FILE **task_popen(const char *command, const char *mode) {
30     int     inhandle  [2];
31     int     outhandle [2];
32     int     errhandle [2];
33     int     trypipe;
34
35     popen_t *data = (popen_t*)mem_a(sizeof(popen_t));
36
37     char *line = (char*)command;
38     std::vector<char *> argv;
39     {
40
41         while (*line != '\0') {
42             while (*line == ' ' || *line == '\t' || *line == '\n')
43                 *line++ = '\0';
44             argv.push_back(line);
45             while (*line != '\0' && *line != ' ' &&
46                    *line != '\t' && *line != '\n') line++;
47         }
48         argv.push_back((char *)0);
49     }
50
51     if ((trypipe = pipe(inhandle))  < 0) goto task_popen_error_0;
52     if ((trypipe = pipe(outhandle)) < 0) goto task_popen_error_1;
53     if ((trypipe = pipe(errhandle)) < 0) goto task_popen_error_2;
54
55     if ((data->pid = fork()) > 0) {
56         /* parent */
57         close(inhandle [0]);
58         close(outhandle [1]);
59         close(errhandle [1]);
60         data->pipes[0] = inhandle [1];
61         data->pipes[1] = outhandle[0];
62         data->pipes[2] = errhandle[0];
63         data->handles[0] = fdopen(inhandle [1], "w");
64         data->handles[1] = fdopen(outhandle[0], mode);
65         data->handles[2] = fdopen(errhandle[0], mode);
66         return data->handles;
67     } else if (data->pid == 0) {
68         /* child */
69         close(inhandle [1]);
70         close(outhandle[0]);
71         close(errhandle[0]);
72
73         /* see piping documentation for this sillyness :P */
74         dup2(inhandle [0], 0);
75         dup2(outhandle[1], 1);
76         dup2(errhandle[1], 2);
77
78         execvp(argv[0], &argv[0]);
79         exit(EXIT_FAILURE);
80     } else {
81         /* fork failed */
82         goto task_popen_error_3;
83     }
84
85 task_popen_error_3: close(errhandle[0]), close(errhandle[1]);
86 task_popen_error_2: close(outhandle[0]), close(outhandle[1]);
87 task_popen_error_1: close(inhandle [0]), close(inhandle [1]);
88 task_popen_error_0:
89
90     return NULL;
91 }
92
93 static int task_pclose(FILE **handles) {
94     popen_t *data   = (popen_t*)handles;
95     int      status = 0;
96
97     close(data->pipes[0]); /* stdin  */
98     close(data->pipes[1]); /* stdout */
99     close(data->pipes[2]); /* stderr */
100
101     waitpid(data->pid, &status, 0);
102
103     mem_d(data);
104
105     return status;
106 }
107
108 #define TASK_COMPILE    0
109 #define TASK_EXECUTE    1
110 /*
111  * Task template system:
112  *  templates are rules for a specific test, used to create a "task" that
113  *  is executed with those set of rules (arguments, and what not). Tests
114  *  that don't have a template with them cannot become tasks, since without
115  *  the information for that test there is no way to properly "test" them.
116  *  Rules for these templates are described in a template file, using a
117  *  task template language.
118  *
119  *  The language is a basic finite statemachine, top-down single-line
120  *  description language.
121  *
122  *  The languge is composed entierly of "tags" which describe a string of
123  *  text for a task.  Think of it much like a configuration file.  Except
124  *  it's been designed to allow flexibility and future support for prodecual
125  *  semantics.
126  *
127  *  The following "tags" are suported by the language
128  *
129  *      D:
130  *          Used to set a description of the current test, this must be
131  *          provided, this tag is NOT optional.
132  *
133  *      T:
134  *          Used to set the procedure for the given task, there are two
135  *          options for this:
136  *              -compile
137  *                  This simply performs compilation only
138  *              -execute
139  *                  This will perform compilation and execution
140  *              -fail
141  *                  This will perform compilation, but requires
142  *                  the compilation to fail in order to succeed.
143  *
144  *          This must be provided, this tag is NOT optional.
145  *
146  *      C:
147  *          Used to set the compilation flags for the given task, this
148  *          must be provided, this tag is NOT optional.
149  *
150  *      F:  Used to set some test suite flags, currently the only option
151  *          is -no-defs (to including of defs.qh)
152  *
153  *      E:
154  *          Used to set the execution flags for the given task. This tag
155  *          must be provided if T == -execute, otherwise it's erroneous
156  *          as compilation only takes place.
157  *
158  *      M:
159  *          Used to describe a string of text that should be matched from
160  *          the output of executing the task.  If this doesn't match the
161  *          task fails.  This tag must be provided if T == -execute, otherwise
162  *          it's erroneous as compilation only takes place.
163  *
164  *      I:
165  *          Used to specify the INPUT source file to operate on, this must be
166  *          provided, this tag is NOT optional
167  *
168  *
169  *  Notes:
170  *      These tags have one-time use, using them more than once will result
171  *      in template compilation errors.
172  *
173  *      Lines beginning with # or // in the template file are comments and
174  *      are ignored by the template parser.
175  *
176  *      Whitespace is optional, with exception to the colon ':' between the
177  *      tag and it's assignment value/
178  *
179  *      The template compiler will detect erronrous tags (optional tags
180  *      that need not be set), as well as missing tags, and error accordingly
181  *      this will result in the task failing.
182  */
183 struct task_template_t {
184     char *description;
185     char *compileflags;
186     char *executeflags;
187     char *proceduretype;
188     char *sourcefile;
189     char *tempfilename;
190     std::vector<char *> comparematch;
191     char *rulesfile;
192     char *testflags;
193 };
194
195 /*
196  * This is very much like a compiler code generator :-).  This generates
197  * a value from some data observed from the compiler.
198  */
199 static bool task_template_generate(task_template_t *tmpl, char tag, const char *file, size_t line, char *value, size_t *pad) {
200     size_t desclen = 0;
201     size_t filelen = 0;
202     char **destval = NULL;
203
204     if (!tmpl)
205         return false;
206
207     switch(tag) {
208         case 'D': destval = &tmpl->description;    break;
209         case 'T': destval = &tmpl->proceduretype;  break;
210         case 'C': destval = &tmpl->compileflags;   break;
211         case 'E': destval = &tmpl->executeflags;   break;
212         case 'I': destval = &tmpl->sourcefile;     break;
213         case 'F': destval = &tmpl->testflags;      break;
214         default:
215             con_printmsg(LVL_ERROR, __FILE__, __LINE__, 0, "internal error",
216                 "invalid tag `%c:` during code generation\n",
217                 tag
218             );
219             return false;
220     }
221
222     /*
223      * Ensure if for the given tag, there already exists a
224      * assigned value.
225      */
226     if (*destval) {
227         con_printmsg(LVL_ERROR, file, line, 0, /*TODO: column for match*/ "compile error",
228             "tag `%c:` already assigned value: %s\n",
229             tag, *destval
230         );
231         return false;
232     }
233
234     /*
235      * Strip any whitespace that might exist in the value for assignments
236      * like "D:      foo"
237      */
238     if (value && *value && (*value == ' ' || *value == '\t'))
239         value++;
240     else if (!value)
241         exit(EXIT_FAILURE);
242
243     /*
244      * Value will contain a newline character at the end, we need to strip
245      * this otherwise kaboom, seriously, kaboom :P
246      */
247     if (strchr(value, '\n'))
248         *strrchr(value, '\n')='\0';
249
250     /*
251      * Now allocate and set the actual value for the specific tag. Which
252      * was properly selected and can be accessed with *destval.
253      */
254     *destval = util_strdup(value);
255
256
257     if (*destval == tmpl->description) {
258         /*
259          * Create some padding for the description to align the
260          * printing of the rules file.
261          */
262         if ((desclen = strlen(tmpl->description)) > pad[0])
263             pad[0] = desclen;
264     }
265
266     if ((filelen = strlen(file)) > pad[2])
267         pad[2] = filelen;
268
269     return true;
270 }
271
272 static bool task_template_parse(const char *file, task_template_t *tmpl, FILE *fp, size_t *pad) {
273     char  *data = NULL;
274     char  *back = NULL;
275     size_t size = 0;
276     size_t line = 1;
277
278     if (!tmpl)
279         return false;
280
281     /* top down parsing */
282     while (util_getline(&back, &size, fp) != EOF) {
283         /* skip whitespace */
284         data = back;
285         if (*data && (*data == ' ' || *data == '\t'))
286             data++;
287
288         switch (*data) {
289             /*
290              * Handle comments inside task tmpl files.  We're strict
291              * about the language for fun :-)
292              */
293             case '/':
294                 if (data[1] != '/') {
295                     con_printmsg(LVL_ERROR, file, line, 0, /*TODO: column for match*/ "tmpl parse error",
296                         "invalid character `/`, perhaps you meant `//` ?");
297
298                     mem_d(back);
299                     return false;
300                 }
301             case '#':
302                 break;
303
304             /*
305              * Empty newlines are acceptable as well, so we handle that here
306              * despite being just odd since there should't be that many
307              * empty lines to begin with.
308              */
309             case '\r':
310             case '\n':
311                 break;
312
313
314             /*
315              * Now begin the actual "tag" stuff.  This works as you expect
316              * it to.
317              */
318             case 'D':
319             case 'T':
320             case 'C':
321             case 'E':
322             case 'I':
323             case 'F':
324                 if (data[1] != ':') {
325                     con_printmsg(LVL_ERROR, file, line, 0, /*TODO: column for match*/ "tmpl parse error",
326                         "expected `:` after `%c`",
327                         *data
328                     );
329                     goto failure;
330                 }
331                 if (!task_template_generate(tmpl, *data, file, line, &data[3], pad)) {
332                     con_printmsg(LVL_ERROR, file, line, 0, /*TODO: column for match*/ "tmpl compile error",
333                         "failed to generate for given task\n"
334                     );
335                     goto failure;
336                 }
337                 break;
338
339             /*
340              * Match requires it's own system since we allow multiple M's
341              * for multi-line matching.
342              */
343             case 'M':
344             {
345                 char *value = &data[3];
346                 if (data[1] != ':') {
347                     con_printmsg(LVL_ERROR, file, line, 0, /*TODO: column for match*/ "tmpl parse error",
348                         "expected `:` after `%c`",
349                         *data
350                     );
351                     goto failure;
352                 }
353
354                 /*
355                  * Value will contain a newline character at the end, we need to strip
356                  * this otherwise kaboom, seriously, kaboom :P
357                  */
358                 if (strrchr(value, '\n'))
359                     *strrchr(value, '\n')='\0';
360                 else /* cppcheck: possible null pointer dereference */
361                     exit(EXIT_FAILURE);
362
363                 tmpl->comparematch.push_back(util_strdup(value));
364
365                 break;
366             }
367
368             default:
369                 con_printmsg(LVL_ERROR, file, line, 0, /*TODO: column for match*/ "tmpl parse error",
370                     "invalid tag `%c`", *data
371                 );
372                 goto failure;
373             /* no break required */
374         }
375
376         /* update line and free old sata */
377         line++;
378         mem_d(back);
379         back = NULL;
380     }
381     if (back)
382         mem_d(back);
383     return true;
384
385 failure:
386     mem_d (back);
387     return false;
388 }
389
390 /*
391  * Nullifies the template data: used during initialization of a new
392  * template and free.
393  */
394 static void task_template_nullify(task_template_t *tmpl) {
395     if (!tmpl)
396         return;
397
398     tmpl->description = NULL;
399     tmpl->proceduretype = NULL;
400     tmpl->compileflags = NULL;
401     tmpl->executeflags = NULL;
402     tmpl->sourcefile = NULL;
403     tmpl->tempfilename = NULL;
404     tmpl->rulesfile = NULL;
405     tmpl->testflags = NULL;
406 }
407
408 static task_template_t *task_template_compile(const char *file, const char *dir, size_t *pad) {
409     /* a page should be enough */
410     char             fullfile[4096];
411     size_t           filepadd = 0;
412     FILE       *tempfile = NULL;
413     task_template_t *tmpl     = NULL;
414
415     util_snprintf(fullfile, sizeof(fullfile), "%s/%s", dir, file);
416
417     tempfile = fopen(fullfile, "r");
418     tmpl = (task_template_t*)mem_a(sizeof(task_template_t));
419     new (tmpl) task_template_t();
420     task_template_nullify(tmpl);
421
422     /*
423      * Create some padding for the printing to align the
424      * printing of the rules file to the console.
425      */
426     if ((filepadd = strlen(fullfile)) > pad[1])
427         pad[1] = filepadd;
428
429     tmpl->rulesfile = util_strdup(fullfile);
430
431     /*
432      * Esnure the file even exists for the task, this is pretty useless
433      * to even do.
434      */
435     if (!tempfile) {
436         con_err("template file: %s does not exist or invalid permissions\n",
437             file
438         );
439         goto failure;
440     }
441
442     if (!task_template_parse(file, tmpl, tempfile, pad)) {
443         con_err("template parse error: error during parsing\n");
444         goto failure;
445     }
446
447     /*
448      * Regardless procedure type, the following tags must exist:
449      *  D
450      *  T
451      *  C
452      *  I
453      */
454     if (!tmpl->description) {
455         con_err("template compile error: %s missing `D:` tag\n", file);
456         goto failure;
457     }
458     if (!tmpl->proceduretype) {
459         con_err("template compile error: %s missing `T:` tag\n", file);
460         goto failure;
461     }
462     if (!tmpl->compileflags) {
463         con_err("template compile error: %s missing `C:` tag\n", file);
464         goto failure;
465     }
466     if (!tmpl->sourcefile) {
467         con_err("template compile error: %s missing `I:` tag\n", file);
468         goto failure;
469     }
470
471     /*
472      * Now lets compile the template, compilation is really just
473      * the process of validating the input.
474      */
475     if (!strcmp(tmpl->proceduretype, "-compile")) {
476         if (tmpl->executeflags)
477             con_err("template compile warning: %s erroneous tag `E:` when only compiling\n", file);
478         if (tmpl->comparematch.size())
479             con_err("template compile warning: %s erroneous tag `M:` when only compiling\n", file);
480         goto success;
481     } else if (!strcmp(tmpl->proceduretype, "-execute")) {
482         if (!tmpl->executeflags) {
483             /* default to $null */
484             tmpl->executeflags = util_strdup("$null");
485         }
486         if (tmpl->comparematch.empty()) {
487             con_err("template compile error: %s missing `M:` tag (use `$null` for exclude)\n", file);
488             goto failure;
489         }
490     } else if (!strcmp(tmpl->proceduretype, "-fail")) {
491         if (tmpl->executeflags)
492             con_err("template compile warning: %s erroneous tag `E:` when only failing\n", file);
493         if (tmpl->comparematch.size())
494             con_err("template compile warning: %s erroneous tag `M:` when only failing\n", file);
495     } else if (!strcmp(tmpl->proceduretype, "-diagnostic")) {
496         if (tmpl->executeflags)
497             con_err("template compile warning: %s erroneous tag `E:` when only diagnostic\n", file);
498         if (tmpl->comparematch.empty()) {
499             con_err("template compile error: %s missing `M:` tag (use `$null` for exclude)\n", file);
500             goto failure;
501         }
502     } else if (!strcmp(tmpl->proceduretype, "-pp")) {
503         if (tmpl->executeflags)
504             con_err("template compile warning: %s erroneous tag `E:` when only preprocessing\n", file);
505         if (tmpl->comparematch.empty()) {
506             con_err("template compile error: %s missing `M:` tag (use `$null` for exclude)\n", file);
507             goto failure;
508         }
509     } else {
510         con_err("template compile error: %s invalid procedure type: %s\n", file, tmpl->proceduretype);
511         goto failure;
512     }
513
514 success:
515     fclose(tempfile);
516     return tmpl;
517
518 failure:
519     /*
520      * The file might not exist and we jump here when that doesn't happen
521      * so the check to see if it's not null here is required.
522      */
523     if (tempfile)
524         fclose(tempfile);
525     mem_d(tmpl);
526
527     return NULL;
528 }
529
530 static void task_template_destroy(task_template_t *tmpl) {
531     if (!tmpl)
532         return;
533
534     if (tmpl->description)    mem_d(tmpl->description);
535     if (tmpl->proceduretype)  mem_d(tmpl->proceduretype);
536     if (tmpl->compileflags)   mem_d(tmpl->compileflags);
537     if (tmpl->executeflags)   mem_d(tmpl->executeflags);
538     if (tmpl->sourcefile)     mem_d(tmpl->sourcefile);
539     if (tmpl->rulesfile)      mem_d(tmpl->rulesfile);
540     if (tmpl->testflags)      mem_d(tmpl->testflags);
541
542
543     for (auto &it : tmpl->comparematch)
544         mem_d(it);
545
546     /*
547      * Nullify all the template members otherwise NULL comparision
548      * checks will fail if tmpl pointer is reused.
549      */
550     mem_d(tmpl->tempfilename);
551     mem_d(tmpl);
552 }
553
554 /*
555  * Now comes the task manager, this system allows adding tasks in and out
556  * of a task list.  This is the executor of the tasks essentially as well.
557  */
558 struct task_t {
559     task_template_t *tmpl;
560     FILE **runhandles;
561     FILE *stderrlog;
562     FILE *stdoutlog;
563     char *stdoutlogfile;
564     char *stderrlogfile;
565     bool compiled;
566 };
567
568 static std::vector<task_t> task_tasks;
569
570 /*
571  * Read a directory and searches for all template files in it
572  * which is later used to run all tests.
573  */
574 static bool task_propagate(const char *curdir, size_t *pad, const char *defs) {
575     bool  success = true;
576     DIR *dir;
577     struct dirent *files;
578     struct stat directory;
579     char buffer[4096];
580     size_t found = 0;
581     std::vector<char *> directories;
582     char *claim = util_strdup(curdir);
583
584     directories.push_back(claim);
585     dir = opendir(claim);
586
587     /*
588      * Generate a list of subdirectories since we'll be checking them too
589      * for tmpl files.
590      */
591     while ((files = readdir(dir))) {
592         util_asprintf(&claim, "%s/%s", curdir, files->d_name);
593         if (stat(claim, &directory) == -1) {
594             closedir(dir);
595             mem_d(claim);
596             return false;
597         }
598
599         if (S_ISDIR(directory.st_mode) && files->d_name[0] != '.') {
600             directories.push_back(claim);
601         } else {
602             mem_d(claim);
603             claim = NULL;
604         }
605     }
606     closedir(dir);
607
608     /*
609      * Now do all the work, by touching all the directories inside
610      * test as well and compile the task templates into data we can
611      * use to run the tests.
612      */
613     for (auto &it : directories) {
614         dir = opendir(it);
615         while ((files = readdir(dir))) {
616             util_snprintf(buffer, sizeof(buffer), "%s/%s", it, files->d_name);
617             if (stat(buffer, &directory) == -1) {
618                 con_err("internal error: stat failed, aborting\n");
619                 abort();
620             }
621
622             if (S_ISDIR(directory.st_mode))
623                 continue;
624
625             /*
626              * We made it here, which concludes the file/directory is not
627              * actually a directory, so it must be a file :)
628              */
629             if (strcmp(files->d_name + strlen(files->d_name) - 5, ".tmpl") == 0) {
630                 task_template_t *tmpl = task_template_compile(files->d_name, it, pad);
631                 char             buf[4096]; /* one page should be enough */
632                 const char      *qcflags = NULL;
633                 task_t           task;
634
635                 found ++;
636                 if (!tmpl) {
637                     con_err("error compiling task template: %s\n", files->d_name);
638                     success = false;
639                     continue;
640                 }
641                 /*
642                  * Generate a temportary file name for the output binary
643                  * so we don't trample over an existing one.
644                  */
645                 tmpl->tempfilename = NULL;
646                 util_asprintf(&tmpl->tempfilename, "%s/TMPDAT.%s.dat", it, files->d_name);
647
648                 /*
649                  * Additional QCFLAGS enviroment variable may be used
650                  * to test compile flags for all tests.  This needs to be
651                  * BEFORE other flags (so that the .tmpl can override them)
652                  */
653                 qcflags = getenv("QCFLAGS");
654
655                 /*
656                  * Generate the command required to open a pipe to a process
657                  * which will be refered to with a handle in the task for
658                  * reading the data from the pipe.
659                  */
660                 if (strcmp(tmpl->proceduretype, "-pp")) {
661                     if (qcflags) {
662                         if (tmpl->testflags && !strcmp(tmpl->testflags, "-no-defs")) {
663                             util_snprintf(buf, sizeof(buf), "%s %s/%s %s %s -o %s",
664                                 task_bins[TASK_COMPILE],
665                                 it,
666                                 tmpl->sourcefile,
667                                 qcflags,
668                                 tmpl->compileflags,
669                                 tmpl->tempfilename
670                             );
671                         } else {
672                             util_snprintf(buf, sizeof(buf), "%s %s/%s %s/%s %s %s -o %s",
673                                 task_bins[TASK_COMPILE],
674                                 curdir,
675                                 defs,
676                                 it,
677                                 tmpl->sourcefile,
678                                 qcflags,
679                                 tmpl->compileflags,
680                                 tmpl->tempfilename
681                             );
682                         }
683                     } else {
684                         if (tmpl->testflags && !strcmp(tmpl->testflags, "-no-defs")) {
685                             util_snprintf(buf, sizeof(buf), "%s %s/%s %s -o %s",
686                                 task_bins[TASK_COMPILE],
687                                 it,
688                                 tmpl->sourcefile,
689                                 tmpl->compileflags,
690                                 tmpl->tempfilename
691                             );
692                         } else {
693                             util_snprintf(buf, sizeof(buf), "%s %s/%s %s/%s %s -o %s",
694                                 task_bins[TASK_COMPILE],
695                                 curdir,
696                                 defs,
697                                 it,
698                                 tmpl->sourcefile,
699                                 tmpl->compileflags,
700                                 tmpl->tempfilename
701                             );
702                         }
703                     }
704                 } else {
705                     /* Preprocessing (qcflags mean shit all here we don't allow them) */
706                     if (tmpl->testflags && !strcmp(tmpl->testflags, "-no-defs")) {
707                         util_snprintf(buf, sizeof(buf), "%s -E %s/%s %s -o %s",
708                             task_bins[TASK_COMPILE],
709                             it,
710                             tmpl->sourcefile,
711                             tmpl->compileflags,
712                             tmpl->tempfilename
713                         );
714                     } else {
715                         util_snprintf(buf, sizeof(buf), "%s -E %s/%s %s/%s %s -o %s",
716                             task_bins[TASK_COMPILE],
717                             curdir,
718                             defs,
719                             it,
720                             tmpl->sourcefile,
721                             tmpl->compileflags,
722                             tmpl->tempfilename
723                         );
724                     }
725                 }
726
727                 /*
728                  * The task template was compiled, now lets create a task from
729                  * the template data which has now been propagated.
730                  */
731                 task.tmpl = tmpl;
732                 if (!(task.runhandles = task_popen(buf, "r"))) {
733                     con_err("error opening pipe to process for test: %s\n", tmpl->description);
734                     success = false;
735                     continue;
736                 }
737
738                 /*
739                  * Open up some file desciptors for logging the stdout/stderr
740                  * to our own.
741                  */
742                 util_snprintf(buf, sizeof(buf), "%s.stdout", tmpl->tempfilename);
743                 task.stdoutlogfile = util_strdup(buf);
744                 if (!(task.stdoutlog = fopen(buf, "w"))) {
745                     con_err("error opening %s for stdout\n", buf);
746                     continue;
747                 }
748
749                 util_snprintf(buf, sizeof(buf), "%s.stderr", tmpl->tempfilename);
750                 task.stderrlogfile = util_strdup(buf);
751                 if (!(task.stderrlog = fopen(buf, "w"))) {
752                     con_err("error opening %s for stderr\n", buf);
753                     continue;
754                 }
755                 task_tasks.push_back(task);
756             }
757         }
758         closedir(dir);
759         mem_d(it); /* free claimed memory */
760     }
761     return success;
762 }
763
764 /*
765  * Task precleanup removes any existing temporary files or log files
766  * left behind from a previous invoke of the test-suite.
767  */
768 static void task_precleanup(const char *curdir) {
769     DIR     *dir;
770     struct dirent  *files;
771     char          buffer[4096];
772
773     dir = opendir(curdir);
774
775     while ((files = readdir(dir))) {
776         if (strstr(files->d_name, "TMP")     ||
777             strstr(files->d_name, ".stdout") ||
778             strstr(files->d_name, ".stderr") ||
779             strstr(files->d_name, ".dat"))
780         {
781             util_snprintf(buffer, sizeof(buffer), "%s/%s", curdir, files->d_name);
782             if (remove(buffer))
783                 con_err("error removing temporary file: %s\n", buffer);
784         }
785     }
786
787     closedir(dir);
788 }
789
790 static void task_destroy(void) {
791     /*
792      * Free all the data in the task list and finally the list itself
793      * then proceed to cleanup anything else outside the program like
794      * temporary files.
795      */
796     for (auto &it : task_tasks) {
797         /*
798          * Close any open handles to files or processes here.  It's mighty
799          * annoying to have to do all this cleanup work.
800          */
801         if (it.stdoutlog) fclose(it.stdoutlog);
802         if (it.stderrlog) fclose(it.stderrlog);
803
804         /*
805          * Only remove the log files if the test actually compiled otherwise
806          * forget about it (or if it didn't compile, and the procedure type
807          * was set to -fail (meaning it shouldn't compile) .. stil remove)
808          */
809         if (it.compiled || !strcmp(it.tmpl->proceduretype, "-fail")) {
810             if (remove(it.stdoutlogfile))
811                 con_err("error removing stdout log file: %s\n", it.stdoutlogfile);
812             if (remove(it.stderrlogfile))
813                 con_err("error removing stderr log file: %s\n", it.stderrlogfile);
814
815             (void)!remove(it.tmpl->tempfilename);
816         }
817
818         /* free util_strdup data for log files */
819         mem_d(it.stdoutlogfile);
820         mem_d(it.stderrlogfile);
821
822         task_template_destroy(it.tmpl);
823     }
824 }
825
826 /*
827  * This executes the QCVM task for a specificly compiled progs.dat
828  * using the template passed into it for call-flags and user defined
829  * messages IF the procedure type is -execute, otherwise it matches
830  * the preprocessor output.
831  */
832 static bool task_trymatch(task_t &task, std::vector<char *> &line) {
833     bool success = true;
834     bool process = true;
835     int retval = EXIT_SUCCESS;
836     FILE *execute;
837     char buffer[4096];
838     task_template_t *tmpl = task.tmpl;
839
840     memset(buffer,0,sizeof(buffer));
841
842     if (!strcmp(tmpl->proceduretype, "-execute")) {
843         /*
844          * Drop the execution flags for the QCVM if none where
845          * actually specified.
846          */
847         if (!strcmp(tmpl->executeflags, "$null")) {
848             util_snprintf(buffer,  sizeof(buffer), "%s %s",
849                 task_bins[TASK_EXECUTE],
850                 tmpl->tempfilename
851             );
852         } else {
853             util_snprintf(buffer,  sizeof(buffer), "%s %s %s",
854                 task_bins[TASK_EXECUTE],
855                 tmpl->executeflags,
856                 tmpl->tempfilename
857             );
858         }
859
860         execute = popen(buffer, "r");
861         if (!execute)
862             return false;
863     } else if (!strcmp(tmpl->proceduretype, "-pp")) {
864         /*
865          * we're preprocessing, which means we need to read int
866          * the produced file and do some really weird shit.
867          */
868         if (!(execute = fopen(tmpl->tempfilename, "r")))
869             return false;
870         process = false;
871     } else {
872         /*
873          * we're testing diagnostic output, which means it will be
874          * in runhandles[2] (stderr) since that is where the compiler
875          * puts it's errors.
876          */
877         if (!(execute = fopen(task.stderrlogfile, "r")))
878             return false;
879         process = false;
880     }
881
882     /*
883      * Now lets read the lines and compare them to the matches we expect
884      * and handle accordingly.
885      */
886     {
887         char  *data    = NULL;
888         size_t size    = 0;
889         size_t compare = 0;
890
891         while (util_getline(&data, &size, execute) != EOF) {
892             if (!strcmp(data, "No main function found\n")) {
893                 con_err("test failure: `%s` (No main function found) [%s]\n",
894                     tmpl->description,
895                     tmpl->rulesfile
896                 );
897                 if (!process)
898                     fclose(execute);
899                 else
900                     pclose((FILE*)execute);
901                 return false;
902             }
903
904             /*
905              * Trim newlines from data since they will just break our
906              * ability to properly validate matches.
907              */
908             if  (strrchr(data, '\n'))
909                 *strrchr(data, '\n') = '\0';
910
911             /*
912              * We remove the file/directory and stuff from the error
913              * match messages when testing diagnostics.
914              */
915             if(!strcmp(tmpl->proceduretype, "-diagnostic")) {
916                 if (strstr(data, "there have been errors, bailing out"))
917                     continue; /* ignore it */
918                 if (strstr(data, ": error: ")) {
919                     char *claim = util_strdup(data + (strstr(data, ": error: ") - data) + 9);
920                     mem_d(data);
921                     data = claim;
922                 }
923             }
924
925             /*
926              * We need to ignore null lines for when -pp is used (preprocessor), since
927              * the preprocessor is likely to create empty newlines in certain macro
928              * instantations, otherwise it's in the wrong nature to ignore empty newlines.
929              */
930             if (!strcmp(tmpl->proceduretype, "-pp") && !*data)
931                 continue;
932
933             if (tmpl->comparematch.size() > compare) {
934                 if (strcmp(data, tmpl->comparematch[compare++])) {
935                     success = false;
936                 }
937             } else {
938                 success = false;
939             }
940
941             line.push_back(data);
942
943             /* reset */
944             data = NULL;
945             size = 0;
946         }
947
948         if (compare != tmpl->comparematch.size())
949             success = false;
950
951         mem_d(data);
952         data = NULL;
953     }
954
955     if (process)
956         retval = pclose((FILE*)execute);
957     else
958         fclose(execute);
959
960     return success && retval == EXIT_SUCCESS;
961 }
962
963 static const char *task_type(task_template_t *tmpl) {
964     if (!strcmp(tmpl->proceduretype, "-pp"))
965         return "type: preprocessor";
966     if (!strcmp(tmpl->proceduretype, "-execute"))
967         return "type: execution";
968     if (!strcmp(tmpl->proceduretype, "-compile"))
969         return "type: compile";
970     if (!strcmp(tmpl->proceduretype, "-diagnostic"))
971         return "type: diagnostic";
972     return "type: fail";
973 }
974
975 /*
976  * This schedualizes all tasks and actually runs them individually
977  * this is generally easy for just -compile variants.  For compile and
978  * execution this takes more work since a task needs to be generated
979  * from thin air and executed INLINE.
980  */
981 #include <math.h>
982 static size_t task_schedualize(size_t *pad) {
983     char space[2][64];
984     bool execute = false;
985     char *data = NULL;
986     std::vector<char *> match;
987     size_t size = 0;
988     size_t i = 0;
989     size_t failed = 0;
990     int status = 0;
991
992     util_snprintf(space[0], sizeof(space[0]), "%d", (int)task_tasks.size());
993
994     for (auto &it : task_tasks) {
995         i++;
996         memset(space[1], 0, sizeof(space[1]));
997         util_snprintf(space[1], sizeof(space[1]), "%d", (int)(i));
998
999         con_out("test #%u %*s", i, strlen(space[0]) - strlen(space[1]), "");
1000
1001         /*
1002          * Generate a task from thin air if it requires execution in
1003          * the QCVM.
1004          */
1005
1006         /* diagnostic is not executed, but compare tested instead, like preproessor */
1007         execute = !! (!strcmp(it.tmpl->proceduretype, "-execute")) ||
1008                      (!strcmp(it.tmpl->proceduretype, "-pp"))      ||
1009                      (!strcmp(it.tmpl->proceduretype, "-diagnostic"));
1010
1011         /*
1012          * We assume it compiled before we actually compiled :).  On error
1013          * we change the value
1014          */
1015         it.compiled = true;
1016
1017         /*
1018          * Read data from stdout first and pipe that stuff into a log file
1019          * then we do the same for stderr.
1020          */
1021         while (util_getline(&data, &size, it.runhandles[1]) != EOF) {
1022             fputs(data, it.stdoutlog);
1023
1024             if (strstr(data, "failed to open file")) {
1025                 it.compiled = false;
1026                 execute                = false;
1027             }
1028         }
1029         while (util_getline(&data, &size, it.runhandles[2]) != EOF) {
1030             /*
1031              * If a string contains an error we just dissalow execution
1032              * of it in the vm.
1033              *
1034              * TODO: make this more percise, e.g if we print a warning
1035              * that refers to a variable named error, or something like
1036              * that .. then this will blowup :P
1037              */
1038             if (strstr(data, "error") && strcmp(it.tmpl->proceduretype, "-diagnostic")) {
1039                 execute                = false;
1040                 it.compiled = false;
1041             }
1042
1043             fputs(data, it.stderrlog);
1044             fflush(it.stderrlog); /* fast flush for read */
1045         }
1046
1047         if (!it.compiled && strcmp(it.tmpl->proceduretype, "-fail")) {
1048             con_out("failure:   `%s` %*s %*s\n",
1049                 it.tmpl->description,
1050                 (pad[0] + pad[1] - strlen(it.tmpl->description)) + (strlen(it.tmpl->rulesfile) - pad[1]),
1051                 it.tmpl->rulesfile,
1052                 (pad[1] + pad[2] - strlen(it.tmpl->rulesfile)) + (strlen("(failed to compile)") - pad[2]),
1053                 "(failed to compile)"
1054             );
1055             failed++;
1056             continue;
1057         }
1058
1059         status = task_pclose(it.runhandles);
1060         if ((!strcmp(it.tmpl->proceduretype, "-fail") && status == EXIT_SUCCESS)
1061         ||  ( strcmp(it.tmpl->proceduretype, "-fail") && status == EXIT_FAILURE)) {
1062             con_out("failure:   `%s` %*s %*s\n",
1063                 it.tmpl->description,
1064                 (pad[0] + pad[1] - strlen(it.tmpl->description)) + (strlen(it.tmpl->rulesfile) - pad[1]),
1065                 it.tmpl->rulesfile,
1066                 (pad[1] + pad[2] - strlen(it.tmpl->rulesfile)) + (strlen("(compiler didn't return exit success)") - pad[2]),
1067                 "(compiler didn't return exit success)"
1068             );
1069             failed++;
1070             continue;
1071         }
1072
1073         if (!execute) {
1074             con_out("succeeded: `%s` %*s %*s\n",
1075                 it.tmpl->description,
1076                 (pad[0] + pad[1] - strlen(it.tmpl->description)) + (strlen(it.tmpl->rulesfile) - pad[1]),
1077                 it.tmpl->rulesfile,
1078                 (pad[1] + pad[2] - strlen(it.tmpl->rulesfile)) + (strlen(task_type(it.tmpl)) - pad[2]),
1079                 task_type(it.tmpl)
1080
1081             );
1082             continue;
1083         }
1084
1085         /*
1086          * If we made it here that concludes the task is to be executed
1087          * in the virtual machine (or the preprocessor output needs to
1088          * be matched).
1089          */
1090         if (!task_trymatch(it, match)) {
1091             size_t d = 0;
1092
1093             con_out("failure:   `%s` %*s %*s\n",
1094                 it.tmpl->description,
1095                 (pad[0] + pad[1] - strlen(it.tmpl->description)) + (strlen(it.tmpl->rulesfile) - pad[1]),
1096                 it.tmpl->rulesfile,
1097                 (pad[1] + pad[2] - strlen(it.tmpl->rulesfile)) + (strlen(
1098                     (strcmp(it.tmpl->proceduretype, "-pp"))
1099                         ? "(invalid results from execution)"
1100                         : (strcmp(it.tmpl->proceduretype, "-diagnostic"))
1101                             ? "(invalid results from preprocessing)"
1102                             : "(invalid results from compiler diagnsotics)"
1103                 ) - pad[2]),
1104                 (strcmp(it.tmpl->proceduretype, "-pp"))
1105                     ? "(invalid results from execution)"
1106                     : (strcmp(it.tmpl->proceduretype, "-diagnostic"))
1107                             ? "(invalid results from preprocessing)"
1108                             : "(invalid results from compiler diagnsotics)"
1109             );
1110
1111             /*
1112              * Print nicely formatted expected match lists to console error
1113              * handler for the all the given matches in the template file and
1114              * what was actually returned from executing.
1115              */
1116             con_out("    Expected From %u Matches: (got %u Matches)\n",
1117                 it.tmpl->comparematch.size(),
1118                 match.size()
1119             );
1120             for (; d < it.tmpl->comparematch.size(); d++) {
1121                 char *select = it.tmpl->comparematch[d];
1122                 size_t length = 60 - strlen(select);
1123                 con_out("        Expected: \"%s\"", select);
1124                 while (length --)
1125                     con_out(" ");
1126                 con_out("| Got: \"%s\"\n", (d >= match.size()) ? "<<nothing else to compare>>" : match[d]);
1127             }
1128
1129             /*
1130              * Print the non-expected out (since we are simply not expecting it)
1131              * This will help track down bugs in template files that fail to match
1132              * something.
1133              */
1134             if (match.size() > it.tmpl->comparematch.size()) {
1135                 for (d = 0; d < match.size() - it.tmpl->comparematch.size(); d++) {
1136                     con_out("        Expected: Nothing                                                       | Got: \"%s\"\n",
1137                         match[d + it.tmpl->comparematch.size()]
1138                     );
1139                 }
1140             }
1141
1142             for (auto &it : match)
1143                 mem_d(it);
1144             match.clear();
1145             failed++;
1146             continue;
1147         }
1148
1149         for (auto &it : match)
1150             mem_d(it);
1151         match.clear();
1152
1153         con_out("succeeded: `%s` %*s %*s\n",
1154             it.tmpl->description,
1155             (pad[0] + pad[1] - strlen(it.tmpl->description)) + (strlen(it.tmpl->rulesfile) - pad[1]),
1156             it.tmpl->rulesfile,
1157             (pad[1] + pad[2] - strlen(it.tmpl->rulesfile)) + (strlen(task_type(it.tmpl))- pad[2]),
1158             task_type(it.tmpl)
1159
1160         );
1161     }
1162     mem_d(data);
1163     return failed;
1164 }
1165
1166 /*
1167  * This is the heart of the whole test-suite process.  This cleans up
1168  * any existing temporary files left behind as well as log files left
1169  * behind.  Then it propagates a list of tests from `curdir` by scaning
1170  * it for template files and compiling them into tasks, in which it
1171  * schedualizes them (executes them) and actually reports errors and
1172  * what not.  It then proceeds to destroy the tasks and return memory
1173  * it's the engine :)
1174  *
1175  * It returns true of tests could be propagated, otherwise it returns
1176  * false.
1177  *
1178  * It expects con_init() was called before hand.
1179  */
1180 static GMQCC_WARN bool test_perform(const char *curdir, const char *defs) {
1181     size_t             failed       = false;
1182     static const char *default_defs = "defs.qh";
1183
1184     size_t pad[] = {
1185         /* test ### [succeed/fail]: `description`      [tests/template.tmpl]     [type] */
1186                     0,                                 0,                        0
1187     };
1188
1189     /*
1190      * If the default definition file isn't set to anything.  We will
1191      * use the default_defs here, which is "defs.qc"
1192      */
1193     if (!defs) {
1194         defs = default_defs;
1195     }
1196
1197
1198     task_precleanup(curdir);
1199     if (!task_propagate(curdir, pad, defs)) {
1200         con_err("error: failed to propagate tasks\n");
1201         task_destroy();
1202         return false;
1203     }
1204     /*
1205      * If we made it here all tasks where propagated from their resultant
1206      * template file.  So we can start the FILO scheduler, this has been
1207      * designed in the most thread-safe way possible for future threading
1208      * it's designed to prevent lock contention, and possible syncronization
1209      * issues.
1210      */
1211     failed = task_schedualize(pad);
1212     if (failed)
1213         con_out("%u out of %u tests failed\n", failed, task_tasks.size());
1214     task_destroy();
1215
1216     return (failed) ? false : true;
1217 }
1218
1219 /*
1220  * Fancy GCC-like LONG parsing allows things like --opt=param with
1221  * assignment operator.  This is used for redirecting stdout/stderr
1222  * console to specific files of your choice.
1223  */
1224 static bool parsecmd(const char *optname, int *argc_, char ***argv_, char **out, int ds, bool split) {
1225     int  argc   = *argc_;
1226     char **argv = *argv_;
1227
1228     size_t len = strlen(optname);
1229
1230     if (strncmp(argv[0]+ds, optname, len))
1231         return false;
1232
1233     /* it's --optname, check how the parameter is supplied */
1234     if (argv[0][ds+len] == '=') {
1235         *out = argv[0]+ds+len+1;
1236         return true;
1237     }
1238
1239     if (!split || argc < ds) /* no parameter was provided, or only single-arg form accepted */
1240         return false;
1241
1242     /* using --opt param */
1243     *out = argv[1];
1244     --*argc_;
1245     ++*argv_;
1246     return true;
1247 }
1248
1249 int main(int argc, char **argv) {
1250     bool succeed  = false;
1251     char *defs = NULL;
1252
1253     con_init();
1254
1255     /*
1256      * Command line option parsing commences now We only need to support
1257      * a few things in the test suite.
1258      */
1259     while (argc > 1) {
1260         ++argv;
1261         --argc;
1262
1263         if (argv[0][0] == '-') {
1264             if (parsecmd("defs", &argc, &argv, &defs, 1, false))
1265                 continue;
1266
1267             if (!strcmp(argv[0]+1, "debug")) {
1268                 OPTS_OPTION_BOOL(OPTION_DEBUG) = true;
1269                 continue;
1270             }
1271             if (!strcmp(argv[0]+1, "nocolor")) {
1272                 con_color(0);
1273                 continue;
1274             }
1275
1276             con_err("invalid argument %s\n", argv[0]+1);
1277             return -1;
1278         }
1279     }
1280     succeed = test_perform("tests", defs);
1281
1282     return (succeed) ? EXIT_SUCCESS : EXIT_FAILURE;
1283 }