##// END OF EJS Templates
Prevent 'iptest IPython' from trying to run....
Thomas Kluyver -
Show More
@@ -1,508 +1,508 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 # print(self.ipydir.name)
171 171 self.dirs.append(self.ipydir)
172 172 self.env['IPYTHONDIR'] = self.ipydir.name
173 173
174 174 def launch(self):
175 175 # start the ipython notebook, so we get the port number
176 176 self._init_server()
177 177
178 178 import IPython.html.tests as t
179 179 test_dir = os.path.join(os.path.dirname(t.__file__), 'casperjs')
180 180 includes = '--includes=' + os.path.join(test_dir,'util.js')
181 181 test_cases = os.path.join(test_dir, 'test_cases')
182 182 port = '--port=' + str(self.server_port)
183 183 self.cmd = ['casperjs', 'test', port, includes, test_cases]
184 184
185 185 super(JSController, self).launch()
186 186
187 187 @property
188 188 def will_run(self):
189 189 return all(have[a] for a in ['zmq', 'tornado', 'jinja2', 'casperjs'])
190 190
191 191 def _init_server(self):
192 192 "Start the notebook server in a separate process"
193 193 self.queue = q = Queue()
194 194 self.server = Process(target=run_webapp, args=(q, self.ipydir.name))
195 195 self.server.start()
196 196 self.server_port = q.get()
197 197
198 198 def cleanup(self):
199 199 self.server.terminate()
200 200 self.server.join()
201 201 TestController.cleanup(self)
202 202
203 203 js_test_group_names = {'js'}
204 204
205 205 def run_webapp(q, nbdir, loglevel=0):
206 206 """start the IPython Notebook, and pass port back to the queue"""
207 207 import os
208 208 import IPython.html.notebookapp as nbapp
209 209 import sys
210 210 sys.stderr = open(os.devnull, 'w')
211 211 os.environ["IPYTHONDIR"] = nbdir
212 212 server = nbapp.NotebookApp()
213 213 args = ['--no-browser']
214 214 args.append('--notebook-dir='+nbdir)
215 215 args.append('--profile-dir='+nbdir)
216 216 args.append('--log-level='+str(loglevel))
217 217 server.initialize(args)
218 218 # communicate the port number to the parent process
219 219 q.put(server.port)
220 220 server.start()
221 221
222 222 def prepare_controllers(options):
223 223 """Returns two lists of TestController instances, those to run, and those
224 224 not to run."""
225 225 testgroups = options.testgroups
226 226
227 227 if testgroups:
228 228 py_testgroups = [g for g in testgroups if (g in py_test_group_names) \
229 or g.startswith('IPython')]
229 or g.startswith('IPython.')]
230 230 js_testgroups = [g for g in testgroups if g in js_test_group_names]
231 231 else:
232 232 py_testgroups = py_test_group_names
233 233 js_testgroups = js_test_group_names
234 234 if not options.all:
235 235 test_sections['parallel'].enabled = False
236 236
237 237 c_js = [JSController(name) for name in js_testgroups]
238 238 c_py = [PyTestController(name) for name in py_testgroups]
239 239
240 240 configure_py_controllers(c_py, xunit=options.xunit,
241 241 coverage=options.coverage, subproc_streams=options.subproc_streams,
242 242 extra_args=options.extra_args)
243 243
244 244 controllers = c_py + c_js
245 245 to_run = [c for c in controllers if c.will_run]
246 246 not_run = [c for c in controllers if not c.will_run]
247 247 return to_run, not_run
248 248
249 249 def configure_py_controllers(controllers, xunit=False, coverage=False,
250 250 subproc_streams='capture', extra_args=()):
251 251 """Apply options for a collection of TestController objects."""
252 252 for controller in controllers:
253 253 if xunit:
254 254 controller.add_xunit()
255 255 if coverage:
256 256 controller.add_coverage()
257 257 controller.env['IPTEST_SUBPROC_STREAMS'] = subproc_streams
258 258 controller.cmd.extend(extra_args)
259 259
260 260 def do_run(controller):
261 261 try:
262 262 try:
263 263 controller.launch()
264 264 except Exception:
265 265 import traceback
266 266 traceback.print_exc()
267 267 return controller, 1 # signal failure
268 268
269 269 exitcode = controller.wait()
270 270 return controller, exitcode
271 271
272 272 except KeyboardInterrupt:
273 273 return controller, -signal.SIGINT
274 274 finally:
275 275 controller.cleanup()
276 276
277 277 def report():
278 278 """Return a string with a summary report of test-related variables."""
279 279 inf = get_sys_info()
280 280 out = []
281 281 def _add(name, value):
282 282 out.append((name, value))
283 283
284 284 _add('IPython version', inf['ipython_version'])
285 285 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
286 286 _add('IPython package', compress_user(inf['ipython_path']))
287 287 _add('Python version', inf['sys_version'].replace('\n',''))
288 288 _add('sys.executable', compress_user(inf['sys_executable']))
289 289 _add('Platform', inf['platform'])
290 290
291 291 width = max(len(n) for (n,v) in out)
292 292 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
293 293
294 294 avail = []
295 295 not_avail = []
296 296
297 297 for k, is_avail in have.items():
298 298 if is_avail:
299 299 avail.append(k)
300 300 else:
301 301 not_avail.append(k)
302 302
303 303 if avail:
304 304 out.append('\nTools and libraries available at test time:\n')
305 305 avail.sort()
306 306 out.append(' ' + ' '.join(avail)+'\n')
307 307
308 308 if not_avail:
309 309 out.append('\nTools and libraries NOT available at test time:\n')
310 310 not_avail.sort()
311 311 out.append(' ' + ' '.join(not_avail)+'\n')
312 312
313 313 return ''.join(out)
314 314
315 315 def run_iptestall(options):
316 316 """Run the entire IPython test suite by calling nose and trial.
317 317
318 318 This function constructs :class:`IPTester` instances for all IPython
319 319 modules and package and then runs each of them. This causes the modules
320 320 and packages of IPython to be tested each in their own subprocess using
321 321 nose.
322 322
323 323 Parameters
324 324 ----------
325 325
326 326 All parameters are passed as attributes of the options object.
327 327
328 328 testgroups : list of str
329 329 Run only these sections of the test suite. If empty, run all the available
330 330 sections.
331 331
332 332 fast : int or None
333 333 Run the test suite in parallel, using n simultaneous processes. If None
334 334 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
335 335
336 336 inc_slow : bool
337 337 Include slow tests, like IPython.parallel. By default, these tests aren't
338 338 run.
339 339
340 340 xunit : bool
341 341 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
342 342
343 343 coverage : bool or str
344 344 Measure code coverage from tests. True will store the raw coverage data,
345 345 or pass 'html' or 'xml' to get reports.
346 346
347 347 extra_args : list
348 348 Extra arguments to pass to the test subprocesses, e.g. '-v'
349 349 """
350 350 if options.fast != 1:
351 351 # If running in parallel, capture output so it doesn't get interleaved
352 352 TestController.buffer_output = True
353 353
354 354 to_run, not_run = prepare_controllers(options)
355 355
356 356 def justify(ltext, rtext, width=70, fill='-'):
357 357 ltext += ' '
358 358 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
359 359 return ltext + rtext
360 360
361 361 # Run all test runners, tracking execution time
362 362 failed = []
363 363 t_start = time.time()
364 364
365 365 print()
366 366 if options.fast == 1:
367 367 # This actually means sequential, i.e. with 1 job
368 368 for controller in to_run:
369 369 print('IPython test group:', controller.section)
370 370 sys.stdout.flush() # Show in correct order when output is piped
371 371 controller, res = do_run(controller)
372 372 if res:
373 373 failed.append(controller)
374 374 if res == -signal.SIGINT:
375 375 print("Interrupted")
376 376 break
377 377 print()
378 378
379 379 else:
380 380 # Run tests concurrently
381 381 try:
382 382 pool = multiprocessing.pool.ThreadPool(options.fast)
383 383 for (controller, res) in pool.imap_unordered(do_run, to_run):
384 384 res_string = 'OK' if res == 0 else 'FAILED'
385 385 print(justify('IPython test group: ' + controller.section, res_string))
386 386 if res:
387 387 print(bytes_to_str(controller.stdout))
388 388 failed.append(controller)
389 389 if res == -signal.SIGINT:
390 390 print("Interrupted")
391 391 break
392 392 except KeyboardInterrupt:
393 393 return
394 394
395 395 for controller in not_run:
396 396 print(justify('IPython test group: ' + controller.section, 'NOT RUN'))
397 397
398 398 t_end = time.time()
399 399 t_tests = t_end - t_start
400 400 nrunners = len(to_run)
401 401 nfail = len(failed)
402 402 # summarize results
403 403 print('_'*70)
404 404 print('Test suite completed for system with the following information:')
405 405 print(report())
406 406 took = "Took %.3fs." % t_tests
407 407 print('Status: ', end='')
408 408 if not failed:
409 409 print('OK (%d test groups).' % nrunners, took)
410 410 else:
411 411 # If anything went wrong, point out what command to rerun manually to
412 412 # see the actual errors and individual summary
413 413 failed_sections = [c.section for c in failed]
414 414 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
415 415 nrunners, ', '.join(failed_sections)), took)
416 416 print()
417 417 print('You may wish to rerun these, with:')
418 418 print(' iptest', *failed_sections)
419 419 print()
420 420
421 421 if options.coverage:
422 422 from coverage import coverage
423 423 cov = coverage(data_file='.coverage')
424 424 cov.combine()
425 425 cov.save()
426 426
427 427 # Coverage HTML report
428 428 if options.coverage == 'html':
429 429 html_dir = 'ipy_htmlcov'
430 430 shutil.rmtree(html_dir, ignore_errors=True)
431 431 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
432 432 sys.stdout.flush()
433 433
434 434 # Custom HTML reporter to clean up module names.
435 435 from coverage.html import HtmlReporter
436 436 class CustomHtmlReporter(HtmlReporter):
437 437 def find_code_units(self, morfs):
438 438 super(CustomHtmlReporter, self).find_code_units(morfs)
439 439 for cu in self.code_units:
440 440 nameparts = cu.name.split(os.sep)
441 441 if 'IPython' not in nameparts:
442 442 continue
443 443 ix = nameparts.index('IPython')
444 444 cu.name = '.'.join(nameparts[ix:])
445 445
446 446 # Reimplement the html_report method with our custom reporter
447 447 cov._harvest_data()
448 448 cov.config.from_args(omit='*%stests' % os.sep, html_dir=html_dir,
449 449 html_title='IPython test coverage',
450 450 )
451 451 reporter = CustomHtmlReporter(cov, cov.config)
452 452 reporter.report(None)
453 453 print('done.')
454 454
455 455 # Coverage XML report
456 456 elif options.coverage == 'xml':
457 457 cov.xml_report(outfile='ipy_coverage.xml')
458 458
459 459 if failed:
460 460 # Ensure that our exit code indicates failure
461 461 sys.exit(1)
462 462
463 463 argparser = argparse.ArgumentParser(description='Run IPython test suite')
464 464 argparser.add_argument('testgroups', nargs='*',
465 465 help='Run specified groups of tests. If omitted, run '
466 466 'all tests.')
467 467 argparser.add_argument('--all', action='store_true',
468 468 help='Include slow tests not run by default.')
469 469 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
470 470 help='Run test sections in parallel.')
471 471 argparser.add_argument('--xunit', action='store_true',
472 472 help='Produce Xunit XML results')
473 473 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
474 474 help="Measure test coverage. Specify 'html' or "
475 475 "'xml' to get reports.")
476 476 argparser.add_argument('--subproc-streams', default='capture',
477 477 help="What to do with stdout/stderr from subprocesses. "
478 478 "'capture' (default), 'show' and 'discard' are the options.")
479 479
480 480 def default_options():
481 481 """Get an argparse Namespace object with the default arguments, to pass to
482 482 :func:`run_iptestall`.
483 483 """
484 484 options = argparser.parse_args([])
485 485 options.extra_args = []
486 486 return options
487 487
488 488 def main():
489 489 # Arguments after -- should be passed through to nose. Argparse treats
490 490 # everything after -- as regular positional arguments, so we separate them
491 491 # first.
492 492 try:
493 493 ix = sys.argv.index('--')
494 494 except ValueError:
495 495 to_parse = sys.argv[1:]
496 496 extra_args = []
497 497 else:
498 498 to_parse = sys.argv[1:ix]
499 499 extra_args = sys.argv[ix+1:]
500 500
501 501 options = argparser.parse_args(to_parse)
502 502 options.extra_args = extra_args
503 503
504 504 run_iptestall(options)
505 505
506 506
507 507 if __name__ == '__main__':
508 508 main()
General Comments 0
You need to be logged in to leave comments. Login now