schemathesis 3.13.0__py3-none-any.whl → 4.4.2__py3-none-any.whl

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