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