schemathesis 3.15.4__py3-none-any.whl → 4.4.2__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.
- schemathesis/__init__.py +53 -25
- schemathesis/auths.py +507 -0
- schemathesis/checks.py +190 -25
- schemathesis/cli/__init__.py +27 -1219
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +133 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +602 -0
- schemathesis/cli/commands/run/context.py +228 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +45 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
- schemathesis/cli/commands/run/handlers/output.py +1750 -0
- schemathesis/cli/commands/run/loaders.py +118 -0
- schemathesis/cli/commands/run/validation.py +256 -0
- schemathesis/cli/constants.py +5 -0
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +203 -0
- schemathesis/cli/ext/options.py +81 -0
- schemathesis/config/__init__.py +202 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +101 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +163 -0
- schemathesis/config/_generation.py +157 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +335 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +253 -0
- schemathesis/config/_projects.py +543 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +89 -0
- schemathesis/config/schema.json +975 -0
- schemathesis/core/__init__.py +72 -0
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +100 -0
- schemathesis/core/deserialization.py +210 -0
- schemathesis/core/errors.py +588 -0
- schemathesis/core/failures.py +316 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +183 -0
- schemathesis/core/jsonschema/keywords.py +40 -0
- schemathesis/core/jsonschema/references.py +222 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/lazy_import.py +15 -0
- schemathesis/core/loaders.py +107 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/core/media_types.py +79 -0
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/core/parameters.py +45 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +34 -0
- schemathesis/core/result.py +27 -0
- schemathesis/core/schema_analysis.py +17 -0
- schemathesis/core/shell.py +203 -0
- schemathesis/core/transforms.py +144 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +73 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +152 -0
- schemathesis/engine/control.py +44 -0
- schemathesis/engine/core.py +201 -0
- schemathesis/engine/errors.py +446 -0
- schemathesis/engine/events.py +284 -0
- schemathesis/engine/observations.py +42 -0
- schemathesis/engine/phases/__init__.py +108 -0
- schemathesis/engine/phases/analysis.py +28 -0
- schemathesis/engine/phases/probes.py +172 -0
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +364 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +220 -0
- schemathesis/engine/phases/unit/_executor.py +459 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +254 -0
- schemathesis/errors.py +47 -0
- schemathesis/filters.py +395 -0
- schemathesis/generation/__init__.py +25 -0
- schemathesis/generation/case.py +478 -0
- schemathesis/generation/coverage.py +1528 -0
- schemathesis/generation/hypothesis/__init__.py +121 -0
- schemathesis/generation/hypothesis/builder.py +992 -0
- schemathesis/generation/hypothesis/examples.py +56 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +285 -0
- schemathesis/generation/meta.py +227 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +127 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +294 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +285 -0
- schemathesis/hooks.py +270 -91
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +467 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +315 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +341 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/pytest/plugin.py +357 -0
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +682 -257
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/nodes.py +26 -2
- schemathesis/specs/graphql/scalars.py +77 -12
- schemathesis/specs/graphql/schemas.py +367 -148
- schemathesis/specs/graphql/validation.py +33 -0
- schemathesis/specs/openapi/__init__.py +9 -1
- schemathesis/specs/openapi/_hypothesis.py +555 -318
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +729 -0
- schemathesis/specs/openapi/adapter/protocol.py +59 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +368 -0
- schemathesis/specs/openapi/adapter/security.py +144 -0
- schemathesis/specs/openapi/adapter/v2.py +30 -0
- schemathesis/specs/openapi/adapter/v3_0.py +30 -0
- schemathesis/specs/openapi/adapter/v3_1.py +30 -0
- schemathesis/specs/openapi/analysis.py +96 -0
- schemathesis/specs/openapi/checks.py +748 -82
- schemathesis/specs/openapi/converter.py +176 -37
- schemathesis/specs/openapi/definitions.py +599 -4
- schemathesis/specs/openapi/examples.py +581 -165
- schemathesis/specs/openapi/expressions/__init__.py +52 -5
- schemathesis/specs/openapi/expressions/extractors.py +25 -0
- schemathesis/specs/openapi/expressions/lexer.py +34 -31
- schemathesis/specs/openapi/expressions/nodes.py +97 -46
- schemathesis/specs/openapi/expressions/parser.py +35 -13
- schemathesis/specs/openapi/formats.py +122 -0
- schemathesis/specs/openapi/media_types.py +75 -0
- schemathesis/specs/openapi/negative/__init__.py +93 -73
- schemathesis/specs/openapi/negative/mutations.py +294 -103
- schemathesis/specs/openapi/negative/utils.py +0 -9
- schemathesis/specs/openapi/patterns.py +458 -0
- schemathesis/specs/openapi/references.py +60 -81
- schemathesis/specs/openapi/schemas.py +647 -666
- schemathesis/specs/openapi/serialization.py +53 -30
- schemathesis/specs/openapi/stateful/__init__.py +403 -68
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
- schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
- schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
- schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
- schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
- schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
- schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
- schemathesis/specs/openapi/stateful/inference.py +254 -0
- schemathesis/specs/openapi/stateful/links.py +219 -78
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/common.py +23 -0
- schemathesis/specs/openapi/types/v2.py +129 -0
- schemathesis/specs/openapi/types/v3.py +134 -0
- schemathesis/specs/openapi/utils.py +7 -6
- schemathesis/specs/openapi/warnings.py +75 -0
- schemathesis/transport/__init__.py +224 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +126 -0
- schemathesis/transport/requests.py +278 -0
- schemathesis/transport/serialization.py +329 -0
- schemathesis/transport/wsgi.py +175 -0
- schemathesis-4.4.2.dist-info/METADATA +213 -0
- schemathesis-4.4.2.dist-info/RECORD +192 -0
- {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
- schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
- {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
- schemathesis/_compat.py +0 -57
- schemathesis/_hypothesis.py +0 -123
- schemathesis/auth.py +0 -214
- schemathesis/cli/callbacks.py +0 -240
- schemathesis/cli/cassettes.py +0 -351
- schemathesis/cli/context.py +0 -38
- schemathesis/cli/debug.py +0 -21
- schemathesis/cli/handlers.py +0 -11
- schemathesis/cli/junitxml.py +0 -41
- schemathesis/cli/options.py +0 -70
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -521
- schemathesis/cli/output/short.py +0 -40
- schemathesis/constants.py +0 -88
- schemathesis/exceptions.py +0 -257
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -16
- schemathesis/extra/pytest_plugin.py +0 -251
- schemathesis/failures.py +0 -145
- schemathesis/fixups/__init__.py +0 -29
- schemathesis/fixups/fast_api.py +0 -30
- schemathesis/graphql.py +0 -5
- schemathesis/internal.py +0 -6
- schemathesis/lazy.py +0 -301
- schemathesis/models.py +0 -1113
- schemathesis/parameters.py +0 -91
- schemathesis/runner/__init__.py +0 -470
- schemathesis/runner/events.py +0 -242
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -791
- schemathesis/runner/impl/solo.py +0 -85
- schemathesis/runner/impl/threadpool.py +0 -367
- schemathesis/runner/serialization.py +0 -206
- schemathesis/serializers.py +0 -253
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -10
- schemathesis/service/client.py +0 -62
- schemathesis/service/constants.py +0 -25
- schemathesis/service/events.py +0 -39
- schemathesis/service/handler.py +0 -46
- schemathesis/service/hosts.py +0 -74
- schemathesis/service/metadata.py +0 -42
- schemathesis/service/models.py +0 -21
- schemathesis/service/serialization.py +0 -184
- schemathesis/service/worker.py +0 -39
- schemathesis/specs/graphql/loaders.py +0 -215
- schemathesis/specs/openapi/constants.py +0 -7
- schemathesis/specs/openapi/expressions/context.py +0 -12
- schemathesis/specs/openapi/expressions/pointers.py +0 -29
- schemathesis/specs/openapi/filters.py +0 -44
- schemathesis/specs/openapi/links.py +0 -303
- schemathesis/specs/openapi/loaders.py +0 -453
- schemathesis/specs/openapi/parameters.py +0 -430
- schemathesis/specs/openapi/security.py +0 -129
- schemathesis/specs/openapi/validation.py +0 -24
- schemathesis/stateful.py +0 -358
- schemathesis/targets.py +0 -32
- schemathesis/types.py +0 -38
- schemathesis/utils.py +0 -475
- schemathesis-3.15.4.dist-info/METADATA +0 -202
- schemathesis-3.15.4.dist-info/RECORD +0 -99
- schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import uuid
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING, Generator
|
|
7
|
+
|
|
8
|
+
from schemathesis.core.result import Result
|
|
9
|
+
from schemathesis.core.schema_analysis import SchemaWarning
|
|
10
|
+
from schemathesis.engine.errors import EngineErrorInfo
|
|
11
|
+
from schemathesis.engine.phases import Phase, PhaseName
|
|
12
|
+
from schemathesis.engine.recorder import ScenarioRecorder
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from schemathesis.engine import Status
|
|
16
|
+
from schemathesis.engine.phases.probes import ProbePayload
|
|
17
|
+
|
|
18
|
+
EventGenerator = Generator["EngineEvent", None, None]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class EngineEvent:
|
|
23
|
+
"""An event within the engine's lifecycle."""
|
|
24
|
+
|
|
25
|
+
id: uuid.UUID
|
|
26
|
+
timestamp: float
|
|
27
|
+
# Indicates whether this event is the last in the event stream
|
|
28
|
+
is_terminal = False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class EngineStarted(EngineEvent):
|
|
33
|
+
"""Start of an engine."""
|
|
34
|
+
|
|
35
|
+
__slots__ = ("id", "timestamp")
|
|
36
|
+
|
|
37
|
+
def __init__(self) -> None:
|
|
38
|
+
self.id = uuid.uuid4()
|
|
39
|
+
self.timestamp = time.time()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class PhaseEvent(EngineEvent):
|
|
44
|
+
"""Event associated with a specific execution phase."""
|
|
45
|
+
|
|
46
|
+
phase: Phase
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class StatefulPhasePayload:
|
|
51
|
+
inferred_links: int
|
|
52
|
+
|
|
53
|
+
__slots__ = ("inferred_links",)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class PhaseStarted(PhaseEvent):
|
|
58
|
+
"""Start of an execution phase."""
|
|
59
|
+
|
|
60
|
+
payload: StatefulPhasePayload | None
|
|
61
|
+
|
|
62
|
+
__slots__ = ("id", "timestamp", "phase", "payload")
|
|
63
|
+
|
|
64
|
+
def __init__(self, *, phase: Phase, payload: StatefulPhasePayload | None) -> None:
|
|
65
|
+
self.id = uuid.uuid4()
|
|
66
|
+
self.timestamp = time.time()
|
|
67
|
+
self.phase = phase
|
|
68
|
+
self.payload = payload
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class PhaseFinished(PhaseEvent):
|
|
73
|
+
"""End of an execution phase."""
|
|
74
|
+
|
|
75
|
+
status: Status
|
|
76
|
+
payload: Result[ProbePayload, Exception] | None
|
|
77
|
+
|
|
78
|
+
__slots__ = ("id", "timestamp", "phase", "status", "payload")
|
|
79
|
+
|
|
80
|
+
def __init__(self, *, phase: Phase, status: Status, payload: Result[ProbePayload, Exception] | None) -> None:
|
|
81
|
+
self.id = uuid.uuid4()
|
|
82
|
+
self.timestamp = time.time()
|
|
83
|
+
self.phase = phase
|
|
84
|
+
self.status = status
|
|
85
|
+
self.payload = payload
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class SchemaAnalysisWarnings(PhaseEvent):
|
|
90
|
+
"""Schema analysis discovered warnings."""
|
|
91
|
+
|
|
92
|
+
warnings: list[SchemaWarning]
|
|
93
|
+
|
|
94
|
+
__slots__ = ("id", "timestamp", "phase", "warnings")
|
|
95
|
+
|
|
96
|
+
def __init__(self, *, phase: Phase, warnings: list[SchemaWarning]) -> None:
|
|
97
|
+
self.id = uuid.uuid4()
|
|
98
|
+
self.timestamp = time.time()
|
|
99
|
+
self.phase = phase
|
|
100
|
+
self.warnings = warnings
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class TestEvent(EngineEvent):
|
|
105
|
+
phase: PhaseName
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class SuiteStarted(TestEvent):
|
|
110
|
+
"""Before executing a set of scenarios."""
|
|
111
|
+
|
|
112
|
+
__slots__ = ("id", "timestamp", "phase")
|
|
113
|
+
|
|
114
|
+
def __init__(self, *, phase: PhaseName) -> None:
|
|
115
|
+
self.id = uuid.uuid4()
|
|
116
|
+
self.timestamp = time.time()
|
|
117
|
+
self.phase = phase
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass
|
|
121
|
+
class SuiteFinished(TestEvent):
|
|
122
|
+
"""After executing a set of test scenarios."""
|
|
123
|
+
|
|
124
|
+
status: Status
|
|
125
|
+
|
|
126
|
+
__slots__ = ("id", "timestamp", "phase", "status")
|
|
127
|
+
|
|
128
|
+
def __init__(self, *, id: uuid.UUID, phase: PhaseName, status: Status) -> None:
|
|
129
|
+
self.id = id
|
|
130
|
+
self.timestamp = time.time()
|
|
131
|
+
self.phase = phase
|
|
132
|
+
self.status = status
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class ScenarioEvent(TestEvent):
|
|
137
|
+
suite_id: uuid.UUID
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass
|
|
141
|
+
class ScenarioStarted(ScenarioEvent):
|
|
142
|
+
"""Before executing a grouped set of test steps."""
|
|
143
|
+
|
|
144
|
+
__slots__ = ("id", "timestamp", "phase", "suite_id", "label")
|
|
145
|
+
|
|
146
|
+
def __init__(self, *, phase: PhaseName, suite_id: uuid.UUID, label: str | None) -> None:
|
|
147
|
+
self.id = uuid.uuid4()
|
|
148
|
+
self.timestamp = time.time()
|
|
149
|
+
self.phase = phase
|
|
150
|
+
self.suite_id = suite_id
|
|
151
|
+
self.label = label
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass
|
|
155
|
+
class ScenarioFinished(ScenarioEvent):
|
|
156
|
+
"""After executing a grouped set of test steps."""
|
|
157
|
+
|
|
158
|
+
status: Status
|
|
159
|
+
recorder: ScenarioRecorder
|
|
160
|
+
elapsed_time: float
|
|
161
|
+
skip_reason: str | None
|
|
162
|
+
# Whether this is a scenario that tries to reproduce a failure
|
|
163
|
+
is_final: bool
|
|
164
|
+
|
|
165
|
+
__slots__ = (
|
|
166
|
+
"id",
|
|
167
|
+
"timestamp",
|
|
168
|
+
"phase",
|
|
169
|
+
"suite_id",
|
|
170
|
+
"label",
|
|
171
|
+
"status",
|
|
172
|
+
"recorder",
|
|
173
|
+
"elapsed_time",
|
|
174
|
+
"skip_reason",
|
|
175
|
+
"is_final",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def __init__(
|
|
179
|
+
self,
|
|
180
|
+
*,
|
|
181
|
+
id: uuid.UUID,
|
|
182
|
+
phase: PhaseName,
|
|
183
|
+
suite_id: uuid.UUID,
|
|
184
|
+
label: str | None,
|
|
185
|
+
status: Status,
|
|
186
|
+
recorder: ScenarioRecorder,
|
|
187
|
+
elapsed_time: float,
|
|
188
|
+
skip_reason: str | None,
|
|
189
|
+
is_final: bool,
|
|
190
|
+
) -> None:
|
|
191
|
+
self.id = id
|
|
192
|
+
self.timestamp = time.time()
|
|
193
|
+
self.phase = phase
|
|
194
|
+
self.suite_id = suite_id
|
|
195
|
+
self.label = label
|
|
196
|
+
self.status = status
|
|
197
|
+
self.recorder = recorder
|
|
198
|
+
self.elapsed_time = elapsed_time
|
|
199
|
+
self.skip_reason = skip_reason
|
|
200
|
+
self.is_final = is_final
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@dataclass
|
|
204
|
+
class Interrupted(EngineEvent):
|
|
205
|
+
"""If execution was interrupted by Ctrl-C, or a received SIGTERM."""
|
|
206
|
+
|
|
207
|
+
phase: PhaseName | None
|
|
208
|
+
|
|
209
|
+
__slots__ = ("id", "timestamp", "phase")
|
|
210
|
+
|
|
211
|
+
def __init__(self, *, phase: PhaseName | None) -> None:
|
|
212
|
+
self.id = uuid.uuid4()
|
|
213
|
+
self.timestamp = time.time()
|
|
214
|
+
self.phase = phase
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@dataclass
|
|
218
|
+
class NonFatalError(EngineEvent):
|
|
219
|
+
"""Error that doesn't halt execution but should be reported."""
|
|
220
|
+
|
|
221
|
+
info: EngineErrorInfo
|
|
222
|
+
value: Exception
|
|
223
|
+
phase: PhaseName
|
|
224
|
+
label: str
|
|
225
|
+
related_to_operation: bool
|
|
226
|
+
|
|
227
|
+
__slots__ = ("id", "timestamp", "info", "value", "phase", "label", "related_to_operation")
|
|
228
|
+
|
|
229
|
+
def __init__(
|
|
230
|
+
self,
|
|
231
|
+
*,
|
|
232
|
+
error: Exception,
|
|
233
|
+
phase: PhaseName,
|
|
234
|
+
label: str,
|
|
235
|
+
related_to_operation: bool,
|
|
236
|
+
code_sample: str | None = None,
|
|
237
|
+
) -> None:
|
|
238
|
+
self.id = uuid.uuid4()
|
|
239
|
+
self.timestamp = time.time()
|
|
240
|
+
self.info = EngineErrorInfo(error=error, code_sample=code_sample)
|
|
241
|
+
self.value = error
|
|
242
|
+
self.phase = phase
|
|
243
|
+
self.label = label
|
|
244
|
+
self.related_to_operation = related_to_operation
|
|
245
|
+
|
|
246
|
+
def __eq__(self, other: object) -> bool:
|
|
247
|
+
assert isinstance(other, NonFatalError)
|
|
248
|
+
return self.label == other.label and type(self.value) is type(other.value)
|
|
249
|
+
|
|
250
|
+
def __hash__(self) -> int:
|
|
251
|
+
return hash((self.label, type(self.value)))
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@dataclass
|
|
255
|
+
class FatalError(EngineEvent):
|
|
256
|
+
"""Internal error in the engine."""
|
|
257
|
+
|
|
258
|
+
exception: Exception
|
|
259
|
+
is_terminal = True
|
|
260
|
+
|
|
261
|
+
__slots__ = ("id", "timestamp", "exception")
|
|
262
|
+
|
|
263
|
+
def __init__(self, *, exception: Exception) -> None:
|
|
264
|
+
self.id = uuid.uuid4()
|
|
265
|
+
self.timestamp = time.time()
|
|
266
|
+
self.exception = exception
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@dataclass
|
|
270
|
+
class EngineFinished(EngineEvent):
|
|
271
|
+
"""The final event of the run.
|
|
272
|
+
|
|
273
|
+
No more events after this point.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
is_terminal = True
|
|
277
|
+
running_time: float
|
|
278
|
+
|
|
279
|
+
__slots__ = ("id", "timestamp", "running_time")
|
|
280
|
+
|
|
281
|
+
def __init__(self, *, running_time: float) -> None:
|
|
282
|
+
self.id = uuid.uuid4()
|
|
283
|
+
self.timestamp = time.time()
|
|
284
|
+
self.running_time = running_time
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from schemathesis.engine.recorder import ScenarioRecorder
|
|
4
|
+
from schemathesis.schemas import APIOperation
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class LocationHeaderEntry:
|
|
9
|
+
"""Value of `Location` coming from API response with a given status code."""
|
|
10
|
+
|
|
11
|
+
status_code: int
|
|
12
|
+
value: str
|
|
13
|
+
|
|
14
|
+
__slots__ = ("status_code", "value")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Observations:
|
|
19
|
+
"""Repository for observations collected during test execution."""
|
|
20
|
+
|
|
21
|
+
location_headers: dict[APIOperation, list[LocationHeaderEntry]]
|
|
22
|
+
|
|
23
|
+
__slots__ = ("location_headers",)
|
|
24
|
+
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
self.location_headers = {}
|
|
27
|
+
|
|
28
|
+
def extract_observations_from(self, recorder: ScenarioRecorder) -> None:
|
|
29
|
+
"""Extract observations from completed test scenario."""
|
|
30
|
+
for id, interaction in recorder.interactions.items():
|
|
31
|
+
response = interaction.response
|
|
32
|
+
if response is not None:
|
|
33
|
+
location = response.headers.get("location")
|
|
34
|
+
if location:
|
|
35
|
+
# Group location headers by the operation that produced them
|
|
36
|
+
entries = self.location_headers.setdefault(recorder.cases[id].value.operation, [])
|
|
37
|
+
entries.append(
|
|
38
|
+
LocationHeaderEntry(
|
|
39
|
+
status_code=response.status_code,
|
|
40
|
+
value=location[0],
|
|
41
|
+
)
|
|
42
|
+
)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
import warnings
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from schemathesis.engine.context import EngineContext
|
|
10
|
+
from schemathesis.engine.events import EventGenerator
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PhaseName(str, enum.Enum):
|
|
14
|
+
"""Available execution phases."""
|
|
15
|
+
|
|
16
|
+
PROBING = "API probing"
|
|
17
|
+
SCHEMA_ANALYSIS = "Schema analysis"
|
|
18
|
+
EXAMPLES = "Examples"
|
|
19
|
+
COVERAGE = "Coverage"
|
|
20
|
+
FUZZING = "Fuzzing"
|
|
21
|
+
STATEFUL_TESTING = "Stateful"
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def defaults(cls) -> list[PhaseName]:
|
|
25
|
+
return [PhaseName.EXAMPLES, PhaseName.COVERAGE, PhaseName.FUZZING, PhaseName.STATEFUL_TESTING]
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def name(self) -> str:
|
|
29
|
+
return {
|
|
30
|
+
PhaseName.PROBING: "probing",
|
|
31
|
+
PhaseName.SCHEMA_ANALYSIS: "schema analysis",
|
|
32
|
+
PhaseName.EXAMPLES: "examples",
|
|
33
|
+
PhaseName.COVERAGE: "coverage",
|
|
34
|
+
PhaseName.FUZZING: "fuzzing",
|
|
35
|
+
PhaseName.STATEFUL_TESTING: "stateful",
|
|
36
|
+
}[self]
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_str(cls, value: str) -> PhaseName:
|
|
40
|
+
return {
|
|
41
|
+
"probing": cls.PROBING,
|
|
42
|
+
"schema analysis": cls.SCHEMA_ANALYSIS,
|
|
43
|
+
"examples": cls.EXAMPLES,
|
|
44
|
+
"coverage": cls.COVERAGE,
|
|
45
|
+
"fuzzing": cls.FUZZING,
|
|
46
|
+
"stateful": cls.STATEFUL_TESTING,
|
|
47
|
+
}[value.lower()]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class PhaseSkipReason(str, enum.Enum):
|
|
51
|
+
"""Reasons why a phase might not be executed."""
|
|
52
|
+
|
|
53
|
+
DISABLED = "disabled" # Explicitly disabled via config
|
|
54
|
+
NOT_SUPPORTED = "not supported" # Feature not supported by schema
|
|
55
|
+
NOT_APPLICABLE = "not applicable" # No relevant data (e.g., no links for stateful)
|
|
56
|
+
FAILURE_LIMIT_REACHED = "failure limit reached"
|
|
57
|
+
NOTHING_TO_TEST = "nothing to test"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class Phase:
|
|
62
|
+
"""A logically separate engine execution phase."""
|
|
63
|
+
|
|
64
|
+
name: PhaseName
|
|
65
|
+
is_supported: bool
|
|
66
|
+
is_enabled: bool
|
|
67
|
+
skip_reason: PhaseSkipReason | None
|
|
68
|
+
|
|
69
|
+
__slots__ = ("name", "is_supported", "is_enabled", "skip_reason")
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self, name: PhaseName, is_supported: bool, is_enabled: bool = True, skip_reason: PhaseSkipReason | None = None
|
|
73
|
+
) -> None:
|
|
74
|
+
self.name = name
|
|
75
|
+
self.is_supported = is_supported
|
|
76
|
+
self.is_enabled = is_enabled
|
|
77
|
+
self.skip_reason = skip_reason
|
|
78
|
+
|
|
79
|
+
def should_execute(self, ctx: EngineContext) -> bool:
|
|
80
|
+
"""Determine if phase should run based on context & configuration."""
|
|
81
|
+
return self.is_enabled and not ctx.has_to_stop
|
|
82
|
+
|
|
83
|
+
def enable(self) -> None:
|
|
84
|
+
"""Enable this test phase."""
|
|
85
|
+
self.is_enabled = True
|
|
86
|
+
self.skip_reason = None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def execute(ctx: EngineContext, phase: Phase) -> EventGenerator:
|
|
90
|
+
from urllib3.exceptions import InsecureRequestWarning
|
|
91
|
+
|
|
92
|
+
from . import analysis, probes, stateful, unit
|
|
93
|
+
|
|
94
|
+
with warnings.catch_warnings():
|
|
95
|
+
warnings.simplefilter("ignore", InsecureRequestWarning)
|
|
96
|
+
|
|
97
|
+
if phase.name == PhaseName.PROBING:
|
|
98
|
+
yield from probes.execute(ctx, phase)
|
|
99
|
+
elif phase.name == PhaseName.SCHEMA_ANALYSIS:
|
|
100
|
+
yield from analysis.execute(ctx, phase)
|
|
101
|
+
elif phase.name == PhaseName.EXAMPLES:
|
|
102
|
+
yield from unit.execute(ctx, phase)
|
|
103
|
+
elif phase.name == PhaseName.COVERAGE:
|
|
104
|
+
yield from unit.execute(ctx, phase)
|
|
105
|
+
elif phase.name == PhaseName.FUZZING:
|
|
106
|
+
yield from unit.execute(ctx, phase)
|
|
107
|
+
elif phase.name == PhaseName.STATEFUL_TESTING:
|
|
108
|
+
yield from stateful.execute(ctx, phase)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from schemathesis.engine import Status, events
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from schemathesis.core.schema_analysis import SchemaWarning
|
|
9
|
+
from schemathesis.engine.context import EngineContext
|
|
10
|
+
from schemathesis.engine.events import EventGenerator
|
|
11
|
+
from schemathesis.engine.phases import Phase
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def execute(ctx: EngineContext, phase: Phase) -> EventGenerator:
|
|
15
|
+
"""Evaluate schema-level warnings once per test run."""
|
|
16
|
+
warnings = _collect_warnings(ctx)
|
|
17
|
+
if warnings:
|
|
18
|
+
yield events.SchemaAnalysisWarnings(phase=phase, warnings=warnings)
|
|
19
|
+
yield events.PhaseFinished(phase=phase, status=Status.SUCCESS, payload=None)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _collect_warnings(ctx: EngineContext) -> list[SchemaWarning]:
|
|
23
|
+
from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
|
|
24
|
+
|
|
25
|
+
schema = ctx.schema
|
|
26
|
+
if isinstance(schema, BaseOpenAPISchema):
|
|
27
|
+
return list(schema.analysis.iter_warnings())
|
|
28
|
+
return []
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Detecting capabilities of the application under test.
|
|
2
|
+
|
|
3
|
+
Schemathesis sends specially crafted requests to the application before running tests in order to detect whether
|
|
4
|
+
the application supports certain inputs. This is done to avoid false positives in the tests.
|
|
5
|
+
For example, certail web servers do not support NULL bytes in headers, in such cases, the generated test case
|
|
6
|
+
will not reach the tested application at all.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import enum
|
|
12
|
+
import warnings
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
from schemathesis.core.result import Ok, Result
|
|
17
|
+
from schemathesis.core.transport import USER_AGENT
|
|
18
|
+
from schemathesis.engine import Status, events
|
|
19
|
+
from schemathesis.transport.prepare import get_default_headers
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
import requests
|
|
23
|
+
|
|
24
|
+
from schemathesis.engine.context import EngineContext
|
|
25
|
+
from schemathesis.engine.events import EventGenerator
|
|
26
|
+
from schemathesis.engine.phases import Phase
|
|
27
|
+
from schemathesis.schemas import BaseSchema
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ProbePayload:
|
|
32
|
+
probes: list[ProbeRun]
|
|
33
|
+
|
|
34
|
+
__slots__ = ("probes",)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def execute(ctx: EngineContext, phase: Phase) -> EventGenerator:
|
|
38
|
+
"""Discover capabilities of the tested app."""
|
|
39
|
+
probes = run(ctx)
|
|
40
|
+
status = Status.SUCCESS
|
|
41
|
+
payload: Result[ProbePayload, Exception] | None = None
|
|
42
|
+
for result in probes:
|
|
43
|
+
if isinstance(result.probe, NullByteInHeader) and result.is_failure:
|
|
44
|
+
from schemathesis.specs.openapi import formats
|
|
45
|
+
from schemathesis.specs.openapi.formats import (
|
|
46
|
+
DEFAULT_HEADER_EXCLUDE_CHARACTERS,
|
|
47
|
+
HEADER_FORMAT,
|
|
48
|
+
header_values,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
formats.register(
|
|
52
|
+
HEADER_FORMAT, header_values(exclude_characters=DEFAULT_HEADER_EXCLUDE_CHARACTERS + "\x00")
|
|
53
|
+
)
|
|
54
|
+
payload = Ok(ProbePayload(probes=probes))
|
|
55
|
+
yield events.PhaseFinished(phase=phase, status=status, payload=payload)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def run(ctx: EngineContext) -> list[ProbeRun]:
|
|
59
|
+
"""Run all probes against the given schema."""
|
|
60
|
+
return [send(probe(), ctx) for probe in PROBES]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
HEADER_NAME = "X-Schemathesis-Probe"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class Probe:
|
|
68
|
+
"""A request to determine the capabilities of the application under test."""
|
|
69
|
+
|
|
70
|
+
name: str
|
|
71
|
+
|
|
72
|
+
__slots__ = ("name",)
|
|
73
|
+
|
|
74
|
+
def prepare_request(
|
|
75
|
+
self, session: requests.Session, request: requests.Request, schema: BaseSchema
|
|
76
|
+
) -> requests.PreparedRequest:
|
|
77
|
+
raise NotImplementedError
|
|
78
|
+
|
|
79
|
+
def analyze_response(self, response: requests.Response) -> ProbeOutcome:
|
|
80
|
+
raise NotImplementedError
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ProbeOutcome(str, enum.Enum):
|
|
84
|
+
# Capability is supported
|
|
85
|
+
SUCCESS = "success"
|
|
86
|
+
# Capability is not supported
|
|
87
|
+
FAILURE = "failure"
|
|
88
|
+
# Probe is not applicable
|
|
89
|
+
SKIP = "skip"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class ProbeRun:
|
|
94
|
+
probe: Probe
|
|
95
|
+
outcome: ProbeOutcome
|
|
96
|
+
request: requests.PreparedRequest | None
|
|
97
|
+
response: requests.Response | None
|
|
98
|
+
error: Exception | None
|
|
99
|
+
|
|
100
|
+
__slots__ = ("probe", "outcome", "request", "response", "error")
|
|
101
|
+
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
probe: Probe,
|
|
105
|
+
outcome: ProbeOutcome,
|
|
106
|
+
request: requests.PreparedRequest | None = None,
|
|
107
|
+
response: requests.Response | None = None,
|
|
108
|
+
error: Exception | None = None,
|
|
109
|
+
) -> None:
|
|
110
|
+
self.probe = probe
|
|
111
|
+
self.outcome = outcome
|
|
112
|
+
self.request = request
|
|
113
|
+
self.response = response
|
|
114
|
+
self.error = error
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def is_failure(self) -> bool:
|
|
118
|
+
return self.outcome == ProbeOutcome.FAILURE
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass
|
|
122
|
+
class NullByteInHeader(Probe):
|
|
123
|
+
"""Support NULL bytes in headers."""
|
|
124
|
+
|
|
125
|
+
__slots__ = ("name",)
|
|
126
|
+
|
|
127
|
+
def __init__(self) -> None:
|
|
128
|
+
self.name = "Supports NULL byte in headers"
|
|
129
|
+
|
|
130
|
+
def prepare_request(
|
|
131
|
+
self, session: requests.Session, request: requests.Request, schema: BaseSchema
|
|
132
|
+
) -> requests.PreparedRequest:
|
|
133
|
+
request.method = "GET"
|
|
134
|
+
request.url = schema.get_base_url()
|
|
135
|
+
request.headers = {"X-Schemathesis-Probe-Null": "\x00"}
|
|
136
|
+
return session.prepare_request(request)
|
|
137
|
+
|
|
138
|
+
def analyze_response(self, response: requests.Response) -> ProbeOutcome:
|
|
139
|
+
if response.status_code == 400:
|
|
140
|
+
return ProbeOutcome.FAILURE
|
|
141
|
+
return ProbeOutcome.SUCCESS
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
PROBES = (NullByteInHeader,)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def send(probe: Probe, ctx: EngineContext) -> ProbeRun:
|
|
148
|
+
"""Send the probe to the application."""
|
|
149
|
+
from requests import PreparedRequest, Request, RequestException
|
|
150
|
+
from requests.exceptions import MissingSchema
|
|
151
|
+
from urllib3.exceptions import InsecureRequestWarning
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
session = ctx.get_session()
|
|
155
|
+
request = probe.prepare_request(session, Request(), ctx.schema)
|
|
156
|
+
request.headers[HEADER_NAME] = probe.name
|
|
157
|
+
request.headers["User-Agent"] = USER_AGENT
|
|
158
|
+
for header, value in get_default_headers().items():
|
|
159
|
+
request.headers.setdefault(header, value)
|
|
160
|
+
with warnings.catch_warnings():
|
|
161
|
+
warnings.simplefilter("ignore", InsecureRequestWarning)
|
|
162
|
+
response = session.send(request, timeout=ctx.config.request_timeout or 2)
|
|
163
|
+
except MissingSchema:
|
|
164
|
+
# In-process ASGI/WSGI testing will have local URLs and requires extra handling
|
|
165
|
+
# which is not currently implemented
|
|
166
|
+
return ProbeRun(probe, ProbeOutcome.SKIP, None, None, None)
|
|
167
|
+
except RequestException as exc:
|
|
168
|
+
# Consider any network errors as a failed probe
|
|
169
|
+
req = exc.request if isinstance(exc.request, PreparedRequest) else None
|
|
170
|
+
return ProbeRun(probe, ProbeOutcome.FAILURE, req, None, exc)
|
|
171
|
+
result_type = probe.analyze_response(response)
|
|
172
|
+
return ProbeRun(probe, result_type, request, response)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import queue
|
|
4
|
+
import threading
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from schemathesis.engine import Status, events
|
|
8
|
+
from schemathesis.engine.phases import Phase, PhaseName, PhaseSkipReason
|
|
9
|
+
from schemathesis.generation.stateful import STATEFUL_TESTS_LABEL
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from schemathesis.engine.context import EngineContext
|
|
13
|
+
|
|
14
|
+
EVENT_QUEUE_TIMEOUT = 0.01
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def execute(engine: EngineContext, phase: Phase) -> events.EventGenerator:
|
|
18
|
+
from schemathesis.engine.phases.stateful._executor import execute_state_machine_loop
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
state_machine = engine.schema.as_state_machine()
|
|
22
|
+
except Exception as exc:
|
|
23
|
+
yield events.NonFatalError(error=exc, phase=phase.name, label=STATEFUL_TESTS_LABEL, related_to_operation=False)
|
|
24
|
+
yield events.PhaseFinished(phase=phase, status=Status.ERROR, payload=None)
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
event_queue: queue.Queue = queue.Queue()
|
|
28
|
+
|
|
29
|
+
thread = threading.Thread(
|
|
30
|
+
target=execute_state_machine_loop,
|
|
31
|
+
kwargs={"state_machine": state_machine, "event_queue": event_queue, "engine": engine},
|
|
32
|
+
name="schemathesis_stateful_tests",
|
|
33
|
+
)
|
|
34
|
+
status: Status | None = None
|
|
35
|
+
is_executed = False
|
|
36
|
+
|
|
37
|
+
thread.start()
|
|
38
|
+
try:
|
|
39
|
+
while True:
|
|
40
|
+
try:
|
|
41
|
+
event = event_queue.get(timeout=EVENT_QUEUE_TIMEOUT)
|
|
42
|
+
is_executed = True
|
|
43
|
+
# Set the run status based on the suite status
|
|
44
|
+
# ERROR & INTERRUPTED statuses are terminal, therefore they should not be overridden
|
|
45
|
+
if (
|
|
46
|
+
isinstance(event, events.SuiteFinished)
|
|
47
|
+
and event.status != Status.SKIP
|
|
48
|
+
and (status is None or status < event.status)
|
|
49
|
+
):
|
|
50
|
+
status = event.status
|
|
51
|
+
yield event
|
|
52
|
+
except queue.Empty:
|
|
53
|
+
if not thread.is_alive():
|
|
54
|
+
break
|
|
55
|
+
except KeyboardInterrupt:
|
|
56
|
+
# Immediately notify the engine thread to stop, even though that the event will be set below in `finally`
|
|
57
|
+
engine.stop()
|
|
58
|
+
status = Status.INTERRUPTED
|
|
59
|
+
yield events.Interrupted(phase=PhaseName.STATEFUL_TESTING)
|
|
60
|
+
finally:
|
|
61
|
+
thread.join()
|
|
62
|
+
|
|
63
|
+
if not is_executed:
|
|
64
|
+
phase.skip_reason = PhaseSkipReason.NOTHING_TO_TEST
|
|
65
|
+
status = Status.SKIP
|
|
66
|
+
elif status is None:
|
|
67
|
+
status = Status.SKIP
|
|
68
|
+
yield events.PhaseFinished(phase=phase, status=status, payload=None)
|