You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
383 lines
11 KiB
383 lines
11 KiB
#!/usr/bin/python
|
|
|
|
###############################################################################
|
|
#
|
|
# builds and runs tests, resolves include dependencies
|
|
#
|
|
# see help for usage: ./run_tests.py --help
|
|
#
|
|
# (c) 2013-2017 Andre Mueller
|
|
#
|
|
###############################################################################
|
|
|
|
import re
|
|
import os
|
|
import glob
|
|
import shutil
|
|
import ntpath
|
|
from os import path
|
|
from os import system
|
|
from sys import stdout
|
|
from sys import argv
|
|
from sys import exit
|
|
from sets import Set
|
|
|
|
#default settings
|
|
builddir = "../build_test"
|
|
incpaths = ["", "../include/"]
|
|
macros = [] # ["NO_DEBUG", "NDEBUG"]
|
|
compiler = "gcc"
|
|
valgrind = "valgrind --error-exitcode=1"
|
|
gcov = "gcov -l "
|
|
|
|
gccflags = ("-std=c++0x -O0 -g "
|
|
" -Wall -Wextra -Wpedantic "
|
|
" -Wno-unknown-pragmas"
|
|
" -Wno-unknown-warning"
|
|
" -Wno-unknown-warning-option"
|
|
" -Wformat=2 "
|
|
" -Wall -Wextra -Wpedantic "
|
|
" -Wcast-align -Wcast-qual "
|
|
" -Wconversion "
|
|
" -Wctor-dtor-privacy "
|
|
" -Wdisabled-optimization "
|
|
" -Wdouble-promotion "
|
|
" -Winit-self "
|
|
" -Wlogical-op "
|
|
" -Wmissing-include-dirs "
|
|
" -Wno-sign-conversion "
|
|
" -Wnoexcept "
|
|
" -Wold-style-cast "
|
|
" -Woverloaded-virtual "
|
|
" -Wredundant-decls "
|
|
" -Wshadow "
|
|
" -Wstrict-aliasing=1 "
|
|
" -Wstrict-null-sentinel "
|
|
" -Wstrict-overflow=5 "
|
|
" -Wswitch-default "
|
|
" -Wundef "
|
|
" -Wno-unknown-pragmas "
|
|
" -Wuseless-cast ")
|
|
|
|
#available compilers
|
|
compilers = {
|
|
"gcc" : {"exe": "g++",
|
|
"flags" : gccflags,
|
|
"macro" : "-D", "incpath" : "-I",
|
|
"obj" : "",
|
|
"cov" : "-fprofile-arcs -ftest-coverage",
|
|
"link" : "-o "
|
|
},
|
|
"clang" : {"exe": "clang++",
|
|
"flags" : gccflags,
|
|
"macro" : "-D", "incpath" : "-I",
|
|
"obj" : "",
|
|
"cov" : "-fprofile-arcs -ftest-coverage",
|
|
"link" : "-o "
|
|
},
|
|
"msvc" : {"exe": "cl",
|
|
"flags" : " /W4 /EHsc ",
|
|
"macro" : "/D:", "incpath" : "/I:",
|
|
"obj" : "/Foc:",
|
|
"cov" : "",
|
|
"link" : "/link /out:"
|
|
}
|
|
}
|
|
|
|
tuext = "cpp"
|
|
separator = "-----------------------------------------------------------------"
|
|
|
|
# [ (needs compilation, dependency regex) ]
|
|
deprxp = [re.compile('^\s*#pragma\s+test\s+needs\(\s*"(.+\..+)"\s*\)\s*$'),
|
|
re.compile('^\s*#include\s+\<(.+\..+)\>\s*$'),
|
|
re.compile('^\s*#include\s+"(.+\..+)"\s*$')]
|
|
|
|
testrxp = re.compile('(.+)\.' + tuext)
|
|
|
|
|
|
|
|
# support functions
|
|
def dependencies(source, searchpaths = [], sofar = Set()):
|
|
""" return set of dependencies for a C++ source file
|
|
the following dependency definitions are recocnized:
|
|
- #include "path/to/file.ext"
|
|
- #include <path/to/file.ext>
|
|
- #pragma test needs("path/to/file.ext")
|
|
note: uses DFS
|
|
"""
|
|
active = Set()
|
|
files = Set()
|
|
|
|
if not path.exists(source):
|
|
return active
|
|
|
|
curpath = path.dirname(source) + "/"
|
|
|
|
with open(source, 'r') as file:
|
|
for line in file:
|
|
dep = ""
|
|
res = None
|
|
for rxp in deprxp:
|
|
# if line encodes dependency
|
|
res = rxp.match(line)
|
|
if res is not None:
|
|
dep = res.group(1)
|
|
if dep != "":
|
|
if not path.exists(dep):
|
|
# try same path as current dependency
|
|
if path.exists(curpath + dep):
|
|
dep = curpath + dep
|
|
else: # try include paths
|
|
for sp in searchpaths:
|
|
if path.exists(sp + dep):
|
|
dep = sp + dep
|
|
break
|
|
|
|
active.add(dep)
|
|
file = path.basename(dep)
|
|
if dep not in sofar and file not in files:
|
|
files.add(file)
|
|
active.update(dependencies(dep, searchpaths, active.union(sofar)))
|
|
break
|
|
|
|
active.add(source)
|
|
return active
|
|
|
|
|
|
|
|
# initialize
|
|
onwindows = os.name == "nt"
|
|
|
|
artifactext = ""
|
|
pathsep = "/"
|
|
if onwindows:
|
|
artifactext = ".exe"
|
|
pathsep = "\\"
|
|
|
|
paths = []
|
|
sources = []
|
|
includes = []
|
|
showDependencies = False
|
|
haltOnFail = True
|
|
recompile = False
|
|
allpass = True
|
|
useValgrind = False
|
|
useGcov = False
|
|
doClean = False
|
|
|
|
# process input args
|
|
if len(argv) > 1:
|
|
next = 0
|
|
for i in range(1,len(argv)):
|
|
if i >= next:
|
|
arg = argv[i]
|
|
if arg == "-h" or arg == "--help":
|
|
print "Usage:"
|
|
print " " + argv[0] + \
|
|
" [--help]" \
|
|
" [--clean]" \
|
|
" [-c (gcc|clang|msvc)]" \
|
|
" [-r]" \
|
|
" [-d]" \
|
|
" [--continue-on-fail]" \
|
|
" [--valgrind]" \
|
|
" [--gcov]" \
|
|
" [<directory|file>...]"
|
|
print ""
|
|
print "Options:"
|
|
print " -h, --help print this screen"
|
|
print " --clean do a clean re-build; removes entire build directory"
|
|
print " -r, --recompile recompile all source files before running"
|
|
print " -d, --show-dependecies show all resolved includes during compilation"
|
|
print " -c, --compiler (gcc|clang|msvc) select compiler"
|
|
print " --valgrind run test through valgrind"
|
|
print " --gcov run test through gcov"
|
|
print " --continue-on-fail continue running regardless of failed builds or tests";
|
|
exit(0)
|
|
elif arg == "--clean":
|
|
doClean = True
|
|
elif arg == "-r" or arg == "--recompile":
|
|
recompile = True
|
|
elif arg == "-d" or arg == "--show-dependencies":
|
|
showDependencies = True
|
|
elif arg == "--continue-on-fail":
|
|
haltOnFail = False
|
|
elif arg == "--valgrind":
|
|
useValgrind = True
|
|
elif arg == "--gcov":
|
|
useGcov = True
|
|
elif arg == "-c" or arg == "--compiler":
|
|
if i+1 < len(argv):
|
|
compiler = argv[i+1]
|
|
next = i + 2
|
|
else:
|
|
paths.append(arg)
|
|
|
|
|
|
|
|
# get compiler-specific strings
|
|
if compiler not in compilers.keys():
|
|
print "ERROR: compiler " + compiler + " not supported"
|
|
print "choose one of:"
|
|
for key in compilers.keys():
|
|
print " " + key
|
|
exit(1)
|
|
|
|
compileexec = compilers[compiler]["exe"]
|
|
compileopts = compilers[compiler]["flags"]
|
|
macroOpt = compilers[compiler]["macro"]
|
|
includeOpt = compilers[compiler]["incpath"]
|
|
objOutOpt = compilers[compiler]["obj"]
|
|
linkOpt = compilers[compiler]["link"]
|
|
coverageOpt = compilers[compiler]["cov"]
|
|
|
|
if useGcov:
|
|
compileopts = compileopts + " " + coverageOpt
|
|
|
|
if onwindows:
|
|
builddir = builddir.replace("/", "\\")
|
|
|
|
builddir = builddir + "_" + compiler
|
|
|
|
|
|
|
|
# gather source file names
|
|
if len(paths) < 1:
|
|
paths = [os.getcwd()]
|
|
|
|
for p in paths:
|
|
if p.endswith("." + tuext):
|
|
sources.append(p)
|
|
else:
|
|
sources.extend([path.join(root, name)
|
|
for root, dirs, files in os.walk(p)
|
|
for name in files
|
|
if name.endswith("." + tuext)])
|
|
|
|
if len(sources) < 1:
|
|
print "ERROR: no source files found"
|
|
exit(1)
|
|
|
|
|
|
|
|
# make build directory
|
|
if doClean:
|
|
if os.path.exists(builddir): shutil.rmtree(builddir)
|
|
for f in glob.glob("*.gcov"): os.remove(f)
|
|
for f in glob.glob("*.gcda"): os.remove(f)
|
|
for f in glob.glob("*.gcno"): os.remove(f)
|
|
|
|
if not os.path.exists(builddir):
|
|
os.makedirs(builddir)
|
|
print separator
|
|
print "C L E A N B U I L D"
|
|
|
|
print separator
|
|
|
|
|
|
|
|
# compile and run tests
|
|
compilecmd = compileexec + " " + compileopts
|
|
print "compiler call: "
|
|
print compilecmd
|
|
print separator
|
|
|
|
for m in macros:
|
|
if onwindows: m = m.replace("/", "\\")
|
|
if m != "": compilecmd = compilecmd + " " + macroOpt + m
|
|
|
|
for ip in incpaths:
|
|
if onwindows: ip = ip.replace("/", "\\")
|
|
if ip != "": compilecmd = compilecmd + " " + includeOpt + ip
|
|
|
|
|
|
for source in sources:
|
|
if onwindows: source = source.replace("/", "\\")
|
|
|
|
res1 = testrxp.match(source)
|
|
res2 = testrxp.match(path.basename(source))
|
|
if res1 is not None and res2 is not None:
|
|
tname = res1.group(1)
|
|
sname = res2.group(1)
|
|
stdout.write("testing " + tname + " > checking depdendencies > ")
|
|
stdout.flush()
|
|
artifact = builddir + pathsep + sname + artifactext
|
|
if onwindows: artifact = artifact.replace("/", "\\")
|
|
|
|
srcdeps = dependencies(source, incpaths)
|
|
|
|
if showDependencies:
|
|
print ""
|
|
for dep in srcdeps: print " needs " + dep
|
|
stdout.write(" ")
|
|
stdout.flush()
|
|
|
|
doCompile = recompile or not path.exists(artifact)
|
|
if not doCompile:
|
|
for dep in srcdeps:
|
|
if path.exists(dep):
|
|
if str(path.getmtime(artifact)) < str(path.getmtime(dep)):
|
|
doCompile = True
|
|
break
|
|
else:
|
|
print "ERROR: dependency " + dep + " could not be found!"
|
|
exit(1)
|
|
|
|
if doCompile:
|
|
stdout.write("compiling > ")
|
|
stdout.flush()
|
|
|
|
if path.exists(artifact):
|
|
os.remove(artifact)
|
|
|
|
tus = ""
|
|
for dep in srcdeps:
|
|
if dep.endswith("." + tuext):
|
|
tus = tus + " " + dep
|
|
|
|
if onwindows: tus = tus.replace("/", "\\")
|
|
|
|
compilecall = compilecmd
|
|
|
|
# object file output
|
|
if objOutOpt != "":
|
|
objfile = builddir + pathsep + sname + ".o"
|
|
compilecall = compilecall + " " + objOutOpt + objfile
|
|
|
|
compilecall = compilecall + " " + tus + " " + linkOpt + artifact
|
|
|
|
system(compilecall)
|
|
if not path.exists(artifact):
|
|
print "FAILED!"
|
|
allpass = False
|
|
if haltOnFail: exit(1);
|
|
|
|
stdout.write("running > ")
|
|
stdout.flush()
|
|
runres = system(artifact)
|
|
|
|
if runres == 0 and useValgrind:
|
|
print "valgrind > "
|
|
runres = system(valgrind + " " + artifact)
|
|
|
|
if runres == 0 and useGcov:
|
|
stdout.write("gcov > ")
|
|
system(gcov + " " + source)
|
|
|
|
if runres == 0:
|
|
print "passed"
|
|
else:
|
|
print "FAILED!"
|
|
allpass = False
|
|
if haltOnFail : exit(1)
|
|
|
|
|
|
print separator
|
|
|
|
if allpass:
|
|
print "All tests passed."
|
|
exit(0)
|
|
else:
|
|
print "Some tests failed."
|
|
exit(1)
|
|
|