##// END OF EJS Templates
more detail in oinfo tests
MinRK -
Show More
@@ -1,422 +1,427 b''
1 """Test suite for our zeromq-based messaging specification.
1 """Test suite for our zeromq-based messaging specification.
2 """
2 """
3 #-----------------------------------------------------------------------------
3 #-----------------------------------------------------------------------------
4 # Copyright (C) 2010-2011 The IPython Development Team
4 # Copyright (C) 2010-2011 The IPython Development Team
5 #
5 #
6 # Distributed under the terms of the BSD License. The full license is in
6 # Distributed under the terms of the BSD License. The full license is in
7 # the file COPYING.txt, distributed as part of this software.
7 # the file COPYING.txt, distributed as part of this software.
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9
9
10 import re
10 import re
11 import sys
11 import sys
12 import time
12 import time
13 from subprocess import PIPE
13 from subprocess import PIPE
14 from Queue import Empty
14 from Queue import Empty
15
15
16 import nose.tools as nt
16 import nose.tools as nt
17
17
18 from ..blockingkernelmanager import BlockingKernelManager
18 from ..blockingkernelmanager import BlockingKernelManager
19
19
20
20
21 from IPython.testing import decorators as dec
21 from IPython.testing import decorators as dec
22 from IPython.utils import io
22 from IPython.utils import io
23 from IPython.utils.traitlets import (
23 from IPython.utils.traitlets import (
24 HasTraits, TraitError, Bool, Unicode, Dict, Integer, List, Enum,
24 HasTraits, TraitError, Bool, Unicode, Dict, Integer, List, Enum,
25 )
25 )
26
26
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28 # Global setup and utilities
28 # Global setup and utilities
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30
30
31 def setup():
31 def setup():
32 global KM
32 global KM
33 KM = BlockingKernelManager()
33 KM = BlockingKernelManager()
34
34
35 KM.start_kernel(stdout=PIPE, stderr=PIPE)
35 KM.start_kernel(stdout=PIPE, stderr=PIPE)
36 KM.start_channels()
36 KM.start_channels()
37
37
38
38
39 def teardown():
39 def teardown():
40 KM.stop_channels()
40 KM.stop_channels()
41 KM.shutdown_kernel()
41 KM.shutdown_kernel()
42
42
43
43
44 def flush_channels():
44 def flush_channels():
45 """flush any messages waiting on the queue"""
45 """flush any messages waiting on the queue"""
46 for channel in (KM.shell_channel, KM.sub_channel):
46 for channel in (KM.shell_channel, KM.sub_channel):
47 while True:
47 while True:
48 try:
48 try:
49 msg = channel.get_msg(block=True, timeout=0.1)
49 msg = channel.get_msg(block=True, timeout=0.1)
50 except Empty:
50 except Empty:
51 break
51 break
52 else:
52 else:
53 validate_message(msg)
53 validate_message(msg)
54
54
55
55
56 def execute(code='', **kwargs):
56 def execute(code='', **kwargs):
57 """wrapper for doing common steps for validating an execution request"""
57 """wrapper for doing common steps for validating an execution request"""
58 shell = KM.shell_channel
58 shell = KM.shell_channel
59 sub = KM.sub_channel
59 sub = KM.sub_channel
60
60
61 msg_id = shell.execute(code=code, **kwargs)
61 msg_id = shell.execute(code=code, **kwargs)
62 reply = shell.get_msg(timeout=2)
62 reply = shell.get_msg(timeout=2)
63 validate_message(reply, 'execute_reply', msg_id)
63 validate_message(reply, 'execute_reply', msg_id)
64 busy = sub.get_msg(timeout=2)
64 busy = sub.get_msg(timeout=2)
65 validate_message(busy, 'status', msg_id)
65 validate_message(busy, 'status', msg_id)
66 nt.assert_equals(busy['content']['execution_state'], 'busy')
66 nt.assert_equals(busy['content']['execution_state'], 'busy')
67
67
68 if not kwargs.get('silent'):
68 if not kwargs.get('silent'):
69 pyin = sub.get_msg(timeout=2)
69 pyin = sub.get_msg(timeout=2)
70 validate_message(pyin, 'pyin', msg_id)
70 validate_message(pyin, 'pyin', msg_id)
71 nt.assert_equals(pyin['content']['code'], code)
71 nt.assert_equals(pyin['content']['code'], code)
72
72
73 return msg_id, reply['content']
73 return msg_id, reply['content']
74
74
75 #-----------------------------------------------------------------------------
75 #-----------------------------------------------------------------------------
76 # MSG Spec References
76 # MSG Spec References
77 #-----------------------------------------------------------------------------
77 #-----------------------------------------------------------------------------
78
78
79
79
80 class Reference(HasTraits):
80 class Reference(HasTraits):
81
81
82 def check(self, d):
82 def check(self, d):
83 """validate a dict against our traits"""
83 """validate a dict against our traits"""
84 for key in self.trait_names():
84 for key in self.trait_names():
85 yield nt.assert_true(key in d, "Missing key: %r, should be found in %s" % (key, d))
85 yield nt.assert_true(key in d, "Missing key: %r, should be found in %s" % (key, d))
86 # FIXME: always allow None, probably not a good idea
86 # FIXME: always allow None, probably not a good idea
87 if d[key] is None:
87 if d[key] is None:
88 continue
88 continue
89 try:
89 try:
90 setattr(self, key, d[key])
90 setattr(self, key, d[key])
91 except TraitError as e:
91 except TraitError as e:
92 yield nt.assert_true(False, str(e))
92 yield nt.assert_true(False, str(e))
93
93
94
94
95 class RMessage(Reference):
95 class RMessage(Reference):
96 msg_id = Unicode()
96 msg_id = Unicode()
97 msg_type = Unicode()
97 msg_type = Unicode()
98 header = Dict()
98 header = Dict()
99 parent_header = Dict()
99 parent_header = Dict()
100 content = Dict()
100 content = Dict()
101
101
102 class RHeader(Reference):
102 class RHeader(Reference):
103 msg_id = Unicode()
103 msg_id = Unicode()
104 msg_type = Unicode()
104 msg_type = Unicode()
105 session = Unicode()
105 session = Unicode()
106 username = Unicode()
106 username = Unicode()
107
107
108 class RContent(Reference):
108 class RContent(Reference):
109 status = Enum((u'ok', u'error'))
109 status = Enum((u'ok', u'error'))
110
110
111
111
112 class ExecuteReply(Reference):
112 class ExecuteReply(Reference):
113 execution_count = Integer()
113 execution_count = Integer()
114 status = Enum((u'ok', u'error'))
114 status = Enum((u'ok', u'error'))
115
115
116 def check(self, d):
116 def check(self, d):
117 for tst in Reference.check(self, d):
117 for tst in Reference.check(self, d):
118 yield tst
118 yield tst
119 if d['status'] == 'ok':
119 if d['status'] == 'ok':
120 for tst in ExecuteReplyOkay().check(d):
120 for tst in ExecuteReplyOkay().check(d):
121 yield tst
121 yield tst
122 elif d['status'] == 'error':
122 elif d['status'] == 'error':
123 for tst in ExecuteReplyError().check(d):
123 for tst in ExecuteReplyError().check(d):
124 yield tst
124 yield tst
125
125
126
126
127 class ExecuteReplyOkay(Reference):
127 class ExecuteReplyOkay(Reference):
128 payload = List(Dict)
128 payload = List(Dict)
129 user_variables = Dict()
129 user_variables = Dict()
130 user_expressions = Dict()
130 user_expressions = Dict()
131
131
132
132
133 class ExecuteReplyError(Reference):
133 class ExecuteReplyError(Reference):
134 ename = Unicode()
134 ename = Unicode()
135 evalue = Unicode()
135 evalue = Unicode()
136 traceback = List(Unicode)
136 traceback = List(Unicode)
137
137
138
138
139 class OInfoReply(Reference):
139 class OInfoReply(Reference):
140 name = Unicode()
140 name = Unicode()
141 found = Bool()
141 found = Bool()
142 ismagic = Bool()
142 ismagic = Bool()
143 isalias = Bool()
143 isalias = Bool()
144 namespace = Enum((u'builtin', u'magics', u'alias', u'Interactive'))
144 namespace = Enum((u'builtin', u'magics', u'alias', u'Interactive'))
145 type_name = Unicode()
145 type_name = Unicode()
146 string_form = Unicode()
146 string_form = Unicode()
147 base_class = Unicode()
147 base_class = Unicode()
148 length = Integer()
148 length = Integer()
149 file = Unicode()
149 file = Unicode()
150 definition = Unicode()
150 definition = Unicode()
151 argspec = Dict()
151 argspec = Dict()
152 init_definition = Unicode()
152 init_definition = Unicode()
153 docstring = Unicode()
153 docstring = Unicode()
154 init_docstring = Unicode()
154 init_docstring = Unicode()
155 class_docstring = Unicode()
155 class_docstring = Unicode()
156 call_def = Unicode()
156 call_def = Unicode()
157 call_docstring = Unicode()
157 call_docstring = Unicode()
158 source = Unicode()
158 source = Unicode()
159
159
160 def check(self, d):
160 def check(self, d):
161 for tst in Reference.check(self, d):
161 for tst in Reference.check(self, d):
162 yield tst
162 yield tst
163 if d['argspec'] is not None:
163 if d['argspec'] is not None:
164 for tst in ArgSpec().check(d['argspec']):
164 for tst in ArgSpec().check(d['argspec']):
165 yield tst
165 yield tst
166
166
167
167
168 class ArgSpec(Reference):
168 class ArgSpec(Reference):
169 args = List(Unicode)
169 args = List(Unicode)
170 varargs = Unicode()
170 varargs = Unicode()
171 varkw = Unicode()
171 varkw = Unicode()
172 defaults = List()
172 defaults = List()
173
173
174
174
175 class Status(Reference):
175 class Status(Reference):
176 execution_state = Enum((u'busy', u'idle'))
176 execution_state = Enum((u'busy', u'idle'))
177
177
178
178
179 class CompleteReply(Reference):
179 class CompleteReply(Reference):
180 matches = List(Unicode)
180 matches = List(Unicode)
181
181
182
182
183 # IOPub messages
183 # IOPub messages
184
184
185 class PyIn(Reference):
185 class PyIn(Reference):
186 code = Unicode()
186 code = Unicode()
187 execution_count = Integer()
187 execution_count = Integer()
188
188
189
189
190 PyErr = ExecuteReplyError
190 PyErr = ExecuteReplyError
191
191
192
192
193 class Stream(Reference):
193 class Stream(Reference):
194 name = Enum((u'stdout', u'stderr'))
194 name = Enum((u'stdout', u'stderr'))
195 data = Unicode()
195 data = Unicode()
196
196
197
197
198 mime_pat = re.compile(r'\w+/\w+')
198 mime_pat = re.compile(r'\w+/\w+')
199
199
200 class DisplayData(Reference):
200 class DisplayData(Reference):
201 source = Unicode()
201 source = Unicode()
202 metadata = Dict()
202 metadata = Dict()
203 data = Dict()
203 data = Dict()
204 def _data_changed(self, name, old, new):
204 def _data_changed(self, name, old, new):
205 for k,v in new.iteritems():
205 for k,v in new.iteritems():
206 nt.assert_true(mime_pat.match(k))
206 nt.assert_true(mime_pat.match(k))
207 nt.assert_true(isinstance(v, basestring), "expected string data, got %r" % v)
207 nt.assert_true(isinstance(v, basestring), "expected string data, got %r" % v)
208
208
209
209
210 references = {
210 references = {
211 'execute_reply' : ExecuteReply(),
211 'execute_reply' : ExecuteReply(),
212 'object_info_reply' : OInfoReply(),
212 'object_info_reply' : OInfoReply(),
213 'status' : Status(),
213 'status' : Status(),
214 'complete_reply' : CompleteReply(),
214 'complete_reply' : CompleteReply(),
215 'pyin' : PyIn(),
215 'pyin' : PyIn(),
216 'pyerr' : PyErr(),
216 'pyerr' : PyErr(),
217 'stream' : Stream(),
217 'stream' : Stream(),
218 'display_data' : DisplayData(),
218 'display_data' : DisplayData(),
219 }
219 }
220
220
221
221
222 def validate_message(msg, msg_type=None, parent=None):
222 def validate_message(msg, msg_type=None, parent=None):
223 """validate a message"""
223 """validate a message"""
224 RMessage().check(msg)
224 RMessage().check(msg)
225 if msg_type:
225 if msg_type:
226 yield nt.assert_equals(msg['msg_type'], msg_type)
226 yield nt.assert_equals(msg['msg_type'], msg_type)
227 if parent:
227 if parent:
228 yield nt.assert_equal(msg['parent_header']['msg_id'], parent)
228 yield nt.assert_equal(msg['parent_header']['msg_id'], parent)
229 content = msg['content']
229 content = msg['content']
230 ref = references[msg['msg_type']]
230 ref = references[msg['msg_type']]
231 for tst in ref.check(content):
231 for tst in ref.check(content):
232 yield tst
232 yield tst
233
233
234
234
235 #-----------------------------------------------------------------------------
235 #-----------------------------------------------------------------------------
236 # Tests
236 # Tests
237 #-----------------------------------------------------------------------------
237 #-----------------------------------------------------------------------------
238
238
239 # Shell channel
239 # Shell channel
240
240
241 @dec.parametric
241 @dec.parametric
242 def test_execute():
242 def test_execute():
243 flush_channels()
243 flush_channels()
244
244
245 shell = KM.shell_channel
245 shell = KM.shell_channel
246 msg_id = shell.execute(code='x=1')
246 msg_id = shell.execute(code='x=1')
247 reply = shell.get_msg(timeout=2)
247 reply = shell.get_msg(timeout=2)
248 for tst in validate_message(reply, 'execute_reply', msg_id):
248 for tst in validate_message(reply, 'execute_reply', msg_id):
249 yield tst
249 yield tst
250
250
251
251
252 @dec.parametric
252 @dec.parametric
253 def test_execute_silent():
253 def test_execute_silent():
254 flush_channels()
254 flush_channels()
255 msg_id, reply = execute(code='x=1', silent=True)
255 msg_id, reply = execute(code='x=1', silent=True)
256
256
257 # flush status=idle
257 # flush status=idle
258 status = KM.sub_channel.get_msg(timeout=2)
258 status = KM.sub_channel.get_msg(timeout=2)
259 for tst in validate_message(status, 'status', msg_id):
259 for tst in validate_message(status, 'status', msg_id):
260 yield tst
260 yield tst
261 nt.assert_equals(status['content']['execution_state'], 'idle')
261 nt.assert_equals(status['content']['execution_state'], 'idle')
262
262
263 yield nt.assert_raises(Empty, KM.sub_channel.get_msg, timeout=0.1)
263 yield nt.assert_raises(Empty, KM.sub_channel.get_msg, timeout=0.1)
264 count = reply['execution_count']
264 count = reply['execution_count']
265
265
266 msg_id, reply = execute(code='x=2', silent=True)
266 msg_id, reply = execute(code='x=2', silent=True)
267
267
268 # flush status=idle
268 # flush status=idle
269 status = KM.sub_channel.get_msg(timeout=2)
269 status = KM.sub_channel.get_msg(timeout=2)
270 for tst in validate_message(status, 'status', msg_id):
270 for tst in validate_message(status, 'status', msg_id):
271 yield tst
271 yield tst
272 yield nt.assert_equals(status['content']['execution_state'], 'idle')
272 yield nt.assert_equals(status['content']['execution_state'], 'idle')
273
273
274 yield nt.assert_raises(Empty, KM.sub_channel.get_msg, timeout=0.1)
274 yield nt.assert_raises(Empty, KM.sub_channel.get_msg, timeout=0.1)
275 count_2 = reply['execution_count']
275 count_2 = reply['execution_count']
276 yield nt.assert_equals(count_2, count)
276 yield nt.assert_equals(count_2, count)
277
277
278
278
279 @dec.parametric
279 @dec.parametric
280 def test_execute_error():
280 def test_execute_error():
281 flush_channels()
281 flush_channels()
282
282
283 msg_id, reply = execute(code='1/0')
283 msg_id, reply = execute(code='1/0')
284 yield nt.assert_equals(reply['status'], 'error')
284 yield nt.assert_equals(reply['status'], 'error')
285 yield nt.assert_equals(reply['ename'], 'ZeroDivisionError')
285 yield nt.assert_equals(reply['ename'], 'ZeroDivisionError')
286
286
287 pyerr = KM.sub_channel.get_msg(timeout=2)
287 pyerr = KM.sub_channel.get_msg(timeout=2)
288 for tst in validate_message(pyerr, 'pyerr', msg_id):
288 for tst in validate_message(pyerr, 'pyerr', msg_id):
289 yield tst
289 yield tst
290
290
291
291
292 def test_execute_inc():
292 def test_execute_inc():
293 """execute request should increment execution_count"""
293 """execute request should increment execution_count"""
294 flush_channels()
294 flush_channels()
295
295
296 msg_id, reply = execute(code='x=1')
296 msg_id, reply = execute(code='x=1')
297 count = reply['execution_count']
297 count = reply['execution_count']
298
298
299 flush_channels()
299 flush_channels()
300
300
301 msg_id, reply = execute(code='x=2')
301 msg_id, reply = execute(code='x=2')
302 count_2 = reply['execution_count']
302 count_2 = reply['execution_count']
303 nt.assert_equals(count_2, count+1)
303 nt.assert_equals(count_2, count+1)
304
304
305
305
306 def test_user_variables():
306 def test_user_variables():
307 flush_channels()
307 flush_channels()
308
308
309 msg_id, reply = execute(code='x=1', user_variables=['x'])
309 msg_id, reply = execute(code='x=1', user_variables=['x'])
310 user_variables = reply['user_variables']
310 user_variables = reply['user_variables']
311 nt.assert_equals(user_variables, {u'x' : u'1'})
311 nt.assert_equals(user_variables, {u'x' : u'1'})
312
312
313
313
314 def test_user_expressions():
314 def test_user_expressions():
315 flush_channels()
315 flush_channels()
316
316
317 msg_id, reply = execute(code='x=1', user_expressions=dict(foo='x+1'))
317 msg_id, reply = execute(code='x=1', user_expressions=dict(foo='x+1'))
318 user_expressions = reply['user_expressions']
318 user_expressions = reply['user_expressions']
319 nt.assert_equals(user_expressions, {u'foo' : u'2'})
319 nt.assert_equals(user_expressions, {u'foo' : u'2'})
320
320
321
321
322 @dec.parametric
322 @dec.parametric
323 def test_oinfo():
323 def test_oinfo():
324 flush_channels()
324 flush_channels()
325
325
326 shell = KM.shell_channel
326 shell = KM.shell_channel
327
327
328 msg_id = shell.object_info('a')
328 msg_id = shell.object_info('a')
329 reply = shell.get_msg(timeout=2)
329 reply = shell.get_msg(timeout=2)
330 for tst in validate_message(reply, 'object_info_reply', msg_id):
330 for tst in validate_message(reply, 'object_info_reply', msg_id):
331 yield tst
331 yield tst
332
332
333
333
334 @dec.parametric
334 @dec.parametric
335 def test_oinfo_found():
335 def test_oinfo_found():
336 flush_channels()
336 flush_channels()
337
337
338 shell = KM.shell_channel
338 shell = KM.shell_channel
339
339
340 msg_id, reply = execute(code='a=5')
340 msg_id, reply = execute(code='a=5')
341
341
342 msg_id = shell.object_info('a')
342 msg_id = shell.object_info('a')
343 reply = shell.get_msg(timeout=2)
343 reply = shell.get_msg(timeout=2)
344 for tst in validate_message(reply, 'object_info_reply', msg_id):
344 for tst in validate_message(reply, 'object_info_reply', msg_id):
345 yield tst
345 yield tst
346 content = reply['content']
346 content = reply['content']
347 yield nt.assert_true(content['found'])
347 yield nt.assert_true(content['found'])
348 argspec = content['argspec']
349 yield nt.assert_true(argspec is None, "didn't expect argspec dict, got %r" % argspec)
348
350
349
351
350 @dec.parametric
352 @dec.parametric
351 def test_oinfo_detail():
353 def test_oinfo_detail():
352 flush_channels()
354 flush_channels()
353
355
354 shell = KM.shell_channel
356 shell = KM.shell_channel
355
357
356 msg_id, reply = execute(code='ip=get_ipython()')
358 msg_id, reply = execute(code='ip=get_ipython()')
357
359
358 msg_id = shell.object_info('ip.object_inspect', detail_level=2)
360 msg_id = shell.object_info('ip.object_inspect', detail_level=2)
359 reply = shell.get_msg(timeout=2)
361 reply = shell.get_msg(timeout=2)
360 for tst in validate_message(reply, 'object_info_reply', msg_id):
362 for tst in validate_message(reply, 'object_info_reply', msg_id):
361 yield tst
363 yield tst
362 content = reply['content']
364 content = reply['content']
363 yield nt.assert_true(content['found'])
365 yield nt.assert_true(content['found'])
366 argspec = content['argspec']
367 yield nt.assert_true(isinstance(argspec, dict), "expected non-empty argspec dict, got %r" % argspec)
368 yield nt.assert_equals(argspec['defaults'], [0])
364
369
365
370
366 @dec.parametric
371 @dec.parametric
367 def test_oinfo_not_found():
372 def test_oinfo_not_found():
368 flush_channels()
373 flush_channels()
369
374
370 shell = KM.shell_channel
375 shell = KM.shell_channel
371
376
372 msg_id = shell.object_info('dne')
377 msg_id = shell.object_info('dne')
373 reply = shell.get_msg(timeout=2)
378 reply = shell.get_msg(timeout=2)
374 for tst in validate_message(reply, 'object_info_reply', msg_id):
379 for tst in validate_message(reply, 'object_info_reply', msg_id):
375 yield tst
380 yield tst
376 content = reply['content']
381 content = reply['content']
377 yield nt.assert_false(content['found'])
382 yield nt.assert_false(content['found'])
378
383
379
384
380 @dec.parametric
385 @dec.parametric
381 def test_complete():
386 def test_complete():
382 flush_channels()
387 flush_channels()
383
388
384 shell = KM.shell_channel
389 shell = KM.shell_channel
385
390
386 msg_id, reply = execute(code="alpha = albert = 5")
391 msg_id, reply = execute(code="alpha = albert = 5")
387
392
388 msg_id = shell.complete('al', 'al', 2)
393 msg_id = shell.complete('al', 'al', 2)
389 reply = shell.get_msg(timeout=2)
394 reply = shell.get_msg(timeout=2)
390 for tst in validate_message(reply, 'complete_reply', msg_id):
395 for tst in validate_message(reply, 'complete_reply', msg_id):
391 yield tst
396 yield tst
392 matches = reply['content']['matches']
397 matches = reply['content']['matches']
393 for name in ('alpha', 'albert'):
398 for name in ('alpha', 'albert'):
394 yield nt.assert_true(name in matches, "Missing match: %r" % name)
399 yield nt.assert_true(name in matches, "Missing match: %r" % name)
395
400
396
401
397 @dec.parametric
402 @dec.parametric
398 def test_stream():
403 def test_stream():
399 flush_channels()
404 flush_channels()
400
405
401 msg_id, reply = execute("print('hi')")
406 msg_id, reply = execute("print('hi')")
402
407
403 stdout = KM.sub_channel.get_msg(timeout=2)
408 stdout = KM.sub_channel.get_msg(timeout=2)
404 for tst in validate_message(stdout, 'stream', msg_id):
409 for tst in validate_message(stdout, 'stream', msg_id):
405 yield tst
410 yield tst
406 content = stdout['content']
411 content = stdout['content']
407 yield nt.assert_equals(content['name'], u'stdout')
412 yield nt.assert_equals(content['name'], u'stdout')
408 yield nt.assert_equals(content['data'], u'hi\n')
413 yield nt.assert_equals(content['data'], u'hi\n')
409
414
410
415
411 @dec.parametric
416 @dec.parametric
412 def test_display_data():
417 def test_display_data():
413 flush_channels()
418 flush_channels()
414
419
415 msg_id, reply = execute("from IPython.core.display import display; display(1)")
420 msg_id, reply = execute("from IPython.core.display import display; display(1)")
416
421
417 display = KM.sub_channel.get_msg(timeout=2)
422 display = KM.sub_channel.get_msg(timeout=2)
418 for tst in validate_message(display, 'display_data', parent=msg_id):
423 for tst in validate_message(display, 'display_data', parent=msg_id):
419 yield tst
424 yield tst
420 data = display['content']['data']
425 data = display['content']['data']
421 yield nt.assert_equals(data['text/plain'], u'1')
426 yield nt.assert_equals(data['text/plain'], u'1')
422
427
General Comments 0
You need to be logged in to leave comments. Login now