Robot Framework Integrated Development Environment (RIDE)
TestRunnerAgent.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 # ----------------------------------------------------------------------------
3 # Copyright 2010 Orbitz WorldWide
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 # Modified by Mikko Korpela under NSN copyrights
18 # Copyright 2008-2015 Nokia Solutions and Networks
19 #
20 # Licensed under the Apache License, Version 2.0 (the "License");
21 # you may not use this file except in compliance with the License.
22 # You may obtain a copy of the License at
23 #
24 # http://www.apache.org/licenses/LICENSE-2.0
25 #
26 # Unless required by applicable law or agreed to in writing, software
27 # distributed under the License is distributed on an "AS IS" BASIS,
28 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
29 # See the License for the specific language governing permissions and
30 # limitations under the License.
31 
32 # Ammended by Timothy Alexander <dragonfyre13@gmail.com>
33 # (StreamHandler class added)
34 # Copyright 2013 Timothy Alexander
35 # Licensed under the Apache License, Version 2.0
36 # http://www.apache.org/licenses/LICENSE-2.0
37 
38 #
39 # Modified by Mateusz Marzec under NSN copyrights
40 # Copyright 2015 Nokia Solutions and Networks
41 # * Licensed under the Apache License, Version 2.0,
42 # * see license.txt file for details.
43 #
44 
45 # Ammended by Helio Guilherme <helioxentric@gmail.com>
46 # Copyright 2008-2015 Nokia Networks
47 # Copyright 2016- Robot Framework Foundation
48 #
49 # Licensed under the Apache License, Version 2.0 (the "License");
50 # you may not use this file except in compliance with the License.
51 # You may obtain a copy of the License at
52 #
53 # http://www.apache.org/licenses/LICENSE-2.0
54 #
55 # Unless required by applicable law or agreed to in writing, software
56 # distributed under the License is distributed on an "AS IS" BASIS,
57 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
58 # See the License for the specific language governing permissions and
59 # limitations under the License.
60 
61 
67 
68 import copy
69 import os
70 import pickle
71 import platform
72 import sys
73 import socket
74 import threading
75 
76 PLATFORM = platform.python_implementation()
77 
78 try:
79  import socketserver as SocketServer
80 except ImportError as e:
81  raise e
82 
83 try:
84  # to find robot (we use provided lib)
85  sys.path.append(os.path.join(os.path.dirname(__file__), '../../lib'))
86  from robot.errors import ExecutionFailed
87  from robot.running import EXECUTION_CONTEXTS
88  from robot.running.signalhandler import STOP_SIGNAL_MONITOR
89  from robot.utils import encoding
90  from robot.utils.encoding import SYSTEM_ENCODING
91 except ImportError:
92  encoding = None
93  raise
94 
95 try:
96  import json
97 
100  _JSONAVAIL = True
101 except ImportError:
102 
105  _JSONAVAIL = False
106 
107 try:
108  from StringIO import StringIO
109 except ImportError: # py3 <=3.6
110  from io import StringIO
111 
112 HOST = "localhost"
113 
114 # Setting Output encoding to UTF-8 and ignoring the platform specs
115 # RIDE will expect UTF-8
116 # Set output encoding to UTF-8 for piped output streams
117 # DEBUG This was working in Linux always!
118 #if encoding:
119 # encoding.OUTPUT_ENCODING = 'UTF-8'
120 # print("DEBUG: TestRunnerAgent encoding %s\n" % SYSTEM_ENCODING )
121 
122 
123 def _is_logged(level):
124  current = EXECUTION_CONTEXTS.current
125  if current is None:
126  return True
127  out = current.output
128  if out is None:
129  return True
130  return out._xmllogger._log_message_is_logged(level)
131 
132 
133 
139  ROBOT_LISTENER_API_VERSION = 2
140 
141  def __init__(self, *args):
142  self.portport = int(args[0])
143  self.hosthost = HOST
144  self.socksock = None
145  self.filehandlerfilehandler = None
146  self.streamhandlerstreamhandler = None
147  self._connect_connect()
148  self._send_pid_send_pid()
149  self._create_debugger_create_debugger((len(args) >= 2) and (args[1] == 'True'))
150  self._create_kill_server_create_kill_server()
151  print("TestRunnerAgent: Running under %s %s\n" %
152  (PLATFORM, sys.version.split()[0]))
153 
154  def _create_debugger(self, pause_on_failure):
155  self._debugger_debugger = RobotDebugger(pause_on_failure)
156 
158  self._killer_killer = RobotKillerServer(self._debugger_debugger)
159  self._server_thread_server_thread = threading.Thread(
160  target=self._killer_killer.serve_forever)
161  # DEPRECATED: self._server_thread.setDaemon(True)
162  self._server_thread_server_thread.daemon = True
163  self._server_thread_server_thread.start()
164  self._send_server_port_send_server_port(self._killer_killer.server_address[1])
165 
166  def _send_pid(self):
167  self._send_socket_send_socket("pid", os.getpid())
168 
169  def _send_server_port(self, port):
170  self._send_socket_send_socket("port", port)
171 
172  def start_test(self, name, attrs):
173  self._send_socket_send_socket("start_test", name, attrs)
174 
175  def end_test(self, name, attrs):
176  self._send_socket_send_socket("end_test", name, attrs)
177 
178  def start_suite(self, name, attrs):
179  attrs_copy = copy.copy(attrs)
180  del attrs_copy['doc']
181  attrs_copy['is_dir'] = os.path.isdir(attrs['source'])
182  self._send_socket_send_socket("start_suite", name, attrs_copy)
183 
184  def end_suite(self, name, attrs):
185  attrs_copy = copy.copy(attrs)
186  del attrs_copy['doc']
187  attrs_copy['is_dir'] = os.path.isdir(attrs['source'])
188  self._send_socket_send_socket("end_suite", name, attrs_copy)
189 
190  def start_keyword(self, name, attrs):
191  # pass empty args, see https://github.com/nokia/RED/issues/32
192 
193  # we're cutting args from original attrs dict, because it may contain
194  # objects which are not json-serializable and we don't need them anyway
195  attrs_copy = copy.copy(attrs)
196  del attrs_copy['args']
197  del attrs_copy['doc']
198  del attrs_copy['assign']
199 
200  self._send_socket_send_socket("start_keyword", name, attrs_copy)
201  if self._debugger_debugger.is_breakpoint(name, attrs): # must check original
202  self._debugger_debugger.pause()
203  paused = self._debugger_debugger.is_paused()
204  if paused:
205  self._send_socket_send_socket('paused')
206  self._debugger_debugger.start_keyword()
207  if paused:
208  self._send_socket_send_socket('continue')
209 
210  def end_keyword(self, name, attrs):
211  # pass empty args, see https://github.com/nokia/RED/issues/32
212  attrs_copy = copy.copy(attrs)
213  del attrs_copy['args']
214  del attrs_copy['doc']
215  del attrs_copy['assign']
216 
217  self._send_socket_send_socket("end_keyword", name, attrs_copy)
218  self._debugger_debugger.end_keyword(attrs['status'] == 'PASS')
219 
220  def message(self, message):
221  pass
222 
223  def log_message(self, message):
224  if _is_logged(message['level']):
225  self._send_socket("log_message", message)
226 
227  def log_file(self, path):
228  self._send_socket_send_socket("log_file", path)
229 
230  def output_file(self, path):
231  pass
232 
233  def report_file(self, path):
234  self._send_socket("report_file", path)
235 
236  def summary_file(self, path):
237  pass
238 
239  def debug_file(self, path):
240  pass
241 
242  def close(self):
243  self._send_socket("close")
244  if self.sock:
245  self.filehandler.close()
246  self.sock.close()
247 
248 
251  def _connect(self):
252  try:
253  self.socksock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
254  self.socksock.connect((self.hosthost, self.portport))
255  # Iron python does not return right object type if not binary mode
256  self.filehandlerfilehandler = self.socksock.makefile('wb')
257  self.streamhandlerstreamhandler = StreamHandler(self.filehandlerfilehandler)
258  except socket.error as e:
259  print('unable to open socket to "%s:%s" error: %s'
260  % (self.hosthost, self.portport, str(e)))
261  self.socksock = None
262  self.filehandlerfilehandler = None
263 
264  def _send_socket(self, name, *args):
265  try:
266  if self.filehandlerfilehandler:
267  packet = (name, args)
268  self.streamhandlerstreamhandler.dump(packet)
269  self.filehandlerfilehandler.flush()
270  except Exception:
271  import traceback
272  traceback.print_exc(file=sys.stdout)
273  sys.stdout.flush()
274  raise
275 
276 
278 
279  def __init__(self, pause_on_failure=False):
280  self._state_state = 'running'
281  self._keyword_level_keyword_level = 0
282  self._pause_when_on_level_pause_when_on_level = -1
283  self._pause_on_failure_pause_on_failure = pause_on_failure
284  self._resume_resume = threading.Event()
285 
286  @staticmethod
287  def is_breakpoint(name, attrs):
288  if len(attrs['args']) > 0:
289  return name == 'BuiltIn.Comment' and \
290  str(attrs['args'][0]).upper().startswith(u"PAUSE")
291 
292  def pause(self):
293  self._resume_resume.clear()
294  self._state_state = 'pause'
295 
296  def pause_on_failure(self, pause):
297  self._pause_on_failure_pause_on_failure = pause
298 
299  def resume(self):
300  self._state_state = 'running'
301  self._pause_when_on_level_pause_when_on_level = -1
302  self._resume_resume.set()
303 
304  def step_next(self):
305  self._state_state = 'step_next'
306  self._resume_resume.set()
307 
308  def step_over(self):
309  self._state_state = 'step_over'
310  self._resume_resume.set()
311 
312  def start_keyword(self):
313  while self._state_state == 'pause':
314  self._resume_resume.wait()
315  self._resume_resume.clear()
316  if self._state_state == 'step_next':
317  self._state_state = 'pause'
318  elif self._state_state == 'step_over':
319  self._pause_when_on_level_pause_when_on_level = self._keyword_level_keyword_level
320  self._state_state = 'resume'
321  self._keyword_level_keyword_level += 1
322 
323  def end_keyword(self, passed=True):
324  self._keyword_level_keyword_level -= 1
325  if self._keyword_level_keyword_level == self._pause_when_on_level_pause_when_on_level or\
326  (self._pause_on_failure_pause_on_failure and not passed):
327  self._state_state = 'pause'
328 
329  def is_paused(self):
330  return self._state_state == 'pause'
331 
332 
333 class RobotKillerServer(SocketServer.TCPServer):
334  allow_reuse_address = True
335 
336  def __init__(self, debugger):
337  SocketServer.TCPServer.__init__(self, ("", 0), RobotKillerHandler)
338  self.debuggerdebugger = debugger
339 
340 
341 class RobotKillerHandler(SocketServer.StreamRequestHandler):
342  def handle(self):
343  data = self.request.makefile('r').read().strip()
344  if data == 'kill':
345  self._signal_kill_signal_kill()
346  elif data == 'pause':
347  self.server.debugger.pause()
348  elif data == 'resume':
349  self.server.debugger.resume()
350  elif data == 'step_next':
351  self.server.debugger.step_next()
352  elif data == 'step_over':
353  self.server.debugger.step_over()
354  elif data == 'pause_on_failure':
355  self.server.debugger.pause_on_failure(True)
356  elif data == 'do_not_pause_on_failure':
357  self.server.debugger.pause_on_failure(False)
358 
359  @staticmethod
361  try:
362  STOP_SIGNAL_MONITOR(1, '')
363  except ExecutionFailed:
364  pass
365 
366 
367 # NOTE: Moved to bottom of TestRunnerAgent per feedback in pull request,
368 # so jybot doesn't encounter issues. Special imports at top of file.
369 
373  pass
374 
375 
376 
380 class EncodeError(StreamError):
381  wrapped_exceptions = (pickle.PicklingError, )
382 
383 
384 
392  # NOTE: No JSONDecodeError in json in stdlib for python >= 2.6
393  wrapped_exceptions = (pickle.UnpicklingError,)
394  if _JSONAVAIL:
395  if hasattr(json, 'JSONDecodeError'):
396  wrapped_exceptions = (pickle.UnpicklingError, json.JSONDecodeError)
397 
398 
399 def dump(obj, fp):
400  StreamHandler(fp).dump(obj)
401 
402 
403 def load(fp):
404  return StreamHandler(fp).load()
405 
406 
407 
415 def dumps(obj):
416  fp = StringIO()
417  StreamHandler(fp).dump(obj)
418  return fp.getvalue()
419 
420 
421 
431 def loads(s):
432  fp = StringIO(s)
433  return StreamHandler(fp).load()
434 
435 
436 
458  loads = staticmethod(loads)
459  dumps = staticmethod(dumps)
460 
461 
471  def __init__(self, fp):
472  if _JSONAVAIL:
473  self._json_encoder_json_encoder = json.JSONEncoder(separators=(',', ':'),
474  sort_keys=True).encode
475  self._json_decoder_json_decoder = json.JSONDecoder(strict=False).decode
476  else:
477  def json_not_impl(dummy):
478  raise NotImplementedError(
479  'Python should include json. Please check your Python installation.')
480  self._json_decoder_json_decoder = staticmethod(json_not_impl)
481  self._json_encoder_json_encoder = staticmethod(json_not_impl)
482  self.fpfp = fp
483 
484 
492  def dump(self, obj):
493  # NOTE: Slightly less efficient than doing iterencode directly into the
494  # fp, however difference is negligable and reduces complexity of
495  # of the StreamHandler class (treating pickle and json the same)
496  write_list = []
497  if _JSONAVAIL:
498  try:
499  s = self._json_encoder_json_encoder(obj)
500  write_list.append('J')
501  write_list.extend([str(len(s)), '|', s])
502  except:
503  # Probably just failed to JSON-encode an object; try pickle.
504  pass
505  if not write_list:
506  s = pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)
507  write_list.append('P')
508  write_list.extend([str(len(s)), '|', s])
509  self.fpfp.write(bytes(''.join(write_list), "UTF-8"))
510 
511 
521  def load(self):
522  header = self._load_header_load_header()
523  msgtype = header[0]
524  msglen = header[1:]
525  if not msglen.isdigit():
526  raise DecodeError('Message header not valid: %r' % header)
527  msglen = int(msglen)
528  buff = StringIO()
529  # Don't use StringIO.len for sizing, reports string len not bytes
530  buff.write(self.fpfp.read(msglen))
531  try:
532  if msgtype == 'J':
533  return self._json_decoder_json_decoder(buff.getvalue())
534  elif msgtype == 'P':
535  return pickle.loads(buff.getvalue())
536  else:
537  raise DecodeError("Message type %r not supported" % msgtype)
538  except DecodeError.wrapped_exceptions as e:
539  raise DecodeError(str(e))
540 
541 
546  def _load_header(self):
547  buff = StringIO()
548  while len(buff.getvalue()) == 0 or buff.getvalue()[-1] != '|':
549  recv_char = self.fpfp.read(1)
550  if not recv_char:
551  raise EOFError('File/Socket closed while reading load header')
552  buff.write(recv_char)
553  return buff.getvalue()[:-1]
This exception is raised when there is a problem decoding an object, such as a security violation.
Base class for EncodeError and DecodeError.
This class provides a common streaming approach for the purpose of reliably sending data over a socke...
def load(self)
Reads in json message prepended with message length header from a file (or socket,...
def dump(self, obj)
Similar method to json dump, prepending data with message length header.
def __init__(self, fp)
Stream handler that encodes objects as either JSON (if available) with message length header prepende...
Pass all listener events to a remote listener.
def _connect(self)
Establish a connection for sending data.
def loads(s)
Reads in json message or pickle message prepended with message length header from a string.
def dumps(obj)
Similar method to json dumps, prepending data with message length header.
def write(msg, level='INFO', html=False)
Writes the message to the log file using the given level.
Definition: logger.py:86