##// END OF EJS Templates
resolve rebase conflict...
MinRK -
Show More
@@ -1,501 +1,501 b''
1 1 """Test suite for our zeromq-based messaging specification.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Copyright (C) 2010-2011 The IPython Development Team
5 5 #
6 6 # Distributed under the terms of the BSD License. The full license is in
7 7 # the file COPYING.txt, distributed as part of this software.
8 8 #-----------------------------------------------------------------------------
9 9
10 10 import re
11 11 import sys
12 12 import time
13 13 from subprocess import PIPE
14 14 from Queue import Empty
15 15
16 16 import nose.tools as nt
17 17
18 18 from ..blockingkernelmanager import BlockingKernelManager
19 19
20 20
21 21 from IPython.testing import decorators as dec
22 22 from IPython.utils import io
23 23 from IPython.utils.traitlets import (
24 24 HasTraits, TraitError, Bool, Unicode, Dict, Integer, List, Enum, Any,
25 25 )
26 26
27 27 #-----------------------------------------------------------------------------
28 28 # Global setup and utilities
29 29 #-----------------------------------------------------------------------------
30 30
31 31 def setup():
32 32 global KM
33 33 KM = BlockingKernelManager()
34 34
35 35 KM.start_kernel(stdout=PIPE, stderr=PIPE)
36 36 KM.start_channels()
37 37
38 38 # wait for kernel to be ready
39 39 KM.shell_channel.execute("pass")
40 40 KM.shell_channel.get_msg(block=True, timeout=5)
41 41 flush_channels()
42 42
43 43
44 44 def teardown():
45 45 KM.stop_channels()
46 46 KM.shutdown_kernel()
47 47
48 48
49 49 def flush_channels(km=None):
50 50 if km is None:
51 51 km = KM
52 52 """flush any messages waiting on the queue"""
53 for channel in (KM.shell_channel, KM.iopub_channel):
53 for channel in (km.shell_channel, km.iopub_channel):
54 54 while True:
55 55 try:
56 56 msg = channel.get_msg(block=True, timeout=0.1)
57 57 except Empty:
58 58 break
59 59 else:
60 60 list(validate_message(msg))
61 61
62 62
63 63 def execute(code='', km=None, **kwargs):
64 64 """wrapper for doing common steps for validating an execution request"""
65 65 if km is None:
66 66 km = KM
67 67 shell = km.shell_channel
68 68 sub = km.iopub_channel
69 69
70 70 msg_id = shell.execute(code=code, **kwargs)
71 71 reply = shell.get_msg(timeout=2)
72 72 list(validate_message(reply, 'execute_reply', msg_id))
73 73 busy = sub.get_msg(timeout=2)
74 74 list(validate_message(busy, 'status', msg_id))
75 75 nt.assert_equal(busy['content']['execution_state'], 'busy')
76 76
77 77 if not kwargs.get('silent'):
78 78 pyin = sub.get_msg(timeout=2)
79 79 list(validate_message(pyin, 'pyin', msg_id))
80 80 nt.assert_equal(pyin['content']['code'], code)
81 81
82 82 return msg_id, reply['content']
83 83
84 84 #-----------------------------------------------------------------------------
85 85 # MSG Spec References
86 86 #-----------------------------------------------------------------------------
87 87
88 88
89 89 class Reference(HasTraits):
90 90
91 91 """
92 92 Base class for message spec specification testing.
93 93
94 94 This class is the core of the message specification test. The
95 95 idea is that child classes implement trait attributes for each
96 96 message keys, so that message keys can be tested against these
97 97 traits using :meth:`check` method.
98 98
99 99 """
100 100
101 101 def check(self, d):
102 102 """validate a dict against our traits"""
103 103 for key in self.trait_names():
104 104 yield nt.assert_true(key in d, "Missing key: %r, should be found in %s" % (key, d))
105 105 # FIXME: always allow None, probably not a good idea
106 106 if d[key] is None:
107 107 continue
108 108 try:
109 109 setattr(self, key, d[key])
110 110 except TraitError as e:
111 111 yield nt.assert_true(False, str(e))
112 112
113 113
114 114 class RMessage(Reference):
115 115 msg_id = Unicode()
116 116 msg_type = Unicode()
117 117 header = Dict()
118 118 parent_header = Dict()
119 119 content = Dict()
120 120
121 121 class RHeader(Reference):
122 122 msg_id = Unicode()
123 123 msg_type = Unicode()
124 124 session = Unicode()
125 125 username = Unicode()
126 126
127 127 class RContent(Reference):
128 128 status = Enum((u'ok', u'error'))
129 129
130 130
131 131 class ExecuteReply(Reference):
132 132 execution_count = Integer()
133 133 status = Enum((u'ok', u'error'))
134 134
135 135 def check(self, d):
136 136 for tst in Reference.check(self, d):
137 137 yield tst
138 138 if d['status'] == 'ok':
139 139 for tst in ExecuteReplyOkay().check(d):
140 140 yield tst
141 141 elif d['status'] == 'error':
142 142 for tst in ExecuteReplyError().check(d):
143 143 yield tst
144 144
145 145
146 146 class ExecuteReplyOkay(Reference):
147 147 payload = List(Dict)
148 148 user_variables = Dict()
149 149 user_expressions = Dict()
150 150
151 151
152 152 class ExecuteReplyError(Reference):
153 153 ename = Unicode()
154 154 evalue = Unicode()
155 155 traceback = List(Unicode)
156 156
157 157
158 158 class OInfoReply(Reference):
159 159 name = Unicode()
160 160 found = Bool()
161 161 ismagic = Bool()
162 162 isalias = Bool()
163 163 namespace = Enum((u'builtin', u'magics', u'alias', u'Interactive'))
164 164 type_name = Unicode()
165 165 string_form = Unicode()
166 166 base_class = Unicode()
167 167 length = Integer()
168 168 file = Unicode()
169 169 definition = Unicode()
170 170 argspec = Dict()
171 171 init_definition = Unicode()
172 172 docstring = Unicode()
173 173 init_docstring = Unicode()
174 174 class_docstring = Unicode()
175 175 call_def = Unicode()
176 176 call_docstring = Unicode()
177 177 source = Unicode()
178 178
179 179 def check(self, d):
180 180 for tst in Reference.check(self, d):
181 181 yield tst
182 182 if d['argspec'] is not None:
183 183 for tst in ArgSpec().check(d['argspec']):
184 184 yield tst
185 185
186 186
187 187 class ArgSpec(Reference):
188 188 args = List(Unicode)
189 189 varargs = Unicode()
190 190 varkw = Unicode()
191 191 defaults = List()
192 192
193 193
194 194 class Status(Reference):
195 195 execution_state = Enum((u'busy', u'idle'))
196 196
197 197
198 198 class CompleteReply(Reference):
199 199 matches = List(Unicode)
200 200
201 201
202 202 def Version(num, trait=Integer):
203 203 return List(trait, default_value=[0] * num, minlen=num, maxlen=num)
204 204
205 205
206 206 class KernelInfoReply(Reference):
207 207
208 208 protocol_version = Version(2)
209 209 ipython_version = Version(4, Any)
210 210 language_version = Version(3)
211 211 language = Unicode()
212 212
213 213 def _ipython_version_changed(self, name, old, new):
214 214 for v in new:
215 215 nt.assert_true(
216 216 isinstance(v, int) or isinstance(v, basestring),
217 217 'expected int or string as version component, got {0!r}'
218 218 .format(v))
219 219
220 220
221 221 # IOPub messages
222 222
223 223 class PyIn(Reference):
224 224 code = Unicode()
225 225 execution_count = Integer()
226 226
227 227
228 228 PyErr = ExecuteReplyError
229 229
230 230
231 231 class Stream(Reference):
232 232 name = Enum((u'stdout', u'stderr'))
233 233 data = Unicode()
234 234
235 235
236 236 mime_pat = re.compile(r'\w+/\w+')
237 237
238 238 class DisplayData(Reference):
239 239 source = Unicode()
240 240 metadata = Dict()
241 241 data = Dict()
242 242 def _data_changed(self, name, old, new):
243 243 for k,v in new.iteritems():
244 244 nt.assert_true(mime_pat.match(k))
245 245 nt.assert_true(isinstance(v, basestring), "expected string data, got %r" % v)
246 246
247 247
248 248 class PyOut(Reference):
249 249 execution_count = Integer()
250 250 data = Dict()
251 251 def _data_changed(self, name, old, new):
252 252 for k,v in new.iteritems():
253 253 nt.assert_true(mime_pat.match(k))
254 254 nt.assert_true(isinstance(v, basestring), "expected string data, got %r" % v)
255 255
256 256
257 257 references = {
258 258 'execute_reply' : ExecuteReply(),
259 259 'object_info_reply' : OInfoReply(),
260 260 'status' : Status(),
261 261 'complete_reply' : CompleteReply(),
262 262 'kernel_info_reply': KernelInfoReply(),
263 263 'pyin' : PyIn(),
264 264 'pyout' : PyOut(),
265 265 'pyerr' : PyErr(),
266 266 'stream' : Stream(),
267 267 'display_data' : DisplayData(),
268 268 }
269 269 """
270 270 Specifications of `content` part of the reply messages.
271 271 """
272 272
273 273
274 274 def validate_message(msg, msg_type=None, parent=None):
275 275 """validate a message
276 276
277 277 This is a generator, and must be iterated through to actually
278 278 trigger each test.
279 279
280 280 If msg_type and/or parent are given, the msg_type and/or parent msg_id
281 281 are compared with the given values.
282 282 """
283 283 RMessage().check(msg)
284 284 if msg_type:
285 285 yield nt.assert_equal(msg['msg_type'], msg_type)
286 286 if parent:
287 287 yield nt.assert_equal(msg['parent_header']['msg_id'], parent)
288 288 content = msg['content']
289 289 ref = references[msg['msg_type']]
290 290 for tst in ref.check(content):
291 291 yield tst
292 292
293 293
294 294 #-----------------------------------------------------------------------------
295 295 # Tests
296 296 #-----------------------------------------------------------------------------
297 297
298 298 # Shell channel
299 299
300 300 @dec.parametric
301 301 def test_execute():
302 302 flush_channels()
303 303
304 304 shell = KM.shell_channel
305 305 msg_id = shell.execute(code='x=1')
306 306 reply = shell.get_msg(timeout=2)
307 307 for tst in validate_message(reply, 'execute_reply', msg_id):
308 308 yield tst
309 309
310 310
311 311 @dec.parametric
312 312 def test_execute_silent():
313 313 flush_channels()
314 314 msg_id, reply = execute(code='x=1', silent=True)
315 315
316 316 # flush status=idle
317 317 status = KM.iopub_channel.get_msg(timeout=2)
318 318 for tst in validate_message(status, 'status', msg_id):
319 319 yield tst
320 320 nt.assert_equal(status['content']['execution_state'], 'idle')
321 321
322 322 yield nt.assert_raises(Empty, KM.iopub_channel.get_msg, timeout=0.1)
323 323 count = reply['execution_count']
324 324
325 325 msg_id, reply = execute(code='x=2', silent=True)
326 326
327 327 # flush status=idle
328 328 status = KM.iopub_channel.get_msg(timeout=2)
329 329 for tst in validate_message(status, 'status', msg_id):
330 330 yield tst
331 331 yield nt.assert_equal(status['content']['execution_state'], 'idle')
332 332
333 333 yield nt.assert_raises(Empty, KM.iopub_channel.get_msg, timeout=0.1)
334 334 count_2 = reply['execution_count']
335 335 yield nt.assert_equal(count_2, count)
336 336
337 337
338 338 @dec.parametric
339 339 def test_execute_error():
340 340 flush_channels()
341 341
342 342 msg_id, reply = execute(code='1/0')
343 343 yield nt.assert_equal(reply['status'], 'error')
344 344 yield nt.assert_equal(reply['ename'], 'ZeroDivisionError')
345 345
346 346 pyerr = KM.iopub_channel.get_msg(timeout=2)
347 347 for tst in validate_message(pyerr, 'pyerr', msg_id):
348 348 yield tst
349 349
350 350
351 351 def test_execute_inc():
352 352 """execute request should increment execution_count"""
353 353 flush_channels()
354 354
355 355 msg_id, reply = execute(code='x=1')
356 356 count = reply['execution_count']
357 357
358 358 flush_channels()
359 359
360 360 msg_id, reply = execute(code='x=2')
361 361 count_2 = reply['execution_count']
362 362 nt.assert_equal(count_2, count+1)
363 363
364 364
365 365 def test_user_variables():
366 366 flush_channels()
367 367
368 368 msg_id, reply = execute(code='x=1', user_variables=['x'])
369 369 user_variables = reply['user_variables']
370 370 nt.assert_equal(user_variables, {u'x' : u'1'})
371 371
372 372
373 373 def test_user_expressions():
374 374 flush_channels()
375 375
376 376 msg_id, reply = execute(code='x=1', user_expressions=dict(foo='x+1'))
377 377 user_expressions = reply['user_expressions']
378 378 nt.assert_equal(user_expressions, {u'foo' : u'2'})
379 379
380 380
381 381 @dec.parametric
382 382 def test_oinfo():
383 383 flush_channels()
384 384
385 385 shell = KM.shell_channel
386 386
387 387 msg_id = shell.object_info('a')
388 388 reply = shell.get_msg(timeout=2)
389 389 for tst in validate_message(reply, 'object_info_reply', msg_id):
390 390 yield tst
391 391
392 392
393 393 @dec.parametric
394 394 def test_oinfo_found():
395 395 flush_channels()
396 396
397 397 shell = KM.shell_channel
398 398
399 399 msg_id, reply = execute(code='a=5')
400 400
401 401 msg_id = shell.object_info('a')
402 402 reply = shell.get_msg(timeout=2)
403 403 for tst in validate_message(reply, 'object_info_reply', msg_id):
404 404 yield tst
405 405 content = reply['content']
406 406 yield nt.assert_true(content['found'])
407 407 argspec = content['argspec']
408 408 yield nt.assert_true(argspec is None, "didn't expect argspec dict, got %r" % argspec)
409 409
410 410
411 411 @dec.parametric
412 412 def test_oinfo_detail():
413 413 flush_channels()
414 414
415 415 shell = KM.shell_channel
416 416
417 417 msg_id, reply = execute(code='ip=get_ipython()')
418 418
419 419 msg_id = shell.object_info('ip.object_inspect', detail_level=2)
420 420 reply = shell.get_msg(timeout=2)
421 421 for tst in validate_message(reply, 'object_info_reply', msg_id):
422 422 yield tst
423 423 content = reply['content']
424 424 yield nt.assert_true(content['found'])
425 425 argspec = content['argspec']
426 426 yield nt.assert_true(isinstance(argspec, dict), "expected non-empty argspec dict, got %r" % argspec)
427 427 yield nt.assert_equal(argspec['defaults'], [0])
428 428
429 429
430 430 @dec.parametric
431 431 def test_oinfo_not_found():
432 432 flush_channels()
433 433
434 434 shell = KM.shell_channel
435 435
436 436 msg_id = shell.object_info('dne')
437 437 reply = shell.get_msg(timeout=2)
438 438 for tst in validate_message(reply, 'object_info_reply', msg_id):
439 439 yield tst
440 440 content = reply['content']
441 441 yield nt.assert_false(content['found'])
442 442
443 443
444 444 @dec.parametric
445 445 def test_complete():
446 446 flush_channels()
447 447
448 448 shell = KM.shell_channel
449 449
450 450 msg_id, reply = execute(code="alpha = albert = 5")
451 451
452 452 msg_id = shell.complete('al', 'al', 2)
453 453 reply = shell.get_msg(timeout=2)
454 454 for tst in validate_message(reply, 'complete_reply', msg_id):
455 455 yield tst
456 456 matches = reply['content']['matches']
457 457 for name in ('alpha', 'albert'):
458 458 yield nt.assert_true(name in matches, "Missing match: %r" % name)
459 459
460 460
461 461 @dec.parametric
462 462 def test_kernel_info_request():
463 463 flush_channels()
464 464
465 465 shell = KM.shell_channel
466 466
467 467 msg_id = shell.kernel_info()
468 468 reply = shell.get_msg(timeout=2)
469 469 for tst in validate_message(reply, 'kernel_info_reply', msg_id):
470 470 yield tst
471 471
472 472
473 473 # IOPub channel
474 474
475 475
476 476 @dec.parametric
477 477 def test_stream():
478 478 flush_channels()
479 479
480 480 msg_id, reply = execute("print('hi')")
481 481
482 482 stdout = KM.iopub_channel.get_msg(timeout=2)
483 483 for tst in validate_message(stdout, 'stream', msg_id):
484 484 yield tst
485 485 content = stdout['content']
486 486 yield nt.assert_equal(content['name'], u'stdout')
487 487 yield nt.assert_equal(content['data'], u'hi\n')
488 488
489 489
490 490 @dec.parametric
491 491 def test_display_data():
492 492 flush_channels()
493 493
494 494 msg_id, reply = execute("from IPython.core.display import display; display(1)")
495 495
496 496 display = KM.iopub_channel.get_msg(timeout=2)
497 497 for tst in validate_message(display, 'display_data', parent=msg_id):
498 498 yield tst
499 499 data = display['content']['data']
500 500 yield nt.assert_equal(data['text/plain'], u'1')
501 501
@@ -1,203 +1,203 b''
1 1 """test the IPython Kernel"""
2 2
3 3 #-------------------------------------------------------------------------------
4 4 # Copyright (C) 2013 The IPython Development Team
5 5 #
6 6 # Distributed under the terms of the BSD License. The full license is in
7 7 # the file COPYING, distributed as part of this software.
8 8 #-------------------------------------------------------------------------------
9 9
10 10 #-------------------------------------------------------------------------------
11 11 # Imports
12 12 #-------------------------------------------------------------------------------
13 13
14 14 import os
15 15 import shutil
16 16 import tempfile
17 17
18 18 from Queue import Empty
19 19 from contextlib import contextmanager
20 20 from subprocess import PIPE
21 21
22 22 import nose.tools as nt
23 23
24 24 from IPython.zmq.blockingkernelmanager import BlockingKernelManager
25 25 from IPython.zmq.tests.test_message_spec import execute, flush_channels
26 26 from IPython.testing import decorators as dec
27 27 from IPython.utils import path, py3compat
28 28
29 29 #-------------------------------------------------------------------------------
30 30 # Tests
31 31 #-------------------------------------------------------------------------------
32 32
33 33 def setup():
34 34 """setup temporary IPYTHONDIR for tests"""
35 35 global IPYTHONDIR
36 36 global save_env
37 37 global save_get_ipython_dir
38 38
39 39 IPYTHONDIR = tempfile.mkdtemp()
40 40
41 41 save_env = os.environ.copy()
42 42 os.environ["IPYTHONDIR"] = IPYTHONDIR
43 43
44 44 save_get_ipython_dir = path.get_ipython_dir
45 45 path.get_ipython_dir = lambda : IPYTHONDIR
46 46
47 47
48 48 def teardown():
49 49 path.get_ipython_dir = save_get_ipython_dir
50 50 os.environ = save_env
51 51
52 52 try:
53 53 shutil.rmtree(IPYTHONDIR)
54 54 except (OSError, IOError):
55 55 # no such file
56 56 pass
57 57
58 58
59 59 @contextmanager
60 60 def new_kernel():
61 61 """start a kernel in a subprocess, and wait for it to be ready
62 62
63 63 Returns
64 64 -------
65 65 kernel_manager: connected KernelManager instance
66 66 """
67 67 KM = BlockingKernelManager()
68 68
69 69 KM.start_kernel(stdout=PIPE, stderr=PIPE)
70 70 KM.start_channels()
71 71
72 72 # wait for kernel to be ready
73 73 KM.shell_channel.execute("import sys")
74 74 KM.shell_channel.get_msg(block=True, timeout=5)
75 75 flush_channels(KM)
76 76 try:
77 77 yield KM
78 78 finally:
79 79 KM.stop_channels()
80 80 KM.shutdown_kernel()
81 81
82 82
83 83 def assemble_output(iopub):
84 84 """assemble stdout/err from an execution"""
85 85 stdout = ''
86 86 stderr = ''
87 87 while True:
88 88 msg = iopub.get_msg(block=True, timeout=1)
89 89 msg_type = msg['msg_type']
90 90 content = msg['content']
91 91 if msg_type == 'status' and content['execution_state'] == 'idle':
92 92 # idle message signals end of output
93 93 break
94 94 elif msg['msg_type'] == 'stream':
95 95 if content['name'] == 'stdout':
96 96 stdout = stdout + content['data']
97 97 elif content['name'] == 'stderr':
98 98 stderr = stderr + content['data']
99 99 else:
100 100 raise KeyError("bad stream: %r" % content['name'])
101 101 else:
102 102 # other output, ignored
103 103 pass
104 104 return stdout, stderr
105 105
106 106
107 107 def _check_mp_mode(km, expected=False, stream="stdout"):
108 108 execute(km=km, code="import sys")
109 109 flush_channels(km)
110 110 msg_id, content = execute(km=km, code="print (sys.%s._check_mp_mode())" % stream)
111 stdout, stderr = assemble_output(km.sub_channel)
111 stdout, stderr = assemble_output(km.iopub_channel)
112 112 nt.assert_equal(eval(stdout.strip()), expected)
113 113
114 114
115 115 def test_simple_print():
116 116 """simple print statement in kernel"""
117 117 with new_kernel() as km:
118 iopub = km.sub_channel
118 iopub = km.iopub_channel
119 119 msg_id, content = execute(km=km, code="print ('hi')")
120 120 stdout, stderr = assemble_output(iopub)
121 121 nt.assert_equal(stdout, 'hi\n')
122 122 nt.assert_equal(stderr, '')
123 123 _check_mp_mode(km, expected=False)
124 124 print ('hello')
125 125
126 126
127 127 def test_subprocess_print():
128 128 """printing from forked mp.Process"""
129 129 with new_kernel() as km:
130 iopub = km.sub_channel
130 iopub = km.iopub_channel
131 131
132 132 _check_mp_mode(km, expected=False)
133 133 flush_channels(km)
134 134 np = 5
135 135 code = '\n'.join([
136 136 "import multiprocessing as mp",
137 137 "def f(x):",
138 138 " print('hello',x)",
139 139 "pool = [mp.Process(target=f,args=(i,)) for i in range(%i)]" % np,
140 140 "for p in pool: p.start()",
141 141 "for p in pool: p.join()"
142 142 ])
143 143
144 144 expected = '\n'.join([
145 145 "hello %s" % i for i in range(np)
146 146 ]) + '\n'
147 147
148 148 msg_id, content = execute(km=km, code=code)
149 149 stdout, stderr = assemble_output(iopub)
150 150 nt.assert_equal(stdout.count("hello"), np, stdout)
151 151 for n in range(np):
152 152 nt.assert_equal(stdout.count(str(n)), 1, stdout)
153 153 nt.assert_equal(stderr, '')
154 154 _check_mp_mode(km, expected=False)
155 155 _check_mp_mode(km, expected=False, stream="stderr")
156 156
157 157
158 158 def test_subprocess_noprint():
159 159 """mp.Process without print doesn't trigger iostream mp_mode"""
160 160 with new_kernel() as km:
161 iopub = km.sub_channel
161 iopub = km.iopub_channel
162 162
163 163 np = 5
164 164 code = '\n'.join([
165 165 "import multiprocessing as mp",
166 166 "def f(x):",
167 167 " return x",
168 168 "pool = [mp.Process(target=f,args=(i,)) for i in range(%i)]" % np,
169 169 "for p in pool: p.start()",
170 170 "for p in pool: p.join()"
171 171 ])
172 172
173 173 msg_id, content = execute(km=km, code=code)
174 174 stdout, stderr = assemble_output(iopub)
175 175 nt.assert_equal(stdout, '')
176 176 nt.assert_equal(stderr, '')
177 177
178 178 _check_mp_mode(km, expected=False)
179 179 _check_mp_mode(km, expected=False, stream="stderr")
180 180
181 181
182 182 def test_subprocess_error():
183 183 """error in mp.Process doesn't crash"""
184 184 with new_kernel() as km:
185 iopub = km.sub_channel
185 iopub = km.iopub_channel
186 186
187 187 code = '\n'.join([
188 188 "import multiprocessing as mp",
189 189 "def f():",
190 190 " return 1/0",
191 191 "p = mp.Process(target=f)",
192 192 "p.start()",
193 193 "p.join()",
194 194 ])
195 195
196 196 msg_id, content = execute(km=km, code=code)
197 197 stdout, stderr = assemble_output(iopub)
198 198 nt.assert_equal(stdout, '')
199 199 nt.assert_true("ZeroDivisionError" in stderr, stderr)
200 200
201 201 _check_mp_mode(km, expected=False)
202 202 _check_mp_mode(km, expected=False, stream="stderr")
203 203
General Comments 0
You need to be logged in to leave comments. Login now