-
Notifications
You must be signed in to change notification settings - Fork 222
/
Copy pathreporting.py
208 lines (164 loc) · 6.13 KB
/
reporting.py
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
"""Reporting functionality.
Collection of the scenario execution statuses, timing and other information
that enriches the pytest test reporting.
"""
from __future__ import annotations
import time
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Any, Callable
from _pytest.fixtures import FixtureRequest
from _pytest.nodes import Item
from _pytest.reports import TestReport
from _pytest.runner import CallInfo
from .parser import Feature, Scenario, Step
class StepReport:
"""Step execution report."""
skipped = False
failed = False
stopped = None
def __init__(self, step: Step) -> None:
"""Step report constructor.
:param pytest_bdd.parser.Step step: Step.
"""
self.step = step
self.started = time.perf_counter()
def serialize(self) -> dict[str, Any]:
"""Serialize the step execution report.
:return: Serialized step execution report.
:rtype: dict
"""
return {
"name": self.step.name,
"type": self.step.type,
"keyword": self.step.keyword,
"line_number": self.step.line_number,
"skipped": self.skipped,
"failed": self.failed,
"duration": self.duration,
}
def finalize(self, failed: bool, skipped=False) -> None:
"""Stop collecting information and finalize the report.
:param bool failed: Whether the step execution is failed.
:param bool skipped: Indicates if the step execution is skipped.
"""
self.stopped = time.perf_counter()
self.skipped = skipped
self.failed = failed
@property
def duration(self) -> float:
"""Step execution duration.
:return: Step execution duration.
:rtype: float
"""
if self.stopped is None:
return 0
return self.stopped - self.started
class ScenarioReport:
"""Scenario execution report."""
def __init__(self, scenario: Scenario) -> None:
"""Scenario report constructor.
:param pytest_bdd.parser.Scenario scenario: Scenario.
:param node: pytest test node object
"""
self.scenario: Scenario = scenario
self.step_reports: list[StepReport] = []
@property
def current_step_report(self) -> StepReport:
"""Get current step report.
:return: Last or current step report.
:rtype: pytest_bdd.reporting.StepReport
"""
return self.step_reports[-1]
def add_step_report(self, step_report: StepReport) -> None:
"""Add new step report.
:param step_report: New current step report.
:type step_report: pytest_bdd.reporting.StepReport
"""
self.step_reports.append(step_report)
def serialize(self) -> dict[str, Any]:
"""Serialize scenario execution report in order to transfer reporting from nodes in the distributed mode.
:return: Serialized report.
:rtype: dict
"""
scenario = self.scenario
feature = scenario.feature
return {
"steps": [step_report.serialize() for step_report in self.step_reports],
"name": scenario.name,
"line_number": scenario.line_number,
"tags": sorted(scenario.tags),
"feature": {
"name": feature.name,
"filename": feature.filename,
"rel_filename": feature.rel_filename,
"line_number": feature.line_number,
"description": feature.description,
"tags": sorted(feature.tags),
},
}
def fail(self) -> None:
"""Stop collecting information and finalize the report as failed."""
self.current_step_report.finalize(failed=True)
remaining_steps = self.scenario.steps[len(self.step_reports) :]
# Fail the rest of the steps and make reports.
for step in remaining_steps:
report = StepReport(step=step)
report.finalize(failed=True)
self.add_step_report(report)
def skip(self):
"""Stop collecting information and finalize the report as skipped."""
self.current_step_report.finalize(failed=False, skipped=True)
remaining_steps = self.scenario.steps[len(self.step_reports) :]
# Skip the rest of the steps and make reports.
for step in remaining_steps:
report = StepReport(step=step)
report.finalize(failed=False, skipped=True)
self.add_step_report(report)
def runtest_makereport(item: Item, call: CallInfo, rep: TestReport) -> None:
"""Store item in the report object."""
try:
scenario_report: ScenarioReport = item.__scenario_report__
except AttributeError:
pass
else:
rep.scenario = scenario_report.serialize()
rep.item = {"name": item.name}
def before_scenario(request: FixtureRequest, feature: Feature, scenario: Scenario) -> None:
"""Create scenario report for the item."""
request.node.__scenario_report__ = ScenarioReport(scenario=scenario)
def step_skip(
request: FixtureRequest,
feature: Feature,
scenario: Scenario,
step: Step,
step_func: Callable,
step_func_args: dict,
exception: Exception,
) -> None:
"""Finalize the step report as skipped."""
request.node.__scenario_report__.skip()
def step_error(
request: FixtureRequest,
feature: Feature,
scenario: Scenario,
step: Step,
step_func: Callable,
step_func_args: dict,
exception: Exception,
) -> None:
"""Finalize the step report as failed."""
request.node.__scenario_report__.fail()
def before_step(request: FixtureRequest, feature: Feature, scenario: Scenario, step: Step, step_func: Callable) -> None:
"""Store step start time."""
request.node.__scenario_report__.add_step_report(StepReport(step=step))
def after_step(
request: FixtureRequest,
feature: Feature,
scenario: Scenario,
step: Step,
step_func: Callable,
step_func_args: dict,
) -> None:
"""Finalize the step report as successful."""
request.node.__scenario_report__.current_step_report.finalize(failed=False)