Skip to content

Commit 8e7b2ae

Browse files
authored
Fix historyId calculation by allure-pytest (fix #743, #744) (#745)
1 parent 14624e4 commit 8e7b2ae

File tree

8 files changed

+165
-11
lines changed

8 files changed

+165
-11
lines changed

allure-pytest/src/listener.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from allure_pytest.utils import get_outcome_status, get_outcome_status_details
2323
from allure_pytest.utils import get_pytest_report_status
2424
from allure_pytest.utils import format_allure_link
25+
from allure_pytest.utils import get_history_id
2526
from allure_commons.utils import md5
2627

2728

@@ -101,18 +102,19 @@ def pytest_runtest_setup(self, item):
101102
self._update_fixtures_children(item)
102103
uuid = self._cache.get(item.nodeid)
103104
test_result = self.allure_logger.get_test(uuid)
104-
params = item.callspec.params if hasattr(item, 'callspec') else {}
105+
params = self.__get_pytest_params(item)
105106
test_result.name = allure_name(item, params)
106107
full_name = allure_full_name(item)
107108
test_result.fullName = full_name
108-
test_result.historyId = md5(item.nodeid)
109109
test_result.testCaseId = md5(full_name)
110110
test_result.description = allure_description(item)
111111
test_result.descriptionHtml = allure_description_html(item)
112112
current_param_names = [param.name for param in test_result.parameters]
113-
test_result.parameters.extend(
114-
[Parameter(name=name, value=represent(value)) for name, value in params.items()
115-
if name not in current_param_names])
113+
test_result.parameters.extend([
114+
Parameter(name=name, value=represent(value))
115+
for name, value in params.items()
116+
if name not in current_param_names
117+
])
116118

117119
@pytest.hookimpl(hookwrapper=True)
118120
def pytest_runtest_call(self, item):
@@ -132,6 +134,11 @@ def pytest_runtest_teardown(self, item):
132134
yield
133135
uuid = self._cache.get(item.nodeid)
134136
test_result = self.allure_logger.get_test(uuid)
137+
test_result.historyId = get_history_id(
138+
test_result.fullName,
139+
test_result.parameters,
140+
original_values=self.__get_pytest_params(item)
141+
)
135142
test_result.labels.extend([Label(name=name, value=value) for name, value in allure_labels(item)])
136143
test_result.labels.extend([Label(name=LabelType.TAG, value=value) for value in pytest_markers(item)])
137144
self.__apply_default_suites(item, test_result)
@@ -287,8 +294,18 @@ def add_parameter(self, name, value, excluded, mode: ParameterMode):
287294
if existing_param:
288295
existing_param.value = represent(value)
289296
else:
290-
test_result.parameters.append(Parameter(name=name, value=represent(value),
291-
excluded=excluded or None, mode=mode.value if mode else None))
297+
test_result.parameters.append(
298+
Parameter(
299+
name=name,
300+
value=represent(value),
301+
excluded=excluded or None,
302+
mode=mode.value if mode else None
303+
)
304+
)
305+
306+
@staticmethod
307+
def __get_pytest_params(item):
308+
return item.callspec.params if hasattr(item, 'callspec') else {}
292309

293310
def __apply_default_suites(self, item, test_result):
294311
default_suites = allure_suite_labels(item)

allure-pytest/src/utils.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22
from itertools import chain, islice
3-
from allure_commons.utils import represent, SafeFormatter
3+
from allure_commons.utils import represent, SafeFormatter, md5
44
from allure_commons.utils import format_exception, format_traceback
55
from allure_commons.model2 import Status
66
from allure_commons.model2 import StatusDetails
@@ -176,3 +176,16 @@ def get_pytest_report_status(pytest_report):
176176
for pytest_status, status in zip(pytest_statuses, statuses):
177177
if getattr(pytest_report, pytest_status):
178178
return status
179+
180+
181+
def get_history_id(full_name, parameters, original_values):
182+
return md5(
183+
full_name,
184+
*(original_values.get(p.name, p.value) for p in sorted(
185+
filter(
186+
lambda p: not p.excluded,
187+
parameters
188+
),
189+
key=lambda p: p.name
190+
))
191+
)

allure-python-commons/src/utils.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@
1818
def md5(*args):
1919
m = hashlib.md5()
2020
for arg in args:
21-
part = arg.encode('utf-8')
22-
m.update(part)
21+
if not isinstance(arg, bytes):
22+
if not isinstance(arg, str):
23+
arg = repr(arg)
24+
arg = arg.encode('utf-8')
25+
m.update(arg)
2326
return m.hexdigest()
2427

2528

tests/allure_pytest/acceptance/history_id/history_id_test.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from hamcrest import assert_that
22
from tests.allure_pytest.pytest_runner import AllurePytestRunner
33

4+
import allure
45
from allure_commons_test.report import has_test_case
56
from allure_commons_test.result import has_history_id
67

@@ -40,3 +41,92 @@ def test_history_id_for_skipped(allure_pytest_runner: AllurePytestRunner):
4041
has_history_id()
4142
)
4243
)
44+
45+
46+
@allure.issue("743", name="Issue 743")
47+
def test_history_id_affected_by_allure_parameter(
48+
allure_pytest_runner: AllurePytestRunner
49+
):
50+
"""
51+
>>> import allure
52+
>>> from time import perf_counter
53+
54+
>>> def test_allure_parameter_with_changing_value():
55+
... allure.dynamic.parameter("time", perf_counter())
56+
"""
57+
58+
first_run = allure_pytest_runner.run_docstring()
59+
second_run = allure_pytest_runner.run_docstring()
60+
61+
assert __get_history_id(first_run) != __get_history_id(second_run)
62+
63+
64+
@allure.issue("743", name="Issue 743")
65+
def test_history_id_not_affected_by_excluded_parameter(
66+
allure_pytest_runner: AllurePytestRunner
67+
):
68+
"""
69+
>>> import allure
70+
>>> from time import perf_counter
71+
72+
>>> def test_excluded_allure_parameter():
73+
... allure.dynamic.parameter("time", perf_counter(), excluded=True)
74+
"""
75+
76+
first_run = allure_pytest_runner.run_docstring()
77+
second_run = allure_pytest_runner.run_docstring()
78+
79+
assert __get_history_id(first_run) == __get_history_id(second_run)
80+
81+
82+
@allure.issue("744", name="Issue 744")
83+
def test_history_id_not_affected_by_pytest_ids(
84+
allure_pytest_runner: AllurePytestRunner
85+
):
86+
# We're using the trick with the same parameter here because it's not easy
87+
# to run pytester multiple times with different code due to caching of
88+
# python modules. In reality this change happens between runs
89+
run_result = allure_pytest_runner.run_pytest(
90+
"""
91+
import pytest
92+
93+
@pytest.mark.parametrize("v", [
94+
pytest.param(1),
95+
pytest.param(1, id="a")
96+
])
97+
def test_two_allure_parameters(v):
98+
pass
99+
"""
100+
)
101+
102+
assert __get_history_id(run_result, 0) == __get_history_id(run_result, 1)
103+
104+
105+
@allure.issue("743", name="Issue 743")
106+
def test_different_byte_arrays_are_distinguishable(
107+
allure_pytest_runner: AllurePytestRunner
108+
):
109+
"""
110+
The 'allure_commons.utils.represent' function used to convert allure
111+
parameter values to strings makes all byte arrays indistinguishable.
112+
Some extra effort is required to properly calculate 'historyId' on tests
113+
that are parametrized with byte arrays.
114+
"""
115+
run_result = allure_pytest_runner.run_pytest(
116+
"""
117+
import pytest
118+
119+
@pytest.mark.parametrize("v", [
120+
pytest.param(b'a'),
121+
pytest.param(b'b')
122+
])
123+
def test_two_allure_parameters(v):
124+
pass
125+
"""
126+
)
127+
128+
assert __get_history_id(run_result, 0) != __get_history_id(run_result, 1)
129+
130+
131+
def __get_history_id(run, index=0):
132+
return run.test_cases[index]["historyId"]

tests/allure_pytest/defects/issue733_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import allure
12
from allure_pytest.utils import allure_title
23

34

5+
@allure.issue("733", name="Issue 733")
46
def test_no_allure_title_error_if_item_obj_missing():
57
item_with_no_obj_attr_stub = object()
68

tests/allure_pytest/unit/__init__.py

Whitespace-only changes.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from allure_pytest.utils import get_history_id
2+
from allure_commons.model2 import Parameter
3+
4+
5+
def test_no_dependency_on_parameters_order():
6+
assert get_history_id("my-full-name", [
7+
Parameter(name="a", value="1"),
8+
Parameter(name="b", value="2")
9+
], {}) == get_history_id("my-full-name", [
10+
Parameter(name="b", value="2"),
11+
Parameter(name="a", value="1")
12+
], {})
13+
14+
15+
def test_original_values_are_used():
16+
assert get_history_id("my-full-name", [
17+
Parameter(name="a", value="1")
18+
], {"a": "b"}) == get_history_id("my-full-name", [
19+
Parameter(name="a", value="b")
20+
], {})
21+
22+
23+
def test_excluded_values_are_ignored():
24+
assert get_history_id("my-full-name", [
25+
Parameter(name="a", value="1")
26+
], {}) == get_history_id("my-full-name", [
27+
Parameter(name="a", value="1"),
28+
Parameter(name="b", value="2", excluded=True)
29+
], {})

tests/e2e.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ def find_node_with_docstring(
148148

149149
node = request.node
150150
while node:
151-
docstring = node.obj.__doc__
151+
docstring = getattr(node, "obj", None).__doc__
152152
if docstring:
153153
break
154154
node = node.parent

0 commit comments

Comments
 (0)