Coverage for src/robotide/contrib/testrunner/testrunnerplugin.py: 43%
998 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-06 10:40 +0100
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-06 10:40 +0100
1# -*- encoding: utf-8 -*-
2# Copyright 2010 Orbitz WorldWide
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
16# Modified by NSN
17# Copyright 2010-2012 Nokia Solutions and Networks
18# Copyright 2013-2015 Nokia Networks
19# Copyright 2016- Robot Framework Foundation
20#
21# Licensed under the Apache License, Version 2.0 (the "License");
22# you may not use this file except in compliance with the License.
23# You may obtain a copy of the License at
24#
25# http://www.apache.org/licenses/LICENSE-2.0
26#
27# Unless required by applicable law or agreed to in writing, software
28# distributed under the License is distributed on an "AS IS" BASIS,
29# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
30# See the License for the specific language governing permissions and
31# limitations under the License.
33"""A plugin for running tests from within RIDE
35Some icons courtesy Mark James and provided under a creative commons
36license. See http://www.famfamfam.com/lab/icons/silk
38Note: this plugin creates a temporary directory for use while a test
39is running. This directory is normally removed when RIDE exits. If
40RIDE is shut down abnormally this directory may not get removed. The
41directories that are created match the pattern RIDE*.d and are in a
42temporary directory appropriate for the platform (for example, on
43linux it's /tmp).
45You can safely manually remove these directories, except for the one
46being used for a currently running test.
47"""
48import atexit
49import builtins
50import datetime
51import shutil
52import subprocess
53import tempfile
54import threading
55import time
56import os
57import psutil
58import re
59import wx
60import wx.stc
61from functools import reduce
62from queue import Queue
63from wx.lib.embeddedimage import PyEmbeddedImage
65from robotide.action.shortcut import localize_shortcuts
66from robotide.context import IS_WINDOWS, IS_MAC
67from robotide.contrib.testrunner import TestRunner
68from robotide.contrib.testrunner import runprofiles
69from robotide.contrib.testrunner.ArgsParser import ArgsParser
70from robotide.contrib.testrunner.CommandArgs import CommandArgs
71from robotide.contrib.testrunner.Command import Command
72from robotide.contrib.testrunner.FileWriter import FileWriter
73from robotide.contrib.testrunner.SettingsParser import SettingsParser
74from robotide.controller.macrocontrollers import TestCaseController
75from robotide.controller.filecontrollers import start_filemanager
76from robotide.publish import RideSettingsChanged, PUBLISHER
77from robotide.publish.messages import RideTestSelectedForRunningChanged
78from robotide.pluginapi import Plugin, ActionInfo
79from robotide.ui.notebook import NoteBook
80from robotide.widgets import Label, ImageProvider, ButtonWithHandler, RIDEDialog
81from robotide.robotapi import LOG_LEVELS
82from robotide.utils import robottime
83from robotide.preferences import PreferenceEditor
84from robotide.preferences.editors import read_fonts
85from sys import getfilesystemencoding, platform
86from robotide.lib.robot.utils.encodingsniffer import (get_console_encoding,
87 get_system_encoding)
89_ = wx.GetTranslation # To keep linter/code analyser happy
90builtins.__dict__['_'] = wx.GetTranslation
92CONSOLE_ENCODING = get_console_encoding()
93SYSTEM_ENCODING = get_system_encoding()
94OUTPUT_ENCODING = getfilesystemencoding()
95encoding = {'CONSOLE': CONSOLE_ENCODING,
96 'SYSTEM': SYSTEM_ENCODING,
97 'OUTPUT': OUTPUT_ENCODING}
99FONT_FACE = 'font face'
100STOP_RUNNING_TEST = _('Stop a running test')
101STEP_OVER = _('Step over')
102ID_RUN = wx.NewIdRef()
103ID_RUNDEBUG = wx.NewIdRef()
104ID_STOP = wx.NewIdRef()
105ID_PAUSE = wx.NewIdRef()
106ID_CONTINUE = wx.NewIdRef()
107ID_STEP_NEXT = wx.NewIdRef()
108ID_STEP_OVER = wx.NewIdRef()
109ID_OPEN_LOGS_DIR = wx.NewId()
110ID_SHOW_REPORT = wx.NewIdRef()
111ID_SHOW_LOG = wx.NewIdRef()
112ID_AUTOSAVE = wx.NewIdRef()
113ID_PAUSE_ON_FAILURE = wx.NewIdRef()
114ID_SHOW_MESSAGE_LOG = wx.NewIdRef()
115STYLE_DEFAULT = 0
116STYLE_STDERR = 2
117STYLE_PASS = 1
118STYLE_SKIP = 3
119STYLE_FAIL = 4
120STYLE_LINK = 5
121FILE_MANAGER = 'file manager'
123ATEXIT_LOCK = threading.RLock()
126def _run_profile(name, run_prefix):
127 return type('Profile', (runprofiles.PybotProfile,),
128 {'name': name, 'get_command': lambda self: run_prefix})
131class TestRunnerPlugin(Plugin):
132 __doc__ = _("""A plugin for running tests from within RIDE""")
133 defaults = {"auto_save": False,
134 "confirm run": True,
135 "profile_name": "robot",
136 "show_console_log": True,
137 "show_message_log": True,
138 "active_status_bar": True,
139 "sash_position": 200,
140 "run_profiles":
141 [('jybot', 'jybot' + ('.bat' if os.name == 'nt' else '')),
142 ('pybot', 'pybot' + ('.bat' if os.name == 'nt' else '')),
143 ('robot 3.1', 'robot')],
144 "font size": 10,
145 "font face": 'Courier New',
146 "foreground": 'black',
147 "background": 'white',
148 "error": 'red',
149 "use colors": False,
150 "fail color": '#FF8E8E',
151 "pass color": '#9FCC9F',
152 "skip color": 'yellow'
153 }
155 report_regex = re.compile(r"^Report: {2}(.*\.html)$", re.MULTILINE)
156 log_regex = re.compile(r"^Log: {5}(.*\.html)$", re.MULTILINE)
157 title = 'Run'
159 def __init__(self, application=None):
160 self.app = application
161 self.title = _('Run')
162 Plugin.__init__(self, application, initially_enabled=True,
163 default_settings=self.defaults)
164 self.version = "3.1"
165 self.metadata = {
166 "url":
167 "https://github.com/robotframework/RIDE/wiki/Test-Runner-Plugin"}
168 self._reload_timer = None
169 self._frame = application.frame
170 self._report_file = None
171 self._log_file = None
172 self._controls = {}
173 self._running = False
174 self._currently_executing_keyword = None
175 self._test_runner = TestRunner(application.model)
176 self._register_shortcuts()
177 self._min_log_level_number = LOG_LEVELS['INFO']
178 self._pause_on_failure = False
179 self._selected_tests: {TestCaseController} = set()
180 self._process = psutil.Process()
181 self._initmemory = None
182 self._limitmemory = None # This will be +80%
183 self._maxmemmsg = None
184 self.active_status_bar = self.__getattr__('active_status_bar')
185 self.use_colors = self.__getattr__('use colors')
186 self.fail_color = self.__getattr__('fail color')
187 self.pass_color = self.__getattr__('pass color')
188 self.skip_color = self.__getattr__('skip color')
190 @property
191 def _names_to_run(self):
192 return list(
193 map(lambda ctrl: (ctrl.datafile_controller.longname, ctrl.longname),
194 self._selected_tests))
196 def _register_shortcuts(self):
197 self.register_shortcut('CtrlCmd-C', self._copy_from_log_ctrls)
198 self.register_shortcut('CtrlCmd-L', self.on_show_log)
199 self.register_shortcut('CtrlCmd-R', self.on_show_report)
200 if IS_WINDOWS or IS_MAC: 200 ↛ exitline 200 didn't return from function '_register_shortcuts' because the condition on line 200 was always true
201 self.register_shortcut('Del', self._delete_pressed)
203 def _delete_pressed(self, event):
204 __ = event
205 if self.notebook.current_page_title != self.title:
206 return
207 self.get_current_profile().delete_pressed()
209 def _copy_from_log_ctrls(self, event):
210 __ = event
211 if self.notebook.current_page_title != self.title:
212 return
213 if self._console_log_ctrl.GetSTCFocus():
214 self._console_log_ctrl.Copy()
215 return
216 if self._message_log_ctrl.GetSTCFocus():
217 self._message_log_ctrl.Copy()
219 def enable(self):
220 self.tree.set_checkboxes_for_tests()
221 self._read_run_profiles()
222 self._register_actions()
223 self._add_tab_to_notebook()
224 self._init_profile_choice(self.profile_name)
225 self._subscribe_to_events()
226 self._test_runner.enable(self._test_runner_events_handler)
227 self._set_stopped()
228 self._create_temporary_directory()
230 def _register_actions(self):
231 run_action_info = ActionInfo(_("Tools"), _("Run Tests"), self.on_run, None,
232 "F8", ImageProvider().TOOLBAR_PLAY,
233 _("Run the selected tests"), position=10)
234 self._run_action = self.register_action(run_action_info)
235 run_action_debug = ActionInfo(_("Tools"), _("Run Tests with Debug"),
236 self.on_run_debug, None,
237 "F9", getBugIconBitmap(),
238 _("Run the selected tests with Debug"),
239 position=8)
240 self._run_action = self.register_action(run_action_debug)
241 stop_action_info = ActionInfo(_("Tools"), _("Stop Test Run"), self.on_stop,
242 None, "CtrlCmd-F8",
243 ImageProvider().TOOLBAR_STOP,
244 STOP_RUNNING_TEST, position=11)
245 self._stop_action = self.register_action(stop_action_info)
247 def _read_run_profiles(self):
248 self._read_run_profiles_from_config()
249 self._read_run_profiles_from_classes()
251 def _read_run_profiles_from_config(self):
252 # Have to keep reference so that these classes are not garbage collected
253 self._profile_classes_from_config = [_run_profile(name, run_prefix)
254 for name, run_prefix in
255 self.run_profiles]
257 def _read_run_profiles_from_classes(self):
258 for profile in self._get_all_subclasses(runprofiles.BaseProfile):
259 self._test_runner.add_profile(profile.name, profile(plugin=self))
261 def _get_all_subclasses(self, class_):
262 classes = []
263 for sub_class in class_.__subclasses__():
264 classes += [sub_class] + self._get_all_subclasses(sub_class)
265 return classes
267 def _subscribe_to_events(self):
268 self.subscribe(self.on_test_selected_for_running_changed,
269 RideTestSelectedForRunningChanged)
270 self.subscribe(self.on_settings_changed, RideSettingsChanged)
272 def on_settings_changed(self, message):
273 """Updates settings"""
274 section, setting = message.keys
275 # print("DEBUG: enter OnSettingsChanged section %s" % (section))
276 if section == 'Test Run': # DEBUG temporarily we have two sections 276 ↛ 277line 276 didn't jump to line 277 because the condition on line 276 was never true
277 self.defaults.setdefault(setting, message.new)
278 self.save_setting(setting, message.new)
280 def on_test_selected_for_running_changed(self, message):
281 self._selected_tests = message.tests
283 def disable(self):
284 self._remove_from_notebook()
285 self._test_runner.clear_server()
286 self.unsubscribe_all()
287 self.unregister_actions()
289 def _create_temporary_directory(self):
290 self._default_output_dir = tempfile.mkdtemp(".d", "RIDE")
291 atexit.register(self._remove_temporary_directory)
292 # this plugin creates a temporary directory which _should_
293 # get reaped at exit. Sometimes things happen which might
294 # cause it to not get deleted. Maybe this would be a good
295 # place to check for temporary directories that match the
296 # signature and delete them if they are more than a few
297 # days old...
299 def _remove_temporary_directory(self):
300 with ATEXIT_LOCK:
301 if os.path.exists(self._default_output_dir):
302 shutil.rmtree(self._default_output_dir)
304 def on_close(self, event):
305 """Shut down the running services and processes"""
306 self._test_runner.kill_process()
307 if self._process_timer:
308 self._process_timer.Stop()
309 self._test_runner.shutdown_server()
310 event.Skip()
312 def _reset_memory_calc(self):
313 self._initmemory = self._process.memory_info()[0]
314 self._limitmemory = self._initmemory * 1.80
315 self._maxmemmsg = None
317 def on_stop(self, event):
318 """Called when the user clicks the "Stop" button
320 This sends a SIGINT to the running process, with the
321 same effect as typing control-c when running from the
322 command line."""
323 __ = event
324 self._reset_memory_calc()
325 self._append_to_console_log(_('[ SENDING STOP SIGNAL ]\n'),
326 source='stderr')
327 self._test_runner.send_stop_signal()
328 if self.active_status_bar:
329 self.statusbar_message(_('[ SENDING STOP SIGNAL ]\n'), 5000)
331 def on_pause(self, event):
332 __ = event
333 self._reset_memory_calc()
334 self._append_to_console_log(_('[ SENDING PAUSE SIGNAL ]\n'))
335 self._test_runner.send_pause_signal()
336 if self.active_status_bar:
337 self.statusbar_message(_('[ SENDING PAUSE SIGNAL ]\n'), 5000)
339 def on_continue(self, event):
340 __ = event
341 self._reset_memory_calc()
342 self._append_to_console_log(_('[ SENDING CONTINUE SIGNAL ]\n'))
343 self._test_runner.send_continue_signal()
344 if self.active_status_bar:
345 self.statusbar_message(_('[ SENDING CONTINUE SIGNAL ]\n'), 5000)
347 def on_step_next(self, event):
348 __ = event
349 self._reset_memory_calc()
350 self._append_to_console_log(_('[ SENDING STEP NEXT SIGNAL ]\n'))
351 self._test_runner.send_step_next_signal()
352 if self.active_status_bar:
353 self.statusbar_message(_('[ SENDING STEP NEXT SIGNAL ]\n'), 5000)
355 def on_step_over(self, event):
356 __ = event
357 self._reset_memory_calc()
358 self._append_to_console_log(_('[ SENDING STEP OVER SIGNAL ]\n'))
359 self._test_runner.send_step_over_signal()
360 if self.active_status_bar:
361 self.statusbar_message(_('[ SENDING STEP OVER SIGNAL ]\n'), 5000)
363 def on_run(self, event):
364 """ Called when the user clicks or presses the F8, Run Tests """
365 __ = event
366 self._run_tests()
368 def on_run_debug(self, event):
369 """ Called when the user clicks or presses the F9, Run Tests with Debug
370 It can still be overwritten in RIDE Arguments line
371 """
372 __ = event
373 self._run_tests("DEBUG")
375 def _run_tests(self, log_level='INFO'):
376 if not self._can_start_running_tests():
377 return
378 if self.__getattr__('confirm run') \
379 and not self._tests_selected() \
380 and not self._ask_user_to_run_anyway():
381 # In Linux NO runs dialog 4 times
382 return
383 self._reset_memory_calc()
384 profile = self.get_current_profile()
385 self.use_colors = self.__getattr__('use colors')
386 command_args = self._create_command_args(profile.get_command_args(), log_level, self.use_colors)
387 args_file = self._save_command_args_in_file(command_args)
388 # print(f"DEBUG: testrunnerplugin _run_tests AFTER _save_command_args_in_file")
389 command = self._create_command(profile.get_command(), args_file)
390 self._initialize_variables_for_running(profile.get_settings(), command_args)
391 self._initialize_ui_for_running()
392 # DEBUG on Py3 it not shows correct if tags with latin chars
393 self._append_to_console_log(_("command: %s\n") % command)
394 try:
395 self._test_runner.run_command(command, self._get_current_working_dir(profile))
396 self._process_timer.Start(41) # roughly 24fps
397 self._set_running()
398 self._progress_bar.Start()
399 except Exception as e:
400 self._set_stopped()
401 error, log_message = self.get_current_profile().format_error(str(e), None)
402 self._append_to_console_log(error, source='stderr')
403 if log_message:
404 log_message.publish()
406 def _create_command_args(self, profile_command_args, log_level='INFO', use_colors=False):
407 return CommandArgs().with_existing_args(profile_command_args) \
408 .with_log_level(log_level) \
409 .with_output_directory(self._default_output_dir) \
410 .with_python_path(self.global_settings.get('pythonpath', None)) \
411 .with_console_width(self._get_console_width()) \
412 .without_console_color(not use_colors) \
413 .with_runnable_tests(self._names_to_run) \
414 .build()
416 def _save_command_args_in_file(self, args):
417 arg_file = os.path.join(self._default_output_dir, 'argfile.txt')
418 FileWriter.write(arg_file, args, 'wb')
419 return arg_file
421 def _create_command(self, profile_command, args_file):
422 return Command().with_prefix(profile_command) \
423 .with_args_file(args_file) \
424 .with_listener(self._test_runner.get_listener_port(),
425 self._pause_on_failure) \
426 .with_tests_suite_file(self.model.suite.source) \
427 .build()
429 def _initialize_variables_for_running(self, profile_settings, args):
430 self._report_file = self._log_file = None
431 self._log_message_queue = Queue()
433 self._min_log_level_number = \
434 ArgsParser.get_message_log_level(args)
436 self._logs_directory = \
437 ArgsParser.get_output_directory(args, self._default_output_dir)
439 console_log_name = \
440 SettingsParser.get_console_log_name(profile_settings)
441 self._console_log = '' if not console_log_name \
442 else os.path.join(self._logs_directory, console_log_name)
444 def _get_current_working_dir(self, profile):
445 if profile.name == runprofiles.CustomScriptProfile.name:
446 return profile.get_cwd()
447 if os.path.isdir(self.model.suite.source):
448 return self.model.suite.source
449 return os.path.dirname(self.model.suite.source)
451 def _can_start_running_tests(self):
452 if self._running or self.model.suite is None:
453 return False
454 if not self.is_unsaved_changes():
455 return True
456 if self.auto_save or self._ask_user_to_save_before_running():
457 self.save_all_unsaved_changes()
458 return True
459 return False
461 @staticmethod
462 def _ask_user_to_save_before_running():
463 message_box = RIDEDialog(title=_("Unsaved Modifications"),
464 message=_("""There are unsaved modifications.
465 Do you want to save all changes and run the tests?"""), style=wx.ICON_QUESTION | wx.YES_NO)
466 ret = message_box.ShowModal()
467 return ret == wx.ID_YES
469 def _tests_selected(self):
470 return len(self._selected_tests) != 0
472 @staticmethod
473 def _ask_user_to_run_anyway():
474 message_box = RIDEDialog(title=_('No tests selected'), message=_('No tests selected. \nContinue anyway?'),
475 style=wx.ICON_QUESTION | wx.YES_NO)
476 ret = message_box.ShowModal()
477 return ret == wx.ID_YES
479 def _initialize_ui_for_running(self):
480 self._show_notebook_tab()
481 self._clear_log_ctrls()
482 self._local_toolbar.EnableTool(ID_OPEN_LOGS_DIR, False)
483 self._local_toolbar.EnableTool(ID_SHOW_REPORT, False)
484 self._local_toolbar.EnableTool(ID_SHOW_LOG, False)
485 self._report_file = self._log_file = None
486 self._log_message_queue = Queue()
488 def _clear_log_ctrls(self):
489 self._clear_text_ctrl(self._console_log_ctrl)
490 self._clear_text_ctrl(self._message_log_ctrl)
492 @staticmethod
493 def _clear_text_ctrl(text_ctrl):
494 text_ctrl.SetReadOnly(False)
495 text_ctrl.ClearAll()
496 text_ctrl.SetReadOnly(True)
498 def on_open_logs_directory(self, event):
499 """Called when the user clicks on the "Open Logs Directory" button"""
500 __ = event
501 # Determine explorer defined tool
502 try:
503 tool = self.global_settings['General'][FILE_MANAGER]
504 except KeyError:
505 tool = None
506 if os.path.exists(self._logs_directory):
507 start_filemanager(self._logs_directory, tool)
508 else:
509 self._notify_user_no_logs_directory()
511 def on_show_report(self, event):
512 """Called when the user clicks on the "Report" button"""
513 __ = event
514 if self._report_file:
515 # wx.LaunchDefaultBrowser("file:%s" % os.path.abspath(self._report_file))
516 url = f"file:{os.path.abspath(self._report_file)}"
517 self.open_browser(url)
519 def on_show_log(self, event):
520 """Called when the user clicks on the "Log" button"""
521 __ = event
522 if self._log_file:
523 # wx.LaunchDefaultBrowser("file:%s" % os.path.abspath(self._log_file))
524 url = f"file:{os.path.abspath(self._log_file)}"
525 self.open_browser(url)
527 def open_browser(self, url):
528 # Determine custom browser definition
529 try:
530 browser = self.global_settings['Plugins']['Test Runner']['browser']
531 except KeyError:
532 browser = None
533 if browser:
534 subprocess.Popen([browser, url])
535 else:
536 wx.LaunchDefaultBrowser(url)
538 def on_process_ended(self, event):
539 __ = event
540 output, errors, log_message = self._test_runner.get_output_and_errors(
541 self.get_current_profile())
542 self._append_to_console_log(output)
543 self._read_report_and_log_from_stdout_if_needed()
544 if len(errors) > 0:
545 self._append_to_console_log(errors, source="stderr")
546 if self._process_timer:
547 self._process_timer.Stop()
548 self._set_stopped()
549 self._progress_bar.Stop()
550 now = datetime.datetime.now().timetuple()
551 self._append_to_console_log(_("\nTest finished {}").format(robottime.format_time(now)))
552 self._test_runner.command_ended()
553 if log_message:
554 log_message.publish()
555 self._local_toolbar.EnableTool(ID_OPEN_LOGS_DIR, True)
557 def _read_report_and_log_from_stdout_if_needed(self):
558 output = self._console_log_ctrl.GetText()
559 if not self._report_file:
560 self._report_file = \
561 self._get_report_or_log(output, self.report_regex)
562 if self._report_file:
563 self._local_toolbar.EnableTool(ID_SHOW_REPORT, True)
564 if not self._log_file:
565 self._log_file = self._get_report_or_log(output, self.log_regex)
566 if self._log_file:
567 self._local_toolbar.EnableTool(ID_SHOW_LOG, True)
569 @staticmethod
570 def _get_report_or_log(output, regex):
571 res = regex.search(output)
572 return res.group(1) if res and os.path.isfile(res.group(1)) else None
574 def on_timer(self, event):
575 """Get process output"""
576 __ = event
577 if not self._log_message_queue.empty():
578 if self._process.memory_info()[0] <= self._limitmemory:
579 texts = []
580 while not self._log_message_queue.empty():
581 texts += [self._log_message_queue.get()]
582 self._append_to_message_log('\n' + '\n'.join(texts))
583 else:
584 if not self._maxmemmsg:
585 self._maxmemmsg = '\n' + _("Messages log exceeded 80% of process memory, stopping for now...")
586 self._append_to_message_log(self._maxmemmsg, "stderr")
587 if not self._test_runner.is_running():
588 self.on_process_ended(None)
589 return
590 out_buffer, err_buffer, __ = self._test_runner.get_output_and_errors(self.get_current_profile())
591 if len(out_buffer) > 0:
592 self._append_to_console_log(out_buffer, source="stdout")
593 if len(err_buffer) > 0:
594 if self._get_last_output_char() != "\n":
595 # Robot prints partial lines to stdout to make the
596 # interactive experience better. It all goes to
597 # heck in a handbasket if something shows up on
598 # stderr. So, to fix that we'll add a newline if
599 # the previous character isn't a newline.
600 self._append_to_console_log("\n")
601 self._append_to_console_log(err_buffer, source="stderr")
603 def _get_last_output_char(self):
604 """Return the last character in the output window"""
605 pos = self._console_log_ctrl.PositionBefore(
606 self._console_log_ctrl.GetLength())
607 char = self._console_log_ctrl.GetCharAt(pos)
608 return chr(char)
610 def _show_notebook_tab(self):
611 """Show the Run notebook tab"""
612 if not self.panel:
613 self._add_tab_to_notebook()
614 self._reload_model()
615 self.show_tab(self.panel)
617 def _append_to_message_log(self, text, source="stdout"):
618 self._append_text(self._message_log_ctrl, text, source)
620 def _append_to_console_log(self, text, source="stdout"):
621 """Put output to the text control"""
622 self._append_text(self._console_log_ctrl, text, source)
623 if self._console_log:
624 FileWriter.write(self._console_log, [text], "ab", "a")
626 def _append_text(self, text_ctrl, text, source="stdout"):
627 # text could be bytes or str
628 if not self.panel or not text_ctrl:
629 return
630 self._color_map = list()
631 if self.use_colors:
632 text = self.parse_colors(text)
633 text_ctrl.update_scroll_width(text)
634 # we need this information to decide whether to autoscroll or not
635 new_text_start = text_ctrl.GetLength()
636 line_count = text_ctrl.GetLineCount()
637 last_visible_line = \
638 text_ctrl.GetFirstVisibleLine() + text_ctrl.LinesOnScreen() - 1
640 text_ctrl.SetReadOnly(False)
641 pos = text_ctrl.GetLastPosition() # DEBUG: Process \r with Replace
642 # print(f"DEBUG: _append_text lastposition={pos}")
643 text_ctrl.AppendText(text)
644 new_text_end = text_ctrl.GetLength()
646 if wx.VERSION < (4, 1, 0):
647 text_ctrl.StartStyling(new_text_start, 0x1f)
648 else:
649 text_ctrl.StartStyling(new_text_start)
650 text_ctrl.SetStyling(0, STYLE_DEFAULT)
651 if source == "stderr" and not self.use_colors:
652 text_ctrl.SetStyling(new_text_end - new_text_start, STYLE_STDERR)
654 if self.use_colors and self._color_map:
655 style = None
656 previous_start = None
657 for item in self._color_map:
658 previous_style = style
659 if item[1] == 'RED':
660 style = STYLE_FAIL
661 elif item[1] == 'GREEN':
662 style = STYLE_PASS
663 elif item[1] == 'YELLOW':
664 style = STYLE_SKIP
665 elif item[1] == 'BLUE':
666 style = STYLE_LINK
667 elif item[1] is None:
668 style = STYLE_DEFAULT
669 if style:
670 if wx.VERSION < (4, 1, 0):
671 text_ctrl.StartStyling(pos + item[0], 0x1f)
672 else:
673 text_ctrl.StartStyling(pos + item[0])
674 previous_start = item[0]
675 if previous_style and style == STYLE_DEFAULT:
676 text_ctrl.SetStyling(item[0] - previous_start, previous_style)
677 if previous_start and style:
678 text_ctrl.SetStyling(item[0] - previous_start, STYLE_DEFAULT)
680 text_ctrl.SetReadOnly(True)
681 if last_visible_line >= line_count - 4:
682 line_count = text_ctrl.GetLineCount()
683 text_ctrl.ScrollToLine(line_count)
685 def parse_colors(self, txt):
686 # print(f"DEBUG: enter parse_colors {txt}")
687 idx = 0
688 t_size = len(txt)
689 while idx < t_size:
690 # print(f"{str(txt[idx])}")
691 if txt[idx] == 27: # .startswith('\033[32m'):
692 color = False
693 if txt[idx + 1:idx+5] == b']8;;' and txt[idx+6] != 27: # New file:// URI in RF 7.1, Start
694 color = True
695 self.store_color(idx, 'BLUE')
696 if txt[idx + 1:idx+5] == b'[34m':
697 color = True
698 self.store_color(idx, 'BLUE')
699 # print(f"DEBUG: parse_colors got BLUE")
700 if txt[idx + 1:idx+5] == b'[33m':
701 color = True
702 self.store_color(idx, 'YELLOW')
703 # print(f"DEBUG: parse_colors got YELLOW")
704 if txt[idx+1:idx+5] == b'[32m':
705 color = True
706 self.store_color(idx, 'GREEN')
707 # print(f"DEBUG: parse_colors got GREEN")
708 if txt[idx+1:idx+5] == b'[31m':
709 color = True
710 self.store_color(idx, 'RED')
711 # print(f"DEBUG: parse_colors got RED")
712 if txt[idx+1:idx+4] == b'[0m':
713 # print(f"DEBUG: parse_colors reset to NORMAL")
714 self.store_color(idx, None)
715 txt = txt[:idx] + txt[idx+4:]
716 elif txt[idx + 1] == 92:
717 self.store_color(idx, None)
718 txt = txt[:idx] + txt[idx+2:]
719 elif txt[idx + 1:idx + 7] == b']8;;\x1b\\': # New file:// URI in RF 7.1, End
720 self.store_color(idx, None)
721 txt = txt[:idx] + txt[idx + 7:]
722 elif color:
723 txt = txt[:idx] + txt[idx+5:]
724 if idx >= len(txt):
725 break
726 t_size = len(txt)
727 if idx < t_size:
728 idx += 1
729 return txt
731 def store_color(self, idx, color):
732 self._color_map.append((idx, color))
734 def _get_console_width(self):
735 # robot wants to know a fixed size for output, so calculate the
736 # width of the window based on average width of a character. A
737 # little is subtracted just to make sure there's a little margin
738 out_width, _ = self._console_log_ctrl.GetSize()
739 char_width = self.__getattr__("font size")
740 return str(int(out_width / char_width) - 10)
742 def _remove_from_notebook(self):
743 """Remove the tab for this plugin from the notebook"""
744 if self.notebook:
745 self.notebook.allow_closing(self.panel)
746 self.notebook.delete_tab(self.panel)
748 def _build_runner_toolbar(self, parent):
749 toolbar = wx.ToolBar(parent, wx.ID_ANY,
750 style=wx.TB_HORIZONTAL | wx.TB_HORZ_TEXT | wx.TB_NODIVIDER)
751 toolbar.SetThemeEnabled(True)
752 toolbar.SetBackgroundColour(self._mysettings.color_background)
753 toolbar.SetForegroundColour(self._mysettings.color_foreground)
754 toolbar.SetOwnBackgroundColour(self._mysettings.color_background)
755 toolbar.SetOwnForegroundColour(self._mysettings.color_foreground)
756 toolbar.AddTool(ID_RUN, _("Start"), ImageProvider().TOOLBAR_PLAY,
757 wx.NullBitmap, wx.ITEM_NORMAL, shortHelp=_("Start robot"),
758 longHelp=_("Start running the robot test suite"))
759 toolbar.AddTool(ID_RUNDEBUG, _("Debug"), getBugIconBitmap(), wx.NullBitmap,
760 wx.ITEM_NORMAL, shortHelp=_("Start robot"),
761 longHelp=_("Start running the robot test suite "
762 "with DEBUG loglevel"))
763 toolbar.AddTool(ID_STOP, _("Stop"), ImageProvider().TOOLBAR_STOP,
764 wx.NullBitmap, wx.ITEM_NORMAL,
765 shortHelp=STOP_RUNNING_TEST,
766 longHelp=STOP_RUNNING_TEST)
767 toolbar.AddTool(ID_PAUSE, _("Pause"), ImageProvider().TOOLBAR_PAUSE,
768 wx.NullBitmap, wx.ITEM_NORMAL,
769 shortHelp=_("Pause test execution"),
770 longHelp=_("Pause test execution"))
771 toolbar.AddTool(ID_CONTINUE, _("Continue"),
772 ImageProvider().TOOLBAR_CONTINUE,
773 wx.NullBitmap, wx.ITEM_NORMAL,
774 shortHelp=_("Continue test execution"),
775 longHelp=_("Continue test execution"))
776 toolbar.AddTool(ID_STEP_NEXT, _("Next"), ImageProvider().TOOLBAR_NEXT,
777 wx.NullBitmap, wx.ITEM_NORMAL, shortHelp=_("Step next"),
778 longHelp=_("Step next"))
779 toolbar.AddTool(ID_STEP_OVER, STEP_OVER, ImageProvider().TOOLBAR_NEXT,
780 wx.NullBitmap, wx.ITEM_NORMAL, shortHelp=STEP_OVER,
781 longHelp=STEP_OVER)
782 for i in toolbar.GetChildren(): 782 ↛ 783line 782 didn't jump to line 783 because the loop on line 782 never started
783 i.SetBackgroundColour(self._mysettings.color_secondary_background)
784 i.SetOwnBackgroundColour(self._mysettings.color_secondary_background)
785 i.SetForegroundColour(self._mysettings.color_secondary_foreground)
786 toolbar.Realize()
787 self._bind_runner_toolbar_events(toolbar)
788 return toolbar
790 def _bind_runner_toolbar_events(self, toolbar):
791 for event, callback, idd in (
792 (wx.EVT_TOOL, self.on_run, ID_RUN),
793 (wx.EVT_TOOL, self.on_run_debug, ID_RUNDEBUG),
794 (wx.EVT_TOOL, self.on_stop, ID_STOP),
795 (wx.EVT_TOOL, self.on_pause, ID_PAUSE),
796 (wx.EVT_TOOL, self.on_continue, ID_CONTINUE),
797 (wx.EVT_TOOL, self.on_step_next, ID_STEP_NEXT),
798 (wx.EVT_TOOL, self.on_step_over, ID_STEP_OVER)):
799 toolbar.Bind(event, callback, id=idd)
801 @property
802 def general_font_size(self):
803 fsize = self.app.settings.get('General', None)['font size']
804 return fsize
806 def _build_local_toolbar(self, parent):
807 toolbar = wx.ToolBar(parent, wx.ID_ANY,
808 style=wx.TB_HORIZONTAL | wx.TB_HORZ_TEXT | wx.TB_NODIVIDER | wx.TB_DOCKABLE)
809 # print(f"DEBUG: toolbar before {toolbar.UseBackgroundColour()}")
810 toolbar.SetThemeEnabled(True)
811 toolbar.SetBackgroundColour(self._mysettings.color_background)
812 toolbar.SetForegroundColour(self._mysettings.color_foreground)
813 toolbar.SetOwnBackgroundColour(self._mysettings.color_background)
814 toolbar.SetOwnForegroundColour(self._mysettings.color_foreground)
815 profile_label = Label(toolbar, label=_("Execution Profile: "))
816 choices = self._test_runner.get_profile_names()
817 self.choice = wx.Choice(toolbar, wx.ID_ANY, choices=choices)
818 self.choice.SetToolTip(wx.ToolTip(_("Choose which method to use for "
819 "running the tests")))
820 toolbar.AddControl(profile_label)
821 toolbar.AddControl(self.choice)
822 toolbar.AddSeparator()
823 report_image = getReportIconBitmap()
824 log_image = getLogIconBitmap()
825 toolbar.AddTool(ID_OPEN_LOGS_DIR, _("Open Logs Directory"),
826 ImageProvider().DATADIRIMG,
827 shortHelp=_("View All Logs in Explorer"))
828 toolbar.AddTool(ID_SHOW_REPORT, _(" Report"), report_image,
829 shortHelp=localize_shortcuts(_("View Robot Report in "
830 "Browser (CtrlCmd-R)")))
831 toolbar.AddTool(ID_SHOW_LOG, _(" Log"), log_image,
832 shortHelp=localize_shortcuts(_("View Robot Log in"
833 " Browser (CtrlCmd-L)")))
834 toolbar.AddSeparator()
835 # the toolbar API doesn't give us a way to specify padding which
836 # is why the label has a couple spaces after the colon. gross,
837 # but effective.
838 self.autosave_cb = \
839 self._create_check_box(toolbar, ID_AUTOSAVE, _(" Autosave "),
840 self.auto_save, _("Automatically save all changes before running"))
841 toolbar.AddControl(self.autosave_cb)
843 self.pause_on_failure_cb = \
844 self._create_check_box(toolbar, ID_PAUSE_ON_FAILURE,
845 _(" Pause after failure "), False,
846 _("Automatically pause after failing keyword"))
847 toolbar.AddControl(self.pause_on_failure_cb)
848 config_button = ButtonWithHandler(toolbar, _('Settings'), bitmap='wrench.png', fsize=self.general_font_size,
849 handler=lambda e: self.on_config_panel())
850 config_button.SetBackgroundColour(self._mysettings.color_background)
851 config_button.SetOwnBackgroundColour(self._mysettings.color_background)
852 config_button.SetForegroundColour(self._mysettings.color_foreground)
853 toolbar.AddStretchableSpace()
854 toolbar.AddControl(config_button)
855 toolbar.EnableTool(ID_OPEN_LOGS_DIR, False)
856 toolbar.EnableTool(ID_SHOW_LOG, False)
857 toolbar.EnableTool(ID_SHOW_REPORT, False)
858 for i in toolbar.GetChildren():
859 i.SetBackgroundColour(self._mysettings.color_background)
860 i.SetOwnBackgroundColour(self._mysettings.color_background)
861 i.SetForegroundColour(self._mysettings.color_foreground)
862 toolbar.Realize()
863 self._bind_local_toolbar_events(toolbar)
864 # print(f"DEBUG: toolbar end {toolbar.UseBackgroundColour()}")
865 return toolbar
867 def _bind_local_toolbar_events(self, toolbar):
868 for event, callback, idd in (
869 (wx.EVT_TOOL, self.on_open_logs_directory, ID_OPEN_LOGS_DIR),
870 (wx.EVT_TOOL, self.on_show_report, ID_SHOW_REPORT),
871 (wx.EVT_TOOL, self.on_show_log, ID_SHOW_LOG)):
872 toolbar.Bind(event, callback, id=idd)
874 for event, handler, source in (
875 (wx.EVT_CHECKBOX, self._on_autosave_cb,
876 self.autosave_cb),
877 (wx.EVT_CHECKBOX, self._on_pause_on_failure_cb,
878 self.pause_on_failure_cb),
879 (wx.EVT_CHOICE, self._on_profile_selection, self.choice)):
880 toolbar.Bind(event, handler, source)
882 def _on_autosave_cb(self, evt):
883 """Called when the user clicks on the "Auto Save" checkbox"""
884 self.save_setting("auto_save", evt.IsChecked())
886 def _on_pause_on_failure_cb(self, evt):
887 self._pause_on_failure = evt.IsChecked()
888 self._test_runner.send_pause_on_failure(evt.IsChecked())
890 def _on_profile_selection(self, event):
891 self.save_setting("profile_name", event.GetString())
892 self._set_profile(self.profile_name)
894 def _init_profile_choice(self, profile_name):
895 """First installation of the profile to be used to run tests"""
896 items = self.choice.GetItems()
897 if profile_name not in items: 897 ↛ 898line 897 didn't jump to line 898 because the condition on line 897 was never true
898 return
899 choice_index = items.index(profile_name)
900 self.choice.Select(choice_index)
901 self._set_profile(profile_name)
903 def _set_profile(self, profile_name):
904 """Set the profile to be used to run tests"""
905 profile = self._test_runner.get_profile(profile_name)
906 self._profile_toolbar = profile.get_toolbar(self._config_panel)
908 if self._profile_toolbar: 908 ↛ exitline 908 didn't return from function '_set_profile' because the condition on line 908 was always true
909 sizer = self._config_panel.GetSizer()
910 sizer.ShowItems(False)
911 sizer.Clear()
912 sizer.Add(self._profile_toolbar, 0, wx.EXPAND)
913 sizer.ShowItems(True)
914 self._config_panel.Parent.Layout()
916 def get_current_profile(self):
917 return self._test_runner.get_profile(self.choice.GetStringSelection())
919 def _add_tab_to_notebook(self):
920 self.panel = wx.Panel(self.notebook)
921 self._mysettings = RIDEDialog(parent=self.panel)
922 self.panel.SetBackgroundColour(self._mysettings.color_background)
923 self.panel.SetForegroundColour(self._mysettings.color_foreground)
924 self._local_toolbar = self._build_local_toolbar(self.panel)
925 self._runner_toolbar = self._build_runner_toolbar(self.panel)
926 self._config_panel = self._build_config_panel(self.panel)
928 sizer = wx.BoxSizer(wx.VERTICAL)
929 sizer.Add(self._local_toolbar, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 5)
930 sizer.Add(wx.StaticLine(self.panel), 0, wx.EXPAND | wx.BOTTOM | wx.TOP, 7)
931 sizer.Add(self._runner_toolbar, 0, wx.EXPAND | wx.ALL, 5)
932 sizer.Add(wx.StaticLine(self.panel), 0, wx.EXPAND | wx.BOTTOM | wx.TOP, 4)
933 sizer.Add(self._config_panel, 0, wx.EXPAND, 5)
934 sizer.Add(wx.StaticLine(self.panel), 0, wx.EXPAND | wx.BOTTOM | wx.TOP, 4)
935 self._output_panel = self._build_output_panel(self.panel)
936 sizer.Add(self._output_panel, 1, wx.EXPAND | wx.TOP, 5)
937 self.panel.SetSizer(sizer)
939 self._process_timer = wx.Timer(self.panel)
940 self.panel.Bind(wx.EVT_TIMER, self.on_timer)
941 self.panel.Bind(wx.EVT_WINDOW_DESTROY, self.on_close)
943 self.add_tab(self.panel, self.title, allow_closing=False)
945 def _build_config_panel(self, parent):
946 """Builds the configuration panel for this plugin"""
947 panel = wx.Panel(parent, wx.ID_ANY,
948 style=wx.BORDER_NONE | wx.TAB_TRAVERSAL)
949 panel.SetBackgroundColour(self._mysettings.color_background)
950 panel.SetForegroundColour(self._mysettings.color_foreground)
951 vertical_sizer = wx.BoxSizer(wx.VERTICAL)
952 panel.SetSizer(vertical_sizer)
953 return panel
955 def _build_output_panel(self, parent):
956 panel = wx.Panel(parent)
957 panel.SetBackgroundColour(self._mysettings.color_background)
958 panel.SetForegroundColour(self._mysettings.color_foreground)
959 self._progress_bar = ProgressBar(panel, self.fail_color, self.pass_color, self.skip_color,
960 self.active_status_bar, caller=self)
961 self._console_log_panel, self._console_log_ctrl = \
962 self._create_collapsible_pane(panel, _('Console log'),
963 self.show_console_log,
964 self.on_console_log_pane_changed)
965 self._message_log_panel, self._message_log_ctrl = \
966 self._create_collapsible_pane(panel, _('Message log'),
967 self.show_message_log,
968 self.on_message_log_pane_changed)
970 panel_sizer = wx.BoxSizer(wx.VERTICAL)
971 panel_sizer.Add(self._progress_bar, 0, wx.EXPAND | wx.BOTTOM, 10)
972 panel_sizer.Add(self._console_log_panel, int(self.show_console_log), wx.EXPAND)
973 panel_sizer.Add(self._message_log_panel, int(self.show_message_log), wx.EXPAND)
974 panel.SetSizer(panel_sizer)
975 return panel
977 def on_console_log_pane_changed(self, evt):
978 self.save_setting("show_console_log", not evt.Collapsed)
979 self._change_item_proportion(self._output_panel,
980 self._console_log_panel,
981 int(not evt.Collapsed))
982 self._output_panel.Layout()
984 def on_message_log_pane_changed(self, evt):
985 self.save_setting("show_message_log", not evt.Collapsed)
986 self._change_item_proportion(self._output_panel,
987 self._message_log_panel,
988 int(not evt.Collapsed))
989 self._output_panel.Layout()
991 @staticmethod
992 def _change_item_proportion(panel, item, proportion):
993 sizer = panel.GetSizer()
994 children = sizer.GetChildren()
995 for item_index in range(len(children)):
996 if item == children[item_index].Window:
997 sizer.Detach(item)
998 sizer.Insert(item_index, item, proportion, wx.EXPAND)
999 return
1001 def _create_collapsible_pane(self, parent, title, expand,
1002 pane_changed_handler):
1003 collapsible_pane = wx.CollapsiblePane(
1004 parent, wx.ID_ANY, title,
1005 style=wx.CP_DEFAULT_STYLE | wx.CP_NO_TLW_RESIZE)
1006 collapsible_pane.SetBackgroundColour(self._mysettings.color_background)
1007 collapsible_pane.SetForegroundColour(self._mysettings.color_foreground)
1008 if expand: 1008 ↛ 1010line 1008 didn't jump to line 1010 because the condition on line 1008 was always true
1009 collapsible_pane.Expand()
1010 collapsible_pane.Bind(wx.EVT_COLLAPSIBLEPANE_CHANGED,
1011 pane_changed_handler,
1012 collapsible_pane)
1014 pane = collapsible_pane.GetPane()
1015 pane.SetBackgroundColour(self._mysettings.color_background)
1016 pane.SetForegroundColour(self._mysettings.color_foreground)
1017 text_ctrl = self._create_text_ctrl(pane)
1018 text_ctrl.SetBackgroundColour(self._mysettings.color_background)
1019 text_ctrl.SetForegroundColour(self._mysettings.color_foreground)
1020 vertical_sizer = wx.BoxSizer(wx.VERTICAL)
1021 vertical_sizer.Add(text_ctrl, 1, wx.EXPAND)
1022 pane.SetSizer(vertical_sizer)
1023 return collapsible_pane, text_ctrl
1025 def _create_text_ctrl(self, parent):
1026 text_ctrl = OutputStyledTextCtrl(parent)
1027 text_ctrl.SetScrollWidth(100)
1028 self._set_margins(text_ctrl)
1029 text_ctrl.SetReadOnly(True)
1030 return text_ctrl
1032 @staticmethod
1033 def _create_check_box(parent, idd, label, value, tooltip):
1034 cb = wx.CheckBox(parent, idd, label)
1035 cb.SetToolTip(wx.ToolTip(tooltip))
1036 cb.SetValue(value)
1037 return cb
1039 @staticmethod
1040 def _set_margins(out):
1041 out.SetMarginLeft(10)
1042 out.SetMarginWidth(0, 0)
1043 out.SetMarginWidth(1, 0)
1044 out.SetMarginWidth(2, 0)
1045 out.SetMarginWidth(3, 0)
1047 def _test_runner_events_handler(self, event, *args):
1048 """Endpoint of the listener interface
1050 This is called via the listener interface. It has an event such as
1051 "start_suite", "start_test", etc., along with metadata about the event.
1052 We use this data to update
1053 the tree and statusbar."""
1054 if not self.panel:
1055 # this should only happen if the notebook tab got deleted
1056 # out from under us. In the immortal words of Jar Jar
1057 # Binks, "How rude!"
1058 return
1059 if event == 'start_test':
1060 self._handle_start_test(args)
1061 return
1062 if event == 'end_test':
1063 self._handle_end_test(args)
1064 return
1065 if event == 'report_file':
1066 self._handle_report_file(args)
1067 return
1068 if event == 'log_file':
1069 self._handle_log_file(args)
1070 return
1071 if event == 'start_keyword':
1072 self._handle_start_keyword(args)
1073 return
1074 if event == 'end_keyword':
1075 self._handle_end_keyword()
1076 return
1077 if event == 'log_message':
1078 self._handle_log_message(args)
1079 return
1080 if event == 'paused':
1081 self._handle_paused(args)
1082 return
1083 if event == 'continue':
1084 self._handle_continue(args)
1086 def _handle_start_test(self, args):
1087 longname = args[1]['longname'].encode('utf-8')
1088 self._log_message_queue.put(
1089 f"{_('Starting test:')} {longname.decode(encoding['OUTPUT'], 'backslashreplace')}")
1091 def _handle_end_test(self, args):
1092 longname = args[1]['longname'].encode('utf-8')
1093 self._log_message_queue.put(
1094 f"{_('Ending test:')} {longname.decode(encoding['OUTPUT'], 'backslashreplace')}\n")
1095 if args[1]['status'] == 'PASS':
1096 self._progress_bar.add_pass()
1097 elif args[1]['status'] == 'SKIP':
1098 self._progress_bar.add_skip()
1099 elif args[1]['status'] == 'FAIL':
1100 self._progress_bar.add_fail()
1101 else:
1102 self._log_message_queue.put(f"{_('UNKNOWN STATUS:')} {args[1]['status']}\n")
1104 def _handle_report_file(self, args):
1105 self._report_file = args[0]
1106 wx.CallAfter(self._local_toolbar.EnableTool, ID_SHOW_REPORT, True)
1108 def _handle_log_file(self, args):
1109 self._log_file = args[0]
1110 wx.CallAfter(self._local_toolbar.EnableTool, ID_SHOW_LOG, True)
1112 def _handle_start_keyword(self, args):
1113 self._progress_bar.set_current_keyword(args[0])
1115 def _handle_end_keyword(self):
1116 self._progress_bar.empty_current_keyword()
1118 def _handle_log_message(self, args):
1119 a = args[0]
1120 if LOG_LEVELS[a['level']] >= self._min_log_level_number:
1121 prefix = '%s : %s : ' % (a['timestamp'], a['level'].rjust(5))
1122 message = a['message']
1123 if '\n' in message:
1124 message = '\n' + message
1125 self._log_message_queue.put(prefix + message)
1127 def _handle_paused(self, args):
1128 __ = args
1129 wx.CallAfter(self._set_paused)
1130 self._log_message_queue.put(_('<< PAUSED >>'))
1131 if self.active_status_bar:
1132 self.statusbar_message(_('<< PAUSED >>'))
1134 def _handle_continue(self, args):
1135 __ = args
1136 wx.CallAfter(self._set_continue)
1137 self._log_message_queue.put(_('<< CONTINUE >>'))
1138 if self.active_status_bar:
1139 self.statusbar_message(_('<< CONTINUE >>'), 5000)
1141 def _set_running(self):
1142 self._run_action.disable()
1143 self._stop_action.enable()
1144 self._enable_runner_toolbar(False, True)
1145 self.get_current_profile().disable_toolbar()
1146 self._running = True
1147 self._test_runner.test_execution_started()
1148 if self.active_status_bar:
1149 self.statusbar_clear()
1151 def _set_paused(self):
1152 self._run_action.disable()
1153 self._stop_action.enable()
1154 self._enable_runner_toolbar(False, False)
1156 def _set_continue(self):
1157 self._run_action.disable()
1158 self._stop_action.enable()
1159 self._enable_runner_toolbar(False, True)
1161 def _set_stopped(self):
1162 self._run_action.enable()
1163 self._stop_action.disable()
1164 self._enable_runner_toolbar(True, False)
1165 self.get_current_profile().enable_toolbar()
1166 self._running = False
1167 if self.active_status_bar: 1167 ↛ exitline 1167 didn't return from function '_set_stopped' because the condition on line 1167 was always true
1168 self.statusbar_clear()
1170 def _enable_runner_toolbar(self, run, paused):
1171 stop = not run
1172 debug = stop and not paused
1173 for idd, enabled in ((ID_RUN, run),
1174 (ID_RUNDEBUG, run),
1175 (ID_STOP, stop),
1176 (ID_PAUSE, paused),
1177 (ID_CONTINUE, debug),
1178 (ID_STEP_NEXT, debug),
1179 (ID_STEP_OVER, debug)):
1180 self._runner_toolbar.EnableTool(idd, enabled)
1182 @staticmethod
1183 def _notify_user_no_logs_directory():
1184 message_box = RIDEDialog(title=_("No logs directory"), message=_("There isn't logs directory. \n"
1185 "Please, run the tests and try again"),
1186 style=wx.OK | wx.ICON_INFORMATION)
1187 message_box.ShowModal()
1189 def on_config_panel(self):
1190 dlg = self.config_panel(self.frame)
1191 dlg.Show(True)
1193 def config_panel(self, parent):
1194 __ = parent
1195 _parent = wx.GetTopLevelWindows()
1196 dlg = PreferenceEditor(_parent[0], _("RIDE - Preferences"),
1197 self.app.preferences, style='single', index=5)
1198 dlg.Show(False)
1199 return dlg
1202class ProgressBar(wx.Panel):
1203 """A progress bar for the test runner plugin"""
1205 def __init__(self, parent, fail_color='#FF8E8E', pass_color="#9FCC9F", skip_color='yellow',
1206 active_status_bar=False, caller=None):
1207 wx.Panel.__init__(self, parent, wx.ID_ANY)
1208 self._sizer = wx.BoxSizer(wx.HORIZONTAL)
1209 self._gauge = wx.Gauge(self, size=(100, 15), style=wx.GA_HORIZONTAL)
1210 self._label = Label(self)
1211 self._sizer.Add(self._label, 1, wx.EXPAND | wx.LEFT, 10)
1212 self._sizer.Add(self._gauge, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 20)
1213 self._sizer.Layout()
1214 self.SetSizer(self._sizer)
1215 self._gauge.Hide()
1216 self._default_colour = parent.GetBackgroundColour()
1217 self._foreground_colour = parent.GetForegroundColour()
1218 self.active_status_bar = active_status_bar
1219 self.caller = caller
1220 self.fail_color = fail_color
1221 self.pass_color = pass_color
1222 self.skip_color = skip_color
1223 self._timer = wx.Timer(self)
1224 self._start_time = None
1225 self.Bind(wx.EVT_TIMER, self.on_timer)
1226 self._initialize_state()
1228 def _initialize_state(self):
1229 self._pass = 0
1230 self._fail = 0
1231 self._skip = 0
1232 self._current_keywords = []
1234 def set_current_keyword(self, name):
1235 self._current_keywords.append(name)
1237 def empty_current_keyword(self):
1238 if self._current_keywords:
1239 self._current_keywords.pop()
1241 def on_timer(self, event):
1242 """A handler for timer events; it updates the statusbar"""
1243 __ = event
1244 self._gauge.Show()
1245 self._gauge.Pulse()
1246 self._update_message()
1248 def Start(self):
1249 """Signals the start of a test run; initialize progressbar."""
1250 self._initialize_state()
1251 self._start_time = time.time()
1252 self._gauge.SetForegroundColour(self._default_colour)
1253 self._gauge.Show()
1254 self._sizer.Layout()
1255 self.SetForegroundColour(self._foreground_colour)
1256 self.SetBackgroundColour(self._default_colour)
1257 self._timer.Start(50)
1259 def Stop(self):
1260 """Signals the end of a test run"""
1261 self._gauge.Hide()
1262 self._timer.Stop()
1264 def add_pass(self):
1265 """Add one to the passed count"""
1266 self._pass += 1
1268 def add_fail(self):
1269 """Add one to the failed count"""
1270 self._fail += 1
1272 def add_skip(self):
1273 """Add one to the skipped count"""
1274 self._skip += 1
1276 def get_visible_color(self, color):
1277 color_diff = wx.Colour.GetRGBA(wx.Colour(color)) - wx.Colour.GetRGBA(self._foreground_colour)
1278 if (wx.Colour.GetRGBA(wx.Colour(color)) > wx.Colour.GetRGBA(self._foreground_colour)
1279 > wx.Colour.GetRGBA(wx.Colour('gray'))):
1280 if color_diff > wx.Colour.GetRGBA(wx.Colour('gray')):
1281 return wx.Colour(self._foreground_colour)
1282 return wx.Colour('black')
1284 def _update_message(self):
1285 """Update the displayed elapsed time, passed and failed counts"""
1286 elapsed = time.time() - self._start_time
1287 message = _("elapsed time: %s pass: %s skip: %s fail: %s") % (
1288 self._seconds_to_string(elapsed), self._pass, self._skip, self._fail)
1289 current_keyword = self._get_current_keyword_text()
1290 message += current_keyword
1291 if self._fail > 0:
1292 self.SetForegroundColour(self.get_visible_color(self.fail_color))
1293 self.SetBackgroundColour(self.fail_color)
1294 self._label.SetForegroundColour(self.get_visible_color(self.fail_color))
1295 self._label.SetBackgroundColour(self.fail_color)
1296 elif self._skip > 0:
1297 self.SetForegroundColour(self.get_visible_color(self.skip_color))
1298 self.SetBackgroundColour(self.skip_color)
1299 self._label.SetForegroundColour(self.get_visible_color(self.skip_color))
1300 self._label.SetBackgroundColour(self.skip_color)
1301 elif self._pass > 0:
1302 self.SetForegroundColour(self.get_visible_color(self.pass_color))
1303 self.SetBackgroundColour(self.pass_color)
1304 self._label.SetForegroundColour(self.get_visible_color(self.pass_color))
1305 self._label.SetBackgroundColour(self.pass_color)
1306 else:
1307 self.SetForegroundColour(self._foreground_colour)
1308 self.SetBackgroundColour(self._default_colour)
1309 self._label.SetForegroundColour(self._foreground_colour)
1310 self._label.SetBackgroundColour(self._default_colour)
1311 self._label.SetLabel(message)
1312 if self.active_status_bar and self.caller:
1313 self.caller.statusbar_message(current_keyword)
1314 # not sure why this is required, but without it the background
1315 # colors don't look right on Windows
1316 self.Refresh()
1318 def _get_current_keyword_text(self):
1319 if not self._current_keywords:
1320 return ''
1321 return _(' current keyword: ') + self._fix_size(' -> '.join(self._current_keywords), 50)
1323 @staticmethod
1324 def _fix_size(text, max_length):
1325 if len(text) <= max_length:
1326 return text
1327 return '...' + text[3 - max_length:]
1329 # stole this off the internet. Nifty.
1330 @staticmethod
1331 def _seconds_to_string(t):
1332 """Convert a number of seconds to a string of the form HH:MM:SS"""
1333 return "%d:%02d:%02d" % \
1334 reduce(lambda ll, b: divmod(ll[0], b) + ll[1:], [(t,), 60, 60])
1337class OutputStyledTextCtrl(wx.stc.StyledTextCtrl):
1339 def __init__(self, parent):
1340 wx.stc.StyledTextCtrl.__init__(self, parent, wx.ID_ANY,
1341 style=wx.SUNKEN_BORDER)
1342 app_settings = self._get_app_settings(parent)
1343 self.stylizer = OutputStylizer(self, app_settings)
1344 self._max_row_len = 0
1346 def update_scroll_width(self, string):
1347 if isinstance(string, bytes):
1348 linesep = b'\n'
1349 else:
1350 linesep = '\n'
1351 string_max_len = max(len(s) for s in string.split(linesep))
1352 if string_max_len <= self._max_row_len:
1353 return
1354 self._max_row_len = string_max_len
1355 try:
1356 width, _ = self.GetTextExtent(string)
1357 if self.GetScrollWidth() < width + 50:
1358 self.SetScrollWidth(width + 50)
1359 except UnicodeDecodeError:
1360 pass
1362 @staticmethod
1363 def _get_app_settings(parent):
1364 while True:
1365 if not parent: 1365 ↛ 1366line 1365 didn't jump to line 1366 because the condition on line 1365 was never true
1366 raise ValueError('Value does not contain NoteBook as parent')
1367 if isinstance(parent, NoteBook):
1368 return parent.app.settings
1369 parent = parent.GetParent()
1372class OutputStylizer(object):
1374 def __init__(self, editor, settings):
1375 self.editor = editor
1376 self.settings = settings.config_obj['Plugins']['Test Runner']
1377 self._ensure_default_font_is_valid()
1378 self._set_styles()
1379 PUBLISHER.subscribe(self.on_settings_changed, RideSettingsChanged)
1381 def on_settings_changed(self, message):
1382 """Redraw colors and font if settings are modified"""
1383 section, _ = message.keys
1384 if section == 'Test Runner': 1384 ↛ 1385line 1384 didn't jump to line 1385 because the condition on line 1384 was never true
1385 self._set_styles()
1387 def _set_styles(self):
1388 """Sets plugin styles"""
1389 background = self.settings.get('background', 'white')
1390 font_size = self.settings.get('font size', 10)
1391 font_face = self.settings.get(FONT_FACE, 'Courier New')
1392 self.fail_color = self.settings.get('fail color', '#FF8E8E')
1393 self.pass_color = self.settings.get('pass color', '#9FCC9F')
1394 self.skip_color = self.settings.get('skip color', 'yellow')
1395 self.link_color = self.settings.get('link color', '#1E1EFF')
1397 default_style = self._get_style_string(
1398 fore=self.settings.get('foreground', 'black'), back=background,
1399 size=font_size, face=font_face)
1400 error_style = self._get_style_string(
1401 fore=self.settings.get('error', 'red'), back=background,
1402 size=font_size, face=font_face)
1403 fail_style = self._get_style_string(fore=self.fail_color, back=background, size=font_size, face=font_face)
1404 pass_style = self._get_style_string(fore=self.pass_color, back=background, size=font_size, face=font_face)
1405 skip_style = self._get_style_string(fore=self.skip_color, back=background, size=font_size, face=font_face)
1406 link_style = self._get_style_string(fore=self.link_color, back=background, size=font_size, face=font_face,
1407 bold='bold')
1409 self.editor.StyleSetSpec(STYLE_DEFAULT, default_style)
1410 self.editor.StyleSetSpec(STYLE_STDERR, error_style)
1411 self.editor.StyleSetSpec(STYLE_FAIL, fail_style)
1412 self.editor.StyleSetSpec(STYLE_PASS, pass_style)
1413 self.editor.StyleSetSpec(STYLE_SKIP, skip_style)
1414 self.editor.StyleSetSpec(STYLE_LINK, link_style)
1415 self.editor.StyleSetSpec(7, error_style)
1416 self.editor.StyleSetBackground(wx.stc.STC_STYLE_DEFAULT, background)
1417 self.editor.Refresh()
1419 @staticmethod
1420 def _get_style_string(back, fore, size, face, bold=None):
1421 # print(f"DEBUG: testrunnerplugin.py get_style_string locals={locals().items()}")
1422 style = ','.join('%s:%s' % (name, value)
1423 for name, value in locals().items() if value and name!='bold')
1424 if bold is not None:
1425 style += ',bold'
1426 return style
1428 def _ensure_default_font_is_valid(self):
1429 """Checks if default font is installed"""
1430 default_font = self.settings.get(FONT_FACE)
1431 if default_font not in read_fonts(): 1431 ↛ 1432line 1431 didn't jump to line 1432 because the condition on line 1431 was never true
1432 sys_font = wx.SystemSettings.GetFont(wx.SYS_ANSI_FIXED_FONT)
1433 self.settings[FONT_FACE] = sys_font.GetFaceName()
1436Robot = PyEmbeddedImage(
1437 "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAnNJ"
1438 "REFUOI2Vkb1Pk3EQxz/PW6EvUN6sEQFBIwUlMBgTMZFZJzcXEzeJiXE1MXFi4g8gGhjcHDA4"
1439 "iFGDKNFojBoJaqQItgrlpYUW0ZZSaJ/ndw5INQZIvMttd5/73vcQEbYrpRSPes5K7NsrUaK2"
1440 "7RERdHaJnLeV4tL9u7XsDNA0qKhrw19erf0nQABBRBEeGyT86YUgIKjtF4nIP+PC0tsRGb11"
1441 "g+hcnAqvl6ZjrQQ7r664ygIV/8opAATIpr53fui53psZfoqsZcn5TEyXjlrPQcNBvMdO0XG5"
1442 "S4M/GPNvWnQ23Ptg4hW1xxsxLAssE0MHHIWgM/f+Me35a1iWmy1IASCOw+f+XhwMQuML/Eik"
1443 "WVA6mlLU6A7+AwEqKxSjN7vlxJUubUtEwcTJ8XF5PfAA23ZIJTMkppdoathLS7CO5EyS1M8M"
1444 "GjpDdwcR/vhWUHAo2KjtaWmWeWeJtlNH0DqamPwSxTQtTl88g21nWUlG6bhwficThWQsKpfO"
1445 "tWMkBFGQXc9j6RYuw8F0WXgOe+i7F9LQTLZu0Au/V8Lzh32UFBfjK3dRWlVEoMaDf59JSbUH"
1446 "d5ULv7uI+7e7RZT9+2+gC5sZ/Tom4U/P8PgMViVHWjZYNxxsl7Bh2uDTCFT7+Dw2ROjdw9/C"
1447 "BfN7fEp+LLxkMrxIKp0mGDxAc8s6dXvrQRc0TUfTYSocxs7rxBOrfHxzh3J/Tvz7TmImYhMs"
1448 "Rl4zG1lDicOT4RBHWyr5GBrH0DcvdGxFWUme+Zk0tY2lzM3NshyfxHDXo0fCEQb6R4hMx3Bs"
1449 "hTiCKMFtpsmoLHl7Ga8fRATHEcRRrCxnGBocIR6L8Qu2hlAKJu0L3QAAAABJRU5ErkJggg==")
1450getRobotBitmap = Robot.GetBitmap
1452MenuButton = PyEmbeddedImage(
1453 "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAKxJ"
1454 "REFUOI3t0jEKg0AUBNAxhmXX9QD2adLnJt7E2luIeB/PkCoQCG5lK8ifdZtNHyQRLGwy5Yd5"
1455 "/GKSGCP25LSr/QcAAOfPQ9/3MYSAZVngvQdJiAhEhFVVZT8BkpKmaZbnOZRS0FojhIBpmh6b"
1456 "Ppjn+ULyqZSyxhiM44hhGEiyXAOStSG1bVuIyMtaq51zJHltmsZtBgCgruuC5N17f+u6brX8"
1457 "Fdia43dwPPAGncZYbvceeuMAAAAASUVORK5CYII=")
1458getMenuButtonBitmap = MenuButton.GetBitmap
1460ProcessStop = PyEmbeddedImage(
1461 "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0"
1462 "RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJJSURBVDjLpZNNbxJRFIb7A/wF"
1463 "/A5YunRDovsmRk3cmLAxcdG0uiFuXDSmkBlLFNOmtYFKgibUtqlJG6UjiGksU0oZPgQs0KEw"
1464 "Mw4Dw8dQjnPuMCNq48abvJub87zn4547BQBTk7q2CDZdDl1OXdNjOcd3tj/jJ8Eruuxzb2RX"
1465 "+NMpHT/MMUfHJwKbSgv7Bxnm9YciPRMSXRiDsb8ZjOGrwWjNzZ4UOL4pg6IOQLsYEbU6fajW"
1466 "RYgdpLilnYIbY00T08COcCrzTen2NMCj9ocgKgMQdLV7Q3KnqH3YTyQV/1YWTezEAPvCsjGz"
1467 "CTfkPtR/9IGXDNWkHlTFnmWysxfj7q/x2I4NDRxh5juNZf8LPm12ifBkimdAheI0smjgjH3N"
1468 "MtgzlmqCNx5tGnq4Abe9LIHLjS7IHQ3OJRWW1zcYZNFgOnl0LOCwmq0BgTEjgqbQoHSuQrGu"
1469 "EqO+dgFrgXUBWWJwyKaIAZaPcEXoWvD1uQjc8rBQ4FUio4oBLK+8sgycH7+kGUnpQUvVrF4x"
1470 "K4KomwuGQf6sQ14mV5GA8gesFhyB3TxdrjZhNAKSwSzXzIpgrtaBbLUDg+EI9j6nwe3btIZo"
1471 "exBsuHajCU6QjSlfBmaqbZIgr2f3Pl/l7vpyxjOai0S9Zd2R91GFF41Aqa1Z1eAyYeZcRQSP"
1472 "P6jMUlu/FmlylecDCfdqKMLFk3ko8zKZCfacLgmwHWVhnlriZrzv/l7lyc9072XJ9fjFNv10"
1473 "cYWhnvmEBS8tPPH4mVlPmL5DZy7/TP/znX8C6zgR9sd1gukAAAAASUVORK5CYII=")
1474getProcessStopBitmap = ProcessStop.GetBitmap
1476# page_white.png from http://www.famfamfam.com/lab/icons/silk
1477ReportIcon = PyEmbeddedImage(
1478 "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAAK/INwWK6QAAABl0"
1479 "RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC4SURBVCjPdZFbDsIgEEWnrsMm"
1480 "7oGGfZrohxvU+Iq1TyjU60Bf1pac4Yc5YS4ZAtGWBMk/drQBOVwJlZrWYkLhsB8UV9K0BUrP"
1481 "Gy9cWbng2CtEEUmLGppPjRwpbixUKHBiZRS0p+ZGhvs4irNEvWD8heHpbsyDXznPhYFOyTjJ"
1482 "c13olIqzZCHBouE0FRMUjA+s1gTjaRgVFpqRwC8mfoXPPEVPS7LbRaJL2y7bOifRCTEli3U7"
1483 "BMWgLzKlW/CuebZPAAAAAElFTkSuQmCC")
1484getReportIconBitmap = ReportIcon.GetBitmap
1486# page_white_text.png from http://www.famfamfam.com/lab/icons/silk
1487LogIcon = PyEmbeddedImage(
1488 "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAAK/INwWK6QAAABl0"
1489 "RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAADoSURBVBgZBcExblNBGAbA2cee"
1490 "gTRBuIKOgiihSZNTcC5LUHAihNJR0kGKCDcYJY6D3/77MdOinTvzAgCw8ysThIvn/VojIyMj"
1491 "IyPP+bS1sUQIV2s95pBDDvmbP/mdkft83tpYguZq5Jh/OeaYh+yzy8hTHvNlaxNNczm+la9O"
1492 "Tlar1UdA/+C2A4trRCnD3jS8BB1obq2Gk6GU6QbQAS4BUaYSQAf4bhhKKTFdAzrAOwAxEUAH"
1493 "+KEM01SY3gM6wBsEAQB0gJ+maZoC3gI6iPYaAIBJsiRmHU0AALOeFC3aK2cWAACUXe7+AwO0"
1494 "lc9eTHYTAAAAAElFTkSuQmCC")
1495getLogIconBitmap = LogIcon.GetBitmap
1497# bug.png from http://www.famfamfam.com/lab/icons/silk
1498BugIcon = PyEmbeddedImage(
1499 "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0"
1500 "RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAKYSURBVDjLnZPJT1NRFMb5G1wD"
1501 "HV5boNiqdHrvFYolCAtsGSSWKpMFKhYqlDI6oAEKaVJwCIgSphaKtLYWCgSNBgRjMNHoxsSF"
1502 "S3cmJmA0NMTw+R6JKKZl4eJL7sm953fOd3JPHIC4WMpcppG5SGnZc8ZjVVF6QLn975sDgfaZ"
1503 "mvg71oRJZIRUYcuAnq/2KWroGfm3QwEn2YpLVPPvOD2oiqj9yq/mGznegl56mx6T7ZbY1M6Y"
1504 "AM0CuZkxT0b2Wg6QW/SsApRXDsotR+d6E9Y/h9DuqoCuJq0lKoDxqU1/pITGR27mBU4h+GEc"
1505 "Tz5OY+ClA5JbyahYzof/9TBO9B/FcWcqpA4xU3We3GJ87ntnfO5meinMvruNnqcmXA2XoDVc"
1506 "Cc0wCYkzBaZpA7ILRJ/2O2B87jA+QT9UeDRe8svZYAG8b/txc6kc9mA+yqayYPQXwvdmBEOr"
1507 "A5B2p0BtFIYOWKCm5RukWwZyXIbA+0F0LpaiKaBHmVsLw4we99ccsM8a8GClF5JOMcQdou8p"
1508 "rULrgRmQo7KI0VcE13MrGv06lE5kodhzGvdWu2GdKkTVWC4DcELcJkKyXbCb1EhAVM//M0DV"
1509 "UNqP2qAJd1baUDaZjTMTeXAttsPi0cM0mgvHvA0NkxYk2QRIrieOsDmEmXttH0DfVfSluSTo"
1510 "WmpD8bgOroUOWNw6VI7koGfOBuq6EqLLTNU6ojrmP5D1HVsjmrkYezGIrlA9LjKgnrlGXJlp"
1511 "gbCOD0EtD0QNN8I3cZqjAlhJr4rXpB1iNLhrYffUQWoT7yUKzbxqJlHLq0jc5JYmgHMunogK"
1512 "YJVqF7mTrPyfgktMRTMX/CrOq1gLF3fYNrLiX+Bs8MoTwT2fQPwXgBXHGL+TaIjfinb3C7cs"
1513 "cRMIcYL6AAAAAElFTkSuQmCC")
1514getBugIconBitmap = BugIcon.GetBitmap