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