##// END OF EJS Templates
don't create js test directories unless they are being used...
MinRK -
Show More
@@ -1,520 +1,521 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 from multiprocessing import Process, Queue
24 24 import os
25 25 import shutil
26 26 import signal
27 27 import sys
28 28 import subprocess
29 29 import time
30 30
31 31 from .iptest import have, test_group_names as py_test_group_names, test_sections
32 32 from IPython.utils.path import compress_user
33 33 from IPython.utils.py3compat import bytes_to_str
34 34 from IPython.utils.sysinfo import get_sys_info
35 35 from IPython.utils.tempdir import TemporaryDirectory
36 36
37 37
38 38 class TestController(object):
39 39 """Run tests in a subprocess
40 40 """
41 41 #: str, IPython test suite to be executed.
42 42 section = None
43 43 #: list, command line arguments to be executed
44 44 cmd = None
45 45 #: dict, extra environment variables to set for the subprocess
46 46 env = None
47 47 #: list, TemporaryDirectory instances to clear up when the process finishes
48 48 dirs = None
49 49 #: subprocess.Popen instance
50 50 process = None
51 51 #: str, process stdout+stderr
52 52 stdout = None
53 53 #: bool, whether to capture process stdout & stderr
54 54 buffer_output = False
55 55
56 56 def __init__(self):
57 57 self.cmd = []
58 58 self.env = {}
59 59 self.dirs = []
60 60
61 61 def launch(self):
62 62 # print('*** ENV:', self.env) # dbg
63 63 # print('*** CMD:', self.cmd) # dbg
64 64 env = os.environ.copy()
65 65 env.update(self.env)
66 66 output = subprocess.PIPE if self.buffer_output else None
67 67 stdout = subprocess.STDOUT if self.buffer_output else None
68 68 self.process = subprocess.Popen(self.cmd, stdout=output,
69 69 stderr=stdout, env=env)
70 70
71 71 def wait(self):
72 72 self.stdout, _ = self.process.communicate()
73 73 return self.process.returncode
74 74
75 75 def cleanup_process(self):
76 76 """Cleanup on exit by killing any leftover processes."""
77 77 subp = self.process
78 78 if subp is None or (subp.poll() is not None):
79 79 return # Process doesn't exist, or is already dead.
80 80
81 81 try:
82 82 print('Cleaning up stale PID: %d' % subp.pid)
83 83 subp.kill()
84 84 except: # (OSError, WindowsError) ?
85 85 # This is just a best effort, if we fail or the process was
86 86 # really gone, ignore it.
87 87 pass
88 88 else:
89 89 for i in range(10):
90 90 if subp.poll() is None:
91 91 time.sleep(0.1)
92 92 else:
93 93 break
94 94
95 95 if subp.poll() is None:
96 96 # The process did not die...
97 97 print('... failed. Manual cleanup may be required.')
98 98
99 99 def cleanup(self):
100 100 "Kill process if it's still alive, and clean up temporary directories"
101 101 self.cleanup_process()
102 102 for td in self.dirs:
103 103 td.cleanup()
104 104
105 105 __del__ = cleanup
106 106
107 107 class PyTestController(TestController):
108 108 """Run Python tests using IPython.testing.iptest"""
109 109 #: str, Python command to execute in subprocess
110 110 pycmd = None
111 111
112 112 def __init__(self, section):
113 113 """Create new test runner."""
114 114 TestController.__init__(self)
115 115 self.section = section
116 116 # pycmd is put into cmd[2] in PyTestController.launch()
117 117 self.cmd = [sys.executable, '-c', None, section]
118 118 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
119 119 ipydir = TemporaryDirectory()
120 120 self.dirs.append(ipydir)
121 121 self.env['IPYTHONDIR'] = ipydir.name
122 122 self.workingdir = workingdir = TemporaryDirectory()
123 123 self.dirs.append(workingdir)
124 124 self.env['IPTEST_WORKING_DIR'] = workingdir.name
125 125 # This means we won't get odd effects from our own matplotlib config
126 126 self.env['MPLCONFIGDIR'] = workingdir.name
127 127
128 128 @property
129 129 def will_run(self):
130 130 try:
131 131 return test_sections[self.section].will_run
132 132 except KeyError:
133 133 return True
134 134
135 135 def add_xunit(self):
136 136 xunit_file = os.path.abspath(self.section + '.xunit.xml')
137 137 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
138 138
139 139 def add_coverage(self):
140 140 try:
141 141 sources = test_sections[self.section].includes
142 142 except KeyError:
143 143 sources = ['IPython']
144 144
145 145 coverage_rc = ("[run]\n"
146 146 "data_file = {data_file}\n"
147 147 "source =\n"
148 148 " {source}\n"
149 149 ).format(data_file=os.path.abspath('.coverage.'+self.section),
150 150 source="\n ".join(sources))
151 151 config_file = os.path.join(self.workingdir.name, '.coveragerc')
152 152 with open(config_file, 'w') as f:
153 153 f.write(coverage_rc)
154 154
155 155 self.env['COVERAGE_PROCESS_START'] = config_file
156 156 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
157 157
158 158 def launch(self):
159 159 self.cmd[2] = self.pycmd
160 160 super(PyTestController, self).launch()
161 161
162 162 js_prefix = 'js/'
163 163
164 164 def get_js_test_dir():
165 165 import IPython.html.tests as t
166 166 return os.path.join(os.path.dirname(t.__file__), '')
167 167
168 168 def all_js_groups():
169 169 import glob
170 170 test_dir = get_js_test_dir()
171 171 all_subdirs = glob.glob(test_dir + '*/')
172 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 174 class JSController(TestController):
175 175 """Run CasperJS tests """
176 176 def __init__(self, section):
177 177 """Create new test runner."""
178 178 TestController.__init__(self)
179 179 self.section = section
180 180
181
182 def launch(self):
181 183 self.ipydir = TemporaryDirectory()
182 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 186 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir1', u'sub βˆ‚ir 1a')))
185 187 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir2', u'sub βˆ‚ir 1b')))
186 188 self.dirs.append(self.ipydir)
187 189 self.dirs.append(self.nbdir)
188
189 def launch(self):
190
190 191 # start the ipython notebook, so we get the port number
191 192 self._init_server()
192 193 js_test_dir = get_js_test_dir()
193 194 includes = '--includes=' + os.path.join(js_test_dir,'util.js')
194 195 test_cases = os.path.join(js_test_dir, self.section[len(js_prefix):])
195 196 port = '--port=' + str(self.server_port)
196 197 self.cmd = ['casperjs', 'test', port, includes, test_cases]
197 198 super(JSController, self).launch()
198 199
199 200 @property
200 201 def will_run(self):
201 202 return all(have[a] for a in ['zmq', 'tornado', 'jinja2', 'casperjs'])
202 203
203 204 def _init_server(self):
204 205 "Start the notebook server in a separate process"
205 206 self.queue = q = Queue()
206 207 self.server = Process(target=run_webapp, args=(q, self.ipydir.name, self.nbdir.name))
207 208 self.server.start()
208 209 self.server_port = q.get()
209 210
210 211 def cleanup(self):
211 212 self.server.terminate()
212 213 self.server.join()
213 214 TestController.cleanup(self)
214 215
215 216 def run_webapp(q, ipydir, nbdir, loglevel=0):
216 217 """start the IPython Notebook, and pass port back to the queue"""
217 218 import os
218 219 import IPython.html.notebookapp as nbapp
219 220 import sys
220 221 sys.stderr = open(os.devnull, 'w')
221 222 server = nbapp.NotebookApp()
222 223 args = ['--no-browser']
223 224 args.extend(['--ipython-dir', ipydir])
224 225 args.extend(['--notebook-dir', nbdir])
225 226 args.extend(['--log-level', str(loglevel)])
226 227 server.initialize(args)
227 228 # communicate the port number to the parent process
228 229 q.put(server.port)
229 230 server.start()
230 231
231 232 def prepare_controllers(options):
232 233 """Returns two lists of TestController instances, those to run, and those
233 234 not to run."""
234 235 testgroups = options.testgroups
235 236
236 237 if testgroups:
237 238 py_testgroups = [g for g in testgroups if (g in py_test_group_names) \
238 239 or g.startswith('IPython.')]
239 240 if 'js' in testgroups:
240 241 js_testgroups = all_js_groups()
241 242 else:
242 243 js_testgroups = [g for g in testgroups if g not in py_testgroups]
243 244 else:
244 245 py_testgroups = py_test_group_names
245 246 js_testgroups = all_js_groups()
246 247 if not options.all:
247 248 test_sections['parallel'].enabled = False
248 249
249 250 c_js = [JSController(name) for name in js_testgroups]
250 251 c_py = [PyTestController(name) for name in py_testgroups]
251 252
252 253 configure_py_controllers(c_py, xunit=options.xunit,
253 254 coverage=options.coverage, subproc_streams=options.subproc_streams,
254 255 extra_args=options.extra_args)
255 256
256 257 controllers = c_py + c_js
257 258 to_run = [c for c in controllers if c.will_run]
258 259 not_run = [c for c in controllers if not c.will_run]
259 260 return to_run, not_run
260 261
261 262 def configure_py_controllers(controllers, xunit=False, coverage=False,
262 263 subproc_streams='capture', extra_args=()):
263 264 """Apply options for a collection of TestController objects."""
264 265 for controller in controllers:
265 266 if xunit:
266 267 controller.add_xunit()
267 268 if coverage:
268 269 controller.add_coverage()
269 270 controller.env['IPTEST_SUBPROC_STREAMS'] = subproc_streams
270 271 controller.cmd.extend(extra_args)
271 272
272 273 def do_run(controller):
273 274 try:
274 275 try:
275 276 controller.launch()
276 277 except Exception:
277 278 import traceback
278 279 traceback.print_exc()
279 280 return controller, 1 # signal failure
280 281
281 282 exitcode = controller.wait()
282 283 return controller, exitcode
283 284
284 285 except KeyboardInterrupt:
285 286 return controller, -signal.SIGINT
286 287 finally:
287 288 controller.cleanup()
288 289
289 290 def report():
290 291 """Return a string with a summary report of test-related variables."""
291 292 inf = get_sys_info()
292 293 out = []
293 294 def _add(name, value):
294 295 out.append((name, value))
295 296
296 297 _add('IPython version', inf['ipython_version'])
297 298 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
298 299 _add('IPython package', compress_user(inf['ipython_path']))
299 300 _add('Python version', inf['sys_version'].replace('\n',''))
300 301 _add('sys.executable', compress_user(inf['sys_executable']))
301 302 _add('Platform', inf['platform'])
302 303
303 304 width = max(len(n) for (n,v) in out)
304 305 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
305 306
306 307 avail = []
307 308 not_avail = []
308 309
309 310 for k, is_avail in have.items():
310 311 if is_avail:
311 312 avail.append(k)
312 313 else:
313 314 not_avail.append(k)
314 315
315 316 if avail:
316 317 out.append('\nTools and libraries available at test time:\n')
317 318 avail.sort()
318 319 out.append(' ' + ' '.join(avail)+'\n')
319 320
320 321 if not_avail:
321 322 out.append('\nTools and libraries NOT available at test time:\n')
322 323 not_avail.sort()
323 324 out.append(' ' + ' '.join(not_avail)+'\n')
324 325
325 326 return ''.join(out)
326 327
327 328 def run_iptestall(options):
328 329 """Run the entire IPython test suite by calling nose and trial.
329 330
330 331 This function constructs :class:`IPTester` instances for all IPython
331 332 modules and package and then runs each of them. This causes the modules
332 333 and packages of IPython to be tested each in their own subprocess using
333 334 nose.
334 335
335 336 Parameters
336 337 ----------
337 338
338 339 All parameters are passed as attributes of the options object.
339 340
340 341 testgroups : list of str
341 342 Run only these sections of the test suite. If empty, run all the available
342 343 sections.
343 344
344 345 fast : int or None
345 346 Run the test suite in parallel, using n simultaneous processes. If None
346 347 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
347 348
348 349 inc_slow : bool
349 350 Include slow tests, like IPython.parallel. By default, these tests aren't
350 351 run.
351 352
352 353 xunit : bool
353 354 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
354 355
355 356 coverage : bool or str
356 357 Measure code coverage from tests. True will store the raw coverage data,
357 358 or pass 'html' or 'xml' to get reports.
358 359
359 360 extra_args : list
360 361 Extra arguments to pass to the test subprocesses, e.g. '-v'
361 362 """
362 363 if options.fast != 1:
363 364 # If running in parallel, capture output so it doesn't get interleaved
364 365 TestController.buffer_output = True
365 366
366 367 to_run, not_run = prepare_controllers(options)
367 368
368 369 def justify(ltext, rtext, width=70, fill='-'):
369 370 ltext += ' '
370 371 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
371 372 return ltext + rtext
372 373
373 374 # Run all test runners, tracking execution time
374 375 failed = []
375 376 t_start = time.time()
376 377
377 378 print()
378 379 if options.fast == 1:
379 380 # This actually means sequential, i.e. with 1 job
380 381 for controller in to_run:
381 382 print('IPython test group:', controller.section)
382 383 sys.stdout.flush() # Show in correct order when output is piped
383 384 controller, res = do_run(controller)
384 385 if res:
385 386 failed.append(controller)
386 387 if res == -signal.SIGINT:
387 388 print("Interrupted")
388 389 break
389 390 print()
390 391
391 392 else:
392 393 # Run tests concurrently
393 394 try:
394 395 pool = multiprocessing.pool.ThreadPool(options.fast)
395 396 for (controller, res) in pool.imap_unordered(do_run, to_run):
396 397 res_string = 'OK' if res == 0 else 'FAILED'
397 398 print(justify('IPython test group: ' + controller.section, res_string))
398 399 if res:
399 400 print(bytes_to_str(controller.stdout))
400 401 failed.append(controller)
401 402 if res == -signal.SIGINT:
402 403 print("Interrupted")
403 404 break
404 405 except KeyboardInterrupt:
405 406 return
406 407
407 408 for controller in not_run:
408 409 print(justify('IPython test group: ' + controller.section, 'NOT RUN'))
409 410
410 411 t_end = time.time()
411 412 t_tests = t_end - t_start
412 413 nrunners = len(to_run)
413 414 nfail = len(failed)
414 415 # summarize results
415 416 print('_'*70)
416 417 print('Test suite completed for system with the following information:')
417 418 print(report())
418 419 took = "Took %.3fs." % t_tests
419 420 print('Status: ', end='')
420 421 if not failed:
421 422 print('OK (%d test groups).' % nrunners, took)
422 423 else:
423 424 # If anything went wrong, point out what command to rerun manually to
424 425 # see the actual errors and individual summary
425 426 failed_sections = [c.section for c in failed]
426 427 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
427 428 nrunners, ', '.join(failed_sections)), took)
428 429 print()
429 430 print('You may wish to rerun these, with:')
430 431 print(' iptest', *failed_sections)
431 432 print()
432 433
433 434 if options.coverage:
434 435 from coverage import coverage
435 436 cov = coverage(data_file='.coverage')
436 437 cov.combine()
437 438 cov.save()
438 439
439 440 # Coverage HTML report
440 441 if options.coverage == 'html':
441 442 html_dir = 'ipy_htmlcov'
442 443 shutil.rmtree(html_dir, ignore_errors=True)
443 444 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
444 445 sys.stdout.flush()
445 446
446 447 # Custom HTML reporter to clean up module names.
447 448 from coverage.html import HtmlReporter
448 449 class CustomHtmlReporter(HtmlReporter):
449 450 def find_code_units(self, morfs):
450 451 super(CustomHtmlReporter, self).find_code_units(morfs)
451 452 for cu in self.code_units:
452 453 nameparts = cu.name.split(os.sep)
453 454 if 'IPython' not in nameparts:
454 455 continue
455 456 ix = nameparts.index('IPython')
456 457 cu.name = '.'.join(nameparts[ix:])
457 458
458 459 # Reimplement the html_report method with our custom reporter
459 460 cov._harvest_data()
460 461 cov.config.from_args(omit='*%stests' % os.sep, html_dir=html_dir,
461 462 html_title='IPython test coverage',
462 463 )
463 464 reporter = CustomHtmlReporter(cov, cov.config)
464 465 reporter.report(None)
465 466 print('done.')
466 467
467 468 # Coverage XML report
468 469 elif options.coverage == 'xml':
469 470 cov.xml_report(outfile='ipy_coverage.xml')
470 471
471 472 if failed:
472 473 # Ensure that our exit code indicates failure
473 474 sys.exit(1)
474 475
475 476 argparser = argparse.ArgumentParser(description='Run IPython test suite')
476 477 argparser.add_argument('testgroups', nargs='*',
477 478 help='Run specified groups of tests. If omitted, run '
478 479 'all tests.')
479 480 argparser.add_argument('--all', action='store_true',
480 481 help='Include slow tests not run by default.')
481 482 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
482 483 help='Run test sections in parallel.')
483 484 argparser.add_argument('--xunit', action='store_true',
484 485 help='Produce Xunit XML results')
485 486 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
486 487 help="Measure test coverage. Specify 'html' or "
487 488 "'xml' to get reports.")
488 489 argparser.add_argument('--subproc-streams', default='capture',
489 490 help="What to do with stdout/stderr from subprocesses. "
490 491 "'capture' (default), 'show' and 'discard' are the options.")
491 492
492 493 def default_options():
493 494 """Get an argparse Namespace object with the default arguments, to pass to
494 495 :func:`run_iptestall`.
495 496 """
496 497 options = argparser.parse_args([])
497 498 options.extra_args = []
498 499 return options
499 500
500 501 def main():
501 502 # Arguments after -- should be passed through to nose. Argparse treats
502 503 # everything after -- as regular positional arguments, so we separate them
503 504 # first.
504 505 try:
505 506 ix = sys.argv.index('--')
506 507 except ValueError:
507 508 to_parse = sys.argv[1:]
508 509 extra_args = []
509 510 else:
510 511 to_parse = sys.argv[1:ix]
511 512 extra_args = sys.argv[ix+1:]
512 513
513 514 options = argparser.parse_args(to_parse)
514 515 options.extra_args = extra_args
515 516
516 517 run_iptestall(options)
517 518
518 519
519 520 if __name__ == '__main__':
520 521 main()
General Comments 0
You need to be logged in to leave comments. Login now