##// END OF EJS Templates
install IPython kernel in temp IPython dir
Min RK -
Show More
@@ -1,742 +1,752 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 # Copyright (c) IPython Development Team.
9 # Copyright (c) IPython Development Team.
10 # Distributed under the terms of the Modified BSD License.
10 # Distributed under the terms of the Modified BSD License.
11
11
12 from __future__ import print_function
12 from __future__ import print_function
13
13
14 import argparse
14 import argparse
15 import json
15 import json
16 import multiprocessing.pool
16 import multiprocessing.pool
17 import os
17 import os
18 import stat
18 import stat
19 import re
19 import re
20 import requests
20 import requests
21 import shutil
21 import shutil
22 import signal
22 import signal
23 import sys
23 import sys
24 import subprocess
24 import subprocess
25 import time
25 import time
26
26
27 from .iptest import (
27 from .iptest import (
28 have, test_group_names as py_test_group_names, test_sections, StreamCapturer,
28 have, test_group_names as py_test_group_names, test_sections, StreamCapturer,
29 test_for,
29 test_for,
30 )
30 )
31 from IPython.utils.path import compress_user
31 from IPython.utils.path import compress_user
32 from IPython.utils.py3compat import bytes_to_str
32 from IPython.utils.py3compat import bytes_to_str
33 from IPython.utils.sysinfo import get_sys_info
33 from IPython.utils.sysinfo import get_sys_info
34 from IPython.utils.tempdir import TemporaryDirectory
34 from IPython.utils.tempdir import TemporaryDirectory
35 from IPython.utils.text import strip_ansi
35 from IPython.utils.text import strip_ansi
36
36
37 try:
37 try:
38 # Python >= 3.3
38 # Python >= 3.3
39 from subprocess import TimeoutExpired
39 from subprocess import TimeoutExpired
40 def popen_wait(p, timeout):
40 def popen_wait(p, timeout):
41 return p.wait(timeout)
41 return p.wait(timeout)
42 except ImportError:
42 except ImportError:
43 class TimeoutExpired(Exception):
43 class TimeoutExpired(Exception):
44 pass
44 pass
45 def popen_wait(p, timeout):
45 def popen_wait(p, timeout):
46 """backport of Popen.wait from Python 3"""
46 """backport of Popen.wait from Python 3"""
47 for i in range(int(10 * timeout)):
47 for i in range(int(10 * timeout)):
48 if p.poll() is not None:
48 if p.poll() is not None:
49 return
49 return
50 time.sleep(0.1)
50 time.sleep(0.1)
51 if p.poll() is None:
51 if p.poll() is None:
52 raise TimeoutExpired
52 raise TimeoutExpired
53
53
54 NOTEBOOK_SHUTDOWN_TIMEOUT = 10
54 NOTEBOOK_SHUTDOWN_TIMEOUT = 10
55
55
56 class TestController(object):
56 class TestController(object):
57 """Run tests in a subprocess
57 """Run tests in a subprocess
58 """
58 """
59 #: str, IPython test suite to be executed.
59 #: str, IPython test suite to be executed.
60 section = None
60 section = None
61 #: list, command line arguments to be executed
61 #: list, command line arguments to be executed
62 cmd = None
62 cmd = None
63 #: dict, extra environment variables to set for the subprocess
63 #: dict, extra environment variables to set for the subprocess
64 env = None
64 env = None
65 #: list, TemporaryDirectory instances to clear up when the process finishes
65 #: list, TemporaryDirectory instances to clear up when the process finishes
66 dirs = None
66 dirs = None
67 #: subprocess.Popen instance
67 #: subprocess.Popen instance
68 process = None
68 process = None
69 #: str, process stdout+stderr
69 #: str, process stdout+stderr
70 stdout = None
70 stdout = None
71
71
72 def __init__(self):
72 def __init__(self):
73 self.cmd = []
73 self.cmd = []
74 self.env = {}
74 self.env = {}
75 self.dirs = []
75 self.dirs = []
76
76
77 def setup(self):
77 def setup(self):
78 """Create temporary directories etc.
78 """Create temporary directories etc.
79
79
80 This is only called when we know the test group will be run. Things
80 This is only called when we know the test group will be run. Things
81 created here may be cleaned up by self.cleanup().
81 created here may be cleaned up by self.cleanup().
82 """
82 """
83 pass
83 pass
84
84
85 def launch(self, buffer_output=False, capture_output=False):
85 def launch(self, buffer_output=False, capture_output=False):
86 # print('*** ENV:', self.env) # dbg
86 # print('*** ENV:', self.env) # dbg
87 # print('*** CMD:', self.cmd) # dbg
87 # print('*** CMD:', self.cmd) # dbg
88 env = os.environ.copy()
88 env = os.environ.copy()
89 env.update(self.env)
89 env.update(self.env)
90 if buffer_output:
90 if buffer_output:
91 capture_output = True
91 capture_output = True
92 self.stdout_capturer = c = StreamCapturer(echo=not buffer_output)
92 self.stdout_capturer = c = StreamCapturer(echo=not buffer_output)
93 c.start()
93 c.start()
94 stdout = c.writefd if capture_output else None
94 stdout = c.writefd if capture_output else None
95 stderr = subprocess.STDOUT if capture_output else None
95 stderr = subprocess.STDOUT if capture_output else None
96 self.process = subprocess.Popen(self.cmd, stdout=stdout,
96 self.process = subprocess.Popen(self.cmd, stdout=stdout,
97 stderr=stderr, env=env)
97 stderr=stderr, env=env)
98
98
99 def wait(self):
99 def wait(self):
100 self.process.wait()
100 self.process.wait()
101 self.stdout_capturer.halt()
101 self.stdout_capturer.halt()
102 self.stdout = self.stdout_capturer.get_buffer()
102 self.stdout = self.stdout_capturer.get_buffer()
103 return self.process.returncode
103 return self.process.returncode
104
104
105 def print_extra_info(self):
105 def print_extra_info(self):
106 """Print extra information about this test run.
106 """Print extra information about this test run.
107
107
108 If we're running in parallel and showing the concise view, this is only
108 If we're running in parallel and showing the concise view, this is only
109 called if the test group fails. Otherwise, it's called before the test
109 called if the test group fails. Otherwise, it's called before the test
110 group is started.
110 group is started.
111
111
112 The base implementation does nothing, but it can be overridden by
112 The base implementation does nothing, but it can be overridden by
113 subclasses.
113 subclasses.
114 """
114 """
115 return
115 return
116
116
117 def cleanup_process(self):
117 def cleanup_process(self):
118 """Cleanup on exit by killing any leftover processes."""
118 """Cleanup on exit by killing any leftover processes."""
119 subp = self.process
119 subp = self.process
120 if subp is None or (subp.poll() is not None):
120 if subp is None or (subp.poll() is not None):
121 return # Process doesn't exist, or is already dead.
121 return # Process doesn't exist, or is already dead.
122
122
123 try:
123 try:
124 print('Cleaning up stale PID: %d' % subp.pid)
124 print('Cleaning up stale PID: %d' % subp.pid)
125 subp.kill()
125 subp.kill()
126 except: # (OSError, WindowsError) ?
126 except: # (OSError, WindowsError) ?
127 # This is just a best effort, if we fail or the process was
127 # This is just a best effort, if we fail or the process was
128 # really gone, ignore it.
128 # really gone, ignore it.
129 pass
129 pass
130 else:
130 else:
131 for i in range(10):
131 for i in range(10):
132 if subp.poll() is None:
132 if subp.poll() is None:
133 time.sleep(0.1)
133 time.sleep(0.1)
134 else:
134 else:
135 break
135 break
136
136
137 if subp.poll() is None:
137 if subp.poll() is None:
138 # The process did not die...
138 # The process did not die...
139 print('... failed. Manual cleanup may be required.')
139 print('... failed. Manual cleanup may be required.')
140
140
141 def cleanup(self):
141 def cleanup(self):
142 "Kill process if it's still alive, and clean up temporary directories"
142 "Kill process if it's still alive, and clean up temporary directories"
143 self.cleanup_process()
143 self.cleanup_process()
144 for td in self.dirs:
144 for td in self.dirs:
145 td.cleanup()
145 td.cleanup()
146
146
147 __del__ = cleanup
147 __del__ = cleanup
148
148
149
149
150 class PyTestController(TestController):
150 class PyTestController(TestController):
151 """Run Python tests using IPython.testing.iptest"""
151 """Run Python tests using IPython.testing.iptest"""
152 #: str, Python command to execute in subprocess
152 #: str, Python command to execute in subprocess
153 pycmd = None
153 pycmd = None
154
154
155 def __init__(self, section, options):
155 def __init__(self, section, options):
156 """Create new test runner."""
156 """Create new test runner."""
157 TestController.__init__(self)
157 TestController.__init__(self)
158 self.section = section
158 self.section = section
159 # pycmd is put into cmd[2] in PyTestController.launch()
159 # pycmd is put into cmd[2] in PyTestController.launch()
160 self.cmd = [sys.executable, '-c', None, section]
160 self.cmd = [sys.executable, '-c', None, section]
161 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
161 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
162 self.options = options
162 self.options = options
163
163
164 def setup(self):
164 def setup(self):
165 ipydir = TemporaryDirectory()
165 ipydir = TemporaryDirectory()
166 self.dirs.append(ipydir)
166 self.dirs.append(ipydir)
167 self.env['IPYTHONDIR'] = ipydir.name
167 self.env['IPYTHONDIR'] = ipydir.name
168 # FIXME: install IPython kernel in temporary IPython dir
169 # remove after big split
170 try:
171 from jupyter_client.kernelspec import KernelSpecManager
172 except ImportError:
173 pass
174 else:
175 ksm = KernelSpecManager(ipython_dir=ipydir.name)
176 ksm.install_native_kernel_spec(user=True)
177
168 self.workingdir = workingdir = TemporaryDirectory()
178 self.workingdir = workingdir = TemporaryDirectory()
169 self.dirs.append(workingdir)
179 self.dirs.append(workingdir)
170 self.env['IPTEST_WORKING_DIR'] = workingdir.name
180 self.env['IPTEST_WORKING_DIR'] = workingdir.name
171 # This means we won't get odd effects from our own matplotlib config
181 # This means we won't get odd effects from our own matplotlib config
172 self.env['MPLCONFIGDIR'] = workingdir.name
182 self.env['MPLCONFIGDIR'] = workingdir.name
173 # For security reasons (http://bugs.python.org/issue16202), use
183 # For security reasons (http://bugs.python.org/issue16202), use
174 # a temporary directory to which other users have no access.
184 # a temporary directory to which other users have no access.
175 self.env['TMPDIR'] = workingdir.name
185 self.env['TMPDIR'] = workingdir.name
176
186
177 # Add a non-accessible directory to PATH (see gh-7053)
187 # Add a non-accessible directory to PATH (see gh-7053)
178 noaccess = os.path.join(self.workingdir.name, "_no_access_")
188 noaccess = os.path.join(self.workingdir.name, "_no_access_")
179 self.noaccess = noaccess
189 self.noaccess = noaccess
180 os.mkdir(noaccess, 0)
190 os.mkdir(noaccess, 0)
181
191
182 PATH = os.environ.get('PATH', '')
192 PATH = os.environ.get('PATH', '')
183 if PATH:
193 if PATH:
184 PATH = noaccess + os.pathsep + PATH
194 PATH = noaccess + os.pathsep + PATH
185 else:
195 else:
186 PATH = noaccess
196 PATH = noaccess
187 self.env['PATH'] = PATH
197 self.env['PATH'] = PATH
188
198
189 # From options:
199 # From options:
190 if self.options.xunit:
200 if self.options.xunit:
191 self.add_xunit()
201 self.add_xunit()
192 if self.options.coverage:
202 if self.options.coverage:
193 self.add_coverage()
203 self.add_coverage()
194 self.env['IPTEST_SUBPROC_STREAMS'] = self.options.subproc_streams
204 self.env['IPTEST_SUBPROC_STREAMS'] = self.options.subproc_streams
195 self.cmd.extend(self.options.extra_args)
205 self.cmd.extend(self.options.extra_args)
196
206
197 def cleanup(self):
207 def cleanup(self):
198 """
208 """
199 Make the non-accessible directory created in setup() accessible
209 Make the non-accessible directory created in setup() accessible
200 again, otherwise deleting the workingdir will fail.
210 again, otherwise deleting the workingdir will fail.
201 """
211 """
202 os.chmod(self.noaccess, stat.S_IRWXU)
212 os.chmod(self.noaccess, stat.S_IRWXU)
203 TestController.cleanup(self)
213 TestController.cleanup(self)
204
214
205 @property
215 @property
206 def will_run(self):
216 def will_run(self):
207 try:
217 try:
208 return test_sections[self.section].will_run
218 return test_sections[self.section].will_run
209 except KeyError:
219 except KeyError:
210 return True
220 return True
211
221
212 def add_xunit(self):
222 def add_xunit(self):
213 xunit_file = os.path.abspath(self.section + '.xunit.xml')
223 xunit_file = os.path.abspath(self.section + '.xunit.xml')
214 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
224 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
215
225
216 def add_coverage(self):
226 def add_coverage(self):
217 try:
227 try:
218 sources = test_sections[self.section].includes
228 sources = test_sections[self.section].includes
219 except KeyError:
229 except KeyError:
220 sources = ['IPython']
230 sources = ['IPython']
221
231
222 coverage_rc = ("[run]\n"
232 coverage_rc = ("[run]\n"
223 "data_file = {data_file}\n"
233 "data_file = {data_file}\n"
224 "source =\n"
234 "source =\n"
225 " {source}\n"
235 " {source}\n"
226 ).format(data_file=os.path.abspath('.coverage.'+self.section),
236 ).format(data_file=os.path.abspath('.coverage.'+self.section),
227 source="\n ".join(sources))
237 source="\n ".join(sources))
228 config_file = os.path.join(self.workingdir.name, '.coveragerc')
238 config_file = os.path.join(self.workingdir.name, '.coveragerc')
229 with open(config_file, 'w') as f:
239 with open(config_file, 'w') as f:
230 f.write(coverage_rc)
240 f.write(coverage_rc)
231
241
232 self.env['COVERAGE_PROCESS_START'] = config_file
242 self.env['COVERAGE_PROCESS_START'] = config_file
233 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
243 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
234
244
235 def launch(self, buffer_output=False):
245 def launch(self, buffer_output=False):
236 self.cmd[2] = self.pycmd
246 self.cmd[2] = self.pycmd
237 super(PyTestController, self).launch(buffer_output=buffer_output)
247 super(PyTestController, self).launch(buffer_output=buffer_output)
238
248
239
249
240 js_prefix = 'js/'
250 js_prefix = 'js/'
241
251
242 def get_js_test_dir():
252 def get_js_test_dir():
243 import IPython.html.tests as t
253 import IPython.html.tests as t
244 return os.path.join(os.path.dirname(t.__file__), '')
254 return os.path.join(os.path.dirname(t.__file__), '')
245
255
246 def all_js_groups():
256 def all_js_groups():
247 import glob
257 import glob
248 test_dir = get_js_test_dir()
258 test_dir = get_js_test_dir()
249 all_subdirs = glob.glob(test_dir + '[!_]*/')
259 all_subdirs = glob.glob(test_dir + '[!_]*/')
250 return [js_prefix+os.path.relpath(x, test_dir) for x in all_subdirs]
260 return [js_prefix+os.path.relpath(x, test_dir) for x in all_subdirs]
251
261
252 class JSController(TestController):
262 class JSController(TestController):
253 """Run CasperJS tests """
263 """Run CasperJS tests """
254
264
255 requirements = ['zmq', 'tornado', 'jinja2', 'casperjs', 'sqlite3',
265 requirements = ['zmq', 'tornado', 'jinja2', 'casperjs', 'sqlite3',
256 'jsonschema']
266 'jsonschema']
257
267
258 def __init__(self, section, xunit=True, engine='phantomjs', url=None):
268 def __init__(self, section, xunit=True, engine='phantomjs', url=None):
259 """Create new test runner."""
269 """Create new test runner."""
260 TestController.__init__(self)
270 TestController.__init__(self)
261 self.engine = engine
271 self.engine = engine
262 self.section = section
272 self.section = section
263 self.xunit = xunit
273 self.xunit = xunit
264 self.url = url
274 self.url = url
265 self.slimer_failure = re.compile('^FAIL.*', flags=re.MULTILINE)
275 self.slimer_failure = re.compile('^FAIL.*', flags=re.MULTILINE)
266 js_test_dir = get_js_test_dir()
276 js_test_dir = get_js_test_dir()
267 includes = '--includes=' + os.path.join(js_test_dir,'util.js')
277 includes = '--includes=' + os.path.join(js_test_dir,'util.js')
268 test_cases = os.path.join(js_test_dir, self.section[len(js_prefix):])
278 test_cases = os.path.join(js_test_dir, self.section[len(js_prefix):])
269 self.cmd = ['casperjs', 'test', includes, test_cases, '--engine=%s' % self.engine]
279 self.cmd = ['casperjs', 'test', includes, test_cases, '--engine=%s' % self.engine]
270
280
271 def setup(self):
281 def setup(self):
272 self.ipydir = TemporaryDirectory()
282 self.ipydir = TemporaryDirectory()
273 self.nbdir = TemporaryDirectory()
283 self.nbdir = TemporaryDirectory()
274 self.dirs.append(self.ipydir)
284 self.dirs.append(self.ipydir)
275 self.dirs.append(self.nbdir)
285 self.dirs.append(self.nbdir)
276 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir1', u'sub βˆ‚ir 1a')))
286 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir1', u'sub βˆ‚ir 1a')))
277 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir2', u'sub βˆ‚ir 1b')))
287 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir2', u'sub βˆ‚ir 1b')))
278
288
279 if self.xunit:
289 if self.xunit:
280 self.add_xunit()
290 self.add_xunit()
281
291
282 # If a url was specified, use that for the testing.
292 # If a url was specified, use that for the testing.
283 if self.url:
293 if self.url:
284 try:
294 try:
285 alive = requests.get(self.url).status_code == 200
295 alive = requests.get(self.url).status_code == 200
286 except:
296 except:
287 alive = False
297 alive = False
288
298
289 if alive:
299 if alive:
290 self.cmd.append("--url=%s" % self.url)
300 self.cmd.append("--url=%s" % self.url)
291 else:
301 else:
292 raise Exception('Could not reach "%s".' % self.url)
302 raise Exception('Could not reach "%s".' % self.url)
293 else:
303 else:
294 # start the ipython notebook, so we get the port number
304 # start the ipython notebook, so we get the port number
295 self.server_port = 0
305 self.server_port = 0
296 self._init_server()
306 self._init_server()
297 if self.server_port:
307 if self.server_port:
298 self.cmd.append("--port=%i" % self.server_port)
308 self.cmd.append("--port=%i" % self.server_port)
299 else:
309 else:
300 # don't launch tests if the server didn't start
310 # don't launch tests if the server didn't start
301 self.cmd = [sys.executable, '-c', 'raise SystemExit(1)']
311 self.cmd = [sys.executable, '-c', 'raise SystemExit(1)']
302
312
303 def add_xunit(self):
313 def add_xunit(self):
304 xunit_file = os.path.abspath(self.section.replace('/','.') + '.xunit.xml')
314 xunit_file = os.path.abspath(self.section.replace('/','.') + '.xunit.xml')
305 self.cmd.append('--xunit=%s' % xunit_file)
315 self.cmd.append('--xunit=%s' % xunit_file)
306
316
307 def launch(self, buffer_output):
317 def launch(self, buffer_output):
308 # If the engine is SlimerJS, we need to buffer the output because
318 # If the engine is SlimerJS, we need to buffer the output because
309 # SlimerJS does not support exit codes, so CasperJS always returns 0.
319 # SlimerJS does not support exit codes, so CasperJS always returns 0.
310 if self.engine == 'slimerjs' and not buffer_output:
320 if self.engine == 'slimerjs' and not buffer_output:
311 return super(JSController, self).launch(capture_output=True)
321 return super(JSController, self).launch(capture_output=True)
312
322
313 else:
323 else:
314 return super(JSController, self).launch(buffer_output=buffer_output)
324 return super(JSController, self).launch(buffer_output=buffer_output)
315
325
316 def wait(self, *pargs, **kwargs):
326 def wait(self, *pargs, **kwargs):
317 """Wait for the JSController to finish"""
327 """Wait for the JSController to finish"""
318 ret = super(JSController, self).wait(*pargs, **kwargs)
328 ret = super(JSController, self).wait(*pargs, **kwargs)
319 # If this is a SlimerJS controller, check the captured stdout for
329 # If this is a SlimerJS controller, check the captured stdout for
320 # errors. Otherwise, just return the return code.
330 # errors. Otherwise, just return the return code.
321 if self.engine == 'slimerjs':
331 if self.engine == 'slimerjs':
322 stdout = bytes_to_str(self.stdout)
332 stdout = bytes_to_str(self.stdout)
323 if ret != 0:
333 if ret != 0:
324 # This could still happen e.g. if it's stopped by SIGINT
334 # This could still happen e.g. if it's stopped by SIGINT
325 return ret
335 return ret
326 return bool(self.slimer_failure.search(strip_ansi(stdout)))
336 return bool(self.slimer_failure.search(strip_ansi(stdout)))
327 else:
337 else:
328 return ret
338 return ret
329
339
330 def print_extra_info(self):
340 def print_extra_info(self):
331 print("Running tests with notebook directory %r" % self.nbdir.name)
341 print("Running tests with notebook directory %r" % self.nbdir.name)
332
342
333 @property
343 @property
334 def will_run(self):
344 def will_run(self):
335 should_run = all(have[a] for a in self.requirements + [self.engine])
345 should_run = all(have[a] for a in self.requirements + [self.engine])
336 return should_run
346 return should_run
337
347
338 def _init_server(self):
348 def _init_server(self):
339 "Start the notebook server in a separate process"
349 "Start the notebook server in a separate process"
340 self.server_command = command = [sys.executable,
350 self.server_command = command = [sys.executable,
341 '-m', 'IPython.html',
351 '-m', 'IPython.html',
342 '--no-browser',
352 '--no-browser',
343 '--ipython-dir', self.ipydir.name,
353 '--ipython-dir', self.ipydir.name,
344 '--notebook-dir', self.nbdir.name,
354 '--notebook-dir', self.nbdir.name,
345 ]
355 ]
346 # ipc doesn't work on Windows, and darwin has crazy-long temp paths,
356 # ipc doesn't work on Windows, and darwin has crazy-long temp paths,
347 # which run afoul of ipc's maximum path length.
357 # which run afoul of ipc's maximum path length.
348 if sys.platform.startswith('linux'):
358 if sys.platform.startswith('linux'):
349 command.append('--KernelManager.transport=ipc')
359 command.append('--KernelManager.transport=ipc')
350 self.stream_capturer = c = StreamCapturer()
360 self.stream_capturer = c = StreamCapturer()
351 c.start()
361 c.start()
352 env = os.environ.copy()
362 env = os.environ.copy()
353 if self.engine == 'phantomjs':
363 if self.engine == 'phantomjs':
354 env['IPYTHON_ALLOW_DRAFT_WEBSOCKETS_FOR_PHANTOMJS'] = '1'
364 env['IPYTHON_ALLOW_DRAFT_WEBSOCKETS_FOR_PHANTOMJS'] = '1'
355 self.server = subprocess.Popen(command,
365 self.server = subprocess.Popen(command,
356 stdout=c.writefd,
366 stdout=c.writefd,
357 stderr=subprocess.STDOUT,
367 stderr=subprocess.STDOUT,
358 cwd=self.nbdir.name,
368 cwd=self.nbdir.name,
359 env=env,
369 env=env,
360 )
370 )
361 self.server_info_file = os.path.join(self.ipydir.name,
371 self.server_info_file = os.path.join(self.ipydir.name,
362 'profile_default', 'security', 'nbserver-%i.json' % self.server.pid
372 'profile_default', 'security', 'nbserver-%i.json' % self.server.pid
363 )
373 )
364 self._wait_for_server()
374 self._wait_for_server()
365
375
366 def _wait_for_server(self):
376 def _wait_for_server(self):
367 """Wait 30 seconds for the notebook server to start"""
377 """Wait 30 seconds for the notebook server to start"""
368 for i in range(300):
378 for i in range(300):
369 if self.server.poll() is not None:
379 if self.server.poll() is not None:
370 return self._failed_to_start()
380 return self._failed_to_start()
371 if os.path.exists(self.server_info_file):
381 if os.path.exists(self.server_info_file):
372 try:
382 try:
373 self._load_server_info()
383 self._load_server_info()
374 except ValueError:
384 except ValueError:
375 # If the server is halfway through writing the file, we may
385 # If the server is halfway through writing the file, we may
376 # get invalid JSON; it should be ready next iteration.
386 # get invalid JSON; it should be ready next iteration.
377 pass
387 pass
378 else:
388 else:
379 return
389 return
380 time.sleep(0.1)
390 time.sleep(0.1)
381 print("Notebook server-info file never arrived: %s" % self.server_info_file,
391 print("Notebook server-info file never arrived: %s" % self.server_info_file,
382 file=sys.stderr
392 file=sys.stderr
383 )
393 )
384
394
385 def _failed_to_start(self):
395 def _failed_to_start(self):
386 """Notebook server exited prematurely"""
396 """Notebook server exited prematurely"""
387 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
397 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
388 print("Notebook failed to start: ", file=sys.stderr)
398 print("Notebook failed to start: ", file=sys.stderr)
389 print(self.server_command)
399 print(self.server_command)
390 print(captured, file=sys.stderr)
400 print(captured, file=sys.stderr)
391
401
392 def _load_server_info(self):
402 def _load_server_info(self):
393 """Notebook server started, load connection info from JSON"""
403 """Notebook server started, load connection info from JSON"""
394 with open(self.server_info_file) as f:
404 with open(self.server_info_file) as f:
395 info = json.load(f)
405 info = json.load(f)
396 self.server_port = info['port']
406 self.server_port = info['port']
397
407
398 def cleanup(self):
408 def cleanup(self):
399 if hasattr(self, 'server'):
409 if hasattr(self, 'server'):
400 try:
410 try:
401 self.server.terminate()
411 self.server.terminate()
402 except OSError:
412 except OSError:
403 # already dead
413 # already dead
404 pass
414 pass
405 # wait 10s for the server to shutdown
415 # wait 10s for the server to shutdown
406 try:
416 try:
407 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
417 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
408 except TimeoutExpired:
418 except TimeoutExpired:
409 # server didn't terminate, kill it
419 # server didn't terminate, kill it
410 try:
420 try:
411 print("Failed to terminate notebook server, killing it.",
421 print("Failed to terminate notebook server, killing it.",
412 file=sys.stderr
422 file=sys.stderr
413 )
423 )
414 self.server.kill()
424 self.server.kill()
415 except OSError:
425 except OSError:
416 # already dead
426 # already dead
417 pass
427 pass
418 # wait another 10s
428 # wait another 10s
419 try:
429 try:
420 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
430 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
421 except TimeoutExpired:
431 except TimeoutExpired:
422 print("Notebook server still running (%s)" % self.server_info_file,
432 print("Notebook server still running (%s)" % self.server_info_file,
423 file=sys.stderr
433 file=sys.stderr
424 )
434 )
425
435
426 self.stream_capturer.halt()
436 self.stream_capturer.halt()
427 TestController.cleanup(self)
437 TestController.cleanup(self)
428
438
429
439
430 def prepare_controllers(options):
440 def prepare_controllers(options):
431 """Returns two lists of TestController instances, those to run, and those
441 """Returns two lists of TestController instances, those to run, and those
432 not to run."""
442 not to run."""
433 testgroups = options.testgroups
443 testgroups = options.testgroups
434 if testgroups:
444 if testgroups:
435 if 'js' in testgroups:
445 if 'js' in testgroups:
436 js_testgroups = all_js_groups()
446 js_testgroups = all_js_groups()
437 else:
447 else:
438 js_testgroups = [g for g in testgroups if g.startswith(js_prefix)]
448 js_testgroups = [g for g in testgroups if g.startswith(js_prefix)]
439 py_testgroups = [g for g in testgroups if not g.startswith('js')]
449 py_testgroups = [g for g in testgroups if not g.startswith('js')]
440 else:
450 else:
441 py_testgroups = py_test_group_names
451 py_testgroups = py_test_group_names
442 if not options.all:
452 if not options.all:
443 js_testgroups = []
453 js_testgroups = []
444 test_sections['parallel'].enabled = False
454 test_sections['parallel'].enabled = False
445 else:
455 else:
446 js_testgroups = all_js_groups()
456 js_testgroups = all_js_groups()
447
457
448 engine = 'slimerjs' if options.slimerjs else 'phantomjs'
458 engine = 'slimerjs' if options.slimerjs else 'phantomjs'
449 c_js = [JSController(name, xunit=options.xunit, engine=engine, url=options.url) for name in js_testgroups]
459 c_js = [JSController(name, xunit=options.xunit, engine=engine, url=options.url) for name in js_testgroups]
450 c_py = [PyTestController(name, options) for name in py_testgroups]
460 c_py = [PyTestController(name, options) for name in py_testgroups]
451
461
452 controllers = c_py + c_js
462 controllers = c_py + c_js
453 to_run = [c for c in controllers if c.will_run]
463 to_run = [c for c in controllers if c.will_run]
454 not_run = [c for c in controllers if not c.will_run]
464 not_run = [c for c in controllers if not c.will_run]
455 return to_run, not_run
465 return to_run, not_run
456
466
457 def do_run(controller, buffer_output=True):
467 def do_run(controller, buffer_output=True):
458 """Setup and run a test controller.
468 """Setup and run a test controller.
459
469
460 If buffer_output is True, no output is displayed, to avoid it appearing
470 If buffer_output is True, no output is displayed, to avoid it appearing
461 interleaved. In this case, the caller is responsible for displaying test
471 interleaved. In this case, the caller is responsible for displaying test
462 output on failure.
472 output on failure.
463
473
464 Returns
474 Returns
465 -------
475 -------
466 controller : TestController
476 controller : TestController
467 The same controller as passed in, as a convenience for using map() type
477 The same controller as passed in, as a convenience for using map() type
468 APIs.
478 APIs.
469 exitcode : int
479 exitcode : int
470 The exit code of the test subprocess. Non-zero indicates failure.
480 The exit code of the test subprocess. Non-zero indicates failure.
471 """
481 """
472 try:
482 try:
473 try:
483 try:
474 controller.setup()
484 controller.setup()
475 if not buffer_output:
485 if not buffer_output:
476 controller.print_extra_info()
486 controller.print_extra_info()
477 controller.launch(buffer_output=buffer_output)
487 controller.launch(buffer_output=buffer_output)
478 except Exception:
488 except Exception:
479 import traceback
489 import traceback
480 traceback.print_exc()
490 traceback.print_exc()
481 return controller, 1 # signal failure
491 return controller, 1 # signal failure
482
492
483 exitcode = controller.wait()
493 exitcode = controller.wait()
484 return controller, exitcode
494 return controller, exitcode
485
495
486 except KeyboardInterrupt:
496 except KeyboardInterrupt:
487 return controller, -signal.SIGINT
497 return controller, -signal.SIGINT
488 finally:
498 finally:
489 controller.cleanup()
499 controller.cleanup()
490
500
491 def report():
501 def report():
492 """Return a string with a summary report of test-related variables."""
502 """Return a string with a summary report of test-related variables."""
493 inf = get_sys_info()
503 inf = get_sys_info()
494 out = []
504 out = []
495 def _add(name, value):
505 def _add(name, value):
496 out.append((name, value))
506 out.append((name, value))
497
507
498 _add('IPython version', inf['ipython_version'])
508 _add('IPython version', inf['ipython_version'])
499 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
509 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
500 _add('IPython package', compress_user(inf['ipython_path']))
510 _add('IPython package', compress_user(inf['ipython_path']))
501 _add('Python version', inf['sys_version'].replace('\n',''))
511 _add('Python version', inf['sys_version'].replace('\n',''))
502 _add('sys.executable', compress_user(inf['sys_executable']))
512 _add('sys.executable', compress_user(inf['sys_executable']))
503 _add('Platform', inf['platform'])
513 _add('Platform', inf['platform'])
504
514
505 width = max(len(n) for (n,v) in out)
515 width = max(len(n) for (n,v) in out)
506 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
516 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
507
517
508 avail = []
518 avail = []
509 not_avail = []
519 not_avail = []
510
520
511 for k, is_avail in have.items():
521 for k, is_avail in have.items():
512 if is_avail:
522 if is_avail:
513 avail.append(k)
523 avail.append(k)
514 else:
524 else:
515 not_avail.append(k)
525 not_avail.append(k)
516
526
517 if avail:
527 if avail:
518 out.append('\nTools and libraries available at test time:\n')
528 out.append('\nTools and libraries available at test time:\n')
519 avail.sort()
529 avail.sort()
520 out.append(' ' + ' '.join(avail)+'\n')
530 out.append(' ' + ' '.join(avail)+'\n')
521
531
522 if not_avail:
532 if not_avail:
523 out.append('\nTools and libraries NOT available at test time:\n')
533 out.append('\nTools and libraries NOT available at test time:\n')
524 not_avail.sort()
534 not_avail.sort()
525 out.append(' ' + ' '.join(not_avail)+'\n')
535 out.append(' ' + ' '.join(not_avail)+'\n')
526
536
527 return ''.join(out)
537 return ''.join(out)
528
538
529 def run_iptestall(options):
539 def run_iptestall(options):
530 """Run the entire IPython test suite by calling nose and trial.
540 """Run the entire IPython test suite by calling nose and trial.
531
541
532 This function constructs :class:`IPTester` instances for all IPython
542 This function constructs :class:`IPTester` instances for all IPython
533 modules and package and then runs each of them. This causes the modules
543 modules and package and then runs each of them. This causes the modules
534 and packages of IPython to be tested each in their own subprocess using
544 and packages of IPython to be tested each in their own subprocess using
535 nose.
545 nose.
536
546
537 Parameters
547 Parameters
538 ----------
548 ----------
539
549
540 All parameters are passed as attributes of the options object.
550 All parameters are passed as attributes of the options object.
541
551
542 testgroups : list of str
552 testgroups : list of str
543 Run only these sections of the test suite. If empty, run all the available
553 Run only these sections of the test suite. If empty, run all the available
544 sections.
554 sections.
545
555
546 fast : int or None
556 fast : int or None
547 Run the test suite in parallel, using n simultaneous processes. If None
557 Run the test suite in parallel, using n simultaneous processes. If None
548 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
558 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
549
559
550 inc_slow : bool
560 inc_slow : bool
551 Include slow tests, like IPython.parallel. By default, these tests aren't
561 Include slow tests, like IPython.parallel. By default, these tests aren't
552 run.
562 run.
553
563
554 slimerjs : bool
564 slimerjs : bool
555 Use slimerjs if it's installed instead of phantomjs for casperjs tests.
565 Use slimerjs if it's installed instead of phantomjs for casperjs tests.
556
566
557 url : unicode
567 url : unicode
558 Address:port to use when running the JS tests.
568 Address:port to use when running the JS tests.
559
569
560 xunit : bool
570 xunit : bool
561 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
571 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
562
572
563 coverage : bool or str
573 coverage : bool or str
564 Measure code coverage from tests. True will store the raw coverage data,
574 Measure code coverage from tests. True will store the raw coverage data,
565 or pass 'html' or 'xml' to get reports.
575 or pass 'html' or 'xml' to get reports.
566
576
567 extra_args : list
577 extra_args : list
568 Extra arguments to pass to the test subprocesses, e.g. '-v'
578 Extra arguments to pass to the test subprocesses, e.g. '-v'
569 """
579 """
570 to_run, not_run = prepare_controllers(options)
580 to_run, not_run = prepare_controllers(options)
571
581
572 def justify(ltext, rtext, width=70, fill='-'):
582 def justify(ltext, rtext, width=70, fill='-'):
573 ltext += ' '
583 ltext += ' '
574 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
584 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
575 return ltext + rtext
585 return ltext + rtext
576
586
577 # Run all test runners, tracking execution time
587 # Run all test runners, tracking execution time
578 failed = []
588 failed = []
579 t_start = time.time()
589 t_start = time.time()
580
590
581 print()
591 print()
582 if options.fast == 1:
592 if options.fast == 1:
583 # This actually means sequential, i.e. with 1 job
593 # This actually means sequential, i.e. with 1 job
584 for controller in to_run:
594 for controller in to_run:
585 print('Test group:', controller.section)
595 print('Test group:', controller.section)
586 sys.stdout.flush() # Show in correct order when output is piped
596 sys.stdout.flush() # Show in correct order when output is piped
587 controller, res = do_run(controller, buffer_output=False)
597 controller, res = do_run(controller, buffer_output=False)
588 if res:
598 if res:
589 failed.append(controller)
599 failed.append(controller)
590 if res == -signal.SIGINT:
600 if res == -signal.SIGINT:
591 print("Interrupted")
601 print("Interrupted")
592 break
602 break
593 print()
603 print()
594
604
595 else:
605 else:
596 # Run tests concurrently
606 # Run tests concurrently
597 try:
607 try:
598 pool = multiprocessing.pool.ThreadPool(options.fast)
608 pool = multiprocessing.pool.ThreadPool(options.fast)
599 for (controller, res) in pool.imap_unordered(do_run, to_run):
609 for (controller, res) in pool.imap_unordered(do_run, to_run):
600 res_string = 'OK' if res == 0 else 'FAILED'
610 res_string = 'OK' if res == 0 else 'FAILED'
601 print(justify('Test group: ' + controller.section, res_string))
611 print(justify('Test group: ' + controller.section, res_string))
602 if res:
612 if res:
603 controller.print_extra_info()
613 controller.print_extra_info()
604 print(bytes_to_str(controller.stdout))
614 print(bytes_to_str(controller.stdout))
605 failed.append(controller)
615 failed.append(controller)
606 if res == -signal.SIGINT:
616 if res == -signal.SIGINT:
607 print("Interrupted")
617 print("Interrupted")
608 break
618 break
609 except KeyboardInterrupt:
619 except KeyboardInterrupt:
610 return
620 return
611
621
612 for controller in not_run:
622 for controller in not_run:
613 print(justify('Test group: ' + controller.section, 'NOT RUN'))
623 print(justify('Test group: ' + controller.section, 'NOT RUN'))
614
624
615 t_end = time.time()
625 t_end = time.time()
616 t_tests = t_end - t_start
626 t_tests = t_end - t_start
617 nrunners = len(to_run)
627 nrunners = len(to_run)
618 nfail = len(failed)
628 nfail = len(failed)
619 # summarize results
629 # summarize results
620 print('_'*70)
630 print('_'*70)
621 print('Test suite completed for system with the following information:')
631 print('Test suite completed for system with the following information:')
622 print(report())
632 print(report())
623 took = "Took %.3fs." % t_tests
633 took = "Took %.3fs." % t_tests
624 print('Status: ', end='')
634 print('Status: ', end='')
625 if not failed:
635 if not failed:
626 print('OK (%d test groups).' % nrunners, took)
636 print('OK (%d test groups).' % nrunners, took)
627 else:
637 else:
628 # If anything went wrong, point out what command to rerun manually to
638 # If anything went wrong, point out what command to rerun manually to
629 # see the actual errors and individual summary
639 # see the actual errors and individual summary
630 failed_sections = [c.section for c in failed]
640 failed_sections = [c.section for c in failed]
631 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
641 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
632 nrunners, ', '.join(failed_sections)), took)
642 nrunners, ', '.join(failed_sections)), took)
633 print()
643 print()
634 print('You may wish to rerun these, with:')
644 print('You may wish to rerun these, with:')
635 print(' iptest', *failed_sections)
645 print(' iptest', *failed_sections)
636 print()
646 print()
637
647
638 if options.coverage:
648 if options.coverage:
639 from coverage import coverage, CoverageException
649 from coverage import coverage, CoverageException
640 cov = coverage(data_file='.coverage')
650 cov = coverage(data_file='.coverage')
641 cov.combine()
651 cov.combine()
642 cov.save()
652 cov.save()
643
653
644 # Coverage HTML report
654 # Coverage HTML report
645 if options.coverage == 'html':
655 if options.coverage == 'html':
646 html_dir = 'ipy_htmlcov'
656 html_dir = 'ipy_htmlcov'
647 shutil.rmtree(html_dir, ignore_errors=True)
657 shutil.rmtree(html_dir, ignore_errors=True)
648 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
658 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
649 sys.stdout.flush()
659 sys.stdout.flush()
650
660
651 # Custom HTML reporter to clean up module names.
661 # Custom HTML reporter to clean up module names.
652 from coverage.html import HtmlReporter
662 from coverage.html import HtmlReporter
653 class CustomHtmlReporter(HtmlReporter):
663 class CustomHtmlReporter(HtmlReporter):
654 def find_code_units(self, morfs):
664 def find_code_units(self, morfs):
655 super(CustomHtmlReporter, self).find_code_units(morfs)
665 super(CustomHtmlReporter, self).find_code_units(morfs)
656 for cu in self.code_units:
666 for cu in self.code_units:
657 nameparts = cu.name.split(os.sep)
667 nameparts = cu.name.split(os.sep)
658 if 'IPython' not in nameparts:
668 if 'IPython' not in nameparts:
659 continue
669 continue
660 ix = nameparts.index('IPython')
670 ix = nameparts.index('IPython')
661 cu.name = '.'.join(nameparts[ix:])
671 cu.name = '.'.join(nameparts[ix:])
662
672
663 # Reimplement the html_report method with our custom reporter
673 # Reimplement the html_report method with our custom reporter
664 cov._harvest_data()
674 cov._harvest_data()
665 cov.config.from_args(omit='*{0}tests{0}*'.format(os.sep), html_dir=html_dir,
675 cov.config.from_args(omit='*{0}tests{0}*'.format(os.sep), html_dir=html_dir,
666 html_title='IPython test coverage',
676 html_title='IPython test coverage',
667 )
677 )
668 reporter = CustomHtmlReporter(cov, cov.config)
678 reporter = CustomHtmlReporter(cov, cov.config)
669 reporter.report(None)
679 reporter.report(None)
670 print('done.')
680 print('done.')
671
681
672 # Coverage XML report
682 # Coverage XML report
673 elif options.coverage == 'xml':
683 elif options.coverage == 'xml':
674 try:
684 try:
675 cov.xml_report(outfile='ipy_coverage.xml')
685 cov.xml_report(outfile='ipy_coverage.xml')
676 except CoverageException as e:
686 except CoverageException as e:
677 print('Generating coverage report failed. Are you running javascript tests only?')
687 print('Generating coverage report failed. Are you running javascript tests only?')
678 import traceback
688 import traceback
679 traceback.print_exc()
689 traceback.print_exc()
680
690
681 if failed:
691 if failed:
682 # Ensure that our exit code indicates failure
692 # Ensure that our exit code indicates failure
683 sys.exit(1)
693 sys.exit(1)
684
694
685 argparser = argparse.ArgumentParser(description='Run IPython test suite')
695 argparser = argparse.ArgumentParser(description='Run IPython test suite')
686 argparser.add_argument('testgroups', nargs='*',
696 argparser.add_argument('testgroups', nargs='*',
687 help='Run specified groups of tests. If omitted, run '
697 help='Run specified groups of tests. If omitted, run '
688 'all tests.')
698 'all tests.')
689 argparser.add_argument('--all', action='store_true',
699 argparser.add_argument('--all', action='store_true',
690 help='Include slow tests not run by default.')
700 help='Include slow tests not run by default.')
691 argparser.add_argument('--slimerjs', action='store_true',
701 argparser.add_argument('--slimerjs', action='store_true',
692 help="Use slimerjs if it's installed instead of phantomjs for casperjs tests.")
702 help="Use slimerjs if it's installed instead of phantomjs for casperjs tests.")
693 argparser.add_argument('--url', help="URL to use for the JS tests.")
703 argparser.add_argument('--url', help="URL to use for the JS tests.")
694 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
704 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
695 help='Run test sections in parallel. This starts as many '
705 help='Run test sections in parallel. This starts as many '
696 'processes as you have cores, or you can specify a number.')
706 'processes as you have cores, or you can specify a number.')
697 argparser.add_argument('--xunit', action='store_true',
707 argparser.add_argument('--xunit', action='store_true',
698 help='Produce Xunit XML results')
708 help='Produce Xunit XML results')
699 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
709 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
700 help="Measure test coverage. Specify 'html' or "
710 help="Measure test coverage. Specify 'html' or "
701 "'xml' to get reports.")
711 "'xml' to get reports.")
702 argparser.add_argument('--subproc-streams', default='capture',
712 argparser.add_argument('--subproc-streams', default='capture',
703 help="What to do with stdout/stderr from subprocesses. "
713 help="What to do with stdout/stderr from subprocesses. "
704 "'capture' (default), 'show' and 'discard' are the options.")
714 "'capture' (default), 'show' and 'discard' are the options.")
705
715
706 def default_options():
716 def default_options():
707 """Get an argparse Namespace object with the default arguments, to pass to
717 """Get an argparse Namespace object with the default arguments, to pass to
708 :func:`run_iptestall`.
718 :func:`run_iptestall`.
709 """
719 """
710 options = argparser.parse_args([])
720 options = argparser.parse_args([])
711 options.extra_args = []
721 options.extra_args = []
712 return options
722 return options
713
723
714 def main():
724 def main():
715 # iptest doesn't work correctly if the working directory is the
725 # iptest doesn't work correctly if the working directory is the
716 # root of the IPython source tree. Tell the user to avoid
726 # root of the IPython source tree. Tell the user to avoid
717 # frustration.
727 # frustration.
718 if os.path.exists(os.path.join(os.getcwd(),
728 if os.path.exists(os.path.join(os.getcwd(),
719 'IPython', 'testing', '__main__.py')):
729 'IPython', 'testing', '__main__.py')):
720 print("Don't run iptest from the IPython source directory",
730 print("Don't run iptest from the IPython source directory",
721 file=sys.stderr)
731 file=sys.stderr)
722 sys.exit(1)
732 sys.exit(1)
723 # Arguments after -- should be passed through to nose. Argparse treats
733 # Arguments after -- should be passed through to nose. Argparse treats
724 # everything after -- as regular positional arguments, so we separate them
734 # everything after -- as regular positional arguments, so we separate them
725 # first.
735 # first.
726 try:
736 try:
727 ix = sys.argv.index('--')
737 ix = sys.argv.index('--')
728 except ValueError:
738 except ValueError:
729 to_parse = sys.argv[1:]
739 to_parse = sys.argv[1:]
730 extra_args = []
740 extra_args = []
731 else:
741 else:
732 to_parse = sys.argv[1:ix]
742 to_parse = sys.argv[1:ix]
733 extra_args = sys.argv[ix+1:]
743 extra_args = sys.argv[ix+1:]
734
744
735 options = argparser.parse_args(to_parse)
745 options = argparser.parse_args(to_parse)
736 options.extra_args = extra_args
746 options.extra_args = extra_args
737
747
738 run_iptestall(options)
748 run_iptestall(options)
739
749
740
750
741 if __name__ == '__main__':
751 if __name__ == '__main__':
742 main()
752 main()
General Comments 0
You need to be logged in to leave comments. Login now