tools.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. """Generic testing tools.
  2. Authors
  3. -------
  4. - Fernando Perez <Fernando.Perez@berkeley.edu>
  5. """
  6. from __future__ import absolute_import
  7. # Copyright (c) IPython Development Team.
  8. # Distributed under the terms of the Modified BSD License.
  9. import os
  10. import re
  11. import sys
  12. import tempfile
  13. from contextlib import contextmanager
  14. from io import StringIO
  15. from subprocess import Popen, PIPE
  16. try:
  17. from unittest.mock import patch
  18. except ImportError:
  19. # Python 2 compatibility
  20. from mock import patch
  21. try:
  22. # These tools are used by parts of the runtime, so we make the nose
  23. # dependency optional at this point. Nose is a hard dependency to run the
  24. # test suite, but NOT to use ipython itself.
  25. import nose.tools as nt
  26. has_nose = True
  27. except ImportError:
  28. has_nose = False
  29. from traitlets.config.loader import Config
  30. from IPython.utils.process import get_output_error_code
  31. from IPython.utils.text import list_strings
  32. from IPython.utils.io import temp_pyfile, Tee
  33. from IPython.utils import py3compat
  34. from IPython.utils.encoding import DEFAULT_ENCODING
  35. from . import decorators as dec
  36. from . import skipdoctest
  37. # The docstring for full_path doctests differently on win32 (different path
  38. # separator) so just skip the doctest there. The example remains informative.
  39. doctest_deco = skipdoctest.skip_doctest if sys.platform == 'win32' else dec.null_deco
  40. @doctest_deco
  41. def full_path(startPath,files):
  42. """Make full paths for all the listed files, based on startPath.
  43. Only the base part of startPath is kept, since this routine is typically
  44. used with a script's ``__file__`` variable as startPath. The base of startPath
  45. is then prepended to all the listed files, forming the output list.
  46. Parameters
  47. ----------
  48. startPath : string
  49. Initial path to use as the base for the results. This path is split
  50. using os.path.split() and only its first component is kept.
  51. files : string or list
  52. One or more files.
  53. Examples
  54. --------
  55. >>> full_path('/foo/bar.py',['a.txt','b.txt'])
  56. ['/foo/a.txt', '/foo/b.txt']
  57. >>> full_path('/foo',['a.txt','b.txt'])
  58. ['/a.txt', '/b.txt']
  59. If a single file is given, the output is still a list::
  60. >>> full_path('/foo','a.txt')
  61. ['/a.txt']
  62. """
  63. files = list_strings(files)
  64. base = os.path.split(startPath)[0]
  65. return [ os.path.join(base,f) for f in files ]
  66. def parse_test_output(txt):
  67. """Parse the output of a test run and return errors, failures.
  68. Parameters
  69. ----------
  70. txt : str
  71. Text output of a test run, assumed to contain a line of one of the
  72. following forms::
  73. 'FAILED (errors=1)'
  74. 'FAILED (failures=1)'
  75. 'FAILED (errors=1, failures=1)'
  76. Returns
  77. -------
  78. nerr, nfail
  79. number of errors and failures.
  80. """
  81. err_m = re.search(r'^FAILED \(errors=(\d+)\)', txt, re.MULTILINE)
  82. if err_m:
  83. nerr = int(err_m.group(1))
  84. nfail = 0
  85. return nerr, nfail
  86. fail_m = re.search(r'^FAILED \(failures=(\d+)\)', txt, re.MULTILINE)
  87. if fail_m:
  88. nerr = 0
  89. nfail = int(fail_m.group(1))
  90. return nerr, nfail
  91. both_m = re.search(r'^FAILED \(errors=(\d+), failures=(\d+)\)', txt,
  92. re.MULTILINE)
  93. if both_m:
  94. nerr = int(both_m.group(1))
  95. nfail = int(both_m.group(2))
  96. return nerr, nfail
  97. # If the input didn't match any of these forms, assume no error/failures
  98. return 0, 0
  99. # So nose doesn't think this is a test
  100. parse_test_output.__test__ = False
  101. def default_argv():
  102. """Return a valid default argv for creating testing instances of ipython"""
  103. return ['--quick', # so no config file is loaded
  104. # Other defaults to minimize side effects on stdout
  105. '--colors=NoColor', '--no-term-title','--no-banner',
  106. '--autocall=0']
  107. def default_config():
  108. """Return a config object with good defaults for testing."""
  109. config = Config()
  110. config.TerminalInteractiveShell.colors = 'NoColor'
  111. config.TerminalTerminalInteractiveShell.term_title = False,
  112. config.TerminalInteractiveShell.autocall = 0
  113. f = tempfile.NamedTemporaryFile(suffix=u'test_hist.sqlite', delete=False)
  114. config.HistoryManager.hist_file = f.name
  115. f.close()
  116. config.HistoryManager.db_cache_size = 10000
  117. return config
  118. def get_ipython_cmd(as_string=False):
  119. """
  120. Return appropriate IPython command line name. By default, this will return
  121. a list that can be used with subprocess.Popen, for example, but passing
  122. `as_string=True` allows for returning the IPython command as a string.
  123. Parameters
  124. ----------
  125. as_string: bool
  126. Flag to allow to return the command as a string.
  127. """
  128. ipython_cmd = [sys.executable, "-m", "IPython"]
  129. if as_string:
  130. ipython_cmd = " ".join(ipython_cmd)
  131. return ipython_cmd
  132. def ipexec(fname, options=None, commands=()):
  133. """Utility to call 'ipython filename'.
  134. Starts IPython with a minimal and safe configuration to make startup as fast
  135. as possible.
  136. Note that this starts IPython in a subprocess!
  137. Parameters
  138. ----------
  139. fname : str
  140. Name of file to be executed (should have .py or .ipy extension).
  141. options : optional, list
  142. Extra command-line flags to be passed to IPython.
  143. commands : optional, list
  144. Commands to send in on stdin
  145. Returns
  146. -------
  147. (stdout, stderr) of ipython subprocess.
  148. """
  149. if options is None: options = []
  150. cmdargs = default_argv() + options
  151. test_dir = os.path.dirname(__file__)
  152. ipython_cmd = get_ipython_cmd()
  153. # Absolute path for filename
  154. full_fname = os.path.join(test_dir, fname)
  155. full_cmd = ipython_cmd + cmdargs + [full_fname]
  156. env = os.environ.copy()
  157. # FIXME: ignore all warnings in ipexec while we have shims
  158. # should we keep suppressing warnings here, even after removing shims?
  159. env['PYTHONWARNINGS'] = 'ignore'
  160. # env.pop('PYTHONWARNINGS', None) # Avoid extraneous warnings appearing on stderr
  161. for k, v in env.items():
  162. # Debug a bizarre failure we've seen on Windows:
  163. # TypeError: environment can only contain strings
  164. if not isinstance(v, str):
  165. print(k, v)
  166. p = Popen(full_cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE, env=env)
  167. out, err = p.communicate(input=py3compat.str_to_bytes('\n'.join(commands)) or None)
  168. out, err = py3compat.bytes_to_str(out), py3compat.bytes_to_str(err)
  169. # `import readline` causes 'ESC[?1034h' to be output sometimes,
  170. # so strip that out before doing comparisons
  171. if out:
  172. out = re.sub(r'\x1b\[[^h]+h', '', out)
  173. return out, err
  174. def ipexec_validate(fname, expected_out, expected_err='',
  175. options=None, commands=()):
  176. """Utility to call 'ipython filename' and validate output/error.
  177. This function raises an AssertionError if the validation fails.
  178. Note that this starts IPython in a subprocess!
  179. Parameters
  180. ----------
  181. fname : str
  182. Name of the file to be executed (should have .py or .ipy extension).
  183. expected_out : str
  184. Expected stdout of the process.
  185. expected_err : optional, str
  186. Expected stderr of the process.
  187. options : optional, list
  188. Extra command-line flags to be passed to IPython.
  189. Returns
  190. -------
  191. None
  192. """
  193. import nose.tools as nt
  194. out, err = ipexec(fname, options, commands)
  195. #print 'OUT', out # dbg
  196. #print 'ERR', err # dbg
  197. # If there are any errors, we must check those befor stdout, as they may be
  198. # more informative than simply having an empty stdout.
  199. if err:
  200. if expected_err:
  201. nt.assert_equal("\n".join(err.strip().splitlines()), "\n".join(expected_err.strip().splitlines()))
  202. else:
  203. raise ValueError('Running file %r produced error: %r' %
  204. (fname, err))
  205. # If no errors or output on stderr was expected, match stdout
  206. nt.assert_equal("\n".join(out.strip().splitlines()), "\n".join(expected_out.strip().splitlines()))
  207. class TempFileMixin(object):
  208. """Utility class to create temporary Python/IPython files.
  209. Meant as a mixin class for test cases."""
  210. def mktmp(self, src, ext='.py'):
  211. """Make a valid python temp file."""
  212. fname, f = temp_pyfile(src, ext)
  213. self.tmpfile = f
  214. self.fname = fname
  215. def tearDown(self):
  216. if hasattr(self, 'tmpfile'):
  217. # If the tmpfile wasn't made because of skipped tests, like in
  218. # win32, there's nothing to cleanup.
  219. self.tmpfile.close()
  220. try:
  221. os.unlink(self.fname)
  222. except:
  223. # On Windows, even though we close the file, we still can't
  224. # delete it. I have no clue why
  225. pass
  226. def __enter__(self):
  227. return self
  228. def __exit__(self, exc_type, exc_value, traceback):
  229. self.tearDown()
  230. pair_fail_msg = ("Testing {0}\n\n"
  231. "In:\n"
  232. " {1!r}\n"
  233. "Expected:\n"
  234. " {2!r}\n"
  235. "Got:\n"
  236. " {3!r}\n")
  237. def check_pairs(func, pairs):
  238. """Utility function for the common case of checking a function with a
  239. sequence of input/output pairs.
  240. Parameters
  241. ----------
  242. func : callable
  243. The function to be tested. Should accept a single argument.
  244. pairs : iterable
  245. A list of (input, expected_output) tuples.
  246. Returns
  247. -------
  248. None. Raises an AssertionError if any output does not match the expected
  249. value.
  250. """
  251. name = getattr(func, "func_name", getattr(func, "__name__", "<unknown>"))
  252. for inp, expected in pairs:
  253. out = func(inp)
  254. assert out == expected, pair_fail_msg.format(name, inp, expected, out)
  255. if py3compat.PY3:
  256. MyStringIO = StringIO
  257. else:
  258. # In Python 2, stdout/stderr can have either bytes or unicode written to them,
  259. # so we need a class that can handle both.
  260. class MyStringIO(StringIO):
  261. def write(self, s):
  262. s = py3compat.cast_unicode(s, encoding=DEFAULT_ENCODING)
  263. super(MyStringIO, self).write(s)
  264. _re_type = type(re.compile(r''))
  265. notprinted_msg = """Did not find {0!r} in printed output (on {1}):
  266. -------
  267. {2!s}
  268. -------
  269. """
  270. class AssertPrints(object):
  271. """Context manager for testing that code prints certain text.
  272. Examples
  273. --------
  274. >>> with AssertPrints("abc", suppress=False):
  275. ... print("abcd")
  276. ... print("def")
  277. ...
  278. abcd
  279. def
  280. """
  281. def __init__(self, s, channel='stdout', suppress=True):
  282. self.s = s
  283. if isinstance(self.s, (py3compat.string_types, _re_type)):
  284. self.s = [self.s]
  285. self.channel = channel
  286. self.suppress = suppress
  287. def __enter__(self):
  288. self.orig_stream = getattr(sys, self.channel)
  289. self.buffer = MyStringIO()
  290. self.tee = Tee(self.buffer, channel=self.channel)
  291. setattr(sys, self.channel, self.buffer if self.suppress else self.tee)
  292. def __exit__(self, etype, value, traceback):
  293. try:
  294. if value is not None:
  295. # If an error was raised, don't check anything else
  296. return False
  297. self.tee.flush()
  298. setattr(sys, self.channel, self.orig_stream)
  299. printed = self.buffer.getvalue()
  300. for s in self.s:
  301. if isinstance(s, _re_type):
  302. assert s.search(printed), notprinted_msg.format(s.pattern, self.channel, printed)
  303. else:
  304. assert s in printed, notprinted_msg.format(s, self.channel, printed)
  305. return False
  306. finally:
  307. self.tee.close()
  308. printed_msg = """Found {0!r} in printed output (on {1}):
  309. -------
  310. {2!s}
  311. -------
  312. """
  313. class AssertNotPrints(AssertPrints):
  314. """Context manager for checking that certain output *isn't* produced.
  315. Counterpart of AssertPrints"""
  316. def __exit__(self, etype, value, traceback):
  317. try:
  318. if value is not None:
  319. # If an error was raised, don't check anything else
  320. self.tee.close()
  321. return False
  322. self.tee.flush()
  323. setattr(sys, self.channel, self.orig_stream)
  324. printed = self.buffer.getvalue()
  325. for s in self.s:
  326. if isinstance(s, _re_type):
  327. assert not s.search(printed),printed_msg.format(
  328. s.pattern, self.channel, printed)
  329. else:
  330. assert s not in printed, printed_msg.format(
  331. s, self.channel, printed)
  332. return False
  333. finally:
  334. self.tee.close()
  335. @contextmanager
  336. def mute_warn():
  337. from IPython.utils import warn
  338. save_warn = warn.warn
  339. warn.warn = lambda *a, **kw: None
  340. try:
  341. yield
  342. finally:
  343. warn.warn = save_warn
  344. @contextmanager
  345. def make_tempfile(name):
  346. """ Create an empty, named, temporary file for the duration of the context.
  347. """
  348. f = open(name, 'w')
  349. f.close()
  350. try:
  351. yield
  352. finally:
  353. os.unlink(name)
  354. def fake_input(inputs):
  355. """Temporarily replace the input() function to return the given values
  356. Use as a context manager:
  357. with fake_input(['result1', 'result2']):
  358. ...
  359. Values are returned in order. If input() is called again after the last value
  360. was used, EOFError is raised.
  361. """
  362. it = iter(inputs)
  363. def mock_input(prompt=''):
  364. try:
  365. return next(it)
  366. except StopIteration:
  367. raise EOFError('No more inputs given')
  368. input_name = '%s.%s' % (py3compat.builtin_mod_name,
  369. 'input' if py3compat.PY3 else 'raw_input')
  370. return patch(input_name, mock_input)
  371. def help_output_test(subcommand=''):
  372. """test that `ipython [subcommand] -h` works"""
  373. cmd = get_ipython_cmd() + [subcommand, '-h']
  374. out, err, rc = get_output_error_code(cmd)
  375. nt.assert_equal(rc, 0, err)
  376. nt.assert_not_in("Traceback", err)
  377. nt.assert_in("Options", out)
  378. nt.assert_in("--help-all", out)
  379. return out, err
  380. def help_all_output_test(subcommand=''):
  381. """test that `ipython [subcommand] --help-all` works"""
  382. cmd = get_ipython_cmd() + [subcommand, '--help-all']
  383. out, err, rc = get_output_error_code(cmd)
  384. nt.assert_equal(rc, 0, err)
  385. nt.assert_not_in("Traceback", err)
  386. nt.assert_in("Options", out)
  387. nt.assert_in("Class", out)
  388. return out, err