tools.py 14 KB

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