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