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 7ee3ab3..bfe6031 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ # Changelog ## [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 +- Client version updated on [5.5.10](https://github.com/reportportal/client-Python/releases/tag/5.5.10), by @HardNorth + +## [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 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/config.py b/pytest_reportportal/config.py index 8663af5..0b20d34 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 @@ -40,6 +40,7 @@ class AgentConfig: rp_hierarchy_code: bool rp_dir_level: int rp_hierarchy_dirs: bool + rp_hierarchy_test_file: bool rp_dir_path_separator: str rp_ignore_attributes: set rp_is_skipped_an_issue: bool @@ -71,24 +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_dir_path_separator = \ - self.find_option(pytest_config, 'rp_hierarchy_dir_path_separator') + 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 = 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') + 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') @@ -103,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: @@ -119,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) @@ -179,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') @@ -215,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 diff --git a/pytest_reportportal/plugin.py b/pytest_reportportal/plugin.py index ae2f07e..4f6d462 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): @@ -505,6 +508,11 @@ def add_shared_option(name, help_str, default=None, action='store'): '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') parser.addini( 'rp_issue_system_url', default='', 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): diff --git a/pytest_reportportal/service.py b/pytest_reportportal/service.py index 295fb61..adef8ca 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 @@ -99,6 +100,7 @@ class LeafType(Enum): """This class stores test item path types.""" DIR = auto() + FILE = auto() CODE = auto() ROOT = auto() @@ -166,6 +168,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"] @@ -209,7 +213,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. @@ -218,8 +222,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="") + 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] def _get_tree_path(self, item: Item) -> List[Item]: @@ -238,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. @@ -270,7 +273,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,11 +283,12 @@ def _build_test_tree(self, session: Session) -> Dict[str, Any]: current_leaf = children_leafs[leaf] return test_tree - 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 item, child_leaf in test_tree['children'].items(): + items = list(test_tree['children'].items()) + for item, child_leaf in items: 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'] @@ -293,6 +299,21 @@ def _remove_root_dirs(self, test_tree: Dict[str, Any], max_dir_level, dir_level= 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: test_tree['name'] = 'root' @@ -300,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'] == 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 +331,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: Dict[str, Any], leaf_types: Set, separator: str): 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'] @@ -323,15 +344,14 @@ def _merge_leaf_type(self, test_tree, leaf_type, 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'] - self._merge_leaf_type(child_leaf, leaf_type, separator) + 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_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: @@ -351,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) @@ -358,7 +379,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 @@ -401,7 +422,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', @@ -429,6 +450,9 @@ def _create_suite_path(self, item: Item): continue self._lock(leaf, lambda p: self._create_suite(p)) + def _get_item_name(self, mark) -> Optional[str]: + 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 # same path on different systems and do not affect Test Case ID on @@ -449,7 +473,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 @@ -509,13 +533,11 @@ 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): + def _get_issue(self, mark) -> Issue: """Add issues description and issue_type to the test item. :param mark: pytest mark @@ -523,8 +545,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 @@ -532,22 +553,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 @@ -558,6 +576,22 @@ def _to_attribute(self, attribute_tuple): else: return {'value': attribute_tuple[1]} + def _process_item_name(self, leaf: Dict[str, Any]) -> str: + """ + Process Item Name if set. + + :param leaf: item context + :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: + mark_name = self._get_item_name(names[0]) + if mark_name: + name = mark_name + return name + def _get_parameters(self, item): """ Get params of item. @@ -579,7 +613,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. @@ -604,6 +638,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 @@ -612,23 +648,23 @@ 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): + 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._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) 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. @@ -641,7 +677,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', @@ -678,10 +714,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)) @@ -864,6 +896,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 +910,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) 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 diff --git a/setup.py b/setup.py index d6793f7..5ecc76d 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): @@ -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/tests/integration/__init__.py b/tests/integration/__init__.py index ae8d352..e062897 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -26,215 +26,163 @@ ] + \ [['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 = \ - [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(**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 = [ [ - {'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': 'test_simple', 'item_type': 'STEP', 'parent_item_id': lambda x: x is None} ] ] diff --git a/tests/integration/test_custom_name.py b/tests/integration/test_custom_name.py new file mode 100644 index 0000000..d0c1c98 --- /dev/null +++ b/tests/integration/test_custom_name.py @@ -0,0 +1,44 @@ +# 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 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 + + +@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' + assert step_call_args['attributes'] == [], 'No attributes should be added for the test item' 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'), 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 6aa125e..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', @@ -354,6 +355,7 @@ def test_pytest_addoption_adds_correct_ini_file_arguments(): 'rp_hierarchy_dirs_level', 'rp_hierarchy_dirs', 'rp_hierarchy_dir_path_separator', + 'rp_hierarchy_test_file', 'rp_issue_system_url', 'rp_bts_issue_url', 'rp_bts_project', @@ -369,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) 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