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