robotframework-pabot 3.1.0__py3-none-any.whl
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.
- pabot/SharedLibrary.py +62 -0
- pabot/__init__.py +4 -0
- pabot/arguments.py +236 -0
- pabot/clientwrapper.py +10 -0
- pabot/coordinatorwrapper.py +8 -0
- pabot/execution_items.py +320 -0
- pabot/pabot.py +2072 -0
- pabot/pabotlib.py +578 -0
- pabot/py3/__init__.py +0 -0
- pabot/py3/client.py +40 -0
- pabot/py3/coordinator.py +63 -0
- pabot/py3/messages.py +104 -0
- pabot/py3/worker.py +52 -0
- pabot/result_merger.py +272 -0
- pabot/robotremoteserver.py +632 -0
- pabot/workerwrapper.py +8 -0
- robotframework_pabot-3.1.0.dist-info/LICENSE.txt +202 -0
- robotframework_pabot-3.1.0.dist-info/METADATA +24 -0
- robotframework_pabot-3.1.0.dist-info/RECORD +22 -0
- robotframework_pabot-3.1.0.dist-info/WHEEL +5 -0
- robotframework_pabot-3.1.0.dist-info/entry_points.txt +2 -0
- robotframework_pabot-3.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
# Copyright 2008-2015 Nokia Networks
|
|
2
|
+
# Copyright 2016- Robot Framework Foundation
|
|
3
|
+
# Copyright 2020->future! Mikko Korpela
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
from __future__ import print_function
|
|
18
|
+
|
|
19
|
+
import inspect
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import select
|
|
23
|
+
import signal
|
|
24
|
+
import sys
|
|
25
|
+
import threading
|
|
26
|
+
import traceback
|
|
27
|
+
|
|
28
|
+
if sys.version_info < (3,):
|
|
29
|
+
from collections import Mapping
|
|
30
|
+
|
|
31
|
+
from SimpleXMLRPCServer import SimpleXMLRPCServer
|
|
32
|
+
from StringIO import StringIO
|
|
33
|
+
from xmlrpclib import Binary, ServerProxy
|
|
34
|
+
|
|
35
|
+
PY2, PY3 = True, False
|
|
36
|
+
else:
|
|
37
|
+
from collections.abc import Mapping
|
|
38
|
+
from io import StringIO
|
|
39
|
+
from xmlrpc.client import Binary, ServerProxy
|
|
40
|
+
from xmlrpc.server import SimpleXMLRPCServer
|
|
41
|
+
|
|
42
|
+
PY2, PY3 = False, True
|
|
43
|
+
unicode = str
|
|
44
|
+
long = int
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
__all__ = ["RobotRemoteServer", "stop_remote_server", "test_remote_server"]
|
|
48
|
+
__version__ = "1.1.1.dev1"
|
|
49
|
+
|
|
50
|
+
BINARY = re.compile("[\x00-\x08\x0B\x0C\x0E-\x1F]")
|
|
51
|
+
NON_ASCII = re.compile("[\x80-\xff]")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class RobotRemoteServer(object):
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
library,
|
|
58
|
+
host="127.0.0.1",
|
|
59
|
+
port=8270,
|
|
60
|
+
port_file=None,
|
|
61
|
+
allow_stop="DEPRECATED",
|
|
62
|
+
serve=True,
|
|
63
|
+
allow_remote_stop=True,
|
|
64
|
+
):
|
|
65
|
+
"""Configure and start-up remote server.
|
|
66
|
+
|
|
67
|
+
:param library: Test library instance or module to host.
|
|
68
|
+
:param host: Address to listen. Use ``'0.0.0.0'`` to listen
|
|
69
|
+
to all available interfaces.
|
|
70
|
+
:param port: Port to listen. Use ``0`` to select a free port
|
|
71
|
+
automatically. Can be given as an integer or as
|
|
72
|
+
a string.
|
|
73
|
+
:param port_file: File to write the port that is used. ``None`` means
|
|
74
|
+
no such file is written. Port file is created after
|
|
75
|
+
the server is started and removed automatically
|
|
76
|
+
after it has stopped.
|
|
77
|
+
:param allow_stop: DEPRECATED since version 1.1. Use
|
|
78
|
+
``allow_remote_stop`` instead.
|
|
79
|
+
:param serve: If ``True``, start the server automatically and
|
|
80
|
+
wait for it to be stopped.
|
|
81
|
+
:param allow_remote_stop: Allow/disallow stopping the server using
|
|
82
|
+
``Stop Remote Server`` keyword and
|
|
83
|
+
``stop_remote_server`` XML-RPC method.
|
|
84
|
+
"""
|
|
85
|
+
self._library = RemoteLibraryFactory(library)
|
|
86
|
+
self._server = StoppableXMLRPCServer(host, int(port))
|
|
87
|
+
self._register_functions(self._server)
|
|
88
|
+
self._port_file = port_file
|
|
89
|
+
self._allow_remote_stop = (
|
|
90
|
+
allow_remote_stop if allow_stop == "DEPRECATED" else allow_stop
|
|
91
|
+
)
|
|
92
|
+
if serve:
|
|
93
|
+
self.serve()
|
|
94
|
+
|
|
95
|
+
def _register_functions(self, server):
|
|
96
|
+
server.register_function(self.get_keyword_names)
|
|
97
|
+
server.register_function(self.run_keyword)
|
|
98
|
+
server.register_function(self.get_keyword_arguments)
|
|
99
|
+
server.register_function(self.get_keyword_documentation)
|
|
100
|
+
server.register_function(self.stop_remote_server)
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def server_address(self):
|
|
104
|
+
"""Server address as a tuple ``(host, port)``."""
|
|
105
|
+
return self._server.server_address
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def server_port(self):
|
|
109
|
+
"""Server port as an integer.
|
|
110
|
+
|
|
111
|
+
If the initial given port is 0, also this property returns 0 until
|
|
112
|
+
the server is activated.
|
|
113
|
+
"""
|
|
114
|
+
return self._server.server_address[1]
|
|
115
|
+
|
|
116
|
+
def activate(self):
|
|
117
|
+
"""Bind port and activate the server but do not yet start serving.
|
|
118
|
+
|
|
119
|
+
:return Port number that the server is going to use. This is the
|
|
120
|
+
actual port to use, even if the initially given port is 0.
|
|
121
|
+
"""
|
|
122
|
+
return self._server.activate()
|
|
123
|
+
|
|
124
|
+
def serve(self, log=True):
|
|
125
|
+
"""Start the server and wait for it to be stopped.
|
|
126
|
+
|
|
127
|
+
:param log: When ``True``, print messages about start and stop to
|
|
128
|
+
the console.
|
|
129
|
+
|
|
130
|
+
Automatically activates the server if it is not activated already.
|
|
131
|
+
|
|
132
|
+
If this method is executed in the main thread, automatically registers
|
|
133
|
+
signals SIGINT, SIGTERM and SIGHUP to stop the server.
|
|
134
|
+
|
|
135
|
+
Using this method requires using ``serve=False`` when initializing the
|
|
136
|
+
server. Using ``serve=True`` is equal to first using ``serve=False``
|
|
137
|
+
and then calling this method.
|
|
138
|
+
|
|
139
|
+
In addition to signals, the server can be stopped with the ``Stop
|
|
140
|
+
Remote Server`` keyword and the ``stop_remote_serve`` XML-RPC method,
|
|
141
|
+
unless they are disabled when the server is initialized. If this method
|
|
142
|
+
is executed in a thread, then it is also possible to stop the server
|
|
143
|
+
using the :meth:`stop` method.
|
|
144
|
+
"""
|
|
145
|
+
self._server.activate()
|
|
146
|
+
self._announce_start(log, self._port_file)
|
|
147
|
+
with SignalHandler(self.stop):
|
|
148
|
+
self._server.serve()
|
|
149
|
+
self._announce_stop(log, self._port_file)
|
|
150
|
+
|
|
151
|
+
def _announce_start(self, log, port_file):
|
|
152
|
+
self._log("started", log)
|
|
153
|
+
if port_file:
|
|
154
|
+
with open(port_file, "w") as pf:
|
|
155
|
+
pf.write(str(self.server_port))
|
|
156
|
+
|
|
157
|
+
def _announce_stop(self, log, port_file):
|
|
158
|
+
self._log("stopped", log)
|
|
159
|
+
if port_file and os.path.exists(port_file):
|
|
160
|
+
os.remove(port_file)
|
|
161
|
+
|
|
162
|
+
def _log(self, action, log=True, warn=False):
|
|
163
|
+
if log:
|
|
164
|
+
address = "%s:%s" % self.server_address
|
|
165
|
+
if warn:
|
|
166
|
+
print("*WARN*", end=" ")
|
|
167
|
+
print("Robot Framework remote server at %s %s." % (address, action))
|
|
168
|
+
|
|
169
|
+
def stop(self):
|
|
170
|
+
"""Stop server."""
|
|
171
|
+
self._server.stop()
|
|
172
|
+
|
|
173
|
+
# Exposed XML-RPC methods. Should they be moved to own class?
|
|
174
|
+
|
|
175
|
+
def stop_remote_server(self, log=True):
|
|
176
|
+
if not self._allow_remote_stop:
|
|
177
|
+
self._log("does not allow stopping", log, warn=True)
|
|
178
|
+
return False
|
|
179
|
+
self.stop()
|
|
180
|
+
return True
|
|
181
|
+
|
|
182
|
+
def get_keyword_names(self):
|
|
183
|
+
return self._library.get_keyword_names() + ["stop_remote_server"]
|
|
184
|
+
|
|
185
|
+
def run_keyword(self, name, args, kwargs=None):
|
|
186
|
+
if name == "stop_remote_server":
|
|
187
|
+
return KeywordRunner(self.stop_remote_server).run_keyword(args, kwargs)
|
|
188
|
+
return self._library.run_keyword(name, args, kwargs)
|
|
189
|
+
|
|
190
|
+
def get_keyword_arguments(self, name):
|
|
191
|
+
if name == "stop_remote_server":
|
|
192
|
+
return []
|
|
193
|
+
return self._library.get_keyword_arguments(name)
|
|
194
|
+
|
|
195
|
+
def get_keyword_documentation(self, name):
|
|
196
|
+
if name == "stop_remote_server":
|
|
197
|
+
return (
|
|
198
|
+
"Stop the remote server unless stopping is disabled.\n\n"
|
|
199
|
+
"Return ``True/False`` depending was server stopped or not."
|
|
200
|
+
)
|
|
201
|
+
return self._library.get_keyword_documentation(name)
|
|
202
|
+
|
|
203
|
+
def get_keyword_tags(self, name):
|
|
204
|
+
if name == "stop_remote_server":
|
|
205
|
+
return []
|
|
206
|
+
return self._library.get_keyword_tags(name)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class StoppableXMLRPCServer(SimpleXMLRPCServer):
|
|
210
|
+
allow_reuse_address = True
|
|
211
|
+
|
|
212
|
+
def __init__(self, host, port):
|
|
213
|
+
SimpleXMLRPCServer.__init__(
|
|
214
|
+
self, (host, port), logRequests=False, bind_and_activate=False
|
|
215
|
+
)
|
|
216
|
+
self._activated = False
|
|
217
|
+
self._stopper_thread = None
|
|
218
|
+
|
|
219
|
+
def activate(self):
|
|
220
|
+
if not self._activated:
|
|
221
|
+
self.server_bind()
|
|
222
|
+
self.server_activate()
|
|
223
|
+
self._activated = True
|
|
224
|
+
return self.server_address[1]
|
|
225
|
+
|
|
226
|
+
def serve(self):
|
|
227
|
+
self.activate()
|
|
228
|
+
try:
|
|
229
|
+
self.serve_forever()
|
|
230
|
+
except select.error:
|
|
231
|
+
# Signals seem to cause this error with Python 2.6.
|
|
232
|
+
if sys.version_info[:2] > (2, 6):
|
|
233
|
+
raise
|
|
234
|
+
self.server_close()
|
|
235
|
+
if self._stopper_thread:
|
|
236
|
+
self._stopper_thread.join()
|
|
237
|
+
self._stopper_thread = None
|
|
238
|
+
|
|
239
|
+
def stop(self):
|
|
240
|
+
self._stopper_thread = threading.Thread(target=self.shutdown)
|
|
241
|
+
self._stopper_thread.daemon = True
|
|
242
|
+
self._stopper_thread.start()
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class SignalHandler(object):
|
|
246
|
+
def __init__(self, handler):
|
|
247
|
+
self._handler = lambda signum, frame: handler()
|
|
248
|
+
self._original = {}
|
|
249
|
+
|
|
250
|
+
def __enter__(self):
|
|
251
|
+
for name in "SIGINT", "SIGTERM", "SIGHUP":
|
|
252
|
+
if hasattr(signal, name):
|
|
253
|
+
try:
|
|
254
|
+
orig = signal.signal(getattr(signal, name), self._handler)
|
|
255
|
+
except ValueError: # Not in main thread
|
|
256
|
+
return
|
|
257
|
+
self._original[name] = orig
|
|
258
|
+
|
|
259
|
+
def __exit__(self, *exc_info):
|
|
260
|
+
while self._original:
|
|
261
|
+
name, handler = self._original.popitem()
|
|
262
|
+
signal.signal(getattr(signal, name), handler)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def RemoteLibraryFactory(library):
|
|
266
|
+
if inspect.ismodule(library):
|
|
267
|
+
return StaticRemoteLibrary(library)
|
|
268
|
+
get_keyword_names = dynamic_method(library, "get_keyword_names")
|
|
269
|
+
if not get_keyword_names:
|
|
270
|
+
return StaticRemoteLibrary(library)
|
|
271
|
+
run_keyword = dynamic_method(library, "run_keyword")
|
|
272
|
+
if not run_keyword:
|
|
273
|
+
return HybridRemoteLibrary(library, get_keyword_names)
|
|
274
|
+
return DynamicRemoteLibrary(library, get_keyword_names, run_keyword)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def dynamic_method(library, underscore_name):
|
|
278
|
+
tokens = underscore_name.split("_")
|
|
279
|
+
camelcase_name = tokens[0] + "".join(t.title() for t in tokens[1:])
|
|
280
|
+
for name in underscore_name, camelcase_name:
|
|
281
|
+
method = getattr(library, name, None)
|
|
282
|
+
if method and is_function_or_method(method):
|
|
283
|
+
return method
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def is_function_or_method(item):
|
|
288
|
+
return inspect.isfunction(item) or inspect.ismethod(item)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class StaticRemoteLibrary(object):
|
|
292
|
+
def __init__(self, library):
|
|
293
|
+
self._library = library
|
|
294
|
+
self._names = None
|
|
295
|
+
self._robot_name_index = None
|
|
296
|
+
|
|
297
|
+
def _construct_keyword_names(self):
|
|
298
|
+
names = []
|
|
299
|
+
robot_name_index = {}
|
|
300
|
+
for name, kw in inspect.getmembers(self._library):
|
|
301
|
+
if is_function_or_method(kw):
|
|
302
|
+
if getattr(kw, "robot_name", None):
|
|
303
|
+
names.append(kw.robot_name)
|
|
304
|
+
robot_name_index[kw.robot_name] = name
|
|
305
|
+
elif name[0] != "_":
|
|
306
|
+
names.append(name)
|
|
307
|
+
return names, robot_name_index
|
|
308
|
+
|
|
309
|
+
def get_keyword_names(self):
|
|
310
|
+
if self._names is None:
|
|
311
|
+
self._names, self._robot_name_index = self._construct_keyword_names()
|
|
312
|
+
return self._names
|
|
313
|
+
|
|
314
|
+
def run_keyword(self, name, args, kwargs=None):
|
|
315
|
+
kw = self._get_keyword(name)
|
|
316
|
+
return KeywordRunner(kw).run_keyword(args, kwargs)
|
|
317
|
+
|
|
318
|
+
def _get_keyword(self, name):
|
|
319
|
+
if self._names is None:
|
|
320
|
+
self._names, self._robot_name_index = self._construct_keyword_names()
|
|
321
|
+
if name in self._robot_name_index:
|
|
322
|
+
name = self._robot_name_index[name]
|
|
323
|
+
return getattr(self._library, name)
|
|
324
|
+
|
|
325
|
+
def get_keyword_arguments(self, name):
|
|
326
|
+
if __name__ == "__init__":
|
|
327
|
+
return []
|
|
328
|
+
kw = self._get_keyword(name)
|
|
329
|
+
args, varargs, kwargs, defaults = inspect.getfullargspec(kw)
|
|
330
|
+
if inspect.ismethod(kw):
|
|
331
|
+
args = args[1:] # drop 'self'
|
|
332
|
+
if defaults:
|
|
333
|
+
args, names = args[: -len(defaults)], args[-len(defaults) :]
|
|
334
|
+
args += ["%s=%s" % (n, d) for n, d in zip(names, defaults)]
|
|
335
|
+
if varargs:
|
|
336
|
+
args.append("*%s" % varargs)
|
|
337
|
+
if kwargs:
|
|
338
|
+
args.append("**%s" % kwargs)
|
|
339
|
+
return args
|
|
340
|
+
|
|
341
|
+
def get_keyword_documentation(self, name):
|
|
342
|
+
if name == "__intro__":
|
|
343
|
+
source = self._library
|
|
344
|
+
elif name == "__init__":
|
|
345
|
+
source = self._get_init(self._library)
|
|
346
|
+
else:
|
|
347
|
+
source = self._get_keyword(name)
|
|
348
|
+
return inspect.getdoc(source) or ""
|
|
349
|
+
|
|
350
|
+
def _get_init(self, library):
|
|
351
|
+
if inspect.ismodule(library):
|
|
352
|
+
return None
|
|
353
|
+
init = getattr(library, "__init__", None)
|
|
354
|
+
return init if self._is_valid_init(init) else None
|
|
355
|
+
|
|
356
|
+
def _is_valid_init(self, init):
|
|
357
|
+
if not init:
|
|
358
|
+
return False
|
|
359
|
+
# https://bitbucket.org/pypy/pypy/issues/2462/
|
|
360
|
+
if "PyPy" in sys.version:
|
|
361
|
+
if PY2:
|
|
362
|
+
return init.__func__ is not object.__init__.__func__
|
|
363
|
+
return init is not object.__init__
|
|
364
|
+
return is_function_or_method(init)
|
|
365
|
+
|
|
366
|
+
def get_keyword_tags(self, name):
|
|
367
|
+
keyword = self._get_keyword(name)
|
|
368
|
+
return getattr(keyword, "robot_tags", [])
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class HybridRemoteLibrary(StaticRemoteLibrary):
|
|
372
|
+
def __init__(self, library, get_keyword_names):
|
|
373
|
+
StaticRemoteLibrary.__init__(self, library)
|
|
374
|
+
self.get_keyword_names = get_keyword_names
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class DynamicRemoteLibrary(HybridRemoteLibrary):
|
|
378
|
+
def __init__(self, library, get_keyword_names, run_keyword):
|
|
379
|
+
HybridRemoteLibrary.__init__(self, library, get_keyword_names)
|
|
380
|
+
self._run_keyword = run_keyword
|
|
381
|
+
self._supports_kwargs = self._get_kwargs_support(run_keyword)
|
|
382
|
+
self._get_keyword_arguments = dynamic_method(library, "get_keyword_arguments")
|
|
383
|
+
self._get_keyword_documentation = dynamic_method(
|
|
384
|
+
library, "get_keyword_documentation"
|
|
385
|
+
)
|
|
386
|
+
self._get_keyword_tags = dynamic_method(library, "get_keyword_tags")
|
|
387
|
+
|
|
388
|
+
def _get_kwargs_support(self, run_keyword):
|
|
389
|
+
spec = inspect.getfullargspec(run_keyword)
|
|
390
|
+
return len(spec.args) > 3 # self, name, args, kwargs=None
|
|
391
|
+
|
|
392
|
+
def run_keyword(self, name, args, kwargs=None):
|
|
393
|
+
args = [name, args, kwargs] if kwargs else [name, args, {}]
|
|
394
|
+
return KeywordRunner(self._run_keyword).run_keyword(args)
|
|
395
|
+
|
|
396
|
+
def get_keyword_arguments(self, name):
|
|
397
|
+
if self._get_keyword_arguments:
|
|
398
|
+
return self._get_keyword_arguments(name)
|
|
399
|
+
if self._supports_kwargs:
|
|
400
|
+
return ["*varargs", "**kwargs"]
|
|
401
|
+
return ["*varargs"]
|
|
402
|
+
|
|
403
|
+
def get_keyword_documentation(self, name):
|
|
404
|
+
if self._get_keyword_documentation:
|
|
405
|
+
return self._get_keyword_documentation(name)
|
|
406
|
+
return ""
|
|
407
|
+
|
|
408
|
+
def get_keyword_tags(self, name):
|
|
409
|
+
if self._get_keyword_tags:
|
|
410
|
+
return self._get_keyword_tags(name)
|
|
411
|
+
return []
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class KeywordRunner(object):
|
|
415
|
+
def __init__(self, keyword):
|
|
416
|
+
self._keyword = keyword
|
|
417
|
+
|
|
418
|
+
def run_keyword(self, args, kwargs=None):
|
|
419
|
+
args = self._handle_binary(args)
|
|
420
|
+
kwargs = self._handle_binary(kwargs or {})
|
|
421
|
+
result = KeywordResult()
|
|
422
|
+
with StandardStreamInterceptor() as interceptor:
|
|
423
|
+
try:
|
|
424
|
+
return_value = self._keyword(*args, **kwargs)
|
|
425
|
+
except Exception:
|
|
426
|
+
result.set_error(*sys.exc_info())
|
|
427
|
+
else:
|
|
428
|
+
try:
|
|
429
|
+
result.set_return(return_value)
|
|
430
|
+
except Exception:
|
|
431
|
+
result.set_error(*sys.exc_info()[:2])
|
|
432
|
+
else:
|
|
433
|
+
result.set_status("PASS")
|
|
434
|
+
result.set_output(interceptor.output)
|
|
435
|
+
return result.data
|
|
436
|
+
|
|
437
|
+
def _handle_binary(self, arg):
|
|
438
|
+
# No need to compare against other iterables or mappings because we
|
|
439
|
+
# only get actual lists and dicts over XML-RPC. Binary cannot be
|
|
440
|
+
# a dictionary key either.
|
|
441
|
+
if isinstance(arg, list):
|
|
442
|
+
return [self._handle_binary(item) for item in arg]
|
|
443
|
+
if isinstance(arg, dict):
|
|
444
|
+
return dict((key, self._handle_binary(arg[key])) for key in arg)
|
|
445
|
+
if isinstance(arg, Binary):
|
|
446
|
+
return arg.data
|
|
447
|
+
return arg
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
class StandardStreamInterceptor(object):
|
|
451
|
+
def __init__(self):
|
|
452
|
+
self.output = ""
|
|
453
|
+
self.origout = sys.stdout
|
|
454
|
+
self.origerr = sys.stderr
|
|
455
|
+
sys.stdout = StringIO()
|
|
456
|
+
sys.stderr = StringIO()
|
|
457
|
+
|
|
458
|
+
def __enter__(self):
|
|
459
|
+
return self
|
|
460
|
+
|
|
461
|
+
def __exit__(self, *exc_info):
|
|
462
|
+
stdout = sys.stdout.getvalue()
|
|
463
|
+
stderr = sys.stderr.getvalue()
|
|
464
|
+
close = [sys.stdout, sys.stderr]
|
|
465
|
+
sys.stdout = self.origout
|
|
466
|
+
sys.stderr = self.origerr
|
|
467
|
+
for stream in close:
|
|
468
|
+
stream.close()
|
|
469
|
+
if stdout and stderr:
|
|
470
|
+
if not stderr.startswith(
|
|
471
|
+
("*TRACE*", "*DEBUG*", "*INFO*", "*HTML*", "*WARN*", "*ERROR*")
|
|
472
|
+
):
|
|
473
|
+
stderr = "*INFO* %s" % stderr
|
|
474
|
+
if not stdout.endswith("\n"):
|
|
475
|
+
stdout += "\n"
|
|
476
|
+
self.output = stdout + stderr
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
class KeywordResult(object):
|
|
480
|
+
_generic_exceptions = (AssertionError, RuntimeError, Exception)
|
|
481
|
+
|
|
482
|
+
def __init__(self):
|
|
483
|
+
self.data = {"status": "FAIL"}
|
|
484
|
+
|
|
485
|
+
def set_error(self, exc_type, exc_value, exc_tb=None):
|
|
486
|
+
self.data["error"] = self._get_message(exc_type, exc_value)
|
|
487
|
+
if exc_tb:
|
|
488
|
+
self.data["traceback"] = self._get_traceback(exc_tb)
|
|
489
|
+
continuable = self._get_error_attribute(exc_value, "CONTINUE")
|
|
490
|
+
if continuable:
|
|
491
|
+
self.data["continuable"] = continuable
|
|
492
|
+
fatal = self._get_error_attribute(exc_value, "EXIT")
|
|
493
|
+
if fatal:
|
|
494
|
+
self.data["fatal"] = fatal
|
|
495
|
+
|
|
496
|
+
def _get_message(self, exc_type, exc_value):
|
|
497
|
+
name = exc_type.__name__
|
|
498
|
+
message = self._get_message_from_exception(exc_value)
|
|
499
|
+
if not message:
|
|
500
|
+
return name
|
|
501
|
+
if exc_type in self._generic_exceptions or getattr(
|
|
502
|
+
exc_value, "ROBOT_SUPPRESS_NAME", False
|
|
503
|
+
):
|
|
504
|
+
return message
|
|
505
|
+
return "%s: %s" % (name, message)
|
|
506
|
+
|
|
507
|
+
def _get_message_from_exception(self, value):
|
|
508
|
+
# UnicodeError occurs if message contains non-ASCII bytes
|
|
509
|
+
try:
|
|
510
|
+
msg = unicode(value)
|
|
511
|
+
except UnicodeError:
|
|
512
|
+
msg = " ".join(self._str(a, handle_binary=False) for a in value.args)
|
|
513
|
+
return self._handle_binary_result(msg)
|
|
514
|
+
|
|
515
|
+
def _get_traceback(self, exc_tb):
|
|
516
|
+
# Latest entry originates from this module so it can be removed
|
|
517
|
+
entries = traceback.extract_tb(exc_tb)[1:]
|
|
518
|
+
trace = "".join(traceback.format_list(entries))
|
|
519
|
+
return "Traceback (most recent call last):\n" + trace
|
|
520
|
+
|
|
521
|
+
def _get_error_attribute(self, exc_value, name):
|
|
522
|
+
return bool(getattr(exc_value, "ROBOT_%s_ON_FAILURE" % name, False))
|
|
523
|
+
|
|
524
|
+
def set_return(self, value):
|
|
525
|
+
value = self._handle_return_value(value)
|
|
526
|
+
if value != "":
|
|
527
|
+
self.data["return"] = value
|
|
528
|
+
|
|
529
|
+
def _handle_return_value(self, ret):
|
|
530
|
+
if isinstance(ret, (str, unicode, bytes)):
|
|
531
|
+
return self._handle_binary_result(ret)
|
|
532
|
+
if isinstance(ret, (int, long, float)):
|
|
533
|
+
return ret
|
|
534
|
+
if isinstance(ret, Mapping):
|
|
535
|
+
return dict(
|
|
536
|
+
(self._str(key), self._handle_return_value(value))
|
|
537
|
+
for key, value in ret.items()
|
|
538
|
+
)
|
|
539
|
+
try:
|
|
540
|
+
return [self._handle_return_value(item) for item in ret]
|
|
541
|
+
except TypeError:
|
|
542
|
+
return self._str(ret)
|
|
543
|
+
|
|
544
|
+
def _handle_binary_result(self, result):
|
|
545
|
+
if not self._contains_binary(result):
|
|
546
|
+
return result
|
|
547
|
+
if not isinstance(result, bytes):
|
|
548
|
+
try:
|
|
549
|
+
result = result.encode("ASCII")
|
|
550
|
+
except UnicodeError:
|
|
551
|
+
raise ValueError("Cannot represent %r as binary." % result)
|
|
552
|
+
# With IronPython Binary cannot be sent if it contains "real" bytes.
|
|
553
|
+
if sys.platform == "cli":
|
|
554
|
+
result = str(result)
|
|
555
|
+
return Binary(result)
|
|
556
|
+
|
|
557
|
+
def _contains_binary(self, result):
|
|
558
|
+
if PY3:
|
|
559
|
+
return isinstance(result, bytes) or BINARY.search(result)
|
|
560
|
+
return (
|
|
561
|
+
isinstance(result, bytes)
|
|
562
|
+
and NON_ASCII.search(result)
|
|
563
|
+
or BINARY.search(result)
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
def _str(self, item, handle_binary=True):
|
|
567
|
+
if item is None:
|
|
568
|
+
return ""
|
|
569
|
+
if not isinstance(item, (str, unicode, bytes)):
|
|
570
|
+
item = unicode(item)
|
|
571
|
+
if handle_binary:
|
|
572
|
+
item = self._handle_binary_result(item)
|
|
573
|
+
return item
|
|
574
|
+
|
|
575
|
+
def set_status(self, status):
|
|
576
|
+
self.data["status"] = status
|
|
577
|
+
|
|
578
|
+
def set_output(self, output):
|
|
579
|
+
if output:
|
|
580
|
+
self.data["output"] = self._handle_binary_result(output)
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def test_remote_server(uri, log=True):
|
|
584
|
+
"""Test is remote server running.
|
|
585
|
+
|
|
586
|
+
:param uri: Server address.
|
|
587
|
+
:param log: Log status message or not.
|
|
588
|
+
:return ``True`` if server is running, ``False`` otherwise.
|
|
589
|
+
"""
|
|
590
|
+
logger = print if log else lambda message: None
|
|
591
|
+
try:
|
|
592
|
+
ServerProxy(uri).get_keyword_names()
|
|
593
|
+
except Exception:
|
|
594
|
+
logger("No remote server running at %s." % uri)
|
|
595
|
+
return False
|
|
596
|
+
logger("Remote server running at %s." % uri)
|
|
597
|
+
return True
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def stop_remote_server(uri, log=True):
|
|
601
|
+
"""Stop remote server unless server has disabled stopping.
|
|
602
|
+
|
|
603
|
+
:param uri: Server address.
|
|
604
|
+
:param log: Log status message or not.
|
|
605
|
+
:return ``True`` if server was stopped or it was not running in
|
|
606
|
+
the first place, ``False`` otherwise.
|
|
607
|
+
"""
|
|
608
|
+
logger = print if log else lambda message: None
|
|
609
|
+
if not test_remote_server(uri, log=False):
|
|
610
|
+
logger("No remote server running at %s." % uri)
|
|
611
|
+
return True
|
|
612
|
+
logger("Stopping remote server at %s." % uri)
|
|
613
|
+
if not ServerProxy(uri).stop_remote_server():
|
|
614
|
+
logger("Stopping not allowed!")
|
|
615
|
+
return False
|
|
616
|
+
return True
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
if __name__ == "__main__":
|
|
620
|
+
|
|
621
|
+
def parse_args(script, *args):
|
|
622
|
+
actions = {"stop": stop_remote_server, "test": test_remote_server}
|
|
623
|
+
if not (0 < len(args) < 3) or args[0] not in actions:
|
|
624
|
+
sys.exit("Usage: %s {test|stop} [uri]" % os.path.basename(script))
|
|
625
|
+
uri = args[1] if len(args) == 2 else "http://127.0.0.1:8270"
|
|
626
|
+
if "://" not in uri:
|
|
627
|
+
uri = "http://" + uri
|
|
628
|
+
return actions[args[0]], uri
|
|
629
|
+
|
|
630
|
+
action, uri = parse_args(*sys.argv)
|
|
631
|
+
success = action(uri)
|
|
632
|
+
sys.exit(0 if success else 1)
|