##// END OF EJS Templates
Simplify parsing -j arg
Thomas Kluyver -
Show More
@@ -1,410 +1,404 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 shutil
25 25 import signal
26 26 import sys
27 27 import subprocess
28 28 import time
29 29
30 30 from .iptest import have, test_group_names, test_sections
31 31 from IPython.utils.py3compat import bytes_to_str
32 32 from IPython.utils.sysinfo import sys_info
33 33 from IPython.utils.tempdir import TemporaryDirectory
34 34
35 35
36 36 class TestController(object):
37 37 """Run tests in a subprocess
38 38 """
39 39 #: str, IPython test suite to be executed.
40 40 section = None
41 41 #: list, command line arguments to be executed
42 42 cmd = None
43 43 #: dict, extra environment variables to set for the subprocess
44 44 env = None
45 45 #: list, TemporaryDirectory instances to clear up when the process finishes
46 46 dirs = None
47 47 #: subprocess.Popen instance
48 48 process = None
49 49 #: str, process stdout+stderr
50 50 stdout = None
51 51 #: bool, whether to capture process stdout & stderr
52 52 buffer_output = False
53 53
54 54 def __init__(self):
55 55 self.cmd = []
56 56 self.env = {}
57 57 self.dirs = []
58 58
59 59 @property
60 60 def will_run(self):
61 61 """Override in subclasses to check for dependencies."""
62 62 return False
63 63
64 64 def launch(self):
65 65 # print('*** ENV:', self.env) # dbg
66 66 # print('*** CMD:', self.cmd) # dbg
67 67 env = os.environ.copy()
68 68 env.update(self.env)
69 69 output = subprocess.PIPE if self.buffer_output else None
70 70 stdout = subprocess.STDOUT if self.buffer_output else None
71 71 self.process = subprocess.Popen(self.cmd, stdout=output,
72 72 stderr=stdout, env=env)
73 73
74 74 def wait(self):
75 75 self.stdout, _ = self.process.communicate()
76 76 return self.process.returncode
77 77
78 78 def cleanup_process(self):
79 79 """Cleanup on exit by killing any leftover processes."""
80 80 subp = self.process
81 81 if subp is None or (subp.poll() is not None):
82 82 return # Process doesn't exist, or is already dead.
83 83
84 84 try:
85 85 print('Cleaning up stale PID: %d' % subp.pid)
86 86 subp.kill()
87 87 except: # (OSError, WindowsError) ?
88 88 # This is just a best effort, if we fail or the process was
89 89 # really gone, ignore it.
90 90 pass
91 91 else:
92 92 for i in range(10):
93 93 if subp.poll() is None:
94 94 time.sleep(0.1)
95 95 else:
96 96 break
97 97
98 98 if subp.poll() is None:
99 99 # The process did not die...
100 100 print('... failed. Manual cleanup may be required.')
101 101
102 102 def cleanup(self):
103 103 "Kill process if it's still alive, and clean up temporary directories"
104 104 self.cleanup_process()
105 105 for td in self.dirs:
106 106 td.cleanup()
107 107
108 108 __del__ = cleanup
109 109
110 110 class PyTestController(TestController):
111 111 """Run Python tests using IPython.testing.iptest"""
112 112 #: str, Python command to execute in subprocess
113 113 pycmd = None
114 114
115 115 def __init__(self, section):
116 116 """Create new test runner."""
117 117 TestController.__init__(self)
118 118 self.section = section
119 119 # pycmd is put into cmd[2] in PyTestController.launch()
120 120 self.cmd = [sys.executable, '-c', None, section]
121 121 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
122 122 ipydir = TemporaryDirectory()
123 123 self.dirs.append(ipydir)
124 124 self.env['IPYTHONDIR'] = ipydir.name
125 125 self.workingdir = workingdir = TemporaryDirectory()
126 126 self.dirs.append(workingdir)
127 127 self.env['IPTEST_WORKING_DIR'] = workingdir.name
128 128 # This means we won't get odd effects from our own matplotlib config
129 129 self.env['MPLCONFIGDIR'] = workingdir.name
130 130
131 131 @property
132 132 def will_run(self):
133 133 try:
134 134 return test_sections[self.section].will_run
135 135 except KeyError:
136 136 return True
137 137
138 138 def add_xunit(self):
139 139 xunit_file = os.path.abspath(self.section + '.xunit.xml')
140 140 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
141 141
142 142 def add_coverage(self):
143 143 try:
144 144 sources = test_sections[self.section].includes
145 145 except KeyError:
146 146 sources = ['IPython']
147 147
148 148 coverage_rc = ("[run]\n"
149 149 "data_file = {data_file}\n"
150 150 "source =\n"
151 151 " {source}\n"
152 152 ).format(data_file=os.path.abspath('.coverage.'+self.section),
153 153 source="\n ".join(sources))
154 154 config_file = os.path.join(self.workingdir.name, '.coveragerc')
155 155 with open(config_file, 'w') as f:
156 156 f.write(coverage_rc)
157 157
158 158 self.env['COVERAGE_PROCESS_START'] = config_file
159 159 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
160 160
161 161 def launch(self):
162 162 self.cmd[2] = self.pycmd
163 163 super(PyTestController, self).launch()
164 164
165 165
166 166 def prepare_py_test_controllers(inc_slow=False):
167 167 """Returns an ordered list of PyTestController instances to be run."""
168 168 to_run, not_run = [], []
169 169 if not inc_slow:
170 170 test_sections['parallel'].enabled = False
171 171
172 172 for name in test_group_names:
173 173 controller = PyTestController(name)
174 174 if controller.will_run:
175 175 to_run.append(controller)
176 176 else:
177 177 not_run.append(controller)
178 178 return to_run, not_run
179 179
180 180 def configure_controllers(controllers, xunit=False, coverage=False):
181 181 """Apply options for a collection of TestController objects."""
182 182 for controller in controllers:
183 183 if xunit:
184 184 controller.add_xunit()
185 185 if coverage:
186 186 controller.add_coverage()
187 187
188 188 def do_run(controller):
189 189 try:
190 190 try:
191 191 controller.launch()
192 192 except Exception:
193 193 import traceback
194 194 traceback.print_exc()
195 195 return controller, 1 # signal failure
196 196
197 197 exitcode = controller.wait()
198 198 return controller, exitcode
199 199
200 200 except KeyboardInterrupt:
201 201 return controller, -signal.SIGINT
202 202 finally:
203 203 controller.cleanup()
204 204
205 205 def report():
206 206 """Return a string with a summary report of test-related variables."""
207 207
208 208 out = [ sys_info(), '\n']
209 209
210 210 avail = []
211 211 not_avail = []
212 212
213 213 for k, is_avail in have.items():
214 214 if is_avail:
215 215 avail.append(k)
216 216 else:
217 217 not_avail.append(k)
218 218
219 219 if avail:
220 220 out.append('\nTools and libraries available at test time:\n')
221 221 avail.sort()
222 222 out.append(' ' + ' '.join(avail)+'\n')
223 223
224 224 if not_avail:
225 225 out.append('\nTools and libraries NOT available at test time:\n')
226 226 not_avail.sort()
227 227 out.append(' ' + ' '.join(not_avail)+'\n')
228 228
229 229 return ''.join(out)
230 230
231 231 def run_iptestall(options):
232 232 """Run the entire IPython test suite by calling nose and trial.
233 233
234 234 This function constructs :class:`IPTester` instances for all IPython
235 235 modules and package and then runs each of them. This causes the modules
236 236 and packages of IPython to be tested each in their own subprocess using
237 237 nose.
238 238
239 239 Parameters
240 240 ----------
241 241
242 242 All parameters are passed as attributes of the options object.
243 243
244 244 testgroups : list of str
245 245 Run only these sections of the test suite. If empty, run all the available
246 246 sections.
247 247
248 248 fast : int or None
249 249 Run the test suite in parallel, using n simultaneous processes. If None
250 250 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
251 251
252 252 inc_slow : bool
253 253 Include slow tests, like IPython.parallel. By default, these tests aren't
254 254 run.
255 255
256 256 xunit : bool
257 257 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
258 258
259 259 coverage : bool or str
260 260 Measure code coverage from tests. True will store the raw coverage data,
261 261 or pass 'html' or 'xml' to get reports.
262 262 """
263 263 if options.fast != 1:
264 264 # If running in parallel, capture output so it doesn't get interleaved
265 265 TestController.buffer_output = True
266 266
267 267 if options.testgroups:
268 268 to_run = [PyTestController(name) for name in options.testgroups]
269 269 not_run = []
270 270 else:
271 271 to_run, not_run = prepare_py_test_controllers(inc_slow=options.all)
272 272
273 273 configure_controllers(to_run, xunit=options.xunit, coverage=options.coverage)
274 274
275 275 def justify(ltext, rtext, width=70, fill='-'):
276 276 ltext += ' '
277 277 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
278 278 return ltext + rtext
279 279
280 280 # Run all test runners, tracking execution time
281 281 failed = []
282 282 t_start = time.time()
283 283
284 284 print()
285 285 if options.fast == 1:
286 286 # This actually means sequential, i.e. with 1 job
287 287 for controller in to_run:
288 288 print('IPython test group:', controller.section)
289 289 controller, res = do_run(controller)
290 290 if res:
291 291 failed.append(controller)
292 292 if res == -signal.SIGINT:
293 293 print("Interrupted")
294 294 break
295 295 print()
296 296
297 297 else:
298 298 # Run tests concurrently
299 299 try:
300 300 pool = multiprocessing.pool.ThreadPool(options.fast)
301 301 for (controller, res) in pool.imap_unordered(do_run, to_run):
302 302 res_string = 'OK' if res == 0 else 'FAILED'
303 303 print(justify('IPython test group: ' + controller.section, res_string))
304 304 if res:
305 305 print(bytes_to_str(controller.stdout))
306 306 failed.append(controller)
307 307 if res == -signal.SIGINT:
308 308 print("Interrupted")
309 309 break
310 310 except KeyboardInterrupt:
311 311 return
312 312
313 313 for controller in not_run:
314 314 print(justify('IPython test group: ' + controller.section, 'NOT RUN'))
315 315
316 316 t_end = time.time()
317 317 t_tests = t_end - t_start
318 318 nrunners = len(to_run)
319 319 nfail = len(failed)
320 320 # summarize results
321 321 print('_'*70)
322 322 print('Test suite completed for system with the following information:')
323 323 print(report())
324 324 print('Ran %s test groups in %.3fs' % (nrunners, t_tests))
325 325 print()
326 326 print('Status: ', end='')
327 327 if not failed:
328 328 print('OK')
329 329 else:
330 330 # If anything went wrong, point out what command to rerun manually to
331 331 # see the actual errors and individual summary
332 332 failed_sections = [c.section for c in failed]
333 333 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
334 334 nrunners, ', '.join(failed_sections)))
335 335 print()
336 336 print('You may wish to rerun these, with:')
337 337 print(' iptest', *failed_sections)
338 338 print()
339 339
340 340 if options.coverage:
341 341 from coverage import coverage
342 342 cov = coverage(data_file='.coverage')
343 343 cov.combine()
344 344 cov.save()
345 345
346 346 # Coverage HTML report
347 347 if options.coverage == 'html':
348 348 html_dir = 'ipy_htmlcov'
349 349 shutil.rmtree(html_dir, ignore_errors=True)
350 350 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
351 351 sys.stdout.flush()
352 352
353 353 # Custom HTML reporter to clean up module names.
354 354 from coverage.html import HtmlReporter
355 355 class CustomHtmlReporter(HtmlReporter):
356 356 def find_code_units(self, morfs):
357 357 super(CustomHtmlReporter, self).find_code_units(morfs)
358 358 for cu in self.code_units:
359 359 nameparts = cu.name.split(os.sep)
360 360 if 'IPython' not in nameparts:
361 361 continue
362 362 ix = nameparts.index('IPython')
363 363 cu.name = '.'.join(nameparts[ix:])
364 364
365 365 # Reimplement the html_report method with our custom reporter
366 366 cov._harvest_data()
367 367 cov.config.from_args(omit='*%stests' % os.sep, html_dir=html_dir,
368 368 html_title='IPython test coverage',
369 369 )
370 370 reporter = CustomHtmlReporter(cov, cov.config)
371 371 reporter.report(None)
372 372 print('done.')
373 373
374 374 # Coverage XML report
375 375 elif options.coverage == 'xml':
376 376 cov.xml_report(outfile='ipy_coverage.xml')
377 377
378 378 if failed:
379 379 # Ensure that our exit code indicates failure
380 380 sys.exit(1)
381 381
382 382
383 383 def main():
384 384 parser = argparse.ArgumentParser(description='Run IPython test suite')
385 385 parser.add_argument('testgroups', nargs='*',
386 386 help='Run specified groups of tests. If omitted, run '
387 387 'all tests.')
388 388 parser.add_argument('--all', action='store_true',
389 389 help='Include slow tests not run by default.')
390 parser.add_argument('-j', '--fast', nargs='?', const=None, default=1,
390 parser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
391 391 help='Run test sections in parallel.')
392 392 parser.add_argument('--xunit', action='store_true',
393 393 help='Produce Xunit XML results')
394 394 parser.add_argument('--coverage', nargs='?', const=True, default=False,
395 395 help="Measure test coverage. Specify 'html' or "
396 396 "'xml' to get reports.")
397 397
398 398 options = parser.parse_args()
399 399
400 try:
401 options.fast = int(options.fast)
402 except TypeError:
403 pass
404
405 # This starts subprocesses
406 400 run_iptestall(options)
407 401
408 402
409 403 if __name__ == '__main__':
410 404 main()
General Comments 0
You need to be logged in to leave comments. Login now