schemathesis 3.39.15__py3-none-any.whl → 4.0.0__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 (255) hide show
  1. schemathesis/__init__.py +41 -79
  2. schemathesis/auths.py +111 -122
  3. schemathesis/checks.py +169 -60
  4. schemathesis/cli/__init__.py +15 -2117
  5. schemathesis/cli/commands/__init__.py +85 -0
  6. schemathesis/cli/commands/data.py +10 -0
  7. schemathesis/cli/commands/run/__init__.py +590 -0
  8. schemathesis/cli/commands/run/context.py +204 -0
  9. schemathesis/cli/commands/run/events.py +60 -0
  10. schemathesis/cli/commands/run/executor.py +157 -0
  11. schemathesis/cli/commands/run/filters.py +53 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
  15. schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1628 -0
  17. schemathesis/cli/commands/run/loaders.py +114 -0
  18. schemathesis/cli/commands/run/validation.py +246 -0
  19. schemathesis/cli/constants.py +5 -58
  20. schemathesis/cli/core.py +19 -0
  21. schemathesis/cli/ext/fs.py +16 -0
  22. schemathesis/cli/ext/groups.py +84 -0
  23. schemathesis/cli/{options.py → ext/options.py} +36 -34
  24. schemathesis/config/__init__.py +189 -0
  25. schemathesis/config/_auth.py +51 -0
  26. schemathesis/config/_checks.py +268 -0
  27. schemathesis/config/_diff_base.py +99 -0
  28. schemathesis/config/_env.py +21 -0
  29. schemathesis/config/_error.py +156 -0
  30. schemathesis/config/_generation.py +149 -0
  31. schemathesis/config/_health_check.py +24 -0
  32. schemathesis/config/_operations.py +327 -0
  33. schemathesis/config/_output.py +171 -0
  34. schemathesis/config/_parameters.py +19 -0
  35. schemathesis/config/_phases.py +187 -0
  36. schemathesis/config/_projects.py +527 -0
  37. schemathesis/config/_rate_limit.py +17 -0
  38. schemathesis/config/_report.py +120 -0
  39. schemathesis/config/_validator.py +9 -0
  40. schemathesis/config/_warnings.py +25 -0
  41. schemathesis/config/schema.json +885 -0
  42. schemathesis/core/__init__.py +67 -0
  43. schemathesis/core/compat.py +32 -0
  44. schemathesis/core/control.py +2 -0
  45. schemathesis/core/curl.py +58 -0
  46. schemathesis/core/deserialization.py +65 -0
  47. schemathesis/core/errors.py +459 -0
  48. schemathesis/core/failures.py +315 -0
  49. schemathesis/core/fs.py +19 -0
  50. schemathesis/core/hooks.py +20 -0
  51. schemathesis/core/loaders.py +104 -0
  52. schemathesis/core/marks.py +66 -0
  53. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  54. schemathesis/core/output/__init__.py +46 -0
  55. schemathesis/core/output/sanitization.py +54 -0
  56. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  57. schemathesis/core/registries.py +31 -0
  58. schemathesis/core/transforms.py +113 -0
  59. schemathesis/core/transport.py +223 -0
  60. schemathesis/core/validation.py +54 -0
  61. schemathesis/core/version.py +7 -0
  62. schemathesis/engine/__init__.py +28 -0
  63. schemathesis/engine/context.py +118 -0
  64. schemathesis/engine/control.py +36 -0
  65. schemathesis/engine/core.py +169 -0
  66. schemathesis/engine/errors.py +464 -0
  67. schemathesis/engine/events.py +258 -0
  68. schemathesis/engine/phases/__init__.py +88 -0
  69. schemathesis/{runner → engine/phases}/probes.py +52 -68
  70. schemathesis/engine/phases/stateful/__init__.py +68 -0
  71. schemathesis/engine/phases/stateful/_executor.py +356 -0
  72. schemathesis/engine/phases/stateful/context.py +85 -0
  73. schemathesis/engine/phases/unit/__init__.py +212 -0
  74. schemathesis/engine/phases/unit/_executor.py +416 -0
  75. schemathesis/engine/phases/unit/_pool.py +82 -0
  76. schemathesis/engine/recorder.py +247 -0
  77. schemathesis/errors.py +43 -0
  78. schemathesis/filters.py +17 -98
  79. schemathesis/generation/__init__.py +5 -33
  80. schemathesis/generation/case.py +317 -0
  81. schemathesis/generation/coverage.py +282 -175
  82. schemathesis/generation/hypothesis/__init__.py +36 -0
  83. schemathesis/generation/hypothesis/builder.py +800 -0
  84. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  85. schemathesis/generation/hypothesis/given.py +66 -0
  86. schemathesis/generation/hypothesis/reporting.py +14 -0
  87. schemathesis/generation/hypothesis/strategies.py +16 -0
  88. schemathesis/generation/meta.py +115 -0
  89. schemathesis/generation/metrics.py +93 -0
  90. schemathesis/generation/modes.py +20 -0
  91. schemathesis/generation/overrides.py +116 -0
  92. schemathesis/generation/stateful/__init__.py +37 -0
  93. schemathesis/generation/stateful/state_machine.py +278 -0
  94. schemathesis/graphql/__init__.py +15 -0
  95. schemathesis/graphql/checks.py +109 -0
  96. schemathesis/graphql/loaders.py +284 -0
  97. schemathesis/hooks.py +80 -101
  98. schemathesis/openapi/__init__.py +13 -0
  99. schemathesis/openapi/checks.py +455 -0
  100. schemathesis/openapi/generation/__init__.py +0 -0
  101. schemathesis/openapi/generation/filters.py +72 -0
  102. schemathesis/openapi/loaders.py +313 -0
  103. schemathesis/pytest/__init__.py +5 -0
  104. schemathesis/pytest/control_flow.py +7 -0
  105. schemathesis/pytest/lazy.py +281 -0
  106. schemathesis/pytest/loaders.py +36 -0
  107. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
  108. schemathesis/python/__init__.py +0 -0
  109. schemathesis/python/asgi.py +12 -0
  110. schemathesis/python/wsgi.py +12 -0
  111. schemathesis/schemas.py +537 -273
  112. schemathesis/specs/graphql/__init__.py +0 -1
  113. schemathesis/specs/graphql/_cache.py +1 -2
  114. schemathesis/specs/graphql/scalars.py +42 -6
  115. schemathesis/specs/graphql/schemas.py +141 -137
  116. schemathesis/specs/graphql/validation.py +11 -17
  117. schemathesis/specs/openapi/__init__.py +6 -1
  118. schemathesis/specs/openapi/_cache.py +1 -2
  119. schemathesis/specs/openapi/_hypothesis.py +142 -156
  120. schemathesis/specs/openapi/checks.py +368 -257
  121. schemathesis/specs/openapi/converter.py +4 -4
  122. schemathesis/specs/openapi/definitions.py +1 -1
  123. schemathesis/specs/openapi/examples.py +23 -21
  124. schemathesis/specs/openapi/expressions/__init__.py +31 -19
  125. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  126. schemathesis/specs/openapi/expressions/lexer.py +1 -1
  127. schemathesis/specs/openapi/expressions/nodes.py +36 -41
  128. schemathesis/specs/openapi/expressions/parser.py +1 -1
  129. schemathesis/specs/openapi/formats.py +35 -7
  130. schemathesis/specs/openapi/media_types.py +53 -12
  131. schemathesis/specs/openapi/negative/__init__.py +7 -4
  132. schemathesis/specs/openapi/negative/mutations.py +6 -5
  133. schemathesis/specs/openapi/parameters.py +7 -10
  134. schemathesis/specs/openapi/patterns.py +94 -31
  135. schemathesis/specs/openapi/references.py +12 -53
  136. schemathesis/specs/openapi/schemas.py +238 -308
  137. schemathesis/specs/openapi/security.py +1 -1
  138. schemathesis/specs/openapi/serialization.py +12 -6
  139. schemathesis/specs/openapi/stateful/__init__.py +268 -133
  140. schemathesis/specs/openapi/stateful/control.py +87 -0
  141. schemathesis/specs/openapi/stateful/links.py +209 -0
  142. schemathesis/transport/__init__.py +142 -0
  143. schemathesis/transport/asgi.py +26 -0
  144. schemathesis/transport/prepare.py +124 -0
  145. schemathesis/transport/requests.py +244 -0
  146. schemathesis/{_xml.py → transport/serialization.py} +69 -11
  147. schemathesis/transport/wsgi.py +171 -0
  148. schemathesis-4.0.0.dist-info/METADATA +204 -0
  149. schemathesis-4.0.0.dist-info/RECORD +164 -0
  150. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
  151. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
  152. schemathesis/_compat.py +0 -74
  153. schemathesis/_dependency_versions.py +0 -19
  154. schemathesis/_hypothesis.py +0 -712
  155. schemathesis/_override.py +0 -50
  156. schemathesis/_patches.py +0 -21
  157. schemathesis/_rate_limiter.py +0 -7
  158. schemathesis/cli/callbacks.py +0 -466
  159. schemathesis/cli/cassettes.py +0 -561
  160. schemathesis/cli/context.py +0 -75
  161. schemathesis/cli/debug.py +0 -27
  162. schemathesis/cli/handlers.py +0 -19
  163. schemathesis/cli/junitxml.py +0 -124
  164. schemathesis/cli/output/__init__.py +0 -1
  165. schemathesis/cli/output/default.py +0 -920
  166. schemathesis/cli/output/short.py +0 -59
  167. schemathesis/cli/reporting.py +0 -79
  168. schemathesis/cli/sanitization.py +0 -26
  169. schemathesis/code_samples.py +0 -151
  170. schemathesis/constants.py +0 -54
  171. schemathesis/contrib/__init__.py +0 -11
  172. schemathesis/contrib/openapi/__init__.py +0 -11
  173. schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
  174. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  175. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  176. schemathesis/contrib/unique_data.py +0 -41
  177. schemathesis/exceptions.py +0 -571
  178. schemathesis/experimental/__init__.py +0 -109
  179. schemathesis/extra/_aiohttp.py +0 -28
  180. schemathesis/extra/_flask.py +0 -13
  181. schemathesis/extra/_server.py +0 -18
  182. schemathesis/failures.py +0 -284
  183. schemathesis/fixups/__init__.py +0 -37
  184. schemathesis/fixups/fast_api.py +0 -41
  185. schemathesis/fixups/utf8_bom.py +0 -28
  186. schemathesis/generation/_methods.py +0 -44
  187. schemathesis/graphql.py +0 -3
  188. schemathesis/internal/__init__.py +0 -7
  189. schemathesis/internal/checks.py +0 -86
  190. schemathesis/internal/copy.py +0 -32
  191. schemathesis/internal/datetime.py +0 -5
  192. schemathesis/internal/deprecation.py +0 -37
  193. schemathesis/internal/diff.py +0 -15
  194. schemathesis/internal/extensions.py +0 -27
  195. schemathesis/internal/jsonschema.py +0 -36
  196. schemathesis/internal/output.py +0 -68
  197. schemathesis/internal/transformation.py +0 -26
  198. schemathesis/internal/validation.py +0 -34
  199. schemathesis/lazy.py +0 -474
  200. schemathesis/loaders.py +0 -122
  201. schemathesis/models.py +0 -1341
  202. schemathesis/parameters.py +0 -90
  203. schemathesis/runner/__init__.py +0 -605
  204. schemathesis/runner/events.py +0 -389
  205. schemathesis/runner/impl/__init__.py +0 -3
  206. schemathesis/runner/impl/context.py +0 -88
  207. schemathesis/runner/impl/core.py +0 -1280
  208. schemathesis/runner/impl/solo.py +0 -80
  209. schemathesis/runner/impl/threadpool.py +0 -391
  210. schemathesis/runner/serialization.py +0 -544
  211. schemathesis/sanitization.py +0 -252
  212. schemathesis/serializers.py +0 -328
  213. schemathesis/service/__init__.py +0 -18
  214. schemathesis/service/auth.py +0 -11
  215. schemathesis/service/ci.py +0 -202
  216. schemathesis/service/client.py +0 -133
  217. schemathesis/service/constants.py +0 -38
  218. schemathesis/service/events.py +0 -61
  219. schemathesis/service/extensions.py +0 -224
  220. schemathesis/service/hosts.py +0 -111
  221. schemathesis/service/metadata.py +0 -71
  222. schemathesis/service/models.py +0 -258
  223. schemathesis/service/report.py +0 -255
  224. schemathesis/service/serialization.py +0 -173
  225. schemathesis/service/usage.py +0 -66
  226. schemathesis/specs/graphql/loaders.py +0 -364
  227. schemathesis/specs/openapi/expressions/context.py +0 -16
  228. schemathesis/specs/openapi/links.py +0 -389
  229. schemathesis/specs/openapi/loaders.py +0 -707
  230. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  231. schemathesis/specs/openapi/stateful/types.py +0 -14
  232. schemathesis/specs/openapi/validation.py +0 -26
  233. schemathesis/stateful/__init__.py +0 -147
  234. schemathesis/stateful/config.py +0 -97
  235. schemathesis/stateful/context.py +0 -135
  236. schemathesis/stateful/events.py +0 -274
  237. schemathesis/stateful/runner.py +0 -309
  238. schemathesis/stateful/sink.py +0 -68
  239. schemathesis/stateful/state_machine.py +0 -328
  240. schemathesis/stateful/statistic.py +0 -22
  241. schemathesis/stateful/validation.py +0 -100
  242. schemathesis/targets.py +0 -77
  243. schemathesis/transports/__init__.py +0 -369
  244. schemathesis/transports/asgi.py +0 -7
  245. schemathesis/transports/auth.py +0 -38
  246. schemathesis/transports/headers.py +0 -36
  247. schemathesis/transports/responses.py +0 -57
  248. schemathesis/types.py +0 -44
  249. schemathesis/utils.py +0 -164
  250. schemathesis-3.39.15.dist-info/METADATA +0 -293
  251. schemathesis-3.39.15.dist-info/RECORD +0 -160
  252. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  253. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  254. /schemathesis/{internal → core}/result.py +0 -0
  255. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,258 @@
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.engine.errors import EngineErrorInfo
10
+ from schemathesis.engine.phases import Phase, PhaseName
11
+ from schemathesis.engine.recorder import ScenarioRecorder
12
+
13
+ if TYPE_CHECKING:
14
+ from schemathesis.engine import Status
15
+ from schemathesis.engine.phases.probes import ProbePayload
16
+
17
+ EventGenerator = Generator["EngineEvent", None, None]
18
+
19
+
20
+ @dataclass
21
+ class EngineEvent:
22
+ """An event within the engine's lifecycle."""
23
+
24
+ id: uuid.UUID
25
+ timestamp: float
26
+ # Indicates whether this event is the last in the event stream
27
+ is_terminal = False
28
+
29
+
30
+ @dataclass
31
+ class EngineStarted(EngineEvent):
32
+ """Start of an engine."""
33
+
34
+ __slots__ = ("id", "timestamp")
35
+
36
+ def __init__(self) -> None:
37
+ self.id = uuid.uuid4()
38
+ self.timestamp = time.time()
39
+
40
+
41
+ @dataclass
42
+ class PhaseEvent(EngineEvent):
43
+ """Event associated with a specific execution phase."""
44
+
45
+ phase: Phase
46
+
47
+
48
+ @dataclass
49
+ class PhaseStarted(PhaseEvent):
50
+ """Start of an execution phase."""
51
+
52
+ __slots__ = ("id", "timestamp", "phase")
53
+
54
+ def __init__(self, *, phase: Phase) -> None:
55
+ self.id = uuid.uuid4()
56
+ self.timestamp = time.time()
57
+ self.phase = phase
58
+
59
+
60
+ @dataclass
61
+ class PhaseFinished(PhaseEvent):
62
+ """End of an execution phase."""
63
+
64
+ status: Status
65
+ payload: Result[ProbePayload, Exception] | None
66
+
67
+ __slots__ = ("id", "timestamp", "phase", "status", "payload")
68
+
69
+ def __init__(self, *, phase: Phase, status: Status, payload: Result[ProbePayload, Exception] | None) -> None:
70
+ self.id = uuid.uuid4()
71
+ self.timestamp = time.time()
72
+ self.phase = phase
73
+ self.status = status
74
+ self.payload = payload
75
+
76
+
77
+ @dataclass
78
+ class TestEvent(EngineEvent):
79
+ phase: PhaseName
80
+
81
+
82
+ @dataclass
83
+ class SuiteStarted(TestEvent):
84
+ """Before executing a set of scenarios."""
85
+
86
+ __slots__ = ("id", "timestamp", "phase")
87
+
88
+ def __init__(self, *, phase: PhaseName) -> None:
89
+ self.id = uuid.uuid4()
90
+ self.timestamp = time.time()
91
+ self.phase = phase
92
+
93
+
94
+ @dataclass
95
+ class SuiteFinished(TestEvent):
96
+ """After executing a set of test scenarios."""
97
+
98
+ status: Status
99
+
100
+ __slots__ = ("id", "timestamp", "phase", "status")
101
+
102
+ def __init__(self, *, id: uuid.UUID, phase: PhaseName, status: Status) -> None:
103
+ self.id = id
104
+ self.timestamp = time.time()
105
+ self.phase = phase
106
+ self.status = status
107
+
108
+
109
+ @dataclass
110
+ class ScenarioEvent(TestEvent):
111
+ suite_id: uuid.UUID
112
+
113
+
114
+ @dataclass
115
+ class ScenarioStarted(ScenarioEvent):
116
+ """Before executing a grouped set of test steps."""
117
+
118
+ __slots__ = ("id", "timestamp", "phase", "suite_id", "label")
119
+
120
+ def __init__(self, *, phase: PhaseName, suite_id: uuid.UUID, label: str | None) -> None:
121
+ self.id = uuid.uuid4()
122
+ self.timestamp = time.time()
123
+ self.phase = phase
124
+ self.suite_id = suite_id
125
+ self.label = label
126
+
127
+
128
+ @dataclass
129
+ class ScenarioFinished(ScenarioEvent):
130
+ """After executing a grouped set of test steps."""
131
+
132
+ status: Status
133
+ recorder: ScenarioRecorder
134
+ elapsed_time: float
135
+ skip_reason: str | None
136
+ # Whether this is a scenario that tries to reproduce a failure
137
+ is_final: bool
138
+
139
+ __slots__ = (
140
+ "id",
141
+ "timestamp",
142
+ "phase",
143
+ "suite_id",
144
+ "label",
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
+ label: str | None,
159
+ status: Status,
160
+ recorder: ScenarioRecorder,
161
+ elapsed_time: float,
162
+ skip_reason: str | None,
163
+ is_final: bool,
164
+ ) -> None:
165
+ self.id = id
166
+ self.timestamp = time.time()
167
+ self.phase = phase
168
+ self.suite_id = suite_id
169
+ self.label = label
170
+ self.status = status
171
+ self.recorder = recorder
172
+ self.elapsed_time = elapsed_time
173
+ self.skip_reason = skip_reason
174
+ self.is_final = is_final
175
+
176
+
177
+ @dataclass
178
+ class Interrupted(EngineEvent):
179
+ """If execution was interrupted by Ctrl-C, or a received SIGTERM."""
180
+
181
+ phase: PhaseName | None
182
+
183
+ __slots__ = ("id", "timestamp", "phase")
184
+
185
+ def __init__(self, *, phase: PhaseName | None) -> None:
186
+ self.id = uuid.uuid4()
187
+ self.timestamp = time.time()
188
+ self.phase = phase
189
+
190
+
191
+ @dataclass
192
+ class NonFatalError(EngineEvent):
193
+ """Error that doesn't halt execution but should be reported."""
194
+
195
+ info: EngineErrorInfo
196
+ value: Exception
197
+ phase: PhaseName
198
+ label: str
199
+ related_to_operation: bool
200
+
201
+ __slots__ = ("id", "timestamp", "info", "value", "phase", "label", "related_to_operation")
202
+
203
+ def __init__(
204
+ self,
205
+ *,
206
+ error: Exception,
207
+ phase: PhaseName,
208
+ label: str,
209
+ related_to_operation: bool,
210
+ code_sample: str | None = None,
211
+ ) -> None:
212
+ self.id = uuid.uuid4()
213
+ self.timestamp = time.time()
214
+ self.info = EngineErrorInfo(error=error, code_sample=code_sample)
215
+ self.value = error
216
+ self.phase = phase
217
+ self.label = label
218
+ self.related_to_operation = related_to_operation
219
+
220
+ def __eq__(self, other: object) -> bool:
221
+ assert isinstance(other, NonFatalError)
222
+ return self.label == other.label and type(self.value) is type(other.value)
223
+
224
+ def __hash__(self) -> int:
225
+ return hash((self.label, type(self.value)))
226
+
227
+
228
+ @dataclass
229
+ class FatalError(EngineEvent):
230
+ """Internal error in the engine."""
231
+
232
+ exception: Exception
233
+ is_terminal = True
234
+
235
+ __slots__ = ("id", "timestamp", "exception")
236
+
237
+ def __init__(self, *, exception: Exception) -> None:
238
+ self.id = uuid.uuid4()
239
+ self.timestamp = time.time()
240
+ self.exception = exception
241
+
242
+
243
+ @dataclass
244
+ class EngineFinished(EngineEvent):
245
+ """The final event of the run.
246
+
247
+ No more events after this point.
248
+ """
249
+
250
+ is_terminal = True
251
+ running_time: float
252
+
253
+ __slots__ = ("id", "timestamp", "running_time")
254
+
255
+ def __init__(self, *, running_time: float) -> None:
256
+ self.id = uuid.uuid4()
257
+ self.timestamp = time.time()
258
+ self.running_time = running_time
@@ -0,0 +1,88 @@
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
+ EXAMPLES = "Examples"
18
+ COVERAGE = "Coverage"
19
+ FUZZING = "Fuzzing"
20
+ STATEFUL_TESTING = "Stateful"
21
+
22
+ @classmethod
23
+ def defaults(cls) -> list[PhaseName]:
24
+ return [PhaseName.EXAMPLES, PhaseName.COVERAGE, PhaseName.FUZZING, PhaseName.STATEFUL_TESTING]
25
+
26
+ @property
27
+ def name(self) -> str:
28
+ return {
29
+ PhaseName.PROBING: "probing",
30
+ PhaseName.EXAMPLES: "examples",
31
+ PhaseName.COVERAGE: "coverage",
32
+ PhaseName.FUZZING: "fuzzing",
33
+ PhaseName.STATEFUL_TESTING: "stateful",
34
+ }[self]
35
+
36
+ @classmethod
37
+ def from_str(cls, value: str) -> PhaseName:
38
+ return {
39
+ "probing": cls.PROBING,
40
+ "examples": cls.EXAMPLES,
41
+ "coverage": cls.COVERAGE,
42
+ "fuzzing": cls.FUZZING,
43
+ "stateful": cls.STATEFUL_TESTING,
44
+ }[value.lower()]
45
+
46
+
47
+ class PhaseSkipReason(str, enum.Enum):
48
+ """Reasons why a phase might not be executed."""
49
+
50
+ DISABLED = "disabled" # Explicitly disabled via config
51
+ NOT_SUPPORTED = "not supported" # Feature not supported by schema
52
+ NOT_APPLICABLE = "not applicable" # No relevant data (e.g., no links for stateful)
53
+ FAILURE_LIMIT_REACHED = "failure limit reached"
54
+ NOTHING_TO_TEST = "nothing to test"
55
+
56
+
57
+ @dataclass
58
+ class Phase:
59
+ """A logically separate engine execution phase."""
60
+
61
+ name: PhaseName
62
+ is_supported: bool
63
+ is_enabled: bool = True
64
+ skip_reason: PhaseSkipReason | None = None
65
+
66
+ def should_execute(self, ctx: EngineContext) -> bool:
67
+ """Determine if phase should run based on context & configuration."""
68
+ return self.is_enabled and not ctx.has_to_stop
69
+
70
+
71
+ def execute(ctx: EngineContext, phase: Phase) -> EventGenerator:
72
+ from urllib3.exceptions import InsecureRequestWarning
73
+
74
+ from . import probes, stateful, unit
75
+
76
+ with warnings.catch_warnings():
77
+ warnings.simplefilter("ignore", InsecureRequestWarning)
78
+
79
+ if phase.name == PhaseName.PROBING:
80
+ yield from probes.execute(ctx, phase)
81
+ elif phase.name == PhaseName.EXAMPLES:
82
+ yield from unit.execute(ctx, phase)
83
+ elif phase.name == PhaseName.COVERAGE:
84
+ yield from unit.execute(ctx, phase)
85
+ elif phase.name == PhaseName.FUZZING:
86
+ yield from unit.execute(ctx, phase)
87
+ elif phase.name == PhaseName.STATEFUL_TESTING:
88
+ yield from stateful.execute(ctx, phase)
@@ -10,32 +10,56 @@ from __future__ import annotations
10
10
 
11
11
  import enum
12
12
  import warnings
13
- from dataclasses import asdict, dataclass, field
14
- from typing import TYPE_CHECKING, Any
13
+ from dataclasses import dataclass
14
+ from typing import TYPE_CHECKING
15
15
 
16
- from ..constants import USER_AGENT
17
- from ..exceptions import format_exception
18
- from ..models import Request, Response
19
- from ..sanitization import sanitize_request, sanitize_response
20
- from ..transports import RequestConfig
21
- 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
19
+ from schemathesis.transport.prepare import get_default_headers
22
20
 
23
21
  if TYPE_CHECKING:
24
22
  import requests
25
23
 
26
- from ..schemas import BaseSchema
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
27
28
 
28
29
 
29
- 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)
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(exclude_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)
30
55
 
31
56
 
32
- @dataclass
33
- class ProbeConfig:
34
- base_url: str | None = None
35
- request: RequestConfig = field(default_factory=RequestConfig)
36
- auth: tuple[str, str] | None = None
37
- auth_type: str | None = None
38
- headers: dict[str, str] | None = None
57
+ def run(ctx: EngineContext) -> list[ProbeRun]:
58
+ """Run all probes against the given schema."""
59
+ return [send(probe(), ctx) for probe in PROBES]
60
+
61
+
62
+ HEADER_NAME = "X-Schemathesis-Probe"
39
63
 
40
64
 
41
65
  @dataclass
@@ -45,7 +69,7 @@ class Probe:
45
69
  name: str
46
70
 
47
71
  def prepare_request(
48
- self, session: requests.Session, request: requests.Request, schema: BaseSchema, config: ProbeConfig
72
+ self, session: requests.Session, request: requests.Request, schema: BaseSchema
49
73
  ) -> requests.PreparedRequest:
50
74
  raise NotImplementedError
51
75
 
@@ -70,49 +94,24 @@ class ProbeRun:
70
94
  outcome: ProbeOutcome
71
95
  request: requests.PreparedRequest | None = None
72
96
  response: requests.Response | None = None
73
- error: requests.RequestException | None = None
97
+ error: Exception | None = None
74
98
 
75
99
  @property
76
100
  def is_failure(self) -> bool:
77
101
  return self.outcome == ProbeOutcome.FAILURE
78
102
 
79
- def serialize(self) -> dict[str, Any]:
80
- """Serialize probe results so it can be sent over the network."""
81
- if self.request:
82
- _request = Request.from_prepared_request(self.request)
83
- sanitize_request(_request)
84
- request = asdict(_request)
85
- else:
86
- request = None
87
- if self.response:
88
- sanitize_response(self.response)
89
- response = asdict(Response.from_requests(self.response))
90
- else:
91
- response = None
92
- if self.error:
93
- error = format_exception(self.error)
94
- else:
95
- error = None
96
- return {
97
- "name": self.probe.name,
98
- "outcome": self.outcome.value,
99
- "request": request,
100
- "response": response,
101
- "error": error,
102
- }
103
-
104
103
 
105
104
  @dataclass
106
105
  class NullByteInHeader(Probe):
107
106
  """Support NULL bytes in headers."""
108
107
 
109
- name: str = "NULL_BYTE_IN_HEADER"
108
+ name: str = "Supports NULL byte in headers"
110
109
 
111
110
  def prepare_request(
112
- self, session: requests.Session, request: requests.Request, schema: BaseSchema, config: ProbeConfig
111
+ self, session: requests.Session, request: requests.Request, schema: BaseSchema
113
112
  ) -> requests.PreparedRequest:
114
113
  request.method = "GET"
115
- request.url = config.base_url or schema.get_base_url()
114
+ request.url = schema.get_base_url()
116
115
  request.headers = {"X-Schemathesis-Probe-Null": "\x00"}
117
116
  return session.prepare_request(request)
118
117
 
@@ -125,22 +124,22 @@ class NullByteInHeader(Probe):
125
124
  PROBES = (NullByteInHeader,)
126
125
 
127
126
 
128
- def send(probe: Probe, session: requests.Session, schema: BaseSchema, config: ProbeConfig) -> ProbeRun:
127
+ def send(probe: Probe, ctx: EngineContext) -> ProbeRun:
129
128
  """Send the probe to the application."""
130
129
  from requests import PreparedRequest, Request, RequestException
131
130
  from requests.exceptions import MissingSchema
132
131
  from urllib3.exceptions import InsecureRequestWarning
133
132
 
134
133
  try:
135
- request = probe.prepare_request(session, Request(), schema, config)
134
+ session = ctx.get_session()
135
+ request = probe.prepare_request(session, Request(), ctx.schema)
136
136
  request.headers[HEADER_NAME] = probe.name
137
137
  request.headers["User-Agent"] = USER_AGENT
138
- kwargs: dict[str, Any] = {"timeout": config.request.prepared_timeout or 2}
139
- if config.request.proxy is not None:
140
- kwargs["proxies"] = {"all": config.request.proxy}
138
+ for header, value in get_default_headers().items():
139
+ request.headers.setdefault(header, value)
141
140
  with warnings.catch_warnings():
142
141
  warnings.simplefilter("ignore", InsecureRequestWarning)
143
- response = session.send(request, **kwargs)
142
+ response = session.send(request, timeout=ctx.config.request_timeout or 2)
144
143
  except MissingSchema:
145
144
  # In-process ASGI/WSGI testing will have local URLs and requires extra handling
146
145
  # which is not currently implemented
@@ -150,18 +149,3 @@ def send(probe: Probe, session: requests.Session, schema: BaseSchema, config: Pr
150
149
  return ProbeRun(probe, ProbeOutcome.ERROR, req, None, exc)
151
150
  result_type = probe.analyze_response(response)
152
151
  return ProbeRun(probe, result_type, request, response)
153
-
154
-
155
- def run(schema: BaseSchema, config: ProbeConfig) -> list[ProbeRun]:
156
- """Run all probes against the given schema."""
157
- from requests import Session
158
-
159
- session = Session()
160
- session.headers.update(config.headers or {})
161
- session.verify = config.request.tls_verify
162
- if config.request.cert is not None:
163
- session.cert = config.request.cert
164
- if config.auth is not None:
165
- session.auth = get_requests_auth(config.auth, config.auth_type)
166
-
167
- return [send(probe(), session, schema, config) for probe in PROBES]
@@ -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)