##// END OF EJS Templates
install IPython kernel in temp IPython dir
Min RK -
Show More
@@ -1,742 +1,752
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 # Copyright (c) IPython Development Team.
10 10 # Distributed under the terms of the Modified BSD License.
11 11
12 12 from __future__ import print_function
13 13
14 14 import argparse
15 15 import json
16 16 import multiprocessing.pool
17 17 import os
18 18 import stat
19 19 import re
20 20 import requests
21 21 import shutil
22 22 import signal
23 23 import sys
24 24 import subprocess
25 25 import time
26 26
27 27 from .iptest import (
28 28 have, test_group_names as py_test_group_names, test_sections, StreamCapturer,
29 29 test_for,
30 30 )
31 31 from IPython.utils.path import compress_user
32 32 from IPython.utils.py3compat import bytes_to_str
33 33 from IPython.utils.sysinfo import get_sys_info
34 34 from IPython.utils.tempdir import TemporaryDirectory
35 35 from IPython.utils.text import strip_ansi
36 36
37 37 try:
38 38 # Python >= 3.3
39 39 from subprocess import TimeoutExpired
40 40 def popen_wait(p, timeout):
41 41 return p.wait(timeout)
42 42 except ImportError:
43 43 class TimeoutExpired(Exception):
44 44 pass
45 45 def popen_wait(p, timeout):
46 46 """backport of Popen.wait from Python 3"""
47 47 for i in range(int(10 * timeout)):
48 48 if p.poll() is not None:
49 49 return
50 50 time.sleep(0.1)
51 51 if p.poll() is None:
52 52 raise TimeoutExpired
53 53
54 54 NOTEBOOK_SHUTDOWN_TIMEOUT = 10
55 55
56 56 class TestController(object):
57 57 """Run tests in a subprocess
58 58 """
59 59 #: str, IPython test suite to be executed.
60 60 section = None
61 61 #: list, command line arguments to be executed
62 62 cmd = None
63 63 #: dict, extra environment variables to set for the subprocess
64 64 env = None
65 65 #: list, TemporaryDirectory instances to clear up when the process finishes
66 66 dirs = None
67 67 #: subprocess.Popen instance
68 68 process = None
69 69 #: str, process stdout+stderr
70 70 stdout = None
71 71
72 72 def __init__(self):
73 73 self.cmd = []
74 74 self.env = {}
75 75 self.dirs = []
76 76
77 77 def setup(self):
78 78 """Create temporary directories etc.
79 79
80 80 This is only called when we know the test group will be run. Things
81 81 created here may be cleaned up by self.cleanup().
82 82 """
83 83 pass
84 84
85 85 def launch(self, buffer_output=False, capture_output=False):
86 86 # print('*** ENV:', self.env) # dbg
87 87 # print('*** CMD:', self.cmd) # dbg
88 88 env = os.environ.copy()
89 89 env.update(self.env)
90 90 if buffer_output:
91 91 capture_output = True
92 92 self.stdout_capturer = c = StreamCapturer(echo=not buffer_output)
93 93 c.start()
94 94 stdout = c.writefd if capture_output else None
95 95 stderr = subprocess.STDOUT if capture_output else None
96 96 self.process = subprocess.Popen(self.cmd, stdout=stdout,
97 97 stderr=stderr, env=env)
98 98
99 99 def wait(self):
100 100 self.process.wait()
101 101 self.stdout_capturer.halt()
102 102 self.stdout = self.stdout_capturer.get_buffer()
103 103 return self.process.returncode
104 104
105 105 def print_extra_info(self):
106 106 """Print extra information about this test run.
107 107
108 108 If we're running in parallel and showing the concise view, this is only
109 109 called if the test group fails. Otherwise, it's called before the test
110 110 group is started.
111 111
112 112 The base implementation does nothing, but it can be overridden by
113 113 subclasses.
114 114 """
115 115 return
116 116
117 117 def cleanup_process(self):
118 118 """Cleanup on exit by killing any leftover processes."""
119 119 subp = self.process
120 120 if subp is None or (subp.poll() is not None):
121 121 return # Process doesn't exist, or is already dead.
122 122
123 123 try:
124 124 print('Cleaning up stale PID: %d' % subp.pid)
125 125 subp.kill()
126 126 except: # (OSError, WindowsError) ?
127 127 # This is just a best effort, if we fail or the process was
128 128 # really gone, ignore it.
129 129 pass
130 130 else:
131 131 for i in range(10):
132 132 if subp.poll() is None:
133 133 time.sleep(0.1)
134 134 else:
135 135 break
136 136
137 137 if subp.poll() is None:
138 138 # The process did not die...
139 139 print('... failed. Manual cleanup may be required.')
140 140
141 141 def cleanup(self):
142 142 "Kill process if it's still alive, and clean up temporary directories"
143 143 self.cleanup_process()
144 144 for td in self.dirs:
145 145 td.cleanup()
146 146
147 147 __del__ = cleanup
148 148
149 149
150 150 class PyTestController(TestController):
151 151 """Run Python tests using IPython.testing.iptest"""
152 152 #: str, Python command to execute in subprocess
153 153 pycmd = None
154 154
155 155 def __init__(self, section, options):
156 156 """Create new test runner."""
157 157 TestController.__init__(self)
158 158 self.section = section
159 159 # pycmd is put into cmd[2] in PyTestController.launch()
160 160 self.cmd = [sys.executable, '-c', None, section]
161 161 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
162 162 self.options = options
163 163
164 164 def setup(self):
165 165 ipydir = TemporaryDirectory()
166 166 self.dirs.append(ipydir)
167 167 self.env['IPYTHONDIR'] = ipydir.name
168 # FIXME: install IPython kernel in temporary IPython dir
169 # remove after big split
170 try:
171 from jupyter_client.kernelspec import KernelSpecManager
172 except ImportError:
173 pass
174 else:
175 ksm = KernelSpecManager(ipython_dir=ipydir.name)
176 ksm.install_native_kernel_spec(user=True)
177
168 178 self.workingdir = workingdir = TemporaryDirectory()
169 179 self.dirs.append(workingdir)
170 180 self.env['IPTEST_WORKING_DIR'] = workingdir.name
171 181 # This means we won't get odd effects from our own matplotlib config
172 182 self.env['MPLCONFIGDIR'] = workingdir.name
173 183 # For security reasons (http://bugs.python.org/issue16202), use
174 184 # a temporary directory to which other users have no access.
175 185 self.env['TMPDIR'] = workingdir.name
176 186
177 187 # Add a non-accessible directory to PATH (see gh-7053)
178 188 noaccess = os.path.join(self.workingdir.name, "_no_access_")
179 189 self.noaccess = noaccess
180 190 os.mkdir(noaccess, 0)
181 191
182 192 PATH = os.environ.get('PATH', '')
183 193 if PATH:
184 194 PATH = noaccess + os.pathsep + PATH
185 195 else:
186 196 PATH = noaccess
187 197 self.env['PATH'] = PATH
188 198
189 199 # From options:
190 200 if self.options.xunit:
191 201 self.add_xunit()
192 202 if self.options.coverage:
193 203 self.add_coverage()
194 204 self.env['IPTEST_SUBPROC_STREAMS'] = self.options.subproc_streams
195 205 self.cmd.extend(self.options.extra_args)
196 206
197 207 def cleanup(self):
198 208 """
199 209 Make the non-accessible directory created in setup() accessible
200 210 again, otherwise deleting the workingdir will fail.
201 211 """
202 212 os.chmod(self.noaccess, stat.S_IRWXU)
203 213 TestController.cleanup(self)
204 214
205 215 @property
206 216 def will_run(self):
207 217 try:
208 218 return test_sections[self.section].will_run
209 219 except KeyError:
210 220 return True
211 221
212 222 def add_xunit(self):
213 223 xunit_file = os.path.abspath(self.section + '.xunit.xml')
214 224 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
215 225
216 226 def add_coverage(self):
217 227 try:
218 228 sources = test_sections[self.section].includes
219 229 except KeyError:
220 230 sources = ['IPython']
221 231
222 232 coverage_rc = ("[run]\n"
223 233 "data_file = {data_file}\n"
224 234 "source =\n"
225 235 " {source}\n"
226 236 ).format(data_file=os.path.abspath('.coverage.'+self.section),
227 237 source="\n ".join(sources))
228 238 config_file = os.path.join(self.workingdir.name, '.coveragerc')
229 239 with open(config_file, 'w') as f:
230 240 f.write(coverage_rc)
231 241
232 242 self.env['COVERAGE_PROCESS_START'] = config_file
233 243 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
234 244
235 245 def launch(self, buffer_output=False):
236 246 self.cmd[2] = self.pycmd
237 247 super(PyTestController, self).launch(buffer_output=buffer_output)
238 248
239 249
240 250 js_prefix = 'js/'
241 251
242 252 def get_js_test_dir():
243 253 import IPython.html.tests as t
244 254 return os.path.join(os.path.dirname(t.__file__), '')
245 255
246 256 def all_js_groups():
247 257 import glob
248 258 test_dir = get_js_test_dir()
249 259 all_subdirs = glob.glob(test_dir + '[!_]*/')
250 260 return [js_prefix+os.path.relpath(x, test_dir) for x in all_subdirs]
251 261
252 262 class JSController(TestController):
253 263 """Run CasperJS tests """
254 264
255 265 requirements = ['zmq', 'tornado', 'jinja2', 'casperjs', 'sqlite3',
256 266 'jsonschema']
257 267
258 268 def __init__(self, section, xunit=True, engine='phantomjs', url=None):
259 269 """Create new test runner."""
260 270 TestController.__init__(self)
261 271 self.engine = engine
262 272 self.section = section
263 273 self.xunit = xunit
264 274 self.url = url
265 275 self.slimer_failure = re.compile('^FAIL.*', flags=re.MULTILINE)
266 276 js_test_dir = get_js_test_dir()
267 277 includes = '--includes=' + os.path.join(js_test_dir,'util.js')
268 278 test_cases = os.path.join(js_test_dir, self.section[len(js_prefix):])
269 279 self.cmd = ['casperjs', 'test', includes, test_cases, '--engine=%s' % self.engine]
270 280
271 281 def setup(self):
272 282 self.ipydir = TemporaryDirectory()
273 283 self.nbdir = TemporaryDirectory()
274 284 self.dirs.append(self.ipydir)
275 285 self.dirs.append(self.nbdir)
276 286 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir1', u'sub βˆ‚ir 1a')))
277 287 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir2', u'sub βˆ‚ir 1b')))
278 288
279 289 if self.xunit:
280 290 self.add_xunit()
281 291
282 292 # If a url was specified, use that for the testing.
283 293 if self.url:
284 294 try:
285 295 alive = requests.get(self.url).status_code == 200
286 296 except:
287 297 alive = False
288 298
289 299 if alive:
290 300 self.cmd.append("--url=%s" % self.url)
291 301 else:
292 302 raise Exception('Could not reach "%s".' % self.url)
293 303 else:
294 304 # start the ipython notebook, so we get the port number
295 305 self.server_port = 0
296 306 self._init_server()
297 307 if self.server_port:
298 308 self.cmd.append("--port=%i" % self.server_port)
299 309 else:
300 310 # don't launch tests if the server didn't start
301 311 self.cmd = [sys.executable, '-c', 'raise SystemExit(1)']
302 312
303 313 def add_xunit(self):
304 314 xunit_file = os.path.abspath(self.section.replace('/','.') + '.xunit.xml')
305 315 self.cmd.append('--xunit=%s' % xunit_file)
306 316
307 317 def launch(self, buffer_output):
308 318 # If the engine is SlimerJS, we need to buffer the output because
309 319 # SlimerJS does not support exit codes, so CasperJS always returns 0.
310 320 if self.engine == 'slimerjs' and not buffer_output:
311 321 return super(JSController, self).launch(capture_output=True)
312 322
313 323 else:
314 324 return super(JSController, self).launch(buffer_output=buffer_output)
315 325
316 326 def wait(self, *pargs, **kwargs):
317 327 """Wait for the JSController to finish"""
318 328 ret = super(JSController, self).wait(*pargs, **kwargs)
319 329 # If this is a SlimerJS controller, check the captured stdout for
320 330 # errors. Otherwise, just return the return code.
321 331 if self.engine == 'slimerjs':
322 332 stdout = bytes_to_str(self.stdout)
323 333 if ret != 0:
324 334 # This could still happen e.g. if it's stopped by SIGINT
325 335 return ret
326 336 return bool(self.slimer_failure.search(strip_ansi(stdout)))
327 337 else:
328 338 return ret
329 339
330 340 def print_extra_info(self):
331 341 print("Running tests with notebook directory %r" % self.nbdir.name)
332 342
333 343 @property
334 344 def will_run(self):
335 345 should_run = all(have[a] for a in self.requirements + [self.engine])
336 346 return should_run
337 347
338 348 def _init_server(self):
339 349 "Start the notebook server in a separate process"
340 350 self.server_command = command = [sys.executable,
341 351 '-m', 'IPython.html',
342 352 '--no-browser',
343 353 '--ipython-dir', self.ipydir.name,
344 354 '--notebook-dir', self.nbdir.name,
345 355 ]
346 356 # ipc doesn't work on Windows, and darwin has crazy-long temp paths,
347 357 # which run afoul of ipc's maximum path length.
348 358 if sys.platform.startswith('linux'):
349 359 command.append('--KernelManager.transport=ipc')
350 360 self.stream_capturer = c = StreamCapturer()
351 361 c.start()
352 362 env = os.environ.copy()
353 363 if self.engine == 'phantomjs':
354 364 env['IPYTHON_ALLOW_DRAFT_WEBSOCKETS_FOR_PHANTOMJS'] = '1'
355 365 self.server = subprocess.Popen(command,
356 366 stdout=c.writefd,
357 367 stderr=subprocess.STDOUT,
358 368 cwd=self.nbdir.name,
359 369 env=env,
360 370 )
361 371 self.server_info_file = os.path.join(self.ipydir.name,
362 372 'profile_default', 'security', 'nbserver-%i.json' % self.server.pid
363 373 )
364 374 self._wait_for_server()
365 375
366 376 def _wait_for_server(self):
367 377 """Wait 30 seconds for the notebook server to start"""
368 378 for i in range(300):
369 379 if self.server.poll() is not None:
370 380 return self._failed_to_start()
371 381 if os.path.exists(self.server_info_file):
372 382 try:
373 383 self._load_server_info()
374 384 except ValueError:
375 385 # If the server is halfway through writing the file, we may
376 386 # get invalid JSON; it should be ready next iteration.
377 387 pass
378 388 else:
379 389 return
380 390 time.sleep(0.1)
381 391 print("Notebook server-info file never arrived: %s" % self.server_info_file,
382 392 file=sys.stderr
383 393 )
384 394
385 395 def _failed_to_start(self):
386 396 """Notebook server exited prematurely"""
387 397 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
388 398 print("Notebook failed to start: ", file=sys.stderr)
389 399 print(self.server_command)
390 400 print(captured, file=sys.stderr)
391 401
392 402 def _load_server_info(self):
393 403 """Notebook server started, load connection info from JSON"""
394 404 with open(self.server_info_file) as f:
395 405 info = json.load(f)
396 406 self.server_port = info['port']
397 407
398 408 def cleanup(self):
399 409 if hasattr(self, 'server'):
400 410 try:
401 411 self.server.terminate()
402 412 except OSError:
403 413 # already dead
404 414 pass
405 415 # wait 10s for the server to shutdown
406 416 try:
407 417 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
408 418 except TimeoutExpired:
409 419 # server didn't terminate, kill it
410 420 try:
411 421 print("Failed to terminate notebook server, killing it.",
412 422 file=sys.stderr
413 423 )
414 424 self.server.kill()
415 425 except OSError:
416 426 # already dead
417 427 pass
418 428 # wait another 10s
419 429 try:
420 430 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
421 431 except TimeoutExpired:
422 432 print("Notebook server still running (%s)" % self.server_info_file,
423 433 file=sys.stderr
424 434 )
425 435
426 436 self.stream_capturer.halt()
427 437 TestController.cleanup(self)
428 438
429 439
430 440 def prepare_controllers(options):
431 441 """Returns two lists of TestController instances, those to run, and those
432 442 not to run."""
433 443 testgroups = options.testgroups
434 444 if testgroups:
435 445 if 'js' in testgroups:
436 446 js_testgroups = all_js_groups()
437 447 else:
438 448 js_testgroups = [g for g in testgroups if g.startswith(js_prefix)]
439 449 py_testgroups = [g for g in testgroups if not g.startswith('js')]
440 450 else:
441 451 py_testgroups = py_test_group_names
442 452 if not options.all:
443 453 js_testgroups = []
444 454 test_sections['parallel'].enabled = False
445 455 else:
446 456 js_testgroups = all_js_groups()
447 457
448 458 engine = 'slimerjs' if options.slimerjs else 'phantomjs'
449 459 c_js = [JSController(name, xunit=options.xunit, engine=engine, url=options.url) for name in js_testgroups]
450 460 c_py = [PyTestController(name, options) for name in py_testgroups]
451 461
452 462 controllers = c_py + c_js
453 463 to_run = [c for c in controllers if c.will_run]
454 464 not_run = [c for c in controllers if not c.will_run]
455 465 return to_run, not_run
456 466
457 467 def do_run(controller, buffer_output=True):
458 468 """Setup and run a test controller.
459 469
460 470 If buffer_output is True, no output is displayed, to avoid it appearing
461 471 interleaved. In this case, the caller is responsible for displaying test
462 472 output on failure.
463 473
464 474 Returns
465 475 -------
466 476 controller : TestController
467 477 The same controller as passed in, as a convenience for using map() type
468 478 APIs.
469 479 exitcode : int
470 480 The exit code of the test subprocess. Non-zero indicates failure.
471 481 """
472 482 try:
473 483 try:
474 484 controller.setup()
475 485 if not buffer_output:
476 486 controller.print_extra_info()
477 487 controller.launch(buffer_output=buffer_output)
478 488 except Exception:
479 489 import traceback
480 490 traceback.print_exc()
481 491 return controller, 1 # signal failure
482 492
483 493 exitcode = controller.wait()
484 494 return controller, exitcode
485 495
486 496 except KeyboardInterrupt:
487 497 return controller, -signal.SIGINT
488 498 finally:
489 499 controller.cleanup()
490 500
491 501 def report():
492 502 """Return a string with a summary report of test-related variables."""
493 503 inf = get_sys_info()
494 504 out = []
495 505 def _add(name, value):
496 506 out.append((name, value))
497 507
498 508 _add('IPython version', inf['ipython_version'])
499 509 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
500 510 _add('IPython package', compress_user(inf['ipython_path']))
501 511 _add('Python version', inf['sys_version'].replace('\n',''))
502 512 _add('sys.executable', compress_user(inf['sys_executable']))
503 513 _add('Platform', inf['platform'])
504 514
505 515 width = max(len(n) for (n,v) in out)
506 516 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
507 517
508 518 avail = []
509 519 not_avail = []
510 520
511 521 for k, is_avail in have.items():
512 522 if is_avail:
513 523 avail.append(k)
514 524 else:
515 525 not_avail.append(k)
516 526
517 527 if avail:
518 528 out.append('\nTools and libraries available at test time:\n')
519 529 avail.sort()
520 530 out.append(' ' + ' '.join(avail)+'\n')
521 531
522 532 if not_avail:
523 533 out.append('\nTools and libraries NOT available at test time:\n')
524 534 not_avail.sort()
525 535 out.append(' ' + ' '.join(not_avail)+'\n')
526 536
527 537 return ''.join(out)
528 538
529 539 def run_iptestall(options):
530 540 """Run the entire IPython test suite by calling nose and trial.
531 541
532 542 This function constructs :class:`IPTester` instances for all IPython
533 543 modules and package and then runs each of them. This causes the modules
534 544 and packages of IPython to be tested each in their own subprocess using
535 545 nose.
536 546
537 547 Parameters
538 548 ----------
539 549
540 550 All parameters are passed as attributes of the options object.
541 551
542 552 testgroups : list of str
543 553 Run only these sections of the test suite. If empty, run all the available
544 554 sections.
545 555
546 556 fast : int or None
547 557 Run the test suite in parallel, using n simultaneous processes. If None
548 558 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
549 559
550 560 inc_slow : bool
551 561 Include slow tests, like IPython.parallel. By default, these tests aren't
552 562 run.
553 563
554 564 slimerjs : bool
555 565 Use slimerjs if it's installed instead of phantomjs for casperjs tests.
556 566
557 567 url : unicode
558 568 Address:port to use when running the JS tests.
559 569
560 570 xunit : bool
561 571 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
562 572
563 573 coverage : bool or str
564 574 Measure code coverage from tests. True will store the raw coverage data,
565 575 or pass 'html' or 'xml' to get reports.
566 576
567 577 extra_args : list
568 578 Extra arguments to pass to the test subprocesses, e.g. '-v'
569 579 """
570 580 to_run, not_run = prepare_controllers(options)
571 581
572 582 def justify(ltext, rtext, width=70, fill='-'):
573 583 ltext += ' '
574 584 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
575 585 return ltext + rtext
576 586
577 587 # Run all test runners, tracking execution time
578 588 failed = []
579 589 t_start = time.time()
580 590
581 591 print()
582 592 if options.fast == 1:
583 593 # This actually means sequential, i.e. with 1 job
584 594 for controller in to_run:
585 595 print('Test group:', controller.section)
586 596 sys.stdout.flush() # Show in correct order when output is piped
587 597 controller, res = do_run(controller, buffer_output=False)
588 598 if res:
589 599 failed.append(controller)
590 600 if res == -signal.SIGINT:
591 601 print("Interrupted")
592 602 break
593 603 print()
594 604
595 605 else:
596 606 # Run tests concurrently
597 607 try:
598 608 pool = multiprocessing.pool.ThreadPool(options.fast)
599 609 for (controller, res) in pool.imap_unordered(do_run, to_run):
600 610 res_string = 'OK' if res == 0 else 'FAILED'
601 611 print(justify('Test group: ' + controller.section, res_string))
602 612 if res:
603 613 controller.print_extra_info()
604 614 print(bytes_to_str(controller.stdout))
605 615 failed.append(controller)
606 616 if res == -signal.SIGINT:
607 617 print("Interrupted")
608 618 break
609 619 except KeyboardInterrupt:
610 620 return
611 621
612 622 for controller in not_run:
613 623 print(justify('Test group: ' + controller.section, 'NOT RUN'))
614 624
615 625 t_end = time.time()
616 626 t_tests = t_end - t_start
617 627 nrunners = len(to_run)
618 628 nfail = len(failed)
619 629 # summarize results
620 630 print('_'*70)
621 631 print('Test suite completed for system with the following information:')
622 632 print(report())
623 633 took = "Took %.3fs." % t_tests
624 634 print('Status: ', end='')
625 635 if not failed:
626 636 print('OK (%d test groups).' % nrunners, took)
627 637 else:
628 638 # If anything went wrong, point out what command to rerun manually to
629 639 # see the actual errors and individual summary
630 640 failed_sections = [c.section for c in failed]
631 641 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
632 642 nrunners, ', '.join(failed_sections)), took)
633 643 print()
634 644 print('You may wish to rerun these, with:')
635 645 print(' iptest', *failed_sections)
636 646 print()
637 647
638 648 if options.coverage:
639 649 from coverage import coverage, CoverageException
640 650 cov = coverage(data_file='.coverage')
641 651 cov.combine()
642 652 cov.save()
643 653
644 654 # Coverage HTML report
645 655 if options.coverage == 'html':
646 656 html_dir = 'ipy_htmlcov'
647 657 shutil.rmtree(html_dir, ignore_errors=True)
648 658 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
649 659 sys.stdout.flush()
650 660
651 661 # Custom HTML reporter to clean up module names.
652 662 from coverage.html import HtmlReporter
653 663 class CustomHtmlReporter(HtmlReporter):
654 664 def find_code_units(self, morfs):
655 665 super(CustomHtmlReporter, self).find_code_units(morfs)
656 666 for cu in self.code_units:
657 667 nameparts = cu.name.split(os.sep)
658 668 if 'IPython' not in nameparts:
659 669 continue
660 670 ix = nameparts.index('IPython')
661 671 cu.name = '.'.join(nameparts[ix:])
662 672
663 673 # Reimplement the html_report method with our custom reporter
664 674 cov._harvest_data()
665 675 cov.config.from_args(omit='*{0}tests{0}*'.format(os.sep), html_dir=html_dir,
666 676 html_title='IPython test coverage',
667 677 )
668 678 reporter = CustomHtmlReporter(cov, cov.config)
669 679 reporter.report(None)
670 680 print('done.')
671 681
672 682 # Coverage XML report
673 683 elif options.coverage == 'xml':
674 684 try:
675 685 cov.xml_report(outfile='ipy_coverage.xml')
676 686 except CoverageException as e:
677 687 print('Generating coverage report failed. Are you running javascript tests only?')
678 688 import traceback
679 689 traceback.print_exc()
680 690
681 691 if failed:
682 692 # Ensure that our exit code indicates failure
683 693 sys.exit(1)
684 694
685 695 argparser = argparse.ArgumentParser(description='Run IPython test suite')
686 696 argparser.add_argument('testgroups', nargs='*',
687 697 help='Run specified groups of tests. If omitted, run '
688 698 'all tests.')
689 699 argparser.add_argument('--all', action='store_true',
690 700 help='Include slow tests not run by default.')
691 701 argparser.add_argument('--slimerjs', action='store_true',
692 702 help="Use slimerjs if it's installed instead of phantomjs for casperjs tests.")
693 703 argparser.add_argument('--url', help="URL to use for the JS tests.")
694 704 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
695 705 help='Run test sections in parallel. This starts as many '
696 706 'processes as you have cores, or you can specify a number.')
697 707 argparser.add_argument('--xunit', action='store_true',
698 708 help='Produce Xunit XML results')
699 709 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
700 710 help="Measure test coverage. Specify 'html' or "
701 711 "'xml' to get reports.")
702 712 argparser.add_argument('--subproc-streams', default='capture',
703 713 help="What to do with stdout/stderr from subprocesses. "
704 714 "'capture' (default), 'show' and 'discard' are the options.")
705 715
706 716 def default_options():
707 717 """Get an argparse Namespace object with the default arguments, to pass to
708 718 :func:`run_iptestall`.
709 719 """
710 720 options = argparser.parse_args([])
711 721 options.extra_args = []
712 722 return options
713 723
714 724 def main():
715 725 # iptest doesn't work correctly if the working directory is the
716 726 # root of the IPython source tree. Tell the user to avoid
717 727 # frustration.
718 728 if os.path.exists(os.path.join(os.getcwd(),
719 729 'IPython', 'testing', '__main__.py')):
720 730 print("Don't run iptest from the IPython source directory",
721 731 file=sys.stderr)
722 732 sys.exit(1)
723 733 # Arguments after -- should be passed through to nose. Argparse treats
724 734 # everything after -- as regular positional arguments, so we separate them
725 735 # first.
726 736 try:
727 737 ix = sys.argv.index('--')
728 738 except ValueError:
729 739 to_parse = sys.argv[1:]
730 740 extra_args = []
731 741 else:
732 742 to_parse = sys.argv[1:ix]
733 743 extra_args = sys.argv[ix+1:]
734 744
735 745 options = argparser.parse_args(to_parse)
736 746 options.extra_args = extra_args
737 747
738 748 run_iptestall(options)
739 749
740 750
741 751 if __name__ == '__main__':
742 752 main()
General Comments 0
You need to be logged in to leave comments. Login now