##// END OF EJS Templates
Improve test output
Thomas Kluyver -
Show More
@@ -1,278 +1,284
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 signal
25 25 import sys
26 26 import subprocess
27 27 import time
28 28
29 29 from .iptest import have, test_group_names, test_sections
30 30 from IPython.utils import py3compat
31 31 from IPython.utils.sysinfo import sys_info
32 32 from IPython.utils.tempdir import TemporaryDirectory
33 33
34 34
35 35 class IPTestController(object):
36 36 """Run iptest in a subprocess
37 37 """
38 38 #: str, IPython test suite to be executed.
39 39 section = None
40 40 #: list, command line arguments to be executed
41 41 cmd = None
42 42 #: dict, extra environment variables to set for the subprocess
43 43 env = None
44 44 #: list, TemporaryDirectory instances to clear up when the process finishes
45 45 dirs = None
46 46 #: subprocess.Popen instance
47 47 process = None
48 48 buffer_output = False
49 49
50 50 def __init__(self, section):
51 51 """Create new test runner."""
52 52 self.section = section
53 53 self.cmd = [sys.executable, '-m', 'IPython.testing.iptest', section]
54 54 self.env = {}
55 55 self.dirs = []
56 56 ipydir = TemporaryDirectory()
57 57 self.dirs.append(ipydir)
58 58 self.env['IPYTHONDIR'] = ipydir.name
59 59 workingdir = TemporaryDirectory()
60 60 self.dirs.append(workingdir)
61 61 self.env['IPTEST_WORKING_DIR'] = workingdir.name
62 62
63 63 def add_xunit(self):
64 64 xunit_file = os.path.abspath(self.section + '.xunit.xml')
65 65 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
66 66
67 67 def add_coverage(self, xml=True):
68 68 self.cmd.extend(['--with-coverage', '--cover-package', self.section])
69 69 if xml:
70 70 coverage_xml = os.path.abspath(self.section + ".coverage.xml")
71 71 self.cmd.extend(['--cover-xml', '--cover-xml-file', coverage_xml])
72 72
73 73
74 74 def launch(self):
75 75 # print('*** ENV:', self.env) # dbg
76 76 # print('*** CMD:', self.cmd) # dbg
77 77 env = os.environ.copy()
78 78 env.update(self.env)
79 79 output = subprocess.PIPE if self.buffer_output else None
80 stdout = subprocess.STDOUT if self.buffer_output else None
80 81 self.process = subprocess.Popen(self.cmd, stdout=output,
81 stderr=output, env=env)
82 stderr=stdout, env=env)
82 83
83 def run(self):
84 """Run the stored commands"""
85 try:
86 retcode = self._run_cmd()
87 except KeyboardInterrupt:
88 return -signal.SIGINT
89 except:
90 import traceback
91 traceback.print_exc()
92 return 1 # signal failure
93
94 if self.coverage_xml:
95 subprocess.call(["coverage", "xml", "-o", self.coverage_xml])
96 return retcode
84 def wait(self):
85 self.stdout, _ = self.process.communicate()
86 return self.process.returncode
97 87
98 88 def cleanup_process(self):
99 89 """Cleanup on exit by killing any leftover processes."""
100 90 subp = self.process
101 91 if subp is None or (subp.poll() is not None):
102 92 return # Process doesn't exist, or is already dead.
103 93
104 94 try:
105 95 print('Cleaning up stale PID: %d' % subp.pid)
106 96 subp.kill()
107 97 except: # (OSError, WindowsError) ?
108 98 # This is just a best effort, if we fail or the process was
109 99 # really gone, ignore it.
110 100 pass
111 101 else:
112 102 for i in range(10):
113 103 if subp.poll() is None:
114 104 time.sleep(0.1)
115 105 else:
116 106 break
117 107
118 108 if subp.poll() is None:
119 109 # The process did not die...
120 110 print('... failed. Manual cleanup may be required.')
121 111
122 112 def cleanup(self):
123 113 "Kill process if it's still alive, and clean up temporary directories"
124 114 self.cleanup_process()
125 115 for td in self.dirs:
126 116 td.cleanup()
127 117
128 118 __del__ = cleanup
129 119
130 120 def test_controllers_to_run(inc_slow=False):
131 121 """Returns an ordered list of IPTestController instances to be run."""
132 122 res = []
133 123 if not inc_slow:
134 124 test_sections['parallel'].enabled = False
125
135 126 for name in test_group_names:
136 127 if test_sections[name].will_run:
137 128 res.append(IPTestController(name))
138 129 return res
139 130
140 131 def do_run(controller):
141 132 try:
142 133 try:
143 134 controller.launch()
144 135 except Exception:
145 136 import traceback
146 137 traceback.print_exc()
147 138 return controller, 1 # signal failure
148 139
149 exitcode = controller.process.wait()
150 controller.cleanup()
140 exitcode = controller.wait()
151 141 return controller, exitcode
152 142
153 143 except KeyboardInterrupt:
154 controller.cleanup()
155 144 return controller, -signal.SIGINT
145 finally:
146 controller.cleanup()
156 147
157 148 def report():
158 149 """Return a string with a summary report of test-related variables."""
159 150
160 151 out = [ sys_info(), '\n']
161 152
162 153 avail = []
163 154 not_avail = []
164 155
165 156 for k, is_avail in have.items():
166 157 if is_avail:
167 158 avail.append(k)
168 159 else:
169 160 not_avail.append(k)
170 161
171 162 if avail:
172 163 out.append('\nTools and libraries available at test time:\n')
173 164 avail.sort()
174 165 out.append(' ' + ' '.join(avail)+'\n')
175 166
176 167 if not_avail:
177 168 out.append('\nTools and libraries NOT available at test time:\n')
178 169 not_avail.sort()
179 170 out.append(' ' + ' '.join(not_avail)+'\n')
180 171
181 172 return ''.join(out)
182 173
183 174 def run_iptestall(inc_slow=False, jobs=1, xunit=False, coverage=False):
184 175 """Run the entire IPython test suite by calling nose and trial.
185 176
186 177 This function constructs :class:`IPTester` instances for all IPython
187 178 modules and package and then runs each of them. This causes the modules
188 179 and packages of IPython to be tested each in their own subprocess using
189 180 nose.
190 181
191 182 Parameters
192 183 ----------
193 184
194 185 inc_slow : bool, optional
195 186 Include slow tests, like IPython.parallel. By default, these tests aren't
196 187 run.
197 188
198 189 fast : bool, option
199 190 Run the test suite in parallel, if True, using as many threads as there
200 191 are processors
201 192 """
202 pool = multiprocessing.pool.ThreadPool(jobs)
203 193 if jobs != 1:
204 194 IPTestController.buffer_output = True
205 195
206 196 controllers = test_controllers_to_run(inc_slow=inc_slow)
207 197
208 198 # Run all test runners, tracking execution time
209 199 failed = []
210 200 t_start = time.time()
211 201
212 202 print('*'*70)
213 for (controller, res) in pool.imap_unordered(do_run, controllers):
214 tgroup = 'IPython test group: ' + controller.section
215 res_string = 'OK' if res == 0 else 'FAILED'
216 res_string = res_string.rjust(70 - len(tgroup), '.')
217 print(tgroup + res_string)
218 if res:
219 failed.append(controller)
220 if res == -signal.SIGINT:
221 print("Interrupted")
222 break
203 if jobs == 1:
204 for controller in controllers:
205 print('IPython test group:', controller.section)
206 controller, res = do_run(controller)
207 if res:
208 failed.append(controller)
209 if res == -signal.SIGINT:
210 print("Interrupted")
211 break
212 print()
213
214 else:
215 try:
216 pool = multiprocessing.pool.ThreadPool(jobs)
217 for (controller, res) in pool.imap_unordered(do_run, controllers):
218 tgroup = 'IPython test group: ' + controller.section + ' '
219 res_string = ' OK' if res == 0 else ' FAILED'
220 res_string = res_string.rjust(70 - len(tgroup), '.')
221 print(tgroup + res_string)
222 if res:
223 print(controller.stdout)
224 failed.append(controller)
225 if res == -signal.SIGINT:
226 print("Interrupted")
227 break
228 except KeyboardInterrupt:
229 return
223 230
224 231 t_end = time.time()
225 232 t_tests = t_end - t_start
226 233 nrunners = len(controllers)
227 234 nfail = len(failed)
228 235 # summarize results
229 print()
230 236 print('*'*70)
231 237 print('Test suite completed for system with the following information:')
232 238 print(report())
233 239 print('Ran %s test groups in %.3fs' % (nrunners, t_tests))
234 240 print()
235 241 print('Status:')
236 242 if not failed:
237 243 print('OK')
238 244 else:
239 245 # If anything went wrong, point out what command to rerun manually to
240 246 # see the actual errors and individual summary
241 247 print('ERROR - %s out of %s test groups failed.' % (nfail, nrunners))
242 248 for controller in failed:
243 249 print('-'*40)
244 250 print('Runner failed:', controller.section)
245 251 print('You may wish to rerun this one individually, with:')
246 252 failed_call_args = [py3compat.cast_unicode(x) for x in controller.cmd]
247 253 print(u' '.join(failed_call_args))
248 254 print()
249 255 # Ensure that our exit code indicates failure
250 256 sys.exit(1)
251 257
252 258
253 259 def main():
254 260 if len(sys.argv) > 1 and (sys.argv[1] in test_sections):
255 261 from .iptest import run_iptest
256 262 # This is in-process
257 263 run_iptest()
258 264 return
259 265
260 266 parser = argparse.ArgumentParser(description='Run IPython test suite')
261 267 parser.add_argument('--all', action='store_true',
262 268 help='Include slow tests not run by default.')
263 269 parser.add_argument('-j', '--fast', nargs='?', const=None, default=1,
264 270 help='Run test sections in parallel.')
265 271 parser.add_argument('--xunit', action='store_true',
266 272 help='Produce Xunit XML results')
267 273 parser.add_argument('--coverage', action='store_true',
268 274 help='Measure test coverage.')
269 275
270 276 options = parser.parse_args()
271 277
272 278 # This starts subprocesses
273 279 run_iptestall(inc_slow=options.all, jobs=options.fast,
274 280 xunit=options.xunit, coverage=options.coverage)
275 281
276 282
277 283 if __name__ == '__main__':
278 284 main()
General Comments 0
You need to be logged in to leave comments. Login now