Skip to content

Commit 739f899

Browse files
valeriobrunoValerio Bruno
andauthored
Fix for 1668: Given a large test suite, selecting all tests from the root of tree, freezes RIDE for some time (#2210)
* 1668: The Select All Tests command on the root of a deeply nested test folder, freezes RIDE for several minutes. The problem is caused by Tree.SelectAllTests strategy: expanding and collapsing all the nodes. This causes CPU waste to expand/collapse (and maybe render) and wait for the creation of the tree node handlers. The idea of the fix is to navigate the tests using the link between controllers. In order to avoid all the dancing of the tree nodes. In this commit: - Changed Tree.SelectAllTests() to extract all the TestCaseController(s) and invoke TestSelectionController to change the selection for these. - Now both _ActionHandler OnDeselectAllTests() and OnSelectAllTests() invoke Tree.SelectAllTests(), with a parameter. - When a new TestCaseHandler is created, is registered to be notified about selection changes. It uses this notification to update the graphical node with the check. - RideTestSelectedForRunningChanged now also holds the controller of the test which has changed selection and whether has been selected or unselected. * Replace Tree DeselectAllTests(node) with SelectAllTests(node,False) as it's much more efficient. * TestSelectionController: Keep a copy of the tests to propagate in the event, rather than rebuild it everytime. * When selecting multiple tests, from the test search, do it efficiently. TestSelectionController: add select_all() to select multiple tests efficiently. Rewrite unselect_all() in terms of select_all() Tree: when selecting multiple tests, no need to navigate the full tree, just use TestSelectionController. * When deselecting multiple tests, from the test search, do it efficiently. Tree: when deselecting multiple tests, no need to navigate the full tree, just use TestSelectionController. * Do not expand all the tree nodes when selecting failed/passed tests. - Keep the information of failed/passed tests in TestCaseController - Update the information on the test execution - When selecting only failed/passed tests navigate the controller'tree rather than the graphical tree. - removed method _for_all_tests() as it was causing all the nodes to be loaded/rendered. * When selecting/deselecting multiple Tests, notify a single selection change event. Rather than one for each Test. * Restore previous behavior with "Select Failed/Passed": select only those and de-select any other test. * keep track of the selected tests using the TestCaseController(s), instead of the list of names. This removes the need for listen of name changes. * No need to generate the list of names, when determining if there's any. * Fix TestSelectionController.add_tag() * Handle RideTestSelectedForRunningChanged event on a new wx thread stack. This is to avoid the "Recursionerror: maximum recursion depth exceeded" * Remove unused variable. * Propagate the RideTestSelectedForRunningChanged event only if the selection actually changed. Co-authored-by: Valerio Bruno <[email protected]>
1 parent 56789e0 commit 739f899

File tree

7 files changed

+131
-102
lines changed

7 files changed

+131
-102
lines changed

src/robotide/contrib/testrunner/testrunnerplugin.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
from robotide.context import IS_WINDOWS, IS_MAC
6161
from robotide.contrib.testrunner import TestRunner
6262
from robotide.contrib.testrunner import runprofiles
63+
from robotide.controller.macrocontrollers import TestCaseController
6364
from robotide.publish import RideSettingsChanged, PUBLISHER
6465
from robotide.publish.messages import RideTestSelectedForRunningChanged
6566
from robotide.pluginapi import Plugin, ActionInfo
@@ -136,7 +137,11 @@ def __init__(self, application=None):
136137
self._test_runner = TestRunner(application.model)
137138
self._register_shortcuts()
138139
self._min_log_level_number = LOG_LEVELS['INFO']
139-
self._names_to_run = set()
140+
self._selected_tests: {TestCaseController} = set()
141+
142+
@property
143+
def _names_to_run(self):
144+
return list(map(lambda ctrl: (ctrl.datafile_controller.longname, ctrl.longname), self._selected_tests))
140145

141146
def _register_shortcuts(self):
142147
self.register_shortcut('CtrlCmd-C', self._copy_from_out)
@@ -219,7 +224,7 @@ def OnSettingsChanged(self, data):
219224
self.save_setting(setting, data.new)
220225

221226
def OnTestSelectedForRunningChanged(self, message):
222-
self._names_to_run = message.tests
227+
self._selected_tests = message.tests
223228

224229
def disable(self):
225230
self._remove_from_notebook()
@@ -305,7 +310,7 @@ def OnRun(self, event):
305310
if not self._can_start_running_tests():
306311
return
307312
if self.__getattr__('confirm run'):
308-
if not self.tests_selected():
313+
if not self._tests_selected():
309314
if not self.ask_user_to_run_anyway():
310315
# In Linux NO runs dialog 4 times
311316
return
@@ -375,8 +380,8 @@ def _ask_user_to_save_before_running(self):
375380
wx.ICON_QUESTION | wx.YES_NO)
376381
return ret == wx.YES
377382

378-
def tests_selected(self):
379-
return len(self._names_to_run) != 0
383+
def _tests_selected(self):
384+
return len(self._selected_tests) != 0
380385

381386
def ask_user_to_run_anyway(self):
382387
ret = wx.MessageBox('No tests selected. \n'

src/robotide/controller/filecontrollers.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,13 @@ def exclude(self):
626626
self.parent.children[index] = result
627627
return result
628628

629+
def retrieve_test_controllers(self):
630+
controllers: list[TestCaseController] = []
631+
for child in self.children:
632+
if isinstance(child,TestCaseFileController) or isinstance(child,TestDataDirectoryController):
633+
controllers += child.retrieve_test_controllers()
634+
return controllers
635+
629636

630637
class DirtyRobotDataException(Exception):
631638
"""
@@ -705,6 +712,11 @@ def reload(self):
705712
def get_template(self):
706713
return self.data.setting_table.test_template
707714

715+
def retrieve_test_controllers(self) :
716+
controllers = []
717+
for test_ctrl in iter(self.tests):
718+
controllers.append(test_ctrl)
719+
return controllers
708720

709721
class ResourceFileControllerFactory(object):
710722

src/robotide/controller/macrocontrollers.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ class TestCaseController(_WithStepsController):
312312

313313
def _init(self, test):
314314
self._test = test
315+
self._run_passed = None
315316

316317
def __eq__(self, other):
317318
if self is other:
@@ -389,6 +390,18 @@ def _get_template(self):
389390
return template
390391
return self.datafile_controller.get_template()
391392

393+
@property
394+
def run_passed(self):
395+
return self._run_passed
396+
397+
@run_passed.setter
398+
def run_passed(self,value):
399+
if value == True:
400+
self._run_passed = True # Test execution passed
401+
elif value == False:
402+
self._run_passed = False # Test execution failed
403+
else:
404+
self._run_passed = None # Test did not run
392405

393406
class UserKeywordController(_WithStepsController):
394407
_populator = robotapi.UserKeywordPopulator

src/robotide/controller/ui/treecontroller.py

Lines changed: 28 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from robotide.action.actioninfo import ActionInfoCollection, ActionInfo
1919
from robotide.context import IS_WINDOWS, ctrl_or_cmd, bind_keys_to_evt_menu
2020
from robotide.controller.ctrlcommands import ChangeTag
21+
from robotide.controller.macrocontrollers import TestCaseController
2122
from robotide.controller.tags import Tag, DefaultTag
2223
from robotide.controller.filecontrollers import TestCaseFileController
2324
from robotide.publish import PUBLISHER, RideTestSelectedForRunningChanged, RideItemNameChanged, RideFileNameChanged, RideNewProject, RideOpenSuite
@@ -60,7 +61,7 @@ def OnAddTagToSelected(self, event):
6061
self._test_selection.add_tag(name)
6162

6263
def OnClearSelected(self, event):
63-
self._tree.DeselectAllTests(self._tree._root)
64+
self._test_selection.clear_all()
6465

6566
def OnGoForward(self, event):
6667
node = self._history.forward()
@@ -175,57 +176,43 @@ def top(self):
175176
class TestSelectionController(object):
176177

177178
def __init__(self):
178-
self._tests = {}
179-
self._subscribe()
180-
181-
def _subscribe(self):
182-
PUBLISHER.subscribe(self._test_name_changed, RideItemNameChanged)
183-
PUBLISHER.subscribe(self._suite_name_changed, RideFileNameChanged)
184-
185-
def _test_name_changed(self, message):
186-
longname = message.item.longname
187-
path, new_name = longname.rsplit('.', 1)
188-
if message.old_name:
189-
old_name = path + '.' + message.old_name
190-
self._tests[longname] = new_name
191-
del self._tests[old_name]
192-
193-
def _suite_name_changed(self, message):
194-
df = message.datafile
195-
if isinstance(df, TestCaseFileController):
196-
filename = os.path.splitext(os.path.basename(message.old_filename))[0]
197-
old_name = df.longname[:-len(df.name)] + filename
198-
for test in self._tests:
199-
if test.lower().startswith(old_name.lower()):
200-
del self._tests[test]
179+
self._tests: {TestCaseController} = set()
201180

202181
def is_empty(self):
203182
return not bool(self._tests)
204183

205184
def is_test_selected(self, test):
206-
return test.longname in self._tests.keys()
185+
return test in self._tests
207186

208-
def clear_all(self, message=None):
209-
self._tests = {}
210-
self.send_selection_changed_message()
187+
def clear_all(self):
188+
self._tests = set()
189+
self._send_selection_changed_message()
211190

212191
def unselect_all(self, tests):
213-
for test in tests:
214-
self.select(test, False)
215-
216-
def select(self, test, selected=True):
217-
if selected:
218-
self._tests[test.longname] = test
219-
elif self.is_test_selected(test):
220-
del self._tests[test.longname]
221-
self.send_selection_changed_message()
192+
self.select_all(tests, selected=False)
222193

223-
def send_selection_changed_message(self):
224-
RideTestSelectedForRunningChanged(tests=set([(t.datafile_controller.longname, t.longname)
225-
for t in self._tests.values()])).publish()
194+
def select_all(self, tests, selected=True):
195+
for test in tests:
196+
self.select(test, selected, notifySelection=False)
197+
self._send_selection_changed_message()
198+
199+
def select(self, test: TestCaseController, doSelect=True, notifySelection=True):
200+
changed = False
201+
if doSelect and not self.is_test_selected(test):
202+
self._tests.add(test)
203+
changed = True
204+
elif not doSelect and self.is_test_selected(test):
205+
self._tests.remove(test)
206+
changed = True
207+
if notifySelection and changed:
208+
self._send_selection_changed_message()
209+
210+
def _send_selection_changed_message(self):
211+
message = RideTestSelectedForRunningChanged(tests=self._tests)
212+
wx.CallAfter(message.publish)
226213

227214
def add_tag(self, name):
228-
for test in self._tests.values():
215+
for test in self._tests:
229216
self._add_tag_to_test(name, test)
230217

231218
def _add_tag_to_test(self, name, test):

src/robotide/ui/treenodehandlers.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from robotide.editor.editordialogs import (
3131
TestCaseNameDialog, UserKeywordNameDialog, ScalarVariableDialog,
3232
ListVariableDialog, CopyUserKeywordDialog, DictionaryVariableDialog)
33-
from robotide.publish import RideOpenVariableDialog
33+
from robotide.publish import RideOpenVariableDialog, RideTestSelectedForRunningChanged, PUBLISHER
3434
from robotide.ui.progress import LoadProgressObserver
3535
from robotide.usages.UsageRunner import Usages, ResourceFileUsages
3636
from .filedialogs import (
@@ -155,7 +155,7 @@ def OnSelectAllTests(self, event):
155155
self._tree.SelectAllTests(self._node)
156156

157157
def OnDeselectAllTests(self, event):
158-
self._tree.DeselectAllTests(self._node)
158+
self._tree.SelectAllTests(self._node, False)
159159

160160
def OnSelectOnlyFailedTests(self, event):
161161
self._tree.SelectFailedTests(self._node)
@@ -488,7 +488,7 @@ def _rename_command(self, label):
488488

489489
@overrides(_FileHandlerThanCanBeRenamed)
490490
def _rename_ok_handler(self):
491-
self._tree.DeselectAllTests(self._node)
491+
self._tree.SelectAllTests(self._node,False)
492492

493493

494494
class _TestOrUserKeywordHandler(_CanBeRenamed, _ActionHandler):
@@ -524,6 +524,11 @@ def OnDelete(self, event):
524524

525525

526526
class TestCaseHandler(_TestOrUserKeywordHandler):
527+
def __init__(self, controller, tree, node, settings):
528+
_TestOrUserKeywordHandler.__init__(self, controller, tree, node, settings)
529+
PUBLISHER.subscribe(self.test_selection_changed, RideTestSelectedForRunningChanged,
530+
key=self) # TODO: unsubscribe when the object is destroyed!
531+
527532
_datalist = property(lambda self: self.item.datalist)
528533
_copy_name_dialog_class = TestCaseNameDialog
529534

@@ -533,6 +538,13 @@ def _add_copy_to_tree(self, parent_node, copied):
533538
def _create_rename_command(self, new_name):
534539
return RenameTest(new_name)
535540

541+
def test_selection_changed(self, message: RideTestSelectedForRunningChanged):
542+
if self.controller in message.tests:
543+
if not self.node.GetValue():
544+
self._tree.CheckItem(self.node, checked=True)
545+
else:
546+
if self.node.GetValue():
547+
self._tree.CheckItem(self.node, checked=False)
536548

537549
class UserKeywordHandler(_TestOrUserKeywordHandler):
538550
is_user_keyword = True

0 commit comments

Comments
 (0)