##// END OF EJS Templates
Better coverage reporting
Thomas Kluyver -
Show More
@@ -43,10 +43,6 b' from nose.plugins import Plugin'
43 43
44 44 # Our own imports
45 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 46 from IPython.testing.plugin.ipdoctest import IPythonDoctest
51 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 301 def check_exclusions_exist():
302 from IPython.utils.path import get_ipython_package_dir
303 from IPython.utils.warn import warn
306 304 parent = os.path.dirname(get_ipython_package_dir())
307 305 for sec in test_sections:
308 306 for pattern in sec.exclusions:
@@ -416,11 +414,12 b' def run_iptest():'
416 414 # objects should, individual shells shouldn't). But for now, this
417 415 # workaround allows the test suite for the inprocess module to complete.
418 416 if section.name != 'kernel.inprocess':
417 from IPython.testing import globalipapp
419 418 globalipapp.start_ipython()
420 419
421 420 # Now nose can run
422 421 TestProgram(argv=argv, addplugins=plugins)
423 422
424
425 423 if __name__ == '__main__':
426 424 run_iptest()
425
@@ -21,6 +21,7 b' from __future__ import print_function'
21 21 import argparse
22 22 import multiprocessing.pool
23 23 import os
24 import shutil
24 25 import signal
25 26 import sys
26 27 import subprocess
@@ -39,6 +40,8 b' class IPTestController(object):'
39 40 section = None
40 41 #: list, command line arguments to be executed
41 42 cmd = None
43 #: str, Python command to execute in subprocess
44 pycmd = None
42 45 #: dict, extra environment variables to set for the subprocess
43 46 env = None
44 47 #: list, TemporaryDirectory instances to clear up when the process finishes
@@ -50,28 +53,38 b' class IPTestController(object):'
50 53 def __init__(self, section):
51 54 """Create new test runner."""
52 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 59 self.env = {}
55 60 self.dirs = []
56 61 ipydir = TemporaryDirectory()
57 62 self.dirs.append(ipydir)
58 63 self.env['IPYTHONDIR'] = ipydir.name
59 workingdir = TemporaryDirectory()
64 self.workingdir = workingdir = TemporaryDirectory()
60 65 self.dirs.append(workingdir)
61 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 70 def add_xunit(self):
64 71 xunit_file = os.path.abspath(self.section + '.xunit.xml')
65 72 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
66 73
67 def add_coverage(self, xml=True):
68 self.cmd.append('--with-coverage')
69 for include in test_sections[self.section].includes:
70 self.cmd.extend(['--cover-package', include])
71 if xml:
72 coverage_xml = os.path.abspath(self.section + ".coverage.xml")
73 self.cmd.extend(['--cover-xml', '--cover-xml-file', coverage_xml])
74 def add_coverage(self):
75 coverage_rc = ("[run]\n"
76 "data_file = {data_file}\n"
77 "source =\n"
78 " {source}\n"
79 ).format(data_file=os.path.abspath('.coverage.'+self.section),
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 89 def launch(self):
77 90 # print('*** ENV:', self.env) # dbg
@@ -80,6 +93,7 b' class IPTestController(object):'
80 93 env.update(self.env)
81 94 output = subprocess.PIPE if self.buffer_output else None
82 95 stdout = subprocess.STDOUT if self.buffer_output else None
96 self.cmd[2] = self.pycmd
83 97 self.process = subprocess.Popen(self.cmd, stdout=output,
84 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 145 if xunit:
132 146 controller.add_xunit()
133 147 if coverage:
134 controller.add_coverage(xml=True)
148 controller.add_coverage()
135 149 res.append(controller)
136 150 return res
137 151
@@ -178,7 +192,7 b' def report():'
178 192
179 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 196 """Run the entire IPython test suite by calling nose and trial.
183 197
184 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 214 if jobs != 1:
201 215 IPTestController.buffer_output = True
202 216
203 controllers = test_controllers_to_run(inc_slow=inc_slow, xunit=xunit,
204 coverage=coverage)
217 controllers = test_controllers_to_run(inc_slow=inc_slow, xunit=xunit_out,
218 coverage=coverage_out)
205 219
206 220 # Run all test runners, tracking execution time
207 221 failed = []
@@ -257,9 +271,48 b' def run_iptestall(inc_slow=False, jobs=1, xunit=False, coverage=False):'
257 271 print('-'*40)
258 272 print('Runner failed:', controller.section)
259 273 print('You may wish to rerun this one individually, with:')
260 failed_call_args = [py3compat.cast_unicode(x) for x in controller.cmd]
261 print(u' '.join(failed_call_args))
274 print(' iptest', *controller.cmd[3:])
262 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 316 # Ensure that our exit code indicates failure
264 317 sys.exit(1)
265 318
@@ -278,14 +331,20 b' def main():'
278 331 help='Run test sections in parallel.')
279 332 parser.add_argument('--xunit', action='store_true',
280 333 help='Produce Xunit XML results')
281 parser.add_argument('--coverage', action='store_true',
282 help='Measure test coverage.')
334 parser.add_argument('--coverage', nargs='?', const=True, default=False,
335 help="Measure test coverage. Specify 'html' or "
336 "'xml' to get reports.")
283 337
284 338 options = parser.parse_args()
285 339
340 try:
341 jobs = int(options.fast)
342 except TypeError:
343 jobs = options.fast
344
286 345 # This starts subprocesses
287 run_iptestall(inc_slow=options.all, jobs=options.fast,
288 xunit=options.xunit, coverage=options.coverage)
346 run_iptestall(inc_slow=options.all, jobs=jobs,
347 xunit_out=options.xunit, coverage_out=options.coverage)
289 348
290 349
291 350 if __name__ == '__main__':
General Comments 0
You need to be logged in to leave comments. Login now