tools.py 14 KB

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