diff --git a/CHANGELOG.md b/CHANGELOG.md index 7427836..0b8c3d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,17 @@ ## [Unreleased] ### Added +- Config variables override by environment variables logic, by @HardNorth +### Fixed +- Code reference generation in `xdist`, by @Sgitario + +## [5.6.0] +### Added - Official `Python 3.14` support, by @HardNorth - Issue [#396](https://github.com/reportportal/agent-python-pytest/issues/396) parametrize marker IDs, by @HardNorth - Custom log level handling with `rp_log_custom_levels` configuration parameter, by @HardNorth +### Changed +- Client version updated on [5.7.0](https://github.com/reportportal/client-Python/releases/tag/5.7.0), by @HardNorth ### Removed - `Python 3.8` support, by @HardNorth - Deprecated `retries` parameter, by @HardNorth diff --git a/pytest_reportportal/config.py b/pytest_reportportal/config.py index 9e48473..0352fa7 100644 --- a/pytest_reportportal/config.py +++ b/pytest_reportportal/config.py @@ -13,6 +13,7 @@ """This module contains class that stores RP agent configuration data.""" +import logging import warnings from os import getenv from typing import Any, Optional, Union @@ -22,12 +23,6 @@ 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 - from _pytest.logging import get_actual_log_level -except ImportError: - from _pytest.logging import get_log_level_for_setting as get_actual_log_level - class AgentConfig: """Storage for the RP agent initialization attributes.""" @@ -86,9 +81,9 @@ class AgentConfig: def __init__(self, pytest_config: Config) -> None: """Initialize required attributes.""" - self.rp_enabled = to_bool(getattr(pytest_config.option, "rp_enabled", True)) - self.rp_rerun = pytest_config.option.rp_rerun or pytest_config.getini("rp_rerun") - self.rp_endpoint = getenv("RP_ENDPOINT") or self.find_option(pytest_config, "rp_endpoint") + self.rp_enabled = to_bool(self.find_option(pytest_config, "rp_enabled", True)) + self.rp_rerun = self.find_option(pytest_config, "rp_rerun") + self.rp_endpoint = self.find_option(pytest_config, "rp_endpoint") 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")) @@ -133,7 +128,13 @@ def __init__(self, pytest_config: Config) -> None: else: self.rp_log_batch_payload_limit = MAX_LOG_BATCH_PAYLOAD_SIZE - self.rp_log_level = get_actual_log_level(pytest_config, "rp_log_level") + log_level = self.find_option(pytest_config, "rp_log_level") + if not log_level: + self.rp_log_level = None + elif isinstance(log_level, int): + self.rp_log_level = log_level + else: + self.rp_log_level = int(getattr(logging, str(log_level).upper(), log_level)) 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") @@ -145,7 +146,7 @@ def __init__(self, pytest_config: Config) -> None: self.rp_api_retries = rp_api_retries_str and int(rp_api_retries_str) # API key auth parameter - self.rp_api_key = getenv("RP_API_KEY") or self.find_option(pytest_config, "rp_api_key") + self.rp_api_key = self.find_option(pytest_config, "rp_api_key") # OAuth 2.0 parameters self.rp_oauth_uri = self.find_option(pytest_config, "rp_oauth_uri") @@ -197,15 +198,23 @@ def find_option(self, pytest_config: Config, option_name: str, default: Any = No The value is retrieved in the following places in priority order: - 1. From `self.pconfig.option.[option_name]`. - 2. From `self.pconfig.getini(option_name)`. + 1. From environment variable ``option_name.upper()`` (e.g. ``rp_endpoint`` -> ``RP_ENDPOINT``). + 2. From ``pytest_config.option.``. + 3. From ``pytest_config.getini(option_name)``. + 4. ``default`` value. :param pytest_config: config object of PyTest :param option_name: name of the option :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) + env_value = getenv(option_name.upper()) + if env_value: + return env_value + value = getattr(pytest_config.option, option_name, None) + if value is not None: + return value + value = pytest_config.getini(option_name) if isinstance(value, bool): return value return value or default diff --git a/pytest_reportportal/service.py b/pytest_reportportal/service.py index 521242c..235ebc7 100644 --- a/pytest_reportportal/service.py +++ b/pytest_reportportal/service.py @@ -367,6 +367,31 @@ def _get_scenario_template(self, scenario: Scenario) -> Optional[ScenarioTemplat if scenario_template and isinstance(scenario_template, ScenarioTemplate): return scenario_template + def _get_method_name(self, item: Item) -> str: + """Get the original test method name. + + Returns item.originalname if available, + otherwise strips any trailing @suffix from item.name while + preserving @ inside parameter brackets. + + :param item: pytest.Item + :return: original method name + """ + if hasattr(item, "originalname") and item.originalname is not None: + return item.originalname + + name = item.name + if "@" not in name: + return name + + last_bracket = name.rfind("]") + at_pos = name.rfind("@") + + if at_pos > last_bracket: + return name[:at_pos] + + return name + def _generate_names(self, test_tree: dict[str, Any]) -> None: if test_tree["type"] == LeafType.ROOT: test_tree["name"] = "root" @@ -584,11 +609,7 @@ def _get_code_ref(self, item: Item) -> str: # same path on different systems and do not affect Test Case ID on # different systems path = os.path.relpath(str(item.fspath), ROOT_DIR).replace("\\", "/") - method_name = ( - item.originalname - if hasattr(item, "originalname") and getattr(item, "originalname") is not None - else item.name - ) + method_name = self._get_method_name(item) parent = item.parent classes = [method_name] while not isinstance(parent, Module): diff --git a/setup.py b/setup.py index febcf4a..58ebab9 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ from setuptools import setup -__version__ = "5.6.0" +__version__ = "5.6.1" def read_file(fname): diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 28cdb51..4f66484 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -70,6 +70,8 @@ def getoption_side_effect(name, default=None): mocked_config.option.rp_hierarchy_dirs = "False" mocked_config.option.rp_hierarchy_test_file = "True" mocked_config.option.rp_skip_connection_test = "False" + mocked_config.option.rp_enabled = True + mocked_config.option.rp_log_level = "debug" return mocked_config diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index bf70947..d8bf04c 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -34,3 +34,48 @@ def test_verify_ssl_true(mocked_config, verify_ssl, expected_result): config = AgentConfig(mocked_config) assert config.rp_verify_ssl == expected_result + + +@pytest.mark.parametrize( + ["env_var", "env_value", "config_attr", "expected"], + [ + ("RP_ENDPOINT", "http://env.example.com", "rp_endpoint", "http://env.example.com"), + ("RP_PROJECT", "env_project", "rp_project", "env_project"), + ("RP_LAUNCH", "env_launch", "rp_launch", "env_launch"), + ("RP_API_KEY", "env_api_key", "rp_api_key", "env_api_key"), + ("RP_LAUNCH_ID", "env_launch_id", "rp_launch_id", "env_launch_id"), + ("RP_MODE", "DEBUG", "rp_mode", "DEBUG"), + ("RP_PARENT_ITEM_ID", "env_parent_id", "rp_parent_item_id", "env_parent_id"), + ("RP_RERUN_OF", "env_rerun_of", "rp_rerun_of", "env_rerun_of"), + ], +) +def test_env_var_overrides_config(monkeypatch, mocked_config, env_var, env_value, config_attr, expected): + monkeypatch.setenv(env_var, env_value) + config = AgentConfig(mocked_config) + assert getattr(config, config_attr) == expected + + +def test_env_var_overrides_verify_ssl(monkeypatch, mocked_config): + monkeypatch.setenv("RP_VERIFY_SSL", "False") + config = AgentConfig(mocked_config) + assert config.rp_verify_ssl is False + + +def test_env_var_overrides_enabled(monkeypatch, mocked_config): + mocked_config.option.rp_enabled = False + monkeypatch.setenv("RP_ENABLED", "True") + config = AgentConfig(mocked_config) + assert config.rp_enabled is True + + +def test_env_var_overrides_log_level(monkeypatch, mocked_config): + import logging + + monkeypatch.setenv("RP_LOG_LEVEL", "ERROR") + config = AgentConfig(mocked_config) + assert config.rp_log_level == logging.ERROR + + +def test_env_var_not_set_falls_back_to_config(mocked_config): + config = AgentConfig(mocked_config) + assert config.rp_endpoint == "http://docker.local:8080/" diff --git a/tests/unit/test_service.py b/tests/unit/test_service.py index 286a4c4..b914cde 100644 --- a/tests/unit/test_service.py +++ b/tests/unit/test_service.py @@ -26,3 +26,47 @@ def test_get_item_parameters(mocked_item, rp_service): expect(rp_service._get_parameters(mocked_item) is None) assert_expectations() + + +def test_get_method_name_regular(mocked_item, rp_service): + """Test that regular test names are returned as-is.""" + mocked_item.name = "test_simple_function" + mocked_item.originalname = None + + result = rp_service._get_method_name(mocked_item) + + expect(result == "test_simple_function") + assert_expectations() + + +def test_get_method_name_uses_originalname(mocked_item, rp_service): + """Test that originalname is preferred when available.""" + mocked_item.name = "test_verify_data[Daily]@sync_group" + mocked_item.originalname = "test_verify_data" + + result = rp_service._get_method_name(mocked_item) + + expect(result == "test_verify_data") + assert_expectations() + + +def test_get_method_name_strips_suffix(mocked_item, rp_service): + """Test that trailing @suffix is stripped when originalname is None.""" + mocked_item.name = "test_export_data@data_export" + mocked_item.originalname = None + + result = rp_service._get_method_name(mocked_item) + + expect(result == "test_export_data") + assert_expectations() + + +def test_get_method_name_preserves_at_inside_params(mocked_item, rp_service): + """Test that @ inside parameter brackets is preserved.""" + mocked_item.name = "test_email[user@example.com]" + mocked_item.originalname = None + + result = rp_service._get_method_name(mocked_item) + + expect(result == "test_email[user@example.com]") + assert_expectations()