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