From e4e9451792d8b7e3ab8a0cd2054e661513308974 Mon Sep 17 00:00:00 2001 From: "reportportal.io" Date: Fri, 10 Apr 2026 20:26:54 +0000 Subject: [PATCH 1/4] Changelog update --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e61ddda..aa6303b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## [Unreleased] + +## [5.6.6] ### Added - Microseconds precision for timestamps, by @HardNorth ### Changed From 9bc916555d0ddeb88f568ffa848ccf7a56d7694b Mon Sep 17 00:00:00 2001 From: "reportportal.io" Date: Fri, 10 Apr 2026 20:26:54 +0000 Subject: [PATCH 2/4] Version update --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7dd91d5..edae772 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ from setuptools import setup -__version__ = "5.6.6" +__version__ = "5.6.7" def read_file(fname): From 1f5a788f38b80ae1d7e267c41114c17de02ba2ad Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Wed, 15 Apr 2026 10:39:52 +0300 Subject: [PATCH 3/4] Attribute splitting --- CHANGELOG.md | 2 ++ pytest_reportportal/config.py | 22 +++++++++++++++-- tests/integration/test_attributes.py | 37 ++++++++++++++++++++++++++++ tests/unit/test_config.py | 34 +++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa6303b..e7c0ffd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## [Unreleased] +### Added +- Attribute splitting if they are passed as `str` in configs, by @HardNorth ## [5.6.6] ### Added diff --git a/pytest_reportportal/config.py b/pytest_reportportal/config.py index 29e56be..2ec362d 100644 --- a/pytest_reportportal/config.py +++ b/pytest_reportportal/config.py @@ -24,6 +24,24 @@ from reportportal_client.logs import MAX_LOG_BATCH_PAYLOAD_SIZE +ATTRIBUTES_SEPARATOR = ";" + + +def normalize_attributes(attributes: Optional[Any]) -> Optional[Any]: + if not attributes: + return attributes + if not isinstance(attributes, str): + return attributes + normalized_attributes = [] + unique_attributes = set() + for attribute in attributes.split(ATTRIBUTES_SEPARATOR): + attribute = attribute.strip() + if attribute and attribute not in unique_attributes: + unique_attributes.add(attribute) + normalized_attributes.append(attribute) + return normalized_attributes + + class AgentConfig: """Storage for the RP agent initialization attributes.""" @@ -115,8 +133,8 @@ def __init__(self, pytest_config: Config) -> None: ) self.rp_launch_uuid = self.find_option(pytest_config, "rp_launch_uuid", self.rp_launch_uuid) - self.rp_launch_attributes = self.find_option(pytest_config, "rp_launch_attributes") - self.rp_tests_attributes = self.find_option(pytest_config, "rp_tests_attributes") + self.rp_launch_attributes = normalize_attributes(self.find_option(pytest_config, "rp_launch_attributes")) + self.rp_tests_attributes = normalize_attributes(self.find_option(pytest_config, "rp_tests_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_limit = self.find_option(pytest_config, "rp_log_batch_payload_limit") diff --git a/tests/integration/test_attributes.py b/tests/integration/test_attributes.py index a09e58c..cff940c 100644 --- a/tests/integration/test_attributes.py +++ b/tests/integration/test_attributes.py @@ -168,3 +168,40 @@ def test_rp_tests_attributes_add(mock_client_init): assert len(attributes) == 2 assert {"key": "scope", "value": "smoke"} in attributes assert {"key": "test_key", "value": "test_value"} in attributes + + +@mock.patch(REPORT_PORTAL_SERVICE) +def test_rp_tests_attributes_string_split_and_deduplicated(mock_client_init, monkeypatch): + """Verify string `rp_tests_attributes` are split and deduplicated.""" + monkeypatch.setenv("RP_TESTS_ATTRIBUTES", " test_key:test_value ; smoke ; test_key:test_value ") + variables = {} + variables.update(utils.DEFAULT_VARIABLES.items()) + result = utils.run_pytest_tests(tests=["examples/test_simple.py"], variables=variables) + assert int(result) == 0, "Exit code should be 0 (no errors)" + + mock_client = mock_client_init.return_value + assert mock_client.start_test_item.call_count > 0, '"start_test_item" called incorrect number of times' + + call_args = mock_client.start_test_item.call_args_list + step_call_args = call_args[-1][1] + actual_attributes = step_call_args["attributes"] + + assert utils.attributes_to_tuples(actual_attributes) == {("test_key", "test_value"), (None, "smoke")} + + +@mock.patch(REPORT_PORTAL_SERVICE) +def test_rp_launch_attributes_string_split_and_deduplicated(mock_client_init, monkeypatch): + """Verify string `rp_launch_attributes` are split and deduplicated.""" + monkeypatch.setenv("RP_LAUNCH_ATTRIBUTES", " launch_key:launch_value ; smoke ; launch_key:launch_value ") + variables = {} + variables.update(utils.DEFAULT_VARIABLES.items()) + result = utils.run_pytest_tests(tests=["examples/test_simple.py"], variables=variables) + assert int(result) == 0, "Exit code should be 0 (no errors)" + + mock_client = mock_client_init.return_value + assert mock_client.start_launch.call_count > 0, '"start_launch" called incorrect number of times' + + launch_call_args = mock_client.start_launch.call_args_list + launch_attributes = launch_call_args[0][1]["attributes"] + + assert {("launch_key", "launch_value"), (None, "smoke")} <= utils.attributes_to_tuples(launch_attributes) diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index c1c932a..f29c8e2 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -79,3 +79,37 @@ def test_env_var_overrides_log_level(monkeypatch, mocked_config): def test_env_var_not_set_falls_back_to_config(mocked_config): config = AgentConfig(mocked_config) assert config.rp_endpoint == "http://docker.local:8080/" + + +@pytest.mark.parametrize( + ["option_name", "option_value", "expected_result"], + [ + ("rp_launch_attributes", " smoke ; launch:demo ; smoke ; launch:demo ", ["smoke", "launch:demo"]), + ("rp_tests_attributes", " test:key ; smoke ; test:key ", ["test:key", "smoke"]), + ], +) +def test_string_attributes_are_split_and_deduplicated(mocked_config, option_name, option_value, expected_result): + mocked_config.option.rp_launch_attributes = None + mocked_config.option.rp_tests_attributes = None + mocked_config.getini.side_effect = lambda x: option_value if x == option_name else None + + config = AgentConfig(mocked_config) + + assert getattr(config, option_name) == expected_result + + +@pytest.mark.parametrize( + ["option_name", "option_value"], + [ + ("rp_launch_attributes", ["smoke", "smoke"]), + ("rp_tests_attributes", ["test:key", "test:key"]), + ], +) +def test_attributes_not_split_if_not_string(mocked_config, option_name, option_value): + mocked_config.option.rp_launch_attributes = None + mocked_config.option.rp_tests_attributes = None + mocked_config.getini.side_effect = lambda x: option_value if x == option_name else None + + config = AgentConfig(mocked_config) + + assert getattr(config, option_name) == option_value From 4920dd2b9efcc7a3aecece6fae58aacbf9a6ba3d Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Wed, 15 Apr 2026 10:55:12 +0300 Subject: [PATCH 4/4] Fix style checks --- pytest_reportportal/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_reportportal/config.py b/pytest_reportportal/config.py index 2ec362d..ba0b70b 100644 --- a/pytest_reportportal/config.py +++ b/pytest_reportportal/config.py @@ -23,11 +23,11 @@ from reportportal_client.helpers import to_bool from reportportal_client.logs import MAX_LOG_BATCH_PAYLOAD_SIZE - ATTRIBUTES_SEPARATOR = ";" def normalize_attributes(attributes: Optional[Any]) -> Optional[Any]: + """Split a string of attributes into a deduplicated list of attributes.""" if not attributes: return attributes if not isinstance(attributes, str):