Skip to content
Open
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
4 changes: 4 additions & 0 deletions allure-behave/src/listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,10 @@ def global_attach_file(self, source, name, attachment_type, extension):
def global_error(self, message, trace):
self.logger.global_error(message=message, trace=trace)

@allure_commons.hookimpl
def add_environment(self, env):
self.logger.environment(env)

@allure_commons.hookimpl
def add_description(self, test_description):
test_result = self.logger.get_test(None)
Expand Down
5 changes: 5 additions & 0 deletions allure-pytest-bdd/src/allure_api_listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .utils import global_attach_data
from .utils import global_attach_file
from .utils import global_error
from .utils import environment
from .utils import get_link_patterns
from .steps import start_step
from .steps import stop_step
Expand Down Expand Up @@ -132,3 +133,7 @@ def global_attach_file(self, source, name, attachment_type, extension):
@allure_commons.hookimpl
def global_error(self, message, trace):
global_error(self.lifecycle, message, trace)

@allure_commons.hookimpl
def add_environment(self, env):
environment(self.lifecycle, env)
4 changes: 4 additions & 0 deletions allure-pytest-bdd/src/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,10 @@ def global_error(lifecycle, message, trace=None):
lifecycle.global_error(message=message, trace=trace)


def environment(lifecycle, env):
lifecycle.environment(env)


def format_csv(rows):
with io.StringIO() as buffer:
writer = csv.writer(buffer)
Expand Down
41 changes: 41 additions & 0 deletions allure-pytest/examples/environment/environment.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
Environment
-----------

Reports can display information about the environment the tests were executed in, such as the operating system, the
browser or the address of the test stand. It is shown on the report's overview page and is provided with invocation of
``allure.environment(*args, **kwargs)``.

Values may be passed as keyword arguments:

>>> import allure

>>> def test_environment_from_keywords():
... allure.environment(browser="chrome", stand="staging")

A mapping may be passed instead, which allows keys that are not valid python identifiers:

>>> def test_environment_from_mapping():
... allure.environment({"os.name": "Windows 11", "browser version": "125.0"})

Both forms may be combined in a single call:

>>> def test_environment_from_mapping_and_keywords():
... allure.environment({"python.version": "3.13.0"}, stand="production")

Values from all the calls made during the test run are merged together, so the environment may be filled in from
different places, for example, from a session-scoped fixture:

>>> import pytest

>>> @pytest.fixture(scope="session")
... def host_name():
... allure.environment(hostname="my.host.local")
... return "my.host.local"

>>> def test_environment_from_fixture(host_name):
... pass

A key set more than once keeps the value of the latest call.

The data is stored in the ``environment.properties`` file in the allure results directory. Note that the calls are only
recorded once the allure results directory is set up, i.e., starting from the ``pytest_sessionstart`` hook.
4 changes: 4 additions & 0 deletions allure-pytest/src/listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,10 @@ def global_attach_file(self, source, name, attachment_type, extension):
def global_error(self, message, trace):
self.allure_logger.global_error(message=message, trace=trace)

@allure_commons.hookimpl
def add_environment(self, env):
self.allure_logger.environment(env)

@allure_commons.hookimpl
def add_title(self, test_title):
test_result = self.allure_logger.get_test(None)
Expand Down
37 changes: 37 additions & 0 deletions allure-python-commons-test/src/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,37 @@
from hamcrest import only_contains
from hamcrest.core.base_matcher import BaseMatcher

ENVIRONMENT_FILE = "environment.properties"
UNESCAPES = {"n": "\n", "r": "\r", "t": "\t"}


def parse_properties(content):
"""Parses a java-style .properties content into (key, value) pairs."""

for line in content.splitlines():
line = line.lstrip()
if not line or line.startswith(("#", "!")):
continue
name, value = "", ""
target, escaped = "name", False
for character in line:
if escaped:
character = UNESCAPES.get(character, character)
elif character == "\\":
escaped = True
continue
elif target == "name" and character in ("=", ":"):
target = "value"
continue
elif target == "value" and not value and character in (" ", "\t"):
continue
escaped = False
if target == "name":
name += character
else:
value += character
yield name, value


class AllureReport:
def __init__(self, result):
Expand Down Expand Up @@ -105,6 +136,12 @@ def __init__(self, result):
"*globals.json"
)
]
self.environment = dict(
item for _, file in self._report_items(
result,
ENVIRONMENT_FILE
) for item in parse_properties(file.read())
)

@staticmethod
def _report_items(report_dir, glob):
Expand Down
2 changes: 2 additions & 0 deletions allure-python-commons/src/allure/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from allure_commons._allure import attach
from allure_commons._allure import global_attach
from allure_commons._allure import global_error
from allure_commons._allure import environment
from allure_commons._allure import manual
from allure_commons.types import Severity as severity_level
from allure_commons.types import AttachmentType as attachment_type
Expand Down Expand Up @@ -42,6 +43,7 @@
"attach",
"global_attach",
"global_error",
"environment",
"attachment_type",
"parameter_mode"
]
4 changes: 4 additions & 0 deletions allure-python-commons/src/allure_commons/_allure.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,10 @@ def global_error(value, trace=None):
plugin_manager.hook.global_error(message=message, trace=trace)


def environment(*args, **kwargs):
plugin_manager.hook.add_environment(env=dict(*args, **kwargs))


class fixture:
def __init__(self, fixture_function, parent_uuid=None, name=None):
self._fixture_function = fixture_function
Expand Down
8 changes: 8 additions & 0 deletions allure-python-commons/src/allure_commons/_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ def global_attach_file(self, source, name, attachment_type, extension):
def global_error(self, message, trace):
""" global error """

@hookspec
def add_environment(self, env):
""" environment """


class AllureDeveloperHooks:

Expand Down Expand Up @@ -116,3 +120,7 @@ def report_attached_data(self, body, file_name):
@hookspec
def report_globals(self, globals_item):
""" reporting """

@hookspec
def report_environment(self, env):
""" reporting """
3 changes: 3 additions & 0 deletions allure-python-commons/src/allure_commons/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ def global_error(self, message=None, trace=None):
GlobalError(message=message, trace=trace, timestamp=now())
]))

def environment(self, env):
plugin_manager.hook.report_environment(env=env)

def __resolve_attachment_filename_and_type(self, uuid, attachment_type=None, extension=None):
mime_type = attachment_type
extension = extension if extension else "attach"
Expand Down
16 changes: 16 additions & 0 deletions allure-python-commons/src/allure_commons/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
import shutil
from attr import asdict
from allure_commons import hookimpl
from allure_commons.model2 import ENVIRONMENT_FILE
from allure_commons.utils import format_properties

INDENT = 4


class AllureFileLogger:

def __init__(self, report_dir, clean=False):
self._environment = {}
self._report_dir = Path(report_dir).absolute()
if self._report_dir.is_dir() and clean:
shutil.rmtree(self._report_dir, ignore_errors=True)
Expand Down Expand Up @@ -51,6 +54,14 @@ def report_attached_data(self, body, file_name):
def report_globals(self, globals_item):
self._report_item(globals_item)

@hookimpl
def report_environment(self, env):
if not env:
return
self._environment.update(env)
with io.open(self._report_dir / ENVIRONMENT_FILE, "w", encoding="utf8") as environment_file:
environment_file.write(format_properties(self._environment))


class AllureMemoryLogger:

Expand All @@ -59,6 +70,7 @@ def __init__(self):
self.test_containers = []
self.attachments = {}
self.globals = []
self.environment = {}

@hookimpl
def report_result(self, result):
Expand All @@ -82,3 +94,7 @@ def report_attached_data(self, body, file_name):
def report_globals(self, globals_item):
data = asdict(globals_item, filter=lambda _, v: v or v is False)
self.globals.append(data)

@hookimpl
def report_environment(self, env):
self.environment.update(env)
1 change: 1 addition & 0 deletions allure-python-commons/src/allure_commons/model2.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
TEST_CASE_PATTERN = "{prefix}-result.json"
ATTACHMENT_PATTERN = "{prefix}-attachment.{ext}"
GLOBALS_PATTERN = "{prefix}-globals.json"
ENVIRONMENT_FILE = "environment.properties"
INDENT = 4


Expand Down
3 changes: 3 additions & 0 deletions allure-python-commons/src/allure_commons/reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ def global_error(self, message=None, trace=None):
GlobalError(message=message, trace=trace, timestamp=now())
]))

def environment(self, env):
plugin_manager.hook.report_environment(env=env)

def __resolve_attachment_filename_and_type(self, uuid, attachment_type=None, extension=None):
mime_type = attachment_type
extension = extension if extension else "attach"
Expand Down
25 changes: 25 additions & 0 deletions allure-python-commons/src/allure_commons/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

from traceback import format_exception_only

PROPERTY_KEY_SPECIAL_CHARACTERS = ("=", ":", " ", "\t", "#", "!")


def md5(*args):
m = hashlib.md5()
Expand Down Expand Up @@ -272,6 +274,29 @@ def format_traceback(exc_traceback):
return "".join(traceback.format_tb(exc_traceback)) if exc_traceback else None


def format_properties(properties):
"""
>>> format_properties({"browser": "chrome", "os name": "Windows 11"})
'browser=chrome\\nos\\\\ name=Windows 11\\n'

"""

return "".join(
f"{escape_property(name, is_key=True)}={escape_property(value)}\n"
for name, value in properties.items()
)


def escape_property(value, is_key=False):
value = str(value).replace("\\", "\\\\").replace("\n", "\\n").replace("\r", "\\r")
if is_key:
for character in PROPERTY_KEY_SPECIAL_CHARACTERS:
value = value.replace(character, f"\\{character}")
elif value.startswith((" ", "\t")):
value = f"\\{value}"
return value


def format_exception(etype, value):
"""
>>> import sys
Expand Down
3 changes: 2 additions & 1 deletion allure-robotframework/src/library/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .allure_library import attach, attach_file, global_attach, global_attach_file, global_error
from .allure_library import environment

__all__ = ["attach", "attach_file", "global_attach", "global_attach_file", "global_error"]
__all__ = ["attach", "attach_file", "global_attach", "global_attach_file", "global_error", "environment"]
6 changes: 5 additions & 1 deletion allure-robotframework/src/library/allure_library.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import allure


__all__ = ["attach", "attach_file", "global_attach", "global_attach_file", "global_error"]
__all__ = ["attach", "attach_file", "global_attach", "global_attach_file", "global_error", "environment"]


def _attachment_type(name):
Expand Down Expand Up @@ -29,3 +29,7 @@ def global_attach_file(source, name=None, attachment_type=None, extension=None):

def global_error(message, trace=None):
allure.global_error(message, trace=trace)


def environment(**kwargs):
allure.environment(**kwargs)
4 changes: 4 additions & 0 deletions allure-robotframework/src/listener/allure_listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,10 @@ def global_attach_file(self, source, name, attachment_type, extension):
def global_error(self, message, trace):
self.lifecycle.global_error(message=message, trace=trace)

@allure_commons.hookimpl
def add_environment(self, env):
self.lifecycle.environment(env)

@allure_commons.hookimpl
def start_step(self, uuid, title, params):
with self.lifecycle.start_step() as step:
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import textwrap
from tests.allure_behave.behave_runner import AllureBehaveRunner as Runner
from hamcrest import assert_that, has_entries


def test_environment_from_hooks(behave_runner: Runner):
behave_runner.run_behave(
feature_literals=[
"""
Feature: Environment
Scenario: Environment from hooks
Given noop
"""
],
step_literals=["given('noop')(lambda c: None)"],
environment_literal=textwrap.dedent(
"""
import allure


def before_all(context):
allure.environment(browser="chrome", stand="staging")


def after_all(context):
allure.environment({"os.name": "Windows 11"}, browser="firefox")
"""
),
)

assert_that(
behave_runner.allure_results.environment,
has_entries({
"browser": "firefox",
"stand": "staging",
"os.name": "Windows 11",
}),
)
Empty file.
Loading
Loading