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

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. 

18 

19"""runProfiles.py 

20 

21This module contains profiles for running robot tests via the 

22runnerPlugin. 

23 

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""" 

29 

30import builtins 

31import os 

32import re 

33import time 

34import wx 

35 

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 

45 

46_ = wx.GetTranslation # To keep linter/code analyser happy 

47builtins.__dict__['_'] = wx.GetTranslation 

48 

49OUTPUT_ENCODING = getfilesystemencoding() 

50 

51 

52class BaseProfile(object): 

53 """Base class for all test runner profiles 

54 

55 At a minimum each profile must set the name attribute, which is 

56 how the profile will appear in the dropdown list. 

57 

58 In case some settings are needed, provide default_settings class attribute 

59 with default values. 

60 

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 """ 

64 

65 # this will be set to the plugin instance at runtime 

66 plugin = None 

67 default_settings = {} 

68 

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 

73 

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 

79 

80 def enable_toolbar(self): 

81 if self._panel is None: 

82 return 

83 self._panel.Enable() 

84 

85 def disable_toolbar(self): 

86 if self._panel is None: 

87 return 

88 self._panel.Enable(False) 

89 

90 def delete_pressed(self): 

91 """Handle delete key pressing""" 

92 pass 

93 

94 def get_command(self): 

95 """Returns a command for this profile""" 

96 return 'robot' 

97 

98 def get_command_args(self): 

99 """Return a list of command arguments unique to this profile. 

100 

101 Returned arguments are in format accepted by Robot Framework's argument 

102 file. 

103 """ 

104 return [] 

105 

106 def get_settings(self): 

107 """Return a list of settings unique to this profile. 

108 

109 Returned settings can be used when running tests. 

110 """ 

111 return [] 

112 

113 def set_setting(self, name, value): 

114 """Sets a plugin setting 

115 

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) 

120 

121 def format_error(self, error, returncode): 

122 return error, self._create_error_log_message(error, returncode) 

123 

124 def _create_error_log_message(self, error, returncode): 

125 _ = error, returncode 

126 return None 

127 

128 def __getattr__(self, name): 

129 """Provides attribute access to profile's settings 

130 

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 

144 

145 def _get_setting_name(self, name): 

146 """Adds profile's name to the setting.""" 

147 return "%s_%s" % (self.name.replace(' ', '_'), name) 

148 

149 

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""" 

155 

156 

157class PybotProfile(BaseProfile): 

158 """A runner profile which uses robot 

159 

160 It is assumed that robot is on the path 

161 """ 

162 _quotes_re = re.compile('(.*)(\".*\")(.*)?') 

163 

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} 

174 

175 def __init__(self, plugin): 

176 BaseProfile.__init__(self, plugin) 

177 self._defined_arguments = self.arguments 

178 self._toolbar = None 

179 self._mysettings = None 

180 

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 

192 

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)] 

197 

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() 

202 

203 def disable_toolbar(self): 

204 if self._toolbar is None: 

205 return 

206 self._enable_toolbar(False) 

207 

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) 

213 

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)) 

221 

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" 

230 

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 

246 

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)]) 

253 

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']) 

263 

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']) 

268 

269 if self.are_log_names_with_timestamp and \ 

270 '--timestampoutputs' not in args and \ 

271 '-T' not in args: 

272 args.append('-T') 

273 

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) 

277 

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 

282 

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() 

289 

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 

327 

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 

335 

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 

366 

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 

390 

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 

399 

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 

409 

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 

414 

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 

423 

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) 

452 

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) 

462 

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 

470 

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) 

475 

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() 

489 

490 def on_suite_name_outputs_check_box(self, evt): 

491 self.set_setting("are_log_names_with_suite_name", evt.IsChecked()) 

492 

493 def on_timestamp_outputs_checkbox(self, evt): 

494 self.set_setting("are_log_names_with_timestamp", evt.IsChecked()) 

495 

496 def on_save_logs_checkbox(self, evt): 

497 self.set_setting("are_saving_logs", evt.IsChecked()) 

498 

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) 

521 

522 pane.SetSizer(horizontal_sizer) 

523 self._validate_arguments(self.arguments or u'') 

524 return collapsible_pane 

525 

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 

532 

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) 

542 

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

566 

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) 

606 

607 return collapsible_pane 

608 

609 def on_collapsible_pane_changed(self, evt=None): 

610 _ = evt 

611 parent = self._toolbar.GetParent().GetParent() 

612 parent.Layout() 

613 

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()) 

617 

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()) 

621 

622 def on_include_tags_changed(self, evt): 

623 _ = evt 

624 self.set_setting("include_tags", self._include_tags_text_ctrl.GetValue()) 

625 

626 def on_exclude_tags_changed(self, evt): 

627 _ = evt 

628 self.set_setting("exclude_tags", self._exclude_tags_text_ctrl.GetValue()) 

629 

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 

637 

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 

649 

650 

651class CustomScriptProfile(PybotProfile): 

652 """A runner profile which uses script given by the user""" 

653 

654 name = "custom script" 

655 default_settings = dict(PybotProfile.default_settings, runner_script="") 

656 

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() 

661 

662 def get_cwd(self): 

663 return os.path.dirname(self.runner_script) 

664 

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)] 

670 

671 def _validate_arguments(self, args): 

672 # Can't say anything about custom script argument validity 

673 pass 

674 

675 def _create_error_log_message(self, error, returncode): 

676 return None 

677 

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) 

684 

685 sizer = wx.BoxSizer(wx.VERTICAL) 

686 sizer.Add(self._script_ctrl, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 5) 

687 

688 panel.SetSizerAndFit(sizer) 

689 return panel 

690 

691 def on_custom_script_changed(self, evt): 

692 _ = evt 

693 self.set_setting("runner_script", self._script_ctrl.GetValue())