summaryrefslogtreecommitdiffstats
path: root/contrib/python/allure-pytest/allure_pytest/utils.py
blob: 56594a0950366594cd20cce51433e2043022be86 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
import pytest
from itertools import repeat
from allure_commons.utils import SafeFormatter, md5
from allure_commons.utils import format_exception, format_traceback
from allure_commons.model2 import Status
from allure_commons.model2 import StatusDetails
from allure_commons.types import LabelType
from allure_pytest.stash import stashed

ALLURE_DESCRIPTION_MARK = 'allure_description'
ALLURE_DESCRIPTION_HTML_MARK = 'allure_description_html'
ALLURE_LABEL_MARK = 'allure_label'
ALLURE_LINK_MARK = 'allure_link'
ALLURE_UNIQUE_LABELS = [
    LabelType.SEVERITY,
    LabelType.FRAMEWORK,
    LabelType.HOST,
    LabelType.SUITE,
    LabelType.PARENT_SUITE,
    LabelType.SUB_SUITE
]

MARK_NAMES_TO_IGNORE = {
    "usefixtures",
    "filterwarnings",
    "skip",
    "skipif",
    "xfail",
    "parametrize",
}


class ParsedPytestNodeId:
    def __init__(self, nodeid):
        filepath, *class_names, function_segment = ensure_len(nodeid.split("::"), 2)
        self.filepath = filepath
        self.path_segments = filepath.split('/')
        *parent_dirs, filename = ensure_len(self.path_segments, 1)
        self.parent_package = '.'.join(parent_dirs)
        self.module = filename.rsplit(".", 1)[0]
        self.package = '.'.join(filter(None, [self.parent_package, self.module]))
        self.class_names = class_names
        self.test_function = function_segment.split("[", 1)[0]


@stashed
def parse_nodeid(item):
    return ParsedPytestNodeId(item.nodeid)


def get_marker_value(item, keyword):
    marker = item.get_closest_marker(keyword)
    return marker.args[0] if marker and marker.args else None


def allure_title(item):
    return getattr(
        getattr(item, "obj", None),
        "__allure_display_name__",
        None
    )


def allure_description(item):
    description = get_marker_value(item, ALLURE_DESCRIPTION_MARK)
    if description:
        return description
    elif hasattr(item, 'function'):
        return item.function.__doc__


def allure_description_html(item):
    return get_marker_value(item, ALLURE_DESCRIPTION_HTML_MARK)


def allure_label(item, label):
    labels = []
    for mark in item.iter_markers(name=ALLURE_LABEL_MARK):
        if mark.kwargs.get("label_type") == label:
            labels.extend(mark.args)
    return labels


def allure_labels(item):
    unique_labels = dict()
    labels = set()
    for mark in item.iter_markers(name=ALLURE_LABEL_MARK):
        label_type = mark.kwargs["label_type"]
        if label_type in ALLURE_UNIQUE_LABELS:
            if label_type not in unique_labels.keys():
                unique_labels[label_type] = mark.args[0]
        else:
            for arg in mark.args:
                labels.add((label_type, arg))
    for k, v in unique_labels.items():
        labels.add((k, v))
    return labels


def allure_links(item):
    for mark in item.iter_markers(name=ALLURE_LINK_MARK):
        yield (mark.kwargs["link_type"], mark.args[0], mark.kwargs["name"])


def format_allure_link(config, url, link_type):
    pattern = dict(config.option.allure_link_pattern).get(link_type, '{}')
    return pattern.format(url)


def pytest_markers(item):
    for mark in item.iter_markers():
        if should_convert_mark_to_tag(mark):
            yield mark.name


def should_convert_mark_to_tag(mark):
    return mark.name not in MARK_NAMES_TO_IGNORE and \
        not mark.args and not mark.kwargs


def allure_package(item):
    return parse_nodeid(item).package


def allure_name(item, parameters, param_id=None):
    name = item.name
    title = allure_title(item)
    param_id_kwargs = {}
    if param_id:
        # if param_id is an ASCII string, it could have been encoded by pytest (_pytest.compat.ascii_escaped)
        if param_id.isascii():
            param_id = param_id.encode().decode("unicode-escape")
        param_id_kwargs["param_id"] = param_id
    return SafeFormatter().format(
        title,
        **{**param_id_kwargs, **parameters, **item.funcargs}
    ) if title else name


def allure_full_name(item: pytest.Item):
    nodeid = parse_nodeid(item)
    class_part = ("." + ".".join(nodeid.class_names)) if nodeid.class_names else ""
    test = item.originalname if isinstance(item, pytest.Function) else nodeid.test_function
    full_name = f"{nodeid.package}{class_part}#{test}"
    return full_name


def allure_title_path(item):
    nodeid = parse_nodeid(item)
    return list(
        filter(None, [*nodeid.path_segments, *nodeid.class_names]),
    )


def ensure_len(value, min_length, fill_value=None):
    yield from value
    yield from repeat(fill_value, min_length - len(value))


def allure_suite_labels(item):
    nodeid = parse_nodeid(item)

    default_suite_labels = {
        LabelType.PARENT_SUITE: nodeid.parent_package,
        LabelType.SUITE: nodeid.module,
        LabelType.SUB_SUITE: " > ".join(nodeid.class_names),
    }

    existing_labels = dict(allure_labels(item))
    resolved_default_suite_labels = []
    for label, value in default_suite_labels.items():
        if label not in existing_labels and value:
            resolved_default_suite_labels.append((label, value))

    return resolved_default_suite_labels


def get_outcome_status(outcome):
    _, exception, _ = outcome.excinfo or (None, None, None)
    return get_status(exception)


def get_outcome_status_details(outcome):
    exception_type, exception, exception_traceback = outcome.excinfo or (None, None, None)
    return get_status_details(exception_type, exception, exception_traceback)


def get_status(exception):
    if exception:
        if isinstance(exception, AssertionError) or isinstance(exception, pytest.fail.Exception):
            return Status.FAILED
        elif isinstance(exception, pytest.skip.Exception):
            return Status.SKIPPED
        return Status.BROKEN
    else:
        return Status.PASSED


def get_status_details(exception_type, exception, exception_traceback):
    message = format_exception(exception_type, exception)
    trace = format_traceback(exception_traceback)
    return StatusDetails(message=message, trace=trace) if message or trace else None


def get_pytest_report_status(pytest_report):
    pytest_statuses = ('failed', 'passed', 'skipped')
    statuses = (Status.FAILED, Status.PASSED, Status.SKIPPED)
    for pytest_status, status in zip(pytest_statuses, statuses):
        if getattr(pytest_report, pytest_status):
            return status


def get_history_id(full_name, parameters, original_values):
    return md5(
        full_name,
        *(original_values.get(p.name, p.value) for p in sorted(
            filter(
                lambda p: not p.excluded,
                parameters
            ),
            key=lambda p: p.name
        ))
    )