smoketest 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. #!/usr/bin/env python
  2. #
  3. # Smoke test tool for the INET Framework: checks that simulations don't crash
  4. # or stop with a runtime error when run for a reasonable (CPU) time.
  5. #
  6. # Accepts one or more CSV files with (at least) two columns: working directory,
  7. # and options to opp_run. More than two columns are accepted so that the
  8. # tool can share the same input files with the fingerprint tester.
  9. # The program runs the INET simulations in the CSV files, and reports crashes
  10. # and runtime errors as FAILs.
  11. #
  12. # Implementation is based on Python's unit testing library, so it can be
  13. # integrated into larger test suites with minimal effort.
  14. #
  15. # Shares a fair amount of code with the fingerprint tester; those parts
  16. # should sometime be factored out as a Python module.
  17. #
  18. # Author: Andras Varga
  19. #
  20. import argparse
  21. import copy
  22. import csv
  23. import glob
  24. import multiprocessing
  25. import os
  26. import re
  27. import subprocess
  28. import sys
  29. import threading
  30. import time
  31. import unittest
  32. from StringIO import StringIO
  33. inetRoot = os.path.abspath("../..")
  34. sep = ";" if sys.platform == 'win32' else ':'
  35. nedPath = inetRoot + "/src" + sep + inetRoot + "/examples" + sep + inetRoot + "/showcases" + sep + inetRoot + "/tutorials"
  36. inetLib = inetRoot + "/src/INET"
  37. opp_run = "opp_run"
  38. cpuTimeLimit = "6s"
  39. logFile = "test.out"
  40. memcheck = False
  41. memcheckFailedErrorCode = 66
  42. class SmokeTestCaseGenerator():
  43. fileToSimulationsMap = {}
  44. def generateFromCSV(self, csvFileList, filterRegexList, excludeFilterRegexList, repeat):
  45. testcases = []
  46. for csvFile in csvFileList:
  47. simulations = self.parseSimulationsTable(csvFile)
  48. self.fileToSimulationsMap[csvFile] = simulations
  49. testcases.extend(self.generateFromDictList(simulations, filterRegexList, excludeFilterRegexList, repeat))
  50. return testcases
  51. def generateFromDictList(self, simulations, filterRegexList, excludeFilterRegexList, repeat):
  52. class StoreFingerprintCallback:
  53. def __init__(self, simulation):
  54. self.simulation = simulation
  55. def __call__(self, fingerprint):
  56. self.simulation['computedFingerprint'] = fingerprint
  57. class StoreExitcodeCallback:
  58. def __init__(self, simulation):
  59. self.simulation = simulation
  60. def __call__(self, exitcode):
  61. self.simulation['exitcode'] = exitcode
  62. testcases = []
  63. for simulation in simulations:
  64. title = simulation['wd'] + " " + simulation['args']
  65. if not filterRegexList or ['x' for regex in filterRegexList if re.search(regex, title)]: # if any regex matches title
  66. if not excludeFilterRegexList or not ['x' for regex in excludeFilterRegexList if re.search(regex, title)]: # if NO exclude-regex matches title
  67. testcases.append(SmokeTestCase(title, simulation['file'], simulation['wd'], simulation['args']))
  68. return testcases
  69. def commentRemover(self, csvData):
  70. p = re.compile(' *#.*$')
  71. for line in csvData:
  72. yield p.sub('',line)
  73. # parse the CSV into a list of dicts
  74. def parseSimulationsTable(self, csvFile):
  75. simulations = []
  76. f = open(csvFile, 'rb')
  77. csvReader = csv.reader(self.commentRemover(f), delimiter=',', quotechar='"', skipinitialspace=True)
  78. for fields in csvReader:
  79. if len(fields) == 0:
  80. continue # empty line
  81. if len(fields) != 2:
  82. raise Exception("Line " + str(csvReader.line_num) + " must contain 2 items, but contains " + str(len(fields)) + ": " + '"' + '", "'.join(fields) + '"')
  83. simulations.append({'file': csvFile, 'line' : csvReader.line_num, 'wd': fields[0], 'args': fields[1]})
  84. f.close()
  85. return simulations
  86. class SimulationResult:
  87. def __init__(self, command, workingdir, exitcode, errorMsg=None, isFingerprintOK=None,
  88. computedFingerprint=None, simulatedTime=None, numEvents=None, elapsedTime=None, cpuTimeLimitReached=None):
  89. self.command = command
  90. self.workingdir = workingdir
  91. self.exitcode = exitcode
  92. self.errorMsg = errorMsg
  93. self.isFingerprintOK = isFingerprintOK
  94. self.computedFingerprint = computedFingerprint
  95. self.simulatedTime = simulatedTime
  96. self.numEvents = numEvents
  97. self.elapsedTime = elapsedTime
  98. self.cpuTimeLimitReached = cpuTimeLimitReached
  99. class SimulationTestCase(unittest.TestCase):
  100. def runSimulation(self, title, command, workingdir):
  101. global logFile
  102. # run the program and log the output
  103. t0 = time.time()
  104. (exitcode, out) = self.runProgram(command, workingdir)
  105. elapsedTime = time.time() - t0
  106. FILE = open(logFile, "a")
  107. FILE.write("------------------------------------------------------\n"
  108. + "Running: " + title + "\n\n"
  109. + "$ cd " + workingdir + "\n"
  110. + "$ " + command + "\n\n"
  111. + out.strip() + "\n\n"
  112. + "Exit code: " + str(exitcode) + "\n"
  113. + "Elapsed time: " + str(round(elapsedTime,2)) + "s\n\n")
  114. FILE.close()
  115. result = SimulationResult(command, workingdir, exitcode, elapsedTime=elapsedTime)
  116. # process error messages
  117. errorLines = re.findall("<!>.*", out, re.M)
  118. errorMsg = ""
  119. for err in errorLines:
  120. err = err.strip()
  121. if re.search("Fingerprint", err):
  122. if re.search("successfully", err):
  123. result.isFingerprintOK = True
  124. else:
  125. m = re.search("(computed|calculated): ([-a-zA-Z0-9]+)", err)
  126. if m:
  127. result.isFingerprintOK = False
  128. result.computedFingerprint = m.group(2)
  129. else:
  130. raise Exception("Cannot parse fingerprint-related error message: " + err)
  131. else:
  132. errorMsg += "\n" + err
  133. if re.search("CPU time limit reached", err):
  134. result.cpuTimeLimitReached = True
  135. m = re.search("at event #([0-9]+), t=([0-9]*(\\.[0-9]+)?)", err)
  136. if m:
  137. result.numEvents = int(m.group(1))
  138. result.simulatedTime = float(m.group(2))
  139. result.errormsg = errorMsg.strip()
  140. return result
  141. def runProgram(self, command, workingdir):
  142. process = subprocess.Popen(command, shell=True, cwd=workingdir, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  143. out = process.communicate()[0]
  144. out = re.sub("\r", "", out)
  145. return (process.returncode, out)
  146. class SmokeTestCase(SimulationTestCase):
  147. def __init__(self, title, csvFile, wd, args):
  148. SimulationTestCase.__init__(self)
  149. self.title = title
  150. self.csvFile = csvFile
  151. self.wd = wd
  152. self.args = args
  153. def runTest(self):
  154. global inetRoot, opp_run, nedPath, cpuTimeLimit, memcheck
  155. # run the simulation
  156. workingdir = _iif(self.wd.startswith('/'), inetRoot + "/" + self.wd, self.wd)
  157. resultdir = os.path.abspath(".") + "/results/";
  158. if not os.path.exists(resultdir):
  159. os.makedirs(resultdir)
  160. memcheckPrefix = ""
  161. if memcheck:
  162. memcheckPrefix = "valgrind --leak-check=full --track-origins=yes --error-exitcode=" + str(memcheckFailedErrorCode) + " --suppressions=" + inetRoot + "/tests/smoke/valgrind.supp "
  163. command = memcheckPrefix + opp_run + " -n " + nedPath + " -l " + inetLib + " -u Cmdenv " + self.args + \
  164. " --cpu-time-limit=" + cpuTimeLimit + \
  165. " --vector-recording=false --scalar-recording=false" \
  166. " --result-dir=" + resultdir
  167. # print "COMMAND: " + command + '\n'
  168. result = self.runSimulation(self.title, command, workingdir)
  169. # process the result
  170. if result.exitcode == memcheckFailedErrorCode:
  171. assert False, "memcheck detected errors"
  172. elif result.exitcode != 0:
  173. assert False, result.errormsg
  174. elif result.simulatedTime == 0:
  175. assert False, "zero time simulated"
  176. else:
  177. pass
  178. def __str__(self):
  179. return self.title
  180. class ThreadSafeIter:
  181. """Takes an iterator/generator and makes it thread-safe by
  182. serializing call to the `next` method of given iterator/generator.
  183. """
  184. def __init__(self, it):
  185. self.it = it
  186. self.lock = threading.Lock()
  187. def __iter__(self):
  188. return self
  189. def next(self):
  190. with self.lock:
  191. return self.it.next()
  192. class ThreadedTestSuite(unittest.BaseTestSuite):
  193. """ runs toplevel tests in n threads
  194. """
  195. # How many test process at the time.
  196. thread_count = multiprocessing.cpu_count()
  197. def run(self, result):
  198. it = ThreadSafeIter(self.__iter__())
  199. result.buffered = True
  200. threads = []
  201. for i in range(self.thread_count):
  202. # Create self.thread_count number of threads that together will
  203. # cooperate removing every ip in the list. Each thread will do the
  204. # job as fast as it can.
  205. t = threading.Thread(target=self.runThread, args=(result, it))
  206. t.daemon = True
  207. t.start()
  208. threads.append(t)
  209. # Wait until all the threads are done. .join() is blocking.
  210. #for t in threads:
  211. # t.join()
  212. runApp = True
  213. while runApp and threading.active_count() > 1:
  214. try:
  215. time.sleep(0.1)
  216. except KeyboardInterrupt:
  217. runApp = False
  218. return result
  219. def runThread(self, result, it):
  220. tresult = result.startThread()
  221. for test in it:
  222. if result.shouldStop:
  223. break
  224. test(tresult)
  225. tresult.stopThread()
  226. class ThreadedTestResult(unittest.TestResult):
  227. """TestResult with threads
  228. """
  229. def __init__(self, stream=None, descriptions=None, verbosity=None):
  230. super(ThreadedTestResult, self).__init__()
  231. self.parent = None
  232. self.lock = threading.Lock()
  233. def startThread(self):
  234. ret = copy.copy(self)
  235. ret.parent = self
  236. return ret
  237. def stop():
  238. super(ThreadedTestResult, self).stop()
  239. if self.parent:
  240. self.parent.stop()
  241. def stopThread(self):
  242. if self.parent == None:
  243. return 0
  244. self.parent.testsRun += self.testsRun
  245. return 1
  246. def startTest(self, test):
  247. "Called when the given test is about to be run"
  248. super(ThreadedTestResult, self).startTest(test)
  249. self.oldstream = self.stream
  250. self.stream = StringIO()
  251. def stopTest(self, test):
  252. """Called when the given test has been run"""
  253. super(ThreadedTestResult, self).stopTest(test)
  254. out = self.stream.getvalue()
  255. with self.lock:
  256. self.stream = self.oldstream
  257. self.stream.write(out)
  258. #
  259. # Copy/paste of TextTestResult, with minor modifications in the output:
  260. # we want to print the error text after ERROR and FAIL, but we don't want
  261. # to print stack traces.
  262. #
  263. class SimulationTextTestResult(ThreadedTestResult):
  264. """A test result class that can print formatted text results to a stream.
  265. Used by TextTestRunner.
  266. """
  267. separator1 = '=' * 70
  268. separator2 = '-' * 70
  269. def __init__(self, stream, descriptions, verbosity):
  270. super(SimulationTextTestResult, self).__init__()
  271. self.stream = stream
  272. self.showAll = verbosity > 1
  273. self.dots = verbosity == 1
  274. self.descriptions = descriptions
  275. def getDescription(self, test):
  276. doc_first_line = test.shortDescription()
  277. if self.descriptions and doc_first_line:
  278. return '\n'.join((str(test), doc_first_line))
  279. else:
  280. return str(test)
  281. def startTest(self, test):
  282. super(SimulationTextTestResult, self).startTest(test)
  283. if self.showAll:
  284. self.stream.write(self.getDescription(test))
  285. self.stream.write(" ... ")
  286. self.stream.flush()
  287. def addSuccess(self, test):
  288. super(SimulationTextTestResult, self).addSuccess(test)
  289. if self.showAll:
  290. self.stream.write(": PASS\n")
  291. elif self.dots:
  292. self.stream.write('.')
  293. self.stream.flush()
  294. def addError(self, test, err):
  295. # modified
  296. super(SimulationTextTestResult, self).addError(test, err)
  297. self.errors[-1] = (test, err[1]) # super class method inserts stack trace; we don't need that, so overwrite it
  298. if self.showAll:
  299. (cause, detail) = self._splitMsg(err[1])
  300. self.stream.write(": ERROR (%s)\n" % cause)
  301. if detail:
  302. self.stream.write(detail)
  303. self.stream.write("\n")
  304. elif self.dots:
  305. self.stream.write('E')
  306. self.stream.flush()
  307. def addFailure(self, test, err):
  308. # modified
  309. super(SimulationTextTestResult, self).addFailure(test, err)
  310. self.failures[-1] = (test, err[1]) # super class method inserts stack trace; we don't need that, so overwrite it
  311. if self.showAll:
  312. (cause, detail) = self._splitMsg(err[1])
  313. self.stream.write(": FAIL (%s)\n" % cause)
  314. if detail:
  315. self.stream.write(detail)
  316. self.stream.write("\n")
  317. elif self.dots:
  318. self.stream.write('F')
  319. self.stream.flush()
  320. def addSkip(self, test, reason):
  321. super(SimulationTextTestResult, self).addSkip(test, reason)
  322. if self.showAll:
  323. self.stream.write(": skipped {0!r}".format(reason))
  324. self.stream.write("\n")
  325. elif self.dots:
  326. self.stream.write("s")
  327. self.stream.flush()
  328. def addExpectedFailure(self, test, err):
  329. super(SimulationTextTestResult, self).addExpectedFailure(test, err)
  330. if self.showAll:
  331. self.stream.write(": expected failure\n")
  332. elif self.dots:
  333. self.stream.write("x")
  334. self.stream.flush()
  335. def addUnexpectedSuccess(self, test):
  336. super(SimulationTextTestResult, self).addUnexpectedSuccess(test)
  337. if self.showAll:
  338. self.stream.write(": unexpected success\n")
  339. elif self.dots:
  340. self.stream.write("u")
  341. self.stream.flush()
  342. def printErrors(self):
  343. # modified
  344. if self.dots or self.showAll:
  345. self.stream.write("\n")
  346. self.printErrorList('Errors', self.errors)
  347. self.printErrorList('Failures', self.failures)
  348. def printErrorList(self, flavour, errors):
  349. # modified
  350. if errors:
  351. self.stream.writeln("%s:" % flavour)
  352. for test, err in errors:
  353. self.stream.write(" %s (%s)\n" % (self.getDescription(test), self._splitMsg(err)[0]))
  354. def _splitMsg(self, msg):
  355. cause = str(msg)
  356. detail = None
  357. if cause.count(': '):
  358. (cause, detail) = cause.split(': ',1)
  359. return (cause, detail)
  360. def _iif(cond,t,f):
  361. return t if cond else f
  362. def ensure_dir(f):
  363. if not os.path.exists(f):
  364. os.makedirs(f)
  365. if __name__ == "__main__":
  366. parser = argparse.ArgumentParser(description='Run the fingerprint tests specified in the input files.')
  367. parser.add_argument('testspecfiles', nargs='*', metavar='testspecfile', help='CSV files that contain the tests to run (default: *.csv). Expected CSV file columns: workingdir, args, simtimelimit, fingerprint')
  368. parser.add_argument('-m', '--match', action='append', metavar='regex', help='Line filter: a line (more precisely, workingdir+SPACE+args) must match any of the regular expressions in order for that test case to be run')
  369. parser.add_argument('-x', '--exclude', action='append', metavar='regex', help='Negative line filter: a line (more precisely, workingdir+SPACE+args) must NOT match any of the regular expressions in order for that test case to be run')
  370. parser.add_argument('-c', '--memcheck', default=False, dest='memcheck', action='store_true', help='run using valgrind memcheck (default: false)')
  371. parser.add_argument('-s', '--cpu-time-limit', default='6s', dest='cpuTimeLimit', help='cpu time limit (default: 6s)')
  372. parser.add_argument('-t', '--threads', type=int, default=multiprocessing.cpu_count(), help='number of parallel threads (default: number of CPUs, currently '+str(multiprocessing.cpu_count())+')')
  373. parser.add_argument('-l', '--logfile', default='test.out', dest='logFile', help='name of logfile (default: test.out)')
  374. args = parser.parse_args()
  375. memcheck = args.memcheck
  376. cpuTimeLimit = args.cpuTimeLimit
  377. logFile = args.logFile
  378. if os.path.isfile(logFile):
  379. FILE = open(logFile, "w")
  380. FILE.close()
  381. if not args.testspecfiles:
  382. args.testspecfiles = glob.glob('*.csv')
  383. generator = SmokeTestCaseGenerator()
  384. testcases = generator.generateFromCSV(args.testspecfiles, args.match, args.exclude, 1)
  385. testSuite = ThreadedTestSuite()
  386. testSuite.addTests(testcases)
  387. testSuite.thread_count = args.threads
  388. testRunner = unittest.TextTestRunner(stream=sys.stdout, verbosity=9, resultclass=SimulationTextTestResult)
  389. testRunner.run(testSuite)
  390. print
  391. print "Log has been saved to %s" % logFile