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