Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 23 additions & 14 deletions pytest_reportportal/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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."""
Expand Down Expand Up @@ -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"))
Expand Down Expand Up @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -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.<option_name>``.
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
31 changes: 26 additions & 5 deletions pytest_reportportal/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from setuptools import setup

__version__ = "5.6.0"
__version__ = "5.6.1"


def read_file(fname):
Expand Down
2 changes: 2 additions & 0 deletions tests/unit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
45 changes: 45 additions & 0 deletions tests/unit/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
44 changes: 44 additions & 0 deletions tests/unit/test_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading