From 4cd7a97385edd81b4e7c7b28afa5c3786d3e1652 Mon Sep 17 00:00:00 2001 From: "reportportal.io" Date: Thu, 31 Oct 2024 14:13:32 +0000 Subject: [PATCH 01/23] Changelog update --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ee3ab3..66b2c76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## [Unreleased] + +## [5.4.5] ### Fixed - Issue [#379](https://github.com/reportportal/agent-python-pytest/issues/379): Fix TypeError when using pytest.skip() in fixtures, by @HardNorth From 416335a401bd6f1728a46ce1adb8668105b3dd6f Mon Sep 17 00:00:00 2001 From: "reportportal.io" Date: Thu, 31 Oct 2024 14:13:33 +0000 Subject: [PATCH 02/23] Version update --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d6793f7..e2b13f4 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ from setuptools import setup -__version__ = '5.4.5' +__version__ = '5.4.6' def read_file(fname): From 2de128e4b020ea1027c9c934362f4c8b561d1204 Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Thu, 21 Nov 2024 14:49:31 +0300 Subject: [PATCH 03/23] Minor warning fix --- pytest_reportportal/service.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pytest_reportportal/service.py b/pytest_reportportal/service.py index 295fb61..70b0034 100644 --- a/pytest_reportportal/service.py +++ b/pytest_reportportal/service.py @@ -218,9 +218,8 @@ def _get_item_dirs(self, item: Item) -> List[str]: """ root_path = item.session.config.rootdir.strpath dir_path = item.fspath.new(basename="") - rel_dir = dir_path.new(dirname=dir_path.relto(root_path), basename="", - drive="") - return [d for d in rel_dir.parts(reverse=False) if d.basename] + rel_dir = dir_path.new(dirname=dir_path.relto(root_path), basename="", drive="") + return [str(d) for d in rel_dir.parts(reverse=False) if d.basename] def _get_tree_path(self, item: Item) -> List[Item]: """Get item of parents. From e9b3bea98fc9d3bb67081b1d8f24b44d05f3822b Mon Sep 17 00:00:00 2001 From: Raz Amir Date: Wed, 20 Nov 2024 20:35:35 +0200 Subject: [PATCH 04/23] Restore rp_display_suite_test_file and fix bug with rp_dir_level --- pytest_reportportal/config.py | 3 +++ pytest_reportportal/plugin.py | 5 ++++ pytest_reportportal/service.py | 46 +++++++++++++++++++++------------- 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/pytest_reportportal/config.py b/pytest_reportportal/config.py index 8663af5..f0c9f9b 100644 --- a/pytest_reportportal/config.py +++ b/pytest_reportportal/config.py @@ -40,6 +40,7 @@ class AgentConfig: rp_hierarchy_code: bool rp_dir_level: int rp_hierarchy_dirs: bool + rp_display_suite_test_file: bool rp_dir_path_separator: str rp_ignore_attributes: set rp_is_skipped_an_issue: bool @@ -80,6 +81,8 @@ def __init__(self, pytest_config: Config) -> None: 'rp_hierarchy_dirs_level')) self.rp_hierarchy_dirs = self.find_option(pytest_config, 'rp_hierarchy_dirs') + self.rp_display_suite_test_file = self.find_option(pytest_config, + 'rp_display_suite_test_file') self.rp_dir_path_separator = \ self.find_option(pytest_config, 'rp_hierarchy_dir_path_separator') self.rp_ignore_attributes = set(self.find_option(pytest_config, 'rp_ignore_attributes') or []) diff --git a/pytest_reportportal/plugin.py b/pytest_reportportal/plugin.py index ae2f07e..6857f41 100644 --- a/pytest_reportportal/plugin.py +++ b/pytest_reportportal/plugin.py @@ -501,6 +501,11 @@ def add_shared_option(name, help_str, default=None, action='store'): default=False, type='bool', help='Enables hierarchy for directories') + parser.addini( + 'rp_display_suite_test_file', + default=True, + type='bool', + help='Show file name in hierarchy. Depends on rp_hierarchy_dirs_level to get deep enough') parser.addini( 'rp_hierarchy_dir_path_separator', default=os.path.sep, diff --git a/pytest_reportportal/service.py b/pytest_reportportal/service.py index 295fb61..325a485 100644 --- a/pytest_reportportal/service.py +++ b/pytest_reportportal/service.py @@ -99,6 +99,7 @@ class LeafType(Enum): """This class stores test item path types.""" DIR = auto() + FILE = auto() CODE = auto() ROOT = auto() @@ -270,7 +271,9 @@ def _build_test_tree(self, session: Session) -> Dict[str, Any]: children_leafs = current_leaf['children'] leaf_type = LeafType.DIR - if i >= len(dir_path): + if i == len(dir_path): + leaf_type = LeafType.FILE + if i > len(dir_path): leaf_type = LeafType.CODE if leaf not in children_leafs: @@ -278,20 +281,27 @@ def _build_test_tree(self, session: Session) -> Dict[str, Any]: current_leaf = children_leafs[leaf] return test_tree + def _remove_node(self, test_tree: Dict[str, Any], max_dir_level, dir_level, recursive): + parent_leaf = test_tree['parent'] + current_item = test_tree['item'] + del parent_leaf['children'][current_item] + for item, child_leaf in test_tree['children'].items(): + parent_leaf['children'][item] = child_leaf + child_leaf['parent'] = parent_leaf + if recursive: + self._remove_root_dirs(child_leaf, max_dir_level, dir_level + 1) + def _remove_root_dirs(self, test_tree: Dict[str, Any], max_dir_level, dir_level=0) -> None: if test_tree['type'] == LeafType.ROOT: - for item, child_leaf in test_tree['children'].items(): + for child_leaf in list(test_tree['children'].values()): self._remove_root_dirs(child_leaf, max_dir_level, 1) - return + return if test_tree['type'] == LeafType.DIR and dir_level <= max_dir_level: - new_level = dir_level + 1 - parent_leaf = test_tree['parent'] - current_item = test_tree['item'] - del parent_leaf['children'][current_item] - for item, child_leaf in test_tree['children'].items(): - parent_leaf['children'][item] = child_leaf - child_leaf['parent'] = parent_leaf - self._remove_root_dirs(child_leaf, max_dir_level, new_level) + self._remove_node(test_tree, max_dir_level, dir_level, recursive=True) + return + if test_tree['type'] == LeafType.FILE and not self._config.rp_display_suite_test_file: + self._remove_node(test_tree, max_dir_level, dir_level, recursive=False) + return def _generate_names(self, test_tree: Dict[str, Any]) -> None: if test_tree['type'] == LeafType.ROOT: @@ -300,7 +310,7 @@ def _generate_names(self, test_tree: Dict[str, Any]) -> None: if test_tree['type'] == LeafType.DIR: test_tree['name'] = test_tree['item'].basename - if test_tree['type'] == LeafType.CODE: + if test_tree['type'] in [LeafType.CODE, LeafType.FILE]: item = test_tree['item'] if isinstance(item, Module): test_tree['name'] = os.path.split(str(item.fspath))[1] @@ -310,11 +320,11 @@ def _generate_names(self, test_tree: Dict[str, Any]) -> None: for item, child_leaf in test_tree['children'].items(): self._generate_names(child_leaf) - def _merge_leaf_type(self, test_tree, leaf_type, separator): + def _merge_leaf_types(self, test_tree, leaf_types, separator): child_items = list(test_tree['children'].items()) - if test_tree['type'] != leaf_type: + if test_tree['type'] not in leaf_types: for item, child_leaf in child_items: - self._merge_leaf_type(child_leaf, leaf_type, separator) + self._merge_leaf_types(child_leaf, leaf_types, separator) elif len(test_tree['children'].items()) > 0: parent_leaf = test_tree['parent'] current_item = test_tree['item'] @@ -325,13 +335,13 @@ def _merge_leaf_type(self, test_tree, leaf_type, separator): child_leaf['parent'] = parent_leaf child_leaf['name'] = \ current_name + separator + child_leaf['name'] - self._merge_leaf_type(child_leaf, leaf_type, separator) + self._merge_leaf_types(child_leaf, leaf_types, separator) def _merge_dirs(self, test_tree: Dict[str, Any]) -> None: - self._merge_leaf_type(test_tree, LeafType.DIR, self._config.rp_dir_path_separator) + self._merge_leaf_types(test_tree, [LeafType.DIR], self._config.rp_dir_path_separator) def _merge_code(self, test_tree: Dict[str, Any]) -> None: - self._merge_leaf_type(test_tree, LeafType.CODE, '::') + self._merge_leaf_types(test_tree, [LeafType.CODE, LeafType.FILE], '::') def _build_item_paths(self, leaf: Dict[str, Any], path: List[Dict[str, Any]]) -> None: if 'children' in leaf and len(leaf['children']) > 0: From 3500c39fa03cc035a27f519b88248a6699c4be75 Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Fri, 22 Nov 2024 13:55:43 +0300 Subject: [PATCH 05/23] Fix kwargs modification --- pytest_reportportal/rp_logging.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pytest_reportportal/rp_logging.py b/pytest_reportportal/rp_logging.py index 8bbe3f5..650082f 100644 --- a/pytest_reportportal/rp_logging.py +++ b/pytest_reportportal/rp_logging.py @@ -18,6 +18,7 @@ import threading from contextlib import contextmanager from functools import wraps +from typing import Any from reportportal_client import current, set_current from reportportal_client import RPLogger @@ -116,23 +117,22 @@ def patching_logger_class(): try: def wrap_log(original_func): @wraps(original_func) - def _log(self, *args, **kwargs): - attachment = kwargs.pop('attachment', None) + def _log(self, *args: list[Any], **kwargs: dict[str, Any]): + my_kwargs = kwargs.copy() + attachment = my_kwargs.pop('attachment', None) if attachment is not None: - kwargs.setdefault('extra', {}).update( - {'attachment': attachment}) + my_kwargs.setdefault('extra', {}).update({'attachment': attachment}) + # Python 3.11 start catches stack frames in wrappers, # so add additional stack level skip to not show it if sys.version_info >= (3, 11): - my_kwargs = kwargs.copy() - if 'stacklevel' in kwargs: + if 'stacklevel' in my_kwargs: my_kwargs['stacklevel'] = my_kwargs['stacklevel'] + 1 else: my_kwargs['stacklevel'] = 2 return original_func(self, *args, **my_kwargs) else: - return original_func(self, *args, **kwargs) - + return original_func(self, *args, **my_kwargs) return _log def wrap_makeRecord(original_func): From 4f33203213719dd835a8786089dd1a74b3e497a5 Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Fri, 22 Nov 2024 14:07:12 +0300 Subject: [PATCH 06/23] Fix agent --- pytest_reportportal/service.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pytest_reportportal/service.py b/pytest_reportportal/service.py index 70b0034..7cda5f0 100644 --- a/pytest_reportportal/service.py +++ b/pytest_reportportal/service.py @@ -24,6 +24,7 @@ from _pytest.doctest import DoctestItem from aenum import auto, Enum, unique +from py.path import local from pytest import Class, Function, Module, Package, Item, Session, PytestWarning from reportportal_client.aio import Task from reportportal_client.core.rp_issues import Issue, ExternalIssue @@ -209,7 +210,7 @@ def start_launch(self) -> Optional[str]: LOGGER.debug('ReportPortal - Launch started: id=%s', self._launch_id) return self._launch_id - def _get_item_dirs(self, item: Item) -> List[str]: + def _get_item_dirs(self, item: Item) -> List[local]: """ Get directory of item. @@ -219,7 +220,7 @@ def _get_item_dirs(self, item: Item) -> List[str]: root_path = item.session.config.rootdir.strpath dir_path = item.fspath.new(basename="") rel_dir = dir_path.new(dirname=dir_path.relto(root_path), basename="", drive="") - return [str(d) for d in rel_dir.parts(reverse=False) if d.basename] + return [d for d in rel_dir.parts(reverse=False) if d.basename] def _get_tree_path(self, item: Item) -> List[Item]: """Get item of parents. From b00fe1ab4cedbad4b26fda98dfbbb912be5005e0 Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Fri, 29 Nov 2024 17:13:16 +0300 Subject: [PATCH 07/23] Client version update --- CHANGELOG.md | 2 ++ requirements.txt | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66b2c76..e5f7699 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## [Unreleased] +### Changed +- Client version updated on [5.5.10](https://github.com/reportportal/client-Python/releases/tag/5.5.10), by @HardNorth ## [5.4.5] ### Fixed diff --git a/requirements.txt b/requirements.txt index 897791c..616a738 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ dill>=0.3.6 pytest>=3.8.0 -reportportal-client~=5.5.9 +reportportal-client~=5.5.10 aenum>=3.1.0 From 36f30273788512d19257f29b2d4b812b782efd0b Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Fri, 29 Nov 2024 17:14:28 +0300 Subject: [PATCH 08/23] Fix Agent crash if Client could not be initialized --- CHANGELOG.md | 2 ++ pytest_reportportal/service.py | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5f7699..c951ee3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## [Unreleased] +### Fixed +- Agent crash if Client could not be initialized, by @HardNorth ### Changed - Client version updated on [5.5.10](https://github.com/reportportal/client-Python/releases/tag/5.5.10), by @HardNorth diff --git a/pytest_reportportal/service.py b/pytest_reportportal/service.py index 7cda5f0..c47dc4d 100644 --- a/pytest_reportportal/service.py +++ b/pytest_reportportal/service.py @@ -167,6 +167,8 @@ def issue_types(self) -> Dict[str, str]: project_settings = self.project_settings if not isinstance(self.project_settings, dict): project_settings = project_settings.blocking_result() + if not project_settings: + return self._issue_types for values in project_settings["subTypes"].values(): for item in values: self._issue_types[item["shortName"]] = item["locator"] @@ -864,6 +866,10 @@ def report_fixture(self, name: str, error_msg: str) -> None: :param name: Name of the fixture :param error_msg: Error message """ + if not self.rp: + yield + return + reporter = self.rp.step_reporter item_id = reporter.start_nested_step(name, timestamp()) @@ -874,6 +880,7 @@ def report_fixture(self, name: str, error_msg: str) -> None: if exception: if type(exception).__name__ != 'Skipped': status = 'FAILED' + self.post_log(name, error_msg, log_level='ERROR') reporter.finish_nested_step(item_id, timestamp(), status) except Exception as e: LOGGER.error('Failed to report fixture: %s', name) From bed04559187f4dfa7b517e44a1e99526d205d37d Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Mon, 2 Dec 2024 16:30:55 +0300 Subject: [PATCH 09/23] Python 3.13 support --- .github/workflows/tests.yml | 4 ++-- CHANGELOG.md | 2 ++ setup.py | 3 ++- tox.ini | 6 ++++-- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f8b2eee..234988f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11', '3.12' ] + python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13' ] steps: - name: Checkout repository uses: actions/checkout@v4 @@ -39,7 +39,7 @@ jobs: run: tox - name: Upload coverage to Codecov - if: matrix.python-version == 3.8 && success() + if: matrix.python-version == 3.10 && success() uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index c951ee3..6c31413 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## [Unreleased] +### Added +- Support for `Python 3.13`, by @HardNorth ### Fixed - Agent crash if Client could not be initialized, by @HardNorth ### Changed diff --git a/setup.py b/setup.py index e2b13f4..5ecc76d 100644 --- a/setup.py +++ b/setup.py @@ -52,7 +52,8 @@ def read_file(fname): 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12' + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13' ], entry_points={ 'pytest11': [ diff --git a/tox.ini b/tox.ini index 51f0d09..ef7de81 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,7 @@ envlist = py310 py311 py312 + py313 [testenv] deps = @@ -27,8 +28,9 @@ commands = pre-commit run --all-files --show-diff-on-failure [gh-actions] python = 3.7: py37 - 3.8: pep, py38 + 3.8: py38 3.9: py39 - 3.10: py310 + 3.10: pep, py310 3.11: py311 3.12: py312 + 3.13: py313 From c860076f449a58c9ab883d2303687af6410b2bf1 Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Mon, 2 Dec 2024 16:52:46 +0300 Subject: [PATCH 10/23] Format update --- pytest_reportportal/service.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/pytest_reportportal/service.py b/pytest_reportportal/service.py index c47dc4d..c5e8eee 100644 --- a/pytest_reportportal/service.py +++ b/pytest_reportportal/service.py @@ -511,10 +511,8 @@ def _get_issue_description_line(self, mark, default_url): issues = "" for i, issue_id in enumerate(issue_ids): issue_url = issue_urls[i] - template = ISSUE_DESCRIPTION_URL_TEMPLATE if issue_url \ - else ISSUE_DESCRIPTION_ID_TEMPLATE - issues += template.format(issue_id=issue_id, - url=issue_url) + template = ISSUE_DESCRIPTION_URL_TEMPLATE if issue_url else ISSUE_DESCRIPTION_ID_TEMPLATE + issues += template.format(issue_id=issue_id, url=issue_url) return ISSUE_DESCRIPTION_LINE_TEMPLATE.format(reason, issues) def _get_issue(self, mark): @@ -525,8 +523,7 @@ def _get_issue(self, mark): """ default_url = self._config.rp_bts_issue_url - issue_description_line = \ - self._get_issue_description_line(mark, default_url) + issue_description_line = self._get_issue_description_line(mark, default_url) # Set issue_type only for first issue mark issue_short_name = None @@ -534,22 +531,19 @@ def _get_issue(self, mark): issue_short_name = mark.kwargs["issue_type"] # default value - issue_short_name = "TI" if issue_short_name is None else \ - issue_short_name + issue_short_name = "TI" if issue_short_name is None else issue_short_name registered_issues = self.issue_types issue = None if issue_short_name in registered_issues: - issue = Issue(registered_issues[issue_short_name], - issue_description_line) + issue = Issue(registered_issues[issue_short_name], issue_description_line) if issue and self._config.rp_bts_project and self._config.rp_bts_url: issue_ids = self._get_issue_ids(mark) issue_urls = self._get_issue_urls(mark, default_url) for issue_id, issue_url in zip(issue_ids, issue_urls): issue.external_issue_add( - ExternalIssue(bts_url=self._config.rp_bts_url, - bts_project=self._config.rp_bts_project, + ExternalIssue(bts_url=self._config.rp_bts_url, bts_project=self._config.rp_bts_project, ticket_id=issue_id, url=issue_url) ) return issue From 1434406fff7d7399530c034688c39a043b680f4a Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Mon, 2 Dec 2024 17:47:14 +0300 Subject: [PATCH 11/23] Custom Item name: WIP --- pytest_reportportal/service.py | 35 ++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/pytest_reportportal/service.py b/pytest_reportportal/service.py index c5e8eee..73abfc1 100644 --- a/pytest_reportportal/service.py +++ b/pytest_reportportal/service.py @@ -360,7 +360,7 @@ def collect_tests(self, session: Session) -> None: self._merge_code(test_tree) self._build_item_paths(test_tree, []) - def _get_item_name(self, name: str) -> str: + def _truncate_item_name(self, name: str) -> str: """Get name of item. :param name: Test Item name @@ -403,7 +403,7 @@ def _build_start_suite_rq(self, leaf): code_ref = str(leaf['item']) if leaf['type'] == LeafType.DIR else str(leaf['item'].fspath) parent_item_id = self._lock(leaf['parent'], lambda p: p.get('item_id')) if 'parent' in leaf else None payload = { - 'name': self._get_item_name(leaf['name']), + 'name': self._truncate_item_name(leaf['name']), 'description': self._get_item_description(leaf['item']), 'start_time': timestamp(), 'item_type': 'SUITE', @@ -431,6 +431,9 @@ def _create_suite_path(self, item: Item): continue self._lock(leaf, lambda p: self._create_suite(p)) + def _get_item_name(self, mark) -> str: + pass + def _get_code_ref(self, item): # Generate script path from work dir, use only backslashes to have the # same path on different systems and do not affect Test Case ID on @@ -451,7 +454,7 @@ def _get_code_ref(self, item): class_path = '.'.join(classes) return '{0}:{1}'.format(path, class_path) - def _get_test_case_id(self, mark, leaf): + def _get_test_case_id(self, mark, leaf) -> str: parameters = leaf.get('parameters', None) parameterized = True selected_params = None @@ -515,7 +518,7 @@ def _get_issue_description_line(self, mark, default_url): issues += template.format(issue_id=issue_id, url=issue_url) return ISSUE_DESCRIPTION_LINE_TEMPLATE.format(reason, issues) - def _get_issue(self, mark): + def _get_issue(self, mark) -> Issue: """Add issues description and issue_type to the test item. :param mark: pytest mark @@ -554,6 +557,17 @@ def _to_attribute(self, attribute_tuple): else: return {'value': attribute_tuple[1]} + def _process_item_name(self, item) -> str: + """ + Process Item Name if set. + + :param item: Pytest.Item + :return: Item Name string + """ + names = [m for m in item.iter_markers() if m.name == 'name'] + if len(names) > 0: + return self._get_item_name(names[0]) + def _get_parameters(self, item): """ Get params of item. @@ -575,7 +589,7 @@ def _process_test_case_id(self, leaf): return self._get_test_case_id(tc_ids[0], leaf) return self._get_test_case_id(None, leaf) - def _process_issue(self, item): + def _process_issue(self, item) -> Issue: """ Process Issue if set. @@ -611,20 +625,21 @@ def _process_attributes(self, item): return [self._to_attribute(attribute) for attribute in attributes] - def _process_metadata_item_start(self, leaf): + def _process_metadata_item_start(self, leaf: Dict[str, Any]): """ Process all types of item metadata for its start event. :param leaf: item context """ item = leaf['item'] + leaf['name'] = self._get_item_name(item) leaf['parameters'] = self._get_parameters(item) leaf['code_ref'] = self._get_code_ref(item) leaf['test_case_id'] = self._process_test_case_id(leaf) leaf['issue'] = self._process_issue(item) leaf['attributes'] = self._process_attributes(item) - def _process_metadata_item_finish(self, leaf): + def _process_metadata_item_finish(self, leaf: Dict[str, Any]): """ Process all types of item metadata for its finish event. @@ -637,7 +652,7 @@ def _process_metadata_item_finish(self, leaf): def _build_start_step_rq(self, leaf): payload = { 'attributes': leaf.get('attributes', None), - 'name': self._get_item_name(leaf['name']), + 'name': self._truncate_item_name(leaf['name']), 'description': self._get_item_description(leaf['item']), 'start_time': timestamp(), 'item_type': 'STEP', @@ -674,10 +689,6 @@ def start_pytest_item(self, test_item: Optional[Item] = None): self.start() self._create_suite_path(test_item) - - # Item type should be sent as "STEP" until we upgrade to RPv6. - # Details at: - # https://github.com/reportportal/agent-Python-RobotFramework/issues/56 current_leaf = self._tree_path[test_item][-1] self._process_metadata_item_start(current_leaf) item_id = self._start_step(self._build_start_step_rq(current_leaf)) From 471fe8a3070525befba45b3904d735bdd3a7c741 Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Mon, 2 Dec 2024 18:02:58 +0300 Subject: [PATCH 12/23] Custom Item name: WIP --- pytest_reportportal/service.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pytest_reportportal/service.py b/pytest_reportportal/service.py index 73abfc1..b2656b5 100644 --- a/pytest_reportportal/service.py +++ b/pytest_reportportal/service.py @@ -431,8 +431,8 @@ def _create_suite_path(self, item: Item): continue self._lock(leaf, lambda p: self._create_suite(p)) - def _get_item_name(self, mark) -> str: - pass + def _get_item_name(self, mark, leaf: Dict[str, Any]) -> str: + return leaf['name'] def _get_code_ref(self, item): # Generate script path from work dir, use only backslashes to have the @@ -557,16 +557,17 @@ def _to_attribute(self, attribute_tuple): else: return {'value': attribute_tuple[1]} - def _process_item_name(self, item) -> str: + def _process_item_name(self, leaf: Dict[str, Any]) -> str: """ Process Item Name if set. - :param item: Pytest.Item + :param leaf: item context :return: Item Name string """ + item = leaf['item'] names = [m for m in item.iter_markers() if m.name == 'name'] if len(names) > 0: - return self._get_item_name(names[0]) + return self._get_item_name(names[0], leaf) def _get_parameters(self, item): """ @@ -632,7 +633,7 @@ def _process_metadata_item_start(self, leaf: Dict[str, Any]): :param leaf: item context """ item = leaf['item'] - leaf['name'] = self._get_item_name(item) + leaf['name'] = self._process_item_name(leaf) leaf['parameters'] = self._get_parameters(item) leaf['code_ref'] = self._get_code_ref(item) leaf['test_case_id'] = self._process_test_case_id(leaf) From 1bcb242a907389df7a971e8872eb60d2dc344f77 Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Mon, 2 Dec 2024 18:08:04 +0300 Subject: [PATCH 13/23] Custom Item name: WIP --- pytest_reportportal/service.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pytest_reportportal/service.py b/pytest_reportportal/service.py index b2656b5..26a0a62 100644 --- a/pytest_reportportal/service.py +++ b/pytest_reportportal/service.py @@ -431,8 +431,8 @@ def _create_suite_path(self, item: Item): continue self._lock(leaf, lambda p: self._create_suite(p)) - def _get_item_name(self, mark, leaf: Dict[str, Any]) -> str: - return leaf['name'] + def _get_item_name(self, mark) -> Optional[str]: + pass def _get_code_ref(self, item): # Generate script path from work dir, use only backslashes to have the @@ -565,9 +565,13 @@ def _process_item_name(self, leaf: Dict[str, Any]) -> str: :return: Item Name string """ item = leaf['item'] + name = leaf['name'] names = [m for m in item.iter_markers() if m.name == 'name'] if len(names) > 0: - return self._get_item_name(names[0], leaf) + mark_name = self._get_item_name(names[0]) + if mark_name: + name = mark_name + return name def _get_parameters(self, item): """ From aebe8a926efe7d3c821d9a238a67d4eb540a047d Mon Sep 17 00:00:00 2001 From: Raz Amir <168956317+ramir-dn@users.noreply.github.com> Date: Mon, 2 Dec 2024 19:02:13 +0200 Subject: [PATCH 14/23] Update test_plugin.py --- tests/unit/test_plugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/test_plugin.py b/tests/unit/test_plugin.py index 6aa125e..9e1619e 100644 --- a/tests/unit/test_plugin.py +++ b/tests/unit/test_plugin.py @@ -353,6 +353,7 @@ def test_pytest_addoption_adds_correct_ini_file_arguments(): 'rp_hierarchy_code', 'rp_hierarchy_dirs_level', 'rp_hierarchy_dirs', + 'rp_display_suite_test_file', 'rp_hierarchy_dir_path_separator', 'rp_issue_system_url', 'rp_bts_issue_url', From 54c04456d5bf27142e256e3718c1cc15666ed168 Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Tue, 3 Dec 2024 10:58:16 +0300 Subject: [PATCH 15/23] Custom Item name: done --- examples/custom_name/test_custom_name_args.py | 22 ++++++++++ .../custom_name/test_custom_name_empty.py | 22 ++++++++++ .../custom_name/test_custom_name_kwargs.py | 22 ++++++++++ pytest_reportportal/plugin.py | 3 ++ pytest_reportportal/service.py | 2 +- tests/integration/test_custom_name.py | 43 +++++++++++++++++++ 6 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 examples/custom_name/test_custom_name_args.py create mode 100644 examples/custom_name/test_custom_name_empty.py create mode 100644 examples/custom_name/test_custom_name_kwargs.py create mode 100644 tests/integration/test_custom_name.py diff --git a/examples/custom_name/test_custom_name_args.py b/examples/custom_name/test_custom_name_args.py new file mode 100644 index 0000000..cf2386f --- /dev/null +++ b/examples/custom_name/test_custom_name_args.py @@ -0,0 +1,22 @@ +# Copyright 2024 EPAM Systems +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pytest + +TEST_NAME_ARGS = 'Test name by mark' + + +@pytest.mark.name(TEST_NAME_ARGS) +def test_name_by_mark_args(): + """Simple example test with the name comes from Pytest mark.""" + assert True diff --git a/examples/custom_name/test_custom_name_empty.py b/examples/custom_name/test_custom_name_empty.py new file mode 100644 index 0000000..50373fe --- /dev/null +++ b/examples/custom_name/test_custom_name_empty.py @@ -0,0 +1,22 @@ +# Copyright 2024 EPAM Systems +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pytest + +TEST_NAME_EMPTY = 'examples/custom_name/test_custom_name_empty.py::test_name_by_mark_empty' + + +@pytest.mark.name() +def test_name_by_mark_empty(): + """Simple example test with the name comes from Pytest mark.""" + assert True diff --git a/examples/custom_name/test_custom_name_kwargs.py b/examples/custom_name/test_custom_name_kwargs.py new file mode 100644 index 0000000..c4bce53 --- /dev/null +++ b/examples/custom_name/test_custom_name_kwargs.py @@ -0,0 +1,22 @@ +# Copyright 2024 EPAM Systems +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pytest + +TEST_NAME_KWARGS = 'Test name by mark, kwargs' + + +@pytest.mark.name(name=TEST_NAME_KWARGS) +def test_name_by_mark_kwargs(): + """Simple example test with the name comes from Pytest mark.""" + assert True diff --git a/pytest_reportportal/plugin.py b/pytest_reportportal/plugin.py index 6857f41..14a1917 100644 --- a/pytest_reportportal/plugin.py +++ b/pytest_reportportal/plugin.py @@ -165,6 +165,9 @@ def register_markers(config) -> None: "params [parameter names as list] - use only specified" "parameters" ) + config.addinivalue_line( + "markers", "name(name): report the test case with a custom Name." + ) def check_connection(agent_config: AgentConfig): diff --git a/pytest_reportportal/service.py b/pytest_reportportal/service.py index 8ac0a4c..164ec7e 100644 --- a/pytest_reportportal/service.py +++ b/pytest_reportportal/service.py @@ -442,7 +442,7 @@ def _create_suite_path(self, item: Item): self._lock(leaf, lambda p: self._create_suite(p)) def _get_item_name(self, mark) -> Optional[str]: - pass + return mark.kwargs.get('name', mark.args[0] if mark.args else None) def _get_code_ref(self, item): # Generate script path from work dir, use only backslashes to have the diff --git a/tests/integration/test_custom_name.py b/tests/integration/test_custom_name.py new file mode 100644 index 0000000..0d31b7d --- /dev/null +++ b/tests/integration/test_custom_name.py @@ -0,0 +1,43 @@ +# Copyright 2024 EPAM Systems +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import mock + +import pytest + +from custom_name.test_custom_name_args import TEST_NAME_ARGS +from custom_name.test_custom_name_empty import TEST_NAME_EMPTY +from custom_name.test_custom_name_kwargs import TEST_NAME_KWARGS +from tests import REPORT_PORTAL_SERVICE +from tests.helpers import utils + + +@pytest.mark.parametrize('test, expected', [ + ('examples/custom_name/test_custom_name_args.py', TEST_NAME_ARGS), + ('examples/custom_name/test_custom_name_kwargs.py', TEST_NAME_KWARGS), + ('examples/custom_name/test_custom_name_empty.py', TEST_NAME_EMPTY) +]) +@mock.patch(REPORT_PORTAL_SERVICE) +def test_custom_attribute_report(mock_client_init, test, expected): + result = utils.run_pytest_tests(tests=[test], variables=utils.DEFAULT_VARIABLES) + assert int(result) == 0, 'Exit code should be 0 (no errors)' + + mock_client = mock_client_init.return_value + start_count = mock_client.start_test_item.call_count + finish_count = mock_client.finish_test_item.call_count + assert start_count == finish_count == 1, 'Incorrect number of "start_test_item" or "finish_test_item" calls' + + call_args = mock_client.start_test_item.call_args_list + step_call_args = call_args[0][1] + assert step_call_args['name'] == expected, 'Incorrect item name' From ef035e796188b6679c4d7d954dbb5031fd321716 Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Tue, 3 Dec 2024 11:09:48 +0300 Subject: [PATCH 16/23] Custom Item name: remove unwanted attribute --- pytest_reportportal/service.py | 5 +++-- tests/integration/test_custom_name.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pytest_reportportal/service.py b/pytest_reportportal/service.py index 164ec7e..0a2a4ab 100644 --- a/pytest_reportportal/service.py +++ b/pytest_reportportal/service.py @@ -629,6 +629,8 @@ def _process_attributes(self, item): for issue_id in self._get_issue_ids(marker): attributes.add((marker.name, issue_id)) continue + if marker.name == 'name': + continue if marker.name in self._config.rp_ignore_attributes \ or marker.name in PYTEST_MARKS_IGNORE: continue @@ -637,8 +639,7 @@ def _process_attributes(self, item): else: attributes.add((None, marker.name)) - return [self._to_attribute(attribute) - for attribute in attributes] + return [self._to_attribute(attribute) for attribute in attributes] def _process_metadata_item_start(self, leaf: Dict[str, Any]): """ diff --git a/tests/integration/test_custom_name.py b/tests/integration/test_custom_name.py index 0d31b7d..e900d79 100644 --- a/tests/integration/test_custom_name.py +++ b/tests/integration/test_custom_name.py @@ -41,3 +41,4 @@ def test_custom_attribute_report(mock_client_init, test, expected): call_args = mock_client.start_test_item.call_args_list step_call_args = call_args[0][1] assert step_call_args['name'] == expected, 'Incorrect item name' + assert step_call_args['attributes'] == [], 'No attributes should be added for the test item' From a3aee0d326aeee45ba911f24f37c6bf2845c1d55 Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Tue, 3 Dec 2024 12:50:38 +0300 Subject: [PATCH 17/23] Test fix --- tests/integration/test_custom_name.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/test_custom_name.py b/tests/integration/test_custom_name.py index e900d79..d0c1c98 100644 --- a/tests/integration/test_custom_name.py +++ b/tests/integration/test_custom_name.py @@ -16,9 +16,9 @@ import pytest -from custom_name.test_custom_name_args import TEST_NAME_ARGS -from custom_name.test_custom_name_empty import TEST_NAME_EMPTY -from custom_name.test_custom_name_kwargs import TEST_NAME_KWARGS +from examples.custom_name.test_custom_name_args import TEST_NAME_ARGS +from examples.custom_name.test_custom_name_empty import TEST_NAME_EMPTY +from examples.custom_name.test_custom_name_kwargs import TEST_NAME_KWARGS from tests import REPORT_PORTAL_SERVICE from tests.helpers import utils From f73ae02fc9d177f41bce028caf5c97d9a240433e Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Tue, 3 Dec 2024 13:01:34 +0300 Subject: [PATCH 18/23] Reformat --- pytest_reportportal/config.py | 62 +++++++++++------------------------ 1 file changed, 19 insertions(+), 43 deletions(-) diff --git a/pytest_reportportal/config.py b/pytest_reportportal/config.py index f0c9f9b..0c51d5b 100644 --- a/pytest_reportportal/config.py +++ b/pytest_reportportal/config.py @@ -19,8 +19,8 @@ from _pytest.config import Config from reportportal_client import OutputType, ClientType -from reportportal_client.logs import MAX_LOG_BATCH_PAYLOAD_SIZE from reportportal_client.helpers import to_bool +from reportportal_client.logs import MAX_LOG_BATCH_PAYLOAD_SIZE try: # This try/except can go away once we support pytest >= 5.4.0 @@ -72,26 +72,16 @@ class AgentConfig: def __init__(self, pytest_config: Config) -> None: """Initialize required attributes.""" - self.rp_rerun = (pytest_config.option.rp_rerun or - pytest_config.getini('rp_rerun')) + self.rp_rerun = (pytest_config.option.rp_rerun or pytest_config.getini('rp_rerun')) self.rp_endpoint = self.find_option(pytest_config, 'rp_endpoint') - self.rp_hierarchy_code = self.find_option(pytest_config, - 'rp_hierarchy_code') - self.rp_dir_level = int(self.find_option(pytest_config, - 'rp_hierarchy_dirs_level')) - self.rp_hierarchy_dirs = self.find_option(pytest_config, - 'rp_hierarchy_dirs') - self.rp_display_suite_test_file = self.find_option(pytest_config, - 'rp_display_suite_test_file') - self.rp_dir_path_separator = \ - self.find_option(pytest_config, 'rp_hierarchy_dir_path_separator') + self.rp_hierarchy_code = self.find_option(pytest_config, 'rp_hierarchy_code') + self.rp_dir_level = int(self.find_option(pytest_config, 'rp_hierarchy_dirs_level')) + self.rp_hierarchy_dirs = self.find_option(pytest_config, 'rp_hierarchy_dirs') + self.rp_display_suite_test_file = self.find_option(pytest_config, 'rp_display_suite_test_file') + self.rp_dir_path_separator = self.find_option(pytest_config, 'rp_hierarchy_dir_path_separator') self.rp_ignore_attributes = set(self.find_option(pytest_config, 'rp_ignore_attributes') or []) - self.rp_is_skipped_an_issue = self.find_option( - pytest_config, - 'rp_is_skipped_an_issue' - ) - self.rp_issue_id_marks = self.find_option(pytest_config, - 'rp_issue_id_marks') + self.rp_is_skipped_an_issue = self.find_option(pytest_config, 'rp_is_skipped_an_issue') + self.rp_issue_id_marks = self.find_option(pytest_config, 'rp_issue_id_marks') self.rp_bts_issue_url = self.find_option(pytest_config, 'rp_bts_issue_url') if not self.rp_bts_issue_url: self.rp_bts_issue_url = self.find_option(pytest_config, 'rp_issue_system_url') @@ -106,14 +96,10 @@ def __init__(self, pytest_config: Config) -> None: self.rp_bts_url = self.find_option(pytest_config, 'rp_bts_url') self.rp_launch = self.find_option(pytest_config, 'rp_launch') self.rp_launch_id = self.find_option(pytest_config, 'rp_launch_id') - self.rp_launch_attributes = self.find_option(pytest_config, - 'rp_launch_attributes') - self.rp_launch_description = self.find_option(pytest_config, - 'rp_launch_description') - self.rp_log_batch_size = int(self.find_option(pytest_config, - 'rp_log_batch_size')) - batch_payload_size = self.find_option( - pytest_config, 'rp_log_batch_payload_size') + self.rp_launch_attributes = self.find_option(pytest_config, 'rp_launch_attributes') + self.rp_launch_description = self.find_option(pytest_config, 'rp_launch_description') + self.rp_log_batch_size = int(self.find_option(pytest_config, 'rp_log_batch_size')) + batch_payload_size = self.find_option(pytest_config, 'rp_log_batch_payload_size') if batch_payload_size: self.rp_log_batch_payload_size = int(batch_payload_size) else: @@ -122,16 +108,10 @@ def __init__(self, pytest_config: Config) -> None: self.rp_log_format = self.find_option(pytest_config, 'rp_log_format') self.rp_thread_logging = to_bool(self.find_option(pytest_config, 'rp_thread_logging') or False) self.rp_mode = self.find_option(pytest_config, 'rp_mode') - self.rp_parent_item_id = self.find_option(pytest_config, - 'rp_parent_item_id') - self.rp_project = self.find_option(pytest_config, - 'rp_project') - self.rp_rerun_of = self.find_option(pytest_config, - 'rp_rerun_of') - self.rp_skip_connection_test = str( - self.find_option(pytest_config, - 'rp_skip_connection_test')).lower() in ( - 'true', '1', 'yes', 'y') + self.rp_parent_item_id = self.find_option(pytest_config, 'rp_parent_item_id') + self.rp_project = self.find_option(pytest_config, 'rp_project') + self.rp_rerun_of = self.find_option(pytest_config, 'rp_rerun_of') + self.rp_skip_connection_test = to_bool(self.find_option(pytest_config, 'rp_skip_connection_test')) rp_api_retries_str = self.find_option(pytest_config, 'rp_api_retries') rp_api_retries = rp_api_retries_str and int(rp_api_retries_str) @@ -182,8 +162,7 @@ def __init__(self, pytest_config: Config) -> None: self.rp_verify_ssl = to_bool(rp_verify_ssl) except (ValueError, AttributeError): self.rp_verify_ssl = rp_verify_ssl - self.rp_launch_timeout = int( - self.find_option(pytest_config, 'rp_launch_timeout')) + self.rp_launch_timeout = int(self.find_option(pytest_config, 'rp_launch_timeout')) self.rp_launch_uuid_print = to_bool(self.find_option(pytest_config, 'rp_launch_uuid_print') or 'False') print_output = self.find_option(pytest_config, 'rp_launch_uuid_print_output') @@ -218,10 +197,7 @@ def find_option(self, pytest_config: Config, option_name: str, default: Any = No :param default: value to be returned if not found :return: option value """ - value = ( - getattr(pytest_config.option, option_name, None) or - pytest_config.getini(option_name) - ) + value = (getattr(pytest_config.option, option_name, None) or pytest_config.getini(option_name)) if isinstance(value, bool): return value return value or default From 7b76677b83371589305d744451e00bf4525a635f Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Tue, 3 Dec 2024 16:20:47 +0300 Subject: [PATCH 19/23] Add handling of `rp_hierarchy_test_file` flag --- pytest_reportportal/config.py | 4 +-- pytest_reportportal/plugin.py | 10 +++---- pytest_reportportal/service.py | 55 ++++++++++++++++++++-------------- 3 files changed, 39 insertions(+), 30 deletions(-) diff --git a/pytest_reportportal/config.py b/pytest_reportportal/config.py index 0c51d5b..b9049b9 100644 --- a/pytest_reportportal/config.py +++ b/pytest_reportportal/config.py @@ -40,7 +40,7 @@ class AgentConfig: rp_hierarchy_code: bool rp_dir_level: int rp_hierarchy_dirs: bool - rp_display_suite_test_file: bool + rp_hierarchy_test_file: bool rp_dir_path_separator: str rp_ignore_attributes: set rp_is_skipped_an_issue: bool @@ -77,7 +77,7 @@ def __init__(self, pytest_config: Config) -> None: self.rp_hierarchy_code = self.find_option(pytest_config, 'rp_hierarchy_code') self.rp_dir_level = int(self.find_option(pytest_config, 'rp_hierarchy_dirs_level')) self.rp_hierarchy_dirs = self.find_option(pytest_config, 'rp_hierarchy_dirs') - self.rp_display_suite_test_file = self.find_option(pytest_config, 'rp_display_suite_test_file') + self.rp_hierarchy_test_file = self.find_option(pytest_config, 'rp_hierarchy_test_file') self.rp_dir_path_separator = self.find_option(pytest_config, 'rp_hierarchy_dir_path_separator') self.rp_ignore_attributes = set(self.find_option(pytest_config, 'rp_ignore_attributes') or []) self.rp_is_skipped_an_issue = self.find_option(pytest_config, 'rp_is_skipped_an_issue') diff --git a/pytest_reportportal/plugin.py b/pytest_reportportal/plugin.py index 14a1917..580e13a 100644 --- a/pytest_reportportal/plugin.py +++ b/pytest_reportportal/plugin.py @@ -504,15 +504,15 @@ def add_shared_option(name, help_str, default=None, action='store'): default=False, type='bool', help='Enables hierarchy for directories') - parser.addini( - 'rp_display_suite_test_file', - default=True, - type='bool', - help='Show file name in hierarchy. Depends on rp_hierarchy_dirs_level to get deep enough') parser.addini( 'rp_hierarchy_dir_path_separator', default=os.path.sep, help='Path separator to display directories in test hierarchy') + parser.addini( + 'rp_hierarchy_test_file', + default=True, + type='bool', + help='Show file name in hierarchy. Depends on rp_hierarchy_dirs_level to get deep enough') parser.addini( 'rp_issue_system_url', default='', diff --git a/pytest_reportportal/service.py b/pytest_reportportal/service.py index 0a2a4ab..adef8ca 100644 --- a/pytest_reportportal/service.py +++ b/pytest_reportportal/service.py @@ -241,7 +241,7 @@ def _get_tree_path(self, item: Item) -> List[Item]: path.reverse() return path - def _get_leaf(self, leaf_type: LeafType, parent_item: Optional[Dict[str, Any]], item: Optional[Item], + def _get_leaf(self, leaf_type, parent_item: Optional[Dict[str, Any]], item: Optional[Item], item_id: Optional[str] = None) -> Dict[str, Any]: """Construct a leaf for the itest tree. @@ -283,27 +283,36 @@ def _build_test_tree(self, session: Session) -> Dict[str, Any]: current_leaf = children_leafs[leaf] return test_tree - def _remove_node(self, test_tree: Dict[str, Any], max_dir_level, dir_level, recursive): - parent_leaf = test_tree['parent'] - current_item = test_tree['item'] - del parent_leaf['children'][current_item] - for item, child_leaf in test_tree['children'].items(): - parent_leaf['children'][item] = child_leaf - child_leaf['parent'] = parent_leaf - if recursive: - self._remove_root_dirs(child_leaf, max_dir_level, dir_level + 1) - - def _remove_root_dirs(self, test_tree: Dict[str, Any], max_dir_level, dir_level=0) -> None: + def _remove_root_dirs(self, test_tree: Dict[str, Any], max_dir_level: int, dir_level: int = 0) -> None: if test_tree['type'] == LeafType.ROOT: - for child_leaf in list(test_tree['children'].values()): + items = list(test_tree['children'].items()) + for item, child_leaf in items: self._remove_root_dirs(child_leaf, max_dir_level, 1) return if test_tree['type'] == LeafType.DIR and dir_level <= max_dir_level: - self._remove_node(test_tree, max_dir_level, dir_level, recursive=True) - return - if test_tree['type'] == LeafType.FILE and not self._config.rp_display_suite_test_file: - self._remove_node(test_tree, max_dir_level, dir_level, recursive=False) + new_level = dir_level + 1 + parent_leaf = test_tree['parent'] + current_item = test_tree['item'] + del parent_leaf['children'][current_item] + for item, child_leaf in test_tree['children'].items(): + parent_leaf['children'][item] = child_leaf + child_leaf['parent'] = parent_leaf + self._remove_root_dirs(child_leaf, max_dir_level, new_level) + + def _remove_file_names(self, test_tree: Dict[str, Any]) -> None: + if test_tree['type'] != LeafType.FILE: + items = list(test_tree['children'].items()) + for item, child_leaf in items: + self._remove_file_names(child_leaf) return + if not self._config.rp_hierarchy_test_file: + parent_leaf = test_tree['parent'] + current_item = test_tree['item'] + del parent_leaf['children'][current_item] + for item, child_leaf in test_tree['children'].items(): + parent_leaf['children'][item] = child_leaf + child_leaf['parent'] = parent_leaf + self._remove_file_names(child_leaf) def _generate_names(self, test_tree: Dict[str, Any]) -> None: if test_tree['type'] == LeafType.ROOT: @@ -312,7 +321,7 @@ def _generate_names(self, test_tree: Dict[str, Any]) -> None: if test_tree['type'] == LeafType.DIR: test_tree['name'] = test_tree['item'].basename - if test_tree['type'] in [LeafType.CODE, LeafType.FILE]: + if test_tree['type'] in {LeafType.CODE, LeafType.FILE}: item = test_tree['item'] if isinstance(item, Module): test_tree['name'] = os.path.split(str(item.fspath))[1] @@ -322,7 +331,7 @@ def _generate_names(self, test_tree: Dict[str, Any]) -> None: for item, child_leaf in test_tree['children'].items(): self._generate_names(child_leaf) - def _merge_leaf_types(self, test_tree, leaf_types, separator): + def _merge_leaf_types(self, test_tree: Dict[str, Any], leaf_types: Set, separator: str): child_items = list(test_tree['children'].items()) if test_tree['type'] not in leaf_types: for item, child_leaf in child_items: @@ -335,15 +344,14 @@ def _merge_leaf_types(self, test_tree, leaf_types, separator): for item, child_leaf in child_items: parent_leaf['children'][item] = child_leaf child_leaf['parent'] = parent_leaf - child_leaf['name'] = \ - current_name + separator + child_leaf['name'] + child_leaf['name'] = current_name + separator + child_leaf['name'] self._merge_leaf_types(child_leaf, leaf_types, separator) def _merge_dirs(self, test_tree: Dict[str, Any]) -> None: - self._merge_leaf_types(test_tree, [LeafType.DIR], self._config.rp_dir_path_separator) + self._merge_leaf_types(test_tree, {LeafType.DIR}, self._config.rp_dir_path_separator) def _merge_code(self, test_tree: Dict[str, Any]) -> None: - self._merge_leaf_types(test_tree, [LeafType.CODE, LeafType.FILE], '::') + self._merge_leaf_types(test_tree, {LeafType.CODE, LeafType.FILE}, '::') def _build_item_paths(self, leaf: Dict[str, Any], path: List[Dict[str, Any]]) -> None: if 'children' in leaf and len(leaf['children']) > 0: @@ -363,6 +371,7 @@ def collect_tests(self, session: Session) -> None: # Create a test tree to be able to apply mutations test_tree = self._build_test_tree(session) self._remove_root_dirs(test_tree, self._config.rp_dir_level) + self._remove_file_names(test_tree) self._generate_names(test_tree) if not self._config.rp_hierarchy_dirs: self._merge_dirs(test_tree) From aab48918fcf38855f1ff46b6c1521fde06a4aa13 Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Tue, 3 Dec 2024 16:33:27 +0300 Subject: [PATCH 20/23] Fix tests --- pytest_reportportal/config.py | 6 +++--- pytest_reportportal/plugin.py | 2 +- tests/unit/conftest.py | 4 ++++ tests/unit/test_plugin.py | 8 ++++---- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/pytest_reportportal/config.py b/pytest_reportportal/config.py index b9049b9..0b20d34 100644 --- a/pytest_reportportal/config.py +++ b/pytest_reportportal/config.py @@ -74,11 +74,11 @@ def __init__(self, pytest_config: Config) -> None: """Initialize required attributes.""" self.rp_rerun = (pytest_config.option.rp_rerun or pytest_config.getini('rp_rerun')) self.rp_endpoint = self.find_option(pytest_config, 'rp_endpoint') - self.rp_hierarchy_code = self.find_option(pytest_config, 'rp_hierarchy_code') + self.rp_hierarchy_code = to_bool(self.find_option(pytest_config, 'rp_hierarchy_code')) self.rp_dir_level = int(self.find_option(pytest_config, 'rp_hierarchy_dirs_level')) - self.rp_hierarchy_dirs = self.find_option(pytest_config, 'rp_hierarchy_dirs') - self.rp_hierarchy_test_file = self.find_option(pytest_config, 'rp_hierarchy_test_file') + self.rp_hierarchy_dirs = to_bool(self.find_option(pytest_config, 'rp_hierarchy_dirs')) self.rp_dir_path_separator = self.find_option(pytest_config, 'rp_hierarchy_dir_path_separator') + self.rp_hierarchy_test_file = to_bool(self.find_option(pytest_config, 'rp_hierarchy_test_file')) self.rp_ignore_attributes = set(self.find_option(pytest_config, 'rp_ignore_attributes') or []) self.rp_is_skipped_an_issue = self.find_option(pytest_config, 'rp_is_skipped_an_issue') self.rp_issue_id_marks = self.find_option(pytest_config, 'rp_issue_id_marks') diff --git a/pytest_reportportal/plugin.py b/pytest_reportportal/plugin.py index 580e13a..4f6d462 100644 --- a/pytest_reportportal/plugin.py +++ b/pytest_reportportal/plugin.py @@ -512,7 +512,7 @@ def add_shared_option(name, help_str, default=None, action='store'): 'rp_hierarchy_test_file', default=True, type='bool', - help='Show file name in hierarchy. Depends on rp_hierarchy_dirs_level to get deep enough') + help='Show file name in hierarchy') parser.addini( 'rp_issue_system_url', default='', diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 232d073..1248b81 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -71,6 +71,10 @@ def getoption_side_effect(name, default=None): mocked_config.option.rp_launch_uuid_print_output = 'STDOUT' mocked_config.option.rp_client_type = 'SYNC' mocked_config.option.rp_report_fixtures = 'False' + mocked_config.option.rp_hierarchy_code = 'False' + mocked_config.option.rp_hierarchy_dirs = 'False' + mocked_config.option.rp_hierarchy_test_file = 'True' + mocked_config.option.rp_skip_connection_test = 'False' return mocked_config diff --git a/tests/unit/test_plugin.py b/tests/unit/test_plugin.py index 9e1619e..85745fa 100644 --- a/tests/unit/test_plugin.py +++ b/tests/unit/test_plugin.py @@ -55,8 +55,8 @@ def test_logger_handle_attachment(mock_handler, logger, log_level): log_call("Some {} message".format(log_level), attachment=attachment) expect(mock_handler.call_count == 1, 'logger.handle called more than 1 time') - expect(getattr(mock_handler.call_args[0][0], "attachment") == attachment, - 'record.attachment in args doesn\'t match real value') + expect(getattr(mock_handler.call_args[0][0], 'attachment') == attachment, + "record.attachment in args doesn't match real value") assert_expectations() @@ -327,6 +327,7 @@ def test_pytest_sessionfinish(mocked_session): def test_pytest_addoption_adds_correct_ini_file_arguments(): """Test the correct list of options are available in the .ini file.""" + mock_parser = mock.MagicMock(spec=Parser) expected_argument_names = ( 'rp_launch', 'rp_launch_id', @@ -353,8 +354,8 @@ def test_pytest_addoption_adds_correct_ini_file_arguments(): 'rp_hierarchy_code', 'rp_hierarchy_dirs_level', 'rp_hierarchy_dirs', - 'rp_display_suite_test_file', 'rp_hierarchy_dir_path_separator', + 'rp_hierarchy_test_file', 'rp_issue_system_url', 'rp_bts_issue_url', 'rp_bts_project', @@ -370,7 +371,6 @@ def test_pytest_addoption_adds_correct_ini_file_arguments(): 'rp_read_timeout', 'rp_report_fixtures' ) - mock_parser = mock.MagicMock(spec=Parser) pytest_addoption(mock_parser) From ac0d3308aae3f5110ca967784f9e9cb6871093c5 Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Tue, 3 Dec 2024 16:49:57 +0300 Subject: [PATCH 21/23] Add more tests --- tests/integration/__init__.py | 15 +++++++++++++-- tests/integration/test_suite_hierarchy.py | 8 +++----- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index ae8d352..a6ecfed 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -26,7 +26,8 @@ ] + \ [['examples/params/test_in_class_parameterized.py']] * 5 + \ [['examples/hierarchy/inner/test_inner_simple.py']] * 7 + \ - [['examples/hierarchy/test_in_class_in_class.py']] + [['examples/hierarchy/test_in_class_in_class.py']] + \ + [['examples/test_simple.py']] * 2 # noinspection PyTypeChecker HIERARCHY_TEST_VARIABLES = \ @@ -54,7 +55,9 @@ dict({'rp_hierarchy_dirs_level': -1, 'rp_hierarchy_code': True, }, **utils.DEFAULT_VARIABLES), dict(**utils.DEFAULT_VARIABLES), - dict(**utils.DEFAULT_VARIABLES) + dict(**utils.DEFAULT_VARIABLES), + dict({'rp_hierarchy_test_file': False}, **utils.DEFAULT_VARIABLES), + dict({'rp_hierarchy_test_file': False, 'rp_hierarchy_dirs_level': 1}, **utils.DEFAULT_VARIABLES) ] HIERARCHY_TEST_EXPECTED_ITEMS = [ @@ -235,6 +238,14 @@ {'name': 'examples/hierarchy/test_in_class_in_class.py::Tests::Test' '::test_in_class_in_class', 'item_type': 'STEP', 'parent_item_id': lambda x: x is None} + ], + [ + {'name': 'examples/test_simple', + 'item_type': 'STEP', 'parent_item_id': lambda x: x is None} + ], + [ + {'name': 'test_simple', + 'item_type': 'STEP', 'parent_item_id': lambda x: x is None} ] ] diff --git a/tests/integration/test_suite_hierarchy.py b/tests/integration/test_suite_hierarchy.py index d61a272..248b47f 100644 --- a/tests/integration/test_suite_hierarchy.py +++ b/tests/integration/test_suite_hierarchy.py @@ -14,7 +14,6 @@ """This module includes integration tests for different suite hierarchy.""" import pytest -from delayed_assert import expect, assert_expectations from unittest import mock from tests import REPORT_PORTAL_SERVICE @@ -29,11 +28,10 @@ def verify_start_item_parameters(mock_client, expected_items): call_args = mock_client.start_test_item.call_args_list for i, call in enumerate(call_args): start_kwargs = call[1] - expect(start_kwargs['name'] == expected_items[i]['name']) - expect(start_kwargs['item_type'] == expected_items[i]['item_type']) + assert start_kwargs['name'] == expected_items[i]['name'] + assert start_kwargs['item_type'] == expected_items[i]['item_type'] verification = expected_items[i]['parent_item_id'] - expect(verification(start_kwargs['parent_item_id'])) - assert_expectations() + assert verification(start_kwargs['parent_item_id']) @pytest.mark.parametrize(('test', 'variables', 'expected_items'), From 4f9a8f746b80d0707ecb7630d06db64f38f38467 Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Tue, 3 Dec 2024 17:05:54 +0300 Subject: [PATCH 22/23] Reformat --- tests/integration/__init__.py | 205 ++++++++++++---------------------- 1 file changed, 71 insertions(+), 134 deletions(-) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index a6ecfed..e062897 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -31,29 +31,22 @@ # noinspection PyTypeChecker HIERARCHY_TEST_VARIABLES = \ - [dict({'rp_hierarchy_dirs': True, 'rp_hierarchy_code': True}, - **utils.DEFAULT_VARIABLES)] * 6 + \ - [ - dict({'rp_hierarchy_dirs': True, 'rp_hierarchy_code': True, - 'rp_hierarchy_dirs_level': 1}, **utils.DEFAULT_VARIABLES), - dict({'rp_hierarchy_dirs': True, 'rp_hierarchy_code': True, - 'rp_hierarchy_dirs_level': 2}, **utils.DEFAULT_VARIABLES), - dict({'rp_hierarchy_dirs': True, 'rp_hierarchy_code': True, - 'rp_hierarchy_dirs_level': 999}, **utils.DEFAULT_VARIABLES), - dict({'rp_hierarchy_dirs': True, 'rp_hierarchy_code': True, - 'rp_hierarchy_dirs_level': -1}, **utils.DEFAULT_VARIABLES), - dict({'rp_hierarchy_dir_path_separator': '/', - 'rp_hierarchy_code': True}, **utils.DEFAULT_VARIABLES), - dict({'rp_hierarchy_dir_path_separator': '\\', - 'rp_hierarchy_code': True}, **utils.DEFAULT_VARIABLES), - dict({'rp_hierarchy_dirs_level': 1, 'rp_hierarchy_code': True, - }, **utils.DEFAULT_VARIABLES), - dict({'rp_hierarchy_dirs_level': 2, 'rp_hierarchy_code': True, - }, **utils.DEFAULT_VARIABLES), - dict({'rp_hierarchy_dirs_level': 999, 'rp_hierarchy_code': True, - }, **utils.DEFAULT_VARIABLES), - dict({'rp_hierarchy_dirs_level': -1, 'rp_hierarchy_code': True, - }, **utils.DEFAULT_VARIABLES), + [dict({'rp_hierarchy_dirs': True, 'rp_hierarchy_code': True}, **utils.DEFAULT_VARIABLES)] * 6 + \ + [ + dict({'rp_hierarchy_dirs': True, 'rp_hierarchy_code': True, 'rp_hierarchy_dirs_level': 1}, + **utils.DEFAULT_VARIABLES), + dict({'rp_hierarchy_dirs': True, 'rp_hierarchy_code': True, 'rp_hierarchy_dirs_level': 2}, + **utils.DEFAULT_VARIABLES), + dict({'rp_hierarchy_dirs': True, 'rp_hierarchy_code': True, 'rp_hierarchy_dirs_level': 999}, + **utils.DEFAULT_VARIABLES), + dict({'rp_hierarchy_dirs': True, 'rp_hierarchy_code': True, 'rp_hierarchy_dirs_level': -1}, + **utils.DEFAULT_VARIABLES), + dict({'rp_hierarchy_dir_path_separator': '/', 'rp_hierarchy_code': True}, **utils.DEFAULT_VARIABLES), + dict({'rp_hierarchy_dir_path_separator': '\\', 'rp_hierarchy_code': True}, **utils.DEFAULT_VARIABLES), + dict({'rp_hierarchy_dirs_level': 1, 'rp_hierarchy_code': True}, **utils.DEFAULT_VARIABLES), + dict({'rp_hierarchy_dirs_level': 2, 'rp_hierarchy_code': True}, **utils.DEFAULT_VARIABLES), + dict({'rp_hierarchy_dirs_level': 999, 'rp_hierarchy_code': True}, **utils.DEFAULT_VARIABLES), + dict({'rp_hierarchy_dirs_level': -1, 'rp_hierarchy_code': True}, **utils.DEFAULT_VARIABLES), dict(**utils.DEFAULT_VARIABLES), dict(**utils.DEFAULT_VARIABLES), dict({'rp_hierarchy_test_file': False}, **utils.DEFAULT_VARIABLES), @@ -62,190 +55,134 @@ HIERARCHY_TEST_EXPECTED_ITEMS = [ [ - {'name': 'examples', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x is None}, - {'name': 'test_simple.py', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x.startswith('examples')}, - {'name': 'test_simple', 'item_type': 'STEP', - 'parent_item_id': lambda x: x.startswith('test_simple.py')} + {'name': 'examples', 'item_type': 'SUITE', 'parent_item_id': lambda x: x is None}, + {'name': 'test_simple.py', 'item_type': 'SUITE', 'parent_item_id': lambda x: x.startswith('examples')}, + {'name': 'test_simple', 'item_type': 'STEP', 'parent_item_id': lambda x: x.startswith('test_simple.py')} ], [ - {'name': 'examples', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x is None}, - {'name': 'hierarchy', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x.startswith('examples')}, - {'name': 'inner', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x.startswith('hierarchy')}, - {'name': 'test_inner_simple.py', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x.startswith('inner')}, - {'name': 'test_simple', 'item_type': 'STEP', - 'parent_item_id': lambda x: x.startswith('test_inner_simple.py')} + {'name': 'examples', 'item_type': 'SUITE', 'parent_item_id': lambda x: x is None}, + {'name': 'hierarchy', 'item_type': 'SUITE', 'parent_item_id': lambda x: x.startswith('examples')}, + {'name': 'inner', 'item_type': 'SUITE', 'parent_item_id': lambda x: x.startswith('hierarchy')}, + {'name': 'test_inner_simple.py', 'item_type': 'SUITE', 'parent_item_id': lambda x: x.startswith('inner')}, + {'name': 'test_simple', 'item_type': 'STEP', 'parent_item_id': lambda x: x.startswith('test_inner_simple.py')} ], [ - {'name': 'examples', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x is None}, - {'name': 'hierarchy', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x.startswith('examples')}, - {'name': 'test_in_class.py', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x.startswith('hierarchy')}, - {'name': 'Tests', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x.startswith('test_in_class.py')}, - {'name': 'test_in_class', 'item_type': 'STEP', - 'parent_item_id': lambda x: x.startswith('Tests')} + {'name': 'examples', 'item_type': 'SUITE', 'parent_item_id': lambda x: x is None}, + {'name': 'hierarchy', 'item_type': 'SUITE', 'parent_item_id': lambda x: x.startswith('examples')}, + {'name': 'test_in_class.py', 'item_type': 'SUITE', 'parent_item_id': lambda x: x.startswith('hierarchy')}, + {'name': 'Tests', 'item_type': 'SUITE', 'parent_item_id': lambda x: x.startswith('test_in_class.py')}, + {'name': 'test_in_class', 'item_type': 'STEP', 'parent_item_id': lambda x: x.startswith('Tests')} ], [ - {'name': 'examples', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x is None}, - {'name': 'hierarchy', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x.startswith('examples')}, + {'name': 'examples', 'item_type': 'SUITE', 'parent_item_id': lambda x: x is None}, + {'name': 'hierarchy', 'item_type': 'SUITE', 'parent_item_id': lambda x: x.startswith('examples')}, {'name': 'test_in_class_in_class.py', 'item_type': 'SUITE', 'parent_item_id': lambda x: x.startswith('hierarchy')}, {'name': 'Tests', 'item_type': 'SUITE', - 'parent_item_id': lambda x: - x.startswith('test_in_class_in_class.py')}, - {'name': 'Test', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x.startswith('Tests')}, - {'name': 'test_in_class_in_class', 'item_type': 'STEP', - 'parent_item_id': lambda x: x.startswith('Test')} + 'parent_item_id': lambda x: x.startswith('test_in_class_in_class.py')}, + {'name': 'Test', 'item_type': 'SUITE', 'parent_item_id': lambda x: x.startswith('Tests')}, + {'name': 'test_in_class_in_class', 'item_type': 'STEP', 'parent_item_id': lambda x: x.startswith('Test')} ], [ - {'name': 'examples', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x is None}, - {'name': 'hierarchy', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x.startswith('examples')}, - {'name': 'another_inner', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x.startswith('hierarchy')}, + {'name': 'examples', 'item_type': 'SUITE', 'parent_item_id': lambda x: x is None}, + {'name': 'hierarchy', 'item_type': 'SUITE', 'parent_item_id': lambda x: x.startswith('examples')}, + {'name': 'another_inner', 'item_type': 'SUITE', 'parent_item_id': lambda x: x.startswith('hierarchy')}, {'name': 'test_another_inner_simple.py', 'item_type': 'SUITE', 'parent_item_id': lambda x: x.startswith('another_inner')}, {'name': 'test_simple', 'item_type': 'STEP', - 'parent_item_id': lambda x: x.startswith( - 'test_another_inner_simple.py')}, - {'name': 'inner', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x.startswith('hierarchy')}, - {'name': 'test_inner_simple.py', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x.startswith('inner')}, - {'name': 'test_simple', 'item_type': 'STEP', - 'parent_item_id': lambda x: x.startswith('test_inner_simple.py')} + 'parent_item_id': lambda x: x.startswith('test_another_inner_simple.py')}, + {'name': 'inner', 'item_type': 'SUITE', 'parent_item_id': lambda x: x.startswith('hierarchy')}, + {'name': 'test_inner_simple.py', 'item_type': 'SUITE', 'parent_item_id': lambda x: x.startswith('inner')}, + {'name': 'test_simple', 'item_type': 'STEP', 'parent_item_id': lambda x: x.startswith('test_inner_simple.py')} ], [ - {'name': 'examples', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x is None}, - {'name': 'params', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x.startswith('examples')}, + {'name': 'examples', 'item_type': 'SUITE', 'parent_item_id': lambda x: x is None}, + {'name': 'params', 'item_type': 'SUITE', 'parent_item_id': lambda x: x.startswith('examples')}, {'name': 'test_in_class_parameterized.py', 'item_type': 'SUITE', 'parent_item_id': lambda x: x.startswith('params')}, {'name': 'Tests', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x.startswith( - 'test_in_class_parameterized.py')}, + 'parent_item_id': lambda x: x.startswith('test_in_class_parameterized.py')}, {'name': 'test_in_class_parameterized[param]', 'item_type': 'STEP', 'parent_item_id': lambda x: x.startswith('Tests')} ], [ - {'name': 'params', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x is None}, + {'name': 'params', 'item_type': 'SUITE', 'parent_item_id': lambda x: x is None}, {'name': 'test_in_class_parameterized.py', 'item_type': 'SUITE', 'parent_item_id': lambda x: x.startswith('params')}, {'name': 'Tests', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x.startswith( - 'test_in_class_parameterized.py')}, + 'parent_item_id': lambda x: x.startswith('test_in_class_parameterized.py')}, {'name': 'test_in_class_parameterized[param]', 'item_type': 'STEP', 'parent_item_id': lambda x: x.startswith('Tests')} ], [ - {'name': 'test_in_class_parameterized.py', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x is None}, + {'name': 'test_in_class_parameterized.py', 'item_type': 'SUITE', 'parent_item_id': lambda x: x is None}, {'name': 'Tests', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x.startswith( - 'test_in_class_parameterized.py')}, + 'parent_item_id': lambda x: x.startswith('test_in_class_parameterized.py')}, {'name': 'test_in_class_parameterized[param]', 'item_type': 'STEP', 'parent_item_id': lambda x: x.startswith('Tests')} ], [ - {'name': 'test_in_class_parameterized.py', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x is None}, + {'name': 'test_in_class_parameterized.py', 'item_type': 'SUITE', 'parent_item_id': lambda x: x is None}, {'name': 'Tests', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x.startswith( - 'test_in_class_parameterized.py')}, + 'parent_item_id': lambda x: x.startswith('test_in_class_parameterized.py')}, {'name': 'test_in_class_parameterized[param]', 'item_type': 'STEP', 'parent_item_id': lambda x: x.startswith('Tests')} ], [ - {'name': 'examples', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x is None}, - {'name': 'params', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x.startswith('examples')}, + {'name': 'examples', 'item_type': 'SUITE', 'parent_item_id': lambda x: x is None}, + {'name': 'params', 'item_type': 'SUITE', 'parent_item_id': lambda x: x.startswith('examples')}, {'name': 'test_in_class_parameterized.py', 'item_type': 'SUITE', 'parent_item_id': lambda x: x.startswith('params')}, {'name': 'Tests', 'item_type': 'SUITE', - 'parent_item_id': lambda x: x.startswith( - 'test_in_class_parameterized.py')}, + 'parent_item_id': lambda x: x.startswith('test_in_class_parameterized.py')}, {'name': 'test_in_class_parameterized[param]', 'item_type': 'STEP', 'parent_item_id': lambda x: x.startswith('Tests')} ], [ - {'name': 'examples/hierarchy/inner/test_inner_simple.py', - 'item_type': 'SUITE', 'parent_item_id': lambda x: x is None}, + {'name': 'examples/hierarchy/inner/test_inner_simple.py', 'item_type': 'SUITE', + 'parent_item_id': lambda x: x is None}, {'name': 'test_simple', 'item_type': 'STEP', - 'parent_item_id': - lambda x: - x.startswith('examples/hierarchy/inner/test_inner_simple.py')} + 'parent_item_id': lambda x: x.startswith('examples/hierarchy/inner/test_inner_simple.py')} ], [ - {'name': 'examples\\hierarchy\\inner\\test_inner_simple.py', - 'item_type': 'SUITE', 'parent_item_id': lambda x: x is None}, + {'name': 'examples\\hierarchy\\inner\\test_inner_simple.py', 'item_type': 'SUITE', + 'parent_item_id': lambda x: x is None}, {'name': 'test_simple', 'item_type': 'STEP', - 'parent_item_id': - lambda x: - x.startswith('examples\\hierarchy\\inner\\test_inner_simple.py')} + 'parent_item_id': lambda x: x.startswith('examples\\hierarchy\\inner\\test_inner_simple.py')} ], [ - {'name': 'hierarchy/inner/test_inner_simple.py', - 'item_type': 'SUITE', 'parent_item_id': lambda x: x is None}, + {'name': 'hierarchy/inner/test_inner_simple.py', 'item_type': 'SUITE', 'parent_item_id': lambda x: x is None}, {'name': 'test_simple', 'item_type': 'STEP', - 'parent_item_id': - lambda x: - x.startswith('hierarchy/inner/test_inner_simple.py')} + 'parent_item_id': lambda x: x.startswith('hierarchy/inner/test_inner_simple.py')} ], [ - {'name': 'inner/test_inner_simple.py', - 'item_type': 'SUITE', 'parent_item_id': lambda x: x is None}, + {'name': 'inner/test_inner_simple.py', 'item_type': 'SUITE', 'parent_item_id': lambda x: x is None}, {'name': 'test_simple', 'item_type': 'STEP', - 'parent_item_id': - lambda x: - x.startswith('inner/test_inner_simple.py')} + 'parent_item_id': lambda x: x.startswith('inner/test_inner_simple.py')} ], [ - {'name': 'test_inner_simple.py', - 'item_type': 'SUITE', 'parent_item_id': lambda x: x is None}, - {'name': 'test_simple', 'item_type': 'STEP', - 'parent_item_id': - lambda x: - x.startswith('test_inner_simple.py')} + {'name': 'test_inner_simple.py', 'item_type': 'SUITE', 'parent_item_id': lambda x: x is None}, + {'name': 'test_simple', 'item_type': 'STEP', 'parent_item_id': lambda x: x.startswith('test_inner_simple.py')} ], [ {'name': 'examples/hierarchy/inner/test_inner_simple.py', 'item_type': 'SUITE', 'parent_item_id': lambda x: x is None}, {'name': 'test_simple', 'item_type': 'STEP', - 'parent_item_id': - lambda x: - x.startswith('examples/hierarchy/inner/test_inner_simple.py')} + 'parent_item_id': lambda x: x.startswith('examples/hierarchy/inner/test_inner_simple.py')} ], [ - {'name': 'examples/hierarchy/inner/test_inner_simple.py::test_simple', - 'item_type': 'STEP', 'parent_item_id': lambda x: x is None} + {'name': 'examples/hierarchy/inner/test_inner_simple.py::test_simple', 'item_type': 'STEP', + 'parent_item_id': lambda x: x is None} ], [ - {'name': 'examples/hierarchy/test_in_class_in_class.py::Tests::Test' - '::test_in_class_in_class', + {'name': 'examples/hierarchy/test_in_class_in_class.py::Tests::Test::test_in_class_in_class', 'item_type': 'STEP', 'parent_item_id': lambda x: x is None} ], [ - {'name': 'examples/test_simple', - 'item_type': 'STEP', 'parent_item_id': lambda x: x is None} + {'name': 'examples/test_simple', 'item_type': 'STEP', 'parent_item_id': lambda x: x is None} ], [ - {'name': 'test_simple', - 'item_type': 'STEP', 'parent_item_id': lambda x: x is None} + {'name': 'test_simple', 'item_type': 'STEP', 'parent_item_id': lambda x: x is None} ] ] From ed5f381367da7db5c22c5e413107e5ba644066a1 Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Tue, 3 Dec 2024 17:21:44 +0300 Subject: [PATCH 23/23] CHANGELOG.md update --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c31413..bfe6031 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## [Unreleased] ### Added - Support for `Python 3.13`, by @HardNorth +- Support for `name` Pytest marker, by @HardNorth +- `rp_hierarchy_test_file` configuration parameter, which controls display of test file name in the hierarchy, by @ramir-dn, @HardNorth ### Fixed - Agent crash if Client could not be initialized, by @HardNorth ### Changed