testutils.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. """Basic MCell testing utilities.
  2. Miscellaneous useful utilities for testing are placed in this module. In
  3. particular, this module contains classes and functions related to invoking and
  4. testing generic MCell runs. Most in-depth output-testing utilities have been
  5. placed in other modules (such as viz_output or reaction_output).
  6. Most new tests will use McellTest (or a subclass of MCellTest) to handle
  7. starting the MCell run in a clean directory, running it, checking a handful of
  8. small invariant criteria that should hold for all MCell runs (for instance,
  9. that it did not segfault.)
  10. """
  11. import unittest
  12. import stat
  13. import os
  14. import sys
  15. import ConfigParser
  16. import string
  17. import types
  18. import random
  19. import re
  20. import shutil
  21. import subprocess
  22. import windows_util
  23. ###################################################################
  24. # Check if assertions are enabled
  25. ###################################################################
  26. def check_assertions_enabled():
  27. assertions_enabled = 0
  28. try:
  29. assert 1 == 2
  30. except AssertionError:
  31. assertions_enabled = 1
  32. return assertions_enabled
  33. ###################################################################
  34. # Complain and exit if assertions are not enabled
  35. ###################################################################
  36. def require_assertions_enabled():
  37. if check_assertions_enabled() == 0:
  38. print "Please turn off Python optimization (remove the -O flag from the command line)."
  39. print "The optimization feature turns off assertions, which are vital to the functioning"
  40. print "of the test suite."
  41. raise Exception("Assertions are disabled, testsuite cannot run")
  42. require_assertions_enabled()
  43. ###################################################################
  44. # Get the filename of the source file containing the function which calls this
  45. # function.
  46. ###################################################################
  47. def get_caller_filename(lvl=1):
  48. frame = sys._getframe()
  49. for i in range(0, lvl):
  50. frame = frame.f_back
  51. return frame.f_code.co_filename
  52. ###################################################################
  53. # Utility to safely concatenate two iterables, even if one or the other may be
  54. # None, or the two iterables have different types. If they have different
  55. # types, the return type will be a tuple.
  56. ###################################################################
  57. def safe_concat(l1, l2):
  58. """safe_concat(l1, l2) -> combined list.
  59. Concatenate two sequences safely, even if one or both may be None, or the
  60. types may differ.
  61. """
  62. if l1 is None:
  63. return l2
  64. elif l2 is None:
  65. return l1
  66. elif type(l1) is not type(l2):
  67. return tuple(l1) + tuple(l2)
  68. else:
  69. return l1 + l2
  70. ###################################################################
  71. # Get the preferred output directory.
  72. ###################################################################
  73. def get_output_dir():
  74. g = globals()
  75. if not g.has_key("test_output_dir"):
  76. global test_output_dir
  77. test_output_dir = "./test_results"
  78. return g["test_output_dir"]
  79. ###################################################################
  80. # Utility to generate a closed range
  81. ###################################################################
  82. def crange(l, h, s=1):
  83. """crange(start, stop[, step]) -> list of integers.
  84. Works like 'range', but is closed on both ends -- i.e. includes both
  85. endpoints.
  86. """
  87. if s > 0:
  88. return range(l, h+1, s)
  89. else:
  90. return range(l, h-1, s)
  91. ###################################################################
  92. # Make sure a directory exists, and contains no garbage. Used to ensure a
  93. # clean working directory for each run of the testsuite.
  94. ###################################################################
  95. def cleandir(directory):
  96. """Ensure that the speficied directory exists, and that it is empty.
  97. """
  98. if os.path.exists(directory):
  99. shutil.rmtree(directory)
  100. #try:
  101. os.mkdir(directory)
  102. #except:
  103. #pass
  104. #for root, dirs, files in os.walk(directory, topdown=False):
  105. # map(lambda f: os.unlink(os.path.join(root, f)), files)
  106. # map(lambda d: os.rmdir(os.path.join(root, d)), dirs)
  107. ###################################################################
  108. # Give an error if a file doesn't exist
  109. ###################################################################
  110. def assertFileExists(fname):
  111. """Raise an error if the specified file does not exist.
  112. """
  113. try:
  114. os.stat(fname)
  115. except:
  116. assert False, "Expected file '%s' was not created" % fname
  117. class RequireFileExists:
  118. def __init__(self, name):
  119. self.name = name
  120. def check(self):
  121. assertFileExists(self.name)
  122. ###################################################################
  123. # Give an error if a file does exist
  124. ###################################################################
  125. def assertFileNotExists(fname):
  126. """Raise an error if the specified file exists.
  127. """
  128. try:
  129. os.stat(fname)
  130. except:
  131. return
  132. assert False, "Specifically unexpected file '%s' was created" % fname
  133. class RequireFileNotExists:
  134. def __init__(self, name):
  135. self.name = name
  136. def check(self):
  137. assertFileNotExists(self.name)
  138. ###################################################################
  139. # Give an error if a file doesn't exist or isn't empty
  140. ###################################################################
  141. def assertFileEmpty(fname):
  142. try:
  143. sb = os.stat(fname)
  144. except:
  145. assert False, "Expected empty file '%s' was not created" % fname
  146. assert sb.st_size == 0, "Expected file '%s' should be empty but has length %d" % (fname, sb.st_size)
  147. class RequireFileEmpty:
  148. def __init__(self, name):
  149. self.name = name
  150. def check(self):
  151. assertFileEmpty(self.name)
  152. ###################################################################
  153. # Give an error if a file doesn't exist or is empty
  154. ###################################################################
  155. def assertFileNonempty(fname, expect=None):
  156. try:
  157. sb = os.stat(fname)
  158. except:
  159. assert False, "Expected file '%s' was not created" % fname
  160. assert sb.st_size != 0, "Expected file '%s' shouldn't be empty but is" % fname
  161. if expect != None:
  162. assert sb.st_size == expect, "Expected file '%s' is the wrong size (%d bytes instead of %d bytes)" % (fname, sb.st_size, expect)
  163. class RequireFileNonempty:
  164. def __init__(self, name, expect=None):
  165. self.name = name
  166. self.expect = expect
  167. def check(self):
  168. assertFileNonempty(self.name, self.expect)
  169. ###################################################################
  170. # Give an error if a file doesn't exist or if its contents differ from the
  171. # contents passed in.
  172. ###################################################################
  173. def assertFileEquals(fname, contents):
  174. try:
  175. got_contents = open(fname).read()
  176. except:
  177. assert False, "Expected file '%s' was not created" % fname
  178. assert got_contents == contents, "Expected file '%s' had incorrect contents" % fname
  179. class RequireFileEquals:
  180. def __init__(self, name, contents):
  181. self.name = name
  182. self.contents = contents
  183. def check(self):
  184. assertFileEquals(self.name, self.contents)
  185. ###################################################################
  186. # Give an error if a file doesn't exist or if its contents match (or do not
  187. # match) the regular expression passed in.
  188. ###################################################################
  189. def assertFileMatches(fname, regex, expectMinMatches=1, expectMaxMatches=sys.maxint):
  190. try:
  191. got_contents = open(fname).read()
  192. except:
  193. assert False, "Expected file '%s' was not created" % fname
  194. matches = re.findall(regex, got_contents)
  195. if len(matches) == 1:
  196. plural = ""
  197. else:
  198. plural = "es"
  199. assert len(matches) >= expectMinMatches, "Expected file '%s' had incorrect contents (found %d match%s for regex '%s', expected at least %d)" % (fname, len(matches), plural, regex, expectMinMatches)
  200. assert len(matches) <= expectMaxMatches, "Expected file '%s' had incorrect contents (found %d match%s for regex '%s', expected at most %d)" % (fname, len(matches), plural, regex, expectMaxMatches)
  201. class RequireFileMatches:
  202. def __init__(self, name, regex, expectMinMatches=1, expectMaxMatches=sys.maxint):
  203. self.name = name
  204. self.regex = regex
  205. self.expectMinMatches = expectMinMatches
  206. self.expectMaxMatches = expectMaxMatches
  207. def check(self):
  208. assertFileMatches(self.name, self.regex, self.expectMinMatches, self.expectMaxMatches)
  209. ###################################################################
  210. # Give an error if a file isn't a symlink, or (optionally) if the destination
  211. # of the symlink isn't as specified.
  212. ###################################################################
  213. def assertFileSymlink(fname, target=None):
  214. assert os.path.exists(fname), "Expected symlink '%s' was not created" % fname
  215. assert os.path.xislink(fname), "Expected symlink '%s' is not a symlink" % fname
  216. if target != None:
  217. got_target = os.xreadlink(fname)
  218. assert got_target == target, "Expected symlink '%s' should point to '%s', but instead points to '%s'" % (fname, target, got_target)
  219. class RequireFileSymlink:
  220. def __init__(self, name, target=None):
  221. self.name = name
  222. self.target = target
  223. def check(self):
  224. assertFileSymlink(self.name, self.target)
  225. ###################################################################
  226. # Give an error if a file isn't a directory.
  227. ###################################################################
  228. def assertFileDir(fname):
  229. try:
  230. sb = os.lstat(fname)
  231. except:
  232. assert False, "Expected directory '%s' was not created" % fname
  233. assert stat.S_ISDIR(sb.st_mode), "Expected directory '%s' is not a directory" % fname
  234. class RequireFileDir:
  235. def __init__(self, name, target=None):
  236. self.name = name
  237. def check(self):
  238. assertFileDir(self.name)
  239. ###################################################################
  240. # Small wrapper class to handle loading test suite configuration
  241. ###################################################################
  242. class test_config(object):
  243. def __init__(self, filepath="./test.cfg"):
  244. dict = {
  245. "mcellpath": "mcell"
  246. }
  247. self.config = ConfigParser.ConfigParser(dict)
  248. self.filepath = filepath
  249. try:
  250. self.config.readfp(open(self.filepath))
  251. except:
  252. print "ERROR: invalid file path '%s' to the configuration file" % self.filepath
  253. sys.exit(0)
  254. def get(self, sect, val):
  255. if self.config.has_section(sect):
  256. return self.config.get(sect, val)
  257. else:
  258. return self.config.get(ConfigParser.DEFAULTSECT, val)
  259. ###################################################################
  260. # Class to handle running command-line executables as PyUnit tests.
  261. ###################################################################
  262. class test_run_context(object):
  263. testidx = 1
  264. def __init__(self, cmd, args):
  265. self.command = cmd
  266. self.args = args
  267. self.testidx = test_run_context.testidx
  268. self.check_stdout = 0
  269. self.check_stderr = 0
  270. self.check_stdin = 0
  271. self.expect_exitcode = 0
  272. self.cleaned = False
  273. test_run_context.testidx += 1
  274. def set_check_std_handles(self, i, o, e):
  275. self.check_stdin = i
  276. self.check_stdout = o
  277. self.check_stderr = e
  278. def set_expected_exit_code(self, ec):
  279. self.expect_exitcode = ec
  280. def invoke(self, sandboxdir):
  281. curdir = os.getcwd()
  282. testpath = '%s/test-%04d' % (sandboxdir, self.testidx)
  283. if not self.cleaned:
  284. cleandir(testpath)
  285. self.cleaned = True
  286. os.chdir(testpath)
  287. try:
  288. try:
  289. self.__run()
  290. self.__check_results()
  291. except AssertionError, e:
  292. e.args = ("%s: %s" % (testpath, e.args[0])),
  293. raise
  294. finally:
  295. os.chdir(curdir)
  296. def check_stdout_valid(self, fullpath):
  297. assertFileEmpty(fullpath)
  298. def check_stderr_valid(self, fullpath):
  299. assertFileEmpty(fullpath)
  300. def check_output_files(self):
  301. pass
  302. def __exit_code(self):
  303. if hasattr(os, 'WIFEXITED') and callable(os.WIFEXITED) and os.WIFEXITED(self.got_exitcode):
  304. return os.WEXITSTATUS(self.got_exitcode)
  305. else:
  306. return self.got_exitcode
  307. def __check_results(self):
  308. assert self.got_exitcode >= 0, "Process died due to signal %d" % -self.got_exitcode
  309. assert self.__exit_code() == self.expect_exitcode, "Expected exit code %d, got exit code %d" % (self.expect_exitcode, self.__exit_code())
  310. if self.check_stdout:
  311. self.check_stdout_valid(os.path.join(os.getcwd(), "stdout"))
  312. if self.check_stderr:
  313. self.check_stderr_valid(os.path.join(os.getcwd(), "stderr"))
  314. self.check_output_files()
  315. def __run(self):
  316. try:
  317. f = open("cmdline.txt", "w", 0644)
  318. f.write("executable: ")
  319. f.write(self.command)
  320. f.write('\n')
  321. f.write("full cmdline: ")
  322. f.write(string.join(self.args))
  323. f.write('\n')
  324. finally:
  325. f.close()
  326. new_stdout = os.open("./stdout", os.O_CREAT | os.O_WRONLY | os.O_EXCL, 0644)
  327. new_stderr = os.open("./stderr", os.O_CREAT | os.O_WRONLY | os.O_EXCL, 0644)
  328. self.got_exitcode = subprocess.call(self.args, executable=self.command, stdin=None, stdout=new_stdout, stderr=new_stderr)
  329. os.close(new_stdout)
  330. os.close(new_stderr)
  331. ###################################################################
  332. # Specialized class for running MCell jobs as PyUnit tests
  333. ###################################################################
  334. class McellTest(test_run_context):
  335. """Utility base class for MCell tests.
  336. This class will build up the command-line, choosing a random seed,
  337. looking up the MCell executable in a configuration file, redirect output
  338. to log/err files using -logfile and -errfile, and do a handful of
  339. global validations (criteria which must be true for any MCell run).
  340. """
  341. rand = random.Random()
  342. def __init__(self, cat, f, args=[]):
  343. """Create a new MCell test runner.
  344. 'cat' determines the section of the config file to search for the
  345. MCell executable path.
  346. 'file' is the name of the MDL file to use.
  347. 'args' is a list of arguments other than the MDL file to pass.
  348. """
  349. path = os.path.dirname(os.path.realpath(get_caller_filename(2)))
  350. try:
  351. os.stat(os.path.join(path, f))
  352. except:
  353. assert False, "Didn't find MDL file '%s' in the expected location (%s)." % (f, path)
  354. mcell = McellTest.config.get(cat, "mcellpath")
  355. try:
  356. os.stat(mcell)
  357. except:
  358. print "ERROR: path '%s' to mcell executable in configuration file is invalid" % mcell
  359. sys.exit(0)
  360. real_args = [mcell]
  361. real_args.extend(["-seed", str(McellTest.rand.randint(0, 50000))])
  362. real_args.extend(["-logfile", "realout"])
  363. real_args.extend(["-errfile", "realerr"])
  364. real_args.extend(args)
  365. real_args.append(os.path.join(path, f))
  366. test_run_context.__init__(self, mcell, real_args)
  367. self.set_check_std_handles(1, 1, 1)
  368. self.set_expected_exit_code(0)
  369. self.extra_checks = []
  370. ## Add an extra check to be performed upon completion of the run
  371. def add_extra_check(self, chk):
  372. """Add an extra post-run check to this test. The check must be
  373. an object that has a 'check' method on it, which contains appropriate
  374. 'assert' statements to perform the check.
  375. Typically, this is used as follows:
  376. o = McellTest(...)
  377. o.add_extra_check(RequireFileEquals(filename, contents))
  378. o.invoke()
  379. This is especially useful for types of checks that cannot be added via the
  380. utility methods given below.
  381. """
  382. self.extra_checks.append(chk)
  383. ## Add one or more expected output files (either a string or an iterable)
  384. def add_exist_file(self, e):
  385. """Utility to add a file or list of files whose existence is a requisite
  386. post-condition for a successful run of the given MDL file.
  387. """
  388. if type(e) == types.StringType:
  389. self.extra_checks.append(RequireFileExists(e))
  390. else:
  391. self.extra_checks.extend([RequireFileExists(x) for x in e])
  392. return self
  393. ## Add one or more expected empty output files (either a string or an
  394. ## iterable)
  395. def add_empty_file(self, e):
  396. """Utility to add a file or list of files whose existence (and emptiness)
  397. is a requisite post-condition for a successful run of the given MDL file.
  398. """
  399. if type(e) == types.StringType:
  400. self.extra_checks.append(RequireFileEmpty(e))
  401. else:
  402. self.extra_checks.extend([RequireFileEmpty(x) for x in e])
  403. return self
  404. ## Add one or more expected non-empty output files (either a string or an
  405. ## iterable)
  406. def add_nonempty_file(self, e, expected_size=None):
  407. """Utility to add a file or list of files whose existence (and
  408. non-emptiness) is a requisite post-condition for a successful run of the
  409. given MDL file.
  410. """
  411. if type(e) == types.StringType:
  412. self.extra_checks.append(RequireFileNonempty(e, expected_size))
  413. else:
  414. self.extra_checks.extend([RequireFileNonempty(x, expected_size) for x in e])
  415. return self
  416. ## Add one or more expected constant output files (either fname and cnt must
  417. ## be strings, or they must be iterables with the same number of items)
  418. def add_constant_file(self, fname, cnt):
  419. """Utility to add a file or list of files whose existence is a requisite
  420. post-condition for a successful run of the given MDL file, and whose
  421. contents must exactly match a specified string.
  422. """
  423. if type(fname) == types.StringType:
  424. self.extra_checks.append(RequireFileEquals(fname, cnt))
  425. else:
  426. self.extra_checks.extend([RequireFileEquals(x, cnt) for x in fname])
  427. return self
  428. ## Add one or more expected symlinks, with optional target. fname may be a
  429. ## string or an iterable returning a string. target may be a string, or if
  430. ## fname is an iterable, an iterable returning a string.
  431. def add_symlink(self, fname, target=None):
  432. """Utility to add a file or list of filenames whose existence is a
  433. requisite post-condition for a successful run of the given MDL file, and
  434. which must be symlinks, optionally checking the target of the symlink
  435. against a pre-computed value.
  436. """
  437. if type(fname) == types.StringType:
  438. self.extra_checks.append(RequireFileSymlink(fname, target))
  439. else:
  440. if target != None:
  441. if type(target) == types.StringType:
  442. self.extra_checks.extend([RequireFileSymlink(x, target) for x in fname])
  443. else:
  444. assert len(fname) == len(target)
  445. self.extra_checks.extend([RequireFileSymlink(*a) for a in zip(fname, target)])
  446. return self
  447. def check_output_files(self):
  448. """Callback from test_run_context which is responsible for checking the
  449. state of all expected output files from the executable.
  450. """
  451. assertFileExists("realout")
  452. assertFileExists("realerr")
  453. for chk in self.extra_checks:
  454. chk.check()