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
« 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.
16from functools import (total_ordering, cmp_to_key)
17import builtins
18import wx
20from .. import robotapi
21from ..action import ActionInfo
22from ..pluginapi import Plugin
23from ..publish import RideOpenTagSearch
24from .dialogsearchtests import TestsDialog
25from ..widgets import ImageProvider
27_ = wx.GetTranslation # To keep linter/code analyser happy
28builtins.__dict__['_'] = wx.GetTranslation
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
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
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()
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()
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)
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()
80 def _add_to_selected(self, tests):
81 self.tree.SelectTests(tests)
83 def _dialog_closed(self, event):
84 self._dialog = None
85 event.Skip()
87 def show_empty_search(self, event):
88 __ = event
89 self.show_search_for('')
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)
98 def _selected(self, selection):
99 self._selection = selection
100 self._selected_timer.Start(400, True)
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])))
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
119 def disable(self):
120 self.unregister_actions()
122 @staticmethod
123 def m_cmp(a, b):
124 return (a > b) - (a < b)
126 def __eq__(self, other):
127 return self.name.lower() == other.name.lower()
129 def __hash__(self):
130 return hash(repr(self))
132 def __lt__(self, other):
133 return self.name.lower() < other.name.lower()
136class TagSearchMatcher(object):
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())
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
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)
158class TestSearchMatcher(object):
159 __test__ = False
161 def __init__(self, text):
162 self._texts = text.split() 1pkqnlmorsjfgidehbc
163 self._texts_lower = [t.lower() for t in self._texts] 1pkqnlmorsjfgidehbc
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
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
184 def _match_in(self, text):
185 return any(word in text for word in self._texts_lower) 1pkqnlmorsjfgidehbc
188class SearchResult(object):
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
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)
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
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
235 def _match_in(self, text):
236 return any(word in text for word in self._search_terms_lower) 1fdebc
238 def is_name_match(self):
239 return self._match_in(self.test.name.lower()) 1fdebc
241 def is_tag_match(self):
242 return any(self._match_in(t) for t in self.tags()) 1bc
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
249 def __repr__(self):
250 return self.test.name 1jfgidehbc
252 @staticmethod
253 def m_cmp(a, b):
254 return (a > b) - (a < b) 1jfgihc
256 def __eq__(self, other):
257 return self.__hash__() == other.__hash__()
259 def __ne__(self, other):
260 return not self.__eq__(other)
262 def __hash__(self):
263 return hash(repr(self))
265 def __lt__(self, other):
266 return self._helper_cmp_(other) == -1
268 def __le__(self, other):
269 return self._helper_cmp_(other) <= 0
271 def __gt__(self, other):
272 return self._helper_cmp_(other) == 1 1jfgidehbc
274 def __ge__(self, other):
275 return self._helper_cmp_(other) >= 0