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.
@@ -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)
pabot/workerwrapper.py ADDED
@@ -0,0 +1,8 @@
1
+ import sys
2
+
3
+ try:
4
+ from .py3.worker import main
5
+ except SyntaxError:
6
+ print("Worker needs to be run in Python >= 3.6")
7
+ print("You are running %s" % sys.version)
8
+ sys.exit(1)