iptest.py 16 KB

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