Coverage for src/robotide/searchtests/searchtests.py: 62%

182 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 

16from functools import (total_ordering, cmp_to_key) 

17import builtins 

18import wx 

19 

20from .. import robotapi 

21from ..action import ActionInfo 

22from ..pluginapi import Plugin 

23from ..publish import RideOpenTagSearch 

24from .dialogsearchtests import TestsDialog 

25from ..widgets import ImageProvider 

26 

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

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

29 

30 

31@total_ordering 

32class TestSearchPlugin(Plugin): 

33 __doc__ = _("""A plugin for searching tests based on name, tags and documentation""") 

34 __test__ = False 

35 HEADER = _('Search Tests') 

36 _selection = None 

37 _dialog = None 

38 

39 def enable(self): 

40 self.register_action(ActionInfo( 

41 _('Tools'), self.HEADER, self.show_empty_search, 

42 shortcut='F3', doc=self.__doc__, 

43 icon=ImageProvider().TEST_SEARCH_ICON, position=50)) 

44 self.register_search_action( 

45 self.HEADER, self.show_search_for, 

46 ImageProvider().TEST_SEARCH_ICON, default=True) 

47 self.subscribe(self.show_tag_search, RideOpenTagSearch) 

48 self._dialog = None 

49 

50 def show_search_for(self, text): 

51 if self._dialog is None: 

52 self._create_tests_dialog() 

53 self._dialog.set_search_model( 

54 text, self._search_results(TestSearchMatcher(text))) 

55 self._dialog.set_focus_to_default_location() 

56 

57 def show_search_for_tag_patterns(self, includes, excludes): 

58 matcher = TagSearchMatcher(includes, excludes) 

59 self._dialog.set_tag_search_model( 

60 includes, excludes, self._search_results(matcher)) 

61 self._dialog.set_focus_to_default_location() 

62 

63 def show_tag_search(self, message): 

64 if self._dialog is None: 

65 self._create_tests_dialog() 

66 self.show_search_for_tag_patterns(message.includes, message.excludes) 

67 self._dialog.select_page(1) 

68 

69 def _create_tests_dialog(self): 

70 self._dialog = TestsDialog( 

71 fuzzy_search_handler=self.show_search_for, 

72 tag_search_handler=self.show_search_for_tag_patterns, 

73 add_to_selected_handler=self._add_to_selected) 

74 self._dialog.add_selection_listener(self._selected) 

75 self._dialog.Bind(wx.EVT_CLOSE, self._dialog_closed) 

76 self._selected_timer = wx.Timer(self._dialog) 

77 self._dialog.Bind(wx.EVT_TIMER, self._do_with_selection) 

78 self._dialog.Show() 

79 

80 def _add_to_selected(self, tests): 

81 self.tree.SelectTests(tests) 

82 

83 def _dialog_closed(self, event): 

84 self._dialog = None 

85 event.Skip() 

86 

87 def show_empty_search(self, event): 

88 __ = event 

89 self.show_search_for('') 

90 

91 def _do_with_selection(self, evt=None): 

92 _ = evt 

93 test, _ = self._selection 

94 if test: 

95 self.tree.select_node_by_data(test) 

96 self._dialog.set_focus_to_default_location(test) 

97 

98 def _selected(self, selection): 

99 self._selection = selection 

100 self._selected_timer.Start(400, True) 

101 

102 def _search_results(self, matcher): 

103 current_suite = self.frame.controller.data 

104 if not current_suite: 

105 return [] 

106 result = self._search(matcher, current_suite) 

107 return sorted(result, key=cmp_to_key(lambda x, y: 

108 self.m_cmp(x[1], y[1]))) 

109 

110 def _search(self, matcher, data): 

111 for test in data.tests: 

112 match = matcher.matches(test) 

113 if match: 

114 yield test, match 

115 for s in data.suites: 

116 for test, match in self._search(matcher, s): 

117 yield test, match 

118 

119 def disable(self): 

120 self.unregister_actions() 

121 

122 @staticmethod 

123 def m_cmp(a, b): 

124 return (a > b) - (a < b) 

125 

126 def __eq__(self, other): 

127 return self.name.lower() == other.name.lower() 

128 

129 def __hash__(self): 

130 return hash(repr(self)) 

131 

132 def __lt__(self, other): 

133 return self.name.lower() < other.name.lower() 

134 

135 

136class TagSearchMatcher(object): 

137 

138 def __init__(self, includes, excludes): 

139 self._tag_pattern_includes = robotapi.TagPatterns( 

140 includes.split()) if includes.split() else None 

141 self._tag_pattern_excludes = robotapi.TagPatterns(excludes.split()) 

142 

143 def matches(self, test): 

144 # Comments in Tests section are saved, but should not be visible 

145 if test.name.startswith('#'): 

146 return False 

147 tags = [str(tag) for tag in test.tags] 

148 if self._matches(tags): 

149 return test.longname 

150 return False 

151 

152 def _matches(self, tags): 

153 return (self._tag_pattern_includes is None or 

154 self._tag_pattern_includes.match(tags)) and \ 

155 not self._tag_pattern_excludes.match(tags) 

156 

157 

158class TestSearchMatcher(object): 

159 __test__ = False 

160 

161 def __init__(self, text): 

162 self._texts = text.split() 1pkqnlmorsjfgidehbc

163 self._texts_lower = [t.lower() for t in self._texts] 1pkqnlmorsjfgidehbc

164 

165 def matches(self, test): 

166 if self._matches(test): 1pkqnlmorsjfgidehbc

167 return SearchResult(self._texts, self._texts_lower, test) 1pkqnlmorjfgidehbc

168 return False 1s

169 

170 def _matches(self, test): 

171 name = test.name.lower() 1pkqnlmorsjfgidehbc

172 # Comments in Tests section are saved, but should not be visible 

173 if name.startswith('#'): 173 ↛ 174line 173 didn't jump to line 174 because the condition on line 173 was never true1pkqnlmorsjfgidehbc

174 return False 

175 if self._match_in(name): 1pkqnlmorsjfgidehbc

176 return True 1pqrjfgdeh

177 if any(self._match_in(str(tag).lower()) for tag in test.tags): 1knlmosgidehbc

178 return True 1noiebc

179 doc = test.documentation.value.lower() 1klmsgdhb

180 if self._match_in(doc): 1klmsgdhb

181 return True 1klmgdhb

182 return False 1s

183 

184 def _match_in(self, text): 

185 return any(word in text for word in self._texts_lower) 1pkqnlmorsjfgidehbc

186 

187 

188class SearchResult(object): 

189 

190 def __init__(self, original_search_terms, search_terms_lower, test): 

191 self._original_search_terms = original_search_terms 1pkqnlmorjfgidehbc

192 self._search_terms_lower = search_terms_lower 1pkqnlmorjfgidehbc

193 self.test = test 1pkqnlmorjfgidehbc

194 self.__total_matches = None 1pkqnlmorjfgidehbc

195 self.__tags = None 1pkqnlmorjfgidehbc

196 

197 # This is ignored on Python 3 

198 """ 

199 def __cmp__(self, other): 

200 """ 

201 def _helper_cmp_(self, other): 

202 totals, other_totals = self.total_matches(), other.total_matches() 1jfgidehbc

203 if totals != other_totals: 1jfgidehbc

204 return self.m_cmp(other_totals, totals) 1jgih

205 names = self._compare( 1fdebc

206 self.is_name_match(), other.is_name_match(), 

207 self.test.name, other.test.name) 

208 if names: 1fdebc

209 return names 1fde

210 tags = self._compare( 1bc

211 self.is_tag_match(), other.is_tag_match(), 

212 self.tags(), other.tags()) 

213 if tags: 213 ↛ 215line 213 didn't jump to line 215 because the condition on line 213 was always true1bc

214 return tags 1bc

215 return self.m_cmp(self.test.name, other.test.name) 

216 

217 def _compare(self, my_result, other_result, my_cmp, other_cmp): 

218 if my_result and not other_result: 218 ↛ 219line 218 didn't jump to line 219 because the condition on line 218 was never true1fdebc

219 return -1 

220 if not my_result and other_result: 1fdebc

221 return 1 1deb

222 if my_result and other_result: 1fbc

223 return self.m_cmp(my_cmp, other_cmp) 1fc

224 return 0 1bc

225 

226 def total_matches(self): 

227 if not self.__total_matches: 1jfgidehbc

228 self.__total_matches = sum( 1jfgidehbc

229 1 for word in self._search_terms_lower 

230 if word in self.test.name.lower() 

231 or any(word in t for t in self.tags()) 

232 or word in self.test.documentation.value.lower()) 

233 return self.__total_matches 1jfgidehbc

234 

235 def _match_in(self, text): 

236 return any(word in text for word in self._search_terms_lower) 1fdebc

237 

238 def is_name_match(self): 

239 return self._match_in(self.test.name.lower()) 1fdebc

240 

241 def is_tag_match(self): 

242 return any(self._match_in(t) for t in self.tags()) 1bc

243 

244 def tags(self): 

245 if self.__tags is None: 1jgidehbc

246 self.__tags = [str(tag).lower() for tag in self.test.tags] 1jgidehbc

247 return self.__tags 1jgidehbc

248 

249 def __repr__(self): 

250 return self.test.name 1jfgidehbc

251 

252 @staticmethod 

253 def m_cmp(a, b): 

254 return (a > b) - (a < b) 1jfgihc

255 

256 def __eq__(self, other): 

257 return self.__hash__() == other.__hash__() 

258 

259 def __ne__(self, other): 

260 return not self.__eq__(other) 

261 

262 def __hash__(self): 

263 return hash(repr(self)) 

264 

265 def __lt__(self, other): 

266 return self._helper_cmp_(other) == -1 

267 

268 def __le__(self, other): 

269 return self._helper_cmp_(other) <= 0 

270 

271 def __gt__(self, other): 

272 return self._helper_cmp_(other) == 1 1jfgidehbc

273 

274 def __ge__(self, other): 

275 return self._helper_cmp_(other) >= 0