Skip to content

Commit 9b68f40

Browse files
author
Janne Härkönen
committed
Import of code from robotframework/tools/remoteserver
The code and documentation may still contain references to the Ruby implementation, which is nowadays a separate project. These references will be removed before first release.
1 parent 2f805b6 commit 9b68f40

21 files changed

Lines changed: 1596 additions & 1 deletion

‎README.rest‎

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,60 @@
1-
Python implementation of Robot Framework remote server.
1+
Robot Framework Remote Servers
2+
==============================
3+
4+
Introduction
5+
------------
6+
7+
This directory contains remote server implementations that can be used
8+
with Robot Framework's Remote library. The main source for information
9+
related to the Remote library and remote servers is the User Guide.
10+
11+
Implemented Remote Servers
12+
--------------------------
13+
14+
There are currently remote server implementations for Python and Ruby in
15+
following files::
16+
17+
robotremoteserver.py
18+
robotremoteserver.rb
19+
20+
Notice that the Python version works also with Jython. New remote
21+
servers, for example for Java and Perl, can be added in the future.
22+
23+
Examples Using Remote Servers
24+
-----------------------------
25+
26+
Examples on how test libraries can use the remote servers can be found from
27+
`example` directory. These example libraries can be started with commands::
28+
29+
python example/examplelibrary.py
30+
jython example/examplelibrary.py
31+
ruby example/examplelibrary.rb
32+
33+
Note that all the above commands require that language's module search
34+
path is set so that the respective remote server module can be imported.
35+
By default the servers listen to connections to localhost on port 8270,
36+
but this can be configured like::
37+
38+
python example/examplelibrary.py localhost 7777
39+
ruby example/examplelibrary.rb 192.168.0.1 8270
40+
41+
These examples will start the remote server so that it provides
42+
keywords implemented in the example module. After the remote server is
43+
started, an example test case file can be executed using the familiar
44+
`pybot` or `jybot` commands, possibly giving the port where the server
45+
is listening as a variable::
46+
47+
pybot example/remote_tests.html
48+
jybot example/remote_tests.html
49+
pybot --variable PORT:7777 example/remote_tests.html
50+
51+
The results should be the same regardless of the example library or start-up
52+
script used.
53+
54+
Testing Remote Servers
55+
----------------------
56+
57+
Tests for the remote servers are inside `test` directory. Acceptance tests
58+
can be executed using `tests/run.py` script and running the script without
59+
arguments provides more information. Notice that tests are not included in
60+
source distributions.

‎example/examplelibrary.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/usr/bin/env python
2+
3+
import os
4+
import sys
5+
6+
7+
class ExampleRemoteLibrary:
8+
"""Example library to be used with Robot Framework's remote server.
9+
10+
This documentation is visible in docs generated by _libdoc.py_
11+
starting from Robot Framework 2.6.2.
12+
"""
13+
14+
def __init__(self):
15+
"""Also this doc should be in shown in library doc."""
16+
17+
def count_items_in_directory(self, path):
18+
"""Returns the number of items in the directory specified by `path`."""
19+
return len([i for i in os.listdir(path) if not i.startswith('.')])
20+
21+
def strings_should_be_equal(self, str1, str2):
22+
print "Comparing '%s' to '%s'" % (str1, str2)
23+
if str1 != str2:
24+
raise AssertionError("Given strings are not equal")
25+
26+
27+
if __name__ == '__main__':
28+
from robotremoteserver import RobotRemoteServer
29+
RobotRemoteServer(ExampleRemoteLibrary(), *sys.argv[1:])

‎example/remote_tests.txt‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
*** Settings ***
2+
Library Remote http://localhost:${PORT}
3+
4+
*** Variables ***
5+
${PORT} 8270
6+
7+
*** Test Cases ***
8+
9+
Count Items in Directory
10+
${items1} = Count Items In Directory ${CURDIR}
11+
${items2} = Count Items In Directory ${TEMPDIR}
12+
Log ${items1} items in '${CURDIR}' and ${items2} items in '${TEMPDIR}'
13+
14+
Failing Example
15+
Strings Should Be Equal Hello Hello
16+
Strings Should Be Equal not equal

‎src/robotremoteserver.py‎

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# Copyright 2008-2012 Nokia Siemens Networks Oyj
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import sys
16+
import inspect
17+
import traceback
18+
from StringIO import StringIO
19+
from SimpleXMLRPCServer import SimpleXMLRPCServer
20+
try:
21+
import signal
22+
except ImportError:
23+
signal = None
24+
25+
26+
class RobotRemoteServer(SimpleXMLRPCServer):
27+
allow_reuse_address = True
28+
29+
def __init__(self, library, host='localhost', port=8270, allow_stop=True):
30+
SimpleXMLRPCServer.__init__(self, (host, int(port)), logRequests=False)
31+
self._library = library
32+
self._allow_stop = allow_stop
33+
self._register_functions()
34+
self._register_signal_handlers()
35+
self._log('Robot Framework remote server starting at %s:%s'
36+
% (host, port))
37+
self.serve_forever()
38+
39+
def _register_functions(self):
40+
self.register_function(self.get_keyword_names)
41+
self.register_function(self.run_keyword)
42+
self.register_function(self.get_keyword_arguments)
43+
self.register_function(self.get_keyword_documentation)
44+
self.register_function(self.stop_remote_server)
45+
46+
def _register_signal_handlers(self):
47+
def stop_with_signal(signum, frame):
48+
self._allow_stop = True
49+
self.stop_remote_server()
50+
if hasattr(signal, 'SIGHUP'):
51+
signal.signal(signal.SIGHUP, stop_with_signal)
52+
if hasattr(signal, 'SIGINT'):
53+
signal.signal(signal.SIGINT, stop_with_signal)
54+
55+
def serve_forever(self):
56+
self._shutdown = False
57+
while not self._shutdown:
58+
self.handle_request()
59+
60+
def stop_remote_server(self):
61+
prefix = 'Robot Framework remote server at %s:%s ' % self.server_address
62+
if self._allow_stop:
63+
self._log(prefix + 'stopping')
64+
self._shutdown = True
65+
else:
66+
self._log(prefix + 'does not allow stopping', 'WARN')
67+
return True
68+
69+
def get_keyword_names(self):
70+
get_kw_names = getattr(self._library, 'get_keyword_names', None) or \
71+
getattr(self._library, 'getKeywordNames', None)
72+
if inspect.isroutine(get_kw_names):
73+
names = get_kw_names()
74+
else:
75+
names = [attr for attr in dir(self._library) if attr[0] != '_'
76+
and inspect.isroutine(getattr(self._library, attr))]
77+
return names + ['stop_remote_server']
78+
79+
def run_keyword(self, name, args):
80+
result = {'status': 'PASS', 'return': '', 'output': '',
81+
'error': '', 'traceback': ''}
82+
self._intercept_stdout()
83+
try:
84+
return_value = self._get_keyword(name)(*args)
85+
except:
86+
result['status'] = 'FAIL'
87+
result['error'], result['traceback'] = self._get_error_details()
88+
else:
89+
result['return'] = self._handle_return_value(return_value)
90+
result['output'] = self._restore_stdout()
91+
return result
92+
93+
def get_keyword_arguments(self, name):
94+
kw = self._get_keyword(name)
95+
if not kw:
96+
return []
97+
return self._arguments_from_kw(kw)
98+
99+
def _arguments_from_kw(self, kw):
100+
args, varargs, _, defaults = inspect.getargspec(kw)
101+
if inspect.ismethod(kw):
102+
args = args[1:] # drop 'self'
103+
if defaults:
104+
args, names = args[:-len(defaults)], args[-len(defaults):]
105+
args += ['%s=%s' % (n, d) for n, d in zip(names, defaults)]
106+
if varargs:
107+
args.append('*%s' % varargs)
108+
return args
109+
110+
def get_keyword_documentation(self, name):
111+
if name == '__intro__':
112+
return inspect.getdoc(self._library) or ''
113+
if name == '__init__' and inspect.ismodule(self._library):
114+
return ''
115+
return inspect.getdoc(self._get_keyword(name)) or ''
116+
117+
def _get_keyword(self, name):
118+
if name == 'stop_remote_server':
119+
return self.stop_remote_server
120+
kw = getattr(self._library, name, None)
121+
if inspect.isroutine(kw):
122+
return kw
123+
return None
124+
125+
def _get_error_details(self):
126+
exc_type, exc_value, exc_tb = sys.exc_info()
127+
if exc_type in (SystemExit, KeyboardInterrupt):
128+
self._restore_stdout()
129+
raise
130+
return (self._get_error_message(exc_type, exc_value),
131+
self._get_error_traceback(exc_tb))
132+
133+
def _get_error_message(self, exc_type, exc_value):
134+
name = exc_type.__name__
135+
message = str(exc_value)
136+
if not message:
137+
return name
138+
if name in ('AssertionError', 'RuntimeError', 'Exception'):
139+
return message
140+
return '%s: %s' % (name, message)
141+
142+
def _get_error_traceback(self, exc_tb):
143+
# Latest entry originates from this class so it can be removed
144+
entries = traceback.extract_tb(exc_tb)[1:]
145+
trace = ''.join(traceback.format_list(entries))
146+
return 'Traceback (most recent call last):\n' + trace
147+
148+
def _handle_return_value(self, ret):
149+
if isinstance(ret, (basestring, int, long, float)):
150+
return ret
151+
if isinstance(ret, (tuple, list)):
152+
return [self._handle_return_value(item) for item in ret]
153+
if isinstance(ret, dict):
154+
return dict([(self._str(key), self._handle_return_value(value))
155+
for key, value in ret.items()])
156+
return self._str(ret)
157+
158+
def _str(self, item):
159+
if item is None:
160+
return ''
161+
return str(item)
162+
163+
def _intercept_stdout(self):
164+
# TODO: What about stderr?
165+
sys.stdout = StringIO()
166+
167+
def _restore_stdout(self):
168+
output = sys.stdout.getvalue()
169+
sys.stdout.close()
170+
sys.stdout = sys.__stdout__
171+
return output
172+
173+
def _log(self, msg, level=None):
174+
if level:
175+
msg = '*%s* %s' % (level.upper(), msg)
176+
print msg

‎test/atest/argument_counts.txt‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
*** Settings ***
2+
Default Tags python ruby
3+
Library Remote localhost:${PORT}
4+
5+
*** Variables ***
6+
${PORT} 8270
7+
8+
*** Test Cases ***
9+
No Arguments
10+
Arguments Should Be Accepted no arguments No Arguments
11+
12+
Required Arguments
13+
Arguments Should Be Accepted some argument One Argument some argument
14+
Arguments Should Be Accepted first argument second argument Two Arguments first argument second argument
15+
Arguments Should Be Accepted 1 2 3 4 5 6 7 Seven Arguments 1 2 3 4 5
16+
... 6 7
17+
18+
Arguments With Default Values
19+
Arguments Should Be Accepted one two three Arguments With Default Values one two three
20+
Arguments Should Be Accepted one two 3 Arguments With Default Values one two
21+
Arguments Should Be Accepted one 2 3 Arguments With Default Values one
22+
23+
Variable Number Of Arguments
24+
Arguments Should Be Accepted ${EMPTY} Variable Number Of Arguments
25+
Arguments Should Be Accepted One argument Variable Number Of Arguments One argument
26+
Arguments Should Be Accepted Three arguments now Variable Number Of Arguments Three arguments now
27+
Arguments Should Be Accepted 1 2 3 4 5 6 7 8 9 10 Variable Number Of Arguments 1 2 3 4 5
28+
... 6 7 8 9 10
29+
30+
Required Arguments, Default Values and Varargs
31+
Arguments Should Be Accepted Hello world Required Defaults And Varargs Hello
32+
Arguments Should Be Accepted Hi tellus Required Defaults And Varargs Hi tellus
33+
Arguments Should Be Accepted Hello again world Required Defaults And Varargs Hello again world
34+
Arguments Should Be Accepted Hi for the last time tellus Required Defaults And Varargs Hi for the last time
35+
... tellus
36+
37+
Using Arguments When No Accepted
38+
[Documentation] FAIL Keyword 'Remote.No Arguments' expected 0 arguments, got 1.
39+
No Arguments not allowed
40+
41+
Too Few Arguments When Using Only Required Args
42+
[Documentation] FAIL Keyword 'Remote.One Argument' expected 1 argument, got 0.
43+
One Argument
44+
45+
Too Many Arguments When Using Only Required Args
46+
[Documentation] FAIL Keyword 'Remote.Two Arguments' expected 2 arguments, got 3.
47+
Two Arguments too many arguments
48+
49+
Too Few Arguments When Using Default Values
50+
[Documentation] FAIL Keyword 'Remote.Arguments With Default Values' expected 1 to 3 arguments, got 0.
51+
[Tags] python
52+
Arguments With Default Values
53+
54+
Too Many Arguments When Using Default Values
55+
[Documentation] FAIL Keyword 'Remote.Arguments With Default Values' expected 1 to 3 arguments, got 5.
56+
[Tags] python
57+
Arguments With Default Values this is way too much
58+
59+
Too Few Arguments When Using Default Values
60+
[Documentation] With Ruby it is not possible to get maximum argument count when using default values. FAIL Keyword 'Remote.Arguments With Default Values' expected at least 1 argument, got 0.
61+
[Tags] ruby
62+
Arguments With Default Values
63+
64+
Too Many Arguments When Using Default Values
65+
[Documentation] With Ruby it is not possible to get maximum argument count when using default values. This error comes from Ruby. FAIL wrong number of arguments (5 for 3)
66+
[Tags] ruby
67+
Arguments With Default Values this is way too much
68+
69+
Too Few Arguments When Using Varargs
70+
[Documentation] FAIL Keyword 'Remote.Required Defaults And Varargs' expected at least 1 argument, got 0.
71+
Required Defaults And Varargs
72+
73+
*** Keywords ***
74+
Arguments Should Be Accepted
75+
[Arguments] ${expected} ${keyword} @{arguments}
76+
${actual} = Run Keyword ${keyword} @{arguments}
77+
Should Be Equal ${actual} ${expected}
78+

0 commit comments

Comments
 (0)