diff --git a/CHANGELOG.md b/CHANGELOG.md index e61ddda..e7c0ffd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] ### Added +- Attribute splitting if they are passed as `str` in configs, by @HardNorth + +## [5.6.6] +### Added - Microseconds precision for timestamps, by @HardNorth ### Changed - Client version updated to [5.7.4](https://github.com/reportportal/client-Python/releases/tag/5.7.4), by @HardNorth diff --git a/pytest_reportportal/config.py b/pytest_reportportal/config.py index 29e56be..ba0b70b 100644 --- a/pytest_reportportal/config.py +++ b/pytest_reportportal/config.py @@ -23,6 +23,24 @@ 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): + 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/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): 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