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