pytest-given 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytest_given/__init__.py +32 -0
- pytest_given/capture/__init__.py +48 -0
- pytest_given/capture/collector.py +369 -0
- pytest_given/capture/decorators.py +502 -0
- pytest_given/capture/file_glossary.py +106 -0
- pytest_given/capture/glossary.py +258 -0
- pytest_given/capture/kind_inference.py +102 -0
- pytest_given/capture/markdown_glossary.py +147 -0
- pytest_given/capture/source.py +208 -0
- pytest_given/capture/story.py +303 -0
- pytest_given/capture/template.py +180 -0
- pytest_given/capture/traceback.py +157 -0
- pytest_given/cli.py +126 -0
- pytest_given/lint/__init__.py +47 -0
- pytest_given/lint/ast_rules.py +395 -0
- pytest_given/lint/base.py +77 -0
- pytest_given/lint/config.py +137 -0
- pytest_given/lint/runtime_rules.py +212 -0
- pytest_given/model/__init__.py +84 -0
- pytest_given/model/errors.py +2 -0
- pytest_given/model/ids.py +32 -0
- pytest_given/model/schema.py +326 -0
- pytest_given/model/serde.py +263 -0
- pytest_given/plugin.py +958 -0
- pytest_given/py.typed +0 -0
- pytest_given/report/__init__.py +10 -0
- pytest_given/report/aggregations.py +382 -0
- pytest_given/report/cli.py +68 -0
- pytest_given/report/coverage.py +175 -0
- pytest_given/report/html_renderer.py +370 -0
- pytest_given/report/inline_markdown.py +35 -0
- pytest_given/report/md_renderer.py +164 -0
- pytest_given/report/source_link.py +212 -0
- pytest_given/report/templates/alpine.min.js +6 -0
- pytest_given/report/templates/app.js +438 -0
- pytest_given/report/templates/report.html.j2 +606 -0
- pytest_given/report/templates/styles.css +1013 -0
- pytest_given/skills_data/pytest-given-authoring/SKILL.md +43 -0
- pytest_given/skills_data/pytest-given-authoring/references/api.md +62 -0
- pytest_given/skills_data/pytest-given-authoring/references/domain-storytelling.md +36 -0
- pytest_given/skills_data/pytest-given-authoring/references/glossaries.md +68 -0
- pytest_given/skills_data/pytest-given-authoring/references/scenarios.md +29 -0
- pytest_given/skills_data/pytest-given-authoring/references/stories.md +53 -0
- pytest_given/skills_data/pytest-given-navigating/SKILL.md +40 -0
- pytest_given/skills_data/pytest-given-navigating/references/report-json.md +69 -0
- pytest_given/skills_data/pytest-given-reviewing/SKILL.md +47 -0
- pytest_given-0.1.0.dist-info/METADATA +510 -0
- pytest_given-0.1.0.dist-info/RECORD +52 -0
- pytest_given-0.1.0.dist-info/WHEEL +4 -0
- pytest_given-0.1.0.dist-info/entry_points.txt +5 -0
- pytest_given-0.1.0.dist-info/licenses/LICENSE.md +21 -0
- pytest_given-0.1.0.dist-info/licenses/THIRD-PARTY-LICENSES +26 -0
pytest_given/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""pytest-given: Generate interactive HTML reports from GWT tests."""
|
|
2
|
+
|
|
3
|
+
from .capture import (
|
|
4
|
+
FileGlossary,
|
|
5
|
+
Template,
|
|
6
|
+
activity,
|
|
7
|
+
attach,
|
|
8
|
+
given,
|
|
9
|
+
path,
|
|
10
|
+
scenario,
|
|
11
|
+
story,
|
|
12
|
+
then,
|
|
13
|
+
when,
|
|
14
|
+
when_then,
|
|
15
|
+
)
|
|
16
|
+
from .model import Glossary, PytestGivenError
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
'FileGlossary',
|
|
20
|
+
'Glossary',
|
|
21
|
+
'PytestGivenError',
|
|
22
|
+
'Template',
|
|
23
|
+
'activity',
|
|
24
|
+
'attach',
|
|
25
|
+
'given',
|
|
26
|
+
'path',
|
|
27
|
+
'scenario',
|
|
28
|
+
'story',
|
|
29
|
+
'then',
|
|
30
|
+
'when',
|
|
31
|
+
'when_then',
|
|
32
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from .collector import (
|
|
2
|
+
Collector,
|
|
3
|
+
FixtureInstanceKey,
|
|
4
|
+
get_active_collector,
|
|
5
|
+
set_active_collector,
|
|
6
|
+
)
|
|
7
|
+
from .decorators import (
|
|
8
|
+
ScenarioMarked,
|
|
9
|
+
StepDecorated,
|
|
10
|
+
attach,
|
|
11
|
+
given,
|
|
12
|
+
scenario,
|
|
13
|
+
then,
|
|
14
|
+
when,
|
|
15
|
+
when_then,
|
|
16
|
+
)
|
|
17
|
+
from .file_glossary import FileGlossary
|
|
18
|
+
from .glossary import DeferredTermHandle, DeferredTermInstance
|
|
19
|
+
from .story import activity, path, story
|
|
20
|
+
from .template import Template, narration_from
|
|
21
|
+
from .traceback import filter_internal_frames, parse_short_repr
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
'Collector',
|
|
25
|
+
'DeferredTermHandle',
|
|
26
|
+
'DeferredTermInstance',
|
|
27
|
+
'FileGlossary',
|
|
28
|
+
'FixtureInstanceKey',
|
|
29
|
+
'ScenarioMarked',
|
|
30
|
+
'StepDecorated',
|
|
31
|
+
'Template',
|
|
32
|
+
'activity',
|
|
33
|
+
'attach',
|
|
34
|
+
'filter_internal_frames',
|
|
35
|
+
'get_active_collector',
|
|
36
|
+
'given',
|
|
37
|
+
'narration_from',
|
|
38
|
+
'parse_short_repr',
|
|
39
|
+
'path',
|
|
40
|
+
'scenario',
|
|
41
|
+
'set_active_collector',
|
|
42
|
+
'story',
|
|
43
|
+
'then',
|
|
44
|
+
'when',
|
|
45
|
+
'when_then',
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
from . import glossary as _glossary_module # noqa: F401 — registers methods on Glossary
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
from collections.abc import Iterator
|
|
3
|
+
from contextvars import ContextVar
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from ..model import (
|
|
7
|
+
ActivityId,
|
|
8
|
+
Attachment,
|
|
9
|
+
ContentType,
|
|
10
|
+
ErrorInfo,
|
|
11
|
+
FixtureRecording,
|
|
12
|
+
Narration,
|
|
13
|
+
NodeId,
|
|
14
|
+
ParamInfo,
|
|
15
|
+
Phase,
|
|
16
|
+
PytestGivenError,
|
|
17
|
+
RecordingState,
|
|
18
|
+
Scenario,
|
|
19
|
+
SourceLocation,
|
|
20
|
+
Step,
|
|
21
|
+
Story,
|
|
22
|
+
StoryId,
|
|
23
|
+
TracebackFrame,
|
|
24
|
+
)
|
|
25
|
+
from .template import Template, narration_from
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class StateToken:
|
|
30
|
+
"""Opaque token returned by enter_* methods; pass to exit_* to restore."""
|
|
31
|
+
|
|
32
|
+
previous_state: RecordingState
|
|
33
|
+
previous_recording: FixtureRecording | None
|
|
34
|
+
previous_fixture_descriptor: object | None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
type FixtureInstanceKey = tuple[object, object]
|
|
38
|
+
|
|
39
|
+
_collector_var: ContextVar[Collector | None] = ContextVar('collector', default=None)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def set_active_collector(collector: Collector | None) -> None:
|
|
43
|
+
"""Set the active collector for the current thread."""
|
|
44
|
+
_collector_var.set(collector)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_active_collector() -> Collector | None:
|
|
48
|
+
"""Get the active collector for the current thread, or None."""
|
|
49
|
+
return _collector_var.get()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Collector:
|
|
53
|
+
"""Collects step data during test execution.
|
|
54
|
+
|
|
55
|
+
Maintains a stack of active steps. Context managers push/pop steps.
|
|
56
|
+
Nested context managers create child steps.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self) -> None:
|
|
60
|
+
self._scenarios: list[Scenario] = []
|
|
61
|
+
self._current_scenario: Scenario | None = None
|
|
62
|
+
self._step_stack: list[Step] = []
|
|
63
|
+
self.start_times: dict[NodeId, float] = {}
|
|
64
|
+
self.param_info: ParamInfo = {}
|
|
65
|
+
self._state: RecordingState = 'idle'
|
|
66
|
+
self._active_recording: FixtureRecording | None = None
|
|
67
|
+
self._active_fixture_descriptor: object | None = None
|
|
68
|
+
self._recordings: dict[FixtureInstanceKey, FixtureRecording] = {}
|
|
69
|
+
self.inside_unannotated_test: bool = False
|
|
70
|
+
# Whether steps record their body's source anchor (narration lint
|
|
71
|
+
# only); off is the zero-cost default — no frame walking happens.
|
|
72
|
+
self.capture_step_source: bool = False
|
|
73
|
+
self.active_scenario_story: Story | None = None
|
|
74
|
+
self.active_scenario_activity_ids: tuple[ActivityId, ...] = ()
|
|
75
|
+
self._discovered_stories: dict[StoryId, Story] = {}
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def state(self) -> RecordingState:
|
|
79
|
+
return self._state
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def active_scenario_id(self) -> NodeId | None:
|
|
83
|
+
if self._current_scenario is None:
|
|
84
|
+
return None
|
|
85
|
+
return self._current_scenario.id
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def scenarios(self) -> list[Scenario]:
|
|
89
|
+
return self._scenarios
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def current_phase(self) -> Phase | None:
|
|
93
|
+
"""The phase of the innermost active step, or None."""
|
|
94
|
+
stack = self._target_stack()
|
|
95
|
+
if stack:
|
|
96
|
+
return stack[-1].phase
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def active_fixture_descriptor(self) -> object | None:
|
|
101
|
+
"""The descriptor pytest_fixture_setup pinned for the current fixture call.
|
|
102
|
+
|
|
103
|
+
Used by StepDescriptor's helper-decorator wrapper to recognise the case
|
|
104
|
+
where pytest is invoking it as a fixture body (in which case
|
|
105
|
+
pytest_fixture_setup has already created the recording's root step from
|
|
106
|
+
the descriptor's narration, and the wrapper must not push a duplicate).
|
|
107
|
+
"""
|
|
108
|
+
return self._active_fixture_descriptor
|
|
109
|
+
|
|
110
|
+
def start_scenario(
|
|
111
|
+
self,
|
|
112
|
+
scenario_id: NodeId,
|
|
113
|
+
name: str | Template | Narration,
|
|
114
|
+
module: str,
|
|
115
|
+
tags: list[str],
|
|
116
|
+
source: SourceLocation | None = None,
|
|
117
|
+
*,
|
|
118
|
+
story: Story | None = None,
|
|
119
|
+
activity_ids: tuple[ActivityId, ...] = (),
|
|
120
|
+
) -> None:
|
|
121
|
+
self._current_scenario = Scenario(
|
|
122
|
+
id=scenario_id,
|
|
123
|
+
narration=narration_from(name),
|
|
124
|
+
module=module,
|
|
125
|
+
tags=tags,
|
|
126
|
+
source=source,
|
|
127
|
+
story_id=story.id if story is not None else None,
|
|
128
|
+
activity_ids=activity_ids,
|
|
129
|
+
)
|
|
130
|
+
self._step_stack = []
|
|
131
|
+
self._state = 'test'
|
|
132
|
+
self.inside_unannotated_test = False
|
|
133
|
+
self.active_scenario_story = story
|
|
134
|
+
self.active_scenario_activity_ids = activity_ids
|
|
135
|
+
if story is not None:
|
|
136
|
+
self._discovered_stories[story.id] = story
|
|
137
|
+
|
|
138
|
+
def finish_scenario(
|
|
139
|
+
self,
|
|
140
|
+
status: str,
|
|
141
|
+
duration_ms: int,
|
|
142
|
+
skip_reason: str | None = None,
|
|
143
|
+
) -> Scenario:
|
|
144
|
+
assert self._current_scenario is not None
|
|
145
|
+
self._current_scenario.status = status
|
|
146
|
+
self._current_scenario.duration_ms = duration_ms
|
|
147
|
+
self._current_scenario.skip_reason = skip_reason
|
|
148
|
+
scenario = self._current_scenario
|
|
149
|
+
self._scenarios.append(scenario)
|
|
150
|
+
self._current_scenario = None
|
|
151
|
+
self._step_stack = []
|
|
152
|
+
self._state = 'idle'
|
|
153
|
+
self.active_scenario_story = None
|
|
154
|
+
self.active_scenario_activity_ids = ()
|
|
155
|
+
return scenario
|
|
156
|
+
|
|
157
|
+
def enter_fixture_setup(
|
|
158
|
+
self,
|
|
159
|
+
recording: FixtureRecording,
|
|
160
|
+
descriptor: object | None = None,
|
|
161
|
+
) -> StateToken:
|
|
162
|
+
token = StateToken(
|
|
163
|
+
previous_state=self._state,
|
|
164
|
+
previous_recording=self._active_recording,
|
|
165
|
+
previous_fixture_descriptor=self._active_fixture_descriptor,
|
|
166
|
+
)
|
|
167
|
+
self._state = 'fixture_setup'
|
|
168
|
+
self._active_recording = recording
|
|
169
|
+
self._active_fixture_descriptor = descriptor
|
|
170
|
+
return token
|
|
171
|
+
|
|
172
|
+
def exit_fixture_setup(self, token: StateToken) -> None:
|
|
173
|
+
self._state = token.previous_state
|
|
174
|
+
self._active_recording = token.previous_recording
|
|
175
|
+
self._active_fixture_descriptor = token.previous_fixture_descriptor
|
|
176
|
+
|
|
177
|
+
def enter_fixture_teardown(self) -> StateToken:
|
|
178
|
+
token = StateToken(
|
|
179
|
+
previous_state=self._state,
|
|
180
|
+
previous_recording=self._active_recording,
|
|
181
|
+
previous_fixture_descriptor=self._active_fixture_descriptor,
|
|
182
|
+
)
|
|
183
|
+
self._state = 'fixture_teardown'
|
|
184
|
+
return token
|
|
185
|
+
|
|
186
|
+
def exit_fixture_teardown(self, token: StateToken) -> None:
|
|
187
|
+
self._state = token.previous_state
|
|
188
|
+
self._active_recording = token.previous_recording
|
|
189
|
+
self._active_fixture_descriptor = token.previous_fixture_descriptor
|
|
190
|
+
|
|
191
|
+
def store_recording(
|
|
192
|
+
self, key: FixtureInstanceKey, recording: FixtureRecording
|
|
193
|
+
) -> None:
|
|
194
|
+
self._recordings[key] = recording
|
|
195
|
+
|
|
196
|
+
def get_recording(self, key: FixtureInstanceKey) -> FixtureRecording | None:
|
|
197
|
+
return self._recordings.get(key)
|
|
198
|
+
|
|
199
|
+
def recordings(self) -> Iterator[tuple[FixtureInstanceKey, FixtureRecording]]:
|
|
200
|
+
"""(key, recording) pairs in storage (setup) order."""
|
|
201
|
+
return iter(self._recordings.items())
|
|
202
|
+
|
|
203
|
+
def drop_recording(self, key: FixtureInstanceKey) -> None:
|
|
204
|
+
self._recordings.pop(key, None)
|
|
205
|
+
|
|
206
|
+
def graft_recording(
|
|
207
|
+
self,
|
|
208
|
+
recording: FixtureRecording,
|
|
209
|
+
*,
|
|
210
|
+
override_narration: Narration | None = None,
|
|
211
|
+
) -> None:
|
|
212
|
+
"""Deep-copy the recording's root into the active scenario's steps.
|
|
213
|
+
|
|
214
|
+
When *override_narration* is given (an Annotated label on the fixture
|
|
215
|
+
parameter), it replaces the grafted root's narration; the recorded
|
|
216
|
+
children and attachments are preserved.
|
|
217
|
+
"""
|
|
218
|
+
if self._current_scenario is None:
|
|
219
|
+
return
|
|
220
|
+
root = copy.deepcopy(recording.root)
|
|
221
|
+
if override_narration is not None:
|
|
222
|
+
root.narration = override_narration
|
|
223
|
+
self._current_scenario.steps.append(root)
|
|
224
|
+
|
|
225
|
+
def graft_leaf_given(self, narration: Narration) -> None:
|
|
226
|
+
"""Append a childless `given` step to the active scenario.
|
|
227
|
+
|
|
228
|
+
Used for Annotated labels on parametrize values and undecorated /
|
|
229
|
+
built-in fixtures — arrangements with no recorded body.
|
|
230
|
+
"""
|
|
231
|
+
if self._current_scenario is None:
|
|
232
|
+
return
|
|
233
|
+
self._current_scenario.steps.append(Step(phase='given', narration=narration))
|
|
234
|
+
|
|
235
|
+
def push_step(
|
|
236
|
+
self,
|
|
237
|
+
phase: Phase,
|
|
238
|
+
narration: Narration,
|
|
239
|
+
*,
|
|
240
|
+
activity_ids: tuple[ActivityId, ...] = (),
|
|
241
|
+
source: SourceLocation | None = None,
|
|
242
|
+
) -> Step:
|
|
243
|
+
if self._state == 'idle':
|
|
244
|
+
raise PytestGivenError(
|
|
245
|
+
f"Cannot record '{phase}: {narration.text}' — "
|
|
246
|
+
'no active scenario or fixture.'
|
|
247
|
+
)
|
|
248
|
+
if self._state == 'fixture_teardown':
|
|
249
|
+
raise PytestGivenError(
|
|
250
|
+
f"Cannot record '{phase}: {narration.text}' from fixture "
|
|
251
|
+
'teardown — teardown is technical, not narrative.'
|
|
252
|
+
)
|
|
253
|
+
stack = self._target_stack()
|
|
254
|
+
if stack and stack[-1].phase != phase:
|
|
255
|
+
raise PytestGivenError(
|
|
256
|
+
f"Cannot nest '{phase}' inside '{stack[-1].phase}'"
|
|
257
|
+
' — restructure your test or use a phase-neutral helper'
|
|
258
|
+
)
|
|
259
|
+
if activity_ids:
|
|
260
|
+
self._check_step_activity_scope(phase, activity_ids)
|
|
261
|
+
step = Step(
|
|
262
|
+
phase=phase, narration=narration, activity_ids=activity_ids, source=source
|
|
263
|
+
)
|
|
264
|
+
if stack:
|
|
265
|
+
stack[-1].children.append(step)
|
|
266
|
+
elif self._state == 'test' and self._current_scenario is not None:
|
|
267
|
+
self._current_scenario.steps.append(step)
|
|
268
|
+
stack.append(step)
|
|
269
|
+
return step
|
|
270
|
+
|
|
271
|
+
def _check_step_activity_scope(
|
|
272
|
+
self,
|
|
273
|
+
phase: Phase,
|
|
274
|
+
activity_ids: tuple[ActivityId, ...],
|
|
275
|
+
) -> None:
|
|
276
|
+
"""Validate step activity_ids against the active scenario's story scope.
|
|
277
|
+
|
|
278
|
+
Lives on Collector (not on StepDescriptor) so every push_step entry
|
|
279
|
+
point — context manager, helper wrapper, future fixture grafting —
|
|
280
|
+
gets the check by construction.
|
|
281
|
+
"""
|
|
282
|
+
story = self.active_scenario_story
|
|
283
|
+
if story is None:
|
|
284
|
+
raise PytestGivenError(
|
|
285
|
+
f'step activity= requires a story on the scenario '
|
|
286
|
+
f'(phase={phase!r}, ids={list(activity_ids)}).'
|
|
287
|
+
)
|
|
288
|
+
scope = self.active_scenario_activity_ids
|
|
289
|
+
valid = scope if scope else tuple(a.id for a in story.activities)
|
|
290
|
+
valid_set = set(valid)
|
|
291
|
+
for aid in activity_ids:
|
|
292
|
+
if aid in valid_set:
|
|
293
|
+
continue
|
|
294
|
+
if scope:
|
|
295
|
+
raise PytestGivenError(
|
|
296
|
+
f'step activity={aid} outside scenario scope '
|
|
297
|
+
f'(scenario activities={sorted(scope)}).'
|
|
298
|
+
)
|
|
299
|
+
raise PytestGivenError(
|
|
300
|
+
f'step activity={aid} not in story {story.title!r} '
|
|
301
|
+
f'(valid: {sorted(valid_set)}).'
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
def pop_step(self) -> Step | None:
|
|
305
|
+
stack = self._target_stack()
|
|
306
|
+
if not stack:
|
|
307
|
+
return None
|
|
308
|
+
# When recording into a fixture, don't pop the root: it's the labeled
|
|
309
|
+
# parent that the test will graft children under.
|
|
310
|
+
if self._state == 'fixture_setup' and len(stack) == 1:
|
|
311
|
+
return None
|
|
312
|
+
return stack.pop()
|
|
313
|
+
|
|
314
|
+
def attach(
|
|
315
|
+
self,
|
|
316
|
+
label: str,
|
|
317
|
+
content: str,
|
|
318
|
+
*,
|
|
319
|
+
content_type: ContentType = 'text',
|
|
320
|
+
) -> None:
|
|
321
|
+
if self._state == 'idle':
|
|
322
|
+
raise PytestGivenError(
|
|
323
|
+
f"Cannot attach '{label}' — no active scenario or fixture."
|
|
324
|
+
)
|
|
325
|
+
if self._state == 'fixture_teardown':
|
|
326
|
+
raise PytestGivenError(
|
|
327
|
+
f"Cannot attach '{label}' from fixture teardown — "
|
|
328
|
+
'teardown is technical, not narrative.'
|
|
329
|
+
)
|
|
330
|
+
stack = self._target_stack()
|
|
331
|
+
if stack:
|
|
332
|
+
stack[-1].attachments.append(
|
|
333
|
+
Attachment(label=label, content=content, content_type=content_type)
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
def _target_stack(self) -> list[Step]:
|
|
337
|
+
"""Return the step stack that push/pop/attach should mutate, per state."""
|
|
338
|
+
if self._state == 'fixture_setup' and self._active_recording is not None:
|
|
339
|
+
return self._active_recording.stack
|
|
340
|
+
return self._step_stack
|
|
341
|
+
|
|
342
|
+
def fail_scenario(
|
|
343
|
+
self,
|
|
344
|
+
message: str,
|
|
345
|
+
frames: list[TracebackFrame] | None = None,
|
|
346
|
+
error_tail: str | None = None,
|
|
347
|
+
) -> None:
|
|
348
|
+
if self._current_scenario is not None:
|
|
349
|
+
self._current_scenario.status = 'failed'
|
|
350
|
+
self._current_scenario.error = ErrorInfo(
|
|
351
|
+
message=message,
|
|
352
|
+
frames=frames or [],
|
|
353
|
+
error_tail=error_tail,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
def fail_current_step(
|
|
357
|
+
self,
|
|
358
|
+
message: str,
|
|
359
|
+
frames: list[TracebackFrame] | None = None,
|
|
360
|
+
error_tail: str | None = None,
|
|
361
|
+
) -> None:
|
|
362
|
+
if self._step_stack:
|
|
363
|
+
step = self._step_stack[-1]
|
|
364
|
+
step.status = 'failed'
|
|
365
|
+
step.error = ErrorInfo(
|
|
366
|
+
message=message,
|
|
367
|
+
frames=frames or [],
|
|
368
|
+
error_tail=error_tail,
|
|
369
|
+
)
|