##// END OF EJS Templates
More concise test summary info
Thomas Kluyver -
Show More
@@ -1,423 +1,422 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 from IPython.utils.sysinfo import sys_info
32 from IPython.utils.sysinfo import brief_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, extra_args=()):
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 controller.cmd.extend(extra_args)
188 188
189 189 def do_run(controller):
190 190 try:
191 191 try:
192 192 controller.launch()
193 193 except Exception:
194 194 import traceback
195 195 traceback.print_exc()
196 196 return controller, 1 # signal failure
197 197
198 198 exitcode = controller.wait()
199 199 return controller, exitcode
200 200
201 201 except KeyboardInterrupt:
202 202 return controller, -signal.SIGINT
203 203 finally:
204 204 controller.cleanup()
205 205
206 206 def report():
207 207 """Return a string with a summary report of test-related variables."""
208 208
209 out = [ sys_info(), '\n']
209 out = [ brief_sys_info(), '\n']
210 210
211 211 avail = []
212 212 not_avail = []
213 213
214 214 for k, is_avail in have.items():
215 215 if is_avail:
216 216 avail.append(k)
217 217 else:
218 218 not_avail.append(k)
219 219
220 220 if avail:
221 221 out.append('\nTools and libraries available at test time:\n')
222 222 avail.sort()
223 223 out.append(' ' + ' '.join(avail)+'\n')
224 224
225 225 if not_avail:
226 226 out.append('\nTools and libraries NOT available at test time:\n')
227 227 not_avail.sort()
228 228 out.append(' ' + ' '.join(not_avail)+'\n')
229 229
230 230 return ''.join(out)
231 231
232 232 def run_iptestall(options):
233 233 """Run the entire IPython test suite by calling nose and trial.
234 234
235 235 This function constructs :class:`IPTester` instances for all IPython
236 236 modules and package and then runs each of them. This causes the modules
237 237 and packages of IPython to be tested each in their own subprocess using
238 238 nose.
239 239
240 240 Parameters
241 241 ----------
242 242
243 243 All parameters are passed as attributes of the options object.
244 244
245 245 testgroups : list of str
246 246 Run only these sections of the test suite. If empty, run all the available
247 247 sections.
248 248
249 249 fast : int or None
250 250 Run the test suite in parallel, using n simultaneous processes. If None
251 251 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
252 252
253 253 inc_slow : bool
254 254 Include slow tests, like IPython.parallel. By default, these tests aren't
255 255 run.
256 256
257 257 xunit : bool
258 258 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
259 259
260 260 coverage : bool or str
261 261 Measure code coverage from tests. True will store the raw coverage data,
262 262 or pass 'html' or 'xml' to get reports.
263 263
264 264 extra_args : list
265 265 Extra arguments to pass to the test subprocesses, e.g. '-v'
266 266 """
267 267 if options.fast != 1:
268 268 # If running in parallel, capture output so it doesn't get interleaved
269 269 TestController.buffer_output = True
270 270
271 271 if options.testgroups:
272 272 to_run = [PyTestController(name) for name in options.testgroups]
273 273 not_run = []
274 274 else:
275 275 to_run, not_run = prepare_py_test_controllers(inc_slow=options.all)
276 276
277 277 configure_controllers(to_run, xunit=options.xunit, coverage=options.coverage,
278 278 extra_args=options.extra_args)
279 279
280 280 def justify(ltext, rtext, width=70, fill='-'):
281 281 ltext += ' '
282 282 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
283 283 return ltext + rtext
284 284
285 285 # Run all test runners, tracking execution time
286 286 failed = []
287 287 t_start = time.time()
288 288
289 289 print()
290 290 if options.fast == 1:
291 291 # This actually means sequential, i.e. with 1 job
292 292 for controller in to_run:
293 293 print('IPython test group:', controller.section)
294 294 sys.stdout.flush() # Show in correct order when output is piped
295 295 controller, res = do_run(controller)
296 296 if res:
297 297 failed.append(controller)
298 298 if res == -signal.SIGINT:
299 299 print("Interrupted")
300 300 break
301 301 print()
302 302
303 303 else:
304 304 # Run tests concurrently
305 305 try:
306 306 pool = multiprocessing.pool.ThreadPool(options.fast)
307 307 for (controller, res) in pool.imap_unordered(do_run, to_run):
308 308 res_string = 'OK' if res == 0 else 'FAILED'
309 309 print(justify('IPython test group: ' + controller.section, res_string))
310 310 if res:
311 311 print(bytes_to_str(controller.stdout))
312 312 failed.append(controller)
313 313 if res == -signal.SIGINT:
314 314 print("Interrupted")
315 315 break
316 316 except KeyboardInterrupt:
317 317 return
318 318
319 319 for controller in not_run:
320 320 print(justify('IPython test group: ' + controller.section, 'NOT RUN'))
321 321
322 322 t_end = time.time()
323 323 t_tests = t_end - t_start
324 324 nrunners = len(to_run)
325 325 nfail = len(failed)
326 326 # summarize results
327 327 print('_'*70)
328 328 print('Test suite completed for system with the following information:')
329 329 print(report())
330 print('Ran %s test groups in %.3fs' % (nrunners, t_tests))
331 print()
330 took = "Took %.3fs." % t_tests
332 331 print('Status: ', end='')
333 332 if not failed:
334 print('OK')
333 print('OK.', took)
335 334 else:
336 335 # If anything went wrong, point out what command to rerun manually to
337 336 # see the actual errors and individual summary
338 337 failed_sections = [c.section for c in failed]
339 338 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
340 nrunners, ', '.join(failed_sections)))
339 nrunners, ', '.join(failed_sections)), took)
341 340 print()
342 341 print('You may wish to rerun these, with:')
343 342 print(' iptest', *failed_sections)
344 343 print()
345 344
346 345 if options.coverage:
347 346 from coverage import coverage
348 347 cov = coverage(data_file='.coverage')
349 348 cov.combine()
350 349 cov.save()
351 350
352 351 # Coverage HTML report
353 352 if options.coverage == 'html':
354 353 html_dir = 'ipy_htmlcov'
355 354 shutil.rmtree(html_dir, ignore_errors=True)
356 355 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
357 356 sys.stdout.flush()
358 357
359 358 # Custom HTML reporter to clean up module names.
360 359 from coverage.html import HtmlReporter
361 360 class CustomHtmlReporter(HtmlReporter):
362 361 def find_code_units(self, morfs):
363 362 super(CustomHtmlReporter, self).find_code_units(morfs)
364 363 for cu in self.code_units:
365 364 nameparts = cu.name.split(os.sep)
366 365 if 'IPython' not in nameparts:
367 366 continue
368 367 ix = nameparts.index('IPython')
369 368 cu.name = '.'.join(nameparts[ix:])
370 369
371 370 # Reimplement the html_report method with our custom reporter
372 371 cov._harvest_data()
373 372 cov.config.from_args(omit='*%stests' % os.sep, html_dir=html_dir,
374 373 html_title='IPython test coverage',
375 374 )
376 375 reporter = CustomHtmlReporter(cov, cov.config)
377 376 reporter.report(None)
378 377 print('done.')
379 378
380 379 # Coverage XML report
381 380 elif options.coverage == 'xml':
382 381 cov.xml_report(outfile='ipy_coverage.xml')
383 382
384 383 if failed:
385 384 # Ensure that our exit code indicates failure
386 385 sys.exit(1)
387 386
388 387
389 388 def main():
390 389 # Arguments after -- should be passed through to nose. Argparse treats
391 390 # everything after -- as regular positional arguments, so we separate them
392 391 # first.
393 392 try:
394 393 ix = sys.argv.index('--')
395 394 except ValueError:
396 395 to_parse = sys.argv[1:]
397 396 extra_args = []
398 397 else:
399 398 to_parse = sys.argv[1:ix]
400 399 extra_args = sys.argv[ix+1:]
401 400
402 401 parser = argparse.ArgumentParser(description='Run IPython test suite')
403 402 parser.add_argument('testgroups', nargs='*',
404 403 help='Run specified groups of tests. If omitted, run '
405 404 'all tests.')
406 405 parser.add_argument('--all', action='store_true',
407 406 help='Include slow tests not run by default.')
408 407 parser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
409 408 help='Run test sections in parallel.')
410 409 parser.add_argument('--xunit', action='store_true',
411 410 help='Produce Xunit XML results')
412 411 parser.add_argument('--coverage', nargs='?', const=True, default=False,
413 412 help="Measure test coverage. Specify 'html' or "
414 413 "'xml' to get reports.")
415 414
416 415 options = parser.parse_args(to_parse)
417 416 options.extra_args = extra_args
418 417
419 418 run_iptestall(options)
420 419
421 420
422 421 if __name__ == '__main__':
423 422 main()
@@ -1,169 +1,197 b''
1 1 # encoding: utf-8
2 2 """
3 3 Utilities for getting information about IPython and the system it's running in.
4 4 """
5 5
6 6 #-----------------------------------------------------------------------------
7 7 # Copyright (C) 2008-2011 The IPython Development Team
8 8 #
9 9 # Distributed under the terms of the BSD License. The full license is in
10 10 # the file COPYING, distributed as part of this software.
11 11 #-----------------------------------------------------------------------------
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16
17 17 import os
18 18 import platform
19 19 import pprint
20 20 import sys
21 21 import subprocess
22 22
23 23 from IPython.core import release
24 24 from IPython.utils import py3compat, _sysinfo, encoding
25 25
26 26 #-----------------------------------------------------------------------------
27 27 # Code
28 28 #-----------------------------------------------------------------------------
29 29
30 30 def pkg_commit_hash(pkg_path):
31 31 """Get short form of commit hash given directory `pkg_path`
32 32
33 33 We get the commit hash from (in order of preference):
34 34
35 35 * IPython.utils._sysinfo.commit
36 36 * git output, if we are in a git repository
37 37
38 38 If these fail, we return a not-found placeholder tuple
39 39
40 40 Parameters
41 41 ----------
42 42 pkg_path : str
43 43 directory containing package
44 44 only used for getting commit from active repo
45 45
46 46 Returns
47 47 -------
48 48 hash_from : str
49 49 Where we got the hash from - description
50 50 hash_str : str
51 51 short form of hash
52 52 """
53 53 # Try and get commit from written commit text file
54 54 if _sysinfo.commit:
55 55 return "installation", _sysinfo.commit
56 56
57 57 # maybe we are in a repository
58 58 proc = subprocess.Popen('git rev-parse --short HEAD',
59 59 stdout=subprocess.PIPE,
60 60 stderr=subprocess.PIPE,
61 61 cwd=pkg_path, shell=True)
62 62 repo_commit, _ = proc.communicate()
63 63 if repo_commit:
64 64 return 'repository', repo_commit.strip()
65 65 return '(none found)', '<not found>'
66 66
67 67
68 68 def pkg_info(pkg_path):
69 69 """Return dict describing the context of this package
70 70
71 71 Parameters
72 72 ----------
73 73 pkg_path : str
74 74 path containing __init__.py for package
75 75
76 76 Returns
77 77 -------
78 78 context : dict
79 79 with named parameters of interest
80 80 """
81 81 src, hsh = pkg_commit_hash(pkg_path)
82 82 return dict(
83 83 ipython_version=release.version,
84 84 ipython_path=pkg_path,
85 85 codename=release.codename,
86 86 commit_source=src,
87 87 commit_hash=hsh,
88 88 sys_version=sys.version,
89 89 sys_executable=sys.executable,
90 90 sys_platform=sys.platform,
91 91 platform=platform.platform(),
92 92 os_name=os.name,
93 93 default_encoding=encoding.DEFAULT_ENCODING,
94 94 )
95 95
96 96
97 97 @py3compat.doctest_refactor_print
98 98 def sys_info():
99 99 """Return useful information about IPython and the system, as a string.
100 100
101 101 Examples
102 102 --------
103 103 ::
104 104
105 105 In [2]: print sys_info()
106 106 {'commit_hash': '144fdae', # random
107 107 'commit_source': 'repository',
108 108 'ipython_path': '/home/fperez/usr/lib/python2.6/site-packages/IPython',
109 109 'ipython_version': '0.11.dev',
110 110 'os_name': 'posix',
111 111 'platform': 'Linux-2.6.35-22-generic-i686-with-Ubuntu-10.10-maverick',
112 112 'sys_executable': '/usr/bin/python',
113 113 'sys_platform': 'linux2',
114 114 'sys_version': '2.6.6 (r266:84292, Sep 15 2010, 15:52:39) \\n[GCC 4.4.5]'}
115 115 """
116 116 p = os.path
117 117 path = p.dirname(p.abspath(p.join(__file__, '..')))
118 118 return pprint.pformat(pkg_info(path))
119 119
120 def _compress_user(path):
121 """Reverse of :func:`os.path.expanduser`
122 """
123 home = os.path.expanduser('~')
124 if path.startswith(home):
125 path = "~" + path[len(home):]
126 return path
127
128 def brief_sys_info():
129 """Return summary information about IPython and the system, as a string.
130 """
131 p = os.path
132 path = p.dirname(p.abspath(p.join(__file__, '..')))
133 inf = pkg_info(path)
134 out = []
135 def _add(name, value):
136 out.append((name, value))
137
138 _add('IPython version', inf['ipython_version'])
139 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
140 _add('IPython package', _compress_user(inf['ipython_path']))
141 _add('Python version', inf['sys_version'].replace('\n',''))
142 _add('sys.executable', _compress_user(inf['sys_executable']))
143 _add('Platform', inf['platform'])
144
145 width = max(len(n) for (n,v) in out)
146 return '\n'.join("{:<{width}}: {}".format(n, v, width=width) for (n,v) in out)
147
120 148
121 149 def _num_cpus_unix():
122 150 """Return the number of active CPUs on a Unix system."""
123 151 return os.sysconf("SC_NPROCESSORS_ONLN")
124 152
125 153
126 154 def _num_cpus_darwin():
127 155 """Return the number of active CPUs on a Darwin system."""
128 156 p = subprocess.Popen(['sysctl','-n','hw.ncpu'],stdout=subprocess.PIPE)
129 157 return p.stdout.read()
130 158
131 159
132 160 def _num_cpus_windows():
133 161 """Return the number of active CPUs on a Windows system."""
134 162 return os.environ.get("NUMBER_OF_PROCESSORS")
135 163
136 164
137 165 def num_cpus():
138 166 """Return the effective number of CPUs in the system as an integer.
139 167
140 168 This cross-platform function makes an attempt at finding the total number of
141 169 available CPUs in the system, as returned by various underlying system and
142 170 python calls.
143 171
144 172 If it can't find a sensible answer, it returns 1 (though an error *may* make
145 173 it return a large positive number that's actually incorrect).
146 174 """
147 175
148 176 # Many thanks to the Parallel Python project (http://www.parallelpython.com)
149 177 # for the names of the keys we needed to look up for this function. This
150 178 # code was inspired by their equivalent function.
151 179
152 180 ncpufuncs = {'Linux':_num_cpus_unix,
153 181 'Darwin':_num_cpus_darwin,
154 182 'Windows':_num_cpus_windows,
155 183 # On Vista, python < 2.5.2 has a bug and returns 'Microsoft'
156 184 # See http://bugs.python.org/issue1082 for details.
157 185 'Microsoft':_num_cpus_windows,
158 186 }
159 187
160 188 ncpufunc = ncpufuncs.get(platform.system(),
161 189 # default to unix version (Solaris, AIX, etc)
162 190 _num_cpus_unix)
163 191
164 192 try:
165 193 ncpus = max(1,int(ncpufunc()))
166 194 except:
167 195 ncpus = 1
168 196 return ncpus
169 197
General Comments 0
You need to be logged in to leave comments. Login now