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