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

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