Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Avatar for KuCalc : devops.
Download
50640 views
1
import pytest
2
import os
3
import re
4
import socket
5
import json
6
import signal
7
import struct
8
import hashlib
9
import time
10
from datetime import datetime
11
12
###
13
# much of the code here is copied from sage_server.py
14
# cut and paste was done because it takes over 30 sec to import sage_server
15
# and requires the script to be run from sage -sh
16
###
17
18
def unicode8(s):
19
try:
20
return unicode(s, 'utf8')
21
except:
22
try:
23
return unicode(s)
24
except:
25
return s
26
27
PID = os.getpid()
28
from datetime import datetime
29
30
def log(*args):
31
mesg = "%s (%s): %s\n"%(PID, datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3], ' '.join([unicode8(x) for x in args]))
32
print(mesg)
33
34
def uuidsha1(data):
35
sha1sum = hashlib.sha1()
36
sha1sum.update(data)
37
s = sha1sum.hexdigest()
38
t = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
39
r = list(t)
40
j = 0
41
for i in range(len(t)):
42
if t[i] == 'x':
43
r[i] = s[j]; j += 1
44
elif t[i] == 'y':
45
# take 8 + low order 3 bits of hex number.
46
r[i] = hex( (int(s[j],16)&0x3) |0x8)[-1]; j += 1
47
return ''.join(r)
48
49
class ConnectionJSON(object):
50
def __init__(self, conn):
51
assert not isinstance(conn, ConnectionJSON) # avoid common mistake -- conn is supposed to be from socket.socket...
52
self._conn = conn
53
54
def close(self):
55
self._conn.close()
56
57
def _send(self, s):
58
length_header = struct.pack(">L", len(s))
59
self._conn.send(length_header + s)
60
61
def send_json(self, m):
62
m = json.dumps(m)
63
#log(u"sending message '", truncate_text(m, 256), u"'")
64
self._send('j' + m)
65
return len(m)
66
67
def send_blob(self, blob):
68
s = uuidsha1(blob)
69
self._send('b' + s + blob)
70
return s
71
72
def send_file(self, filename):
73
#log("sending file '%s'"%filename)
74
f = open(filename, 'rb')
75
data = f.read()
76
f.close()
77
return self.send_blob(data)
78
79
def _recv(self, n):
80
for i in range(20): # see http://stackoverflow.com/questions/3016369/catching-blocking-sigint-during-system-call
81
try:
82
r = self._conn.recv(n)
83
return r
84
except socket.error as exc:
85
if isinstance(exc, socket.timeout):
86
raise
87
else:
88
(errno, msg) = exc
89
if errno != 4:
90
raise
91
raise EOFError
92
93
def recv(self):
94
n = self._recv(4)
95
if len(n) < 4:
96
print("expecting 4 byte header, got", n)
97
tries = 0
98
while tries < 5:
99
tries += 1
100
n2 = self._recv(4 - len(n))
101
n += n2
102
if len(n) >= 4:
103
break
104
else:
105
raise EOFError
106
n = struct.unpack('>L', n)[0] # big endian 32 bits
107
#print("test got header, expect message of length %s"%n)
108
s = self._recv(n)
109
while len(s) < n:
110
t = self._recv(n - len(s))
111
if len(t) == 0:
112
raise EOFError
113
s += t
114
115
if s[0] == 'j':
116
try:
117
return 'json', json.loads(s[1:])
118
except Exception as msg:
119
log("Unable to parse JSON '%s'"%s[1:])
120
raise
121
122
elif s[0] == 'b':
123
return 'blob', s[1:]
124
raise ValueError("unknown message type '%s'"%s[0])
125
126
def truncate_text(s, max_size):
127
if len(s) > max_size:
128
return s[:max_size] + "[...]", True
129
else:
130
return s, False
131
132
class Message(object):
133
def _new(self, event, props={}):
134
m = {'event':event}
135
for key, val in props.iteritems():
136
if key != 'self':
137
m[key] = val
138
return m
139
140
def start_session(self):
141
return self._new('start_session')
142
143
def session_description(self, pid):
144
return self._new('session_description', {'pid':pid})
145
146
def send_signal(self, pid, signal=signal.SIGINT):
147
return self._new('send_signal', locals())
148
149
def terminate_session(self, done=True):
150
return self._new('terminate_session', locals())
151
152
def execute_code(self, id, code, preparse=True):
153
return self._new('execute_code', locals())
154
155
def execute_javascript(self, code, obj=None, coffeescript=False):
156
return self._new('execute_javascript', locals())
157
158
def output(self, id,
159
stdout = None,
160
stderr = None,
161
code = None,
162
html = None,
163
javascript = None,
164
coffeescript = None,
165
interact = None,
166
md = None,
167
tex = None,
168
d3 = None,
169
file = None,
170
raw_input = None,
171
obj = None,
172
once = None,
173
hide = None,
174
show = None,
175
events = None,
176
clear = None,
177
delete_last = None,
178
done = False # CRITICAL: done must be specified for multi-response; this is assumed by sage_session.coffee; otherwise response assumed single.
179
):
180
m = self._new('output')
181
m['id'] = id
182
t = truncate_text_warn
183
did_truncate = False
184
import sage_server # we do this so that the user can customize the MAX's below.
185
if code is not None:
186
code['source'], did_truncate, tmsg = t(code['source'], sage_server.MAX_CODE_SIZE, 'MAX_CODE_SIZE')
187
m['code'] = code
188
if stderr is not None and len(stderr) > 0:
189
m['stderr'], did_truncate, tmsg = t(stderr, sage_server.MAX_STDERR_SIZE, 'MAX_STDERR_SIZE')
190
if stdout is not None and len(stdout) > 0:
191
m['stdout'], did_truncate, tmsg = t(stdout, sage_server.MAX_STDOUT_SIZE, 'MAX_STDOUT_SIZE')
192
if html is not None and len(html) > 0:
193
m['html'], did_truncate, tmsg = t(html, sage_server.MAX_HTML_SIZE, 'MAX_HTML_SIZE')
194
if md is not None and len(md) > 0:
195
m['md'], did_truncate, tmsg = t(md, sage_server.MAX_MD_SIZE, 'MAX_MD_SIZE')
196
if tex is not None and len(tex)>0:
197
tex['tex'], did_truncate, tmsg = t(tex['tex'], sage_server.MAX_TEX_SIZE, 'MAX_TEX_SIZE')
198
m['tex'] = tex
199
if javascript is not None: m['javascript'] = javascript
200
if coffeescript is not None: m['coffeescript'] = coffeescript
201
if interact is not None: m['interact'] = interact
202
if d3 is not None: m['d3'] = d3
203
if obj is not None: m['obj'] = json.dumps(obj)
204
if file is not None: m['file'] = file # = {'filename':..., 'uuid':...}
205
if raw_input is not None: m['raw_input'] = raw_input
206
if done is not None: m['done'] = done
207
if once is not None: m['once'] = once
208
if hide is not None: m['hide'] = hide
209
if show is not None: m['show'] = show
210
if events is not None: m['events'] = events
211
if clear is not None: m['clear'] = clear
212
if delete_last is not None: m['delete_last'] = delete_last
213
if did_truncate:
214
if 'stderr' in m:
215
m['stderr'] += '\n' + tmsg
216
else:
217
m['stderr'] = '\n' + tmsg
218
return m
219
220
def introspect_completions(self, id, completions, target):
221
m = self._new('introspect_completions', locals())
222
m['id'] = id
223
return m
224
225
def introspect_docstring(self, id, docstring, target):
226
m = self._new('introspect_docstring', locals())
227
m['id'] = id
228
return m
229
230
def introspect_source_code(self, id, source_code, target):
231
m = self._new('introspect_source_code', locals())
232
m['id'] = id
233
return m
234
235
# NOTE: these functions are NOT in sage_server.py
236
def save_blob(self, sha1):
237
return self._new('save_blob', {'sha1':sha1})
238
239
def introspect(self, id, line, top):
240
return self._new('introspect', {'id':id, 'line':line, 'top':top})
241
242
message = Message()
243
244
###
245
# end of copy region
246
###
247
248
def set_salvus_path(self, id):
249
r"""
250
create json message to set path and file at start of virtual worksheet
251
"""
252
m = self._new('execute_code', locals())
253
254
# hard code SMC for now so we don't have to run with sage wrapper
255
SMC = os.path.join(os.environ["HOME"], ".smc")
256
default_log_file = os.path.join(SMC, "sage_server", "sage_server.log")
257
default_pid_file = os.path.join(SMC, "sage_server", "sage_server.pid")
258
259
def get_sage_server_info(log_file = default_log_file):
260
for loop_count in range(3):
261
# log file ~/.smc/sage_server/sage_server.log
262
# sample sage_server startup line in first lines of log:
263
# 3136 (2016-08-18 15:02:49.372): Sage server 127.0.0.1:44483
264
try:
265
with open(log_file, "r") as inf:
266
for lno in range(5):
267
line = inf.readline().strip()
268
m = re.search("Sage server (?P<host>[\w.]+):(?P<port>\d+)$", line)
269
if m:
270
host = m.group('host')
271
port = int(m.group('port'))
272
#return host, int(port)
273
break
274
else:
275
raise ValueError('Server info not found in log_file',log_file)
276
break
277
except IOError:
278
print("starting new sage_server")
279
os.system("smc-sage-server start")
280
time.sleep(5.0)
281
else:
282
pytest.fail("Unable to open log file %s\nThere is probably no sage server running. You either have to open a sage worksheet or run smc-sage-server start"%log_file)
283
print("got host %s port %s"%(host, port))
284
return host, int(port)
285
286
secret_token = None
287
secret_token_path = os.path.join(os.environ['SMC'], 'secret_token')
288
289
if 'COCALC_SECRET_TOKEN' in os.environ:
290
secret_token_path = os.environ['COCALC_SECRET_TOKEN']
291
else:
292
secret_token_path = os.path.join(os.environ['SMC'], 'secret_token')
293
294
295
def client_unlock_connection(sock):
296
secret_token = open(secret_token_path).read().strip()
297
sock.sendall(secret_token)
298
299
def path_info():
300
file = __file__
301
full_path = os.path.abspath(file)
302
head, tail = os.path.split(full_path)
303
#file = head + "/testing.sagews"
304
return head, file
305
306
def recv_til_done(conn, test_id):
307
r"""
308
Discard json messages from server for current test_id until 'done' is True
309
or limit is reached. Used in finalizer for single cell tests.
310
"""
311
for loop_count in range(5):
312
typ, mesg = conn.recv()
313
assert typ == 'json'
314
assert mesg['id'] == test_id
315
assert 'done' in mesg
316
if mesg['done']:
317
break
318
else:
319
pytest.fail("too many responses for message id %s"%test_id)
320
###
321
# Start of fixtures
322
###
323
324
@pytest.fixture(autouse = True, scope = "session")
325
def sage_server_setup(pid_file = default_pid_file, log_file = default_log_file):
326
r"""
327
make sure sage_server pid file exists and process running at given pid
328
"""
329
print("initial fixture")
330
try:
331
pid = int(open(pid_file).read())
332
os.kill(pid, 0)
333
except:
334
assert os.geteuid() != 0, "Do not run as root."
335
os.system("pkill -f sage_server_command_line")
336
os.system("rm -f %s"%pid_file)
337
os.system("smc-sage-server start")
338
for loop_count in range(20):
339
time.sleep(0.5)
340
if not os.path.exists(log_file):
341
continue
342
lmsg = "Starting server listening for connections"
343
if lmsg in open(log_file).read():
344
break
345
else:
346
pytest.fail("Unable to start sage_server and setup log file")
347
return
348
349
@pytest.fixture()
350
def test_id(request):
351
r"""
352
Return increasing sequence of integers starting at 1. This number is used as
353
test id as well as message 'id' value so sage_server log can be matched
354
with pytest output.
355
"""
356
test_id.id += 1
357
return test_id.id
358
test_id.id = 1
359
360
# see http://doc.pytest.org/en/latest/tmpdir.html#the-tmpdir-factory-fixture
361
@pytest.fixture(scope='session')
362
def image_file(tmpdir_factory):
363
def make_img():
364
import matplotlib
365
matplotlib.use('Agg')
366
import matplotlib.pyplot as plt
367
my_circle=plt.Circle((0.5,0.5),0.2)
368
fig, ax = plt.subplots()
369
ax.add_artist(my_circle)
370
return fig
371
fn = tmpdir_factory.mktemp('data').join('my_circle.png')
372
make_img().savefig(str(fn))
373
return fn
374
375
@pytest.fixture(scope='session')
376
def data_path(tmpdir_factory):
377
path = tmpdir_factory.mktemp("data")
378
path.ensure_dir()
379
return path
380
381
@pytest.fixture()
382
def execdoc(request, sagews, test_id):
383
r"""
384
Fixture function execdoc. Depends on two other fixtures, sagews and test_id.
385
386
EXAMPLES:
387
388
::
389
390
def test_assg(execdoc):
391
execdoc("random?")
392
"""
393
def execfn(code, pattern='Docstring'):
394
m = message.execute_code(code = code, id = test_id)
395
sagews.send_json(m)
396
typ, mesg = sagews.recv()
397
assert typ == 'json'
398
assert mesg['id'] == test_id
399
assert 'code' in mesg
400
assert 'source' in mesg['code']
401
assert re.sub('\s+','',pattern) in re.sub('\s+','',mesg['code']['source'])
402
403
def fin():
404
recv_til_done(sagews, test_id)
405
406
request.addfinalizer(fin)
407
return execfn
408
409
410
@pytest.fixture()
411
def exec2(request, sagews, test_id):
412
r"""
413
Fixture function exec2. Depends on two other fixtures, sagews and test_id.
414
If output & patterns are omitted, the cell is not expected to produce a
415
stdout result. All arguments after 'code' are optional.
416
417
- `` code `` -- string of code to run
418
419
- `` output `` -- string or list of strings of output to be matched up to leading & trailing whitespace
420
421
- `` pattern `` -- regex to match with expected stdout output
422
423
- `` html_pattern `` -- regex to match with expected html output
424
425
EXAMPLES:
426
427
::
428
429
def test_assg(exec2):
430
code = "x = 42\nx\n"
431
output = "42\n"
432
exec2(code, output)
433
434
::
435
436
def test_set_file_env(exec2):
437
code = "os.chdir(salvus.data[\'path\']);__file__=salvus.data[\'file\']"
438
exec2(code)
439
440
::
441
442
def test_sh(exec2):
443
exec2("sh('date +%Y-%m-%d')", pattern = '^\d{4}-\d{2}-\d{2}$')
444
445
.. NOTE::
446
447
If `output` is a list of strings, `pattern` and `html_pattern` are ignored
448
449
"""
450
def execfn(code, output = None, pattern = None, html_pattern = None):
451
m = message.execute_code(code = code, id = test_id)
452
m['preparse'] = True
453
# send block of code to be executed
454
sagews.send_json(m)
455
456
# check stdout
457
if isinstance(output, list):
458
for o in output:
459
typ, mesg = sagews.recv()
460
assert typ == 'json'
461
assert mesg['id'] == test_id
462
assert 'stdout' in mesg
463
assert o.strip() in (mesg['stdout']).strip()
464
elif output or pattern:
465
typ, mesg = sagews.recv()
466
assert typ == 'json'
467
assert mesg['id'] == test_id
468
assert 'stdout' in mesg
469
mout = mesg['stdout']
470
if output is not None:
471
assert output.strip() in mout
472
elif pattern is not None:
473
assert re.search(pattern, mout) is not None
474
elif html_pattern:
475
typ, mesg = sagews.recv()
476
assert typ == 'json'
477
assert mesg['id'] == test_id
478
assert 'html' in mesg
479
assert re.search(html_pattern, mesg['html']) is not None
480
481
def fin():
482
recv_til_done(sagews, test_id)
483
484
request.addfinalizer(fin)
485
return execfn
486
487
@pytest.fixture()
488
def execinteract(request, sagews, test_id):
489
def execfn(code):
490
m = message.execute_code(code = code, id = test_id)
491
m['preparse'] = True
492
sagews.send_json(m)
493
typ, mesg = sagews.recv()
494
assert typ == 'json'
495
assert mesg['id'] == test_id
496
assert 'interact' in mesg
497
498
def fin():
499
recv_til_done(sagews, test_id)
500
501
request.addfinalizer(fin)
502
return execfn
503
504
505
@pytest.fixture()
506
def execblob(request, sagews, test_id):
507
508
def execblobfn(code, want_html=True, want_javascript=False, file_type = 'png', ignore_stdout=False):
509
510
SHA_LEN = 36
511
512
# format and send the plot command
513
m = message.execute_code(code = code, id = test_id)
514
sagews.send_json(m)
515
516
# expect several responses before "done", but order may vary
517
want_blob = True
518
want_name = True
519
while any([want_blob, want_name, want_html, want_javascript]):
520
typ, mesg = sagews.recv()
521
if typ == 'blob':
522
assert want_blob
523
want_blob = False
524
# when a blob is sent, the first 36 bytes are the sha1 uuid
525
print("blob len %s"%len(mesg))
526
file_uuid = mesg[:SHA_LEN]
527
assert file_uuid == uuidsha1(mesg[SHA_LEN:])
528
529
# sage_server expects an ack with the right uuid
530
m = message.save_blob(sha1 = file_uuid)
531
sagews.send_json(m)
532
else:
533
assert typ == 'json'
534
if 'html' in mesg:
535
assert want_html
536
want_html = False
537
print('got html')
538
elif 'javascript' in mesg:
539
assert want_javascript
540
want_javascript = False
541
print('got javascript')
542
elif ignore_stdout and 'stdout' in mesg:
543
pass
544
else:
545
assert want_name
546
want_name = False
547
assert 'file' in mesg
548
print('got file name')
549
assert file_type in mesg['file']['filename']
550
551
# final response is json "done" message
552
typ, mesg = sagews.recv()
553
assert typ == 'json'
554
assert mesg['done'] == True
555
556
return execblobfn
557
558
@pytest.fixture()
559
def execintrospect(request, sagews, test_id):
560
def execfn(line, completions, target, top=None):
561
if top is None:
562
top = line
563
m = message.introspect(test_id, line=line, top=top)
564
m['preparse'] = True
565
sagews.send_json(m)
566
typ, mesg = sagews.recv()
567
assert typ == 'json'
568
assert mesg['id'] == test_id
569
assert mesg['event'] == "introspect_completions"
570
assert mesg['completions'] == completions
571
assert mesg['target'] == target
572
573
return execfn
574
575
@pytest.fixture(scope = "class")
576
def sagews(request):
577
r"""
578
Module-scoped fixture for tests that don't leave
579
extra threads running.
580
"""
581
# setup connection to sage_server TCP listener
582
host, port = get_sage_server_info()
583
print("host %s port %s"%(host, port))
584
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
585
sock.connect((host, port))
586
# jupyter kernels can take over 10 seconds to start
587
sock.settimeout(45)
588
print("connected to socket")
589
590
# unlock
591
client_unlock_connection(sock)
592
print("socket unlocked")
593
conn = ConnectionJSON(sock)
594
c_ack = conn._recv(1)
595
assert c_ack == 'y',"expect ack for token, got %s"%c_ack
596
597
# open connection with sage_server and run tests
598
msg = message.start_session()
599
msg['type'] = 'sage'
600
conn.send_json(msg)
601
print("start_session sent")
602
typ, mesg = conn.recv()
603
assert typ == 'json'
604
pid = mesg['pid']
605
print("sage_server PID = %s" % pid)
606
607
# teardown needed - terminate session nicely
608
# use yield instead of request.addfinalizer in newer versions of pytest
609
def fin():
610
print("\nExiting Sage client.")
611
conn.send_json(message.terminate_session())
612
# wait several seconds for client to die
613
for loop_count in range(8):
614
try:
615
os.kill(pid, 0)
616
except OSError:
617
# client is dead
618
break
619
time.sleep(0.5)
620
else:
621
print("sending sigterm to %s"%pid)
622
try:
623
os.kill(pid, signal.SIGTERM)
624
except OSError:
625
pass
626
request.addfinalizer(fin)
627
return conn
628
629
import time
630
631
@pytest.fixture(scope = "class")
632
def own_sage_server(request):
633
assert os.geteuid() != 0, "Do not run as root, will kill all sage_servers."
634
#os.system("pkill -f sage_server_command_line")
635
print("starting new sage_server")
636
os.system("smc-sage-server start")
637
time.sleep(0.5)
638
def fin():
639
print("killing all sage_server processes")
640
os.system("pkill -f sage_server_command_line")
641
request.addfinalizer(fin)
642
643
@pytest.fixture(scope = "class")
644
def test_ro_data_dir(request):
645
"""
646
Return the directory containing the test file.
647
Used for tests which have read-only data files in the test dir.
648
"""
649
return os.path.dirname(request.module.__file__)
650
651
#
652
# Write machine-readable report files into the $HOME directory
653
# http://doc.pytest.org/en/latest/example/simple.html#post-process-test-reports-failures
654
#
655
import os
656
report_json = os.path.expanduser('~/sagews-test-report.json')
657
report_prom = os.path.expanduser('~/sagews-test-report.prom')
658
results = []
659
start_time = None
660
661
@pytest.hookimpl
662
def pytest_configure(config):
663
global start_time
664
start_time = datetime.utcnow()
665
666
@pytest.hookimpl
667
def pytest_unconfigure(config):
668
global start_time
669
data = {
670
'name' : 'smc_sagews.test',
671
'version' : 1,
672
'start' : str(start_time),
673
'end' : str(datetime.utcnow()),
674
'fields' : ['name', 'outcome', 'duration'],
675
'results' : results,
676
}
677
with open(report_json, 'w') as out:
678
json.dump(data, out, indent=1)
679
# this is a plain text prometheus report
680
# https://prometheus.io/docs/instrumenting/exposition_formats/#text-format-details
681
# timestamp milliseconds since epoch
682
ts = int(1000 * time.mktime(start_time.timetuple()))
683
# first write to temp file ...
684
report_prom_tmp = report_prom + '~'
685
with open(report_prom_tmp, 'w') as prom:
686
for (name, outcome, duration) in results:
687
labels = 'name="{name}",outcome="{outcome}"'.format(**locals())
688
line = 'sagews_test{{{labels}}} {duration} {ts}'.format(**locals())
689
prom.write(line + '\n')
690
# ... then atomically overwrite the real one
691
os.rename(report_prom_tmp, report_prom)
692
693
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
694
def pytest_runtest_makereport(item, call):
695
# execute all other hooks to obtain the report object
696
outcome = yield
697
rep = outcome.get_result()
698
699
if rep.when != "call":
700
return
701
702
#import pdb; pdb.set_trace() # uncomment to inspect item and rep objects
703
# the following `res` should match the `fields` above
704
# parent: item.parent.name could be interesting, but just () for auto discovery
705
name = item.name
706
test_ = 'test_'
707
if name.startswith(test_):
708
name = name[len(test_):]
709
res = [name, rep.outcome, rep.duration]
710
results.append(res)
711
712