##// END OF EJS Templates
add assert_big_text_equal...
MinRK -
Show More
@@ -1,36 +1,38 b''
1 1 """Tests for notebook.py"""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import json
7 7
8 8 from .base import ExportersTestsBase
9 9 from ..notebook import NotebookExporter
10 10
11 from IPython.testing.tools import assert_big_text_equal
12
11 13 class TestNotebookExporter(ExportersTestsBase):
12 14 """Contains test functions for notebook.py"""
13 15
14 16 exporter_class = NotebookExporter
15 17
16 18 def test_export(self):
17 19 """
18 20 Does the NotebookExporter return the file unchanged?
19 21 """
20 22 with open(self._get_notebook()) as f:
21 23 file_contents = f.read()
22 24 (output, resources) = self.exporter_class().from_filename(self._get_notebook())
23 25 assert len(output) > 0
24 self.assertEqual(output, file_contents)
26 assert_big_text_equal(output, file_contents)
25 27
26 28 def test_downgrade_3(self):
27 29 exporter = self.exporter_class(nbformat_version=3)
28 30 (output, resources) = exporter.from_filename(self._get_notebook())
29 31 nb = json.loads(output)
30 32 self.assertEqual(nb['nbformat'], 3)
31 33
32 34 def test_downgrade_2(self):
33 35 exporter = self.exporter_class(nbformat_version=2)
34 36 (output, resources) = exporter.from_filename(self._get_notebook())
35 37 nb = json.loads(output)
36 38 self.assertEqual(nb['nbformat'], 2)
@@ -1,467 +1,487 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 f = tempfile.NamedTemporaryFile(suffix=u'test_hist.sqlite', delete=False)
156 156 config.HistoryManager.hist_file = f.name
157 157 f.close()
158 158 config.HistoryManager.db_cache_size = 10000
159 159 return config
160 160
161 161
162 162 def get_ipython_cmd(as_string=False):
163 163 """
164 164 Return appropriate IPython command line name. By default, this will return
165 165 a list that can be used with subprocess.Popen, for example, but passing
166 166 `as_string=True` allows for returning the IPython command as a string.
167 167
168 168 Parameters
169 169 ----------
170 170 as_string: bool
171 171 Flag to allow to return the command as a string.
172 172 """
173 173 ipython_cmd = [sys.executable, "-m", "IPython"]
174 174
175 175 if as_string:
176 176 ipython_cmd = " ".join(ipython_cmd)
177 177
178 178 return ipython_cmd
179 179
180 180 def ipexec(fname, options=None):
181 181 """Utility to call 'ipython filename'.
182 182
183 183 Starts IPython with a minimal and safe configuration to make startup as fast
184 184 as possible.
185 185
186 186 Note that this starts IPython in a subprocess!
187 187
188 188 Parameters
189 189 ----------
190 190 fname : str
191 191 Name of file to be executed (should have .py or .ipy extension).
192 192
193 193 options : optional, list
194 194 Extra command-line flags to be passed to IPython.
195 195
196 196 Returns
197 197 -------
198 198 (stdout, stderr) of ipython subprocess.
199 199 """
200 200 if options is None: options = []
201 201
202 202 # For these subprocess calls, eliminate all prompt printing so we only see
203 203 # output from script execution
204 204 prompt_opts = [ '--PromptManager.in_template=""',
205 205 '--PromptManager.in2_template=""',
206 206 '--PromptManager.out_template=""'
207 207 ]
208 208 cmdargs = default_argv() + prompt_opts + options
209 209
210 210 test_dir = os.path.dirname(__file__)
211 211
212 212 ipython_cmd = get_ipython_cmd()
213 213 # Absolute path for filename
214 214 full_fname = os.path.join(test_dir, fname)
215 215 full_cmd = ipython_cmd + cmdargs + [full_fname]
216 216 env = os.environ.copy()
217 217 env.pop('PYTHONWARNINGS', None) # Avoid extraneous warnings appearing on stderr
218 218 p = Popen(full_cmd, stdout=PIPE, stderr=PIPE, env=env)
219 219 out, err = p.communicate()
220 220 out, err = py3compat.bytes_to_str(out), py3compat.bytes_to_str(err)
221 221 # `import readline` causes 'ESC[?1034h' to be output sometimes,
222 222 # so strip that out before doing comparisons
223 223 if out:
224 224 out = re.sub(r'\x1b\[[^h]+h', '', out)
225 225 return out, err
226 226
227 227
228 228 def ipexec_validate(fname, expected_out, expected_err='',
229 229 options=None):
230 230 """Utility to call 'ipython filename' and validate output/error.
231 231
232 232 This function raises an AssertionError if the validation fails.
233 233
234 234 Note that this starts IPython in a subprocess!
235 235
236 236 Parameters
237 237 ----------
238 238 fname : str
239 239 Name of the file to be executed (should have .py or .ipy extension).
240 240
241 241 expected_out : str
242 242 Expected stdout of the process.
243 243
244 244 expected_err : optional, str
245 245 Expected stderr of the process.
246 246
247 247 options : optional, list
248 248 Extra command-line flags to be passed to IPython.
249 249
250 250 Returns
251 251 -------
252 252 None
253 253 """
254 254
255 255 import nose.tools as nt
256 256
257 257 out, err = ipexec(fname, options)
258 258 #print 'OUT', out # dbg
259 259 #print 'ERR', err # dbg
260 260 # If there are any errors, we must check those befor stdout, as they may be
261 261 # more informative than simply having an empty stdout.
262 262 if err:
263 263 if expected_err:
264 264 nt.assert_equal("\n".join(err.strip().splitlines()), "\n".join(expected_err.strip().splitlines()))
265 265 else:
266 266 raise ValueError('Running file %r produced error: %r' %
267 267 (fname, err))
268 268 # If no errors or output on stderr was expected, match stdout
269 269 nt.assert_equal("\n".join(out.strip().splitlines()), "\n".join(expected_out.strip().splitlines()))
270 270
271 271
272 272 class TempFileMixin(object):
273 273 """Utility class to create temporary Python/IPython files.
274 274
275 275 Meant as a mixin class for test cases."""
276 276
277 277 def mktmp(self, src, ext='.py'):
278 278 """Make a valid python temp file."""
279 279 fname, f = temp_pyfile(src, ext)
280 280 self.tmpfile = f
281 281 self.fname = fname
282 282
283 283 def tearDown(self):
284 284 if hasattr(self, 'tmpfile'):
285 285 # If the tmpfile wasn't made because of skipped tests, like in
286 286 # win32, there's nothing to cleanup.
287 287 self.tmpfile.close()
288 288 try:
289 289 os.unlink(self.fname)
290 290 except:
291 291 # On Windows, even though we close the file, we still can't
292 292 # delete it. I have no clue why
293 293 pass
294 294
295 295 pair_fail_msg = ("Testing {0}\n\n"
296 296 "In:\n"
297 297 " {1!r}\n"
298 298 "Expected:\n"
299 299 " {2!r}\n"
300 300 "Got:\n"
301 301 " {3!r}\n")
302 302 def check_pairs(func, pairs):
303 303 """Utility function for the common case of checking a function with a
304 304 sequence of input/output pairs.
305 305
306 306 Parameters
307 307 ----------
308 308 func : callable
309 309 The function to be tested. Should accept a single argument.
310 310 pairs : iterable
311 311 A list of (input, expected_output) tuples.
312 312
313 313 Returns
314 314 -------
315 315 None. Raises an AssertionError if any output does not match the expected
316 316 value.
317 317 """
318 318 name = getattr(func, "func_name", getattr(func, "__name__", "<unknown>"))
319 319 for inp, expected in pairs:
320 320 out = func(inp)
321 321 assert out == expected, pair_fail_msg.format(name, inp, expected, out)
322 322
323 323
324 324 if py3compat.PY3:
325 325 MyStringIO = StringIO
326 326 else:
327 327 # In Python 2, stdout/stderr can have either bytes or unicode written to them,
328 328 # so we need a class that can handle both.
329 329 class MyStringIO(StringIO):
330 330 def write(self, s):
331 331 s = py3compat.cast_unicode(s, encoding=DEFAULT_ENCODING)
332 332 super(MyStringIO, self).write(s)
333 333
334 334 _re_type = type(re.compile(r''))
335 335
336 336 notprinted_msg = """Did not find {0!r} in printed output (on {1}):
337 337 -------
338 338 {2!s}
339 339 -------
340 340 """
341 341
342 342 class AssertPrints(object):
343 343 """Context manager for testing that code prints certain text.
344 344
345 345 Examples
346 346 --------
347 347 >>> with AssertPrints("abc", suppress=False):
348 348 ... print("abcd")
349 349 ... print("def")
350 350 ...
351 351 abcd
352 352 def
353 353 """
354 354 def __init__(self, s, channel='stdout', suppress=True):
355 355 self.s = s
356 356 if isinstance(self.s, (py3compat.string_types, _re_type)):
357 357 self.s = [self.s]
358 358 self.channel = channel
359 359 self.suppress = suppress
360 360
361 361 def __enter__(self):
362 362 self.orig_stream = getattr(sys, self.channel)
363 363 self.buffer = MyStringIO()
364 364 self.tee = Tee(self.buffer, channel=self.channel)
365 365 setattr(sys, self.channel, self.buffer if self.suppress else self.tee)
366 366
367 367 def __exit__(self, etype, value, traceback):
368 368 try:
369 369 if value is not None:
370 370 # If an error was raised, don't check anything else
371 371 return False
372 372 self.tee.flush()
373 373 setattr(sys, self.channel, self.orig_stream)
374 374 printed = self.buffer.getvalue()
375 375 for s in self.s:
376 376 if isinstance(s, _re_type):
377 377 assert s.search(printed), notprinted_msg.format(s.pattern, self.channel, printed)
378 378 else:
379 379 assert s in printed, notprinted_msg.format(s, self.channel, printed)
380 380 return False
381 381 finally:
382 382 self.tee.close()
383 383
384 384 printed_msg = """Found {0!r} in printed output (on {1}):
385 385 -------
386 386 {2!s}
387 387 -------
388 388 """
389 389
390 390 class AssertNotPrints(AssertPrints):
391 391 """Context manager for checking that certain output *isn't* produced.
392 392
393 393 Counterpart of AssertPrints"""
394 394 def __exit__(self, etype, value, traceback):
395 395 try:
396 396 if value is not None:
397 397 # If an error was raised, don't check anything else
398 398 self.tee.close()
399 399 return False
400 400 self.tee.flush()
401 401 setattr(sys, self.channel, self.orig_stream)
402 402 printed = self.buffer.getvalue()
403 403 for s in self.s:
404 404 if isinstance(s, _re_type):
405 405 assert not s.search(printed),printed_msg.format(
406 406 s.pattern, self.channel, printed)
407 407 else:
408 408 assert s not in printed, printed_msg.format(
409 409 s, self.channel, printed)
410 410 return False
411 411 finally:
412 412 self.tee.close()
413 413
414 414 @contextmanager
415 415 def mute_warn():
416 416 from IPython.utils import warn
417 417 save_warn = warn.warn
418 418 warn.warn = lambda *a, **kw: None
419 419 try:
420 420 yield
421 421 finally:
422 422 warn.warn = save_warn
423 423
424 424 @contextmanager
425 425 def make_tempfile(name):
426 426 """ Create an empty, named, temporary file for the duration of the context.
427 427 """
428 428 f = open(name, 'w')
429 429 f.close()
430 430 try:
431 431 yield
432 432 finally:
433 433 os.unlink(name)
434 434
435 435
436 436 @contextmanager
437 437 def monkeypatch(obj, name, attr):
438 438 """
439 439 Context manager to replace attribute named `name` in `obj` with `attr`.
440 440 """
441 441 orig = getattr(obj, name)
442 442 setattr(obj, name, attr)
443 443 yield
444 444 setattr(obj, name, orig)
445 445
446 446
447 447 def help_output_test(subcommand=''):
448 448 """test that `ipython [subcommand] -h` works"""
449 449 cmd = get_ipython_cmd() + [subcommand, '-h']
450 450 out, err, rc = get_output_error_code(cmd)
451 451 nt.assert_equal(rc, 0, err)
452 452 nt.assert_not_in("Traceback", err)
453 453 nt.assert_in("Options", out)
454 454 nt.assert_in("--help-all", out)
455 455 return out, err
456 456
457 457
458 458 def help_all_output_test(subcommand=''):
459 459 """test that `ipython [subcommand] --help-all` works"""
460 460 cmd = get_ipython_cmd() + [subcommand, '--help-all']
461 461 out, err, rc = get_output_error_code(cmd)
462 462 nt.assert_equal(rc, 0, err)
463 463 nt.assert_not_in("Traceback", err)
464 464 nt.assert_in("Options", out)
465 465 nt.assert_in("Class parameters", out)
466 466 return out, err
467 467
468 def assert_big_text_equal(a, b, chunk_size=80):
469 """assert that large strings are equal
470
471 Zooms in on first chunk that differs,
472 to give better info than vanilla assertEqual for large text blobs.
473 """
474 for i in range(0, len(a), chunk_size):
475 chunk_a = a[i:i + chunk_size]
476 chunk_b = b[i:i + chunk_size]
477 nt.assert_equal(chunk_a, chunk_b, "[offset: %i]\n%r != \n%r" % (
478 i, chunk_a, chunk_b))
479
480 if len(a) > len(b):
481 nt.fail("Length doesn't match (%i > %i). Extra text:\n%r" % (
482 len(a), len(b), a[len(b):]
483 ))
484 elif len(a) < len(b):
485 nt.fail("Length doesn't match (%i < %i). Extra text:\n%r" % (
486 len(a), len(b), b[len(a):]
487 ))
General Comments 0
You need to be logged in to leave comments. Login now