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.
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 -1760
  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/{runner → engine/phases}/probes.py +50 -67
  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 +139 -23
  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 +478 -369
  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.6.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
  144. {schemathesis-3.25.6.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 -58
  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 -790
  156. schemathesis/cli/output/short.py +0 -44
  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 -1234
  182. schemathesis/parameters.py +0 -86
  183. schemathesis/runner/__init__.py +0 -570
  184. schemathesis/runner/events.py +0 -329
  185. schemathesis/runner/impl/__init__.py +0 -3
  186. schemathesis/runner/impl/core.py +0 -1035
  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 -323
  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 -199
  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.6.dist-info/METADATA +0 -356
  219. schemathesis-3.25.6.dist-info/RECORD +0 -134
  220. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  221. {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 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 ..types import RequestCert
25
- from ..schemas import BaseSchema
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
- HEADER_NAME = "X-Schemathesis-Probe"
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
- @dataclass
32
- class ProbeConfig:
33
- base_url: str | None = None
34
- request_tls_verify: bool | str = True
35
- request_proxy: str | None = None
36
- request_cert: RequestCert | None = None
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, config: ProbeConfig
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: requests.RequestException | None = None
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 = "NULL_BYTE_IN_HEADER"
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, config: ProbeConfig
111
+ self, session: requests.Session, request: requests.Request, schema: BaseSchema
114
112
  ) -> requests.PreparedRequest:
115
113
  request.method = "GET"
116
- request.url = config.base_url or schema.get_base_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: ProbeConfig) -> ProbeRun:
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, config)
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)