Coverage for src/robotide/controller/ui/treecontroller.py: 79%

183 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-05-06 10:40 +0100

1# Copyright 2008-2015 Nokia Networks 

2# Copyright 2016- Robot Framework Foundation 

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 

16import builtins 1ab

17import wx 1ab

18from wx.lib.agw.customtreectrl import GenericTreeItem 1ab

19 

20from robotide import utils 1ab

21from robotide.action.actioninfo import action_info_collection, ActionInfo 1ab

22from robotide.context import IS_WINDOWS, ctrl_or_cmd, bind_keys_to_evt_menu 1ab

23from ..macrocontrollers import TestCaseController 1ab

24from robotide.controller import ctrlcommands 1ab

25from robotide.controller.tags import Tag, DefaultTag 1ab

26from robotide.publish import RideTestSelectedForRunningChanged 1ab

27 

28_ = wx.GetTranslation # To keep linter/code analyser happy 1ab

29builtins.__dict__['_'] = wx.GetTranslation 1ab

30 

31 

32class TreeController(object): 1ab

33 

34 def __init__(self, tree, action_registerer, settings, test_selection, history=None): 1ab

35 self._tree = tree 1aC

36 self._action_registerer = action_registerer 1aC

37 self.settings = settings 1aC

38 self._history = history or _History() 1aC

39 self._test_selection = test_selection 1aC

40 

41 def register_tree_actions(self): 1ab

42 tree_actions = _("""[Navigate] 1aC

43 !Go &Back | Go back to previous location in tree | Alt-%s | ART_GO_BACK 

44 !Go &Forward | Go forward to next location in tree | Alt-%s | ART_GO_FORWARD 

45 """) % (('Left', 'Right') if IS_WINDOWS else ('Z', 'X')) 

46 # Left and right cannot be overridden in tree on non Windows OSses, issue 354 

47 

48 tree_actions_nt = """[Navigate] 1aC

49 !Go &Back | Go back to previous location in tree | Alt-%s | ART_GO_BACK 

50 !Go &Forward | Go forward to next location in tree | Alt-%s | ART_GO_FORWARD 

51 """ % (('Left', 'Right') if IS_WINDOWS else ('Z', 'X')) 

52 

53 # print(f"DEBUG: treecontroller.py register_tree_actions ENTER tree_actions={tree_actions}") 

54 actions = action_info_collection(tree_actions, self, data_nt=tree_actions_nt, container=self._tree) 1aC

55 self._action_registerer.register_actions(actions) 1aC

56 self._action_registerer.register_action(ActionInfo(menu_name=_('Edit'), name=_('Add Tag to selected'), 1aC

57 action=self.on_add_tag_to_selected)) 

58 self._action_registerer.register_action(ActionInfo(menu_name=_('Edit'), name=_('Clear Selected'), 1aC

59 action=self.on_clear_selected)) 

60 

61 def on_go_back(self, event): 1ab

62 __ = event 1hKLMiN

63 node = self._history.back() 1hKLMiN

64 if node: 64 ↛ exitline 64 didn't return from function 'on_go_back' because the condition on line 64 was always true1hKLMiN

65 self._tree.SelectItem(node) 1hKLMiN

66 

67 def on_add_tag_to_selected(self, event): 1ab

68 __ = event 

69 if self._test_selection.is_empty(): 

70 return 

71 name = wx.GetTextFromUser(message=_('Enter Tag Name'), caption=_('Add Tag To Selected')) 

72 if name: 

73 self._test_selection.add_tag(name) 

74 

75 def on_clear_selected(self, event): 1ab

76 __ = event 

77 self._test_selection.clear_all(message=None) 

78 

79 def on_go_forward(self, event): 1ab

80 __ = event 1hi

81 node = self._history.forward() 1hi

82 if node: 82 ↛ exitline 82 didn't return from function 'on_go_forward' because the condition on line 82 was always true1hi

83 self._tree.SelectItem(node) 1hi

84 

85 def add_to_history(self, node): 1ab

86 self._history.change(node) 1apqrsthKLMiNuvwxyzAcdjo

87 

88 def clear_history(self): 1ab

89 self._history.clear() 

90 

91 def mark_controller_dirty(self, controller): 1ab

92 if not controller.dirty: 92 ↛ 93line 92 didn't jump to line 93 because the condition on line 92 was never true1mcdef

93 return 

94 node = self.find_node_by_controller(controller) 1mcdef

95 if node: 1mcdef

96 self.mark_node_dirty(node) 1cdef

97 

98 def mark_node_dirty(self, node): 1ab

99 text = self._tree.GetItemText(node) 1cdef

100 if not text.startswith('*'): 1cdef

101 self._tree.SetItemText(node, '*' + text) 1cdef

102 

103 def find_node_by_controller(self, controller): 1ab

104 def match_handler(n): 1amDgEFGHIcdef

105 handler = self.get_handler(n) 1amDgEFGHIcdef

106 return handler and controller is handler.controller 1amDgEFGHIcdef

107 return self._find_node_with_predicate(self._tree.root, match_handler) 1amDgEFGHIcdef

108 

109 def find_node_with_label(self, node, label): 1ab

110 # print(f"DEBUG: treecontroller.py TreeController find_node_with_label node={node} LABEL={label}") 

111 def matcher(n): return utils.eq(self._tree.GetItemText(n), label) 1cdBjoef

112 return self._find_node_with_predicate(node, matcher) 1cdBjoef

113 

114 def _find_node_with_predicate(self, node, predicate): 1ab

115 # print(f"DEBUG: treecontroller.py TreeController find_node_with_label ENTER node={node}" 

116 # f" node is type={type(node)}") 

117 if node != self._tree.root and isinstance(node, GenericTreeItem) and predicate(node): 117 ↛ 118line 117 didn't jump to line 118 because the condition on line 117 was never true1amDgEFGHIcdBjoef

118 return node 

119 if not isinstance(node, GenericTreeItem): 119 ↛ 120line 119 didn't jump to line 120 because the condition on line 119 was never true1amDgEFGHIcdBjoef

120 node = self._tree.root 

121 item, cookie = self._tree.GetFirstChild(node) 1amDgEFGHIcdBjoef

122 while item: 1amDgEFGHIcdBjoef

123 if predicate(item): 1amDgEFGHIcdBjoef

124 return item 1acdBjoef

125 if self._tree.ItemHasChildren(item): 1amDgEFGHIcdBjoef

126 result = self._find_node_with_predicate(item, predicate) 1acdBjef

127 if result: 1acdBjef

128 return result 1acBjef

129 item, cookie = self._tree.GetNextChild(node, cookie) 1amDgEFGHIcdBjoef

130 return None 1amDgEFGHIcdBjef

131 

132 def get_handler(self, node=None): 1ab

133 return self._tree.GetItemData(node or self._tree.GetSelection()) 1apqrstu4vwxy5zA67mDgEFGHIcdjoef

134 

135 def bind_keys(self): 1ab

136 bind_keys_to_evt_menu(self._tree, self._get_bind_keys()) 

137 

138 def _get_bind_keys(self): 1ab

139 bindings = [ 

140 (ctrl_or_cmd(), wx.WXK_UP, self._tree.on_move_up), 

141 (ctrl_or_cmd(), wx.WXK_DOWN, self._tree.on_move_down), 

142 (wx.ACCEL_NORMAL, wx.WXK_F2, self._tree.label_editor.on_label_edit), 

143 (wx.ACCEL_NORMAL, wx.WXK_WINDOWS_MENU, self._tree.on_right_click), 

144 (ctrl_or_cmd() | wx.ACCEL_SHIFT, ord('d'), lambda event: self._expanded_handler().on_safe_delete(event)), 

145 (ctrl_or_cmd() | wx.ACCEL_SHIFT, ord('f'), 

146 lambda event: self._expanded_handler().on_new_suite(event)), 

147 (ctrl_or_cmd() | wx.ACCEL_SHIFT, ord('k'), 

148 lambda event: self._expanded_handler().on_new_user_keyword(event)), 

149 (ctrl_or_cmd() | wx.ACCEL_SHIFT, ord('t'), 

150 lambda event: self._expanded_handler().on_new_test_case(event)), 

151 (ctrl_or_cmd() | wx.ACCEL_SHIFT, ord('v'), 

152 lambda event: self._expanded_handler().on_new_scalar(event)), 

153 (ctrl_or_cmd() | wx.ACCEL_SHIFT, ord('l'), 

154 lambda event: self._expanded_handler().on_new_list_variable(event)), 

155 (ctrl_or_cmd() | wx.ACCEL_SHIFT, ord('c'), 

156 lambda event: self._expanded_handler().on_copy(event)) 

157 ] 

158 if not IS_WINDOWS: 158 ↛ 160line 158 didn't jump to line 160 because the condition on line 158 was always true

159 bindings.append((wx.ACCEL_NORMAL, wx.WXK_LEFT, self._tree.on_left_arrow)) 

160 return bindings 

161 

162 def _expanded_handler(self): 1ab

163 handler = self.get_handler() 

164 if not self._tree.IsExpanded(handler.node): 

165 self._tree.Expand(handler.node) 

166 return handler 

167 

168 

169class _History(object): 1ab

170 

171 def __init__(self): 1ab

172 self._back = [] 1aCYZ

173 self._forward = [] 1aCYZ

174 

175 def change(self, state): 1ab

176 if not self._back or state != self._back[-1]: 1apqrsthKLMiNuvwxyzAcdjo8OXQ

177 self._back.append(state) 1apqrsthKLMiNuvwxyzAcdjoOQ

178 self._forward = [] 1apqrsthKLMiNuvwxyzAcdjoOQ

179 

180 def back(self): 1ab

181 if not self._back: 1hKLMiNY012OXUQVW

182 return None 1Y

183 if len(self._back) > 1: 1hKLMiN012OXUQVW

184 self._forward.append(self._back.pop()) 1hKLMiN012OXUQVW

185 return self._back[-1] 1hKLMiN012OXUQVW

186 

187 def forward(self): 1ab

188 if not self._forward: 1hiOUQ3ZVW

189 return None 1Q3Z

190 state = self._forward.pop() 1hiOUVW

191 self._back.append(state) 1hiOUVW

192 return state 1hiOUVW

193 

194 def top(self): 1ab

195 return self._back and self._back[-1] or None 

196 

197 def clear(self): 1ab

198 self._back.clear() 

199 self._forward.clear() 

200 

201 

202class TestSelectionController(object): 1ab

203 __test__ = False 1ab

204 

205 def __init__(self): 1ab

206 self._tests: {TestCaseController} = set() 

207 

208 def is_empty(self): 1ab

209 return not bool(self._tests) 1kJ9P

210 

211 def is_test_selected(self, test): 1ab

212 return test in self._tests 1lnRkJP

213 

214 def clear_all(self, message): 1ab

215 _ = message 

216 self._tests = set() 

217 self._send_selection_changed_message() 

218 

219 def unselect_all(self, tests): 1ab

220 self.select_all(tests, selected=False) 

221 

222 def select_all(self, tests, selected=True): 1ab

223 for test in tests: 

224 self.select(test, selected, notify_selection=False) 

225 self._send_selection_changed_message() 

226 

227 def select(self, test: TestCaseController, do_select=True, notify_selection=True): 1ab

228 changed = False 1lnRkJP

229 if do_select and not self.is_test_selected(test): 1lnRkJP

230 self._tests.add(test) 1lnRkJP

231 changed = True 1lnRkJP

232 elif not do_select and self.is_test_selected(test): 232 ↛ 235line 232 didn't jump to line 235 because the condition on line 232 was always true1J

233 self._tests.remove(test) 1J

234 changed = True 1J

235 if notify_selection and changed: 235 ↛ exitline 235 didn't return from function 'select' because the condition on line 235 was always true1lnRkJP

236 self._send_selection_changed_message() 1lnRkJP

237 

238 def remove_invalid_cases_selection(self, cases_file_controller): 1ab

239 from .. import ResourceFileController 1pqrstkuvwxyzASTg

240 invalid_cases = list() 1pqrstkuvwxyzASTg

241 to_select_cases = list() 1pqrstkuvwxyzASTg

242 for test in self._tests: 1pqrstkuvwxyzASTg

243 if test.datafile_controller == cases_file_controller: 243 ↛ 244line 243 didn't jump to line 244 because the condition on line 243 was never true1k

244 if not isinstance(cases_file_controller, ResourceFileController): 

245 for newobj in cases_file_controller.tests: 

246 if test.longname == newobj.longname: 

247 to_select_cases.append(newobj) 

248 invalid_cases.append(test) 

249 for _ in invalid_cases: 249 ↛ 250line 249 didn't jump to line 250 because the loop on line 249 never started1pqrstkuvwxyzASTg

250 self._tests.remove(_) 

251 for test in to_select_cases: 251 ↛ 252line 251 didn't jump to line 252 because the loop on line 251 never started1pqrstkuvwxyzASTg

252 self.select(test, True, False) 

253 self._send_selection_changed_message() 1pqrstkuvwxyzASTg

254 

255 def _send_selection_changed_message(self): 1ab

256 message = RideTestSelectedForRunningChanged(tests=self._tests) 1apqrstlnRkJPuvwxyzASTg

257 wx.CallAfter(message.publish) 1apqrstlnRkJPuvwxyzASTg

258 

259 def add_tag(self, name): 1ab

260 for test in self._tests: 1ln

261 self._add_tag_to_test(name, test) 1ln

262 

263 def _add_tag_to_test(self, name, test): 1ab

264 if name not in [t.name for t in test.tags]: 264 ↛ exitline 264 didn't return from function '_add_tag_to_test' because the condition on line 264 was always true1ln

265 self._move_default_tags_to_test(test) 1ln

266 self._add_tag(test, name) 1ln

267 

268 def _move_default_tags_to_test(self, test): 1ab

269 for tag in test.tags: 1ln

270 if isinstance(tag, DefaultTag): 270 ↛ 269line 270 didn't jump to line 269 because the condition on line 270 was always true1l

271 self._add_tag(test, tag.name) 1l

272 

273 @staticmethod 1ab

274 def _add_tag(test, name): 1ab

275 test.tags.execute(ctrlcommands.ChangeTag(Tag(None), name)) 1ln