Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ab14314

Browse files
authored
feat: add titlePath to results generated by all allure-pytest integrations (allure-framework#870)
1 parent 0f7da56 commit ab14314

File tree

28 files changed

+622
-53
lines changed

28 files changed

+622
-53
lines changed

allure-behave/src/listener.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from allure_behave.utils import scenario_links
2121
from allure_behave.utils import scenario_labels
2222
from allure_behave.utils import get_fullname
23+
from allure_behave.utils import get_title_path
2324
from allure_behave.utils import TEST_PLAN_SKIP_REASON
2425
from allure_behave.utils import get_hook_name
2526

@@ -77,6 +78,7 @@ def start_scenario(self, scenario):
7778
test_case = TestResult(uuid=self.current_scenario_uuid, start=now())
7879
test_case.name = scenario_name(scenario)
7980
test_case.fullName = get_fullname(scenario)
81+
test_case.titlePath = get_title_path(scenario)
8082
test_case.historyId = scenario_history_id(scenario)
8183
test_case.description = '\n'.join(scenario.description)
8284
test_case.parameters = scenario_parameters(scenario)

allure-behave/src/utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import csv
22
import io
33
from enum import Enum
4+
from pathlib import Path
45
from behave.runner_util import make_undefined_step_snippet
56
from allure_commons.types import Severity, LabelType
67
from allure_commons.model2 import Status, Parameter
@@ -97,6 +98,29 @@ def get_fullname(scenario):
9798
return f"{scenario.feature.name}: {name}"
9899

99100

101+
def get_title_path(scenario):
102+
path_parts = []
103+
feature_part = scenario.feature.name
104+
105+
# filename is set to "<string>" if the feature comes from a string literal
106+
if scenario.filename and scenario.filename != "<string>":
107+
path = Path(scenario.filename)
108+
109+
# remove the filename because it's redundant: a feature file can only have one feature defined
110+
path_parts = path.parts[:-1]
111+
112+
if not feature_part:
113+
# if no feature name is defined, fallback to the filename
114+
feature_part = path.name
115+
116+
if not feature_part:
117+
# Neither feature name nor filename is defined, use the "Feature" keyword
118+
feature_part = scenario.feature.keyword
119+
120+
# reminder: scenario name should not be included in titlePath because it is already part of the test case title
121+
return [*path_parts, feature_part]
122+
123+
100124
def get_hook_name(name, parameters):
101125
tag = None
102126
if name in ["before_tag", "after_tag"]:

allure-nose2/src/plugin.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515

1616
from .utils import timestamp_millis, status_details, update_attrs, labels, name, fullname, params
17+
from .utils import get_title_path
1718
import allure_commons
1819

1920

@@ -90,6 +91,7 @@ def startTest(self, event):
9091
test_result.fullName = fullname(event)
9192
test_result.testCaseId = md5(test_result.fullName)
9293
test_result.historyId = md5(event.test.id())
94+
test_result.titlePath = get_title_path(event)
9395
test_result.labels.extend(labels(event.test))
9496
test_result.labels.append(Label(name=LabelType.HOST, value=self._host))
9597
test_result.labels.append(Label(name=LabelType.THREAD, value=self._thread))

allure-nose2/src/utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ def fullname(event):
8181
return test_id.split(":")[0]
8282

8383

84+
def get_title_path(event):
85+
test_id = event.test.id()
86+
return test_id.split(":", 1)[0].rsplit(".")[:-1]
87+
88+
8489
def params(event):
8590
def _params(names, values):
8691
return [Parameter(name=name, value=represent(value)) for name, value in zip(names, values)]

allure-pytest-bdd/src/pytest_bdd_listener.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from .utils import get_allure_links
2424
from .utils import convert_params
2525
from .utils import get_full_name
26+
from .utils import get_title_path
2627
from .utils import get_outline_params
2728
from .utils import get_pytest_params
2829
from .utils import get_pytest_report_status
@@ -59,6 +60,7 @@ def pytest_bdd_before_scenario(self, request, feature, scenario):
5960
full_name = get_full_name(feature, scenario)
6061
with self.lifecycle.schedule_test_case(uuid=uuid) as test_result:
6162
test_result.fullName = full_name
63+
test_result.titlePath = get_title_path(request, feature)
6264
test_result.name = get_test_name(item, scenario, params)
6365
test_result.description = get_allure_description(item, feature, scenario)
6466
test_result.descriptionHtml = get_allure_description_html(item)

allure-pytest-bdd/src/utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44
from urllib.parse import urlparse
55
from uuid import UUID
6+
from pathlib import Path
67

78
import pytest
89

@@ -171,6 +172,16 @@ def get_full_name(feature, scenario):
171172
return f"{feature_path}:{scenario.name}"
172173

173174

175+
def get_rootdir(request):
176+
config = request.config
177+
return getattr(config, "rootpath", None) or Path(config.rootdir)
178+
179+
180+
def get_title_path(request, feature):
181+
parts = Path(feature.filename).relative_to(get_rootdir(request)).parts
182+
return [*parts[:-1], feature.name or parts[-1]]
183+
184+
174185
def get_uuid(*args):
175186
return str(UUID(md5(*args)))
176187

allure-pytest/src/listener.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from allure_pytest.utils import allure_description, allure_description_html
2020
from allure_pytest.utils import allure_labels, allure_links, pytest_markers
2121
from allure_pytest.utils import allure_full_name, allure_package, allure_name
22+
from allure_pytest.utils import allure_title_path
2223
from allure_pytest.utils import allure_suite_labels
2324
from allure_pytest.utils import get_status, get_status_details
2425
from allure_pytest.utils import get_outcome_status, get_outcome_status_details
@@ -109,6 +110,7 @@ def pytest_runtest_setup(self, item):
109110
test_result.name = allure_name(item, params, param_id)
110111
full_name = allure_full_name(item)
111112
test_result.fullName = full_name
113+
test_result.titlePath = [*allure_title_path(item)]
112114
test_result.testCaseId = md5(full_name)
113115
test_result.description = allure_description(item)
114116
test_result.descriptionHtml = allure_description_html(item)

allure-pytest/src/stash.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import pytest
2+
from functools import wraps
3+
4+
HAS_STASH = hasattr(pytest, 'StashKey')
5+
6+
7+
def create_stashkey_safe():
8+
"""
9+
If pytest stash is available, returns a new stash key.
10+
Otherwise, returns `None`.
11+
"""
12+
13+
return pytest.StashKey() if HAS_STASH else None
14+
15+
16+
def stash_get_safe(item, key):
17+
"""
18+
If pytest stash is available and contains the key, retrieves the associated value.
19+
Otherwise, returns `None`.
20+
"""
21+
22+
if HAS_STASH and key in item.stash:
23+
return item.stash[key]
24+
25+
26+
def stash_set_safe(item: pytest.Item, key, value):
27+
"""
28+
If pytest stash is available, associates the value with the key in the stash.
29+
Otherwise, does nothing.
30+
"""
31+
32+
if HAS_STASH:
33+
item.stash[key] = value
34+
35+
36+
def stashed(arg=None):
37+
"""
38+
Cashes the result of the decorated function in the pytest item stash.
39+
The first argument of the function must be a pytest item.
40+
41+
In pytest<7.0 the stash is not available, so the decorator does nothing.
42+
"""
43+
44+
key = create_stashkey_safe() if arg is None or callable(arg) else arg
45+
46+
def decorator(func):
47+
if not HAS_STASH:
48+
return func
49+
50+
@wraps(func)
51+
def wrapper(item, *args, **kwargs):
52+
if key in item.stash:
53+
return item.stash[key]
54+
55+
value = func(item, *args, **kwargs)
56+
item.stash[key] = value
57+
return value
58+
59+
return wrapper
60+
61+
return decorator(arg) if callable(arg) else decorator

allure-pytest/src/utils.py

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import pytest
2-
from itertools import chain, islice, repeat
2+
from itertools import repeat
33
from allure_commons.utils import 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
77
from allure_commons.types import LabelType
8-
8+
from allure_pytest.stash import stashed
99

1010
ALLURE_DESCRIPTION_MARK = 'allure_description'
1111
ALLURE_DESCRIPTION_HTML_MARK = 'allure_description_html'
@@ -30,6 +30,24 @@
3030
}
3131

3232

33+
class ParsedPytestNodeId:
34+
def __init__(self, nodeid):
35+
filepath, *class_names, function_segment = ensure_len(nodeid.split("::"), 2)
36+
self.filepath = filepath
37+
self.path_segments = filepath.split('/')
38+
*parent_dirs, filename = ensure_len(self.path_segments, 1)
39+
self.parent_package = '.'.join(parent_dirs)
40+
self.module = filename.rsplit(".", 1)[0]
41+
self.package = '.'.join(filter(None, [self.parent_package, self.module]))
42+
self.class_names = class_names
43+
self.test_function = function_segment.split("[", 1)[0]
44+
45+
46+
@stashed
47+
def parse_nodeid(item):
48+
return ParsedPytestNodeId(item.nodeid)
49+
50+
3351
def get_marker_value(item, keyword):
3452
marker = item.get_closest_marker(keyword)
3553
return marker.args[0] if marker and marker.args else None
@@ -101,9 +119,7 @@ def should_convert_mark_to_tag(mark):
101119

102120

103121
def allure_package(item):
104-
parts = item.nodeid.split('::')
105-
path = parts[0].rsplit('.', 1)[0]
106-
return path.replace('/', '.')
122+
return parse_nodeid(item).package
107123

108124

109125
def allure_name(item, parameters, param_id=None):
@@ -122,37 +138,41 @@ def allure_name(item, parameters, param_id=None):
122138

123139

124140
def allure_full_name(item: pytest.Item):
125-
package = allure_package(item)
126-
class_names = item.nodeid.split("::")[1:-1]
127-
class_part = ("." + ".".join(class_names)) if class_names else ""
128-
test = item.originalname if isinstance(item, pytest.Function) else item.name.split("[")[0]
129-
full_name = f'{package}{class_part}#{test}'
141+
nodeid = parse_nodeid(item)
142+
class_part = ("." + ".".join(nodeid.class_names)) if nodeid.class_names else ""
143+
test = item.originalname if isinstance(item, pytest.Function) else nodeid.test_function
144+
full_name = f"{nodeid.package}{class_part}#{test}"
130145
return full_name
131146

132147

148+
def allure_title_path(item):
149+
nodeid = parse_nodeid(item)
150+
return list(
151+
filter(None, [*nodeid.path_segments, *nodeid.class_names]),
152+
)
153+
154+
133155
def ensure_len(value, min_length, fill_value=None):
134156
yield from value
135157
yield from repeat(fill_value, min_length - len(value))
136158

137159

138160
def allure_suite_labels(item):
139-
head, *class_names, _ = ensure_len(item.nodeid.split("::"), 2)
140-
file_name, path = islice(chain(reversed(head.rsplit('/', 1)), [None]), 2)
141-
module = file_name.split('.')[0]
142-
package = path.replace('/', '.') if path else None
143-
pairs = dict(
144-
zip(
145-
[LabelType.PARENT_SUITE, LabelType.SUITE, LabelType.SUB_SUITE],
146-
[package, module, " > ".join(class_names)],
147-
),
148-
)
149-
labels = dict(allure_labels(item))
150-
default_suite_labels = []
151-
for label, value in pairs.items():
152-
if label not in labels.keys() and value:
153-
default_suite_labels.append((label, value))
161+
nodeid = parse_nodeid(item)
162+
163+
default_suite_labels = {
164+
LabelType.PARENT_SUITE: nodeid.parent_package,
165+
LabelType.SUITE: nodeid.module,
166+
LabelType.SUB_SUITE: " > ".join(nodeid.class_names),
167+
}
168+
169+
existing_labels = dict(allure_labels(item))
170+
resolved_default_suite_labels = []
171+
for label, value in default_suite_labels.items():
172+
if label not in existing_labels and value:
173+
resolved_default_suite_labels.append((label, value))
154174

155-
return default_suite_labels
175+
return resolved_default_suite_labels
156176

157177

158178
def get_outcome_status(outcome):

allure-python-commons-test/src/result.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ def has_title(title):
7474
return has_entry('name', title)
7575

7676

77+
def has_title_path(*matchers):
78+
return has_entry(
79+
"titlePath",
80+
contains_exactly(*matchers),
81+
)
82+
83+
7784
def has_description(*matchers):
7885
return has_entry('description', all_of(*matchers))
7986

0 commit comments

Comments
 (0)