Coverage for src/robotide/contrib/testrunner/runprofiles.py: 42%
434 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# Copyright 2010 Orbitz WorldWide
2#
3# Ammended by Helio Guilherme <helioxentric@gmail.com>
4# Copyright 2011-2015 Nokia Networks
5# Copyright 2016- Robot Framework Foundation
6#
7# Licensed under the Apache License, Version 2.0 (the "License");
8# you may not use this file except in compliance with the License.
9# You may obtain a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS,
15# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16# See the License for the specific language governing permissions and
17# limitations under the License.
19"""runProfiles.py
21This module contains profiles for running robot tests via the
22runnerPlugin.
24Each class that is a subclass as BaseProfile will appear in a
25drop-down list within the plugin. The chosen profile will be used to
26build up a command that will be passed in the tests to run as well as
27any additional arguments.
28"""
30import builtins
31import os
32import re
33import time
34import wx
36from robotide.publish.messages import RideLogMessage
37from robotide.context import IS_WINDOWS
38from robotide.contrib.testrunner.usages import USAGE
39from robotide.lib.robot.utils import format_time
40from robotide.robotapi import DataError, Information
41from robotide.utils import ArgumentParser
42from robotide.widgets import ButtonWithHandler, Label, RIDEDialog
43from sys import getfilesystemencoding
44from wx.lib.filebrowsebutton import FileBrowseButton
46_ = wx.GetTranslation # To keep linter/code analyser happy
47builtins.__dict__['_'] = wx.GetTranslation
49OUTPUT_ENCODING = getfilesystemencoding()
52class BaseProfile(object):
53 """Base class for all test runner profiles
55 At a minimum each profile must set the name attribute, which is
56 how the profile will appear in the dropdown list.
58 In case some settings are needed, provide default_settings class attribute
59 with default values.
61 This class (BaseProfile) will _not_ appear as one of the choices.
62 Think of it as an abstract class, if Python 2.5 had such a thing.
63 """
65 # this will be set to the plugin instance at runtime
66 plugin = None
67 default_settings = {}
69 def __init__(self, plugin):
70 """plugin is required so that the profiles can save their settings"""
71 self.plugin = plugin
72 self._panel = None
74 def get_toolbar(self, parent):
75 """Returns a panel with toolbar controls for this profile"""
76 if self._panel is None:
77 self._panel = wx.Panel(parent, wx.ID_ANY)
78 return self.panel
80 def enable_toolbar(self):
81 if self._panel is None:
82 return
83 self._panel.Enable()
85 def disable_toolbar(self):
86 if self._panel is None:
87 return
88 self._panel.Enable(False)
90 def delete_pressed(self):
91 """Handle delete key pressing"""
92 pass
94 def get_command(self):
95 """Returns a command for this profile"""
96 return 'robot'
98 def get_command_args(self):
99 """Return a list of command arguments unique to this profile.
101 Returned arguments are in format accepted by Robot Framework's argument
102 file.
103 """
104 return []
106 def get_settings(self):
107 """Return a list of settings unique to this profile.
109 Returned settings can be used when running tests.
110 """
111 return []
113 def set_setting(self, name, value):
114 """Sets a plugin setting
116 setting is automatically prefixed with profile's name, and it can be
117 accessed with direct attribute access. See also __getattr__.
118 """
119 self.plugin.save_setting(self._get_setting_name(name), value, delay=2)
121 def format_error(self, error, returncode):
122 return error, self._create_error_log_message(error, returncode)
124 def _create_error_log_message(self, error, returncode):
125 _ = error, returncode
126 return None
128 def __getattr__(self, name):
129 """Provides attribute access to profile's settings
131 If for example default_settings = {'setting1' = ""} is defined
132 then setting1 value can be used like self.setting1
133 set_setting is used to store the value.
134 """
135 try:
136 return getattr(self.plugin, self._get_setting_name(name))
137 except AttributeError:
138 try:
139 return getattr(self.plugin, name)
140 except AttributeError:
141 if name in self.default_settings: 141 ↛ 143line 141 didn't jump to line 143 because the condition on line 141 was always true
142 return self.default_settings[name]
143 raise
145 def _get_setting_name(self, name):
146 """Adds profile's name to the setting."""
147 return "%s_%s" % (self.name.replace(' ', '_'), name)
150RF_INSTALLATION_NOT_FOUND = """Robot Framework installation not found.<br>
151To run tests, you need to install Robot Framework separately.<br>
152See <a href="https://robotframework.org/">https://robotframework.org/</a> for
153installation instructions.
154"""
157class PybotProfile(BaseProfile):
158 """A runner profile which uses robot
160 It is assumed that robot is on the path
161 """
162 _quotes_re = re.compile('(.*)(\".*\")(.*)?')
164 name = "robot"
165 default_settings = {"arguments": "",
166 "output_directory": "",
167 "include_tags": "",
168 "exclude_tags": "",
169 "are_log_names_with_suite_name": False,
170 "are_log_names_with_timestamp": False,
171 "are_saving_logs": False,
172 "apply_include_tags": False,
173 "apply_exclude_tags": False}
175 def __init__(self, plugin):
176 BaseProfile.__init__(self, plugin)
177 self._defined_arguments = self.arguments
178 self._toolbar = None
179 self._mysettings = None
181 def get_toolbar(self, parent):
182 if self._toolbar is None: 182 ↛ 191line 182 didn't jump to line 191 because the condition on line 182 was always true
183 self._toolbar = wx.Panel(parent, wx.ID_ANY)
184 self._mysettings = RIDEDialog(parent=self._toolbar)
185 self._toolbar.SetBackgroundColour(self._mysettings.color_background)
186 self._toolbar.SetForegroundColour(self._mysettings.color_foreground)
187 sizer = wx.BoxSizer(wx.VERTICAL)
188 for item in self.get_toolbar_items(self._toolbar):
189 sizer.Add(item, 0, wx.EXPAND)
190 self._toolbar.SetSizer(sizer)
191 return self._toolbar
193 def get_toolbar_items(self, parent):
194 return [self._get_arguments_panel(parent),
195 self._get_tags_panel(parent),
196 self._get_log_options_panel(parent)]
198 def enable_toolbar(self):
199 if self._toolbar is None: 199 ↛ 200line 199 didn't jump to line 200 because the condition on line 199 was never true
200 return
201 self._enable_toolbar()
203 def disable_toolbar(self):
204 if self._toolbar is None:
205 return
206 self._enable_toolbar(False)
208 def _enable_toolbar(self, enable=True):
209 for panel in self._toolbar.GetChildren():
210 if isinstance(panel, wx.CollapsiblePane):
211 panel = panel.GetPane()
212 panel.Enable(enable)
214 def delete_pressed(self):
215 focused = wx.Window.FindFocus()
216 if focused not in [self._arguments, self._include_tags,
217 self._exclude_tags]:
218 return
219 start, end = focused.GetSelection()
220 focused.Remove(start, max(end, start + 1))
222 def get_command(self):
223 from subprocess import call
224 from tempfile import TemporaryFile
225 try:
226 with TemporaryFile(mode="at") as out:
227 result = call(["robot", "--version"], stdout=out)
228 if result == 251:
229 return "robot"
231 with TemporaryFile(mode="at") as out:
232 result = call(["robot.bat" if os.name == "nt" else "robot",
233 "--version"], stdout=out)
234 if result == 251:
235 return "robot.bat" if os.name == "nt" else "robot"
236 except OSError:
237 try:
238 with TemporaryFile(mode="at") as out:
239 result = call(["pybot.bat" if os.name == "nt" else "pybot",
240 "--version"], stdout=out)
241 if result == 251:
242 return "pybot.bat" if os.name == "nt" else "pybot"
243 except OSError:
244 result = "no pybot"
245 return result
247 def get_command_args(self):
248 args = self._get_arguments()
249 if self.output_directory and \
250 '--outputdir' not in args and \
251 '-d' not in args:
252 args.extend(['-d', os.path.abspath(self.output_directory)])
254 log_name_format = '%s'
255 if self.are_log_names_with_suite_name:
256 log_name_format = f'{self.plugin.model.suite.name}-%s'
257 if '--log' not in args and '-l' not in args:
258 args.extend(['-l', log_name_format % 'Log.html'])
259 if '--report' not in args and '-r' not in args:
260 args.extend(['-r', log_name_format % 'Report.html'])
261 if '--output' not in args and '-o' not in args:
262 args.extend(['-o', log_name_format % 'Output.xml'])
264 if self.are_saving_logs and \
265 '--debugfile' not in args and \
266 '-b' not in args:
267 args.extend(['-b', log_name_format % 'Message.log'])
269 if self.are_log_names_with_timestamp and \
270 '--timestampoutputs' not in args and \
271 '-T' not in args:
272 args.append('-T')
274 if self.apply_include_tags and self.include_tags:
275 for include in self._get_tags_from_string(self.include_tags):
276 args.append('--include=%s' % include)
278 if self.apply_exclude_tags and self.exclude_tags:
279 for exclude in self._get_tags_from_string(self.exclude_tags):
280 args.append('--exclude=%s' % exclude)
281 return args
283 def _get_arguments(self):
284 if IS_WINDOWS:
285 self._parse_windows_command()
286 else:
287 self._parse_posix_command()
288 return self._save_filenames()
290 def _save_filenames(self):
291 args = self._defined_arguments.replace('\\"', '"')
292 res = self._quotes_re.match(args)
293 if not res:
294 return args.strip().strip().split()
295 clean = []
296 # DEBUG: example args
297 # --xunit "another output file.xml" --variablefile "a test file for variables.py" -v abc:new
298 # --debugfile "debug file.log"
299 # print(f"DEBUG: Run Profiles _save_filenames res.groups {res.groups()}")
300 for gr in res.groups():
301 line = []
302 if gr is not None and gr != '':
303 second_m = re.split('"', gr)
304 # print(f"DEBUG: Run Profiles _save_filenames second_m = {second_m}")
305 m = len(second_m)
306 if m > 2: # the middle element is the content
307 m = len(second_m)
308 for idx in range(0, m):
309 if second_m[idx]:
310 if idx % 2 == 0:
311 line.extend(second_m[idx].strip().strip().split())
312 elif idx % 2 != 0:
313 line.append(f"{second_m[idx]}")
314 else:
315 for idx in range(0, m):
316 if second_m[idx]:
317 line.extend(second_m[idx].strip().strip().split())
318 clean.extend(line)
319 # Fix variables
320 # print(f"DEBUG: Run Profiles _save_filenames DEFORE FIX VARIABLES clean= {clean}")
321 for idx, value in enumerate(clean):
322 if value[-1] == ':' and idx + 1 < len(clean):
323 clean[idx] = ''.join([value, clean[idx+1]])
324 clean.pop(idx+1)
325 # print(f"DEBUG: Run Profiles _save_filenames returnin clean= {clean}")
326 return clean
328 def _parse_windows_command(self):
329 from subprocess import Popen, PIPE
330 try:
331 p = Popen(['echo', self.arguments], stdin=PIPE, stdout=PIPE,
332 stderr=PIPE, shell=True)
333 output, _ = p.communicate()
334 from ctypes import cdll
336 code_page = cdll.kernel32.GetConsoleCP()
337 if code_page == 0:
338 os_encoding = os.getenv('RIDE_ENCODING', OUTPUT_ENCODING)
339 else:
340 os_encoding = 'cp' + str(code_page)
341 try:
342 output = output.decode(os_encoding)
343 except UnicodeDecodeError:
344 message_box = RIDEDialog(title="UnicodeDecodeError",
345 message=f"An UnicodeDecodeError occurred when processing the Arguments."
346 f" The encoding used was '{os_encoding}'. You may try to define the environment variable"
347 f" RIDE_ENCODING with a proper value. Other possibility, is to replace 'pythonw.exe' by "
348 f"'python.exe' in the Desktop Shortcut.", style=wx.OK | wx.ICON_ERROR)
349 message_box.ShowModal()
350 output = str(output).lstrip("b\'").lstrip('"').replace('\\r\\n', '').replace('\'', '').\
351 replace('\\""', '\"').strip()
352 # print(f"DEBUG: run_profiles _parse_windows_command: output ={output}")
353 even = True
354 counter = 0
355 for idx in range(0, len(output)):
356 if output[idx] == '"':
357 counter += 1
358 even = counter % 2 == 0
359 # print(f"DEBUG: run_profiles loop({idx} counter:{counter}")
360 self._defined_arguments = output.replace('\'', '')\
361 .replace('\\\\', '\\').replace('\\r\\n', '')
362 if not even:
363 self._defined_arguments = self._defined_arguments.rstrip('"')
364 except IOError:
365 pass
367 def _parse_posix_command(self):
368 # print(f"DEBUG: run_profiles _parse_posix_command: ENTER self.arguments={self.arguments}")
369 from subprocess import Popen, PIPE
370 try:
371 p = Popen(['echo ' + self.arguments.replace('"', '\\"')], stdin=PIPE, stdout=PIPE,
372 stderr=PIPE, shell=True)
373 output, _ = p.communicate()
374 # print(f"DEBUG: run_profiles _parse_posix_command: RAW output ={output}")
375 output = str(output).lstrip("b\'").replace('\\n', '').rstrip("\'").strip()
376 # print(f"DEBUG: run_profiles _parse_posix_command: output ={output}")
377 even = True
378 counter = 0
379 for idx in range(0, len(output)):
380 if output[idx] == '"':
381 counter += 1
382 even = counter % 2 == 0
383 # print(f"DEBUG: run_profiles loop({idx} counter:{counter}")
384 self._defined_arguments = output.replace('\'', '')\
385 .replace('\\\\', '\\').replace('\\n', '')
386 if not even:
387 self._defined_arguments = self._defined_arguments.rstrip('"')
388 except IOError:
389 pass
391 @staticmethod
392 def _get_tags_from_string(tag_string):
393 tags = []
394 for tag in tag_string.split(","):
395 tag = tag.strip().replace(' ', '')
396 if len(tag) > 0:
397 tags.append(tag)
398 return tags
400 def get_settings(self):
401 settings = []
402 if self.are_saving_logs:
403 name = 'Console.txt'
404 if self.are_log_names_with_timestamp:
405 start_timestamp = format_time(time.time(), '', '-', '')
406 base, ext = os.path.splitext(name)
407 base = f'{base}-{start_timestamp}'
408 name = base + ext
410 if self.are_log_names_with_suite_name:
411 name = f'{self.plugin.model.suite.name}-{name}'
412 settings.extend(['console_log_name', name])
413 return settings
415 def _create_error_log_message(self, error, returncode):
416 # bash and zsh use return code 127 and the text `command not found`
417 # In Windows, the error is `The system cannot file the file specified`
418 if b'not found' in error \
419 or returncode == 127 or \
420 b'system cannot find the file specified' in error:
421 return RideLogMessage(RF_INSTALLATION_NOT_FOUND, notify_user=True)
422 return None
424 def _get_log_options_panel(self, parent):
425 collapsible_pane = wx.CollapsiblePane(
426 parent, wx.ID_ANY, _('Log options'),
427 style=wx.CP_DEFAULT_STYLE | wx.CP_NO_TLW_RESIZE)
428 collapsible_pane.Bind(wx.EVT_COLLAPSIBLEPANE_CHANGED,
429 self.on_collapsible_pane_changed,
430 collapsible_pane)
431 pane = collapsible_pane.GetPane()
432 pane.SetThemeEnabled(False)
433 pane.SetBackgroundColour(self._mysettings.color_background)
434 pane.SetForegroundColour(self._mysettings.color_foreground)
435 label = Label(pane, label=_("Output directory: "))
436 label.SetBackgroundColour(self._mysettings.color_background)
437 label.SetForegroundColour(self._mysettings.color_foreground)
438 self._output_directory_text_ctrl = \
439 self._create_text_ctrl(pane, self.output_directory,
440 "removed due unicode_error (delete this)",
441 self.on_output_directory_changed)
442 self._output_directory_text_ctrl.SetBackgroundColour(self._mysettings.color_secondary_background)
443 self._output_directory_text_ctrl.SetForegroundColour(self._mysettings.color_secondary_foreground)
444 button = ButtonWithHandler(pane, "...", handler=self._handle_select_directory)
445 button.SetBackgroundColour(self._mysettings.color_secondary_background)
446 button.SetForegroundColour(self._mysettings.color_secondary_foreground)
447 horizontal_sizer = wx.BoxSizer(wx.HORIZONTAL)
448 horizontal_sizer.Add(label, 0,
449 wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 10)
450 horizontal_sizer.Add(self._output_directory_text_ctrl, 1, wx.EXPAND)
451 horizontal_sizer.Add(button, 0, wx.LEFT | wx.RIGHT, 10)
453 suite_name_outputs_cb = self._create_checkbox(
454 pane, self.are_log_names_with_suite_name,
455 _("Add suite name to log names"), self.on_suite_name_outputs_check_box)
456 timestamp_outputs_cb = self._create_checkbox(
457 pane, self.are_log_names_with_timestamp,
458 _("Add timestamp to log names"), self.on_timestamp_outputs_checkbox)
459 save_logs_cb = self._create_checkbox(
460 pane, self.are_saving_logs,
461 _("Save Console and Message logs"), self.on_save_logs_checkbox)
463 vertical_sizer = wx.BoxSizer(wx.VERTICAL)
464 vertical_sizer.Add(horizontal_sizer, 0, wx.EXPAND)
465 vertical_sizer.Add(suite_name_outputs_cb, 0, wx.LEFT | wx.TOP, 10)
466 vertical_sizer.Add(timestamp_outputs_cb, 0, wx.LEFT | wx.TOP, 10)
467 vertical_sizer.Add(save_logs_cb, 0, wx.LEFT | wx.TOP | wx.BOTTOM, 10)
468 pane.SetSizer(vertical_sizer)
469 return collapsible_pane
471 def on_output_directory_changed(self, evt):
472 _ = evt
473 value = self._output_directory_text_ctrl.GetValue()
474 self.set_setting("output_directory", value)
476 def _handle_select_directory(self, event):
477 __ = event
478 path = self._output_directory_text_ctrl.GetValue()
479 dlg = wx.DirDialog(None, _("Select Logs Directory"),
480 path, wx.DD_DEFAULT_STYLE)
481 dlg.SetBackgroundColour(self._mysettings.color_background)
482 dlg.SetForegroundColour(self._mysettings.color_foreground)
483 for item in dlg.GetChildren(): # DEBUG This is not working
484 item.SetBackgroundColour(self._mysettings.color_secondary_background)
485 item.SetForegroundColour(self._mysettings.color_secondary_foreground)
486 if dlg.ShowModal() == wx.ID_OK and dlg.Path:
487 self._output_directory_text_ctrl.SetValue(dlg.Path)
488 dlg.Destroy()
490 def on_suite_name_outputs_check_box(self, evt):
491 self.set_setting("are_log_names_with_suite_name", evt.IsChecked())
493 def on_timestamp_outputs_checkbox(self, evt):
494 self.set_setting("are_log_names_with_timestamp", evt.IsChecked())
496 def on_save_logs_checkbox(self, evt):
497 self.set_setting("are_saving_logs", evt.IsChecked())
499 def _get_arguments_panel(self, parent):
500 collapsible_pane = wx.CollapsiblePane(
501 parent, wx.ID_ANY, _('Arguments'),
502 style=wx.CP_DEFAULT_STYLE | wx.CP_NO_TLW_RESIZE)
503 collapsible_pane.Bind(wx.EVT_COLLAPSIBLEPANE_CHANGED,
504 self.on_collapsible_pane_changed,
505 collapsible_pane)
506 pane = collapsible_pane.GetPane()
507 pane.SetThemeEnabled(False)
508 pane.SetBackgroundColour(self._mysettings.color_background)
509 pane.SetForegroundColour(self._mysettings.color_foreground)
510 self._args_text_ctrl = \
511 self._create_text_ctrl(pane, self.arguments,
512 "removed due unicode_error (delete this)",
513 self.on_arguments_changed)
514 self._args_text_ctrl.SetToolTip(_("Arguments for the test run. "
515 "Arguments are space separated list."))
516 self._args_text_ctrl.SetBackgroundColour(self._mysettings.color_secondary_background)
517 self._args_text_ctrl.SetForegroundColour(self._mysettings.color_secondary_foreground)
518 horizontal_sizer = wx.BoxSizer(wx.HORIZONTAL)
519 horizontal_sizer.Add(self._args_text_ctrl, 1,
520 wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)
522 pane.SetSizer(horizontal_sizer)
523 self._validate_arguments(self.arguments or u'')
524 return collapsible_pane
526 def on_arguments_changed(self, evt):
527 _ = evt
528 args = self._args_text_ctrl.GetValue()
529 self._validate_arguments(args or u'')
530 self.set_setting("arguments", args)
531 self._defined_arguments = args
533 def _validate_arguments(self, args):
534 invalid_message = self._get_invalid_message(args)
535 self._args_text_ctrl.SetBackgroundColour(
536 'red' if invalid_message else self._mysettings.color_secondary_background)
537 self._args_text_ctrl.SetForegroundColour(
538 'white' if invalid_message else self._mysettings.color_secondary_foreground)
539 if not bool(invalid_message): 539 ↛ 541line 539 didn't jump to line 541 because the condition on line 539 was always true
540 invalid_message = _("Arguments for the test run. Arguments are space separated list.")
541 self._args_text_ctrl.SetToolTip(invalid_message)
543 @staticmethod
544 def _get_invalid_message(args):
545 invalid = False 1abcdef
546 if not args: 546 ↛ 547line 546 didn't jump to line 547 because the condition on line 546 was never true1abcdef
547 return None
548 try: 1abcdef
549 clean_args = args.split("`") # Shell commands 1abcdef
550 # print(f"DEBUG: run_profiles _get_invalid_message ENTER clean_args= {clean_args}")
551 for idx, item in enumerate(clean_args): 1abcdef
552 clean_args[idx] = item.strip() 1abcdef
553 if clean_args[idx] and clean_args[idx][0] != '-': # Not option, then is argument 553 ↛ 554line 553 didn't jump to line 554 because the condition on line 553 was never true1abcdef
554 clean_args[idx] = 'arg'
555 args = " ".join(clean_args) 1abcdef
556 # print(f"DEBUG: run_profiles _get_invalid_message: Check invalid args={args}")
557 __, invalid = ArgumentParser(USAGE).parse_args(args) # DEBUG .split()) 1abcdef
558 except Information: 1b
559 return _('Does not execute - help or version option given')
560 except (DataError, Exception) as e: 1b
561 if e.message: 561 ↛ 563line 561 didn't jump to line 563 because the condition on line 561 was always true1b
562 return e.message 1b
563 if bool(invalid): 563 ↛ 564line 563 didn't jump to line 564 because the condition on line 563 was never true1acdef
564 return f'{_("Unknown option(s):")} {invalid}'
565 return None 1acdef
567 def _get_tags_panel(self, parent):
568 """Create a panel to input include/exclude tags"""
569 collapsible_pane = wx.CollapsiblePane(
570 parent, wx.ID_ANY, _('Tests filters'),
571 style=wx.CP_DEFAULT_STYLE | wx.CP_NO_TLW_RESIZE)
572 collapsible_pane.Bind(wx.EVT_COLLAPSIBLEPANE_CHANGED,
573 self.on_collapsible_pane_changed,
574 collapsible_pane)
575 pane = collapsible_pane.GetPane()
576 pane.SetThemeEnabled(False)
577 pane.SetBackgroundColour(self._mysettings.color_background)
578 pane.SetForegroundColour(self._mysettings.color_foreground)
579 include_cb = self._create_checkbox(pane, self.apply_include_tags,
580 _("Only run tests with these tags:"),
581 self.on_include_checkbox)
582 exclude_cb = self._create_checkbox(pane, self.apply_exclude_tags,
583 _("Skip tests with these tags:"),
584 self.on_exclude_checkbox)
585 self._include_tags_text_ctrl = \
586 self._create_text_ctrl(pane, self.include_tags, "unicode_error",
587 self.on_include_tags_changed,
588 self.apply_include_tags)
589 self._exclude_tags_text_ctrl = \
590 self._create_text_ctrl(pane, self.exclude_tags, "unicode error",
591 self.on_exclude_tags_changed,
592 self.apply_exclude_tags)
593 self._include_tags_text_ctrl.SetBackgroundColour(self._mysettings.color_secondary_background)
594 self._include_tags_text_ctrl.SetForegroundColour(self._mysettings.color_secondary_foreground)
595 self._exclude_tags_text_ctrl.SetBackgroundColour(self._mysettings.color_secondary_background)
596 self._exclude_tags_text_ctrl.SetForegroundColour(self._mysettings.color_secondary_foreground)
597 horizontal_sizer = wx.BoxSizer(wx.HORIZONTAL)
598 horizontal_sizer.Add(include_cb, 0, wx.ALIGN_CENTER_VERTICAL)
599 horizontal_sizer.Add(self._include_tags_text_ctrl, 1, wx.EXPAND)
600 horizontal_sizer.Add(exclude_cb, 0, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 10)
601 horizontal_sizer.Add(self._exclude_tags_text_ctrl, 1, wx.EXPAND)
602 # Set Left, Right and Bottom content margins
603 sizer = wx.BoxSizer(wx.HORIZONTAL)
604 sizer.Add(horizontal_sizer, 1, wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)
605 pane.SetSizer(sizer)
607 return collapsible_pane
609 def on_collapsible_pane_changed(self, evt=None):
610 _ = evt
611 parent = self._toolbar.GetParent().GetParent()
612 parent.Layout()
614 def on_include_checkbox(self, evt):
615 self.set_setting("apply_include_tags", evt.IsChecked())
616 self._include_tags_text_ctrl.Enable(evt.IsChecked())
618 def on_exclude_checkbox(self, evt):
619 self.set_setting("apply_exclude_tags", evt.IsChecked())
620 self._exclude_tags_text_ctrl.Enable(evt.IsChecked())
622 def on_include_tags_changed(self, evt):
623 _ = evt
624 self.set_setting("include_tags", self._include_tags_text_ctrl.GetValue())
626 def on_exclude_tags_changed(self, evt):
627 _ = evt
628 self.set_setting("exclude_tags", self._exclude_tags_text_ctrl.GetValue())
630 def _create_checkbox(self, parent, value, title, handler):
631 checkbox = wx.CheckBox(parent, wx.ID_ANY, title)
632 checkbox.SetValue(value)
633 checkbox.SetBackgroundColour(self._mysettings.color_background)
634 checkbox.SetForegroundColour(self._mysettings.color_foreground)
635 parent.Bind(wx.EVT_CHECKBOX, handler, checkbox)
636 return checkbox
638 def _create_text_ctrl(self, parent, value, value_for_error,
639 text_change_handler, enable=True):
640 try:
641 text_ctrl = wx.TextCtrl(parent, wx.ID_ANY, value=value)
642 except UnicodeError:
643 text_ctrl = wx.TextCtrl(parent, wx.ID_ANY, value=value_for_error)
644 text_ctrl.SetBackgroundColour(self._mysettings.color_background)
645 text_ctrl.SetForegroundColour(self._mysettings.color_foreground)
646 text_ctrl.Bind(wx.EVT_TEXT, text_change_handler)
647 text_ctrl.Enable(enable)
648 return text_ctrl
651class CustomScriptProfile(PybotProfile):
652 """A runner profile which uses script given by the user"""
654 name = "custom script"
655 default_settings = dict(PybotProfile.default_settings, runner_script="")
657 def get_command(self):
658 # strip the starting and ending spaces to ensure
659 # /bin/sh finding the executable file
660 return self.runner_script.strip()
662 def get_cwd(self):
663 return os.path.dirname(self.runner_script)
665 def get_toolbar_items(self, parent):
666 return [self._get_run_script_panel(parent),
667 self._get_arguments_panel(parent),
668 self._get_tags_panel(parent),
669 self._get_log_options_panel(parent)]
671 def _validate_arguments(self, args):
672 # Can't say anything about custom script argument validity
673 pass
675 def _create_error_log_message(self, error, returncode):
676 return None
678 def _get_run_script_panel(self, parent):
679 panel = wx.Panel(parent, wx.ID_ANY)
680 self._script_ctrl = FileBrowseButton(
681 panel, labelText=_("Script to run tests:"), size=(-1, -1),
682 fileMask="*", changeCallback=self.on_custom_script_changed)
683 self._script_ctrl.SetValue(self.runner_script)
685 sizer = wx.BoxSizer(wx.VERTICAL)
686 sizer.Add(self._script_ctrl, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
688 panel.SetSizerAndFit(sizer)
689 return panel
691 def on_custom_script_changed(self, evt):
692 _ = evt
693 self.set_setting("runner_script", self._script_ctrl.GetValue())