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