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.
Files changed (221) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +102 -82
  3. schemathesis/checks.py +126 -46
  4. schemathesis/cli/__init__.py +11 -1766
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +37 -0
  7. schemathesis/cli/commands/run/__init__.py +662 -0
  8. schemathesis/cli/commands/run/checks.py +80 -0
  9. schemathesis/cli/commands/run/context.py +117 -0
  10. schemathesis/cli/commands/run/events.py +35 -0
  11. schemathesis/cli/commands/run/executor.py +138 -0
  12. schemathesis/cli/commands/run/filters.py +194 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +18 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  17. schemathesis/cli/commands/run/handlers/output.py +746 -0
  18. schemathesis/cli/commands/run/hypothesis.py +105 -0
  19. schemathesis/cli/commands/run/loaders.py +129 -0
  20. schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
  21. schemathesis/cli/constants.py +5 -52
  22. schemathesis/cli/core.py +17 -0
  23. schemathesis/cli/ext/fs.py +14 -0
  24. schemathesis/cli/ext/groups.py +55 -0
  25. schemathesis/cli/{options.py → ext/options.py} +39 -10
  26. schemathesis/cli/hooks.py +36 -0
  27. schemathesis/contrib/__init__.py +1 -3
  28. schemathesis/contrib/openapi/__init__.py +1 -3
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
  30. schemathesis/core/__init__.py +58 -0
  31. schemathesis/core/compat.py +25 -0
  32. schemathesis/core/control.py +2 -0
  33. schemathesis/core/curl.py +58 -0
  34. schemathesis/core/deserialization.py +65 -0
  35. schemathesis/core/errors.py +370 -0
  36. schemathesis/core/failures.py +285 -0
  37. schemathesis/core/fs.py +19 -0
  38. schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
  39. schemathesis/core/loaders.py +104 -0
  40. schemathesis/core/marks.py +66 -0
  41. schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
  42. schemathesis/core/output/__init__.py +69 -0
  43. schemathesis/core/output/sanitization.py +197 -0
  44. schemathesis/core/rate_limit.py +60 -0
  45. schemathesis/core/registries.py +31 -0
  46. schemathesis/{internal → core}/result.py +1 -1
  47. schemathesis/core/transforms.py +113 -0
  48. schemathesis/core/transport.py +108 -0
  49. schemathesis/core/validation.py +38 -0
  50. schemathesis/core/version.py +7 -0
  51. schemathesis/engine/__init__.py +30 -0
  52. schemathesis/engine/config.py +59 -0
  53. schemathesis/engine/context.py +119 -0
  54. schemathesis/engine/control.py +36 -0
  55. schemathesis/engine/core.py +157 -0
  56. schemathesis/engine/errors.py +394 -0
  57. schemathesis/engine/events.py +337 -0
  58. schemathesis/engine/phases/__init__.py +66 -0
  59. schemathesis/{cli → engine/phases}/probes.py +63 -70
  60. schemathesis/engine/phases/stateful/__init__.py +65 -0
  61. schemathesis/engine/phases/stateful/_executor.py +326 -0
  62. schemathesis/engine/phases/stateful/context.py +85 -0
  63. schemathesis/engine/phases/unit/__init__.py +174 -0
  64. schemathesis/engine/phases/unit/_executor.py +321 -0
  65. schemathesis/engine/phases/unit/_pool.py +74 -0
  66. schemathesis/engine/recorder.py +241 -0
  67. schemathesis/errors.py +31 -0
  68. schemathesis/experimental/__init__.py +18 -14
  69. schemathesis/filters.py +103 -14
  70. schemathesis/generation/__init__.py +21 -37
  71. schemathesis/generation/case.py +190 -0
  72. schemathesis/generation/coverage.py +931 -0
  73. schemathesis/generation/hypothesis/__init__.py +30 -0
  74. schemathesis/generation/hypothesis/builder.py +585 -0
  75. schemathesis/generation/hypothesis/examples.py +50 -0
  76. schemathesis/generation/hypothesis/given.py +66 -0
  77. schemathesis/generation/hypothesis/reporting.py +14 -0
  78. schemathesis/generation/hypothesis/strategies.py +16 -0
  79. schemathesis/generation/meta.py +115 -0
  80. schemathesis/generation/modes.py +28 -0
  81. schemathesis/generation/overrides.py +96 -0
  82. schemathesis/generation/stateful/__init__.py +20 -0
  83. schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
  84. schemathesis/generation/targets.py +69 -0
  85. schemathesis/graphql/__init__.py +15 -0
  86. schemathesis/graphql/checks.py +115 -0
  87. schemathesis/graphql/loaders.py +131 -0
  88. schemathesis/hooks.py +99 -67
  89. schemathesis/openapi/__init__.py +13 -0
  90. schemathesis/openapi/checks.py +412 -0
  91. schemathesis/openapi/generation/__init__.py +0 -0
  92. schemathesis/openapi/generation/filters.py +63 -0
  93. schemathesis/openapi/loaders.py +178 -0
  94. schemathesis/pytest/__init__.py +5 -0
  95. schemathesis/pytest/control_flow.py +7 -0
  96. schemathesis/pytest/lazy.py +273 -0
  97. schemathesis/pytest/loaders.py +12 -0
  98. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
  99. schemathesis/python/__init__.py +0 -0
  100. schemathesis/python/asgi.py +12 -0
  101. schemathesis/python/wsgi.py +12 -0
  102. schemathesis/schemas.py +537 -261
  103. schemathesis/specs/graphql/__init__.py +0 -1
  104. schemathesis/specs/graphql/_cache.py +25 -0
  105. schemathesis/specs/graphql/nodes.py +1 -0
  106. schemathesis/specs/graphql/scalars.py +7 -5
  107. schemathesis/specs/graphql/schemas.py +215 -187
  108. schemathesis/specs/graphql/validation.py +11 -18
  109. schemathesis/specs/openapi/__init__.py +7 -1
  110. schemathesis/specs/openapi/_cache.py +122 -0
  111. schemathesis/specs/openapi/_hypothesis.py +146 -165
  112. schemathesis/specs/openapi/checks.py +565 -67
  113. schemathesis/specs/openapi/converter.py +33 -6
  114. schemathesis/specs/openapi/definitions.py +11 -18
  115. schemathesis/specs/openapi/examples.py +153 -39
  116. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  117. schemathesis/specs/openapi/expressions/context.py +4 -6
  118. schemathesis/specs/openapi/expressions/extractors.py +23 -0
  119. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  120. schemathesis/specs/openapi/expressions/nodes.py +38 -14
  121. schemathesis/specs/openapi/expressions/parser.py +26 -5
  122. schemathesis/specs/openapi/formats.py +45 -0
  123. schemathesis/specs/openapi/links.py +65 -165
  124. schemathesis/specs/openapi/media_types.py +32 -0
  125. schemathesis/specs/openapi/negative/__init__.py +7 -3
  126. schemathesis/specs/openapi/negative/mutations.py +24 -8
  127. schemathesis/specs/openapi/parameters.py +46 -30
  128. schemathesis/specs/openapi/patterns.py +137 -0
  129. schemathesis/specs/openapi/references.py +47 -57
  130. schemathesis/specs/openapi/schemas.py +483 -367
  131. schemathesis/specs/openapi/security.py +25 -7
  132. schemathesis/specs/openapi/serialization.py +11 -6
  133. schemathesis/specs/openapi/stateful/__init__.py +185 -73
  134. schemathesis/specs/openapi/utils.py +6 -1
  135. schemathesis/transport/__init__.py +104 -0
  136. schemathesis/transport/asgi.py +26 -0
  137. schemathesis/transport/prepare.py +99 -0
  138. schemathesis/transport/requests.py +221 -0
  139. schemathesis/{_xml.py → transport/serialization.py} +143 -28
  140. schemathesis/transport/wsgi.py +165 -0
  141. schemathesis-4.0.0a1.dist-info/METADATA +297 -0
  142. schemathesis-4.0.0a1.dist-info/RECORD +152 -0
  143. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
  144. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
  145. schemathesis/_compat.py +0 -74
  146. schemathesis/_dependency_versions.py +0 -17
  147. schemathesis/_hypothesis.py +0 -246
  148. schemathesis/_override.py +0 -49
  149. schemathesis/cli/cassettes.py +0 -375
  150. schemathesis/cli/context.py +0 -55
  151. schemathesis/cli/debug.py +0 -26
  152. schemathesis/cli/handlers.py +0 -16
  153. schemathesis/cli/junitxml.py +0 -43
  154. schemathesis/cli/output/__init__.py +0 -1
  155. schemathesis/cli/output/default.py +0 -765
  156. schemathesis/cli/output/short.py +0 -40
  157. schemathesis/cli/sanitization.py +0 -20
  158. schemathesis/code_samples.py +0 -149
  159. schemathesis/constants.py +0 -55
  160. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  161. schemathesis/contrib/openapi/formats/uuid.py +0 -15
  162. schemathesis/contrib/unique_data.py +0 -41
  163. schemathesis/exceptions.py +0 -560
  164. schemathesis/extra/_aiohttp.py +0 -27
  165. schemathesis/extra/_flask.py +0 -10
  166. schemathesis/extra/_server.py +0 -17
  167. schemathesis/failures.py +0 -209
  168. schemathesis/fixups/__init__.py +0 -36
  169. schemathesis/fixups/fast_api.py +0 -41
  170. schemathesis/fixups/utf8_bom.py +0 -29
  171. schemathesis/graphql.py +0 -4
  172. schemathesis/internal/__init__.py +0 -7
  173. schemathesis/internal/copy.py +0 -13
  174. schemathesis/internal/datetime.py +0 -5
  175. schemathesis/internal/deprecation.py +0 -34
  176. schemathesis/internal/jsonschema.py +0 -35
  177. schemathesis/internal/transformation.py +0 -15
  178. schemathesis/internal/validation.py +0 -34
  179. schemathesis/lazy.py +0 -361
  180. schemathesis/loaders.py +0 -120
  181. schemathesis/models.py +0 -1231
  182. schemathesis/parameters.py +0 -86
  183. schemathesis/runner/__init__.py +0 -555
  184. schemathesis/runner/events.py +0 -309
  185. schemathesis/runner/impl/__init__.py +0 -3
  186. schemathesis/runner/impl/core.py +0 -986
  187. schemathesis/runner/impl/solo.py +0 -90
  188. schemathesis/runner/impl/threadpool.py +0 -400
  189. schemathesis/runner/serialization.py +0 -411
  190. schemathesis/sanitization.py +0 -248
  191. schemathesis/serializers.py +0 -315
  192. schemathesis/service/__init__.py +0 -18
  193. schemathesis/service/auth.py +0 -11
  194. schemathesis/service/ci.py +0 -201
  195. schemathesis/service/client.py +0 -100
  196. schemathesis/service/constants.py +0 -38
  197. schemathesis/service/events.py +0 -57
  198. schemathesis/service/hosts.py +0 -107
  199. schemathesis/service/metadata.py +0 -46
  200. schemathesis/service/models.py +0 -49
  201. schemathesis/service/report.py +0 -255
  202. schemathesis/service/serialization.py +0 -184
  203. schemathesis/service/usage.py +0 -65
  204. schemathesis/specs/graphql/loaders.py +0 -344
  205. schemathesis/specs/openapi/filters.py +0 -49
  206. schemathesis/specs/openapi/loaders.py +0 -667
  207. schemathesis/specs/openapi/stateful/links.py +0 -92
  208. schemathesis/specs/openapi/validation.py +0 -25
  209. schemathesis/stateful/__init__.py +0 -133
  210. schemathesis/targets.py +0 -45
  211. schemathesis/throttling.py +0 -41
  212. schemathesis/transports/__init__.py +0 -5
  213. schemathesis/transports/auth.py +0 -15
  214. schemathesis/transports/headers.py +0 -35
  215. schemathesis/transports/responses.py +0 -52
  216. schemathesis/types.py +0 -35
  217. schemathesis/utils.py +0 -169
  218. schemathesis-3.25.5.dist-info/METADATA +0 -356
  219. schemathesis-3.25.5.dist-info/RECORD +0 -134
  220. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  221. {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 asdict, dataclass
13
- from typing import TYPE_CHECKING, Any
13
+ from dataclasses import dataclass
14
+ from typing import TYPE_CHECKING
14
15
 
15
- from ..constants import USER_AGENT
16
- from ..exceptions import format_exception
17
- from ..models import Request, Response
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 ..schemas import BaseSchema
25
- from . import LoaderConfig
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, config: LoaderConfig
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) -> ProbeResultType:
76
+ def analyze_response(self, response: requests.Response) -> ProbeOutcome:
43
77
  raise NotImplementedError
44
78
 
45
79
 
46
- class ProbeResultType(str, enum.Enum):
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 ProbeResult:
59
- """Result of a probe."""
60
-
92
+ class ProbeRun:
61
93
  probe: Probe
62
- type: ProbeResultType
94
+ outcome: ProbeOutcome
63
95
  request: requests.PreparedRequest | None = None
64
96
  response: requests.Response | None = None
65
- error: requests.RequestException | None = None
97
+ error: Exception | None = None
66
98
 
67
99
  @property
68
100
  def is_failure(self) -> bool:
69
- return self.type == ProbeResultType.FAILURE
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 = "NULL_BYTE_IN_HEADER"
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, config: LoaderConfig
111
+ self, session: requests.Session, request: requests.Request, schema: BaseSchema
105
112
  ) -> requests.PreparedRequest:
106
113
  request.method = "GET"
107
- request.url = config.base_url or schema.get_base_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) -> ProbeResultType:
118
+ def analyze_response(self, response: requests.Response) -> ProbeOutcome:
112
119
  if response.status_code == 400:
113
- return ProbeResultType.FAILURE
114
- return ProbeResultType.SUCCESS
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: LoaderConfig) -> ProbeResult:
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, PreparedRequest
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, config)
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 ProbeResult(probe, ProbeResultType.SKIP, None, None, None)
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 ProbeResult(probe, ProbeResultType.ERROR, req, None, exc)
146
+ return ProbeRun(probe, ProbeOutcome.ERROR, req, None, exc)
140
147
  result_type = probe.analyze_response(response)
141
- return ProbeResult(probe, result_type, request, response)
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)