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