##// END OF EJS Templates
Restore the ability to run tests from a function.
Thomas Kluyver -
Show More
@@ -1,29 +1,31 b''
1 1 """Testing support (tools to test IPython itself).
2 2 """
3 3
4 4 #-----------------------------------------------------------------------------
5 5 # Copyright (C) 2009-2011 The IPython Development Team
6 6 #
7 7 # Distributed under the terms of the BSD License. The full license is in
8 8 # the file COPYING, distributed as part of this software.
9 9 #-----------------------------------------------------------------------------
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Functions
13 13 #-----------------------------------------------------------------------------
14 14
15 15 # User-level entry point for testing
16 16 def test(all=False):
17 17 """Run the entire IPython test suite.
18 18
19 19 For fine-grained control, you should use the :file:`iptest` script supplied
20 20 with the IPython installation."""
21 21
22 22 # Do the import internally, so that this function doesn't increase total
23 23 # import time
24 from .iptest import run_iptestall
25 run_iptestall(inc_slow=all)
24 from .iptestcontroller import run_iptestall, default_options
25 options = default_options()
26 options.all = all
27 run_iptestall(options)
26 28
27 29 # So nose doesn't try to run this as a test itself and we end up with an
28 30 # infinite test loop
29 31 test.__test__ = False
@@ -1,494 +1,501 b''
1 1 # -*- coding: utf-8 -*-
2 2 """IPython Test Process Controller
3 3
4 4 This module runs one or more subprocesses which will actually run the IPython
5 5 test suite.
6 6
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2009-2011 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19 from __future__ import print_function
20 20
21 21 import argparse
22 22 import multiprocessing.pool
23 23 from multiprocessing import Process, Queue
24 24 import os
25 25 import shutil
26 26 import signal
27 27 import sys
28 28 import subprocess
29 29 import time
30 30
31 31 from .iptest import have, test_group_names as py_test_group_names, test_sections
32 32 from IPython.utils.path import compress_user
33 33 from IPython.utils.py3compat import bytes_to_str
34 34 from IPython.utils.sysinfo import get_sys_info
35 35 from IPython.utils.tempdir import TemporaryDirectory
36 36
37 37
38 38 class TestController(object):
39 39 """Run tests in a subprocess
40 40 """
41 41 #: str, IPython test suite to be executed.
42 42 section = None
43 43 #: list, command line arguments to be executed
44 44 cmd = None
45 45 #: dict, extra environment variables to set for the subprocess
46 46 env = None
47 47 #: list, TemporaryDirectory instances to clear up when the process finishes
48 48 dirs = None
49 49 #: subprocess.Popen instance
50 50 process = None
51 51 #: str, process stdout+stderr
52 52 stdout = None
53 53 #: bool, whether to capture process stdout & stderr
54 54 buffer_output = False
55 55
56 56 def __init__(self):
57 57 self.cmd = []
58 58 self.env = {}
59 59 self.dirs = []
60 60
61 61 def launch(self):
62 62 # print('*** ENV:', self.env) # dbg
63 63 # print('*** CMD:', self.cmd) # dbg
64 64 env = os.environ.copy()
65 65 env.update(self.env)
66 66 output = subprocess.PIPE if self.buffer_output else None
67 67 stdout = subprocess.STDOUT if self.buffer_output else None
68 68 self.process = subprocess.Popen(self.cmd, stdout=output,
69 69 stderr=stdout, env=env)
70 70
71 71 def wait(self):
72 72 self.stdout, _ = self.process.communicate()
73 73 return self.process.returncode
74 74
75 75 def cleanup_process(self):
76 76 """Cleanup on exit by killing any leftover processes."""
77 77 subp = self.process
78 78 if subp is None or (subp.poll() is not None):
79 79 return # Process doesn't exist, or is already dead.
80 80
81 81 try:
82 82 print('Cleaning up stale PID: %d' % subp.pid)
83 83 subp.kill()
84 84 except: # (OSError, WindowsError) ?
85 85 # This is just a best effort, if we fail or the process was
86 86 # really gone, ignore it.
87 87 pass
88 88 else:
89 89 for i in range(10):
90 90 if subp.poll() is None:
91 91 time.sleep(0.1)
92 92 else:
93 93 break
94 94
95 95 if subp.poll() is None:
96 96 # The process did not die...
97 97 print('... failed. Manual cleanup may be required.')
98 98
99 99 def cleanup(self):
100 100 "Kill process if it's still alive, and clean up temporary directories"
101 101 self.cleanup_process()
102 102 for td in self.dirs:
103 103 td.cleanup()
104 104
105 105 __del__ = cleanup
106 106
107 107 class PyTestController(TestController):
108 108 """Run Python tests using IPython.testing.iptest"""
109 109 #: str, Python command to execute in subprocess
110 110 pycmd = None
111 111
112 112 def __init__(self, section):
113 113 """Create new test runner."""
114 114 TestController.__init__(self)
115 115 self.section = section
116 116 # pycmd is put into cmd[2] in PyTestController.launch()
117 117 self.cmd = [sys.executable, '-c', None, section]
118 118 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
119 119 ipydir = TemporaryDirectory()
120 120 self.dirs.append(ipydir)
121 121 self.env['IPYTHONDIR'] = ipydir.name
122 122 self.workingdir = workingdir = TemporaryDirectory()
123 123 self.dirs.append(workingdir)
124 124 self.env['IPTEST_WORKING_DIR'] = workingdir.name
125 125 # This means we won't get odd effects from our own matplotlib config
126 126 self.env['MPLCONFIGDIR'] = workingdir.name
127 127
128 128 @property
129 129 def will_run(self):
130 130 try:
131 131 return test_sections[self.section].will_run
132 132 except KeyError:
133 133 return True
134 134
135 135 def add_xunit(self):
136 136 xunit_file = os.path.abspath(self.section + '.xunit.xml')
137 137 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
138 138
139 139 def add_coverage(self):
140 140 try:
141 141 sources = test_sections[self.section].includes
142 142 except KeyError:
143 143 sources = ['IPython']
144 144
145 145 coverage_rc = ("[run]\n"
146 146 "data_file = {data_file}\n"
147 147 "source =\n"
148 148 " {source}\n"
149 149 ).format(data_file=os.path.abspath('.coverage.'+self.section),
150 150 source="\n ".join(sources))
151 151 config_file = os.path.join(self.workingdir.name, '.coveragerc')
152 152 with open(config_file, 'w') as f:
153 153 f.write(coverage_rc)
154 154
155 155 self.env['COVERAGE_PROCESS_START'] = config_file
156 156 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
157 157
158 158 def launch(self):
159 159 self.cmd[2] = self.pycmd
160 160 super(PyTestController, self).launch()
161 161
162 162 class JSController(TestController):
163 163 """Run CasperJS tests """
164 164 def __init__(self, section):
165 165 """Create new test runner."""
166 166 TestController.__init__(self)
167 167 self.section = section
168 168
169 169 self.ipydir = TemporaryDirectory()
170 170 self.dirs.append(self.ipydir)
171 171 self.env['IPYTHONDIR'] = self.ipydir.name
172 172
173 173 def launch(self):
174 174 # start the ipython notebook, so we get the port number
175 175 self._init_server()
176 176
177 177 import IPython.html.tests as t
178 178 test_dir = os.path.join(os.path.dirname(t.__file__), 'casperjs')
179 179 includes = '--includes=' + os.path.join(test_dir,'util.js')
180 180 test_cases = os.path.join(test_dir, 'test_cases')
181 181 port = '--port=' + str(self.server_port)
182 182 self.cmd = ['casperjs', 'test', port, includes, test_cases]
183 183
184 184 super(JSController, self).launch()
185 185
186 186 @property
187 187 def will_run(self):
188 188 return all(have[a] for a in ['zmq', 'tornado', 'jinja2', 'casperjs'])
189 189
190 190 def _init_server(self):
191 191 "Start the notebook server in a separate process"
192 192 self.queue = q = Queue()
193 193 self.server = Process(target=run_webapp, args=(q, self.ipydir.name))
194 194 self.server.start()
195 195 self.server_port = q.get()
196 196
197 197 def cleanup(self):
198 198 self.server.terminate()
199 199 self.server.join()
200 200 TestController.cleanup(self)
201 201
202 202 js_test_group_names = {'js'}
203 203
204 204 def run_webapp(q, nbdir, loglevel=0):
205 205 """start the IPython Notebook, and pass port back to the queue"""
206 206 import os
207 207 import IPython.html.notebookapp as nbapp
208 208 import sys
209 209 sys.stderr = open(os.devnull, 'w')
210 210 os.environ["IPYTHONDIR"] = nbdir
211 211 server = nbapp.NotebookApp()
212 212 args = ['--no-browser']
213 213 args.append('--notebook-dir='+nbdir)
214 214 args.append('--profile-dir='+nbdir)
215 215 args.append('--log-level='+str(loglevel))
216 216 server.initialize(args)
217 217 # communicate the port number to the parent process
218 218 q.put(server.port)
219 219 server.start()
220 220
221 221 def prepare_controllers(options):
222 222 """Returns two lists of TestController instances, those to run, and those
223 223 not to run."""
224 224 testgroups = options.testgroups
225 225
226 226 if testgroups:
227 227 py_testgroups = [g for g in testgroups if (g in py_test_group_names) \
228 228 or g.startswith('IPython')]
229 229 js_testgroups = [g for g in testgroups if g in js_test_group_names]
230 230 else:
231 231 py_testgroups = py_test_group_names
232 232 js_testgroups = js_test_group_names
233 233 if not options.all:
234 234 test_sections['parallel'].enabled = False
235 235
236 236 c_js = [JSController(name) for name in js_testgroups]
237 237 c_py = [PyTestController(name) for name in py_testgroups]
238 238
239 239 configure_py_controllers(c_py, xunit=options.xunit,
240 240 coverage=options.coverage, extra_args=options.extra_args)
241 241
242 242 controllers = c_py + c_js
243 243 to_run = [c for c in controllers if c.will_run]
244 244 not_run = [c for c in controllers if not c.will_run]
245 245 return to_run, not_run
246 246
247 247 def configure_py_controllers(controllers, xunit=False, coverage=False, extra_args=()):
248 248 """Apply options for a collection of TestController objects."""
249 249 for controller in controllers:
250 250 if xunit:
251 251 controller.add_xunit()
252 252 if coverage:
253 253 controller.add_coverage()
254 254 controller.cmd.extend(extra_args)
255 255
256 256 def do_run(controller):
257 257 try:
258 258 try:
259 259 controller.launch()
260 260 except Exception:
261 261 import traceback
262 262 traceback.print_exc()
263 263 return controller, 1 # signal failure
264 264
265 265 exitcode = controller.wait()
266 266 return controller, exitcode
267 267
268 268 except KeyboardInterrupt:
269 269 return controller, -signal.SIGINT
270 270 finally:
271 271 controller.cleanup()
272 272
273 273 def report():
274 274 """Return a string with a summary report of test-related variables."""
275 275 inf = get_sys_info()
276 276 out = []
277 277 def _add(name, value):
278 278 out.append((name, value))
279 279
280 280 _add('IPython version', inf['ipython_version'])
281 281 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
282 282 _add('IPython package', compress_user(inf['ipython_path']))
283 283 _add('Python version', inf['sys_version'].replace('\n',''))
284 284 _add('sys.executable', compress_user(inf['sys_executable']))
285 285 _add('Platform', inf['platform'])
286 286
287 287 width = max(len(n) for (n,v) in out)
288 288 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
289 289
290 290 avail = []
291 291 not_avail = []
292 292
293 293 for k, is_avail in have.items():
294 294 if is_avail:
295 295 avail.append(k)
296 296 else:
297 297 not_avail.append(k)
298 298
299 299 if avail:
300 300 out.append('\nTools and libraries available at test time:\n')
301 301 avail.sort()
302 302 out.append(' ' + ' '.join(avail)+'\n')
303 303
304 304 if not_avail:
305 305 out.append('\nTools and libraries NOT available at test time:\n')
306 306 not_avail.sort()
307 307 out.append(' ' + ' '.join(not_avail)+'\n')
308 308
309 309 return ''.join(out)
310 310
311 311 def run_iptestall(options):
312 312 """Run the entire IPython test suite by calling nose and trial.
313 313
314 314 This function constructs :class:`IPTester` instances for all IPython
315 315 modules and package and then runs each of them. This causes the modules
316 316 and packages of IPython to be tested each in their own subprocess using
317 317 nose.
318 318
319 319 Parameters
320 320 ----------
321 321
322 322 All parameters are passed as attributes of the options object.
323 323
324 324 testgroups : list of str
325 325 Run only these sections of the test suite. If empty, run all the available
326 326 sections.
327 327
328 328 fast : int or None
329 329 Run the test suite in parallel, using n simultaneous processes. If None
330 330 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
331 331
332 332 inc_slow : bool
333 333 Include slow tests, like IPython.parallel. By default, these tests aren't
334 334 run.
335 335
336 336 xunit : bool
337 337 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
338 338
339 339 coverage : bool or str
340 340 Measure code coverage from tests. True will store the raw coverage data,
341 341 or pass 'html' or 'xml' to get reports.
342 342
343 343 extra_args : list
344 344 Extra arguments to pass to the test subprocesses, e.g. '-v'
345 345 """
346 346 if options.fast != 1:
347 347 # If running in parallel, capture output so it doesn't get interleaved
348 348 TestController.buffer_output = True
349 349
350 350 to_run, not_run = prepare_controllers(options)
351 351
352 352 def justify(ltext, rtext, width=70, fill='-'):
353 353 ltext += ' '
354 354 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
355 355 return ltext + rtext
356 356
357 357 # Run all test runners, tracking execution time
358 358 failed = []
359 359 t_start = time.time()
360 360
361 361 print()
362 362 if options.fast == 1:
363 363 # This actually means sequential, i.e. with 1 job
364 364 for controller in to_run:
365 365 print('IPython test group:', controller.section)
366 366 sys.stdout.flush() # Show in correct order when output is piped
367 367 controller, res = do_run(controller)
368 368 if res:
369 369 failed.append(controller)
370 370 if res == -signal.SIGINT:
371 371 print("Interrupted")
372 372 break
373 373 print()
374 374
375 375 else:
376 376 # Run tests concurrently
377 377 try:
378 378 pool = multiprocessing.pool.ThreadPool(options.fast)
379 379 for (controller, res) in pool.imap_unordered(do_run, to_run):
380 380 res_string = 'OK' if res == 0 else 'FAILED'
381 381 print(justify('IPython test group: ' + controller.section, res_string))
382 382 if res:
383 383 print(bytes_to_str(controller.stdout))
384 384 failed.append(controller)
385 385 if res == -signal.SIGINT:
386 386 print("Interrupted")
387 387 break
388 388 except KeyboardInterrupt:
389 389 return
390 390
391 391 for controller in not_run:
392 392 print(justify('IPython test group: ' + controller.section, 'NOT RUN'))
393 393
394 394 t_end = time.time()
395 395 t_tests = t_end - t_start
396 396 nrunners = len(to_run)
397 397 nfail = len(failed)
398 398 # summarize results
399 399 print('_'*70)
400 400 print('Test suite completed for system with the following information:')
401 401 print(report())
402 402 took = "Took %.3fs." % t_tests
403 403 print('Status: ', end='')
404 404 if not failed:
405 405 print('OK (%d test groups).' % nrunners, took)
406 406 else:
407 407 # If anything went wrong, point out what command to rerun manually to
408 408 # see the actual errors and individual summary
409 409 failed_sections = [c.section for c in failed]
410 410 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
411 411 nrunners, ', '.join(failed_sections)), took)
412 412 print()
413 413 print('You may wish to rerun these, with:')
414 414 print(' iptest', *failed_sections)
415 415 print()
416 416
417 417 if options.coverage:
418 418 from coverage import coverage
419 419 cov = coverage(data_file='.coverage')
420 420 cov.combine()
421 421 cov.save()
422 422
423 423 # Coverage HTML report
424 424 if options.coverage == 'html':
425 425 html_dir = 'ipy_htmlcov'
426 426 shutil.rmtree(html_dir, ignore_errors=True)
427 427 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
428 428 sys.stdout.flush()
429 429
430 430 # Custom HTML reporter to clean up module names.
431 431 from coverage.html import HtmlReporter
432 432 class CustomHtmlReporter(HtmlReporter):
433 433 def find_code_units(self, morfs):
434 434 super(CustomHtmlReporter, self).find_code_units(morfs)
435 435 for cu in self.code_units:
436 436 nameparts = cu.name.split(os.sep)
437 437 if 'IPython' not in nameparts:
438 438 continue
439 439 ix = nameparts.index('IPython')
440 440 cu.name = '.'.join(nameparts[ix:])
441 441
442 442 # Reimplement the html_report method with our custom reporter
443 443 cov._harvest_data()
444 444 cov.config.from_args(omit='*%stests' % os.sep, html_dir=html_dir,
445 445 html_title='IPython test coverage',
446 446 )
447 447 reporter = CustomHtmlReporter(cov, cov.config)
448 448 reporter.report(None)
449 449 print('done.')
450 450
451 451 # Coverage XML report
452 452 elif options.coverage == 'xml':
453 453 cov.xml_report(outfile='ipy_coverage.xml')
454 454
455 455 if failed:
456 456 # Ensure that our exit code indicates failure
457 457 sys.exit(1)
458 458
459 argparser = argparse.ArgumentParser(description='Run IPython test suite')
460 argparser.add_argument('testgroups', nargs='*',
461 help='Run specified groups of tests. If omitted, run '
462 'all tests.')
463 argparser.add_argument('--all', action='store_true',
464 help='Include slow tests not run by default.')
465 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
466 help='Run test sections in parallel.')
467 argparser.add_argument('--xunit', action='store_true',
468 help='Produce Xunit XML results')
469 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
470 help="Measure test coverage. Specify 'html' or "
471 "'xml' to get reports.")
472
473 def default_options():
474 """Get an argparse Namespace object with the default arguments, to pass to
475 :func:`run_iptestall`.
476 """
477 options = argparser.parse_args([])
478 options.extra_args = []
479 return options
459 480
460 481 def main():
461 482 # Arguments after -- should be passed through to nose. Argparse treats
462 483 # everything after -- as regular positional arguments, so we separate them
463 484 # first.
464 485 try:
465 486 ix = sys.argv.index('--')
466 487 except ValueError:
467 488 to_parse = sys.argv[1:]
468 489 extra_args = []
469 490 else:
470 491 to_parse = sys.argv[1:ix]
471 492 extra_args = sys.argv[ix+1:]
472 493
473 parser = argparse.ArgumentParser(description='Run IPython test suite')
474 parser.add_argument('testgroups', nargs='*',
475 help='Run specified groups of tests. If omitted, run '
476 'all tests.')
477 parser.add_argument('--all', action='store_true',
478 help='Include slow tests not run by default.')
479 parser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
480 help='Run test sections in parallel.')
481 parser.add_argument('--xunit', action='store_true',
482 help='Produce Xunit XML results')
483 parser.add_argument('--coverage', nargs='?', const=True, default=False,
484 help="Measure test coverage. Specify 'html' or "
485 "'xml' to get reports.")
486
487 options = parser.parse_args(to_parse)
494 options = argparser.parse_args(to_parse)
488 495 options.extra_args = extra_args
489 496
490 497 run_iptestall(options)
491 498
492 499
493 500 if __name__ == '__main__':
494 501 main()
General Comments 0
You need to be logged in to leave comments. Login now