From 7da1ceccce9d8111c223ba08c716ed23156d55af Mon Sep 17 00:00:00 2001 From: "Netzulo.com" Date: Fri, 19 Apr 2019 11:14:07 +0200 Subject: [PATCH] [qacode] new ControlTable class, #248 (#264) * [qacode] starting issue #248 , WIP control_table + new class ControlTable + inherit of ControlForm + validate tag table tag at instantiation * [qacode] more WIP changes for control_table #248 + added new property rows allow to acces to tr tags as rows at list of ControlBase array + tests * [qacode] more fixes for issue #248 + some exception messages moved to logger_messages + controls can handle None strict_tags values + ctl_dd refactor , added internal check for some methods + fix last commit refactor for ctl_form, bad call + ctl_tb added html5 structure data tags load + added more tests for ctl_tb * [qacode] some ControlTable fixes + complexity + #248 ControlTable + #265 Complexity * [qacode] renamed test class for ControlTable * [qacode] some suite vars fixes ControlTable tests * [qacode] CHANGELOG for PR #264 + issues: #248 , #265 --- CHANGELOG.md | 6 +- qacode/configs/settings.json | 3 +- qacode/core/loggers/logger_messages.py | 16 +- qacode/core/webs/controls/__init__.py | 5 +- qacode/core/webs/controls/control_dropdown.py | 38 ++-- qacode/core/webs/controls/control_form.py | 12 +- qacode/core/webs/controls/control_table.py | 144 ++++++++++++++ .../001_functionals/suite_008_controltable.py | 180 ++++++++++++++++++ ..._008_pagebase.py => suite_009_pagebase.py} | 0 ...tlink.py => suite_010_reportertestlink.py} | 0 10 files changed, 374 insertions(+), 30 deletions(-) create mode 100644 qacode/core/webs/controls/control_table.py create mode 100644 tests/001_functionals/suite_008_controltable.py rename tests/001_functionals/{suite_008_pagebase.py => suite_009_pagebase.py} (100%) rename tests/001_functionals/{suite_009_reportertestlink.py => suite_010_reportertestlink.py} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index d95e4cb7..c727cceb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - New module at 'qacode.core.loggers' named 'logger_messages' #untracked - New nav_base method named ele_wait_value #untracked - Move dropdown methods to new control dropdown class #258 +- Added new class named 'ControlTable' #248 ### Changed - Separate benchmark test from all functional tests at tox -e coverage #251 @@ -18,12 +19,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Refactor for control suites after changes from #247 , #untracked - Updated USAGE.rst documentation #258 - Now get_text check for input tag #untracked +- Function with Cognitive Complexity of 13 (exceeds 5 allowed) #265 +- New internal method to reduce duplication at ControlDropdown #untracked ### Fixed - Can't use dropdown methods if ControlForm strict_tags is disabled #247 - Some PageExceptions was failing at instantiation #untracked - Now get_tag update self property -- fixed CI complexity issue for #261 +- Fixed CI complexity issue for #261 +- Some ControlForm+inherit could fail if stric_rules was None #248 ### Removed - Deleted ControlGroup + tests #256 diff --git a/qacode/configs/settings.json b/qacode/configs/settings.json index 502b1925..ab75a18d 100644 --- a/qacode/configs/settings.json +++ b/qacode/configs/settings.json @@ -51,7 +51,8 @@ "web_controls": { "control_base": false, "control_form": false, - "control_dropdown": false + "control_dropdown": false, + "control_table": false }, "web_pages": false, "benchmarks": true diff --git a/qacode/core/loggers/logger_messages.py b/qacode/core/loggers/logger_messages.py index fc4f2d31..427d6a4f 100644 --- a/qacode/core/loggers/logger_messages.py +++ b/qacode/core/loggers/logger_messages.py @@ -63,12 +63,20 @@ CF_PARSERULES_LOADED = "ctl_form | parse_rules: parsed" # noqa:E501 CF_RELOAD_LOADED = "ctl_form | reload: reloaded ctl" # noqa:E501 CF_STRICT_ATTRS_RAISES = "Validation raises for strict_attrs for this element: control={}, strict_attrs=[{}]" # noqa:E501 +CF_BADTAG = "ctl_form | This tag can't be loaded as strict_rule" # noqa:E501 # ControlDropdown CDD_SELECT_LOADING = "ctl_dd | select: selecting..." # noqa:E501 CDD_SELECT_LOADED = "ctl_dd | select: selected" # noqa:E501 CDD_SELECT_LOADING = "ctl_dd | deselect: deselecting..." # noqa:E501 CDD_DESESELECT_LOADED = "ctl_dd | select: deselected" # noqa:E501 -CDD_DESELECTALL_LOADING = "ctl_form | dropdown_select: deselecting all..." # noqa:E501 -CDD_DESELECTALL_LOADED = "ctl_form | dropdown_select: deselected all" # noqa:E501 -CDD_LOADED = "ctl_form | ctl.dropdown property for tag element" # noqa:E501 +CDD_DESELECTALL_LOADING = "ctl_dd | dropdown_select: deselecting all..." # noqa:E501 +CDD_DESELECTALL_LOADED = "ctl_dd | dropdown_select: deselected all" # noqa:E501 +CDD_LOADED = "ctl_dd | ctl.dropdown property for tag element" # noqa:E501 +CDD_BADPARAMS = "ctl_dd | Can't use this function with all flags with True values" # noqa:E501 +CDD_BADINDEXTYPE = "ctl_dd | index must be an int value" # noqa:E501 +# ControlTable +CT_BADTAG = "ctl_tb | Can't use this for not tag element" # noqa:E501 +CT_LOADED = "ctl_tb | ctl.table property for
" # noqa:E501 +CT_TBLNOTCHILD = "ctl_tb | this table haven't got '{}' selector" # noqa:E501 +CT_TBL2ORMORETBODIES = "2 or more tbodys not supported, Open an issue on Github" # noqa:E501 diff --git a/qacode/core/webs/controls/__init__.py b/qacode/core/webs/controls/__init__.py index 561b4ab3..4f854677 100755 --- a/qacode/core/webs/controls/__init__.py +++ b/qacode/core/webs/controls/__init__.py @@ -3,7 +3,10 @@ from qacode.core.webs.controls import control_base +from qacode.core.webs.controls import control_dropdown from qacode.core.webs.controls import control_form +from qacode.core.webs.controls import control_table -__all__ = ['control_base', 'control_form'] +__all__ = [ + 'control_base', 'control_dropdown', 'control_form', 'control_table'] diff --git a/qacode/core/webs/controls/control_dropdown.py b/qacode/core/webs/controls/control_dropdown.py index 9dabf322..b9178e90 100644 --- a/qacode/core/webs/controls/control_dropdown.py +++ b/qacode/core/webs/controls/control_dropdown.py @@ -18,10 +18,11 @@ def __init__(self, bot, **kwargs): Some elements need to search False to be search at future """ kwargs.update({"instance": "ControlDropdown"}) - strict_rules = kwargs.get("strict_rules") + strict_rules = kwargs.get("strict_rules") or [] if not bool(strict_rules): strict_rules.append( {"tag": "select", "type": "tag", "severity": "hight"}) + kwargs.update({"strict_rules": strict_rules}) super(ControlDropdown, self).__init__(bot, **kwargs) if not self.IS_DROPDOWN and self.tag is not None: raise ControlException(msg=MSG.CDD_BADTAG) @@ -32,8 +33,8 @@ def __check_reload__form__(self): if it's neccessary reload element properties """ super(ControlDropdown, self).__check_reload__form__() - reload_dropdown_needed = not self.element or not self.dropdown - if reload_dropdown_needed: + reload_needed = not self.element or not self.dropdown + if reload_needed: self.reload(**self.settings) def reload(self, **kwargs): @@ -43,6 +44,16 @@ def reload(self, **kwargs): super(ControlDropdown, self).reload(**kwargs) self.dropdown = Select(self.element) + def __check_dropdown__(self, text, by_value=False, by_index=False): + """Internal funcionality for select/deselect methods""" + self.__check_reload__form__() + if self.dropdown is None: + raise ControlException(msg=MSG.CDD_BADTAG) + if by_value and by_index: + raise ControlException(msg=MSG.CDD_BADPARAMS) + if by_index and not isinstance(text, int): + raise ControlException(msg=MSG.CDD_BADINDEXTYPE) + def select(self, text, by_value=False, by_index=False): """The Select class only works with tags which have select tags. Using the Index of Dropdown (int) @@ -63,19 +74,12 @@ def select(self, text, by_value=False, by_index=False): ControlException -- if tag is not 'select' ControlException -- if all flags are 'True' """ + self.__check_dropdown__( + text, by_value=by_value, by_index=by_index) self.bot.log.debug(MSG.CDD_SELECT_LOADING) - self.__check_reload__form__() - if self.dropdown is None: - raise ControlException( - msg="Element must be dropdown, tag={})".format(self.tag)) - if by_value and by_index: - raise ControlException( - msg="Can't use this function with all flags with True values") if by_value: self.dropdown.select_by_value(text) elif by_index: - if not isinstance(text, int): - raise ControlException(msg="index must be an int value") self.dropdown.select_by_index(int(text)) else: self.dropdown.select_by_visible_text(text) @@ -102,15 +106,11 @@ def deselect(self, text, by_value=False, by_index=False): ControlException -- if all flags are 'True' """ self.bot.log.debug(MSG.CDD_SELECT_LOADING) - self.__check_reload__form__() - if by_value and by_index: - raise ControlException( - msg="Can't use this function with all flags with True values") + self.__check_dropdown__( + text, by_value=by_value, by_index=by_index) if by_value: self.dropdown.deselect_by_value(text) elif by_index: - if not isinstance(text, int): - raise ControlException(msg="index must be an int value") self.dropdown.deselect_by_index(int(text)) else: self.dropdown.deselect_by_visible_text(text) @@ -124,6 +124,6 @@ def deselect_all(self): ControlException -- if tag is not 'select' """ self.bot.log.debug(MSG.CDD_DESELECTALL_LOADING) - self.__check_reload__form__() + self.__check_dropdown__('') self.dropdown.deselect_all() self.bot.log.debug(MSG.CDD_DESELECTALL_LOADED) diff --git a/qacode/core/webs/controls/control_form.py b/qacode/core/webs/controls/control_form.py index df56537b..cef675a6 100644 --- a/qacode/core/webs/controls/control_form.py +++ b/qacode/core/webs/controls/control_form.py @@ -18,6 +18,8 @@ class ControlForm(ControlBase): strict_tag = None # tag=select IS_DROPDOWN = None + # tag=select + IS_TABLE = None def __init__(self, bot, **kwargs): """Instance of ControlForm. Load properties from settings dict. @@ -54,7 +56,7 @@ def __load__rules__(self, enabled=False): """Validate strict rules for each type of StricRule""" self.bot.log.debug(MSG.CF_PARSERULES_LOADING) if not enabled: - self.warning(MSG.CF_STRICT_DISABLED) + self.bot.log.warning(MSG.CF_STRICT_DISABLED) return False typed_rules = list() # parsing rules > to enums > to instance @@ -97,14 +99,16 @@ def __load_strict_tag__(self, strict_tag): instance ControlForm specific properties """ self.IS_DROPDOWN = False + self.IS_TABLE = False self.strict_tag = strict_tag - valid_tags = ['select'] + valid_tags = ['select', 'table'] self.bot.log.debug(MSG.CF_STRICTTAG_LOADING) if self.strict_tag.value not in valid_tags: - raise ControlException( - msg="This tag can be loaded as strict_rule") + raise ControlException(msg=MSG.CF_BADTAG) if self.tag == HtmlTag.TAG_SELECT.value: self.IS_DROPDOWN = True + if self.tag == HtmlTag.TAG_TABLE.value: + self.IS_TABLE = True self.bot.log.debug(MSG.CF_STRICTTAG_LOADED) return True diff --git a/qacode/core/webs/controls/control_table.py b/qacode/core/webs/controls/control_table.py new file mode 100644 index 00000000..5ab32a01 --- /dev/null +++ b/qacode/core/webs/controls/control_table.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +"""Package module qacode.core.webs.control_form""" + + +from qacode.core.exceptions.control_exception import ControlException +from qacode.core.exceptions.core_exception import CoreException +from qacode.core.loggers import logger_messages as MSG +from qacode.core.webs.controls.control_base import ControlBase +from qacode.core.webs.controls.control_form import ControlForm +from selenium.common.exceptions import WebDriverException +from selenium.webdriver.remote.webelement import WebElement + + +class ControlTable(ControlForm): + """TODO: doc class""" + + _table = None + _rows = None + + # public properties + + caption = None + thead = None + tfoot = None + tbodies = None + + def __init__(self, bot, **kwargs): + """Instance of ControlForm. Load properties from settings dict. + Some elements need to search False to be search at future + """ + kwargs.update({"instance": "ControlTable"}) + strict_rules = kwargs.get("strict_rules") or [] + if not bool(strict_rules): + strict_rules.append( + {"tag": "table", "type": "tag", "severity": "hight"}) + kwargs.update({"strict_rules": strict_rules}) + super(ControlTable, self).__init__(bot, **kwargs) + if not self.IS_TABLE and self.tag is not None: + raise ControlException(msg=MSG.CT_BADTAG) + self.bot.log.debug(MSG.CT_LOADED) + + def __load_table__(self, element=None): + """Allow to load all TR > TD items from a TABLE element + + Before structure some checks are necessary for some children elements: + caption {ControlBase}-- optional
element + + Examples: + Use case 1. TABLE > (TR > TH)+(TR > TD) + Use case 2. TABLE > (THEAD > (TR > TH))+(TBODY > (TR > TH)) + """ + if element is None: + element = self.element + self._table = ControlBase(self.bot, **{ + "selector": self.selector, + "element": element}) + # Preload + self.tbodies = self.__try__("find_children", "tbody") + if bool(self.tbodies): + self._rows = self.__load_table_html5__() + else: + self._rows = self.__load_table_html4__() + + def __load_table_html4__(self): + """Allow to load table with this structure + TABLE > (TR > TH)+(TR > TD) + """ + rows = [] + ctls_rows = self._table.find_children("tr") + for index, ctl_row in enumerate(ctls_rows): + if index == 0: + rows.append(self.__get_row__(ctl_row, "th")) + else: + rows.append(self.__get_row__(ctl_row, "td")) + return rows + + def __load_table_html5__(self): + """Allow to load table with this structure + TABLE > (THEAD > (TR > TH))+(TBODY > (TR > TH)) + """ + self.caption = self.__try__("find_child", "caption") + self.thead = self.__try__("find_child", "thead") + self.tfoot = self.__try__("find_child", "tfoot") + if len(self.tbodies) > 1: + raise ControlException(MSG.CT_TBL2ORMORETBODIES) + rows = [] + rows.append(self.__get_row__(self.thead.find_child("tr"), "th")) + for ctl_row in self.tbodies[0].find_children("tr"): + rows.append(self.__get_row__(ctl_row, "td")) + return rows + + def __get_row__(self, ctl_row, selector): + """WARNING: this method just can be used from __load_table__""" + row = [] + for cell in ctl_row.find_children(selector): + text = cell.get_text() + cell.settings.update({"name": text}) + cell.name = text + row.append(cell) + return row + + def __try__(self, method, selector): + """Allow to exec some method to handle exception""" + try: + return getattr(self._table, method)(selector) + except (ControlException, CoreException, WebDriverException): + self.bot.log.debug(MSG.CT_TBLNOTCHILD.format(selector)) + return None + + def __check_reload__form__(self): + """Allow to check before methods calls to ensure + if it's neccessary reload element properties + """ + super(ControlTable, self).__check_reload__form__() + reload_needed = not self.element or not self.table + if reload_needed: + self.reload(**self.settings) + if not self.IS_TABLE and self.tag is not None: + raise ControlException(msg=MSG.CT_BADTAG) + + def reload(self, **kwargs): + """Reload 'self.settings' property:dict and call to instance + logic with new configuration + """ + super(ControlTable, self).reload(**kwargs) + self.__load_table__(element=self.element) + + @property + def table(self): + """GETTER for 'table' property""" + return self._table + + @table.setter + def table(self, value): + """SETTER for 'table' property""" + if value is None or not isinstance(value, WebElement): + raise ControlException("Can't set not 'WebElement' instance") + self.__load_table__(element=value) + + @property + def rows(self): + """GETTER for 'rows' property""" + self.__check_reload__form__() + return self._rows diff --git a/tests/001_functionals/suite_008_controltable.py b/tests/001_functionals/suite_008_controltable.py new file mode 100644 index 00000000..34c7bc15 --- /dev/null +++ b/tests/001_functionals/suite_008_controltable.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +"""Testsuite for package qacode.core.webs.controls""" + + +import pytest +from qacode.core.testing.test_info import TestInfoBotUnique +from qacode.core.webs.controls.control_base import ControlBase +from qacode.core.webs.controls.control_table import ControlTable +from qautils.files import settings +from selenium.webdriver.remote.webelement import WebElement + + +SETTINGS = settings(file_path="qacode/configs/") +SKIP_CONTROLS = SETTINGS['tests']['skip']['web_controls']['control_table'] +SKIP_CONTROLS_MSG = 'web_controls DISABLED by config file' + + +class TestControlTable(TestInfoBotUnique): + """Test Suite for ControlBase class""" + + # app from config + app = None + # page from config: app + page = None + url = None + # page from config: app + page_inputs = None + url_inputs = None + # elements from config: page + form_login = None + txt_username = None + txt_password = None + btn_submit = None + # elements from config: page_data + dd_base = None + dd_menu_data = None + dd_menu_data_lists = None + tbl_ok = None + + @classmethod + def setup_class(cls, **kwargs): + """TODO: doc method""" + super(TestControlTable, cls).setup_class( + config=settings(file_path="qacode/configs/"), + skip_force=SKIP_CONTROLS) + cls.add_property('app', cls.settings_app('qadmin')) + # page + cls.add_property('page', cls.settings_page('qacode_login')) + cls.add_property('url', cls.page.get('url')) + cls.add_property('form_login', cls.settings_control('form_login')) + cls.add_property('txt_username', cls.settings_control('txt_username')) + cls.add_property('txt_password', cls.settings_control('txt_password')) + cls.add_property('btn_submit', cls.settings_control('btn_submit')) + # page_inputs + cls.add_property('page_inputs', cls.settings_page('qacode_inputs')) + cls.add_property('url_inputs', cls.page_inputs.get('url')) + cls.add_property('dd_base', cls.settings_control('dd_base')) + cls.add_property('dd_menu_data', cls.settings_control('dd_menu_data')) + cls.add_property( + 'dd_menu_data_lists', cls.settings_control('dd_menu_data_lists')) + cls.add_property('tbl_ok', cls.settings_control('tbl_ok')) + cls.add_property('tbl_html5_ok', cls.settings_control('tbl_html5_ok')) + + def setup_method(self, test_method): + """Configure self.attribute""" + super(TestControlTable, self).setup_method( + test_method, config=settings(file_path="qacode/configs/")) + self.setup_login_to_data() + + def setup_login_to_data(self): + """Do login before to exec some testcases""" + # setup_login + self.bot.navigation.get_url(self.page.get('url'), wait_for_load=10) + txt_username = self.bot.navigation.find_element( + self.txt_username.get("selector")) + txt_password = self.bot.navigation.find_element( + self.txt_password.get("selector")) + btn_submit = self.bot.navigation.find_element( + self.btn_submit.get("selector")) + self.bot.navigation.ele_write(txt_username, "admin") + self.bot.navigation.ele_write(txt_password, "admin") + self.bot.navigation.ele_click(btn_submit) + self.bot.navigation.ele_click( + self.bot.navigation.find_element_wait( + self.dd_menu_data.get("selector"))) + self.bot.navigation.ele_click( + self.bot.navigation.find_element_wait( + self.dd_menu_data_lists.get("selector"))) + # end setup_login + + @pytest.mark.skipIf(SKIP_CONTROLS, SKIP_CONTROLS_MSG) + @pytest.mark.parametrize("on_instance_search", [True, False]) + @pytest.mark.parametrize("auto_reload", [True, False]) + @pytest.mark.parametrize("strict_rules", [ + [{"tag": "table", "type": "tag", "severity": "hight"}], + [] + ]) + @pytest.mark.parametrize("ctl_name", ['tbl_ok', 'tbl_html5_ok']) + def test_controltable_instance(self, on_instance_search, + strict_rules, auto_reload, ctl_name): + """Testcase: test_controltable_instance""" + cfg = getattr(self, ctl_name).copy() + cfg.update({ + "instance": "ControlTable", + "on_instance_search": on_instance_search, + "auto_reload": auto_reload, + "strict_rules": strict_rules + }) + # functional testcases + ctl = ControlTable(self.bot, **cfg) + self.assert_is_instance(ctl, ControlTable) + self.assert_equals(ctl.selector, cfg.get('selector')) + self.assert_equals(ctl.instance, cfg.get('instance')) + self.assert_equals(ctl.name, cfg.get('name')) + self.assert_equals(ctl.locator, 'css selector') + self.assert_equals( + ctl.on_instance_search, cfg.get('on_instance_search')) + self.assert_equals(ctl.auto_reload, cfg.get('auto_reload')) + if bool(strict_rules): + self.assert_equals( + len(ctl.strict_rules), len(cfg.get('strict_rules'))) + if on_instance_search: + self.assert_is_instance(ctl.element, WebElement) + if auto_reload is not None: + self.assert_none(ctl.table) + ctl.reload(**ctl.settings) + self.assert_is_instance(ctl.table, ControlBase) + self.assert_is_instance(ctl.rows, list) + # Use case 1. not html5:: TABLE > (TR > TH)+(TR > TD) + if ctl_name == 'tbl_ok': + self.assert_lower(len(ctl.rows), 3) + for row in ctl.rows: + self.assert_is_instance(row, list) + self.assert_lower(len(row), 2) + for cell in row: + self.assert_is_instance(cell, ControlBase) + # Use case 2. html5:: TABLE > (THEAD > (TR > TH))+(TBODY > (TR > TH)) + if ctl_name == 'tbl_html5_ok': + self.assert_lower(len(ctl.rows), 4) + for row in ctl.rows: + self.assert_is_instance(row, list) + self.assert_lower(len(row), 3) + for cell in row: + self.assert_is_instance(cell, ControlBase) + + @pytest.mark.skipIf(SKIP_CONTROLS, SKIP_CONTROLS_MSG) + @pytest.mark.parametrize("strict_rules", [None]) + @pytest.mark.parametrize("ctl_name", ['tbl_ok', 'tbl_html5_ok']) + def test_controltable_instance_raises(self, strict_rules, ctl_name): + """Testcase: test_controltable_instance_raises""" + cfg = getattr(self, ctl_name).copy() + cfg.update({ + "instance": "ControlTable", + "strict_rules": strict_rules, + "selector": "span" + }) + # functional testcases + ctl = ControlTable(self.bot, **cfg) + self.assert_is_instance(ctl, ControlTable) + self.assert_equals(ctl.selector, cfg.get('selector')) + self.assert_equals(ctl.instance, cfg.get('instance')) + self.assert_equals(ctl.name, cfg.get('name')) + self.assert_equals(ctl.locator, 'css selector') + + @pytest.mark.skipIf(SKIP_CONTROLS, SKIP_CONTROLS_MSG) + def test_controltable_internals_ok(self): + """Testcase: test_controltable_internals_ok""" + ctl = ControlTable(self.bot, **self.tbl_ok) + ctl.__load_table__() + ctl.__check_reload__form__() + + @pytest.mark.skipIf(SKIP_CONTROLS, SKIP_CONTROLS_MSG) + def test_controltable_properties_ok(self): + """Testcase: test_controltable_properties_ok""" + ctl = ControlTable(self.bot, **self.tbl_ok) + rows_before = len(ctl.rows) + ctl.table = ctl.element + rows_after = len(ctl.rows) + self.assert_is_instance(ctl.table, ControlBase) + self.assert_equals(rows_before, rows_after) diff --git a/tests/001_functionals/suite_008_pagebase.py b/tests/001_functionals/suite_009_pagebase.py similarity index 100% rename from tests/001_functionals/suite_008_pagebase.py rename to tests/001_functionals/suite_009_pagebase.py diff --git a/tests/001_functionals/suite_009_reportertestlink.py b/tests/001_functionals/suite_010_reportertestlink.py similarity index 100% rename from tests/001_functionals/suite_009_reportertestlink.py rename to tests/001_functionals/suite_010_reportertestlink.py