##// END OF EJS Templates
Allow xunit and coverage output
Thomas Kluyver -
Show More
@@ -1,284 +1,292 b''
1 1 # -*- coding: utf-8 -*-
2 2 """IPython Test Process Controller
3 3
4 4 This module runs one or more subprocesses which will actually run the IPython
5 5 test suite.
6 6
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2009-2011 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19 from __future__ import print_function
20 20
21 21 import argparse
22 22 import multiprocessing.pool
23 23 import os
24 24 import signal
25 25 import sys
26 26 import subprocess
27 27 import time
28 28
29 29 from .iptest import have, test_group_names, test_sections
30 30 from IPython.utils import py3compat
31 31 from IPython.utils.sysinfo import sys_info
32 32 from IPython.utils.tempdir import TemporaryDirectory
33 33
34 34
35 35 class IPTestController(object):
36 36 """Run iptest in a subprocess
37 37 """
38 38 #: str, IPython test suite to be executed.
39 39 section = None
40 40 #: list, command line arguments to be executed
41 41 cmd = None
42 42 #: dict, extra environment variables to set for the subprocess
43 43 env = None
44 44 #: list, TemporaryDirectory instances to clear up when the process finishes
45 45 dirs = None
46 46 #: subprocess.Popen instance
47 47 process = None
48 48 buffer_output = False
49 49
50 50 def __init__(self, section):
51 51 """Create new test runner."""
52 52 self.section = section
53 53 self.cmd = [sys.executable, '-m', 'IPython.testing.iptest', section]
54 54 self.env = {}
55 55 self.dirs = []
56 56 ipydir = TemporaryDirectory()
57 57 self.dirs.append(ipydir)
58 58 self.env['IPYTHONDIR'] = ipydir.name
59 59 workingdir = TemporaryDirectory()
60 60 self.dirs.append(workingdir)
61 61 self.env['IPTEST_WORKING_DIR'] = workingdir.name
62 62
63 63 def add_xunit(self):
64 64 xunit_file = os.path.abspath(self.section + '.xunit.xml')
65 65 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
66 66
67 67 def add_coverage(self, xml=True):
68 self.cmd.extend(['--with-coverage', '--cover-package', self.section])
68 self.cmd.append('--with-coverage')
69 for include in test_sections[self.section].includes:
70 self.cmd.extend(['--cover-package', include])
69 71 if xml:
70 72 coverage_xml = os.path.abspath(self.section + ".coverage.xml")
71 73 self.cmd.extend(['--cover-xml', '--cover-xml-file', coverage_xml])
72 74
73 75
74 76 def launch(self):
75 77 # print('*** ENV:', self.env) # dbg
76 78 # print('*** CMD:', self.cmd) # dbg
77 79 env = os.environ.copy()
78 80 env.update(self.env)
79 81 output = subprocess.PIPE if self.buffer_output else None
80 82 stdout = subprocess.STDOUT if self.buffer_output else None
81 83 self.process = subprocess.Popen(self.cmd, stdout=output,
82 84 stderr=stdout, env=env)
83 85
84 86 def wait(self):
85 87 self.stdout, _ = self.process.communicate()
86 88 return self.process.returncode
87 89
88 90 def cleanup_process(self):
89 91 """Cleanup on exit by killing any leftover processes."""
90 92 subp = self.process
91 93 if subp is None or (subp.poll() is not None):
92 94 return # Process doesn't exist, or is already dead.
93 95
94 96 try:
95 97 print('Cleaning up stale PID: %d' % subp.pid)
96 98 subp.kill()
97 99 except: # (OSError, WindowsError) ?
98 100 # This is just a best effort, if we fail or the process was
99 101 # really gone, ignore it.
100 102 pass
101 103 else:
102 104 for i in range(10):
103 105 if subp.poll() is None:
104 106 time.sleep(0.1)
105 107 else:
106 108 break
107 109
108 110 if subp.poll() is None:
109 111 # The process did not die...
110 112 print('... failed. Manual cleanup may be required.')
111 113
112 114 def cleanup(self):
113 115 "Kill process if it's still alive, and clean up temporary directories"
114 116 self.cleanup_process()
115 117 for td in self.dirs:
116 118 td.cleanup()
117 119
118 120 __del__ = cleanup
119 121
120 def test_controllers_to_run(inc_slow=False):
122 def test_controllers_to_run(inc_slow=False, xunit=False, coverage=False):
121 123 """Returns an ordered list of IPTestController instances to be run."""
122 124 res = []
123 125 if not inc_slow:
124 126 test_sections['parallel'].enabled = False
125 127
126 128 for name in test_group_names:
127 129 if test_sections[name].will_run:
128 res.append(IPTestController(name))
130 controller = IPTestController(name)
131 if xunit:
132 controller.add_xunit()
133 if coverage:
134 controller.add_coverage(xml=True)
135 res.append(controller)
129 136 return res
130 137
131 138 def do_run(controller):
132 139 try:
133 140 try:
134 141 controller.launch()
135 142 except Exception:
136 143 import traceback
137 144 traceback.print_exc()
138 145 return controller, 1 # signal failure
139 146
140 147 exitcode = controller.wait()
141 148 return controller, exitcode
142 149
143 150 except KeyboardInterrupt:
144 151 return controller, -signal.SIGINT
145 152 finally:
146 153 controller.cleanup()
147 154
148 155 def report():
149 156 """Return a string with a summary report of test-related variables."""
150 157
151 158 out = [ sys_info(), '\n']
152 159
153 160 avail = []
154 161 not_avail = []
155 162
156 163 for k, is_avail in have.items():
157 164 if is_avail:
158 165 avail.append(k)
159 166 else:
160 167 not_avail.append(k)
161 168
162 169 if avail:
163 170 out.append('\nTools and libraries available at test time:\n')
164 171 avail.sort()
165 172 out.append(' ' + ' '.join(avail)+'\n')
166 173
167 174 if not_avail:
168 175 out.append('\nTools and libraries NOT available at test time:\n')
169 176 not_avail.sort()
170 177 out.append(' ' + ' '.join(not_avail)+'\n')
171 178
172 179 return ''.join(out)
173 180
174 181 def run_iptestall(inc_slow=False, jobs=1, xunit=False, coverage=False):
175 182 """Run the entire IPython test suite by calling nose and trial.
176 183
177 184 This function constructs :class:`IPTester` instances for all IPython
178 185 modules and package and then runs each of them. This causes the modules
179 186 and packages of IPython to be tested each in their own subprocess using
180 187 nose.
181 188
182 189 Parameters
183 190 ----------
184 191
185 192 inc_slow : bool, optional
186 193 Include slow tests, like IPython.parallel. By default, these tests aren't
187 194 run.
188 195
189 196 fast : bool, option
190 197 Run the test suite in parallel, if True, using as many threads as there
191 198 are processors
192 199 """
193 200 if jobs != 1:
194 201 IPTestController.buffer_output = True
195 202
196 controllers = test_controllers_to_run(inc_slow=inc_slow)
203 controllers = test_controllers_to_run(inc_slow=inc_slow, xunit=xunit,
204 coverage=coverage)
197 205
198 206 # Run all test runners, tracking execution time
199 207 failed = []
200 208 t_start = time.time()
201 209
202 210 print('*'*70)
203 211 if jobs == 1:
204 212 for controller in controllers:
205 213 print('IPython test group:', controller.section)
206 214 controller, res = do_run(controller)
207 215 if res:
208 216 failed.append(controller)
209 217 if res == -signal.SIGINT:
210 218 print("Interrupted")
211 219 break
212 220 print()
213 221
214 222 else:
215 223 try:
216 224 pool = multiprocessing.pool.ThreadPool(jobs)
217 225 for (controller, res) in pool.imap_unordered(do_run, controllers):
218 226 tgroup = 'IPython test group: ' + controller.section + ' '
219 227 res_string = ' OK' if res == 0 else ' FAILED'
220 228 res_string = res_string.rjust(70 - len(tgroup), '.')
221 229 print(tgroup + res_string)
222 230 if res:
223 231 print(controller.stdout)
224 232 failed.append(controller)
225 233 if res == -signal.SIGINT:
226 234 print("Interrupted")
227 235 break
228 236 except KeyboardInterrupt:
229 237 return
230 238
231 239 t_end = time.time()
232 240 t_tests = t_end - t_start
233 241 nrunners = len(controllers)
234 242 nfail = len(failed)
235 243 # summarize results
236 244 print('*'*70)
237 245 print('Test suite completed for system with the following information:')
238 246 print(report())
239 247 print('Ran %s test groups in %.3fs' % (nrunners, t_tests))
240 248 print()
241 249 print('Status:')
242 250 if not failed:
243 251 print('OK')
244 252 else:
245 253 # If anything went wrong, point out what command to rerun manually to
246 254 # see the actual errors and individual summary
247 255 print('ERROR - %s out of %s test groups failed.' % (nfail, nrunners))
248 256 for controller in failed:
249 257 print('-'*40)
250 258 print('Runner failed:', controller.section)
251 259 print('You may wish to rerun this one individually, with:')
252 260 failed_call_args = [py3compat.cast_unicode(x) for x in controller.cmd]
253 261 print(u' '.join(failed_call_args))
254 262 print()
255 263 # Ensure that our exit code indicates failure
256 264 sys.exit(1)
257 265
258 266
259 267 def main():
260 268 if len(sys.argv) > 1 and (sys.argv[1] in test_sections):
261 269 from .iptest import run_iptest
262 270 # This is in-process
263 271 run_iptest()
264 272 return
265 273
266 274 parser = argparse.ArgumentParser(description='Run IPython test suite')
267 275 parser.add_argument('--all', action='store_true',
268 276 help='Include slow tests not run by default.')
269 277 parser.add_argument('-j', '--fast', nargs='?', const=None, default=1,
270 278 help='Run test sections in parallel.')
271 279 parser.add_argument('--xunit', action='store_true',
272 280 help='Produce Xunit XML results')
273 281 parser.add_argument('--coverage', action='store_true',
274 282 help='Measure test coverage.')
275 283
276 284 options = parser.parse_args()
277 285
278 286 # This starts subprocesses
279 287 run_iptestall(inc_slow=options.all, jobs=options.fast,
280 288 xunit=options.xunit, coverage=options.coverage)
281 289
282 290
283 291 if __name__ == '__main__':
284 292 main()
General Comments 0
You need to be logged in to leave comments. Login now