schemathesis 3.39.16__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 +233 -307
  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.16.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
  151. {schemathesis-3.39.16.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 -717
  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.16.dist-info/METADATA +0 -293
  251. schemathesis-3.39.16.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.16.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,356 @@
1
+ from __future__ import annotations # noqa: I001
2
+
3
+ import queue
4
+ import time
5
+ import unittest
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+ from warnings import catch_warnings
9
+
10
+ import hypothesis
11
+ import requests
12
+ from hypothesis.control import current_build_context
13
+ from hypothesis.errors import Flaky, Unsatisfiable
14
+ from hypothesis.stateful import Rule
15
+ from requests.exceptions import ChunkedEncodingError
16
+ from requests.structures import CaseInsensitiveDict
17
+
18
+ from schemathesis.checks import CheckContext, CheckFunction, run_checks
19
+ from schemathesis.core.failures import Failure, FailureGroup
20
+ from schemathesis.core.transport import Response
21
+ from schemathesis.engine import Status, events
22
+ from schemathesis.engine.context import EngineContext
23
+ from schemathesis.engine.control import ExecutionControl
24
+ from schemathesis.engine.errors import (
25
+ TestingState,
26
+ UnrecoverableNetworkError,
27
+ clear_hypothesis_notes,
28
+ is_unrecoverable_network_error,
29
+ )
30
+ from schemathesis.engine.phases import PhaseName
31
+ from schemathesis.engine.phases.stateful.context import StatefulContext
32
+ from schemathesis.engine.recorder import ScenarioRecorder
33
+ from schemathesis.generation import overrides
34
+ from schemathesis.generation.case import Case
35
+ from schemathesis.generation.hypothesis.reporting import ignore_hypothesis_output
36
+ from schemathesis.generation.stateful import STATEFUL_TESTS_LABEL
37
+ from schemathesis.generation.stateful.state_machine import (
38
+ DEFAULT_STATE_MACHINE_SETTINGS,
39
+ APIStateMachine,
40
+ StepInput,
41
+ StepOutput,
42
+ )
43
+ from schemathesis.generation.metrics import MetricCollector
44
+
45
+
46
+ def _get_hypothesis_settings_kwargs_override(settings: hypothesis.settings) -> dict[str, Any]:
47
+ """Get the settings that should be overridden to match the defaults for API state machines."""
48
+ kwargs = {}
49
+ hypothesis_default = hypothesis.settings()
50
+ if settings.phases == hypothesis_default.phases:
51
+ kwargs["phases"] = DEFAULT_STATE_MACHINE_SETTINGS.phases
52
+ if settings.stateful_step_count == hypothesis_default.stateful_step_count:
53
+ kwargs["stateful_step_count"] = DEFAULT_STATE_MACHINE_SETTINGS.stateful_step_count
54
+ if settings.deadline == hypothesis_default.deadline:
55
+ kwargs["deadline"] = DEFAULT_STATE_MACHINE_SETTINGS.deadline
56
+ if settings.suppress_health_check == hypothesis_default.suppress_health_check:
57
+ kwargs["suppress_health_check"] = DEFAULT_STATE_MACHINE_SETTINGS.suppress_health_check
58
+ return kwargs
59
+
60
+
61
+ @dataclass
62
+ class CachedCheckContextData:
63
+ override: Any
64
+ auth: Any
65
+ headers: Any
66
+ config: Any
67
+ transport_kwargs: Any
68
+
69
+ __slots__ = ("override", "auth", "headers", "config", "transport_kwargs")
70
+
71
+
72
+ def execute_state_machine_loop(
73
+ *,
74
+ state_machine: type[APIStateMachine],
75
+ event_queue: queue.Queue,
76
+ engine: EngineContext,
77
+ ) -> None:
78
+ """Execute the state machine testing loop."""
79
+ configured_hypothesis_settings = engine.config.get_hypothesis_settings(phase="stateful")
80
+ kwargs = _get_hypothesis_settings_kwargs_override(configured_hypothesis_settings)
81
+ hypothesis_settings = hypothesis.settings(configured_hypothesis_settings, **kwargs)
82
+ generation = engine.config.generation_for(phase="stateful")
83
+
84
+ ctx = StatefulContext(metric_collector=MetricCollector(metrics=generation.maximize))
85
+ state = TestingState()
86
+
87
+ # Caches for validate_response to avoid repeated config lookups per operation
88
+ _check_context_cache: dict[str, CachedCheckContextData] = {}
89
+
90
+ class _InstrumentedStateMachine(state_machine): # type: ignore[valid-type,misc]
91
+ """State machine with additional hooks for emitting events."""
92
+
93
+ def setup(self) -> None:
94
+ scenario_started = events.ScenarioStarted(label=None, phase=PhaseName.STATEFUL_TESTING, suite_id=suite_id)
95
+ self._start_time = time.monotonic()
96
+ self._scenario_id = scenario_started.id
97
+ event_queue.put(scenario_started)
98
+
99
+ def get_call_kwargs(self, case: Case) -> dict[str, Any]:
100
+ return engine.get_transport_kwargs(operation=case.operation)
101
+
102
+ def _repr_step(self, rule: Rule, data: dict, result: StepOutput) -> str:
103
+ return ""
104
+
105
+ def before_call(self, case: Case) -> None:
106
+ override = overrides.for_operation(engine.config, operation=case.operation)
107
+ for location in ("query", "headers", "cookies", "path_parameters"):
108
+ entry = getattr(override, location)
109
+ if entry:
110
+ container = getattr(case, location) or {}
111
+ container.update(entry)
112
+ setattr(case, location, container)
113
+ return super().before_call(case)
114
+
115
+ def step(self, input: StepInput) -> StepOutput | None:
116
+ # Checking the stop event once inside `step` is sufficient as it is called frequently
117
+ # The idea is to stop the execution as soon as possible
118
+ if engine.has_to_stop:
119
+ raise KeyboardInterrupt
120
+ try:
121
+ if generation.unique_inputs:
122
+ cached = ctx.get_step_outcome(input.case)
123
+ if isinstance(cached, BaseException):
124
+ raise cached
125
+ elif cached is None:
126
+ return None
127
+ result = super().step(input)
128
+ ctx.step_succeeded()
129
+ except FailureGroup as exc:
130
+ if generation.unique_inputs:
131
+ for failure in exc.exceptions:
132
+ ctx.store_step_outcome(input.case, failure)
133
+ ctx.step_failed()
134
+ raise
135
+ except Exception as exc:
136
+ if isinstance(exc, (requests.ConnectionError, ChunkedEncodingError)) and is_unrecoverable_network_error(
137
+ exc
138
+ ):
139
+ transport_kwargs = engine.get_transport_kwargs(operation=input.case.operation)
140
+ if exc.request is not None:
141
+ headers = {key: value[0] for key, value in exc.request.headers.items()}
142
+ else:
143
+ headers = {**dict(input.case.headers or {}), **transport_kwargs.get("headers", {})}
144
+ verify = transport_kwargs.get("verify", True)
145
+ state.unrecoverable_network_error = UnrecoverableNetworkError(
146
+ error=exc,
147
+ code_sample=input.case.as_curl_command(headers=headers, verify=verify),
148
+ )
149
+
150
+ if generation.unique_inputs:
151
+ ctx.store_step_outcome(input.case, exc)
152
+ ctx.step_errored()
153
+ raise
154
+ except KeyboardInterrupt:
155
+ ctx.step_interrupted()
156
+ raise
157
+ except BaseException as exc:
158
+ if generation.unique_inputs:
159
+ ctx.store_step_outcome(input.case, exc)
160
+ raise exc
161
+ else:
162
+ if generation.unique_inputs:
163
+ ctx.store_step_outcome(input.case, None)
164
+ return result
165
+
166
+ def validate_response(
167
+ self, response: Response, case: Case, additional_checks: tuple[CheckFunction, ...] = (), **kwargs: Any
168
+ ) -> None:
169
+ self.recorder.record_response(case_id=case.id, response=response)
170
+ ctx.collect_metric(case, response)
171
+ ctx.current_response = response
172
+
173
+ label = case.operation.label
174
+ cached = _check_context_cache.get(label)
175
+ if cached is None:
176
+ headers = engine.config.headers_for(operation=case.operation)
177
+ cached = CachedCheckContextData(
178
+ override=overrides.for_operation(engine.config, operation=case.operation),
179
+ auth=engine.config.auth_for(operation=case.operation),
180
+ headers=CaseInsensitiveDict(headers) if headers else None,
181
+ config=engine.config.checks_config_for(operation=case.operation, phase="stateful"),
182
+ transport_kwargs=engine.get_transport_kwargs(operation=case.operation),
183
+ )
184
+ _check_context_cache[label] = cached
185
+
186
+ check_ctx = CheckContext(
187
+ override=cached.override,
188
+ auth=cached.auth,
189
+ headers=cached.headers,
190
+ config=cached.config,
191
+ transport_kwargs=cached.transport_kwargs,
192
+ recorder=self.recorder,
193
+ )
194
+ validate_response(
195
+ response=response,
196
+ case=case,
197
+ stateful_ctx=ctx,
198
+ check_ctx=check_ctx,
199
+ checks=check_ctx._checks,
200
+ control=engine.control,
201
+ recorder=self.recorder,
202
+ additional_checks=additional_checks,
203
+ )
204
+
205
+ def teardown(self) -> None:
206
+ build_ctx = current_build_context()
207
+ event_queue.put(
208
+ events.ScenarioFinished(
209
+ id=self._scenario_id,
210
+ suite_id=suite_id,
211
+ phase=PhaseName.STATEFUL_TESTING,
212
+ label=None,
213
+ status=ctx.current_scenario_status or Status.SKIP,
214
+ recorder=self.recorder,
215
+ elapsed_time=time.monotonic() - self._start_time,
216
+ skip_reason=None,
217
+ is_final=build_ctx.is_final,
218
+ )
219
+ )
220
+ ctx.maximize_metrics()
221
+ ctx.reset_scenario()
222
+ super().teardown()
223
+
224
+ seed = engine.config.seed
225
+
226
+ while True:
227
+ # This loop is running until no new failures are found in a single iteration
228
+ suite_started = events.SuiteStarted(phase=PhaseName.STATEFUL_TESTING)
229
+ suite_id = suite_started.id
230
+ event_queue.put(suite_started)
231
+ if engine.is_interrupted:
232
+ event_queue.put(events.Interrupted(phase=PhaseName.STATEFUL_TESTING))
233
+ event_queue.put(
234
+ events.SuiteFinished(
235
+ id=suite_started.id,
236
+ phase=PhaseName.STATEFUL_TESTING,
237
+ status=Status.INTERRUPTED,
238
+ )
239
+ )
240
+ break
241
+ suite_status = Status.SUCCESS
242
+ InstrumentedStateMachine = hypothesis.seed(seed)(_InstrumentedStateMachine)
243
+ # Predictably change the seed to avoid re-running the same sequences if tests fail
244
+ # yet have reproducible results
245
+ seed += 1
246
+ try:
247
+ with catch_warnings(), ignore_hypothesis_output(): # type: ignore
248
+ InstrumentedStateMachine.run(settings=hypothesis_settings)
249
+ except KeyboardInterrupt:
250
+ # Raised in the state machine when the stop event is set or it is raised by the user's code
251
+ # that is placed in the base class of the state machine.
252
+ # Therefore, set the stop event to cover the latter case
253
+ engine.stop()
254
+ suite_status = Status.INTERRUPTED
255
+ event_queue.put(events.Interrupted(phase=PhaseName.STATEFUL_TESTING))
256
+ break
257
+ except unittest.case.SkipTest:
258
+ # If `explicit` phase is used and there are no examples
259
+ suite_status = Status.SKIP
260
+ break
261
+ except FailureGroup as exc:
262
+ # When a check fails, the state machine is stopped
263
+ # The failure is already sent to the queue by the state machine
264
+ # Here we need to either exit or re-run the state machine with this failure marked as known
265
+ suite_status = Status.FAILURE
266
+ if engine.has_reached_the_failure_limit:
267
+ break # type: ignore[unreachable]
268
+ for failure in exc.exceptions:
269
+ ctx.mark_as_seen_in_run(failure)
270
+ continue
271
+ except Flaky:
272
+ # Ignore flakiness
273
+ if engine.has_reached_the_failure_limit:
274
+ break # type: ignore[unreachable]
275
+ # Mark all failures in this suite as seen to prevent them being re-discovered
276
+ ctx.mark_current_suite_as_seen_in_run()
277
+ continue
278
+ except Exception as exc:
279
+ if isinstance(exc, Unsatisfiable) and ctx.completed_scenarios > 0:
280
+ # Sometimes Hypothesis randomly gives up on generating some complex cases. However, if we know that
281
+ # values are possible to generate based on the previous observations, we retry the generation
282
+ if ctx.completed_scenarios >= hypothesis_settings.max_examples:
283
+ # Avoid infinite restarts
284
+ break
285
+ continue
286
+ clear_hypothesis_notes(exc)
287
+ # Any other exception is an inner error and the test run should be stopped
288
+ suite_status = Status.ERROR
289
+ code_sample: str | None = None
290
+ if state.unrecoverable_network_error is not None:
291
+ exc = state.unrecoverable_network_error.error
292
+ code_sample = state.unrecoverable_network_error.code_sample
293
+ event_queue.put(
294
+ events.NonFatalError(
295
+ error=exc,
296
+ phase=PhaseName.STATEFUL_TESTING,
297
+ label=STATEFUL_TESTS_LABEL,
298
+ related_to_operation=False,
299
+ code_sample=code_sample,
300
+ )
301
+ )
302
+ break
303
+ finally:
304
+ event_queue.put(
305
+ events.SuiteFinished(
306
+ id=suite_started.id,
307
+ phase=PhaseName.STATEFUL_TESTING,
308
+ status=suite_status,
309
+ )
310
+ )
311
+ ctx.reset()
312
+ # Exit on the first successful state machine execution
313
+ break
314
+
315
+
316
+ def validate_response(
317
+ *,
318
+ response: Response,
319
+ case: Case,
320
+ stateful_ctx: StatefulContext,
321
+ check_ctx: CheckContext,
322
+ control: ExecutionControl,
323
+ checks: list[CheckFunction],
324
+ recorder: ScenarioRecorder,
325
+ additional_checks: tuple[CheckFunction, ...] = (),
326
+ ) -> None:
327
+ """Validate the response against the provided checks."""
328
+
329
+ def on_failure(name: str, collected: set[Failure], failure: Failure) -> None:
330
+ if stateful_ctx.is_seen_in_suite(failure) or stateful_ctx.is_seen_in_run(failure):
331
+ return
332
+ failure_data = recorder.find_failure_data(parent_id=case.id, failure=failure)
333
+ recorder.record_check_failure(
334
+ name=name,
335
+ case_id=failure_data.case.id,
336
+ code_sample=failure_data.case.as_curl_command(headers=failure_data.headers, verify=failure_data.verify),
337
+ failure=failure,
338
+ )
339
+ control.count_failure()
340
+ stateful_ctx.mark_as_seen_in_suite(failure)
341
+ collected.add(failure)
342
+
343
+ def on_success(name: str, case: Case) -> None:
344
+ recorder.record_check_success(name=name, case_id=case.id)
345
+
346
+ failures = run_checks(
347
+ case=case,
348
+ response=response,
349
+ ctx=check_ctx,
350
+ checks=tuple(checks) + tuple(additional_checks),
351
+ on_failure=on_failure,
352
+ on_success=on_success,
353
+ )
354
+
355
+ if failures:
356
+ raise FailureGroup(list(failures)) from None
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ from schemathesis.core import NOT_SET, NotSet
6
+ from schemathesis.core.failures import Failure
7
+ from schemathesis.core.transport import Response
8
+ from schemathesis.engine import Status
9
+ from schemathesis.generation.case import Case
10
+ from schemathesis.generation.metrics import MetricCollector
11
+
12
+
13
+ @dataclass
14
+ class StatefulContext:
15
+ """Mutable context for state machine execution."""
16
+
17
+ # All seen failure keys, both grouped and individual ones
18
+ seen_in_run: set[Failure] = field(default_factory=set)
19
+ # Failures keys seen in the current suite
20
+ seen_in_suite: set[Failure] = field(default_factory=set)
21
+ # Status of the current step
22
+ current_step_status: Status | None = None
23
+ # The currently processed response
24
+ current_response: Response | None = None
25
+ # Total number of failures
26
+ failures_count: int = 0
27
+ # The total number of completed test scenario
28
+ completed_scenarios: int = 0
29
+ # Metrics collector for targeted testing
30
+ metric_collector: MetricCollector = field(default_factory=MetricCollector)
31
+ step_outcomes: dict[int, BaseException | None] = field(default_factory=dict)
32
+
33
+ @property
34
+ def current_scenario_status(self) -> Status | None:
35
+ return self.current_step_status
36
+
37
+ def reset_scenario(self) -> None:
38
+ self.completed_scenarios += 1
39
+ self.current_step_status = None
40
+ self.current_response = None
41
+ self.step_outcomes.clear()
42
+
43
+ def step_succeeded(self) -> None:
44
+ self.current_step_status = Status.SUCCESS
45
+
46
+ def step_failed(self) -> None:
47
+ self.current_step_status = Status.FAILURE
48
+
49
+ def step_errored(self) -> None:
50
+ self.current_step_status = Status.ERROR
51
+
52
+ def step_interrupted(self) -> None:
53
+ self.current_step_status = Status.INTERRUPTED
54
+
55
+ def mark_as_seen_in_run(self, exc: Failure) -> None:
56
+ self.seen_in_run.add(exc)
57
+
58
+ def mark_as_seen_in_suite(self, exc: Failure) -> None:
59
+ self.seen_in_suite.add(exc)
60
+
61
+ def mark_current_suite_as_seen_in_run(self) -> None:
62
+ self.seen_in_run.update(self.seen_in_suite)
63
+
64
+ def is_seen_in_run(self, exc: Failure) -> bool:
65
+ return exc in self.seen_in_run
66
+
67
+ def is_seen_in_suite(self, exc: Failure) -> bool:
68
+ return exc in self.seen_in_suite
69
+
70
+ def collect_metric(self, case: Case, response: Response) -> None:
71
+ self.metric_collector.store(case, response)
72
+
73
+ def maximize_metrics(self) -> None:
74
+ self.metric_collector.maximize()
75
+
76
+ def reset(self) -> None:
77
+ self.seen_in_suite.clear()
78
+ self.reset_scenario()
79
+ self.metric_collector.reset()
80
+
81
+ def store_step_outcome(self, case: Case, outcome: BaseException | None) -> None:
82
+ self.step_outcomes[hash(case)] = outcome
83
+
84
+ def get_step_outcome(self, case: Case) -> BaseException | None | NotSet:
85
+ return self.step_outcomes.get(hash(case), NOT_SET)
@@ -0,0 +1,212 @@
1
+ """Unit testing by Schemathesis Engine.
2
+
3
+ This module provides high-level flow for single-, and multi-threaded modes.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import queue
9
+ import uuid
10
+ import warnings
11
+ from queue import Queue
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ from schemathesis.core.errors import InvalidSchema
15
+ from schemathesis.core.result import Ok
16
+ from schemathesis.engine import Status, events
17
+ from schemathesis.engine.phases import PhaseName, PhaseSkipReason
18
+ from schemathesis.engine.recorder import ScenarioRecorder
19
+ from schemathesis.generation import overrides
20
+ from schemathesis.generation.hypothesis.builder import HypothesisTestConfig, HypothesisTestMode
21
+ from schemathesis.generation.hypothesis.reporting import ignore_hypothesis_output
22
+
23
+ from ._pool import TaskProducer, WorkerPool
24
+
25
+ if TYPE_CHECKING:
26
+ from schemathesis.engine.context import EngineContext
27
+ from schemathesis.engine.phases import Phase
28
+ from schemathesis.schemas import APIOperation
29
+
30
+ WORKER_TIMEOUT = 0.1
31
+
32
+
33
+ def execute(engine: EngineContext, phase: Phase) -> events.EventGenerator:
34
+ """Run a set of unit tests.
35
+
36
+ Implemented as a producer-consumer pattern via a task queue.
37
+ The main thread provides an iterator over API operations and worker threads create test functions and run them.
38
+ """
39
+ if phase.name == PhaseName.EXAMPLES:
40
+ mode = HypothesisTestMode.EXAMPLES
41
+ elif phase.name == PhaseName.COVERAGE:
42
+ mode = HypothesisTestMode.COVERAGE
43
+ else:
44
+ mode = HypothesisTestMode.FUZZING
45
+ producer = TaskProducer(engine)
46
+
47
+ suite_started = events.SuiteStarted(phase=phase.name)
48
+
49
+ yield suite_started
50
+
51
+ status = None
52
+ is_executed = False
53
+
54
+ try:
55
+ with WorkerPool(
56
+ workers_num=engine.config.workers,
57
+ producer=producer,
58
+ worker_factory=worker_task,
59
+ ctx=engine,
60
+ mode=mode,
61
+ phase=phase.name,
62
+ suite_id=suite_started.id,
63
+ ) as pool:
64
+ try:
65
+ while True:
66
+ try:
67
+ event = pool.events_queue.get(timeout=WORKER_TIMEOUT)
68
+ is_executed = True
69
+ if engine.is_interrupted:
70
+ raise KeyboardInterrupt
71
+ yield event
72
+ if isinstance(event, events.NonFatalError):
73
+ status = Status.ERROR
74
+ if isinstance(event, events.ScenarioFinished):
75
+ if event.status != Status.SKIP and (status is None or status < event.status):
76
+ status = event.status
77
+ if event.status in (Status.ERROR, Status.FAILURE):
78
+ engine.control.count_failure()
79
+ if isinstance(event, events.Interrupted) or engine.is_interrupted:
80
+ status = Status.INTERRUPTED
81
+ engine.stop()
82
+ if engine.has_to_stop:
83
+ break # type: ignore[unreachable]
84
+ except queue.Empty:
85
+ if all(not worker.is_alive() for worker in pool.workers):
86
+ break
87
+ continue
88
+ except KeyboardInterrupt:
89
+ # Soft stop, waiting for workers to terminate
90
+ engine.stop()
91
+ status = Status.INTERRUPTED
92
+ yield events.Interrupted(phase=phase.name)
93
+ except KeyboardInterrupt:
94
+ # Hard stop, don't wait for worker threads
95
+ pass
96
+
97
+ if not is_executed:
98
+ phase.skip_reason = PhaseSkipReason.NOTHING_TO_TEST
99
+ status = Status.SKIP
100
+ elif status is None:
101
+ status = Status.SKIP
102
+ # NOTE: Right now there is just one suite, hence two events go one after another
103
+ yield events.SuiteFinished(id=suite_started.id, phase=phase.name, status=status)
104
+ yield events.PhaseFinished(phase=phase, status=status, payload=None)
105
+
106
+
107
+ def worker_task(
108
+ *,
109
+ events_queue: Queue,
110
+ producer: TaskProducer,
111
+ ctx: EngineContext,
112
+ mode: HypothesisTestMode,
113
+ phase: PhaseName,
114
+ suite_id: uuid.UUID,
115
+ ) -> None:
116
+ from hypothesis.errors import HypothesisWarning, InvalidArgument
117
+
118
+ from schemathesis.generation.hypothesis.builder import create_test
119
+
120
+ from ._executor import run_test, test_func
121
+
122
+ def on_error(error: Exception, *, method: str | None = None, path: str | None = None) -> None:
123
+ if method and path:
124
+ label = f"{method.upper()} {path}"
125
+ scenario_started = events.ScenarioStarted(label=label, phase=phase, suite_id=suite_id)
126
+ events_queue.put(scenario_started)
127
+
128
+ events_queue.put(events.NonFatalError(error=error, phase=phase, label=label, related_to_operation=True))
129
+
130
+ events_queue.put(
131
+ events.ScenarioFinished(
132
+ id=scenario_started.id,
133
+ suite_id=suite_id,
134
+ phase=phase,
135
+ label=label,
136
+ status=Status.ERROR,
137
+ recorder=ScenarioRecorder(label="Error"),
138
+ elapsed_time=0.0,
139
+ skip_reason=None,
140
+ is_final=True,
141
+ )
142
+ )
143
+ else:
144
+ events_queue.put(
145
+ events.NonFatalError(
146
+ error=error,
147
+ phase=phase,
148
+ label=path or "-",
149
+ related_to_operation=False,
150
+ )
151
+ )
152
+
153
+ warnings.filterwarnings("ignore", message="The recursion limit will not be reset", category=HypothesisWarning)
154
+ with ignore_hypothesis_output():
155
+ try:
156
+ while not ctx.has_to_stop:
157
+ result = producer.next_operation()
158
+ if result is None:
159
+ break
160
+
161
+ if isinstance(result, Ok):
162
+ operation = result.ok()
163
+ phases = ctx.config.phases_for(operation=operation)
164
+ # Skip tests if this phase is disabled
165
+ if (
166
+ (phase == PhaseName.EXAMPLES and not phases.examples.enabled)
167
+ or (phase == PhaseName.FUZZING and not phases.fuzzing.enabled)
168
+ or (phase == PhaseName.COVERAGE and not phases.coverage.enabled)
169
+ ):
170
+ continue
171
+ as_strategy_kwargs = get_strategy_kwargs(ctx, operation=operation)
172
+ try:
173
+ test_function = create_test(
174
+ operation=operation,
175
+ test_func=test_func,
176
+ config=HypothesisTestConfig(
177
+ modes=[mode],
178
+ settings=ctx.config.get_hypothesis_settings(operation=operation, phase=phase.name),
179
+ seed=ctx.config.seed,
180
+ project=ctx.config,
181
+ as_strategy_kwargs=as_strategy_kwargs,
182
+ ),
183
+ )
184
+ except (InvalidSchema, InvalidArgument) as exc:
185
+ on_error(exc, method=operation.method, path=operation.path)
186
+ continue
187
+
188
+ # The test is blocking, meaning that even if CTRL-C comes to the main thread, this tasks will continue
189
+ # executing. However, as we set a stop event, it will be checked before the next network request.
190
+ # However, this is still suboptimal, as there could be slow requests and they will block for longer
191
+ for event in run_test(
192
+ operation=operation, test_function=test_function, ctx=ctx, phase=phase, suite_id=suite_id
193
+ ):
194
+ events_queue.put(event)
195
+ else:
196
+ error = result.err()
197
+ on_error(error, method=error.method, path=error.path)
198
+ except KeyboardInterrupt:
199
+ events_queue.put(events.Interrupted(phase=phase))
200
+
201
+
202
+ def get_strategy_kwargs(ctx: EngineContext, *, operation: APIOperation) -> dict[str, Any]:
203
+ kwargs = {}
204
+ override = overrides.for_operation(ctx.config, operation=operation)
205
+ for location in ("query", "headers", "cookies", "path_parameters"):
206
+ entry = getattr(override, location)
207
+ if entry:
208
+ kwargs[location] = entry
209
+ headers = ctx.config.headers_for(operation=operation)
210
+ if headers:
211
+ kwargs["headers"] = {key: value for key, value in headers.items() if key.lower() != "user-agent"}
212
+ return kwargs