##// END OF EJS Templates
Better coverage reporting
Thomas Kluyver -
Show More
@@ -43,10 +43,6 b' from nose.plugins import Plugin'
43
43
44 # Our own imports
44 # Our own imports
45 from IPython.utils.importstring import import_item
45 from IPython.utils.importstring import import_item
46 from IPython.utils.path import get_ipython_package_dir
47 from IPython.utils.warn import warn
48
49 from IPython.testing import globalipapp
50 from IPython.testing.plugin.ipdoctest import IPythonDoctest
46 from IPython.testing.plugin.ipdoctest import IPythonDoctest
51 from IPython.external.decorators import KnownFailure, knownfailureif
47 from IPython.external.decorators import KnownFailure, knownfailureif
52
48
@@ -303,6 +299,8 b" sec.exclude('exporters.tests.files')"
303 #-----------------------------------------------------------------------------
299 #-----------------------------------------------------------------------------
304
300
305 def check_exclusions_exist():
301 def check_exclusions_exist():
302 from IPython.utils.path import get_ipython_package_dir
303 from IPython.utils.warn import warn
306 parent = os.path.dirname(get_ipython_package_dir())
304 parent = os.path.dirname(get_ipython_package_dir())
307 for sec in test_sections:
305 for sec in test_sections:
308 for pattern in sec.exclusions:
306 for pattern in sec.exclusions:
@@ -416,11 +414,12 b' def run_iptest():'
416 # objects should, individual shells shouldn't). But for now, this
414 # objects should, individual shells shouldn't). But for now, this
417 # workaround allows the test suite for the inprocess module to complete.
415 # workaround allows the test suite for the inprocess module to complete.
418 if section.name != 'kernel.inprocess':
416 if section.name != 'kernel.inprocess':
417 from IPython.testing import globalipapp
419 globalipapp.start_ipython()
418 globalipapp.start_ipython()
420
419
421 # Now nose can run
420 # Now nose can run
422 TestProgram(argv=argv, addplugins=plugins)
421 TestProgram(argv=argv, addplugins=plugins)
423
422
424
425 if __name__ == '__main__':
423 if __name__ == '__main__':
426 run_iptest()
424 run_iptest()
425
@@ -21,6 +21,7 b' from __future__ import print_function'
21 import argparse
21 import argparse
22 import multiprocessing.pool
22 import multiprocessing.pool
23 import os
23 import os
24 import shutil
24 import signal
25 import signal
25 import sys
26 import sys
26 import subprocess
27 import subprocess
@@ -39,6 +40,8 b' class IPTestController(object):'
39 section = None
40 section = None
40 #: list, command line arguments to be executed
41 #: list, command line arguments to be executed
41 cmd = None
42 cmd = None
43 #: str, Python command to execute in subprocess
44 pycmd = None
42 #: dict, extra environment variables to set for the subprocess
45 #: dict, extra environment variables to set for the subprocess
43 env = None
46 env = None
44 #: list, TemporaryDirectory instances to clear up when the process finishes
47 #: list, TemporaryDirectory instances to clear up when the process finishes
@@ -50,28 +53,38 b' class IPTestController(object):'
50 def __init__(self, section):
53 def __init__(self, section):
51 """Create new test runner."""
54 """Create new test runner."""
52 self.section = section
55 self.section = section
53 self.cmd = [sys.executable, '-m', 'IPython.testing.iptest', section]
56 # pycmd is put into cmd[2] in IPTestController.launch()
57 self.cmd = [sys.executable, '-c', None, section]
58 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
54 self.env = {}
59 self.env = {}
55 self.dirs = []
60 self.dirs = []
56 ipydir = TemporaryDirectory()
61 ipydir = TemporaryDirectory()
57 self.dirs.append(ipydir)
62 self.dirs.append(ipydir)
58 self.env['IPYTHONDIR'] = ipydir.name
63 self.env['IPYTHONDIR'] = ipydir.name
59 workingdir = TemporaryDirectory()
64 self.workingdir = workingdir = TemporaryDirectory()
60 self.dirs.append(workingdir)
65 self.dirs.append(workingdir)
61 self.env['IPTEST_WORKING_DIR'] = workingdir.name
66 self.env['IPTEST_WORKING_DIR'] = workingdir.name
67 # This means we won't get odd effects from our own matplotlib config
68 self.env['MPLCONFIGDIR'] = workingdir.name
62
69
63 def add_xunit(self):
70 def add_xunit(self):
64 xunit_file = os.path.abspath(self.section + '.xunit.xml')
71 xunit_file = os.path.abspath(self.section + '.xunit.xml')
65 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
72 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
66
73
67 def add_coverage(self, xml=True):
74 def add_coverage(self):
68 self.cmd.append('--with-coverage')
75 coverage_rc = ("[run]\n"
69 for include in test_sections[self.section].includes:
76 "data_file = {data_file}\n"
70 self.cmd.extend(['--cover-package', include])
77 "source =\n"
71 if xml:
78 " {source}\n"
72 coverage_xml = os.path.abspath(self.section + ".coverage.xml")
79 ).format(data_file=os.path.abspath('.coverage.'+self.section),
73 self.cmd.extend(['--cover-xml', '--cover-xml-file', coverage_xml])
80 source="\n ".join(test_sections[self.section].includes))
74
81
82 config_file = os.path.join(self.workingdir.name, '.coveragerc')
83 with open(config_file, 'w') as f:
84 f.write(coverage_rc)
85
86 self.env['COVERAGE_PROCESS_START'] = config_file
87 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
75
88
76 def launch(self):
89 def launch(self):
77 # print('*** ENV:', self.env) # dbg
90 # print('*** ENV:', self.env) # dbg
@@ -80,6 +93,7 b' class IPTestController(object):'
80 env.update(self.env)
93 env.update(self.env)
81 output = subprocess.PIPE if self.buffer_output else None
94 output = subprocess.PIPE if self.buffer_output else None
82 stdout = subprocess.STDOUT if self.buffer_output else None
95 stdout = subprocess.STDOUT if self.buffer_output else None
96 self.cmd[2] = self.pycmd
83 self.process = subprocess.Popen(self.cmd, stdout=output,
97 self.process = subprocess.Popen(self.cmd, stdout=output,
84 stderr=stdout, env=env)
98 stderr=stdout, env=env)
85
99
@@ -131,7 +145,7 b' def test_controllers_to_run(inc_slow=False, xunit=False, coverage=False):'
131 if xunit:
145 if xunit:
132 controller.add_xunit()
146 controller.add_xunit()
133 if coverage:
147 if coverage:
134 controller.add_coverage(xml=True)
148 controller.add_coverage()
135 res.append(controller)
149 res.append(controller)
136 return res
150 return res
137
151
@@ -178,7 +192,7 b' def report():'
178
192
179 return ''.join(out)
193 return ''.join(out)
180
194
181 def run_iptestall(inc_slow=False, jobs=1, xunit=False, coverage=False):
195 def run_iptestall(inc_slow=False, jobs=1, xunit_out=False, coverage_out=False):
182 """Run the entire IPython test suite by calling nose and trial.
196 """Run the entire IPython test suite by calling nose and trial.
183
197
184 This function constructs :class:`IPTester` instances for all IPython
198 This function constructs :class:`IPTester` instances for all IPython
@@ -200,8 +214,8 b' def run_iptestall(inc_slow=False, jobs=1, xunit=False, coverage=False):'
200 if jobs != 1:
214 if jobs != 1:
201 IPTestController.buffer_output = True
215 IPTestController.buffer_output = True
202
216
203 controllers = test_controllers_to_run(inc_slow=inc_slow, xunit=xunit,
217 controllers = test_controllers_to_run(inc_slow=inc_slow, xunit=xunit_out,
204 coverage=coverage)
218 coverage=coverage_out)
205
219
206 # Run all test runners, tracking execution time
220 # Run all test runners, tracking execution time
207 failed = []
221 failed = []
@@ -257,9 +271,48 b' def run_iptestall(inc_slow=False, jobs=1, xunit=False, coverage=False):'
257 print('-'*40)
271 print('-'*40)
258 print('Runner failed:', controller.section)
272 print('Runner failed:', controller.section)
259 print('You may wish to rerun this one individually, with:')
273 print('You may wish to rerun this one individually, with:')
260 failed_call_args = [py3compat.cast_unicode(x) for x in controller.cmd]
274 print(' iptest', *controller.cmd[3:])
261 print(u' '.join(failed_call_args))
262 print()
275 print()
276
277 if coverage_out:
278 from coverage import coverage
279 cov = coverage(data_file='.coverage')
280 cov.combine()
281 cov.save()
282
283 # Coverage HTML report
284 if coverage_out == 'html':
285 html_dir = 'ipy_htmlcov'
286 shutil.rmtree(html_dir, ignore_errors=True)
287 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
288 sys.stdout.flush()
289
290 # Custom HTML reporter to clean up module names.
291 from coverage.html import HtmlReporter
292 class CustomHtmlReporter(HtmlReporter):
293 def find_code_units(self, morfs):
294 super(CustomHtmlReporter, self).find_code_units(morfs)
295 for cu in self.code_units:
296 nameparts = cu.name.split(os.sep)
297 if 'IPython' not in nameparts:
298 continue
299 ix = nameparts.index('IPython')
300 cu.name = '.'.join(nameparts[ix:])
301
302 # Reimplement the html_report method with our custom reporter
303 cov._harvest_data()
304 cov.config.from_args(omit='*%stests' % os.sep, html_dir=html_dir,
305 html_title='IPython test coverage',
306 )
307 reporter = CustomHtmlReporter(cov, cov.config)
308 reporter.report(None)
309 print('done.')
310
311 # Coverage XML report
312 elif coverage_out == 'xml':
313 cov.xml_report(outfile='ipy_coverage.xml')
314
315 if failed:
263 # Ensure that our exit code indicates failure
316 # Ensure that our exit code indicates failure
264 sys.exit(1)
317 sys.exit(1)
265
318
@@ -278,14 +331,20 b' def main():'
278 help='Run test sections in parallel.')
331 help='Run test sections in parallel.')
279 parser.add_argument('--xunit', action='store_true',
332 parser.add_argument('--xunit', action='store_true',
280 help='Produce Xunit XML results')
333 help='Produce Xunit XML results')
281 parser.add_argument('--coverage', action='store_true',
334 parser.add_argument('--coverage', nargs='?', const=True, default=False,
282 help='Measure test coverage.')
335 help="Measure test coverage. Specify 'html' or "
336 "'xml' to get reports.")
283
337
284 options = parser.parse_args()
338 options = parser.parse_args()
285
339
340 try:
341 jobs = int(options.fast)
342 except TypeError:
343 jobs = options.fast
344
286 # This starts subprocesses
345 # This starts subprocesses
287 run_iptestall(inc_slow=options.all, jobs=options.fast,
346 run_iptestall(inc_slow=options.all, jobs=jobs,
288 xunit=options.xunit, coverage=options.coverage)
347 xunit_out=options.xunit, coverage_out=options.coverage)
289
348
290
349
291 if __name__ == '__main__':
350 if __name__ == '__main__':
General Comments 0
You need to be logged in to leave comments. Login now