zprocess 2.26.0__tar.gz → 2.26.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {zprocess-2.26.0/zprocess.egg-info → zprocess-2.26.1}/PKG-INFO +2 -2
- {zprocess-2.26.0 → zprocess-2.26.1}/pyproject.toml +1 -1
- {zprocess-2.26.0 → zprocess-2.26.1}/tests/test_zprocess.py +513 -510
- {zprocess-2.26.0 → zprocess-2.26.1/zprocess.egg-info}/PKG-INFO +2 -2
- {zprocess-2.26.0 → zprocess-2.26.1}/.github/workflows/release.yml +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/.gitignore +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/LICENSE.txt +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/README.md +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/doc/Makefile +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/doc/conf.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/doc/index.rst +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/doc/requirements.txt +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/pytest.ini +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/requirements.txt +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/run_tests.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/setup.cfg +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/tests/GUI_output_redirection.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/tests/output_redirection.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/tests/test_remote.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/tests/test_zlock_server.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/tests/test_zlog_server.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/__init__.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/__version__.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/clientserver.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/cryptoproxy.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/examples/example_child.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/examples/example_parent.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/examples/processclass_example.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/examples/processclass_remote_example.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/locking/__init__.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/locking/__main__.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/makesecret.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/process_class_wrapper.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/process_tree.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/remote/__init__.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/remote/__main__.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/remote/curses_server.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/remote/server.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/security.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/socks/__init__.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/socks/server.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/tasks.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/utils.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/zlock/__init__.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/zlock/__main__.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/zlock/server.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/zlog/__init__.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/zlog/__main__.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/zlog/server.py +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess.egg-info/SOURCES.txt +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess.egg-info/dependency_links.txt +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess.egg-info/requires.txt +0 -0
- {zprocess-2.26.0 → zprocess-2.26.1}/zprocess.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: zprocess
|
|
3
|
-
Version: 2.26.
|
|
3
|
+
Version: 2.26.1
|
|
4
4
|
Summary: A set of utilities for multiprocessing using zeromq
|
|
5
5
|
Author-email: Christopher Billington <chrisjbillington@gmail.com>
|
|
6
6
|
License: Copyright (c) 2013, Christopher Billington
|
|
@@ -34,7 +34,7 @@ Project-URL: Tracker, https://github.com/chrisjbillington/zprocess/issues
|
|
|
34
34
|
Keywords: multiprocessing,IPC,zeromq
|
|
35
35
|
Classifier: License :: OSI Approved :: BSD License
|
|
36
36
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
37
|
-
Requires-Python: >=3.
|
|
37
|
+
Requires-Python: >=3.8
|
|
38
38
|
Description-Content-Type: text/markdown
|
|
39
39
|
License-File: LICENSE.txt
|
|
40
40
|
Requires-Dist: setuptools_scm
|
|
@@ -1,510 +1,513 @@
|
|
|
1
|
-
import unittest
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
import sys
|
|
4
|
-
import os
|
|
5
|
-
import time
|
|
6
|
-
import threading
|
|
7
|
-
|
|
8
|
-
import zmq
|
|
9
|
-
import pytest
|
|
10
|
-
|
|
11
|
-
THIS_DIR = Path(__file__).absolute().parent
|
|
12
|
-
|
|
13
|
-
# Add project root to import path
|
|
14
|
-
PROJECT_ROOT = THIS_DIR.parent
|
|
15
|
-
if PROJECT_ROOT not in [Path(s).absolute() for s in sys.path]:
|
|
16
|
-
sys.path.insert(0, str(PROJECT_ROOT))
|
|
17
|
-
|
|
18
|
-
import zprocess
|
|
19
|
-
zprocess._silent = True
|
|
20
|
-
from zprocess import (ZMQServer, Process, TimeoutError, RichStreamHandler, rich_print,
|
|
21
|
-
raise_exception_in_thread, zmq_get, zmq_push, zmq_get_raw,
|
|
22
|
-
ExternalBroker)
|
|
23
|
-
import zprocess.clientserver as clientserver
|
|
24
|
-
from zprocess.clientserver import _typecheck_or_convert_data
|
|
25
|
-
from zprocess.process_tree import _default_process_tree, EventBroker
|
|
26
|
-
shared_secret = _default_process_tree.shared_secret
|
|
27
|
-
from zprocess.security import SecureContext
|
|
28
|
-
from zprocess.tasks import Task, TaskQueue
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class TestError(Exception):
|
|
32
|
-
pass
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class RaiseExceptionInThreadTest(unittest.TestCase):
|
|
36
|
-
|
|
37
|
-
def setUp(self):
|
|
38
|
-
# Mock threading.Thread to just run a function in the main thread:
|
|
39
|
-
class MockThread(object):
|
|
40
|
-
used = False
|
|
41
|
-
def __init__(self, target, args):
|
|
42
|
-
self.target = target
|
|
43
|
-
self.args = args
|
|
44
|
-
def start(self):
|
|
45
|
-
MockThread.used = True
|
|
46
|
-
self.target(*self.args)
|
|
47
|
-
self.mock_thread = MockThread
|
|
48
|
-
self.orig_thread = threading.Thread
|
|
49
|
-
threading.Thread = MockThread
|
|
50
|
-
|
|
51
|
-
def test_can_raise_exception_in_thread(self):
|
|
52
|
-
try:
|
|
53
|
-
raise TestError('test')
|
|
54
|
-
except Exception:
|
|
55
|
-
exc_info = sys.exc_info()
|
|
56
|
-
with self.assertRaises(TestError):
|
|
57
|
-
raise_exception_in_thread(exc_info)
|
|
58
|
-
self.assertTrue(self.mock_thread.used)
|
|
59
|
-
|
|
60
|
-
def tearDown(self):
|
|
61
|
-
# Restore threading.Thread to what it should be
|
|
62
|
-
threading.Thread = self.orig_thread
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
class TypeCheckConvertTests(unittest.TestCase):
|
|
66
|
-
"""test the _typecheck_or_convert_data function"""
|
|
67
|
-
|
|
68
|
-
def test_turns_None_into_empty_bytestring_raw(self):
|
|
69
|
-
result = _typecheck_or_convert_data(None, 'raw')
|
|
70
|
-
self.assertEqual(result, b'')
|
|
71
|
-
|
|
72
|
-
def test_turns_None_into_empty_bytestring_multipart(self):
|
|
73
|
-
result = _typecheck_or_convert_data(None, 'multipart')
|
|
74
|
-
self.assertEqual(result, [b''])
|
|
75
|
-
|
|
76
|
-
def test_wraps_bytestring_into_list_multipart(self):
|
|
77
|
-
data = b'spam'
|
|
78
|
-
result = _typecheck_or_convert_data(data, 'multipart')
|
|
79
|
-
self.assertEqual(result, [data])
|
|
80
|
-
|
|
81
|
-
def test_accepts_bytes_raw(self):
|
|
82
|
-
data = b'spam'
|
|
83
|
-
result = _typecheck_or_convert_data(data, 'raw')
|
|
84
|
-
self.assertEqual(result, data)
|
|
85
|
-
|
|
86
|
-
def test_accepts_list_of_bytes_multipart(self):
|
|
87
|
-
data = [b'spam', b'ham']
|
|
88
|
-
result = _typecheck_or_convert_data(data, 'multipart')
|
|
89
|
-
self.assertEqual(result, data)
|
|
90
|
-
|
|
91
|
-
def test_accepts_string_string(self):
|
|
92
|
-
data = 'spam'
|
|
93
|
-
result = _typecheck_or_convert_data(data, 'string')
|
|
94
|
-
self.assertEqual(result, data)
|
|
95
|
-
|
|
96
|
-
def test_accepts_pyobj_pyobj(self):
|
|
97
|
-
data = {'spam': ['ham'], 'eggs': True}
|
|
98
|
-
result = _typecheck_or_convert_data(data, 'pyobj')
|
|
99
|
-
self.assertEqual(result, data)
|
|
100
|
-
|
|
101
|
-
def test_rejects_string_raw(self):
|
|
102
|
-
data = 'spam'
|
|
103
|
-
with self.assertRaises(TypeError):
|
|
104
|
-
_typecheck_or_convert_data(data, 'raw')
|
|
105
|
-
|
|
106
|
-
def test_rejects_string_multipart(self):
|
|
107
|
-
data = [b'spam', 'ham']
|
|
108
|
-
with self.assertRaises(TypeError):
|
|
109
|
-
_typecheck_or_convert_data(data, 'multipart')
|
|
110
|
-
|
|
111
|
-
def test_rejects_pyobj_string(self):
|
|
112
|
-
data = {'spam': ['ham'], 'eggs': True}
|
|
113
|
-
with self.assertRaises(TypeError):
|
|
114
|
-
_typecheck_or_convert_data(data, 'string')
|
|
115
|
-
|
|
116
|
-
def test_rejects_invalid_send_type(self):
|
|
117
|
-
data = {'spam': ['ham'], 'eggs': True}
|
|
118
|
-
with self.assertRaises(ValueError):
|
|
119
|
-
_typecheck_or_convert_data(data, 'invalid_send_type')
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
class TestProcess(Process):
|
|
123
|
-
def run(self):
|
|
124
|
-
item = self.from_parent.get()
|
|
125
|
-
x, y = item
|
|
126
|
-
sys.stdout.write(repr(x))
|
|
127
|
-
sys.stderr.write(y)
|
|
128
|
-
self.to_parent.put(item)
|
|
129
|
-
os.system('echo hello from echo')
|
|
130
|
-
# Wait some time here to reduce the chance of output arriving out of order,
|
|
131
|
-
# which would break the tests even though we don't actually guarantee that the
|
|
132
|
-
# order will come out right.
|
|
133
|
-
time.sleep(0.2)
|
|
134
|
-
|
|
135
|
-
# And now test logging:
|
|
136
|
-
import logging
|
|
137
|
-
logger = logging.Logger('test')
|
|
138
|
-
logger.setLevel(logging.DEBUG)
|
|
139
|
-
logger.addHandler(RichStreamHandler())
|
|
140
|
-
logger.info('this is a log message')
|
|
141
|
-
|
|
142
|
-
# And now test rich printing
|
|
143
|
-
rich_print('some test text', color='#123456', bold=True, italic=False)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
class ProcessClassTests(unittest.TestCase):
|
|
148
|
-
|
|
149
|
-
def setUp(self):
|
|
150
|
-
"""Create a subprocess with output redirection to a zmq port"""
|
|
151
|
-
context = SecureContext.instance(shared_secret=shared_secret)
|
|
152
|
-
self.redirection_sock = context.socket(zmq.PULL)
|
|
153
|
-
redirection_port = self.redirection_sock.bind_to_random_port(
|
|
154
|
-
'tcp://127.0.0.1')
|
|
155
|
-
self.process = TestProcess(output_redirection_port=redirection_port)
|
|
156
|
-
|
|
157
|
-
def test_process(self):
|
|
158
|
-
to_child, from_child = self.process.start()
|
|
159
|
-
# Check the child process is running:
|
|
160
|
-
self.assertIs(self.process.child.poll(), None)
|
|
161
|
-
|
|
162
|
-
# Send some data:
|
|
163
|
-
x = [('spam', ['ham']), ('eggs', True)]
|
|
164
|
-
y = 'über'
|
|
165
|
-
data = (x, y)
|
|
166
|
-
to_child.put(data)
|
|
167
|
-
|
|
168
|
-
# Subprocess should send back the data unmodified:
|
|
169
|
-
recv_data = from_child.get(timeout=1)
|
|
170
|
-
self.assertEqual(recv_data, data)
|
|
171
|
-
|
|
172
|
-
# Subprocess should not send any more data, expect TimeoutError:
|
|
173
|
-
with self.assertRaises(TimeoutError):
|
|
174
|
-
from_child.get(timeout=0.1)
|
|
175
|
-
|
|
176
|
-
# Check we recieved its stdout and stderr:
|
|
177
|
-
self.assertEqual(self.redirection_sock.poll(1000), zmq.POLLIN)
|
|
178
|
-
self.assertEqual(self.redirection_sock.recv_multipart(),
|
|
179
|
-
[b'stdout', repr(x).encode('utf8')])
|
|
180
|
-
self.assertEqual(self.redirection_sock.poll(1000), zmq.POLLIN)
|
|
181
|
-
self.assertEqual(self.redirection_sock.recv_multipart(),
|
|
182
|
-
[b'stderr', y.encode('utf8')])
|
|
183
|
-
# And the shell output:
|
|
184
|
-
self.assertEqual(self.redirection_sock.poll(1000), zmq.POLLIN)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
#
|
|
199
|
-
self.assertEqual(
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
self.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
sock.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
sock.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
self.
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
self.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
#
|
|
286
|
-
self.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
self.
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
self.
|
|
312
|
-
self.
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
self.
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
self.assertIs(queue
|
|
383
|
-
|
|
384
|
-
#
|
|
385
|
-
queue.
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
server = MyServer(port=None,
|
|
427
|
-
bind_address='tcp://127.0.0.1')
|
|
428
|
-
elif argname == '
|
|
429
|
-
server = MyServer(None, 'raw',
|
|
430
|
-
bind_address='tcp://127.0.0.1')
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
self.
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
def
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
1
|
+
import unittest
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
import threading
|
|
7
|
+
|
|
8
|
+
import zmq
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
THIS_DIR = Path(__file__).absolute().parent
|
|
12
|
+
|
|
13
|
+
# Add project root to import path
|
|
14
|
+
PROJECT_ROOT = THIS_DIR.parent
|
|
15
|
+
if PROJECT_ROOT not in [Path(s).absolute() for s in sys.path]:
|
|
16
|
+
sys.path.insert(0, str(PROJECT_ROOT))
|
|
17
|
+
|
|
18
|
+
import zprocess
|
|
19
|
+
zprocess._silent = True
|
|
20
|
+
from zprocess import (ZMQServer, Process, TimeoutError, RichStreamHandler, rich_print,
|
|
21
|
+
raise_exception_in_thread, zmq_get, zmq_push, zmq_get_raw,
|
|
22
|
+
ExternalBroker)
|
|
23
|
+
import zprocess.clientserver as clientserver
|
|
24
|
+
from zprocess.clientserver import _typecheck_or_convert_data
|
|
25
|
+
from zprocess.process_tree import _default_process_tree, EventBroker
|
|
26
|
+
shared_secret = _default_process_tree.shared_secret
|
|
27
|
+
from zprocess.security import SecureContext
|
|
28
|
+
from zprocess.tasks import Task, TaskQueue
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TestError(Exception):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RaiseExceptionInThreadTest(unittest.TestCase):
|
|
36
|
+
|
|
37
|
+
def setUp(self):
|
|
38
|
+
# Mock threading.Thread to just run a function in the main thread:
|
|
39
|
+
class MockThread(object):
|
|
40
|
+
used = False
|
|
41
|
+
def __init__(self, target, args):
|
|
42
|
+
self.target = target
|
|
43
|
+
self.args = args
|
|
44
|
+
def start(self):
|
|
45
|
+
MockThread.used = True
|
|
46
|
+
self.target(*self.args)
|
|
47
|
+
self.mock_thread = MockThread
|
|
48
|
+
self.orig_thread = threading.Thread
|
|
49
|
+
threading.Thread = MockThread
|
|
50
|
+
|
|
51
|
+
def test_can_raise_exception_in_thread(self):
|
|
52
|
+
try:
|
|
53
|
+
raise TestError('test')
|
|
54
|
+
except Exception:
|
|
55
|
+
exc_info = sys.exc_info()
|
|
56
|
+
with self.assertRaises(TestError):
|
|
57
|
+
raise_exception_in_thread(exc_info)
|
|
58
|
+
self.assertTrue(self.mock_thread.used)
|
|
59
|
+
|
|
60
|
+
def tearDown(self):
|
|
61
|
+
# Restore threading.Thread to what it should be
|
|
62
|
+
threading.Thread = self.orig_thread
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TypeCheckConvertTests(unittest.TestCase):
|
|
66
|
+
"""test the _typecheck_or_convert_data function"""
|
|
67
|
+
|
|
68
|
+
def test_turns_None_into_empty_bytestring_raw(self):
|
|
69
|
+
result = _typecheck_or_convert_data(None, 'raw')
|
|
70
|
+
self.assertEqual(result, b'')
|
|
71
|
+
|
|
72
|
+
def test_turns_None_into_empty_bytestring_multipart(self):
|
|
73
|
+
result = _typecheck_or_convert_data(None, 'multipart')
|
|
74
|
+
self.assertEqual(result, [b''])
|
|
75
|
+
|
|
76
|
+
def test_wraps_bytestring_into_list_multipart(self):
|
|
77
|
+
data = b'spam'
|
|
78
|
+
result = _typecheck_or_convert_data(data, 'multipart')
|
|
79
|
+
self.assertEqual(result, [data])
|
|
80
|
+
|
|
81
|
+
def test_accepts_bytes_raw(self):
|
|
82
|
+
data = b'spam'
|
|
83
|
+
result = _typecheck_or_convert_data(data, 'raw')
|
|
84
|
+
self.assertEqual(result, data)
|
|
85
|
+
|
|
86
|
+
def test_accepts_list_of_bytes_multipart(self):
|
|
87
|
+
data = [b'spam', b'ham']
|
|
88
|
+
result = _typecheck_or_convert_data(data, 'multipart')
|
|
89
|
+
self.assertEqual(result, data)
|
|
90
|
+
|
|
91
|
+
def test_accepts_string_string(self):
|
|
92
|
+
data = 'spam'
|
|
93
|
+
result = _typecheck_or_convert_data(data, 'string')
|
|
94
|
+
self.assertEqual(result, data)
|
|
95
|
+
|
|
96
|
+
def test_accepts_pyobj_pyobj(self):
|
|
97
|
+
data = {'spam': ['ham'], 'eggs': True}
|
|
98
|
+
result = _typecheck_or_convert_data(data, 'pyobj')
|
|
99
|
+
self.assertEqual(result, data)
|
|
100
|
+
|
|
101
|
+
def test_rejects_string_raw(self):
|
|
102
|
+
data = 'spam'
|
|
103
|
+
with self.assertRaises(TypeError):
|
|
104
|
+
_typecheck_or_convert_data(data, 'raw')
|
|
105
|
+
|
|
106
|
+
def test_rejects_string_multipart(self):
|
|
107
|
+
data = [b'spam', 'ham']
|
|
108
|
+
with self.assertRaises(TypeError):
|
|
109
|
+
_typecheck_or_convert_data(data, 'multipart')
|
|
110
|
+
|
|
111
|
+
def test_rejects_pyobj_string(self):
|
|
112
|
+
data = {'spam': ['ham'], 'eggs': True}
|
|
113
|
+
with self.assertRaises(TypeError):
|
|
114
|
+
_typecheck_or_convert_data(data, 'string')
|
|
115
|
+
|
|
116
|
+
def test_rejects_invalid_send_type(self):
|
|
117
|
+
data = {'spam': ['ham'], 'eggs': True}
|
|
118
|
+
with self.assertRaises(ValueError):
|
|
119
|
+
_typecheck_or_convert_data(data, 'invalid_send_type')
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class TestProcess(Process):
|
|
123
|
+
def run(self):
|
|
124
|
+
item = self.from_parent.get()
|
|
125
|
+
x, y = item
|
|
126
|
+
sys.stdout.write(repr(x))
|
|
127
|
+
sys.stderr.write(y)
|
|
128
|
+
self.to_parent.put(item)
|
|
129
|
+
os.system('echo hello from echo')
|
|
130
|
+
# Wait some time here to reduce the chance of output arriving out of order,
|
|
131
|
+
# which would break the tests even though we don't actually guarantee that the
|
|
132
|
+
# order will come out right.
|
|
133
|
+
time.sleep(0.2)
|
|
134
|
+
|
|
135
|
+
# And now test logging:
|
|
136
|
+
import logging
|
|
137
|
+
logger = logging.Logger('test')
|
|
138
|
+
logger.setLevel(logging.DEBUG)
|
|
139
|
+
logger.addHandler(RichStreamHandler())
|
|
140
|
+
logger.info('this is a log message')
|
|
141
|
+
|
|
142
|
+
# And now test rich printing
|
|
143
|
+
rich_print('some test text', color='#123456', bold=True, italic=False)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class ProcessClassTests(unittest.TestCase):
|
|
148
|
+
|
|
149
|
+
def setUp(self):
|
|
150
|
+
"""Create a subprocess with output redirection to a zmq port"""
|
|
151
|
+
context = SecureContext.instance(shared_secret=shared_secret)
|
|
152
|
+
self.redirection_sock = context.socket(zmq.PULL)
|
|
153
|
+
redirection_port = self.redirection_sock.bind_to_random_port(
|
|
154
|
+
'tcp://127.0.0.1')
|
|
155
|
+
self.process = TestProcess(output_redirection_port=redirection_port)
|
|
156
|
+
|
|
157
|
+
def test_process(self):
|
|
158
|
+
to_child, from_child = self.process.start()
|
|
159
|
+
# Check the child process is running:
|
|
160
|
+
self.assertIs(self.process.child.poll(), None)
|
|
161
|
+
|
|
162
|
+
# Send some data:
|
|
163
|
+
x = [('spam', ['ham']), ('eggs', True)]
|
|
164
|
+
y = 'über'
|
|
165
|
+
data = (x, y)
|
|
166
|
+
to_child.put(data)
|
|
167
|
+
|
|
168
|
+
# Subprocess should send back the data unmodified:
|
|
169
|
+
recv_data = from_child.get(timeout=1)
|
|
170
|
+
self.assertEqual(recv_data, data)
|
|
171
|
+
|
|
172
|
+
# Subprocess should not send any more data, expect TimeoutError:
|
|
173
|
+
with self.assertRaises(TimeoutError):
|
|
174
|
+
from_child.get(timeout=0.1)
|
|
175
|
+
|
|
176
|
+
# Check we recieved its stdout and stderr:
|
|
177
|
+
self.assertEqual(self.redirection_sock.poll(1000), zmq.POLLIN)
|
|
178
|
+
self.assertEqual(self.redirection_sock.recv_multipart(),
|
|
179
|
+
[b'stdout', repr(x).encode('utf8')])
|
|
180
|
+
self.assertEqual(self.redirection_sock.poll(1000), zmq.POLLIN)
|
|
181
|
+
self.assertEqual(self.redirection_sock.recv_multipart(),
|
|
182
|
+
[b'stderr', y.encode('utf8')])
|
|
183
|
+
# And the shell output:
|
|
184
|
+
self.assertEqual(self.redirection_sock.poll(1000), zmq.POLLIN)
|
|
185
|
+
|
|
186
|
+
self.assertEqual(
|
|
187
|
+
[p.strip() for p in self.redirection_sock.recv_multipart()],
|
|
188
|
+
[b'stdout', b'hello from echo']
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# And the formatted logging:
|
|
192
|
+
self.assertEqual(self.redirection_sock.recv_multipart(),
|
|
193
|
+
[b'INFO', b'this is a log message\n'])
|
|
194
|
+
|
|
195
|
+
# And the formatted printing:
|
|
196
|
+
import ast
|
|
197
|
+
charfmt_repr, text = self.redirection_sock.recv_multipart()
|
|
198
|
+
self.assertEqual(ast.literal_eval(charfmt_repr.decode('utf8')), ('#123456', True, False))
|
|
199
|
+
self.assertEqual(text, b'some test text\n')
|
|
200
|
+
|
|
201
|
+
# And no more...
|
|
202
|
+
self.assertEqual(self.redirection_sock.poll(100), 0)
|
|
203
|
+
|
|
204
|
+
def tearDown(self):
|
|
205
|
+
self.process.terminate()
|
|
206
|
+
self.redirection_sock.close()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class HeartbeatClientTestProcess(Process):
|
|
210
|
+
"""For testing that subprocesses are behaving correcly re. heartbeats"""
|
|
211
|
+
def run(self):
|
|
212
|
+
self.from_parent.get()
|
|
213
|
+
# If the parent sends a message, acquire the kill lock for 3 seconds:
|
|
214
|
+
with self.kill_lock:
|
|
215
|
+
time.sleep(3)
|
|
216
|
+
time.sleep(10)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class HeartbeatServerTestProcess(Process):
|
|
220
|
+
"""For testing that parent processes are behaving correcly re. heartbeats"""
|
|
221
|
+
def run(self):
|
|
222
|
+
# We'll send heartbeats of our own, independent of the HeartbeatClient
|
|
223
|
+
# already running in this process:
|
|
224
|
+
shared_secret = self.process_tree.shared_secret
|
|
225
|
+
context = SecureContext.instance(shared_secret=shared_secret)
|
|
226
|
+
sock = context.socket(zmq.REQ)
|
|
227
|
+
sock.setsockopt(zmq.LINGER, 0)
|
|
228
|
+
server_port = self.from_parent.get()
|
|
229
|
+
sock.connect('tcp://127.0.0.1:%s' % server_port)
|
|
230
|
+
# Send a heartbeat with whatever data:
|
|
231
|
+
data = b'heartbeat_data'
|
|
232
|
+
sock.send(data)
|
|
233
|
+
if sock.poll(1000):
|
|
234
|
+
response = sock.recv()
|
|
235
|
+
# Tell the parent whether things were as expected:
|
|
236
|
+
if response == data:
|
|
237
|
+
self.to_parent.put(True)
|
|
238
|
+
time.sleep(1) # Ensure it sends before we return
|
|
239
|
+
return
|
|
240
|
+
self.to_parent.put(False)
|
|
241
|
+
time.sleep(1) # Ensure it sends before we return
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class HeartbeatTests(unittest.TestCase):
|
|
245
|
+
def setUp(self):
|
|
246
|
+
"""Create a sock for output redirection and a zmq port to mock a heartbeat
|
|
247
|
+
server"""
|
|
248
|
+
import zmq
|
|
249
|
+
context = SecureContext.instance(shared_secret=shared_secret)
|
|
250
|
+
self.heartbeat_sock = context.socket(zmq.REP)
|
|
251
|
+
heartbeat_port = self.heartbeat_sock.bind_to_random_port('tcp://127.0.0.1')
|
|
252
|
+
|
|
253
|
+
class mock_heartbeat_server(object):
|
|
254
|
+
port = heartbeat_port
|
|
255
|
+
|
|
256
|
+
self.mock_heartbeat_server = mock_heartbeat_server
|
|
257
|
+
|
|
258
|
+
def test_subproc_lives_with_heartbeats(self):
|
|
259
|
+
_default_process_tree.heartbeat_server = self.mock_heartbeat_server
|
|
260
|
+
self.process = HeartbeatClientTestProcess()
|
|
261
|
+
self.process.start()
|
|
262
|
+
for i in range(3):
|
|
263
|
+
# Wait for a heartbeat request:
|
|
264
|
+
self.assertTrue(self.heartbeat_sock.poll(3000))
|
|
265
|
+
# Echo it back:
|
|
266
|
+
self.heartbeat_sock.send(self.heartbeat_sock.recv())
|
|
267
|
+
|
|
268
|
+
def test_subproc_dies_without_heartbeats(self):
|
|
269
|
+
_default_process_tree.heartbeat_server = self.mock_heartbeat_server
|
|
270
|
+
self.process = HeartbeatClientTestProcess()
|
|
271
|
+
self.process.start()
|
|
272
|
+
# Wait for a heartbeat request:
|
|
273
|
+
self.assertTrue(self.heartbeat_sock.poll(3000))
|
|
274
|
+
# Don't respond to it
|
|
275
|
+
time.sleep(2)
|
|
276
|
+
# Process should be dead:
|
|
277
|
+
self.assertIsNot(self.process.child.poll(), None)
|
|
278
|
+
|
|
279
|
+
def test_subproc_dies_on_incorrect_response(self):
|
|
280
|
+
_default_process_tree.heartbeat_server = self.mock_heartbeat_server
|
|
281
|
+
self.process = HeartbeatClientTestProcess()
|
|
282
|
+
self.process.start()
|
|
283
|
+
# Wait for a heartbeat request:
|
|
284
|
+
self.assertTrue(self.heartbeat_sock.poll(3000))
|
|
285
|
+
# Echo it back wrongly:
|
|
286
|
+
self.heartbeat_sock.send(self.heartbeat_sock.recv() + b'wrong')
|
|
287
|
+
time.sleep(1)
|
|
288
|
+
# Process should be dead:
|
|
289
|
+
self.assertIsNot(self.process.child.poll(), None)
|
|
290
|
+
|
|
291
|
+
def test_subproc_survives_until_kill_lock_released(self):
|
|
292
|
+
_default_process_tree.heartbeat_server = self.mock_heartbeat_server
|
|
293
|
+
self.process = HeartbeatClientTestProcess()
|
|
294
|
+
to_child, from_child = self.process.start()
|
|
295
|
+
# Wait for a heartbeat request:
|
|
296
|
+
self.assertTrue(self.heartbeat_sock.poll(3000))
|
|
297
|
+
# Tell child to acquire kill lock for 3 sec:
|
|
298
|
+
to_child.put(None)
|
|
299
|
+
# Don't respond to the heartbeat, process should still be alive 2 sec later
|
|
300
|
+
time.sleep(2)
|
|
301
|
+
# Process should be alive:
|
|
302
|
+
self.assertIs(self.process.child.poll(), None)
|
|
303
|
+
# After kill lock released, child should be terminated:
|
|
304
|
+
time.sleep(2)
|
|
305
|
+
# Process should be dead:
|
|
306
|
+
self.assertIsNot(self.process.child.poll(), None)
|
|
307
|
+
|
|
308
|
+
def test_parent_correctly_responds_to_heartbeats(self):
|
|
309
|
+
# No mock server this time, we're testing the real one:
|
|
310
|
+
_default_process_tree.heartbeat_server = None
|
|
311
|
+
self.process = HeartbeatServerTestProcess()
|
|
312
|
+
to_child, from_child = self.process.start()
|
|
313
|
+
to_child.put(_default_process_tree.heartbeat_server.port)
|
|
314
|
+
self.assertTrue(from_child.sock.poll(1000))
|
|
315
|
+
self.assertTrue(from_child.get())
|
|
316
|
+
|
|
317
|
+
def tearDown(self):
|
|
318
|
+
self.heartbeat_sock.close()
|
|
319
|
+
try:
|
|
320
|
+
self.process.terminate()
|
|
321
|
+
except Exception:
|
|
322
|
+
pass # already dead
|
|
323
|
+
|
|
324
|
+
class TestEventProcess(Process):
|
|
325
|
+
def run(self):
|
|
326
|
+
event = self.process_tree.event('hello', role='post')
|
|
327
|
+
event.post('1', data=u'boo')
|
|
328
|
+
time.sleep(0.5)
|
|
329
|
+
|
|
330
|
+
class TestExternalEventProcess(Process):
|
|
331
|
+
def run(self, broker_details):
|
|
332
|
+
event = self.process_tree.event('hello', role='post', external_broker=broker_details)
|
|
333
|
+
event.post('1', data=u'boo')
|
|
334
|
+
time.sleep(0.5)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class EventTests(unittest.TestCase):
|
|
339
|
+
def test_events(self):
|
|
340
|
+
proc = TestEventProcess()
|
|
341
|
+
event = _default_process_tree.event('hello', role='wait')
|
|
342
|
+
proc.start()
|
|
343
|
+
try:
|
|
344
|
+
data = event.wait('1', timeout=1)
|
|
345
|
+
self.assertEqual(data, u'boo')
|
|
346
|
+
finally:
|
|
347
|
+
proc.terminate()
|
|
348
|
+
|
|
349
|
+
def test_external_broker(self):
|
|
350
|
+
broker = EventBroker(bind_address='tcp://127.0.0.1')
|
|
351
|
+
broker_details = ExternalBroker('localhost', broker.in_port, broker.out_port)
|
|
352
|
+
proc = TestExternalEventProcess()
|
|
353
|
+
event = _default_process_tree.event('hello', role='wait', external_broker=broker_details)
|
|
354
|
+
proc.start(broker_details)
|
|
355
|
+
try:
|
|
356
|
+
data = event.wait('1', timeout=1)
|
|
357
|
+
self.assertEqual(data, u'boo')
|
|
358
|
+
finally:
|
|
359
|
+
proc.terminate()
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class TaskTests(unittest.TestCase):
|
|
363
|
+
def test_cant_call_task_twice(self):
|
|
364
|
+
task = Task(1, lambda: None)
|
|
365
|
+
task()
|
|
366
|
+
with self.assertRaises(RuntimeError):
|
|
367
|
+
task()
|
|
368
|
+
|
|
369
|
+
def test_queue(self):
|
|
370
|
+
# Test insert order:
|
|
371
|
+
queue = TaskQueue()
|
|
372
|
+
task1 = Task(1, lambda: None)
|
|
373
|
+
task2 = Task(2, lambda: None)
|
|
374
|
+
task3 = Task(3, lambda: None)
|
|
375
|
+
|
|
376
|
+
queue.add(task1)
|
|
377
|
+
queue.add(task3)
|
|
378
|
+
queue.add(task2)
|
|
379
|
+
|
|
380
|
+
self.assertIs(queue[0], task3)
|
|
381
|
+
self.assertIs(queue[1], task2)
|
|
382
|
+
self.assertIs(queue[2], task1)
|
|
383
|
+
|
|
384
|
+
# Test correct task pops:
|
|
385
|
+
self.assertIs(queue.pop(), task1)
|
|
386
|
+
|
|
387
|
+
# test cancel:
|
|
388
|
+
queue.cancel(task2)
|
|
389
|
+
self.assertEqual(queue, [task3])
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
class ClientServerTests(unittest.TestCase):
|
|
393
|
+
|
|
394
|
+
def test_rep_server(self):
|
|
395
|
+
class MyServer(ZMQServer):
|
|
396
|
+
def handler(self, data):
|
|
397
|
+
if data == 'error':
|
|
398
|
+
raise TestError
|
|
399
|
+
return data
|
|
400
|
+
|
|
401
|
+
server = MyServer(port=None, bind_address='tcp://127.0.0.1')
|
|
402
|
+
try:
|
|
403
|
+
self.assertIsInstance(server.context, SecureContext)
|
|
404
|
+
response = zmq_get(server.port, data='hello!')
|
|
405
|
+
self.assertEqual(response, 'hello!')
|
|
406
|
+
|
|
407
|
+
# Ignore the exception in the other thread:
|
|
408
|
+
clientserver.raise_exception_in_thread = lambda *args: None
|
|
409
|
+
try:
|
|
410
|
+
with self.assertRaises(TestError):
|
|
411
|
+
zmq_get(server.port, data='error')
|
|
412
|
+
finally:
|
|
413
|
+
clientserver.raise_exception_in_thread = raise_exception_in_thread
|
|
414
|
+
finally:
|
|
415
|
+
server.shutdown()
|
|
416
|
+
|
|
417
|
+
def test_raw_server(self):
|
|
418
|
+
class MyServer(ZMQServer):
|
|
419
|
+
def handler(self, data):
|
|
420
|
+
if data == b'error':
|
|
421
|
+
raise TestError
|
|
422
|
+
return data
|
|
423
|
+
|
|
424
|
+
for argname in ["dtype", "type", "positional"]:
|
|
425
|
+
if argname == 'dtype':
|
|
426
|
+
server = MyServer(port=None, dtype='raw',
|
|
427
|
+
bind_address='tcp://127.0.0.1')
|
|
428
|
+
elif argname == 'type':
|
|
429
|
+
server = MyServer(port=None, type='raw',
|
|
430
|
+
bind_address='tcp://127.0.0.1')
|
|
431
|
+
elif argname == 'positional':
|
|
432
|
+
server = MyServer(None, 'raw',
|
|
433
|
+
bind_address='tcp://127.0.0.1')
|
|
434
|
+
try:
|
|
435
|
+
self.assertIsInstance(server.context, SecureContext)
|
|
436
|
+
response = zmq_get_raw(server.port, data=b'hello!')
|
|
437
|
+
self.assertEqual(response, b'hello!')
|
|
438
|
+
|
|
439
|
+
# Ignore the exception in the other thread:
|
|
440
|
+
clientserver.raise_exception_in_thread = lambda *args: None
|
|
441
|
+
try:
|
|
442
|
+
self.assertIn(b'TestError', zmq_get_raw(server.port, data=b'error'))
|
|
443
|
+
finally:
|
|
444
|
+
clientserver.raise_exception_in_thread = \
|
|
445
|
+
raise_exception_in_thread
|
|
446
|
+
finally:
|
|
447
|
+
server.shutdown()
|
|
448
|
+
|
|
449
|
+
def test_pull_server(self):
|
|
450
|
+
|
|
451
|
+
testcase = self
|
|
452
|
+
got_data = threading.Event()
|
|
453
|
+
|
|
454
|
+
class MyPullServer(ZMQServer):
|
|
455
|
+
def handler(self, data):
|
|
456
|
+
if data == 'error!':
|
|
457
|
+
return "not None!"
|
|
458
|
+
testcase.assertEqual(data, 'hello!')
|
|
459
|
+
got_data.set()
|
|
460
|
+
|
|
461
|
+
server = MyPullServer(port=None, bind_address='tcp://127.0.0.1',
|
|
462
|
+
pull_only=True)
|
|
463
|
+
|
|
464
|
+
# So we can catch errors raised by raise_exception_in_thread
|
|
465
|
+
|
|
466
|
+
try:
|
|
467
|
+
self.assertIsInstance(server.context, SecureContext)
|
|
468
|
+
response = zmq_push(server.port, data='hello!')
|
|
469
|
+
self.assertEqual(response, None)
|
|
470
|
+
self.assertEqual(got_data.wait(timeout=1), True)
|
|
471
|
+
got_data.clear()
|
|
472
|
+
|
|
473
|
+
# Confirm you get an error when the handler returns something:
|
|
474
|
+
got_error = threading.Event()
|
|
475
|
+
class MockThread(object):
|
|
476
|
+
def __init__(self, target, args):
|
|
477
|
+
self.target = target
|
|
478
|
+
self.args = args
|
|
479
|
+
def start(self):
|
|
480
|
+
try:
|
|
481
|
+
self.target(*self.args)
|
|
482
|
+
except ValueError:
|
|
483
|
+
got_error.set()
|
|
484
|
+
|
|
485
|
+
orig_thread = threading.Thread
|
|
486
|
+
try:
|
|
487
|
+
threading.Thread = MockThread
|
|
488
|
+
response = zmq_push(server.port, data='error!')
|
|
489
|
+
self.assertEqual(got_error.wait(timeout=1), True)
|
|
490
|
+
finally:
|
|
491
|
+
threading.Thread = orig_thread
|
|
492
|
+
|
|
493
|
+
# Confirm the server still works:
|
|
494
|
+
response = zmq_push(server.port, data='hello!')
|
|
495
|
+
self.assertEqual(response, None)
|
|
496
|
+
self.assertEqual(got_data.wait(timeout=1), True)
|
|
497
|
+
got_data.clear()
|
|
498
|
+
finally:
|
|
499
|
+
server.shutdown()
|
|
500
|
+
|
|
501
|
+
def test_customauth_backcompat(self):
|
|
502
|
+
class MyCustomAuthServer(ZMQServer):
|
|
503
|
+
def setup_auth(self, context):
|
|
504
|
+
pass
|
|
505
|
+
|
|
506
|
+
server = MyCustomAuthServer(port=None, bind_address='tcp://127.0.0.1')
|
|
507
|
+
try:
|
|
508
|
+
self.assertNotIsInstance(server.context, SecureContext)
|
|
509
|
+
finally:
|
|
510
|
+
server.shutdown()
|
|
511
|
+
|
|
512
|
+
if __name__ == '__main__':
|
|
513
|
+
pytest.main([__file__, '-v'])
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: zprocess
|
|
3
|
-
Version: 2.26.
|
|
3
|
+
Version: 2.26.1
|
|
4
4
|
Summary: A set of utilities for multiprocessing using zeromq
|
|
5
5
|
Author-email: Christopher Billington <chrisjbillington@gmail.com>
|
|
6
6
|
License: Copyright (c) 2013, Christopher Billington
|
|
@@ -34,7 +34,7 @@ Project-URL: Tracker, https://github.com/chrisjbillington/zprocess/issues
|
|
|
34
34
|
Keywords: multiprocessing,IPC,zeromq
|
|
35
35
|
Classifier: License :: OSI Approved :: BSD License
|
|
36
36
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
37
|
-
Requires-Python: >=3.
|
|
37
|
+
Requires-Python: >=3.8
|
|
38
38
|
Description-Content-Type: text/markdown
|
|
39
39
|
License-File: LICENSE.txt
|
|
40
40
|
Requires-Dist: setuptools_scm
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|