schemathesis 3.25.5__py3-none-any.whl → 4.0.0a1__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 +27 -65
- schemathesis/auths.py +102 -82
- schemathesis/checks.py +126 -46
- schemathesis/cli/__init__.py +11 -1766
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +37 -0
- schemathesis/cli/commands/run/__init__.py +662 -0
- schemathesis/cli/commands/run/checks.py +80 -0
- schemathesis/cli/commands/run/context.py +117 -0
- schemathesis/cli/commands/run/events.py +35 -0
- schemathesis/cli/commands/run/executor.py +138 -0
- schemathesis/cli/commands/run/filters.py +194 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +746 -0
- schemathesis/cli/commands/run/hypothesis.py +105 -0
- schemathesis/cli/commands/run/loaders.py +129 -0
- schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
- schemathesis/cli/constants.py +5 -52
- schemathesis/cli/core.py +17 -0
- schemathesis/cli/ext/fs.py +14 -0
- schemathesis/cli/ext/groups.py +55 -0
- schemathesis/cli/{options.py → ext/options.py} +39 -10
- schemathesis/cli/hooks.py +36 -0
- schemathesis/contrib/__init__.py +1 -3
- schemathesis/contrib/openapi/__init__.py +1 -3
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
- schemathesis/core/__init__.py +58 -0
- schemathesis/core/compat.py +25 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +370 -0
- schemathesis/core/failures.py +285 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
- schemathesis/core/output/__init__.py +69 -0
- schemathesis/core/output/sanitization.py +197 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +31 -0
- schemathesis/{internal → core}/result.py +1 -1
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +108 -0
- schemathesis/core/validation.py +38 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +30 -0
- schemathesis/engine/config.py +59 -0
- schemathesis/engine/context.py +119 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +157 -0
- schemathesis/engine/errors.py +394 -0
- schemathesis/engine/events.py +337 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{cli → engine/phases}/probes.py +63 -70
- schemathesis/engine/phases/stateful/__init__.py +65 -0
- schemathesis/engine/phases/stateful/_executor.py +326 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +174 -0
- schemathesis/engine/phases/unit/_executor.py +321 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +241 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +18 -14
- schemathesis/filters.py +103 -14
- schemathesis/generation/__init__.py +21 -37
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/generation/hypothesis/__init__.py +30 -0
- schemathesis/generation/hypothesis/builder.py +585 -0
- schemathesis/generation/hypothesis/examples.py +50 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/modes.py +28 -0
- schemathesis/generation/overrides.py +96 -0
- schemathesis/generation/stateful/__init__.py +20 -0
- schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +115 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +99 -67
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +412 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +63 -0
- schemathesis/openapi/loaders.py +178 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +273 -0
- schemathesis/pytest/loaders.py +12 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -261
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +25 -0
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +7 -5
- schemathesis/specs/graphql/schemas.py +215 -187
- schemathesis/specs/graphql/validation.py +11 -18
- schemathesis/specs/openapi/__init__.py +7 -1
- schemathesis/specs/openapi/_cache.py +122 -0
- schemathesis/specs/openapi/_hypothesis.py +146 -165
- schemathesis/specs/openapi/checks.py +565 -67
- schemathesis/specs/openapi/converter.py +33 -6
- schemathesis/specs/openapi/definitions.py +11 -18
- schemathesis/specs/openapi/examples.py +153 -39
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +4 -6
- schemathesis/specs/openapi/expressions/extractors.py +23 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +38 -14
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +45 -0
- schemathesis/specs/openapi/links.py +65 -165
- schemathesis/specs/openapi/media_types.py +32 -0
- schemathesis/specs/openapi/negative/__init__.py +7 -3
- schemathesis/specs/openapi/negative/mutations.py +24 -8
- schemathesis/specs/openapi/parameters.py +46 -30
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +47 -57
- schemathesis/specs/openapi/schemas.py +483 -367
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +11 -6
- schemathesis/specs/openapi/stateful/__init__.py +185 -73
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/transport/__init__.py +104 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +99 -0
- schemathesis/transport/requests.py +221 -0
- schemathesis/{_xml.py → transport/serialization.py} +143 -28
- schemathesis/transport/wsgi.py +165 -0
- schemathesis-4.0.0a1.dist-info/METADATA +297 -0
- schemathesis-4.0.0a1.dist-info/RECORD +152 -0
- {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
- {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -17
- schemathesis/_hypothesis.py +0 -246
- schemathesis/_override.py +0 -49
- schemathesis/cli/cassettes.py +0 -375
- schemathesis/cli/context.py +0 -55
- schemathesis/cli/debug.py +0 -26
- schemathesis/cli/handlers.py +0 -16
- schemathesis/cli/junitxml.py +0 -43
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -765
- schemathesis/cli/output/short.py +0 -40
- schemathesis/cli/sanitization.py +0 -20
- schemathesis/code_samples.py +0 -149
- schemathesis/constants.py +0 -55
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -15
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -560
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -17
- schemathesis/failures.py +0 -209
- schemathesis/fixups/__init__.py +0 -36
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -29
- schemathesis/graphql.py +0 -4
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/copy.py +0 -13
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -34
- schemathesis/internal/jsonschema.py +0 -35
- schemathesis/internal/transformation.py +0 -15
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -361
- schemathesis/loaders.py +0 -120
- schemathesis/models.py +0 -1231
- schemathesis/parameters.py +0 -86
- schemathesis/runner/__init__.py +0 -555
- schemathesis/runner/events.py +0 -309
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -986
- schemathesis/runner/impl/solo.py +0 -90
- schemathesis/runner/impl/threadpool.py +0 -400
- schemathesis/runner/serialization.py +0 -411
- schemathesis/sanitization.py +0 -248
- schemathesis/serializers.py +0 -315
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -201
- schemathesis/service/client.py +0 -100
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -57
- schemathesis/service/hosts.py +0 -107
- schemathesis/service/metadata.py +0 -46
- schemathesis/service/models.py +0 -49
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -184
- schemathesis/service/usage.py +0 -65
- schemathesis/specs/graphql/loaders.py +0 -344
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/loaders.py +0 -667
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis/specs/openapi/validation.py +0 -25
- schemathesis/stateful/__init__.py +0 -133
- schemathesis/targets.py +0 -45
- schemathesis/throttling.py +0 -41
- schemathesis/transports/__init__.py +0 -5
- schemathesis/transports/auth.py +0 -15
- schemathesis/transports/headers.py +0 -35
- schemathesis/transports/responses.py +0 -52
- schemathesis/types.py +0 -35
- schemathesis/utils.py +0 -169
- schemathesis-3.25.5.dist-info/METADATA +0 -356
- schemathesis-3.25.5.dist-info/RECORD +0 -134
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,337 @@
|
|
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.transport import Response
|
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 PhaseStarted(PhaseEvent):
|
51
|
+
"""Start of an execution phase."""
|
52
|
+
|
53
|
+
__slots__ = ("id", "timestamp", "phase")
|
54
|
+
|
55
|
+
def __init__(self, *, phase: Phase) -> None:
|
56
|
+
self.id = uuid.uuid4()
|
57
|
+
self.timestamp = time.time()
|
58
|
+
self.phase = phase
|
59
|
+
|
60
|
+
|
61
|
+
@dataclass
|
62
|
+
class PhaseFinished(PhaseEvent):
|
63
|
+
"""End of an execution phase."""
|
64
|
+
|
65
|
+
status: Status
|
66
|
+
payload: Result[ProbePayload, Exception] | None
|
67
|
+
|
68
|
+
__slots__ = ("id", "timestamp", "phase", "status", "payload")
|
69
|
+
|
70
|
+
def __init__(self, *, phase: Phase, status: Status, payload: Result[ProbePayload, Exception] | None) -> None:
|
71
|
+
self.id = uuid.uuid4()
|
72
|
+
self.timestamp = time.time()
|
73
|
+
self.phase = phase
|
74
|
+
self.status = status
|
75
|
+
self.payload = payload
|
76
|
+
|
77
|
+
|
78
|
+
@dataclass
|
79
|
+
class TestEvent(EngineEvent):
|
80
|
+
phase: PhaseName
|
81
|
+
|
82
|
+
|
83
|
+
@dataclass
|
84
|
+
class SuiteStarted(TestEvent):
|
85
|
+
"""Before executing a set of scenarios."""
|
86
|
+
|
87
|
+
__slots__ = ("id", "timestamp", "phase")
|
88
|
+
|
89
|
+
def __init__(self, *, phase: PhaseName) -> None:
|
90
|
+
self.id = uuid.uuid4()
|
91
|
+
self.timestamp = time.time()
|
92
|
+
self.phase = phase
|
93
|
+
|
94
|
+
|
95
|
+
@dataclass
|
96
|
+
class SuiteFinished(TestEvent):
|
97
|
+
"""After executing a set of test scenarios."""
|
98
|
+
|
99
|
+
status: Status
|
100
|
+
|
101
|
+
__slots__ = ("id", "timestamp", "phase", "status")
|
102
|
+
|
103
|
+
def __init__(self, *, id: uuid.UUID, phase: PhaseName, status: Status) -> None:
|
104
|
+
self.id = id
|
105
|
+
self.timestamp = time.time()
|
106
|
+
self.phase = phase
|
107
|
+
self.status = status
|
108
|
+
|
109
|
+
|
110
|
+
@dataclass
|
111
|
+
class ScenarioEvent(TestEvent):
|
112
|
+
suite_id: uuid.UUID
|
113
|
+
|
114
|
+
|
115
|
+
@dataclass
|
116
|
+
class ScenarioStarted(ScenarioEvent):
|
117
|
+
"""Before executing a grouped set of test steps."""
|
118
|
+
|
119
|
+
__slots__ = ("id", "timestamp", "phase", "suite_id", "label")
|
120
|
+
|
121
|
+
def __init__(self, *, phase: PhaseName, suite_id: uuid.UUID, label: str | None) -> None:
|
122
|
+
self.id = uuid.uuid4()
|
123
|
+
self.timestamp = time.time()
|
124
|
+
self.phase = phase
|
125
|
+
self.suite_id = suite_id
|
126
|
+
self.label = label
|
127
|
+
|
128
|
+
|
129
|
+
@dataclass
|
130
|
+
class ScenarioFinished(ScenarioEvent):
|
131
|
+
"""After executing a grouped set of test steps."""
|
132
|
+
|
133
|
+
status: Status
|
134
|
+
recorder: ScenarioRecorder
|
135
|
+
elapsed_time: float
|
136
|
+
skip_reason: str | None
|
137
|
+
# Whether this is a scenario that tries to reproduce a failure
|
138
|
+
is_final: bool
|
139
|
+
|
140
|
+
__slots__ = (
|
141
|
+
"id",
|
142
|
+
"timestamp",
|
143
|
+
"phase",
|
144
|
+
"suite_id",
|
145
|
+
"status",
|
146
|
+
"recorder",
|
147
|
+
"elapsed_time",
|
148
|
+
"skip_reason",
|
149
|
+
"is_final",
|
150
|
+
)
|
151
|
+
|
152
|
+
def __init__(
|
153
|
+
self,
|
154
|
+
*,
|
155
|
+
id: uuid.UUID,
|
156
|
+
phase: PhaseName,
|
157
|
+
suite_id: uuid.UUID,
|
158
|
+
status: Status,
|
159
|
+
recorder: ScenarioRecorder,
|
160
|
+
elapsed_time: float,
|
161
|
+
skip_reason: str | None,
|
162
|
+
is_final: bool,
|
163
|
+
) -> None:
|
164
|
+
self.id = id
|
165
|
+
self.timestamp = time.time()
|
166
|
+
self.phase = phase
|
167
|
+
self.suite_id = suite_id
|
168
|
+
self.status = status
|
169
|
+
self.recorder = recorder
|
170
|
+
self.elapsed_time = elapsed_time
|
171
|
+
self.skip_reason = skip_reason
|
172
|
+
self.is_final = is_final
|
173
|
+
|
174
|
+
|
175
|
+
@dataclass
|
176
|
+
class StepEvent(ScenarioEvent):
|
177
|
+
scenario_id: uuid.UUID
|
178
|
+
|
179
|
+
|
180
|
+
@dataclass
|
181
|
+
class StepStarted(StepEvent):
|
182
|
+
"""Before executing a test case."""
|
183
|
+
|
184
|
+
__slots__ = (
|
185
|
+
"id",
|
186
|
+
"timestamp",
|
187
|
+
"phase",
|
188
|
+
"suite_id",
|
189
|
+
"scenario_id",
|
190
|
+
)
|
191
|
+
|
192
|
+
def __init__(
|
193
|
+
self,
|
194
|
+
*,
|
195
|
+
phase: PhaseName,
|
196
|
+
suite_id: uuid.UUID,
|
197
|
+
scenario_id: uuid.UUID,
|
198
|
+
) -> None:
|
199
|
+
self.id = uuid.uuid4()
|
200
|
+
self.timestamp = time.time()
|
201
|
+
self.phase = phase
|
202
|
+
self.suite_id = suite_id
|
203
|
+
self.scenario_id = scenario_id
|
204
|
+
|
205
|
+
|
206
|
+
@dataclass
|
207
|
+
class TransitionId:
|
208
|
+
"""Id of the the that was hit."""
|
209
|
+
|
210
|
+
name: str
|
211
|
+
# Status code as defined in the transition, i.e. may be `default`
|
212
|
+
status_code: str
|
213
|
+
source: str
|
214
|
+
|
215
|
+
__slots__ = ("name", "status_code", "source")
|
216
|
+
|
217
|
+
|
218
|
+
@dataclass
|
219
|
+
class ResponseData:
|
220
|
+
"""Common data for responses."""
|
221
|
+
|
222
|
+
status_code: int
|
223
|
+
elapsed: float
|
224
|
+
__slots__ = ("status_code", "elapsed")
|
225
|
+
|
226
|
+
|
227
|
+
@dataclass
|
228
|
+
class StepFinished(StepEvent):
|
229
|
+
"""After executing a test case."""
|
230
|
+
|
231
|
+
status: Status | None
|
232
|
+
transition_id: TransitionId | None
|
233
|
+
target: str
|
234
|
+
response: Response | None
|
235
|
+
|
236
|
+
__slots__ = (
|
237
|
+
"id",
|
238
|
+
"timestamp",
|
239
|
+
"phase",
|
240
|
+
"status",
|
241
|
+
"suite_id",
|
242
|
+
"scenario_id",
|
243
|
+
"transition_id",
|
244
|
+
"target",
|
245
|
+
"response",
|
246
|
+
)
|
247
|
+
|
248
|
+
def __init__(
|
249
|
+
self,
|
250
|
+
*,
|
251
|
+
phase: PhaseName,
|
252
|
+
id: uuid.UUID,
|
253
|
+
suite_id: uuid.UUID,
|
254
|
+
scenario_id: uuid.UUID,
|
255
|
+
status: Status | None,
|
256
|
+
transition_id: TransitionId | None,
|
257
|
+
target: str,
|
258
|
+
response: Response | None,
|
259
|
+
) -> None:
|
260
|
+
self.id = id
|
261
|
+
self.timestamp = time.time()
|
262
|
+
self.phase = phase
|
263
|
+
self.status = status
|
264
|
+
self.suite_id = suite_id
|
265
|
+
self.scenario_id = scenario_id
|
266
|
+
self.transition_id = transition_id
|
267
|
+
self.target = target
|
268
|
+
self.response = response
|
269
|
+
|
270
|
+
|
271
|
+
@dataclass
|
272
|
+
class Interrupted(EngineEvent):
|
273
|
+
"""If execution was interrupted by Ctrl-C, or a received SIGTERM."""
|
274
|
+
|
275
|
+
phase: PhaseName | None
|
276
|
+
|
277
|
+
__slots__ = ("id", "timestamp", "phase")
|
278
|
+
|
279
|
+
def __init__(self, *, phase: PhaseName | None) -> None:
|
280
|
+
self.id = uuid.uuid4()
|
281
|
+
self.timestamp = time.time()
|
282
|
+
self.phase = phase
|
283
|
+
|
284
|
+
|
285
|
+
@dataclass
|
286
|
+
class NonFatalError(EngineEvent):
|
287
|
+
"""Error that doesn't halt execution but should be reported."""
|
288
|
+
|
289
|
+
info: EngineErrorInfo
|
290
|
+
value: Exception
|
291
|
+
phase: PhaseName
|
292
|
+
label: str
|
293
|
+
related_to_operation: bool
|
294
|
+
|
295
|
+
__slots__ = ("id", "timestamp", "info", "value", "phase", "label", "related_to_operation")
|
296
|
+
|
297
|
+
def __init__(self, *, error: Exception, phase: PhaseName, label: str, related_to_operation: bool) -> None:
|
298
|
+
self.id = uuid.uuid4()
|
299
|
+
self.timestamp = time.time()
|
300
|
+
self.info = EngineErrorInfo(error=error)
|
301
|
+
self.value = error
|
302
|
+
self.phase = phase
|
303
|
+
self.label = label
|
304
|
+
self.related_to_operation = related_to_operation
|
305
|
+
|
306
|
+
|
307
|
+
@dataclass
|
308
|
+
class FatalError(EngineEvent):
|
309
|
+
"""Internal error in the engine."""
|
310
|
+
|
311
|
+
exception: Exception
|
312
|
+
is_terminal = True
|
313
|
+
|
314
|
+
__slots__ = ("id", "timestamp", "exception")
|
315
|
+
|
316
|
+
def __init__(self, *, exception: Exception) -> None:
|
317
|
+
self.id = uuid.uuid4()
|
318
|
+
self.timestamp = time.time()
|
319
|
+
self.exception = exception
|
320
|
+
|
321
|
+
|
322
|
+
@dataclass
|
323
|
+
class EngineFinished(EngineEvent):
|
324
|
+
"""The final event of the run.
|
325
|
+
|
326
|
+
No more events after this point.
|
327
|
+
"""
|
328
|
+
|
329
|
+
is_terminal = True
|
330
|
+
running_time: float
|
331
|
+
|
332
|
+
__slots__ = ("id", "timestamp", "running_time")
|
333
|
+
|
334
|
+
def __init__(self, *, running_time: float) -> None:
|
335
|
+
self.id = uuid.uuid4()
|
336
|
+
self.timestamp = time.time()
|
337
|
+
self.running_time = running_time
|
@@ -0,0 +1,66 @@
|
|
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(enum.Enum):
|
14
|
+
"""Available execution phases."""
|
15
|
+
|
16
|
+
PROBING = "API probing"
|
17
|
+
UNIT_TESTING = "Unit testing"
|
18
|
+
STATEFUL_TESTING = "Stateful testing"
|
19
|
+
|
20
|
+
@classmethod
|
21
|
+
def from_str(cls, value: str) -> PhaseName:
|
22
|
+
return {
|
23
|
+
"probing": cls.PROBING,
|
24
|
+
"unit": cls.UNIT_TESTING,
|
25
|
+
"stateful": cls.STATEFUL_TESTING,
|
26
|
+
}[value.lower()]
|
27
|
+
|
28
|
+
|
29
|
+
class PhaseSkipReason(str, enum.Enum):
|
30
|
+
"""Reasons why a phase might not be executed."""
|
31
|
+
|
32
|
+
DISABLED = "disabled" # Explicitly disabled via config
|
33
|
+
NOT_SUPPORTED = "not supported" # Feature not supported by schema
|
34
|
+
NOT_APPLICABLE = "not applicable" # No relevant data (e.g., no links for stateful)
|
35
|
+
FAILURE_LIMIT_REACHED = "failure limit reached"
|
36
|
+
NOTHING_TO_TEST = "nothing to test"
|
37
|
+
|
38
|
+
|
39
|
+
@dataclass
|
40
|
+
class Phase:
|
41
|
+
"""A logically separate engine execution phase."""
|
42
|
+
|
43
|
+
name: PhaseName
|
44
|
+
is_supported: bool
|
45
|
+
is_enabled: bool = True
|
46
|
+
skip_reason: PhaseSkipReason | None = None
|
47
|
+
|
48
|
+
def should_execute(self, ctx: EngineContext) -> bool:
|
49
|
+
"""Determine if phase should run based on context & configuration."""
|
50
|
+
return self.is_enabled and not ctx.has_to_stop
|
51
|
+
|
52
|
+
|
53
|
+
def execute(ctx: EngineContext, phase: Phase) -> EventGenerator:
|
54
|
+
from urllib3.exceptions import InsecureRequestWarning
|
55
|
+
|
56
|
+
from . import probes, stateful, unit
|
57
|
+
|
58
|
+
with warnings.catch_warnings():
|
59
|
+
warnings.simplefilter("ignore", InsecureRequestWarning)
|
60
|
+
|
61
|
+
if phase.name == PhaseName.PROBING:
|
62
|
+
yield from probes.execute(ctx, phase)
|
63
|
+
elif phase.name == PhaseName.UNIT_TESTING:
|
64
|
+
yield from unit.execute(ctx, phase)
|
65
|
+
elif phase.name == PhaseName.STATEFUL_TESTING:
|
66
|
+
yield from stateful.execute(ctx, phase)
|
@@ -5,24 +5,58 @@ the application supports certain inputs. This is done to avoid false positives i
|
|
5
5
|
For example, certail web servers do not support NULL bytes in headers, in such cases, the generated test case
|
6
6
|
will not reach the tested application at all.
|
7
7
|
"""
|
8
|
+
|
8
9
|
from __future__ import annotations
|
9
10
|
|
10
11
|
import enum
|
11
12
|
import warnings
|
12
|
-
from dataclasses import
|
13
|
-
from typing import TYPE_CHECKING
|
13
|
+
from dataclasses import dataclass
|
14
|
+
from typing import TYPE_CHECKING
|
14
15
|
|
15
|
-
from
|
16
|
-
from
|
17
|
-
from
|
18
|
-
from ..sanitization import sanitize_request, sanitize_response
|
19
|
-
from ..transports.auth import get_requests_auth
|
16
|
+
from schemathesis.core.result import Err, Ok, Result
|
17
|
+
from schemathesis.core.transport import USER_AGENT
|
18
|
+
from schemathesis.engine import Status, events
|
20
19
|
|
21
20
|
if TYPE_CHECKING:
|
22
21
|
import requests
|
23
22
|
|
24
|
-
from
|
25
|
-
from . import
|
23
|
+
from schemathesis.engine.config import NetworkConfig
|
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.schema, ctx.session, ctx.config.network)
|
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 ...specs.openapi import formats
|
45
|
+
from ...specs.openapi.formats import HEADER_FORMAT, header_values
|
46
|
+
|
47
|
+
formats.register(HEADER_FORMAT, header_values(blacklist_characters="\n\r\x00"))
|
48
|
+
if result.error is not None:
|
49
|
+
status = Status.ERROR
|
50
|
+
payload = Err(result.error)
|
51
|
+
else:
|
52
|
+
status = Status.SUCCESS
|
53
|
+
payload = Ok(ProbePayload(probes=probes))
|
54
|
+
yield events.PhaseFinished(phase=phase, status=status, payload=payload)
|
55
|
+
|
56
|
+
|
57
|
+
def run(schema: BaseSchema, session: requests.Session, config: NetworkConfig) -> list[ProbeRun]:
|
58
|
+
"""Run all probes against the given schema."""
|
59
|
+
return [send(probe(), session, schema, config) for probe in PROBES]
|
26
60
|
|
27
61
|
|
28
62
|
HEADER_NAME = "X-Schemathesis-Probe"
|
@@ -35,15 +69,15 @@ class Probe:
|
|
35
69
|
name: str
|
36
70
|
|
37
71
|
def prepare_request(
|
38
|
-
self, session: requests.Session, request: requests.Request, schema: BaseSchema
|
72
|
+
self, session: requests.Session, request: requests.Request, schema: BaseSchema
|
39
73
|
) -> requests.PreparedRequest:
|
40
74
|
raise NotImplementedError
|
41
75
|
|
42
|
-
def analyze_response(self, response: requests.Response) ->
|
76
|
+
def analyze_response(self, response: requests.Response) -> ProbeOutcome:
|
43
77
|
raise NotImplementedError
|
44
78
|
|
45
79
|
|
46
|
-
class
|
80
|
+
class ProbeOutcome(str, enum.Enum):
|
47
81
|
# Capability is supported
|
48
82
|
SUCCESS = "success"
|
49
83
|
# Capability is not supported
|
@@ -55,101 +89,60 @@ class ProbeResultType(str, enum.Enum):
|
|
55
89
|
|
56
90
|
|
57
91
|
@dataclass
|
58
|
-
class
|
59
|
-
"""Result of a probe."""
|
60
|
-
|
92
|
+
class ProbeRun:
|
61
93
|
probe: Probe
|
62
|
-
|
94
|
+
outcome: ProbeOutcome
|
63
95
|
request: requests.PreparedRequest | None = None
|
64
96
|
response: requests.Response | None = None
|
65
|
-
error:
|
97
|
+
error: Exception | None = None
|
66
98
|
|
67
99
|
@property
|
68
100
|
def is_failure(self) -> bool:
|
69
|
-
return self.
|
70
|
-
|
71
|
-
def serialize(self) -> dict[str, Any]:
|
72
|
-
"""Serialize probe results so it can be sent over the network."""
|
73
|
-
if self.request:
|
74
|
-
_request = Request.from_prepared_request(self.request)
|
75
|
-
sanitize_request(_request)
|
76
|
-
request = asdict(_request)
|
77
|
-
else:
|
78
|
-
request = None
|
79
|
-
if self.response:
|
80
|
-
sanitize_response(self.response)
|
81
|
-
response = asdict(Response.from_requests(self.response))
|
82
|
-
else:
|
83
|
-
response = None
|
84
|
-
if self.error:
|
85
|
-
error = format_exception(self.error)
|
86
|
-
else:
|
87
|
-
error = None
|
88
|
-
return {
|
89
|
-
"name": self.probe.name,
|
90
|
-
"type": self.type.value,
|
91
|
-
"request": request,
|
92
|
-
"response": response,
|
93
|
-
"error": error,
|
94
|
-
}
|
101
|
+
return self.outcome == ProbeOutcome.FAILURE
|
95
102
|
|
96
103
|
|
97
104
|
@dataclass
|
98
105
|
class NullByteInHeader(Probe):
|
99
106
|
"""Support NULL bytes in headers."""
|
100
107
|
|
101
|
-
name: str = "
|
108
|
+
name: str = "Supports NULL byte in headers"
|
102
109
|
|
103
110
|
def prepare_request(
|
104
|
-
self, session: requests.Session, request: requests.Request, schema: BaseSchema
|
111
|
+
self, session: requests.Session, request: requests.Request, schema: BaseSchema
|
105
112
|
) -> requests.PreparedRequest:
|
106
113
|
request.method = "GET"
|
107
|
-
request.url =
|
114
|
+
request.url = schema.get_base_url()
|
108
115
|
request.headers = {"X-Schemathesis-Probe-Null": "\x00"}
|
109
116
|
return session.prepare_request(request)
|
110
117
|
|
111
|
-
def analyze_response(self, response: requests.Response) ->
|
118
|
+
def analyze_response(self, response: requests.Response) -> ProbeOutcome:
|
112
119
|
if response.status_code == 400:
|
113
|
-
return
|
114
|
-
return
|
120
|
+
return ProbeOutcome.FAILURE
|
121
|
+
return ProbeOutcome.SUCCESS
|
115
122
|
|
116
123
|
|
117
124
|
PROBES = (NullByteInHeader,)
|
118
125
|
|
119
126
|
|
120
|
-
def send(probe: Probe, session: requests.Session, schema: BaseSchema, config:
|
127
|
+
def send(probe: Probe, session: requests.Session, schema: BaseSchema, config: NetworkConfig) -> ProbeRun:
|
121
128
|
"""Send the probe to the application."""
|
122
|
-
from requests import Request, RequestException
|
129
|
+
from requests import PreparedRequest, Request, RequestException
|
123
130
|
from requests.exceptions import MissingSchema
|
124
131
|
from urllib3.exceptions import InsecureRequestWarning
|
125
132
|
|
126
133
|
try:
|
127
|
-
request = probe.prepare_request(session, Request(), schema
|
134
|
+
request = probe.prepare_request(session, Request(), schema)
|
128
135
|
request.headers[HEADER_NAME] = probe.name
|
129
136
|
request.headers["User-Agent"] = USER_AGENT
|
130
137
|
with warnings.catch_warnings():
|
131
138
|
warnings.simplefilter("ignore", InsecureRequestWarning)
|
132
|
-
response = session.send(request)
|
139
|
+
response = session.send(request, timeout=config.timeout or 2)
|
133
140
|
except MissingSchema:
|
134
141
|
# In-process ASGI/WSGI testing will have local URLs and requires extra handling
|
135
142
|
# which is not currently implemented
|
136
|
-
return
|
143
|
+
return ProbeRun(probe, ProbeOutcome.SKIP, None, None, None)
|
137
144
|
except RequestException as exc:
|
138
145
|
req = exc.request if isinstance(exc.request, PreparedRequest) else None
|
139
|
-
return
|
146
|
+
return ProbeRun(probe, ProbeOutcome.ERROR, req, None, exc)
|
140
147
|
result_type = probe.analyze_response(response)
|
141
|
-
return
|
142
|
-
|
143
|
-
|
144
|
-
def run(schema: BaseSchema, config: LoaderConfig) -> list[ProbeResult]:
|
145
|
-
"""Run all probes against the given schema."""
|
146
|
-
from requests import Session
|
147
|
-
|
148
|
-
session = Session()
|
149
|
-
session.verify = config.request_tls_verify
|
150
|
-
if config.request_cert is not None:
|
151
|
-
session.cert = config.request_cert
|
152
|
-
if config.auth is not None:
|
153
|
-
session.auth = get_requests_auth(config.auth, config.auth_type)
|
154
|
-
|
155
|
-
return [send(probe(), session, schema, config) for probe in PROBES]
|
148
|
+
return ProbeRun(probe, result_type, request, response)
|
@@ -0,0 +1,65 @@
|
|
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
|
+
|
10
|
+
if TYPE_CHECKING:
|
11
|
+
from schemathesis.engine.context import EngineContext
|
12
|
+
|
13
|
+
EVENT_QUEUE_TIMEOUT = 0.01
|
14
|
+
|
15
|
+
|
16
|
+
def execute(engine: EngineContext, phase: Phase) -> events.EventGenerator:
|
17
|
+
from schemathesis.engine.phases.stateful._executor import execute_state_machine_loop
|
18
|
+
|
19
|
+
try:
|
20
|
+
state_machine = engine.schema.as_state_machine()
|
21
|
+
except Exception as exc:
|
22
|
+
yield events.NonFatalError(error=exc, phase=phase.name, label="Stateful tests", related_to_operation=False)
|
23
|
+
return
|
24
|
+
|
25
|
+
event_queue: queue.Queue = queue.Queue()
|
26
|
+
|
27
|
+
thread = threading.Thread(
|
28
|
+
target=execute_state_machine_loop,
|
29
|
+
kwargs={"state_machine": state_machine, "event_queue": event_queue, "engine": engine},
|
30
|
+
)
|
31
|
+
status: Status | None = None
|
32
|
+
is_executed = False
|
33
|
+
|
34
|
+
thread.start()
|
35
|
+
try:
|
36
|
+
while True:
|
37
|
+
try:
|
38
|
+
event = event_queue.get(timeout=EVENT_QUEUE_TIMEOUT)
|
39
|
+
is_executed = True
|
40
|
+
# Set the run status based on the suite status
|
41
|
+
# ERROR & INTERRUPTED statuses are terminal, therefore they should not be overridden
|
42
|
+
if (
|
43
|
+
isinstance(event, events.SuiteFinished)
|
44
|
+
and event.status != Status.SKIP
|
45
|
+
and (status is None or status < event.status)
|
46
|
+
):
|
47
|
+
status = event.status
|
48
|
+
yield event
|
49
|
+
except queue.Empty:
|
50
|
+
if not thread.is_alive():
|
51
|
+
break
|
52
|
+
except KeyboardInterrupt:
|
53
|
+
# Immediately notify the engine thread to stop, even though that the event will be set below in `finally`
|
54
|
+
engine.stop()
|
55
|
+
status = Status.INTERRUPTED
|
56
|
+
yield events.Interrupted(phase=PhaseName.STATEFUL_TESTING)
|
57
|
+
finally:
|
58
|
+
thread.join()
|
59
|
+
|
60
|
+
if not is_executed:
|
61
|
+
phase.skip_reason = PhaseSkipReason.NOTHING_TO_TEST
|
62
|
+
status = Status.SKIP
|
63
|
+
elif status is None:
|
64
|
+
status = Status.SKIP
|
65
|
+
yield events.PhaseFinished(phase=phase, status=status, payload=None)
|