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

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. 

15 

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. 

32 

33"""A plugin for running tests from within RIDE 

34 

35Some icons courtesy Mark James and provided under a creative commons 

36license. See http://www.famfamfam.com/lab/icons/silk 

37 

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

44 

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 

64 

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) 

88 

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

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

91 

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} 

98 

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' 

122 

123ATEXIT_LOCK = threading.RLock() 

124 

125 

126def _run_profile(name, run_prefix): 

127 return type('Profile', (runprofiles.PybotProfile,), 

128 {'name': name, 'get_command': lambda self: run_prefix}) 

129 

130 

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 } 

154 

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' 

158 

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

189 

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

195 

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) 

202 

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

208 

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

218 

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

229 

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) 

246 

247 def _read_run_profiles(self): 

248 self._read_run_profiles_from_config() 

249 self._read_run_profiles_from_classes() 

250 

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] 

256 

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

260 

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 

266 

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) 

271 

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) 

279 

280 def on_test_selected_for_running_changed(self, message): 

281 self._selected_tests = message.tests 

282 

283 def disable(self): 

284 self._remove_from_notebook() 

285 self._test_runner.clear_server() 

286 self.unsubscribe_all() 

287 self.unregister_actions() 

288 

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

298 

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) 

303 

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

311 

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 

316 

317 def on_stop(self, event): 

318 """Called when the user clicks the "Stop" button 

319 

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) 

330 

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) 

338 

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) 

346 

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) 

354 

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) 

362 

363 def on_run(self, event): 

364 """ Called when the user clicks or presses the F8, Run Tests """ 

365 __ = event 

366 self._run_tests() 

367 

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

374 

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

405 

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

415 

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 

420 

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

428 

429 def _initialize_variables_for_running(self, profile_settings, args): 

430 self._report_file = self._log_file = None 

431 self._log_message_queue = Queue() 

432 

433 self._min_log_level_number = \ 

434 ArgsParser.get_message_log_level(args) 

435 

436 self._logs_directory = \ 

437 ArgsParser.get_output_directory(args, self._default_output_dir) 

438 

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) 

443 

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) 

450 

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 

460 

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 

468 

469 def _tests_selected(self): 

470 return len(self._selected_tests) != 0 

471 

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 

478 

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

487 

488 def _clear_log_ctrls(self): 

489 self._clear_text_ctrl(self._console_log_ctrl) 

490 self._clear_text_ctrl(self._message_log_ctrl) 

491 

492 @staticmethod 

493 def _clear_text_ctrl(text_ctrl): 

494 text_ctrl.SetReadOnly(False) 

495 text_ctrl.ClearAll() 

496 text_ctrl.SetReadOnly(True) 

497 

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

510 

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) 

518 

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) 

526 

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) 

537 

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) 

556 

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) 

568 

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 

573 

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

602 

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) 

609 

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) 

616 

617 def _append_to_message_log(self, text, source="stdout"): 

618 self._append_text(self._message_log_ctrl, text, source) 

619 

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

625 

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 

639 

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

645 

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) 

653 

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) 

679 

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) 

684 

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 

730 

731 def store_color(self, idx, color): 

732 self._color_map.append((idx, color)) 

733 

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) 

741 

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) 

747 

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 

789 

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) 

800 

801 @property 

802 def general_font_size(self): 

803 fsize = self.app.settings.get('General', None)['font size'] 

804 return fsize 

805 

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) 

842 

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 

866 

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) 

873 

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) 

881 

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

885 

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

889 

890 def _on_profile_selection(self, event): 

891 self.save_setting("profile_name", event.GetString()) 

892 self._set_profile(self.profile_name) 

893 

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) 

902 

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) 

907 

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

915 

916 def get_current_profile(self): 

917 return self._test_runner.get_profile(self.choice.GetStringSelection()) 

918 

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) 

927 

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) 

938 

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) 

942 

943 self.add_tab(self.panel, self.title, allow_closing=False) 

944 

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 

954 

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) 

969 

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 

976 

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

983 

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

990 

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 

1000 

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) 

1013 

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 

1024 

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 

1031 

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 

1038 

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) 

1046 

1047 def _test_runner_events_handler(self, event, *args): 

1048 """Endpoint of the listener interface 

1049 

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) 

1085 

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')}") 

1090 

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

1103 

1104 def _handle_report_file(self, args): 

1105 self._report_file = args[0] 

1106 wx.CallAfter(self._local_toolbar.EnableTool, ID_SHOW_REPORT, True) 

1107 

1108 def _handle_log_file(self, args): 

1109 self._log_file = args[0] 

1110 wx.CallAfter(self._local_toolbar.EnableTool, ID_SHOW_LOG, True) 

1111 

1112 def _handle_start_keyword(self, args): 

1113 self._progress_bar.set_current_keyword(args[0]) 

1114 

1115 def _handle_end_keyword(self): 

1116 self._progress_bar.empty_current_keyword() 

1117 

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) 

1126 

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

1133 

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) 

1140 

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

1150 

1151 def _set_paused(self): 

1152 self._run_action.disable() 

1153 self._stop_action.enable() 

1154 self._enable_runner_toolbar(False, False) 

1155 

1156 def _set_continue(self): 

1157 self._run_action.disable() 

1158 self._stop_action.enable() 

1159 self._enable_runner_toolbar(False, True) 

1160 

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

1169 

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) 

1181 

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

1188 

1189 def on_config_panel(self): 

1190 dlg = self.config_panel(self.frame) 

1191 dlg.Show(True) 

1192 

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 

1200 

1201 

1202class ProgressBar(wx.Panel): 

1203 """A progress bar for the test runner plugin""" 

1204 

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

1227 

1228 def _initialize_state(self): 

1229 self._pass = 0 

1230 self._fail = 0 

1231 self._skip = 0 

1232 self._current_keywords = [] 

1233 

1234 def set_current_keyword(self, name): 

1235 self._current_keywords.append(name) 

1236 

1237 def empty_current_keyword(self): 

1238 if self._current_keywords: 

1239 self._current_keywords.pop() 

1240 

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

1247 

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) 

1258 

1259 def Stop(self): 

1260 """Signals the end of a test run""" 

1261 self._gauge.Hide() 

1262 self._timer.Stop() 

1263 

1264 def add_pass(self): 

1265 """Add one to the passed count""" 

1266 self._pass += 1 

1267 

1268 def add_fail(self): 

1269 """Add one to the failed count""" 

1270 self._fail += 1 

1271 

1272 def add_skip(self): 

1273 """Add one to the skipped count""" 

1274 self._skip += 1 

1275 

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

1283 

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

1317 

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) 

1322 

1323 @staticmethod 

1324 def _fix_size(text, max_length): 

1325 if len(text) <= max_length: 

1326 return text 

1327 return '...' + text[3 - max_length:] 

1328 

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

1335 

1336 

1337class OutputStyledTextCtrl(wx.stc.StyledTextCtrl): 

1338 

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 

1345 

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 

1361 

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

1370 

1371 

1372class OutputStylizer(object): 

1373 

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) 

1380 

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

1386 

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

1396 

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

1408 

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

1418 

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 

1427 

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

1434 

1435 

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 

1451 

1452MenuButton = PyEmbeddedImage( 

1453 "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAKxJ" 

1454 "REFUOI3t0jEKg0AUBNAxhmXX9QD2adLnJt7E2luIeB/PkCoQCG5lK8ifdZtNHyQRLGwy5Yd5" 

1455 "/GKSGCP25LSr/QcAAOfPQ9/3MYSAZVngvQdJiAhEhFVVZT8BkpKmaZbnOZRS0FojhIBpmh6b" 

1456 "Ppjn+ULyqZSyxhiM44hhGEiyXAOStSG1bVuIyMtaq51zJHltmsZtBgCgruuC5N17f+u6brX8" 

1457 "Fdia43dwPPAGncZYbvceeuMAAAAASUVORK5CYII=") 

1458getMenuButtonBitmap = MenuButton.GetBitmap 

1459 

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 

1475 

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 

1485 

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 

1496 

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