schemathesis 3.25.6__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 -1760
- 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/{runner → engine/phases}/probes.py +50 -67
- 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 +139 -23
- 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 +478 -369
- 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.6.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
- {schemathesis-3.25.6.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 -58
- 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 -790
- schemathesis/cli/output/short.py +0 -44
- 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 -1234
- schemathesis/parameters.py +0 -86
- schemathesis/runner/__init__.py +0 -570
- schemathesis/runner/events.py +0 -329
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -1035
- 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 -323
- 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 -199
- 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.6.dist-info/METADATA +0 -356
- schemathesis-3.25.6.dist-info/RECORD +0 -134
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- {schemathesis-3.25.6.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,38 +5,61 @@ 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
|
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
|
26
28
|
|
27
29
|
|
28
|
-
|
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)
|
29
55
|
|
30
56
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
auth: tuple[str, str] | None = None
|
38
|
-
auth_type: str | None = None
|
39
|
-
headers: dict[str, str] | None = None
|
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]
|
60
|
+
|
61
|
+
|
62
|
+
HEADER_NAME = "X-Schemathesis-Probe"
|
40
63
|
|
41
64
|
|
42
65
|
@dataclass
|
@@ -46,7 +69,7 @@ class Probe:
|
|
46
69
|
name: str
|
47
70
|
|
48
71
|
def prepare_request(
|
49
|
-
self, session: requests.Session, request: requests.Request, schema: BaseSchema
|
72
|
+
self, session: requests.Session, request: requests.Request, schema: BaseSchema
|
50
73
|
) -> requests.PreparedRequest:
|
51
74
|
raise NotImplementedError
|
52
75
|
|
@@ -71,49 +94,24 @@ class ProbeRun:
|
|
71
94
|
outcome: ProbeOutcome
|
72
95
|
request: requests.PreparedRequest | None = None
|
73
96
|
response: requests.Response | None = None
|
74
|
-
error:
|
97
|
+
error: Exception | None = None
|
75
98
|
|
76
99
|
@property
|
77
100
|
def is_failure(self) -> bool:
|
78
101
|
return self.outcome == ProbeOutcome.FAILURE
|
79
102
|
|
80
|
-
def serialize(self) -> dict[str, Any]:
|
81
|
-
"""Serialize probe results so it can be sent over the network."""
|
82
|
-
if self.request:
|
83
|
-
_request = Request.from_prepared_request(self.request)
|
84
|
-
sanitize_request(_request)
|
85
|
-
request = asdict(_request)
|
86
|
-
else:
|
87
|
-
request = None
|
88
|
-
if self.response:
|
89
|
-
sanitize_response(self.response)
|
90
|
-
response = asdict(Response.from_requests(self.response))
|
91
|
-
else:
|
92
|
-
response = None
|
93
|
-
if self.error:
|
94
|
-
error = format_exception(self.error)
|
95
|
-
else:
|
96
|
-
error = None
|
97
|
-
return {
|
98
|
-
"name": self.probe.name,
|
99
|
-
"outcome": self.outcome.value,
|
100
|
-
"request": request,
|
101
|
-
"response": response,
|
102
|
-
"error": error,
|
103
|
-
}
|
104
|
-
|
105
103
|
|
106
104
|
@dataclass
|
107
105
|
class NullByteInHeader(Probe):
|
108
106
|
"""Support NULL bytes in headers."""
|
109
107
|
|
110
|
-
name: str = "
|
108
|
+
name: str = "Supports NULL byte in headers"
|
111
109
|
|
112
110
|
def prepare_request(
|
113
|
-
self, session: requests.Session, request: requests.Request, schema: BaseSchema
|
111
|
+
self, session: requests.Session, request: requests.Request, schema: BaseSchema
|
114
112
|
) -> requests.PreparedRequest:
|
115
113
|
request.method = "GET"
|
116
|
-
request.url =
|
114
|
+
request.url = schema.get_base_url()
|
117
115
|
request.headers = {"X-Schemathesis-Probe-Null": "\x00"}
|
118
116
|
return session.prepare_request(request)
|
119
117
|
|
@@ -126,19 +124,19 @@ class NullByteInHeader(Probe):
|
|
126
124
|
PROBES = (NullByteInHeader,)
|
127
125
|
|
128
126
|
|
129
|
-
def send(probe: Probe, session: requests.Session, schema: BaseSchema, config:
|
127
|
+
def send(probe: Probe, session: requests.Session, schema: BaseSchema, config: NetworkConfig) -> ProbeRun:
|
130
128
|
"""Send the probe to the application."""
|
131
129
|
from requests import PreparedRequest, Request, RequestException
|
132
130
|
from requests.exceptions import MissingSchema
|
133
131
|
from urllib3.exceptions import InsecureRequestWarning
|
134
132
|
|
135
133
|
try:
|
136
|
-
request = probe.prepare_request(session, Request(), schema
|
134
|
+
request = probe.prepare_request(session, Request(), schema)
|
137
135
|
request.headers[HEADER_NAME] = probe.name
|
138
136
|
request.headers["User-Agent"] = USER_AGENT
|
139
137
|
with warnings.catch_warnings():
|
140
138
|
warnings.simplefilter("ignore", InsecureRequestWarning)
|
141
|
-
response = session.send(request, timeout=2)
|
139
|
+
response = session.send(request, timeout=config.timeout or 2)
|
142
140
|
except MissingSchema:
|
143
141
|
# In-process ASGI/WSGI testing will have local URLs and requires extra handling
|
144
142
|
# which is not currently implemented
|
@@ -148,18 +146,3 @@ def send(probe: Probe, session: requests.Session, schema: BaseSchema, config: Pr
|
|
148
146
|
return ProbeRun(probe, ProbeOutcome.ERROR, req, None, exc)
|
149
147
|
result_type = probe.analyze_response(response)
|
150
148
|
return ProbeRun(probe, result_type, request, response)
|
151
|
-
|
152
|
-
|
153
|
-
def run(schema: BaseSchema, config: ProbeConfig) -> list[ProbeRun]:
|
154
|
-
"""Run all probes against the given schema."""
|
155
|
-
from requests import Session
|
156
|
-
|
157
|
-
session = Session()
|
158
|
-
session.headers.update(config.headers or {})
|
159
|
-
session.verify = config.request_tls_verify
|
160
|
-
if config.request_cert is not None:
|
161
|
-
session.cert = config.request_cert
|
162
|
-
if config.auth is not None:
|
163
|
-
session.auth = get_requests_auth(config.auth, config.auth_type)
|
164
|
-
|
165
|
-
return [send(probe(), session, schema, config) for probe in PROBES]
|
@@ -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)
|