1#!/usr/bin/python
2
3###############################################################################
4#
5# builds and runs tests, resolves include dependencies
6#
7# see help for usage: ./run_tests.py --help
8#
9# (c) 2013-2017 Andre Mueller
10#
11###############################################################################
12
13import re
14import os
15import glob
16import shutil
17import ntpath
18from os import path
19from os import system
20from sys import stdout
21from sys import argv
22from sys import exit
23from sets import Set
24
25#default settings
26builddir = "../build_test"
27incpaths = ["", "../include/"]
28macros   = [] # ["NO_DEBUG", "NDEBUG"]
29compiler = "gcc"
30valgrind = "valgrind --error-exitcode=1"
31gcov     = "gcov -l "
32
33gccflags = ("-std=c++0x -O0 -g "
34                    " -Wall -Wextra -Wpedantic "
35                    " -Wno-unknown-pragmas"
36                    " -Wno-unknown-warning"
37                    " -Wno-unknown-warning-option"
38                    " -Wformat=2 "
39                    " -Wall -Wextra -Wpedantic "
40                    " -Wcast-align -Wcast-qual "
41                    " -Wconversion "
42                    " -Wctor-dtor-privacy "
43                    " -Wdisabled-optimization "
44                    " -Wdouble-promotion "
45                    " -Winit-self "
46                    " -Wlogical-op "
47                    " -Wmissing-include-dirs "
48                    " -Wno-sign-conversion "
49                    " -Wnoexcept "
50                    " -Wold-style-cast "
51                    " -Woverloaded-virtual "
52                    " -Wredundant-decls "
53                    " -Wshadow "
54                    " -Wstrict-aliasing=1 "
55                    " -Wstrict-null-sentinel "
56                    " -Wstrict-overflow=5 "
57                    " -Wswitch-default "
58                    " -Wundef "
59                    " -Wno-unknown-pragmas "
60                    " -Wuseless-cast ")
61
62#available compilers
63compilers = {
64    "gcc"   : {"exe": "g++",
65               "flags" : gccflags,
66               "macro" : "-D", "incpath" : "-I",
67               "obj" : "",
68               "cov" : "-fprofile-arcs -ftest-coverage",
69               "link" : "-o "
70    },
71    "clang" : {"exe": "clang++",
72               "flags" : gccflags,
73               "macro" : "-D", "incpath" : "-I",
74               "obj" : "",
75               "cov" : "-fprofile-arcs -ftest-coverage",
76               "link" : "-o "
77    },
78    "msvc" : {"exe": "cl",
79              "flags" : " /W4 /EHsc ",
80              "macro" : "/D:", "incpath" : "/I:",
81              "obj" : "/Foc:",
82              "cov" : "",
83              "link" : "/link /out:"
84    }
85}
86
87tuext      = "cpp"
88separator  = "-----------------------------------------------------------------"
89
90# [ (needs compilation, dependency regex) ]
91deprxp = [re.compile('^\s*#pragma\s+test\s+needs\(\s*"(.+\..+)"\s*\)\s*$'),
92          re.compile('^\s*#include\s+\<(.+\..+)\>\s*$'),
93          re.compile('^\s*#include\s+"(.+\..+)"\s*$')]
94
95testrxp = re.compile('(.+)\.' + tuext)
96
97
98
99# support functions
100def dependencies(source, searchpaths = [], sofar = Set()):
101    """ return set of dependencies for a C++ source file
102        the following dependency definitions are recocnized:
103          - #include "path/to/file.ext"
104          - #include <path/to/file.ext>
105          - #pragma test needs("path/to/file.ext")
106        note: uses DFS
107    """
108    active = Set()
109    files = Set()
110
111    if not path.exists(source):
112        return active
113
114    curpath = path.dirname(source) + "/"
115
116    with open(source, 'r') as file:
117        for line in file:
118            dep = ""
119            res = None
120            for rxp in deprxp:
121                # if line encodes dependency
122                res = rxp.match(line)
123                if res is not None:
124                    dep = res.group(1)
125                    if dep != "":
126                        if not path.exists(dep):
127                            # try same path as current dependency
128                            if path.exists(curpath + dep):
129                                dep = curpath + dep
130                            else: # try include paths
131                                for sp in searchpaths:
132                                    if path.exists(sp + dep):
133                                        dep = sp + dep
134                                        break
135
136                        active.add(dep)
137                        file = path.basename(dep)
138                        if dep not in sofar and file not in files:
139                            files.add(file)
140                            active.update(dependencies(dep, searchpaths, active.union(sofar)))
141                        break
142
143    active.add(source)
144    return active
145
146
147
148# initialize
149onwindows = os.name == "nt"
150
151artifactext = ""
152pathsep = "/"
153if onwindows:
154    artifactext = ".exe"
155    pathsep = "\\"
156
157paths = []
158sources = []
159includes = []
160showDependencies = False
161haltOnFail = True
162recompile = False
163allpass = True
164useValgrind = False
165useGcov = False
166doClean = False
167
168# process input args
169if len(argv) > 1:
170    next = 0
171    for i in range(1,len(argv)):
172        if i >= next:
173            arg = argv[i]
174            if arg == "-h" or arg == "--help":
175                print "Usage:"
176                print "  " + argv[0] + \
177                    " [--help]" \
178                    " [--clean]" \
179                    " [-c (gcc|clang|msvc)]" \
180                    " [-r]" \
181                    " [-d]" \
182                    " [--continue-on-fail]" \
183                    " [--valgrind]" \
184                    " [--gcov]" \
185                    " [<directory|file>...]"
186                print ""
187                print "Options:"
188                print "  -h, --help                        print this screen"
189                print "  --clean                           do a clean re-build; removes entire build directory"
190                print "  -r, --recompile                   recompile all source files before running"
191                print "  -d, --show-dependecies            show all resolved includes during compilation"
192                print "  -c, --compiler (gcc|clang|msvc)   select compiler"
193                print "  --valgrind                        run test through valgrind"
194                print "  --gcov                            run test through gcov"
195                print "  --continue-on-fail                continue running regardless of failed builds or tests";
196                exit(0)
197            elif arg == "--clean":
198                doClean = True
199            elif arg == "-r" or arg == "--recompile":
200                recompile = True
201            elif arg == "-d" or arg == "--show-dependencies":
202                showDependencies = True
203            elif arg == "--continue-on-fail":
204                haltOnFail = False
205            elif arg == "--valgrind":
206                useValgrind = True
207            elif arg == "--gcov":
208                useGcov = True
209            elif arg == "-c" or arg == "--compiler":
210                if i+1 < len(argv):
211                    compiler = argv[i+1]
212                    next = i + 2
213            else:
214                paths.append(arg)
215
216
217
218# get compiler-specific strings
219if compiler not in compilers.keys():
220    print "ERROR: compiler " + compiler + " not supported"
221    print "choose one of:"
222    for key in compilers.keys():
223        print "    " + key
224    exit(1)
225
226compileexec = compilers[compiler]["exe"]
227compileopts = compilers[compiler]["flags"]
228macroOpt    = compilers[compiler]["macro"]
229includeOpt  = compilers[compiler]["incpath"]
230objOutOpt   = compilers[compiler]["obj"]
231linkOpt     = compilers[compiler]["link"]
232coverageOpt = compilers[compiler]["cov"]
233
234if useGcov:
235    compileopts = compileopts + " " + coverageOpt
236
237if onwindows:
238    builddir = builddir.replace("/", "\\")
239
240builddir = builddir + "_" + compiler
241
242
243
244# gather source file names
245if len(paths) < 1:
246    paths = [os.getcwd()]
247
248for p in paths:
249    if p.endswith("." + tuext):
250        sources.append(p)
251    else:
252        sources.extend([path.join(root, name)
253             for root, dirs, files in os.walk(p)
254             for name in files
255             if name.endswith("." + tuext)])
256
257if len(sources) < 1:
258    print "ERROR: no source files found"
259    exit(1)
260
261
262
263# make build directory
264if doClean:
265    if os.path.exists(builddir): shutil.rmtree(builddir)
266    for f in glob.glob("*.gcov"): os.remove(f)
267    for f in glob.glob("*.gcda"): os.remove(f)
268    for f in glob.glob("*.gcno"): os.remove(f)
269
270if not os.path.exists(builddir):
271    os.makedirs(builddir)
272    print separator
273    print "C L E A N  B U I L D"
274
275print separator
276
277
278
279# compile and run tests
280compilecmd = compileexec + " " + compileopts
281print "compiler call: "
282print compilecmd
283print separator
284
285for m in macros:
286    if onwindows: m = m.replace("/", "\\")
287    if m != "": compilecmd = compilecmd + " " + macroOpt + m
288
289for ip in incpaths:
290    if onwindows: ip = ip.replace("/", "\\")
291    if ip != "": compilecmd = compilecmd + " " + includeOpt + ip
292
293
294for source in sources:
295    if onwindows: source = source.replace("/", "\\")
296
297    res1 = testrxp.match(source)
298    res2 = testrxp.match(path.basename(source))
299    if res1 is not None and res2 is not None:
300        tname = res1.group(1)
301        sname = res2.group(1)
302        stdout.write("testing " + tname + " > checking depdendencies > ")
303        stdout.flush()
304        artifact = builddir + pathsep + sname + artifactext
305        if onwindows: artifact = artifact.replace("/", "\\")
306
307        srcdeps = dependencies(source, incpaths)
308
309        if showDependencies:
310            print ""
311            for dep in srcdeps: print "    needs " + dep
312            stdout.write("    ")
313            stdout.flush()
314
315        doCompile = recompile or not path.exists(artifact)
316        if not doCompile:
317             for dep in srcdeps:
318                if path.exists(dep):
319                    if str(path.getmtime(artifact)) < str(path.getmtime(dep)):
320                        doCompile = True
321                        break
322                else:
323                    print "ERROR: dependency " + dep + " could not be found!"
324                    exit(1)
325
326        if doCompile:
327            stdout.write("compiling > ")
328            stdout.flush()
329
330            if path.exists(artifact):
331                os.remove(artifact)
332
333            tus = ""
334            for dep in srcdeps:
335                 if dep.endswith("." + tuext):
336                     tus = tus + " " + dep
337
338            if onwindows: tus = tus.replace("/", "\\")
339
340            compilecall = compilecmd
341
342            # object file output
343            if objOutOpt != "":
344                objfile = builddir + pathsep + sname + ".o"
345                compilecall = compilecall + " " + objOutOpt + objfile
346
347            compilecall = compilecall + " " + tus + " " + linkOpt + artifact
348
349            system(compilecall)
350            if not path.exists(artifact):
351                print "FAILED!"
352                allpass = False
353                if haltOnFail: exit(1);
354
355        stdout.write("running > ")
356        stdout.flush()
357        runres = system(artifact)
358
359        if runres == 0 and useValgrind:
360            print "valgrind > "
361            runres = system(valgrind + " " + artifact)
362
363        if runres == 0 and useGcov:
364            stdout.write("gcov > ")
365            system(gcov + " " + source)
366
367        if runres == 0:
368            print "passed"
369        else:
370            print "FAILED!"
371            allpass = False
372            if haltOnFail : exit(1)
373
374
375print separator
376
377if allpass:
378    print "All tests passed."
379    exit(0)
380else:
381    print "Some tests failed."
382    exit(1)
383
384