]> git.xonotic.org Git - xonotic/gmqcc.git/blob - test.c
Test system works, and added some working tests!
[xonotic/gmqcc.git] / test.c
1 /*
2  * Copyright (C) 2012
3  *     Dale Weiler
4  *
5  * Permission is hereby granted, free of charge, to any person obtaining a copy of
6  * this software and associated documentation files (the "Software"), to deal in
7  * the Software without restriction, including without limitation the rights to
8  * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9  * of the Software, and to permit persons to whom the Software is furnished to do
10  * so, subject to the following conditions:
11  *
12  * The above copyright notice and this permission notice shall be included in all
13  * copies or substantial portions of the Software.
14  *
15  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21  * SOFTWARE.
22  */
23 #include "gmqcc.h"
24 #include <sys/types.h>
25 #include <sys/stat.h>
26 #include <dirent.h>
27
28 bool  opts_memchk = true;
29 bool  opts_debug  = false;
30 char *task_bins[] = {
31     "./gmqcc",
32     "./qcvm"
33 };
34
35 #define TASK_COMPILE 0
36 #define TASK_EXECUTE 1
37
38 /*
39  * Task template system:
40  *  templates are rules for a specific test, used to create a "task" that
41  *  is executed with those set of rules (arguments, and what not). Tests
42  *  that don't have a template with them cannot become tasks, since without
43  *  the information for that test there is no way to properly "test" them.
44  *  Rules for these templates are described in a template file, using a
45  *  task template language.
46  * 
47  *  The language is a basic finite statemachine, top-down single-line
48  *  description language.
49  * 
50  *  The languge is composed entierly of "tags" which describe a string of
51  *  text for a task.  Think of it much like a configuration file.  Except
52  *  it's been designed to allow flexibility and future support for prodecual
53  *  semantics.
54  * 
55  *  The following "tags" are suported by the language
56  * 
57  *      D:
58  *          Used to set a description of the current test, this must be
59  *          provided, this tag is NOT optional.
60  * 
61  *      F:
62  *          Used to set a failure message, this message will be displayed
63  *          if the test fails, this tag is optional
64  * 
65  *      S:
66  *          Used to set a success message, this message will be displayed
67  *          if the test succeeds, this tag is optional.
68  * 
69  *      T:
70  *          Used to set the procedure for the given task, there are two
71  *          options for this:
72  *              -compile
73  *                  This simply performs compilation only
74  *              -execute
75  *                  This will perform compilation and execution
76  * 
77  *          This must be provided, this tag is NOT optional.
78  * 
79  *      C:
80  *          Used to set the compilation flags for the given task, this
81  *          must be provided, this tag is NOT optional.
82  * 
83  *      E:
84  *          Used to set the execution flags for the given task. This tag
85  *          must be provided if T == -execute, otherwise it's erroneous
86  *          as compilation only takes place. 
87  *      
88  *      M:
89  *          Used to describe a string of text that should be matched from
90  *          the output of executing the task.  If this doesn't match the
91  *          task fails.  This tag must be provided if T == -execute, otherwise
92  *          it's erroneous as compilation only takes place.
93  * 
94  *      I:
95  *          Used to specify the INPUT source file to operate on, this must be
96  *          provided, this tag is NOT optional.
97  * 
98  *  Notes:
99  *      These tags have one-time use, using them more than once will result
100  *      in template compilation errors.
101  * 
102  *      Lines beginning with # or // in the template file are comments and
103  *      are ignored by the template parser.
104  * 
105  *      Whitespace is optional, with exception to the colon ':' between the
106  *      tag and it's assignment value/
107  * 
108  *      The template compiler will detect erronrous tags (optional tags
109  *      that need not be set), as well as missing tags, and error accordingly
110  *      this will result in the task failing.
111  */
112 typedef struct {
113     char *description;
114     char *failuremessage;
115     char *successmessage;
116     char *compileflags;
117     char *executeflags;
118     char *comparematch;
119     char *proceduretype;
120     char *sourcefile;
121     char *tempfilename;
122 } task_template_t;
123
124 /*
125  * This is very much like a compiler code generator :-).  This generates
126  * a value from some data observed from the compiler.
127  */
128 bool task_template_generate(task_template_t *template, char tag, const char *file, size_t line, const char *value) {
129     char **destval = NULL;
130     
131     if (!template)
132         return false;
133     
134     switch(tag) {
135         case 'D': destval = &template->description;    break;
136         case 'F': destval = &template->failuremessage; break;
137         case 'S': destval = &template->successmessage; break;
138         case 'T': destval = &template->proceduretype;  break;
139         case 'C': destval = &template->compileflags;   break;
140         case 'E': destval = &template->executeflags;   break;
141         case 'M': destval = &template->comparematch;   break;
142         case 'I': destval = &template->sourcefile;     break;
143         default:
144             con_printmsg(LVL_ERROR, __FILE__, __LINE__, "internal error",
145                 "invalid tag `%c:` during code generation\n",
146                 tag
147             );
148             return false;
149     }
150     
151     /*
152      * Ensure if for the given tag, there already exists a
153      * assigned value.
154      */
155     if (*destval) {
156         con_printmsg(LVL_ERROR, file, line, "compile error",
157             "tag `%c:` already assigned value: %s\n",
158             tag, *destval
159         );
160         return false;
161     }
162     
163     /*
164      * Strip any whitespace that might exist in the value for assignments
165      * like "D:      foo"
166      */
167     if (value && *value && (*value == ' ' || *value == '\t'))
168         value++;
169     
170     /*
171      * Value will contain a newline character at the end, we need to strip
172      * this otherwise kaboom, seriously, kaboom :P
173      */
174     *strrchr(value, '\n')='\0';
175     
176     /*
177      * Now allocate and set the actual value for the specific tag. Which
178      * was properly selected and can be accessed with *destval.
179      */
180     *destval = util_strdup(value);
181     
182     return true;
183 }
184
185 bool task_template_parse(const char *file, task_template_t *template, FILE *fp) {
186     char  *data = NULL;
187     char  *back = NULL;
188     size_t size = 0;
189     size_t line = 1;
190     
191     if (!template)
192         return false;
193     
194     /* top down parsing */
195     while (util_getline(&back, &size, fp) != EOF) {
196         /* skip whitespace */
197         data = back;
198         if (*data && (*data == ' ' || *data == '\t'))
199             data++;
200             
201         switch (*data) {
202             /*
203              * Handle comments inside task template files.  We're strict
204              * about the language for fun :-)
205              */
206             case '/':
207                 if (data[1] != '/') {
208                     con_printmsg(LVL_ERROR, file, line, "template parse error",
209                         "invalid character `/`, perhaps you meant `//` ?");
210                     
211                     mem_d(back);
212                     return false;
213                 }
214             case '#':
215                 break;
216                 
217             /*
218              * Empty newlines are acceptable as well, so we handle that here
219              * despite being just odd since there should't be that many
220              * empty lines to begin with.
221              */
222             case '\r':
223             case '\n':
224                 break;
225                 
226                 
227             /*
228              * Now begin the actual "tag" stuff.  This works as you expect
229              * it to.
230              */
231             case 'D':
232             case 'F':
233             case 'S':
234             case 'T':
235             case 'C':
236             case 'E':
237             case 'M':
238             case 'I':
239                 if (data[1] != ':') {
240                     con_printmsg(LVL_ERROR, file, line, "template parse error",
241                         "expected `:` after `%c`",
242                         *data
243                     );
244                     goto failure;
245                 }
246                 if (!task_template_generate(template, *data, file, line, &data[3])) {
247                     con_printmsg(LVL_ERROR, file, line, "template compile error",
248                         "failed to generate for given task\n"
249                     );
250                     goto failure;
251                 }
252                 break;
253             
254             default:
255                 con_printmsg(LVL_ERROR, file, line, "template parse error",
256                     "invalid tag `%c`", *data
257                 );
258                 goto failure;
259             /* no break required */
260         }
261         
262         /* update line and free old sata */
263         line++;
264         mem_d(back);
265         back = NULL;
266     }
267     if (back)
268         mem_d(back);
269     return true;
270     
271 failure:
272     if (back)
273         mem_d (back);
274     return false;
275 }
276
277 /*
278  * Nullifies the template data: used during initialization of a new
279  * template and free.
280  */
281 void task_template_nullify(task_template_t *template) {
282     if (!template)
283         return;
284         
285     template->description    = NULL;
286     template->failuremessage = NULL;
287     template->successmessage = NULL;
288     template->proceduretype  = NULL;
289     template->compileflags   = NULL;
290     template->executeflags   = NULL;
291     template->comparematch   = NULL;
292     template->sourcefile     = NULL;
293     template->tempfilename   = NULL;
294 }
295
296 task_template_t *task_template_compile(const char *file, const char *dir) {
297     /* a page should be enough */
298     char             fullfile[4096];
299     FILE            *tempfile = NULL;
300     task_template_t *template = NULL;
301     
302     memset  (fullfile, 0, sizeof(fullfile));
303     snprintf(fullfile,    sizeof(fullfile), "%s/%s", dir, file);
304     
305     tempfile = fopen(fullfile, "r");
306     template = mem_a(sizeof(task_template_t));
307     task_template_nullify(template);
308     
309     /*
310      * Esnure the file even exists for the task, this is pretty useless
311      * to even do.
312      */
313     if (!tempfile) {
314         con_err("template file: %s does not exist or invalid permissions\n",
315             file
316         );
317         goto failure;
318     }
319     
320     if (!task_template_parse(file, template, tempfile)) {
321         con_err("template parse error: error during parsing\n");
322         goto failure;
323     }
324     
325     /*
326      * Regardless procedure type, the following tags must exist:
327      *  D
328      *  T
329      *  C
330      *  I
331      */
332     if (!template->description) {
333         con_err("template compile error: %s missing `D:` tag\n", file);
334         goto failure;
335     }
336     if (!template->proceduretype) {
337         con_err("template compile error: %s missing `T:` tag\n", file);
338         goto failure;
339     }
340     if (!template->compileflags) {
341         con_err("template compile error: %s missing `C:` tag\n", file);
342         goto failure;
343     }
344     if (!template->sourcefile) {
345         con_err("template compile error: %s missing `I:` tag\n", file);
346         goto failure;
347     }
348     
349     /*
350      * Now lets compile the template, compilation is really just
351      * the process of validating the input.
352      */
353     if (!strcmp(template->proceduretype, "-compile")) {
354         if (template->executeflags)
355             con_err("template compile warning: %s erroneous tag `E:` when only compiling\n", file);
356         if (template->comparematch)
357             con_err("template compile warning: %s erroneous tag `M:` when only compiling\n", file);
358         goto success;
359     } else if (!strcmp(template->proceduretype, "-execute")) {
360         if (!template->executeflags) {
361             con_err("template compile error: %s missing `E:` tag (use `$null` for exclude)\n", file);
362             goto failure;
363         }
364         if (!template->comparematch) {
365             con_err("template compile error: %s missing `M:` tag (use `$null` for exclude)\n", file);
366             goto failure;
367         }
368     } else {
369         con_err("template compile error: %s invalid procedure type: %s\n", file, template->proceduretype);
370         goto failure;
371     }
372     
373 success:
374     fclose(tempfile);
375     return template;
376     
377 failure:
378     /*
379      * The file might not exist and we jump here when that doesn't happen
380      * so the check to see if it's not null here is required.
381      */
382     if (tempfile)
383         fclose(tempfile);
384     mem_d (template);
385     
386     return NULL;
387 }
388
389 void task_template_destroy(task_template_t **template) {
390     if (!template)
391         return;
392         
393     if ((*template)->description)    mem_d((*template)->description);
394     if ((*template)->failuremessage) mem_d((*template)->failuremessage);
395     if ((*template)->successmessage) mem_d((*template)->successmessage);
396     if ((*template)->proceduretype)  mem_d((*template)->proceduretype);
397     if ((*template)->compileflags)   mem_d((*template)->compileflags);
398     if ((*template)->executeflags)   mem_d((*template)->executeflags);
399     if ((*template)->comparematch)   mem_d((*template)->comparematch);
400     if ((*template)->sourcefile)     mem_d((*template)->sourcefile);
401     
402     /*
403      * Nullify all the template members otherwise NULL comparision
404      * checks will fail if template pointer is reused.
405      */
406     mem_d(*template);
407     task_template_nullify(*template);
408     *template = NULL;
409 }
410
411 /*
412  * Now comes the task manager, this system allows adding tasks in and out
413  * of a task list.  This is the executor of the tasks essentially as well.
414  */
415 typedef struct {
416     task_template_t *template;
417     FILE            *handle;
418 } task_t;
419
420 task_t *task_tasks = NULL;
421
422 /*
423  * Read a directory and searches for all template files in it
424  * which is later used to run all tests.
425  */
426 bool task_propogate(const char *curdir) {
427     bool             success = true;
428     DIR             *dir;
429     struct dirent   *files;
430     struct stat      directory;
431     
432     dir = opendir(curdir);
433     
434     while ((files = readdir(dir))) {
435         stat(files->d_name, &directory);
436         
437         /* skip directories */
438         if (S_ISDIR(directory.st_mode))
439             continue;
440             
441         /*
442          * We made it here, which concludes the file/directory is not
443          * actually a directory, so it must be a file :)
444          */
445         if (strstr(files->d_name, ".tmpl")) {
446             con_out("compiling task template: %s/%s\n", curdir, files->d_name);
447             task_template_t *template = task_template_compile(files->d_name, curdir);
448             if (!template) {
449                 con_err("error compiling task template: %s\n", files->d_name);
450                 success = false;
451                 continue;
452             }
453             /*
454              * Generate a temportary file name for the output binary
455              * so we don't trample over an existing one.
456              */
457             template->tempfilename = tempnam(curdir, "TMPDAT");
458             
459             
460             /*
461              * Generate the command required to open a pipe to a process
462              * which will be refered to with a handle in the task for
463              * reading the data from the pipe.
464              */
465             char     buf[4096]; /* one page should be enough */
466             memset  (buf,0,sizeof(buf));
467             snprintf(buf,  sizeof(buf), "%s %s/%s %s -o %s",
468                 task_bins[TASK_COMPILE],
469                 curdir,
470                 template->sourcefile,
471                 template->compileflags,
472                 template->tempfilename
473             );
474             
475             /*
476              * The task template was compiled, now lets create a task from
477              * the template data which has now been propogated.
478              */
479             task_t task;
480             task.template = template;
481             if (!(task.handle = popen(buf, "r"))) {
482                 con_err("error opening pipe to process for test: %s\n", template->description);
483                 success = false;
484                 continue;
485             }
486             con_out("executing test: `%s` [%s]\n", template->description, buf);
487             
488             vec_push(task_tasks, task);
489         }
490     }
491     
492     closedir(dir);
493     return success;
494 }
495
496 /*
497  * Removes all temporary 'progs.dat' files created during compilation
498  * of all tests'
499  */
500 void task_cleanup(const char *curdir) {
501     DIR             *dir;
502     struct dirent   *files;
503     struct stat      directory;
504     char             buffer[4096];
505
506     dir = opendir(curdir);
507     
508     while ((files = readdir(dir))) {
509         memset(buffer, 0, sizeof(buffer));
510         stat(files->d_name, &directory);
511     
512         if (strstr(files->d_name, "TMP")) {
513             snprintf(buffer, sizeof(buffer), "%s/%s", curdir, files->d_name);
514             if (remove(buffer))
515                 con_err("error removing temporary file: %s\n", buffer);
516             else
517                 con_out("removed temporary file: %s\n", buffer);
518         }
519     }
520     
521     closedir(dir);
522 }
523
524 void task_destroy(const char *curdir) {
525     /*
526      * Free all the data in the task list and finally the list itself
527      * then proceed to cleanup anything else outside the program like
528      * temporary files.
529      */
530     size_t i;
531     for (i = 0; i < vec_size(task_tasks); i++)
532         task_template_destroy(&task_tasks[i].template);
533     vec_free(task_tasks);
534     
535     /*
536      * Cleanup outside stuff like temporary files.
537      */
538     task_cleanup(curdir);
539 }
540
541 /*
542  * This executes the QCVM task for a specificly compiled progs.dat
543  * using the template passed into it for call-flags and user defined
544  * messages.
545  */
546 bool task_execute(task_template_t *template) {
547     bool     success = false;
548     FILE    *execute;
549     char     buffer[4096];
550     memset  (buffer,0,sizeof(buffer));
551     
552     /*
553      * Drop the execution flags for the QCVM if none where
554      * actually specified.
555      */
556     if (!strcmp(template->executeflags, "$null")) {
557         snprintf(buffer,  sizeof(buffer), "%s %s",
558             task_bins[TASK_EXECUTE],
559             template->tempfilename
560         );
561     } else {
562         snprintf(buffer,  sizeof(buffer), "%s %s %s",
563             task_bins[TASK_EXECUTE],
564             template->executeflags,
565             template->tempfilename
566         );
567     }
568     
569     con_out("executing qcvm: `%s` [%s]\n",
570         template->description,
571         buffer
572     );
573     
574     execute = popen(buffer, "r");
575     if (!execute)
576         return false;
577         
578     /*
579      * Now lets read the lines and compare them to the matches we expect
580      * and handle accordingly.
581      */
582     {
583         char  *data  = NULL;
584         size_t size  = 0;
585         while (util_getline(&data, &size, execute) != EOF) {}
586         
587         if (!strcmp(data, "No main function found\n")) {
588             con_err("test failure: `%s` [%s] (No main function found)\n",
589                 template->description,
590                 (template->failuremessage) ?
591                 template->failuremessage : "unknown"
592             );
593             pclose(execute);
594             return false;
595         }
596         
597         /* 
598          * Trim newlines from data since they will just break our
599          * ability to properly validate matches.
600          */
601         if  (strrchr(data, '\n'))
602             *strrchr(data, '\n') = '\0';
603         
604         /* null list */
605         if (!strcmp(template->comparematch, "$null"))
606             success = true;
607         
608         /*
609          * We only care about the last line from the output for now
610          * implementing multi-line match is TODO.
611          */
612         if (!strcmp(data, template->comparematch))
613             success = true;
614     }
615     pclose(execute);
616     return success;
617 }
618
619 /*
620  * This schedualizes all tasks and actually runs them individually
621  * this is generally easy for just -compile variants.  For compile and
622  * execution this takes more work since a task needs to be generated
623  * from thin air and executed INLINE.
624  */
625 void task_schedualize(const char *curdir) {
626     bool   execute = false;
627     char  *back    = NULL;
628     char  *data    = NULL;
629     size_t size    = 0;
630     size_t i;
631     
632     for (i = 0; i < vec_size(task_tasks); i++) {
633         /*
634         * Generate a task from thin air if it requires execution in
635         * the QCVM.
636         */
637         if (!strcmp(task_tasks[i].template->proceduretype, "-execute"))
638             execute = true;
639             
640         while (util_getline(&data, &size, task_tasks[i].handle) != EOF) {
641             back = data;
642             /* chances are we want to print errors */
643             if (strstr(data, "error")) {
644                 con_out("compile failed: %s\n",
645                     /* strip the newline from the end */
646                     (*strrchr(data, '\n')='\0')
647                 );
648                 
649                 /*
650                  * The compilation failed which means it cannot be executed
651                  * as the file simply will not exist.
652                  */
653                 execute = false;
654                 break;
655             }
656         }
657         if (back)
658             mem_d(back);
659         
660         /*
661          * If we can execute we do so after all data has been read and
662          * this paticular task has coupled execution in its procedure type
663          */
664         if (!execute)
665             continue;
666         
667         /*
668          * If we made it here that concludes the task is to be executed
669          * in the virtual machine.
670          */
671         if (!task_execute(task_tasks[i].template)) {
672             con_err("test failure: `%s` [%s]\n",
673                 task_tasks[i].template->description,
674                 (task_tasks[i].template->failuremessage) ?
675                 task_tasks[i].template->failuremessage : "unknown"
676             );
677             continue;
678         }
679         
680         con_out("test succeed: `%s` [%s]\n",
681             task_tasks[i].template->description,
682             (task_tasks[i].template->successmessage) ?
683             task_tasks[i].template->successmessage  : "unknown"
684         );
685     }
686     if (back)
687         mem_d(back);
688 }
689
690 int main(int argc, char **argv) {
691     con_init();
692     if (!task_propogate("tests")) {
693         con_err("error: failed to propogate tasks\n");
694         task_destroy("tests");
695         return -1;
696     }
697     /*
698      * If we made it here all tasks where propogated from their resultant
699      * template file.  So we can start the FILO scheduler, this has been
700      * designed in the most thread-safe way possible for future threading
701      * it's designed to prevent lock contention, and possible syncronization
702      * issues.
703      */
704     task_schedualize("tests");
705     task_destroy("tests");
706     
707     util_meminfo();
708     return 0;
709 }