PageRenderTime 45ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/4Suite-XML-1.0.2/Ft/Lib/TestSuite/Tester.py

#
Python | 493 lines | 457 code | 15 blank | 21 comment | 13 complexity | 427c7910bf0ca9b7cd60d3961562b8b6 MD5 | raw file
  1. ########################################################################
  2. # $Header: /var/local/cvsroot/4Suite/Ft/Lib/TestSuite/Tester.py,v 1.13 2004/11/04 05:33:46 mbrown Exp $
  3. """
  4. Provides the Tester class, which is the hub for all testing.
  5. Copyright 2004 Fourthought, Inc. (USA).
  6. Detailed license and copyright information: http://4suite.org/COPYRIGHT
  7. Project home, documentation, distributions: http://4suite.org/
  8. """
  9. import sys, os, time
  10. import traceback, linecache
  11. from distutils import spawn
  12. from sys import _getframe
  13. from Ft.Lib import number, Terminal
  14. from Ft.Lib.Terminal import AnsiEscapes
  15. ###########################################
  16. #
  17. # Verbosity Levels
  18. # 4 print out group headers, all test names and test results and debug messges
  19. # 3 print out group headers, all test names and test results
  20. # 2 print out group headers and errors and warnings
  21. # 1 print out group headers and errors
  22. # 0 Nothing
  23. VERBOSE_DEBUG = 4
  24. VERBOSE_MSG = 3
  25. VERBOSE_WARN = 2
  26. VERBOSE_ERROR = 1
  27. VERBOSE_OFF = 0
  28. SHOW_TESTS = VERBOSE_MSG
  29. SHOW_GROUPS = VERBOSE_ERROR
  30. _group_headers = ['#', '*', '=', ':', '%', '+', '-','@','$','&']
  31. def _frame_lineno(frame):
  32. """
  33. Calculate correct line number of stack frame given in frame.
  34. """
  35. code = frame.f_code
  36. if not hasattr(code, 'co_lnotab'):
  37. return frame.f_lineno
  38. tab = code.co_lnotab
  39. line = code.co_firstlineno
  40. stopat = frame.f_lasti
  41. addr = 0
  42. for i in range(0, len(tab), 2):
  43. addr = addr + ord(tab[i])
  44. if addr > stopat:
  45. break
  46. line = line + ord(tab[i+1])
  47. return line
  48. def extract_stack(frame=None, limit=None):
  49. if frame is None:
  50. # Skip this function body
  51. frame = _getframe(1)
  52. if limit is None:
  53. limit = getattr(sys, 'tracebacklimit', 1000)
  54. stack = []
  55. while frame and limit > 0:
  56. lineno = _frame_lineno(frame)
  57. co = frame.f_code
  58. filename = co.co_filename
  59. name = co.co_name
  60. line = linecache.getline(filename, lineno)
  61. if line:
  62. line = line.strip()
  63. else:
  64. line = None
  65. stack.append((filename, lineno, name, line))
  66. frame = frame.f_back
  67. limit -= 1
  68. stack.reverse()
  69. return stack
  70. def format_stack(frame=None, limit=None):
  71. """Shorthand for 'format_list(extract_stack(f, limit))'."""
  72. return traceback.format_list(extract_stack(frame, limit))
  73. class TestItem:
  74. def __init__(self, title):
  75. self.title = title
  76. self.messages = []
  77. self.hasErrors = 0
  78. self.hasWarnings = 0
  79. self.comparisons = 0
  80. self.compareTime = 0.0
  81. self.totalTime = 0.0
  82. self.runTime = 0.0
  83. self.startTime = time.time()
  84. return
  85. def __repr__(self):
  86. return '<%s, title=%r, compares=%d>' % (self.__class__.__name__,
  87. self.title,
  88. self.comparisons)
  89. def debug(self, msg):
  90. if msg:
  91. self.messages.append((VERBOSE_DEBUG, msg))
  92. return
  93. def message(self, msg):
  94. if msg:
  95. self.messages.append((VERBOSE_MSG, msg))
  96. return
  97. def warning(self, msg):
  98. self.hasWarnings = 1
  99. if msg:
  100. self.messages.append((VERBOSE_MSG, msg))
  101. return
  102. def error(self, msg):
  103. self.hasErrors = 1
  104. if msg:
  105. self.messages.append((VERBOSE_ERROR, msg))
  106. return
  107. def finish(self):
  108. self.totalTime = time.time() - self.startTime
  109. self.runTime = self.totalTime - self.compareTime
  110. return
  111. class Tester:
  112. double_sep = '=' * 72
  113. single_sep = '-' * 72
  114. NORMAL = AnsiEscapes.Colors.DEFAULT
  115. GRAY = AnsiEscapes.Colors.FOREGROUND_GRAY
  116. RED = AnsiEscapes.Colors.FOREGROUND_RED
  117. GREEN = AnsiEscapes.Colors.FOREGROUND_LIME
  118. BROWN = AnsiEscapes.Colors.FOREGROUND_BROWN
  119. YELLOW = AnsiEscapes.Colors.FOREGROUND_YELLOW
  120. WHITE = AnsiEscapes.Colors.FOREGROUND_WHITE
  121. def __init__(self, stopOnError=1, useColor=1, verbose=VERBOSE_DEBUG,
  122. stream=sys.stdout):
  123. self.stopOnError = stopOnError
  124. self.verbose = verbose
  125. self.stream = stream
  126. self.tty = Terminal.Terminal(stream, keepAnsiEscapes=useColor)
  127. self._writetty = self.tty.writetty
  128. self.test_data = {}
  129. self.groups = []
  130. self.test = None
  131. self.testTime = 0.0
  132. self._diffCommand = spawn.find_executable('diff')
  133. self._compareCtr = 0
  134. # Reporting information
  135. self.warnings = []
  136. self.failures = []
  137. self.exceptions = []
  138. self.totalGroups = 0
  139. self.totalTests = 0
  140. self.totalComparisons = 0
  141. return
  142. ### Testing Methods ###
  143. def startGroup(self, title):
  144. header = _group_headers[len(self.groups)]*10
  145. self.writeline(SHOW_GROUPS, '%s %s %s' % (header, title, header))
  146. self.groups.append(title)
  147. self.totalGroups += 1
  148. return
  149. def groupDone(self):
  150. if self.groups:
  151. del self.groups[-1]
  152. else:
  153. self.warning('groupDone called without active group')
  154. return
  155. def startTest(self, title):
  156. if self.test:
  157. # Add warning to current test
  158. self.warning('testDone not called')
  159. self.testDone()
  160. self.test = TestItem(title)
  161. # Add the warning to the new test as well
  162. self.warning('startTest called with active test')
  163. else:
  164. self.test = TestItem(title)
  165. self.totalTests += 1
  166. return
  167. def testDone(self):
  168. if not self.test:
  169. self.warning('testDone called without active test')
  170. return
  171. self.test.finish()
  172. if self.test.hasErrors:
  173. status = '[%sFAILED%s]' % (self.RED,
  174. self.NORMAL)
  175. level = VERBOSE_ERROR
  176. elif self.test.hasWarnings:
  177. status = '[%s WARN %s]' % (self.YELLOW,
  178. self.NORMAL)
  179. level = VERBOSE_WARN
  180. else:
  181. status = '[%s OK %s]' % (self.GREEN,
  182. self.NORMAL)
  183. level = SHOW_TESTS
  184. if not self.test.comparisons:
  185. title = '%s (%0.3f secs)' % (self.test.title,
  186. self.test.totalTime)
  187. else:
  188. title = '%s (%0.3f run, %0.3f total)' % (self.test.title,
  189. self.test.runTime,
  190. self.test.totalTime)
  191. spaces = self.tty.columns() - 9 # length of status
  192. if len(title) > spaces:
  193. self.writeline(level, title)
  194. self.writeline(level, ' '*spaces + status)
  195. else:
  196. self.writeline(level, title.ljust(spaces) + status)
  197. # Write the messages in the queue
  198. for level, line in self.test.messages:
  199. self.writeline(level, line)
  200. self.testTime += self.test.totalTime
  201. self.test = None
  202. return
  203. # -- result testing ----------------------------------------------
  204. def testException(self, func, args, etype, value={}, stackLevel=1, kwargs={}):
  205. # Increase stack trimming to include this function call
  206. stackLevel = stackLevel + 1
  207. try:
  208. func(*args, **kwargs)
  209. except etype, e:
  210. for attr, expected in value.items():
  211. if hasattr(e, attr):
  212. self.compare(expected, getattr(e, attr),
  213. 'exception attribute %s' % repr(attr),
  214. stackLevel=stackLevel)
  215. except:
  216. self.exception("Wrong exception raised")
  217. else:
  218. self.error("Expected exception '%s' not raised" % etype,
  219. stackLevel=stackLevel)
  220. return
  221. def compare(self, expected, actual, msg=None, func=cmp, diff=0, stackLevel=1, funcArgs={}):
  222. """
  223. Uses func to compare the expected result with actual result
  224. of a regression test.
  225. diff is ignored.
  226. msg is an optional custom message to print if the
  227. comparison tests positive (i.e. the results differ).
  228. func is the comparison function to use, and must be a
  229. function that returns the same as the built-in cmp().
  230. stackLevel affects exception reporting.
  231. funcArgs is an optional dictionary of keyword arguments that
  232. will be passed to the comparison function, if the dictionary
  233. is not empty.
  234. """
  235. self.totalComparisons += 1
  236. # Normalize float values
  237. if type(expected) == type(actual) == float:
  238. if number.finite(expected):
  239. expected = float(str(expected))
  240. elif number.isnan(expected):
  241. expected = 'NaN'
  242. elif number.isinf(expected) > 0:
  243. expected = 'Inf'
  244. else:
  245. expected = '-Inf'
  246. if number.finite(actual):
  247. actual = float(str(actual))
  248. elif number.isnan(actual):
  249. actual = 'NaN'
  250. elif number.isinf(actual) > 0:
  251. actual = 'Inf'
  252. else:
  253. actual = '-Inf'
  254. # Make sure there was a message for this comparison
  255. if not msg:
  256. if self.test:
  257. self.test.comparisons += 1
  258. msg = 'Test %d' % (self.test.comparisons)
  259. else:
  260. msg = 'Test %d of all tests' % self.totalComparisons
  261. start = time.time()
  262. try:
  263. if funcArgs:
  264. res = func(expected, actual, **funcArgs)
  265. else:
  266. res = func(expected, actual)
  267. if res:
  268. # failure
  269. self.message(msg)
  270. if diff and self.verbose >= VERBOSE_DEBUG:
  271. self._diff(expected, actual)
  272. error = '%sExpected:%s %s\n' % (self.GREEN,
  273. self.NORMAL,
  274. repr(expected))
  275. error += '%sCompared:%s %s' % (self.RED,
  276. self.NORMAL,
  277. repr(actual))
  278. self.error(error, stackLevel=(stackLevel+1))
  279. return 0
  280. finally:
  281. end = time.time()
  282. if self.test:
  283. self.test.compareTime += (end - start)
  284. # success
  285. return 1
  286. def compareIn(self, expected, actual, msg=None, stackLevel=1):
  287. """Test that 'actual' is in 'expected'"""
  288. func = lambda expected, actual: actual not in expected
  289. return self.compare(expected, actual, msg, func,
  290. stackLevel=(stackLevel+1))
  291. # -- display functions -------------------------------------------
  292. def writeline(self, level, msg):
  293. if self.verbose >= level:
  294. self._writetty(msg)
  295. self._writetty('\n')
  296. return
  297. def debug(self, msg):
  298. """debug-level messages"""
  299. if self.test:
  300. self.test.debug(msg)
  301. else:
  302. self.writeline(VERBOSE_DEBUG, msg)
  303. return
  304. def message(self, msg):
  305. """informational"""
  306. if self.test:
  307. self.test.message(msg)
  308. else:
  309. self.writeline(VERBOSE_MSG, msg)
  310. return
  311. def warning(self, msg):
  312. """warning conditions"""
  313. titles = self.groups[:]
  314. if self.test:
  315. titles.append(self.test.title)
  316. self.test.warning(msg)
  317. else:
  318. self.writeline(VERBOSE_WARN, msg)
  319. self.warnings.append((titles, msg))
  320. return
  321. def error(self, msg, traceLimit=1, stackLevel=1):
  322. """error conditions"""
  323. if self.stopOnError:
  324. traceLimit = getattr(sys, 'tracebacklimit', 1000)
  325. # Trim off 'stackLevel' items from the frame stack.
  326. frame = _getframe(stackLevel)
  327. # Format only 'traceLimit' items in the stack
  328. lines = format_stack(frame, traceLimit)
  329. msg += '\n' + ''.join(lines)
  330. titles = self.groups[:]
  331. if self.test:
  332. titles.append(self.test.title)
  333. self.test.error(msg)
  334. else:
  335. self.writeline(VERBOSE_ERROR, msg)
  336. self.failures.append((titles, msg))
  337. if self.stopOnError:
  338. self.testDone()
  339. raise SystemExit(1)
  340. return
  341. def exception(self, msg):
  342. """system is unusable""" # ??? looks like it's being used anyway
  343. if not sys.exc_info()[2]:
  344. raise AttributeError('No exception; use error method instead')
  345. try:
  346. etype, value, tb = sys.exc_info()
  347. lines = traceback.format_exception(etype, value, tb)
  348. finally:
  349. etype = value = tb = None
  350. msg += '\n' + ''.join(lines)
  351. titles = self.groups[:]
  352. if self.test:
  353. titles.append(self.test.title)
  354. self.test.error(msg)
  355. else:
  356. self.writeline(VERBOSE_ERROR, msg)
  357. self.exceptions.append((titles, msg))
  358. if self.stopOnError:
  359. self.testDone()
  360. raise SystemExit(1)
  361. return
  362. def _displayList(self, title, color, list):
  363. for titles, message in list:
  364. header = ': '.join(titles)
  365. self._writetty('\n%s%s: %s%s\n' % (color, title, header,
  366. self.NORMAL))
  367. self._writetty(message)
  368. self._writetty('\n')
  369. if list:
  370. self._writetty('\n%s\n' % self.single_sep)
  371. return
  372. def report(self):
  373. self._writetty('\n')
  374. self._writetty(self.double_sep)
  375. self._writetty('\n')
  376. self._displayList('WARN', self.YELLOW, self.warnings)
  377. self._displayList('FAIL', self.RED, self.failures)
  378. self._displayList('EXCEPTION', self.WHITE, self.exceptions)
  379. self._writetty('\n')
  380. self._writetty(" Test Groups Run: %d\n" % self.totalGroups)
  381. self._writetty(" Test Items Run: %d\n" % self.totalTests)
  382. self._writetty(" Results Compared: %d\n" % self.totalComparisons)
  383. self._writetty("Total Time To Run: %0.3fs\n" % self.testTime)
  384. self._writetty('\n')
  385. # -- internal functions ------------------------------------------
  386. def _diff(self, expected, compared):
  387. # get the temporary file directory
  388. import tempfile
  389. tempdir = tempfile.gettempdir()
  390. if self._diffCommand:
  391. # create the expected output file
  392. expected_file = os.path.join(tempdir, 'expected')
  393. fd = open(expected_file, 'w')
  394. fd.write(str(expected))
  395. fd.close()
  396. # create the compared output file
  397. compared_file = os.path.join(tempdir, 'compared')
  398. fd = open(compared_file, 'w')
  399. fd.write(str(compared))
  400. fd.close()
  401. # run diff, capturing stdout and stderr
  402. cmdline = '%s -u %s %s' % (self._diffCommand, expected_file,
  403. compared_file)
  404. self.debug(cmdline)
  405. f_in, f_out = os.popen4(cmdline)
  406. f_in.close()
  407. self.debug(f_out.read())
  408. f_out.close()
  409. os.unlink(expected_file)
  410. os.unlink(compared_file)
  411. return