noseclasses.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. # These classes implement a doctest runner plugin for nose, a "known failure"
  2. # error class, and a customized TestProgram for NumPy.
  3. # Because this module imports nose directly, it should not
  4. # be used except by nosetester.py to avoid a general NumPy
  5. # dependency on nose.
  6. import os
  7. import sys
  8. import doctest
  9. import inspect
  10. import numpy
  11. import nose
  12. from nose.plugins import doctests as npd
  13. from nose.plugins.errorclass import ErrorClass, ErrorClassPlugin
  14. from nose.plugins.base import Plugin
  15. from nose.util import src
  16. from .nosetester import get_package_name
  17. from .utils import KnownFailureException, KnownFailureTest
  18. # Some of the classes in this module begin with 'Numpy' to clearly distinguish
  19. # them from the plethora of very similar names from nose/unittest/doctest
  20. #-----------------------------------------------------------------------------
  21. # Modified version of the one in the stdlib, that fixes a python bug (doctests
  22. # not found in extension modules, https://bugs.python.org/issue3158)
  23. class NumpyDocTestFinder(doctest.DocTestFinder):
  24. def _from_module(self, module, object):
  25. """
  26. Return true if the given object is defined in the given
  27. module.
  28. """
  29. if module is None:
  30. return True
  31. elif inspect.isfunction(object):
  32. return module.__dict__ is object.__globals__
  33. elif inspect.isbuiltin(object):
  34. return module.__name__ == object.__module__
  35. elif inspect.isclass(object):
  36. return module.__name__ == object.__module__
  37. elif inspect.ismethod(object):
  38. # This one may be a bug in cython that fails to correctly set the
  39. # __module__ attribute of methods, but since the same error is easy
  40. # to make by extension code writers, having this safety in place
  41. # isn't such a bad idea
  42. return module.__name__ == object.__self__.__class__.__module__
  43. elif inspect.getmodule(object) is not None:
  44. return module is inspect.getmodule(object)
  45. elif hasattr(object, '__module__'):
  46. return module.__name__ == object.__module__
  47. elif isinstance(object, property):
  48. return True # [XX] no way not be sure.
  49. else:
  50. raise ValueError("object must be a class or function")
  51. def _find(self, tests, obj, name, module, source_lines, globs, seen):
  52. """
  53. Find tests for the given object and any contained objects, and
  54. add them to `tests`.
  55. """
  56. doctest.DocTestFinder._find(self, tests, obj, name, module,
  57. source_lines, globs, seen)
  58. # Below we re-run pieces of the above method with manual modifications,
  59. # because the original code is buggy and fails to correctly identify
  60. # doctests in extension modules.
  61. # Local shorthands
  62. from inspect import (
  63. isroutine, isclass, ismodule, isfunction, ismethod
  64. )
  65. # Look for tests in a module's contained objects.
  66. if ismodule(obj) and self._recurse:
  67. for valname, val in obj.__dict__.items():
  68. valname1 = f'{name}.{valname}'
  69. if ( (isroutine(val) or isclass(val))
  70. and self._from_module(module, val)):
  71. self._find(tests, val, valname1, module, source_lines,
  72. globs, seen)
  73. # Look for tests in a class's contained objects.
  74. if isclass(obj) and self._recurse:
  75. for valname, val in obj.__dict__.items():
  76. # Special handling for staticmethod/classmethod.
  77. if isinstance(val, staticmethod):
  78. val = getattr(obj, valname)
  79. if isinstance(val, classmethod):
  80. val = getattr(obj, valname).__func__
  81. # Recurse to methods, properties, and nested classes.
  82. if ((isfunction(val) or isclass(val) or
  83. ismethod(val) or isinstance(val, property)) and
  84. self._from_module(module, val)):
  85. valname = f'{name}.{valname}'
  86. self._find(tests, val, valname, module, source_lines,
  87. globs, seen)
  88. # second-chance checker; if the default comparison doesn't
  89. # pass, then see if the expected output string contains flags that
  90. # tell us to ignore the output
  91. class NumpyOutputChecker(doctest.OutputChecker):
  92. def check_output(self, want, got, optionflags):
  93. ret = doctest.OutputChecker.check_output(self, want, got,
  94. optionflags)
  95. if not ret:
  96. if "#random" in want:
  97. return True
  98. # it would be useful to normalize endianness so that
  99. # bigendian machines don't fail all the tests (and there are
  100. # actually some bigendian examples in the doctests). Let's try
  101. # making them all little endian
  102. got = got.replace("'>", "'<")
  103. want = want.replace("'>", "'<")
  104. # try to normalize out 32 and 64 bit default int sizes
  105. for sz in [4, 8]:
  106. got = got.replace("'<i%d'" % sz, "int")
  107. want = want.replace("'<i%d'" % sz, "int")
  108. ret = doctest.OutputChecker.check_output(self, want,
  109. got, optionflags)
  110. return ret
  111. # Subclass nose.plugins.doctests.DocTestCase to work around a bug in
  112. # its constructor that blocks non-default arguments from being passed
  113. # down into doctest.DocTestCase
  114. class NumpyDocTestCase(npd.DocTestCase):
  115. def __init__(self, test, optionflags=0, setUp=None, tearDown=None,
  116. checker=None, obj=None, result_var='_'):
  117. self._result_var = result_var
  118. self._nose_obj = obj
  119. doctest.DocTestCase.__init__(self, test,
  120. optionflags=optionflags,
  121. setUp=setUp, tearDown=tearDown,
  122. checker=checker)
  123. print_state = numpy.get_printoptions()
  124. class NumpyDoctest(npd.Doctest):
  125. name = 'numpydoctest' # call nosetests with --with-numpydoctest
  126. score = 1000 # load late, after doctest builtin
  127. # always use whitespace and ellipsis options for doctests
  128. doctest_optflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
  129. # files that should be ignored for doctests
  130. doctest_ignore = ['generate_numpy_api.py',
  131. 'setup.py']
  132. # Custom classes; class variables to allow subclassing
  133. doctest_case_class = NumpyDocTestCase
  134. out_check_class = NumpyOutputChecker
  135. test_finder_class = NumpyDocTestFinder
  136. # Don't use the standard doctest option handler; hard-code the option values
  137. def options(self, parser, env=os.environ):
  138. Plugin.options(self, parser, env)
  139. # Test doctests in 'test' files / directories. Standard plugin default
  140. # is False
  141. self.doctest_tests = True
  142. # Variable name; if defined, doctest results stored in this variable in
  143. # the top-level namespace. None is the standard default
  144. self.doctest_result_var = None
  145. def configure(self, options, config):
  146. # parent method sets enabled flag from command line --with-numpydoctest
  147. Plugin.configure(self, options, config)
  148. self.finder = self.test_finder_class()
  149. self.parser = doctest.DocTestParser()
  150. if self.enabled:
  151. # Pull standard doctest out of plugin list; there's no reason to run
  152. # both. In practice the Unplugger plugin above would cover us when
  153. # run from a standard numpy.test() call; this is just in case
  154. # someone wants to run our plugin outside the numpy.test() machinery
  155. config.plugins.plugins = [p for p in config.plugins.plugins
  156. if p.name != 'doctest']
  157. def set_test_context(self, test):
  158. """ Configure `test` object to set test context
  159. We set the numpy / scipy standard doctest namespace
  160. Parameters
  161. ----------
  162. test : test object
  163. with ``globs`` dictionary defining namespace
  164. Returns
  165. -------
  166. None
  167. Notes
  168. -----
  169. `test` object modified in place
  170. """
  171. # set the namespace for tests
  172. pkg_name = get_package_name(os.path.dirname(test.filename))
  173. # Each doctest should execute in an environment equivalent to
  174. # starting Python and executing "import numpy as np", and,
  175. # for SciPy packages, an additional import of the local
  176. # package (so that scipy.linalg.basic.py's doctests have an
  177. # implicit "from scipy import linalg" as well).
  178. #
  179. # Note: __file__ allows the doctest in NoseTester to run
  180. # without producing an error
  181. test.globs = {'__builtins__':__builtins__,
  182. '__file__':'__main__',
  183. '__name__':'__main__',
  184. 'np':numpy}
  185. # add appropriate scipy import for SciPy tests
  186. if 'scipy' in pkg_name:
  187. p = pkg_name.split('.')
  188. p2 = p[-1]
  189. test.globs[p2] = __import__(pkg_name, test.globs, {}, [p2])
  190. # Override test loading to customize test context (with set_test_context
  191. # method), set standard docstring options, and install our own test output
  192. # checker
  193. def loadTestsFromModule(self, module):
  194. if not self.matches(module.__name__):
  195. npd.log.debug("Doctest doesn't want module %s", module)
  196. return
  197. try:
  198. tests = self.finder.find(module)
  199. except AttributeError:
  200. # nose allows module.__test__ = False; doctest does not and
  201. # throws AttributeError
  202. return
  203. if not tests:
  204. return
  205. tests.sort()
  206. module_file = src(module.__file__)
  207. for test in tests:
  208. if not test.examples:
  209. continue
  210. if not test.filename:
  211. test.filename = module_file
  212. # Set test namespace; test altered in place
  213. self.set_test_context(test)
  214. yield self.doctest_case_class(test,
  215. optionflags=self.doctest_optflags,
  216. checker=self.out_check_class(),
  217. result_var=self.doctest_result_var)
  218. # Add an afterContext method to nose.plugins.doctests.Doctest in order
  219. # to restore print options to the original state after each doctest
  220. def afterContext(self):
  221. numpy.set_printoptions(**print_state)
  222. # Ignore NumPy-specific build files that shouldn't be searched for tests
  223. def wantFile(self, file):
  224. bn = os.path.basename(file)
  225. if bn in self.doctest_ignore:
  226. return False
  227. return npd.Doctest.wantFile(self, file)
  228. class Unplugger:
  229. """ Nose plugin to remove named plugin late in loading
  230. By default it removes the "doctest" plugin.
  231. """
  232. name = 'unplugger'
  233. enabled = True # always enabled
  234. score = 4000 # load late in order to be after builtins
  235. def __init__(self, to_unplug='doctest'):
  236. self.to_unplug = to_unplug
  237. def options(self, parser, env):
  238. pass
  239. def configure(self, options, config):
  240. # Pull named plugin out of plugins list
  241. config.plugins.plugins = [p for p in config.plugins.plugins
  242. if p.name != self.to_unplug]
  243. class KnownFailurePlugin(ErrorClassPlugin):
  244. '''Plugin that installs a KNOWNFAIL error class for the
  245. KnownFailureClass exception. When KnownFailure is raised,
  246. the exception will be logged in the knownfail attribute of the
  247. result, 'K' or 'KNOWNFAIL' (verbose) will be output, and the
  248. exception will not be counted as an error or failure.'''
  249. enabled = True
  250. knownfail = ErrorClass(KnownFailureException,
  251. label='KNOWNFAIL',
  252. isfailure=False)
  253. def options(self, parser, env=os.environ):
  254. env_opt = 'NOSE_WITHOUT_KNOWNFAIL'
  255. parser.add_option('--no-knownfail', action='store_true',
  256. dest='noKnownFail', default=env.get(env_opt, False),
  257. help='Disable special handling of KnownFailure '
  258. 'exceptions')
  259. def configure(self, options, conf):
  260. if not self.can_configure:
  261. return
  262. self.conf = conf
  263. disable = getattr(options, 'noKnownFail', False)
  264. if disable:
  265. self.enabled = False
  266. KnownFailure = KnownFailurePlugin # backwards compat
  267. class FPUModeCheckPlugin(Plugin):
  268. """
  269. Plugin that checks the FPU mode before and after each test,
  270. raising failures if the test changed the mode.
  271. """
  272. def prepareTestCase(self, test):
  273. from numpy.core._multiarray_tests import get_fpu_mode
  274. def run(result):
  275. old_mode = get_fpu_mode()
  276. test.test(result)
  277. new_mode = get_fpu_mode()
  278. if old_mode != new_mode:
  279. try:
  280. raise AssertionError(
  281. "FPU mode changed from {0:#x} to {1:#x} during the "
  282. "test".format(old_mode, new_mode))
  283. except AssertionError:
  284. result.addFailure(test, sys.exc_info())
  285. return run
  286. # Class allows us to save the results of the tests in runTests - see runTests
  287. # method docstring for details
  288. class NumpyTestProgram(nose.core.TestProgram):
  289. def runTests(self):
  290. """Run Tests. Returns true on success, false on failure, and
  291. sets self.success to the same value.
  292. Because nose currently discards the test result object, but we need
  293. to return it to the user, override TestProgram.runTests to retain
  294. the result
  295. """
  296. if self.testRunner is None:
  297. self.testRunner = nose.core.TextTestRunner(stream=self.config.stream,
  298. verbosity=self.config.verbosity,
  299. config=self.config)
  300. plug_runner = self.config.plugins.prepareTestRunner(self.testRunner)
  301. if plug_runner is not None:
  302. self.testRunner = plug_runner
  303. self.result = self.testRunner.run(self.test)
  304. self.success = self.result.wasSuccessful()
  305. return self.success