From 0353501aa8c0d7b5d4d2f3f1f1184a656ea442a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9lio=20Guilherme?= Date: Sat, 7 Oct 2023 21:12:28 +0100 Subject: [PATCH] Add Test Tags to test suites settings (#2651) --- CHANGELOG.adoc | 2 + src/robotide/application/CHANGELOG.html | 4 +- src/robotide/application/releasenotes.py | 1 + src/robotide/controller/filecontrollers.py | 20 ++++++-- src/robotide/controller/macrocontrollers.py | 5 ++ src/robotide/controller/settingcontrollers.py | 47 +++++++++++++++++-- src/robotide/controller/tags.py | 8 ++++ src/robotide/editor/dialoghelps.py | 7 +++ src/robotide/editor/editordialogs.py | 6 +++ src/robotide/editor/editors.py | 8 +--- src/robotide/editor/tags.py | 12 ++++- src/robotide/lib/robot/parsing/model.py | 23 +++++---- src/robotide/lib/robot/running/defaults.py | 4 +- src/robotide/version.py | 2 +- utest/controller/test_controllers.py | 1 + utest/controller/test_filecontrollers.py | 4 +- utest/searchtests/test_matcher.py | 1 + 17 files changed, 122 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index c5a8900aa..7d82f92d1 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -10,6 +10,8 @@ and this project adheres to http://semver.org/spec/v2.0.0.html[Semantic Versioni === Added +- Added Test Tags field (new, since Robot Framework 6.0) to Test Suites settings. This field will replace Default and +Force Tags settings, after Robot Framework 7.0 - Added content help pop-up on Text Editor by pressing ``Ctrl`` for text at cursor position or selected autocomplete list item - Added Exclude option in context nenu for Test files, previously was only possible for Test Suites folders - Added exclusion of monitoring filesystem changes for files and directories excluded in Preferences diff --git a/src/robotide/application/CHANGELOG.html b/src/robotide/application/CHANGELOG.html index df139036d..15661cb78 100644 --- a/src/robotide/application/CHANGELOG.html +++ b/src/robotide/application/CHANGELOG.html @@ -1,6 +1,8 @@ Changelog

Changelog


All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog -and this project adheres to Semantic Versioning.

1.1. Added

  • +and this project adheres to Semantic Versioning.

    1.1. Added

      +
    • Added Test Tags field (new, since Robot Framework 6.0) to Test Suites settings. This field will replace Default and Force Tags settings, after Robot Framework 7.0
    • +
    • Added content help pop-up on Text Editor by pressing ``Ctrl`` for text at cursor position or selected autocomplete list item
    • Added Exclude option in context nenu for Test files, previously was only possible for Test Suites folders diff --git a/src/robotide/application/releasenotes.py b/src/robotide/application/releasenotes.py index 4c467fcc6..ece29db8d 100644 --- a/src/robotide/application/releasenotes.py +++ b/src/robotide/application/releasenotes.py @@ -168,6 +168,7 @@ def set_content(self, html_win, content):

    New Features and Fixes Highlights

      +
    • Added Test Tags field (new, since Robot Framework 6.0) to Test Suites settings. This field will replace Default and Force Tags settings, after Robot Framework 7.0
    • Improved RIDE Log and Parser Log windows to allow Zoom In/Out with Ctrl-Mouse Wheel
    • Hide continuation markers in Project Tree
    • Improved content assistance in Text Editor by allowing to filter list as we type
    • diff --git a/src/robotide/controller/filecontrollers.py b/src/robotide/controller/filecontrollers.py index 08b1179fa..5d5c5986a 100644 --- a/src/robotide/controller/filecontrollers.py +++ b/src/robotide/controller/filecontrollers.py @@ -29,7 +29,7 @@ from .basecontroller import WithUndoRedoStacks, _BaseController, WithNamespace, ControllerWithParent from .robotdata import new_test_case_file, new_test_data_directory from .settingcontrollers import (DocumentationController, FixtureController, TimeoutController, TemplateController, - DefaultTagsController, ForceTagsController) + DefaultTagsController, ForceTagsController, TestTagsController) from .tablecontrollers import (VariableTableController, TestCaseTableController, KeywordTableController, ImportSettingsController, MetadataListController) from .macrocontrollers import TestCaseController, UserKeywordController @@ -161,7 +161,7 @@ def internal_settings(self): FixtureController(self, ss.suite_teardown), FixtureController(self, ss.test_setup), FixtureController(self, ss.test_teardown), - self.force_tags] + self.force_tags, self.test_tags] @property def setting_table(self): @@ -171,6 +171,10 @@ def setting_table(self): def force_tags(self): return ForceTagsController(self, self.setting_table.force_tags) + @property + def test_tags(self): + return TestTagsController(self, self.setting_table.test_tags) + @property def variables(self): if self._variables_table_controller is None: @@ -1015,7 +1019,7 @@ def internal_settings(self): FixtureController(self, ss.suite_teardown), FixtureController(self, ss.test_setup), FixtureController(self, ss.test_teardown), - self.force_tags] + self.force_tags, self.test_tags] @property def setting_table(self): @@ -1025,6 +1029,10 @@ def setting_table(self): def force_tags(self): return ForceTagsController(self, self.setting_table.force_tags) + @property + def test_tags(self): + return TestTagsController(self, self.setting_table.test_tags) + @property def dirty(self): return False @@ -1089,7 +1097,7 @@ def internal_settings(self): sett = _DataController.internal_settings(self) sett.insert(-1, TemplateController(self, ss.test_template)) sett.insert(-1, TimeoutController(self, ss.test_timeout)) - return sett + [self.default_tags, self.force_tags] # OK doing some cheating here ;) + return sett + [self.default_tags, self.force_tags, self.test_tags] # OK doing some cheating here ;) @property def longname(self): @@ -1173,6 +1181,10 @@ def setting_table(self): def force_tags(self): # Yes, I know this is impossible, but is Exclude file, right? return None # ForceTagsController(self, self.setting_table.force_tags) + @property + def test_tags(self): # Yes, I know this is impossible, but is Exclude file, right? + return None + @property def dirty(self): return False diff --git a/src/robotide/controller/macrocontrollers.py b/src/robotide/controller/macrocontrollers.py index 289bb8c17..8c69a4438 100644 --- a/src/robotide/controller/macrocontrollers.py +++ b/src/robotide/controller/macrocontrollers.py @@ -342,6 +342,10 @@ def tags(self): def force_tags(self): return self.datafile_controller.force_tags + @property + def test_tags(self): + return self.datafile_controller.test_tags + @property def default_tags(self): return self.datafile_controller.default_tags @@ -416,6 +420,7 @@ def _init(self, kw): # Needed for API compatibility in tag search self.force_tags = [] self.default_tags = [] + self.test_tags = [] def __eq__(self, other): if self is other: diff --git a/src/robotide/controller/settingcontrollers.py b/src/robotide/controller/settingcontrollers.py index a54f2fc8d..c4f0cb411 100644 --- a/src/robotide/controller/settingcontrollers.py +++ b/src/robotide/controller/settingcontrollers.py @@ -22,7 +22,7 @@ RideItemSettingsChanged, RideImportSettingAdded) from ..utils import variablematcher, unescape_newlines_and_whitespaces from .basecontroller import ControllerWithParent -from .tags import Tag, ForcedTag, DefaultTag +from .tags import Tag, ForcedTag, DefaultTag, TestTag class _SettingController(ControllerWithParent): @@ -222,13 +222,14 @@ def remove(self, tag): def __iter__(self): forced = self._parent.force_tags + test_tags = self._parent.test_tags if self.tags.value is None: - return chain(forced, self._parent.default_tags).__iter__() + return chain(forced, self._parent.default_tags, test_tags).__iter__() if len(self.tags.value) == 0: - return chain(forced, [Tag('', controller=self)]) + return chain(forced, test_tags, [Tag('', controller=self)]) own_tags = (Tag(t, index, self) for index, t in enumerate(self.tags.value)) - return chain(forced, own_tags).__iter__() + return chain(forced, test_tags, own_tags).__iter__() @property def is_set(self): @@ -284,11 +285,47 @@ def _recursive_gather_from(self, obj, result): def _gather_from_data(tags, parent): if tags.value is None: return [] - print(f"DEBUG: SettingsController _gather_from_data entry tags={tags.value}") return [ForcedTag(t, index, parent) for index, t in enumerate(tags.value)] +class TestTagsController(TagsController): + + def empty_tag(self): + return TestTag(None, controller=self) + + def __iter__(self): + return self._recursive_gather_from(self.parent, []).__iter__() + + def __eq__(self, other): + if self is other: + return True + if other is None: + return False + if not isinstance(other, self.__class__): + return False + return self.tags == other.tags + + def _recursive_gather_from(self, obj, result): + if obj is None: + return result + try: + test_tags = obj.setting_table.test_tags + except AttributeError: # In the case of a .resource file, there is no Test Tags fields + return result + # print(f"DEBUG: SettingsController _recursive_gather_from force_tags={force_tags}, obj.parent={obj.parent}") + return self._recursive_gather_from( + obj.parent, + self._gather_from_data(test_tags, obj.test_tags) + result) + + @staticmethod + def _gather_from_data(tags, parent): + if tags.value is None: + return [] + return [TestTag(t, index, parent) + for index, t in enumerate(tags.value)] + + class TimeoutController(_SettingController): def _init(self, timeout): diff --git a/src/robotide/controller/tags.py b/src/robotide/controller/tags.py index d97e03a0f..411b5a8af 100644 --- a/src/robotide/controller/tags.py +++ b/src/robotide/controller/tags.py @@ -65,3 +65,11 @@ class DefaultTag(Tag): def tooltip(self): return u'Default tag from suite {0}'.format( self.controller.datafile_controller.name) + + +class TestTag(Tag): + + @property + def tooltip(self): + return u'Apply Test Tags from suite {0} (since Robot Framework 6.0)'.format( + self.controller.datafile_controller.name) diff --git a/src/robotide/editor/dialoghelps.py b/src/robotide/editor/dialoghelps.py index c8b8ed2cf..61767308f 100644 --- a/src/robotide/editor/dialoghelps.py +++ b/src/robotide/editor/dialoghelps.py @@ -79,6 +79,13 @@ def get_help(title): %(TAG)s %(ESCAPE)s +Test Tags +These tags are applied to all test cases in this test suite. This field exists since Robot Framework 6.0 and will + replace Force and Default Tags after version 7.0. +Inherited tags are not shown in this view. +%(TAG)s +%(ESCAPE)s + Tags These tags are set to this test case in addition to Force Tags and they override possible Default Tags. Inherited tags are not shown in this view. diff --git a/src/robotide/editor/editordialogs.py b/src/robotide/editor/editordialogs.py index 173e854bb..33502efc7 100644 --- a/src/robotide/editor/editordialogs.py +++ b/src/robotide/editor/editordialogs.py @@ -236,6 +236,12 @@ def _execute(self): pass +class TestTagsDialog(_SettingDialog): + def _execute(self): + """ Just ignore it """ + pass + + class TagsDialog(_SettingDialog): def _execute(self): """ Just ignore it """ diff --git a/src/robotide/editor/editors.py b/src/robotide/editor/editors.py index 00dae9c08..e42f87e03 100644 --- a/src/robotide/editor/editors.py +++ b/src/robotide/editor/editors.py @@ -92,7 +92,7 @@ def __init__(self, plugin, parent, controller, tree): self.SetSizer(self.sizer) if self.title: self.sizer.Add(self._create_header(self.title), - 0, wx.EXPAND | wx.ALL, 5) + 0, wx.EXPAND | wx.ALL, 6) self._editors = [] self._last_shown_tooltip = None self._reset_last_show_tooltip() @@ -269,12 +269,6 @@ def _get_editor_class(controller): def build(self, settings, plugin, tree): for setting in settings: editor = self.create_editor_for(setting, plugin, tree) - """ - editor.SetBackgroundColour(Colour(200, 222, 40)) - editor.SetOwnBackgroundColour(Colour(200, 222, 40)) - editor.SetForegroundColour(Colour(7, 0, 70)) - editor.SetOwnForegroundColour(Colour(7, 0, 70)) - """ self._sizer.Add(editor, 0, wx.ALL | wx.EXPAND, self.BORDER) self._editors.append(editor) editor.Refresh() diff --git a/src/robotide/editor/tags.py b/src/robotide/editor/tags.py index 513a7244f..599dba539 100644 --- a/src/robotide/editor/tags.py +++ b/src/robotide/editor/tags.py @@ -17,7 +17,7 @@ from wx.lib.scrolledpanel import ScrolledPanel from ..context import IS_WINDOWS from ..controller import ctrlcommands -from ..controller.tags import ForcedTag, DefaultTag +from ..controller.tags import ForcedTag, DefaultTag, TestTag class TagsDisplay(ScrolledPanel): @@ -225,7 +225,8 @@ def properties(tag, controller): if tag.controller == controller: return TagBoxProperties(tag) return tag.choose({ForcedTag: ForcedTagBoxProperties, - DefaultTag: DefaultTagBoxProperties})(tag) + DefaultTag: DefaultTagBoxProperties, + TestTag: TestTagBoxProperties})(tag) class _TagBoxProperties(object): @@ -292,3 +293,10 @@ class DefaultTagBoxProperties(_TagBoxProperties): foreground_color = '#666666' background_color = '#D3D3D3' # Colour(200, 222, 40) enabled = False + + +class TestTagBoxProperties(_TagBoxProperties): + # DEBUG: Use colours from settings + foreground_color = 'orange' + background_color = '#D3D3D3' # Colour(200, 222, 40) + enabled = False diff --git a/src/robotide/lib/robot/parsing/model.py b/src/robotide/lib/robot/parsing/model.py index 0e8f22bfb..858bf4205 100644 --- a/src/robotide/lib/robot/parsing/model.py +++ b/src/robotide/lib/robot/parsing/model.py @@ -45,7 +45,7 @@ def TestData(parent=None, source=None, include_suites=None, :returns: :class:`~.model.TestDataDirectory` if `source` is a directory, :class:`~.model.TestCaseFile` otherwise. """ - # TODO: Remove in RF 3.2. + # DEBUG: Remove in RF 3.2. if warn_on_skipped != 'DEPRECATED': warnings.warn("Option 'warn_on_skipped' is deprecated and has no " "effect.", DeprecationWarning) @@ -77,7 +77,7 @@ def _get_tables(self): (self._keyword_table_names, self.keyword_table), (self._comment_table_names, None)]: # remove Comments section, because we want to keep them as they are in files - # , (self._comment_table_names, None)]: + # , (self._comment_table_names, None) for name in names: yield name, table @@ -117,7 +117,7 @@ def _resolve_deprecated_table(self, used_name): for name in (self._setting_table_names + self._variable_table_names + self._testcase_table_names + self._keyword_table_names): # remove Comments section, because we want to keep them as they are in files - # + self._comment_table_names): + # + self._comment_table_names if normalize(name) == normalized: self._report_deprecated_table(used_name, name) return name @@ -414,8 +414,9 @@ def __init__(self, parent): self.suite_teardown = Fixture('Suite Teardown', self) self.test_setup = Fixture('Test Setup', self) self.test_teardown = Fixture('Test Teardown', self) - self.force_tags = Tags('Force Tags', self) - self.default_tags = Tags('Default Tags', self) + self.force_tags = Tags('Force Tags', self) # To deprecate after RF 7.0 + self.default_tags = Tags('Default Tags', self) # To deprecate after RF 7.0 + self.test_tags = Tags('Test Tags', self) # New since RF 6.0 self.test_template = Template('Test Template', self) self.test_timeout = Timeout('Test Timeout', self) self.metadata = MetadataList(self) @@ -454,6 +455,7 @@ class TestCaseFileSettingTable(_SettingTable): 'Test Teardown': lambda s: s.test_teardown.populate, 'Force Tags': lambda s: s.force_tags.populate, 'Default Tags': lambda s: s.default_tags.populate, + 'Test Tags': lambda s: s.test_tags.populate, 'Test Template': lambda s: s.test_template.populate, 'Test Timeout': lambda s: s.test_timeout.populate, 'Library': lambda s: s.imports.populate_library, @@ -468,7 +470,7 @@ class TestCaseFileSettingTable(_SettingTable): def __iter__(self): for setting in [self.doc, self.suite_setup, self.suite_teardown, self.test_setup, self.test_teardown, self.force_tags, - self.default_tags, self.test_template, self.test_timeout] \ + self.default_tags, self.test_tags, self.test_template, self.test_timeout] \ + self.metadata.data + self.imports.data: yield setting @@ -492,6 +494,7 @@ class InitFileSettingTable(_SettingTable): 'Test Teardown': lambda s: s.test_teardown.populate, 'Test Timeout': lambda s: s.test_timeout.populate, 'Force Tags': lambda s: s.force_tags.populate, + 'Test Tags': lambda s: s.test_tags.populate, 'Library': lambda s: s.imports.populate_library, 'Resource': lambda s: s.imports.populate_resource, 'Variables': lambda s: s.imports.populate_variables, @@ -499,7 +502,7 @@ class InitFileSettingTable(_SettingTable): def __iter__(self): for setting in [self.doc, self.suite_setup, self.suite_teardown, - self.test_setup, self.test_teardown, self.force_tags, + self.test_setup, self.test_teardown, self.force_tags, self.test_tags, self.test_timeout] + self.metadata.data + self.imports.data: yield setting @@ -875,7 +878,7 @@ def __init__(self, content, comment=None): # print(f"DEBUG: RFLib Model init Step: index={index} inner_kw_pos = {self.inner_kw_pos} indent={self.indent[:]} \ncontent {content}") self.args = content[index + 1:] if content and index <= len(content) - 1 else [] # print(f"DEBUG: RFLib Model init Step: 1st cell len(content)={len(content)} index {index} indent={self.indent[:]}") # 1st cell: {content[index]}") - # TODO: Create setters for Step.name and Step.args, see stepcontrollers.py replace_keyword + # DEBUG: Create setters for Step.name and Step.args, see stepcontrollers.py replace_keyword if index < len(content): self.name = content[index] if content else None else: @@ -899,7 +902,7 @@ def _get_assign(self): if 0 <= index < len(cells) and self.is_kind_of_comment(cells[index]): # Special case for commented content return [] # print(f"DEBUG: RFLib Model _get_assign VAR NORMAL (index={index}) inner_kw_pos={self.inner_kw_pos} content={content[:]}") - # first handle non FOR cases + # first handle non-FOR cases idx = 0 try: if cells[self.inner_kw_pos] != 'FOR': @@ -953,7 +956,7 @@ def is_comment(self): return self.name.lower() == 'comment' or not (self.assign or self.name or self.args) def is_for_loop(self): - # TODO: remove steps ForLoop: return self.name == 'FOR' + # DEBUG: remove steps ForLoop: return self.name == 'FOR' return False def is_set(self): diff --git a/src/robotide/lib/robot/running/defaults.py b/src/robotide/lib/robot/running/defaults.py index 9fcf3745a..2b2c70ae3 100644 --- a/src/robotide/lib/robot/running/defaults.py +++ b/src/robotide/lib/robot/running/defaults.py @@ -22,12 +22,14 @@ def __init__(self, settings, parent=None): self.timeout = settings.test_timeout self.force_tags = settings.force_tags self.default_tags = settings.default_tags + self.test_tags = settings.test_tags self.template = settings.test_template if parent: self.setup = self.setup or parent.setup self.teardown = self.teardown or parent.teardown self.timeout = self.timeout or parent.timeout self.force_tags += parent.force_tags + self.test_tags += parent.test_tags def get_test_values(self, test): return TestValues(test, self) @@ -40,4 +42,4 @@ def __init__(self, test, defaults): self.teardown = test.teardown or defaults.teardown self.timeout = test.timeout or defaults.timeout self.template = test.template or defaults.template - self.tags = (test.tags or defaults.default_tags) + defaults.force_tags + self.tags = (test.tags or defaults.default_tags) + defaults.force_tags + defaults.test_tags diff --git a/src/robotide/version.py b/src/robotide/version.py index 3aa8ad401..a92a18e61 100644 --- a/src/robotide/version.py +++ b/src/robotide/version.py @@ -14,4 +14,4 @@ # limitations under the License. # # Automatically generated by `tasks.py`. -VERSION = 'v2.0.8dev18' +VERSION = 'v2.0.8dev19' diff --git a/utest/controller/test_controllers.py b/utest/controller/test_controllers.py index bfc1ff2c7..08bdfc147 100644 --- a/utest/controller/test_controllers.py +++ b/utest/controller/test_controllers.py @@ -57,6 +57,7 @@ def __init__(self): self.datafile = None self.force_tags = ForceTagsController(self, Tags('Force Tags')) self.default_tags = DefaultTagsController(self, Tags('Default Tags')) + self.test_tags = DefaultTagsController(self, Tags('Test Tags')) self._setting_table = self def mark_dirty(self): diff --git a/utest/controller/test_filecontrollers.py b/utest/controller/test_filecontrollers.py index ecb062e29..a487e68cc 100644 --- a/utest/controller/test_filecontrollers.py +++ b/utest/controller/test_filecontrollers.py @@ -88,7 +88,7 @@ def setUp(self): def test_creation(self): for st in self.ctrl.settings: assert st is not None - assert len(self.ctrl.settings) == 9 + assert len(self.ctrl.settings) == 10 assert not self.ctrl.dirty def test_has_format(self): @@ -236,7 +236,7 @@ def test_creation(self): ctrl = TestDataDirectoryController(self.data) for st in ctrl.settings: assert st is not None - assert len(ctrl.settings) == 6 + assert len(ctrl.settings) == 7 def test_has_format(self): ctrl = TestDataDirectoryController(self.data) diff --git a/utest/searchtests/test_matcher.py b/utest/searchtests/test_matcher.py index 55a22f9de..0b240068e 100644 --- a/utest/searchtests/test_matcher.py +++ b/utest/searchtests/test_matcher.py @@ -28,6 +28,7 @@ def _test(self, name='name', tags=None, doc='documentation'): parent.register_for_namespace_updates = lambda *_: 0 parent.force_tags = [] parent.default_tags = [] + parent.test_tags = [] robot_test = TestCase(parent=parent, name=name) robot_test.get_setter('documentation')(doc) robot_test.get_setter('tags')(tags or [])