iptest.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. # -*- coding: utf-8 -*-
  2. """IPython Test Suite Runner.
  3. This module provides a main entry point to a user script to test IPython
  4. itself from the command line. There are two ways of running this script:
  5. 1. With the syntax `iptest all`. This runs our entire test suite by
  6. calling this script (with different arguments) recursively. This
  7. causes modules and package to be tested in different processes, using nose
  8. or trial where appropriate.
  9. 2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
  10. the script simply calls nose, but with special command line flags and
  11. plugins loaded.
  12. """
  13. # Copyright (c) IPython Development Team.
  14. # Distributed under the terms of the Modified BSD License.
  15. from __future__ import print_function
  16. import glob
  17. from io import BytesIO
  18. import os
  19. import os.path as path
  20. import sys
  21. from threading import Thread, Lock, Event
  22. import warnings
  23. import nose.plugins.builtin
  24. from nose.plugins.xunit import Xunit
  25. from nose import SkipTest
  26. from nose.core import TestProgram
  27. from nose.plugins import Plugin
  28. from nose.util import safe_str
  29. from IPython import version_info
  30. from IPython.utils.py3compat import bytes_to_str
  31. from IPython.utils.importstring import import_item
  32. from IPython.testing.plugin.ipdoctest import IPythonDoctest
  33. from IPython.external.decorators import KnownFailure, knownfailureif
  34. pjoin = path.join
  35. # Enable printing all warnings raise by IPython's modules
  36. warnings.filterwarnings('ignore', message='.*Matplotlib is building the font cache.*', category=UserWarning, module='.*')
  37. if sys.version_info > (3,0):
  38. warnings.filterwarnings('error', message='.*', category=ResourceWarning, module='.*')
  39. warnings.filterwarnings('error', message=".*{'config': True}.*", category=DeprecationWarning, module='IPy.*')
  40. warnings.filterwarnings('default', message='.*', category=Warning, module='IPy.*')
  41. if version_info < (6,):
  42. # nose.tools renames all things from `camelCase` to `snake_case` which raise an
  43. # warning with the runner they also import from standard import library. (as of Dec 2015)
  44. # Ignore, let's revisit that in a couple of years for IPython 6.
  45. warnings.filterwarnings('ignore', message='.*Please use assertEqual instead', category=Warning, module='IPython.*')
  46. # ------------------------------------------------------------------------------
  47. # Monkeypatch Xunit to count known failures as skipped.
  48. # ------------------------------------------------------------------------------
  49. def monkeypatch_xunit():
  50. try:
  51. knownfailureif(True)(lambda: None)()
  52. except Exception as e:
  53. KnownFailureTest = type(e)
  54. def addError(self, test, err, capt=None):
  55. if issubclass(err[0], KnownFailureTest):
  56. err = (SkipTest,) + err[1:]
  57. return self.orig_addError(test, err, capt)
  58. Xunit.orig_addError = Xunit.addError
  59. Xunit.addError = addError
  60. #-----------------------------------------------------------------------------
  61. # Check which dependencies are installed and greater than minimum version.
  62. #-----------------------------------------------------------------------------
  63. def extract_version(mod):
  64. return mod.__version__
  65. def test_for(item, min_version=None, callback=extract_version):
  66. """Test to see if item is importable, and optionally check against a minimum
  67. version.
  68. If min_version is given, the default behavior is to check against the
  69. `__version__` attribute of the item, but specifying `callback` allows you to
  70. extract the value you are interested in. e.g::
  71. In [1]: import sys
  72. In [2]: from IPython.testing.iptest import test_for
  73. In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
  74. Out[3]: True
  75. """
  76. try:
  77. check = import_item(item)
  78. except (ImportError, RuntimeError):
  79. # GTK reports Runtime error if it can't be initialized even if it's
  80. # importable.
  81. return False
  82. else:
  83. if min_version:
  84. if callback:
  85. # extra processing step to get version to compare
  86. check = callback(check)
  87. return check >= min_version
  88. else:
  89. return True
  90. # Global dict where we can store information on what we have and what we don't
  91. # have available at test run time
  92. have = {'matplotlib': test_for('matplotlib'),
  93. 'pygments': test_for('pygments'),
  94. 'sqlite3': test_for('sqlite3')}
  95. #-----------------------------------------------------------------------------
  96. # Test suite definitions
  97. #-----------------------------------------------------------------------------
  98. test_group_names = ['core',
  99. 'extensions', 'lib', 'terminal', 'testing', 'utils',
  100. ]
  101. class TestSection(object):
  102. def __init__(self, name, includes):
  103. self.name = name
  104. self.includes = includes
  105. self.excludes = []
  106. self.dependencies = []
  107. self.enabled = True
  108. def exclude(self, module):
  109. if not module.startswith('IPython'):
  110. module = self.includes[0] + "." + module
  111. self.excludes.append(module.replace('.', os.sep))
  112. def requires(self, *packages):
  113. self.dependencies.extend(packages)
  114. @property
  115. def will_run(self):
  116. return self.enabled and all(have[p] for p in self.dependencies)
  117. # Name -> (include, exclude, dependencies_met)
  118. test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
  119. # Exclusions and dependencies
  120. # ---------------------------
  121. # core:
  122. sec = test_sections['core']
  123. if not have['sqlite3']:
  124. sec.exclude('tests.test_history')
  125. sec.exclude('history')
  126. if not have['matplotlib']:
  127. sec.exclude('pylabtools'),
  128. sec.exclude('tests.test_pylabtools')
  129. # lib:
  130. sec = test_sections['lib']
  131. sec.exclude('kernel')
  132. if not have['pygments']:
  133. sec.exclude('tests.test_lexers')
  134. # We do this unconditionally, so that the test suite doesn't import
  135. # gtk, changing the default encoding and masking some unicode bugs.
  136. sec.exclude('inputhookgtk')
  137. # We also do this unconditionally, because wx can interfere with Unix signals.
  138. # There are currently no tests for it anyway.
  139. sec.exclude('inputhookwx')
  140. # Testing inputhook will need a lot of thought, to figure out
  141. # how to have tests that don't lock up with the gui event
  142. # loops in the picture
  143. sec.exclude('inputhook')
  144. # testing:
  145. sec = test_sections['testing']
  146. # These have to be skipped on win32 because they use echo, rm, cd, etc.
  147. # See ticket https://github.com/ipython/ipython/issues/87
  148. if sys.platform == 'win32':
  149. sec.exclude('plugin.test_exampleip')
  150. sec.exclude('plugin.dtexample')
  151. # don't run jupyter_console tests found via shim
  152. test_sections['terminal'].exclude('console')
  153. # extensions:
  154. sec = test_sections['extensions']
  155. # This is deprecated in favour of rpy2
  156. sec.exclude('rmagic')
  157. # autoreload does some strange stuff, so move it to its own test section
  158. sec.exclude('autoreload')
  159. sec.exclude('tests.test_autoreload')
  160. test_sections['autoreload'] = TestSection('autoreload',
  161. ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
  162. test_group_names.append('autoreload')
  163. #-----------------------------------------------------------------------------
  164. # Functions and classes
  165. #-----------------------------------------------------------------------------
  166. def check_exclusions_exist():
  167. from IPython.paths import get_ipython_package_dir
  168. from warnings import warn
  169. parent = os.path.dirname(get_ipython_package_dir())
  170. for sec in test_sections:
  171. for pattern in sec.exclusions:
  172. fullpath = pjoin(parent, pattern)
  173. if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
  174. warn("Excluding nonexistent file: %r" % pattern)
  175. class ExclusionPlugin(Plugin):
  176. """A nose plugin to effect our exclusions of files and directories.
  177. """
  178. name = 'exclusions'
  179. score = 3000 # Should come before any other plugins
  180. def __init__(self, exclude_patterns=None):
  181. """
  182. Parameters
  183. ----------
  184. exclude_patterns : sequence of strings, optional
  185. Filenames containing these patterns (as raw strings, not as regular
  186. expressions) are excluded from the tests.
  187. """
  188. self.exclude_patterns = exclude_patterns or []
  189. super(ExclusionPlugin, self).__init__()
  190. def options(self, parser, env=os.environ):
  191. Plugin.options(self, parser, env)
  192. def configure(self, options, config):
  193. Plugin.configure(self, options, config)
  194. # Override nose trying to disable plugin.
  195. self.enabled = True
  196. def wantFile(self, filename):
  197. """Return whether the given filename should be scanned for tests.
  198. """
  199. if any(pat in filename for pat in self.exclude_patterns):
  200. return False
  201. return None
  202. def wantDirectory(self, directory):
  203. """Return whether the given directory should be scanned for tests.
  204. """
  205. if any(pat in directory for pat in self.exclude_patterns):
  206. return False
  207. return None
  208. class StreamCapturer(Thread):
  209. daemon = True # Don't hang if main thread crashes
  210. started = False
  211. def __init__(self, echo=False):
  212. super(StreamCapturer, self).__init__()
  213. self.echo = echo
  214. self.streams = []
  215. self.buffer = BytesIO()
  216. self.readfd, self.writefd = os.pipe()
  217. self.buffer_lock = Lock()
  218. self.stop = Event()
  219. def run(self):
  220. self.started = True
  221. while not self.stop.is_set():
  222. chunk = os.read(self.readfd, 1024)
  223. with self.buffer_lock:
  224. self.buffer.write(chunk)
  225. if self.echo:
  226. sys.stdout.write(bytes_to_str(chunk))
  227. os.close(self.readfd)
  228. os.close(self.writefd)
  229. def reset_buffer(self):
  230. with self.buffer_lock:
  231. self.buffer.truncate(0)
  232. self.buffer.seek(0)
  233. def get_buffer(self):
  234. with self.buffer_lock:
  235. return self.buffer.getvalue()
  236. def ensure_started(self):
  237. if not self.started:
  238. self.start()
  239. def halt(self):
  240. """Safely stop the thread."""
  241. if not self.started:
  242. return
  243. self.stop.set()
  244. os.write(self.writefd, b'\0') # Ensure we're not locked in a read()
  245. self.join()
  246. class SubprocessStreamCapturePlugin(Plugin):
  247. name='subprocstreams'
  248. def __init__(self):
  249. Plugin.__init__(self)
  250. self.stream_capturer = StreamCapturer()
  251. self.destination = os.environ.get('IPTEST_SUBPROC_STREAMS', 'capture')
  252. # This is ugly, but distant parts of the test machinery need to be able
  253. # to redirect streams, so we make the object globally accessible.
  254. nose.iptest_stdstreams_fileno = self.get_write_fileno
  255. def get_write_fileno(self):
  256. if self.destination == 'capture':
  257. self.stream_capturer.ensure_started()
  258. return self.stream_capturer.writefd
  259. elif self.destination == 'discard':
  260. return os.open(os.devnull, os.O_WRONLY)
  261. else:
  262. return sys.__stdout__.fileno()
  263. def configure(self, options, config):
  264. Plugin.configure(self, options, config)
  265. # Override nose trying to disable plugin.
  266. if self.destination == 'capture':
  267. self.enabled = True
  268. def startTest(self, test):
  269. # Reset log capture
  270. self.stream_capturer.reset_buffer()
  271. def formatFailure(self, test, err):
  272. # Show output
  273. ec, ev, tb = err
  274. captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
  275. if captured.strip():
  276. ev = safe_str(ev)
  277. out = [ev, '>> begin captured subprocess output <<',
  278. captured,
  279. '>> end captured subprocess output <<']
  280. return ec, '\n'.join(out), tb
  281. return err
  282. formatError = formatFailure
  283. def finalize(self, result):
  284. self.stream_capturer.halt()
  285. def run_iptest():
  286. """Run the IPython test suite using nose.
  287. This function is called when this script is **not** called with the form
  288. `iptest all`. It simply calls nose with appropriate command line flags
  289. and accepts all of the standard nose arguments.
  290. """
  291. # Apply our monkeypatch to Xunit
  292. if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
  293. monkeypatch_xunit()
  294. arg1 = sys.argv[1]
  295. if arg1 in test_sections:
  296. section = test_sections[arg1]
  297. sys.argv[1:2] = section.includes
  298. elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
  299. section = test_sections[arg1[8:]]
  300. sys.argv[1:2] = section.includes
  301. else:
  302. section = TestSection(arg1, includes=[arg1])
  303. argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
  304. # We add --exe because of setuptools' imbecility (it
  305. # blindly does chmod +x on ALL files). Nose does the
  306. # right thing and it tries to avoid executables,
  307. # setuptools unfortunately forces our hand here. This
  308. # has been discussed on the distutils list and the
  309. # setuptools devs refuse to fix this problem!
  310. '--exe',
  311. ]
  312. if '-a' not in argv and '-A' not in argv:
  313. argv = argv + ['-a', '!crash']
  314. if nose.__version__ >= '0.11':
  315. # I don't fully understand why we need this one, but depending on what
  316. # directory the test suite is run from, if we don't give it, 0 tests
  317. # get run. Specifically, if the test suite is run from the source dir
  318. # with an argument (like 'iptest.py IPython.core', 0 tests are run,
  319. # even if the same call done in this directory works fine). It appears
  320. # that if the requested package is in the current dir, nose bails early
  321. # by default. Since it's otherwise harmless, leave it in by default
  322. # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
  323. argv.append('--traverse-namespace')
  324. plugins = [ ExclusionPlugin(section.excludes), KnownFailure(),
  325. SubprocessStreamCapturePlugin() ]
  326. # we still have some vestigial doctests in core
  327. if (section.name.startswith(('core', 'IPython.core'))):
  328. plugins.append(IPythonDoctest())
  329. argv.extend([
  330. '--with-ipdoctest',
  331. '--ipdoctest-tests',
  332. '--ipdoctest-extension=txt',
  333. ])
  334. # Use working directory set by parent process (see iptestcontroller)
  335. if 'IPTEST_WORKING_DIR' in os.environ:
  336. os.chdir(os.environ['IPTEST_WORKING_DIR'])
  337. # We need a global ipython running in this process, but the special
  338. # in-process group spawns its own IPython kernels, so for *that* group we
  339. # must avoid also opening the global one (otherwise there's a conflict of
  340. # singletons). Ultimately the solution to this problem is to refactor our
  341. # assumptions about what needs to be a singleton and what doesn't (app
  342. # objects should, individual shells shouldn't). But for now, this
  343. # workaround allows the test suite for the inprocess module to complete.
  344. if 'kernel.inprocess' not in section.name:
  345. from IPython.testing import globalipapp
  346. globalipapp.start_ipython()
  347. # Now nose can run
  348. TestProgram(argv=argv, addplugins=plugins)
  349. if __name__ == '__main__':
  350. run_iptest()