schemathesis 3.25.6__py3-none-any.whl → 4.0.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +102 -82
  3. schemathesis/checks.py +126 -46
  4. schemathesis/cli/__init__.py +11 -1760
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +37 -0
  7. schemathesis/cli/commands/run/__init__.py +662 -0
  8. schemathesis/cli/commands/run/checks.py +80 -0
  9. schemathesis/cli/commands/run/context.py +117 -0
  10. schemathesis/cli/commands/run/events.py +35 -0
  11. schemathesis/cli/commands/run/executor.py +138 -0
  12. schemathesis/cli/commands/run/filters.py +194 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +18 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  17. schemathesis/cli/commands/run/handlers/output.py +746 -0
  18. schemathesis/cli/commands/run/hypothesis.py +105 -0
  19. schemathesis/cli/commands/run/loaders.py +129 -0
  20. schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
  21. schemathesis/cli/constants.py +5 -52
  22. schemathesis/cli/core.py +17 -0
  23. schemathesis/cli/ext/fs.py +14 -0
  24. schemathesis/cli/ext/groups.py +55 -0
  25. schemathesis/cli/{options.py → ext/options.py} +39 -10
  26. schemathesis/cli/hooks.py +36 -0
  27. schemathesis/contrib/__init__.py +1 -3
  28. schemathesis/contrib/openapi/__init__.py +1 -3
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
  30. schemathesis/core/__init__.py +58 -0
  31. schemathesis/core/compat.py +25 -0
  32. schemathesis/core/control.py +2 -0
  33. schemathesis/core/curl.py +58 -0
  34. schemathesis/core/deserialization.py +65 -0
  35. schemathesis/core/errors.py +370 -0
  36. schemathesis/core/failures.py +285 -0
  37. schemathesis/core/fs.py +19 -0
  38. schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
  39. schemathesis/core/loaders.py +104 -0
  40. schemathesis/core/marks.py +66 -0
  41. schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
  42. schemathesis/core/output/__init__.py +69 -0
  43. schemathesis/core/output/sanitization.py +197 -0
  44. schemathesis/core/rate_limit.py +60 -0
  45. schemathesis/core/registries.py +31 -0
  46. schemathesis/{internal → core}/result.py +1 -1
  47. schemathesis/core/transforms.py +113 -0
  48. schemathesis/core/transport.py +108 -0
  49. schemathesis/core/validation.py +38 -0
  50. schemathesis/core/version.py +7 -0
  51. schemathesis/engine/__init__.py +30 -0
  52. schemathesis/engine/config.py +59 -0
  53. schemathesis/engine/context.py +119 -0
  54. schemathesis/engine/control.py +36 -0
  55. schemathesis/engine/core.py +157 -0
  56. schemathesis/engine/errors.py +394 -0
  57. schemathesis/engine/events.py +337 -0
  58. schemathesis/engine/phases/__init__.py +66 -0
  59. schemathesis/{runner → engine/phases}/probes.py +50 -67
  60. schemathesis/engine/phases/stateful/__init__.py +65 -0
  61. schemathesis/engine/phases/stateful/_executor.py +326 -0
  62. schemathesis/engine/phases/stateful/context.py +85 -0
  63. schemathesis/engine/phases/unit/__init__.py +174 -0
  64. schemathesis/engine/phases/unit/_executor.py +321 -0
  65. schemathesis/engine/phases/unit/_pool.py +74 -0
  66. schemathesis/engine/recorder.py +241 -0
  67. schemathesis/errors.py +31 -0
  68. schemathesis/experimental/__init__.py +18 -14
  69. schemathesis/filters.py +103 -14
  70. schemathesis/generation/__init__.py +21 -37
  71. schemathesis/generation/case.py +190 -0
  72. schemathesis/generation/coverage.py +931 -0
  73. schemathesis/generation/hypothesis/__init__.py +30 -0
  74. schemathesis/generation/hypothesis/builder.py +585 -0
  75. schemathesis/generation/hypothesis/examples.py +50 -0
  76. schemathesis/generation/hypothesis/given.py +66 -0
  77. schemathesis/generation/hypothesis/reporting.py +14 -0
  78. schemathesis/generation/hypothesis/strategies.py +16 -0
  79. schemathesis/generation/meta.py +115 -0
  80. schemathesis/generation/modes.py +28 -0
  81. schemathesis/generation/overrides.py +96 -0
  82. schemathesis/generation/stateful/__init__.py +20 -0
  83. schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
  84. schemathesis/generation/targets.py +69 -0
  85. schemathesis/graphql/__init__.py +15 -0
  86. schemathesis/graphql/checks.py +115 -0
  87. schemathesis/graphql/loaders.py +131 -0
  88. schemathesis/hooks.py +99 -67
  89. schemathesis/openapi/__init__.py +13 -0
  90. schemathesis/openapi/checks.py +412 -0
  91. schemathesis/openapi/generation/__init__.py +0 -0
  92. schemathesis/openapi/generation/filters.py +63 -0
  93. schemathesis/openapi/loaders.py +178 -0
  94. schemathesis/pytest/__init__.py +5 -0
  95. schemathesis/pytest/control_flow.py +7 -0
  96. schemathesis/pytest/lazy.py +273 -0
  97. schemathesis/pytest/loaders.py +12 -0
  98. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
  99. schemathesis/python/__init__.py +0 -0
  100. schemathesis/python/asgi.py +12 -0
  101. schemathesis/python/wsgi.py +12 -0
  102. schemathesis/schemas.py +537 -261
  103. schemathesis/specs/graphql/__init__.py +0 -1
  104. schemathesis/specs/graphql/_cache.py +25 -0
  105. schemathesis/specs/graphql/nodes.py +1 -0
  106. schemathesis/specs/graphql/scalars.py +7 -5
  107. schemathesis/specs/graphql/schemas.py +215 -187
  108. schemathesis/specs/graphql/validation.py +11 -18
  109. schemathesis/specs/openapi/__init__.py +7 -1
  110. schemathesis/specs/openapi/_cache.py +122 -0
  111. schemathesis/specs/openapi/_hypothesis.py +146 -165
  112. schemathesis/specs/openapi/checks.py +565 -67
  113. schemathesis/specs/openapi/converter.py +33 -6
  114. schemathesis/specs/openapi/definitions.py +11 -18
  115. schemathesis/specs/openapi/examples.py +139 -23
  116. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  117. schemathesis/specs/openapi/expressions/context.py +4 -6
  118. schemathesis/specs/openapi/expressions/extractors.py +23 -0
  119. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  120. schemathesis/specs/openapi/expressions/nodes.py +38 -14
  121. schemathesis/specs/openapi/expressions/parser.py +26 -5
  122. schemathesis/specs/openapi/formats.py +45 -0
  123. schemathesis/specs/openapi/links.py +65 -165
  124. schemathesis/specs/openapi/media_types.py +32 -0
  125. schemathesis/specs/openapi/negative/__init__.py +7 -3
  126. schemathesis/specs/openapi/negative/mutations.py +24 -8
  127. schemathesis/specs/openapi/parameters.py +46 -30
  128. schemathesis/specs/openapi/patterns.py +137 -0
  129. schemathesis/specs/openapi/references.py +47 -57
  130. schemathesis/specs/openapi/schemas.py +478 -369
  131. schemathesis/specs/openapi/security.py +25 -7
  132. schemathesis/specs/openapi/serialization.py +11 -6
  133. schemathesis/specs/openapi/stateful/__init__.py +185 -73
  134. schemathesis/specs/openapi/utils.py +6 -1
  135. schemathesis/transport/__init__.py +104 -0
  136. schemathesis/transport/asgi.py +26 -0
  137. schemathesis/transport/prepare.py +99 -0
  138. schemathesis/transport/requests.py +221 -0
  139. schemathesis/{_xml.py → transport/serialization.py} +143 -28
  140. schemathesis/transport/wsgi.py +165 -0
  141. schemathesis-4.0.0a1.dist-info/METADATA +297 -0
  142. schemathesis-4.0.0a1.dist-info/RECORD +152 -0
  143. {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
  144. {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
  145. schemathesis/_compat.py +0 -74
  146. schemathesis/_dependency_versions.py +0 -17
  147. schemathesis/_hypothesis.py +0 -246
  148. schemathesis/_override.py +0 -49
  149. schemathesis/cli/cassettes.py +0 -375
  150. schemathesis/cli/context.py +0 -58
  151. schemathesis/cli/debug.py +0 -26
  152. schemathesis/cli/handlers.py +0 -16
  153. schemathesis/cli/junitxml.py +0 -43
  154. schemathesis/cli/output/__init__.py +0 -1
  155. schemathesis/cli/output/default.py +0 -790
  156. schemathesis/cli/output/short.py +0 -44
  157. schemathesis/cli/sanitization.py +0 -20
  158. schemathesis/code_samples.py +0 -149
  159. schemathesis/constants.py +0 -55
  160. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  161. schemathesis/contrib/openapi/formats/uuid.py +0 -15
  162. schemathesis/contrib/unique_data.py +0 -41
  163. schemathesis/exceptions.py +0 -560
  164. schemathesis/extra/_aiohttp.py +0 -27
  165. schemathesis/extra/_flask.py +0 -10
  166. schemathesis/extra/_server.py +0 -17
  167. schemathesis/failures.py +0 -209
  168. schemathesis/fixups/__init__.py +0 -36
  169. schemathesis/fixups/fast_api.py +0 -41
  170. schemathesis/fixups/utf8_bom.py +0 -29
  171. schemathesis/graphql.py +0 -4
  172. schemathesis/internal/__init__.py +0 -7
  173. schemathesis/internal/copy.py +0 -13
  174. schemathesis/internal/datetime.py +0 -5
  175. schemathesis/internal/deprecation.py +0 -34
  176. schemathesis/internal/jsonschema.py +0 -35
  177. schemathesis/internal/transformation.py +0 -15
  178. schemathesis/internal/validation.py +0 -34
  179. schemathesis/lazy.py +0 -361
  180. schemathesis/loaders.py +0 -120
  181. schemathesis/models.py +0 -1234
  182. schemathesis/parameters.py +0 -86
  183. schemathesis/runner/__init__.py +0 -570
  184. schemathesis/runner/events.py +0 -329
  185. schemathesis/runner/impl/__init__.py +0 -3
  186. schemathesis/runner/impl/core.py +0 -1035
  187. schemathesis/runner/impl/solo.py +0 -90
  188. schemathesis/runner/impl/threadpool.py +0 -400
  189. schemathesis/runner/serialization.py +0 -411
  190. schemathesis/sanitization.py +0 -248
  191. schemathesis/serializers.py +0 -323
  192. schemathesis/service/__init__.py +0 -18
  193. schemathesis/service/auth.py +0 -11
  194. schemathesis/service/ci.py +0 -201
  195. schemathesis/service/client.py +0 -100
  196. schemathesis/service/constants.py +0 -38
  197. schemathesis/service/events.py +0 -57
  198. schemathesis/service/hosts.py +0 -107
  199. schemathesis/service/metadata.py +0 -46
  200. schemathesis/service/models.py +0 -49
  201. schemathesis/service/report.py +0 -255
  202. schemathesis/service/serialization.py +0 -199
  203. schemathesis/service/usage.py +0 -65
  204. schemathesis/specs/graphql/loaders.py +0 -344
  205. schemathesis/specs/openapi/filters.py +0 -49
  206. schemathesis/specs/openapi/loaders.py +0 -667
  207. schemathesis/specs/openapi/stateful/links.py +0 -92
  208. schemathesis/specs/openapi/validation.py +0 -25
  209. schemathesis/stateful/__init__.py +0 -133
  210. schemathesis/targets.py +0 -45
  211. schemathesis/throttling.py +0 -41
  212. schemathesis/transports/__init__.py +0 -5
  213. schemathesis/transports/auth.py +0 -15
  214. schemathesis/transports/headers.py +0 -35
  215. schemathesis/transports/responses.py +0 -52
  216. schemathesis/types.py +0 -35
  217. schemathesis/utils.py +0 -169
  218. schemathesis-3.25.6.dist-info/METADATA +0 -356
  219. schemathesis-3.25.6.dist-info/RECORD +0 -134
  220. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  221. {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,326 @@
1
+ from __future__ import annotations
2
+
3
+ import queue
4
+ import time
5
+ import unittest
6
+ from dataclasses import replace
7
+ from typing import Any
8
+
9
+ import hypothesis
10
+ from hypothesis.control import current_build_context
11
+ from hypothesis.errors import Flaky, Unsatisfiable
12
+ from hypothesis.stateful import Rule
13
+
14
+ from schemathesis.checks import CheckContext, CheckFunction, run_checks
15
+ from schemathesis.core.failures import Failure, FailureGroup
16
+ from schemathesis.core.transport import Response
17
+ from schemathesis.engine import Status, events
18
+ from schemathesis.engine.context import EngineContext
19
+ from schemathesis.engine.control import ExecutionControl
20
+ from schemathesis.engine.phases import PhaseName
21
+ from schemathesis.engine.phases.stateful.context import StatefulContext
22
+ from schemathesis.engine.recorder import ScenarioRecorder
23
+ from schemathesis.generation.case import Case
24
+ from schemathesis.generation.hypothesis.reporting import ignore_hypothesis_output
25
+ from schemathesis.generation.stateful.state_machine import (
26
+ DEFAULT_STATE_MACHINE_SETTINGS,
27
+ APIStateMachine,
28
+ Direction,
29
+ StepResult,
30
+ )
31
+ from schemathesis.generation.targets import TargetMetricCollector
32
+
33
+
34
+ def _get_hypothesis_settings_kwargs_override(settings: hypothesis.settings) -> dict[str, Any]:
35
+ """Get the settings that should be overridden to match the defaults for API state machines."""
36
+ kwargs = {}
37
+ hypothesis_default = hypothesis.settings()
38
+ if settings.phases == hypothesis_default.phases:
39
+ kwargs["phases"] = DEFAULT_STATE_MACHINE_SETTINGS.phases
40
+ if settings.stateful_step_count == hypothesis_default.stateful_step_count:
41
+ kwargs["stateful_step_count"] = DEFAULT_STATE_MACHINE_SETTINGS.stateful_step_count
42
+ if settings.deadline == hypothesis_default.deadline:
43
+ kwargs["deadline"] = DEFAULT_STATE_MACHINE_SETTINGS.deadline
44
+ if settings.suppress_health_check == hypothesis_default.suppress_health_check:
45
+ kwargs["suppress_health_check"] = DEFAULT_STATE_MACHINE_SETTINGS.suppress_health_check
46
+ return kwargs
47
+
48
+
49
+ def execute_state_machine_loop(
50
+ *,
51
+ state_machine: type[APIStateMachine],
52
+ event_queue: queue.Queue,
53
+ engine: EngineContext,
54
+ ) -> None:
55
+ """Execute the state machine testing loop."""
56
+ kwargs = _get_hypothesis_settings_kwargs_override(engine.config.execution.hypothesis_settings)
57
+ if kwargs:
58
+ config = replace(
59
+ engine.config,
60
+ execution=replace(
61
+ engine.config.execution,
62
+ hypothesis_settings=hypothesis.settings(engine.config.execution.hypothesis_settings, **kwargs),
63
+ ),
64
+ )
65
+ else:
66
+ config = engine.config
67
+
68
+ ctx = StatefulContext(metric_collector=TargetMetricCollector(targets=config.execution.targets))
69
+
70
+ transport_kwargs = engine.transport_kwargs
71
+
72
+ class _InstrumentedStateMachine(state_machine): # type: ignore[valid-type,misc]
73
+ """State machine with additional hooks for emitting events."""
74
+
75
+ def setup(self) -> None:
76
+ scenario_started = events.ScenarioStarted(label=None, phase=PhaseName.STATEFUL_TESTING, suite_id=suite_id)
77
+ self._start_time = time.monotonic()
78
+ self._scenario_id = scenario_started.id
79
+ event_queue.put(scenario_started)
80
+ self.recorder = ScenarioRecorder(label="Stateful tests")
81
+ self._check_ctx = engine.get_check_context(self.recorder)
82
+
83
+ def get_call_kwargs(self, case: Case) -> dict[str, Any]:
84
+ return transport_kwargs
85
+
86
+ def _repr_step(self, rule: Rule, data: dict, result: StepResult) -> str:
87
+ return ""
88
+
89
+ if config.override is not None:
90
+
91
+ def before_call(self, case: Case) -> None:
92
+ for location, entry in config.override.for_operation(case.operation).items(): # type: ignore[union-attr]
93
+ if entry:
94
+ container = getattr(case, location) or {}
95
+ container.update(entry)
96
+ setattr(case, location, container)
97
+ return super().before_call(case)
98
+
99
+ def step(self, case: Case, previous: tuple[StepResult, Direction] | None = None) -> StepResult | None:
100
+ # Checking the stop event once inside `step` is sufficient as it is called frequently
101
+ # The idea is to stop the execution as soon as possible
102
+ if previous is not None:
103
+ step_result, _ = previous
104
+ self.recorder.record_case(parent_id=step_result.case.id, case=case)
105
+ else:
106
+ self.recorder.record_case(parent_id=None, case=case)
107
+ if engine.has_to_stop:
108
+ raise KeyboardInterrupt
109
+ step_started = events.StepStarted(
110
+ phase=PhaseName.STATEFUL_TESTING, suite_id=suite_id, scenario_id=self._scenario_id
111
+ )
112
+ event_queue.put(step_started)
113
+ try:
114
+ if config.execution.unique_inputs:
115
+ cached = ctx.get_step_outcome(case)
116
+ if isinstance(cached, BaseException):
117
+ raise cached
118
+ elif cached is None:
119
+ return None
120
+ result = super().step(case, previous)
121
+ ctx.step_succeeded()
122
+ except FailureGroup as exc:
123
+ if config.execution.unique_inputs:
124
+ for failure in exc.exceptions:
125
+ ctx.store_step_outcome(case, failure)
126
+ ctx.step_failed()
127
+ raise
128
+ except Exception as exc:
129
+ if config.execution.unique_inputs:
130
+ ctx.store_step_outcome(case, exc)
131
+ ctx.step_errored()
132
+ raise
133
+ except KeyboardInterrupt:
134
+ ctx.step_interrupted()
135
+ raise
136
+ except BaseException as exc:
137
+ if config.execution.unique_inputs:
138
+ ctx.store_step_outcome(case, exc)
139
+ raise exc
140
+ else:
141
+ if config.execution.unique_inputs:
142
+ ctx.store_step_outcome(case, None)
143
+ finally:
144
+ transition_id: events.TransitionId | None
145
+ if previous is not None:
146
+ transition = previous[1]
147
+ transition_id = events.TransitionId(
148
+ name=transition.name,
149
+ status_code=transition.status_code,
150
+ source=transition.operation.label,
151
+ )
152
+ else:
153
+ transition_id = None
154
+ event_queue.put(
155
+ events.StepFinished(
156
+ id=step_started.id,
157
+ suite_id=suite_id,
158
+ scenario_id=self._scenario_id,
159
+ phase=PhaseName.STATEFUL_TESTING,
160
+ status=ctx.current_step_status,
161
+ transition_id=transition_id,
162
+ target=case.operation.label,
163
+ response=ctx.current_response,
164
+ )
165
+ )
166
+ return result
167
+
168
+ def validate_response(
169
+ self, response: Response, case: Case, additional_checks: tuple[CheckFunction, ...] = ()
170
+ ) -> None:
171
+ self.recorder.record_response(case_id=case.id, response=response)
172
+ ctx.collect_metric(case, response)
173
+ ctx.current_response = response
174
+ validate_response(
175
+ response=response,
176
+ case=case,
177
+ stateful_ctx=ctx,
178
+ check_ctx=self._check_ctx,
179
+ checks=config.execution.checks,
180
+ control=engine.control,
181
+ recorder=self.recorder,
182
+ additional_checks=additional_checks,
183
+ )
184
+
185
+ def teardown(self) -> None:
186
+ build_ctx = current_build_context()
187
+ event_queue.put(
188
+ events.ScenarioFinished(
189
+ id=self._scenario_id,
190
+ suite_id=suite_id,
191
+ phase=PhaseName.STATEFUL_TESTING,
192
+ # With dry run there will be no status
193
+ status=ctx.current_scenario_status or Status.SKIP,
194
+ recorder=self.recorder,
195
+ elapsed_time=time.monotonic() - self._start_time,
196
+ skip_reason=None,
197
+ is_final=build_ctx.is_final,
198
+ )
199
+ )
200
+ ctx.maximize_metrics()
201
+ ctx.reset_scenario()
202
+ super().teardown()
203
+
204
+ if config.execution.seed is not None:
205
+ InstrumentedStateMachine = hypothesis.seed(config.execution.seed)(_InstrumentedStateMachine)
206
+ else:
207
+ InstrumentedStateMachine = _InstrumentedStateMachine
208
+
209
+ while True:
210
+ # This loop is running until no new failures are found in a single iteration
211
+ suite_started = events.SuiteStarted(phase=PhaseName.STATEFUL_TESTING)
212
+ suite_id = suite_started.id
213
+ event_queue.put(suite_started)
214
+ if engine.is_interrupted:
215
+ event_queue.put(events.Interrupted(phase=PhaseName.STATEFUL_TESTING))
216
+ event_queue.put(
217
+ events.SuiteFinished(
218
+ id=suite_started.id,
219
+ phase=PhaseName.STATEFUL_TESTING,
220
+ status=Status.INTERRUPTED,
221
+ )
222
+ )
223
+ break
224
+ suite_status = Status.SUCCESS
225
+ try:
226
+ with ignore_hypothesis_output(): # type: ignore
227
+ InstrumentedStateMachine.run(settings=config.execution.hypothesis_settings)
228
+ except KeyboardInterrupt:
229
+ # Raised in the state machine when the stop event is set or it is raised by the user's code
230
+ # that is placed in the base class of the state machine.
231
+ # Therefore, set the stop event to cover the latter case
232
+ engine.stop()
233
+ suite_status = Status.INTERRUPTED
234
+ event_queue.put(events.Interrupted(phase=PhaseName.STATEFUL_TESTING))
235
+ break
236
+ except unittest.case.SkipTest:
237
+ # If `explicit` phase is used and there are not examples
238
+ suite_status = Status.SKIP
239
+ break
240
+ except FailureGroup as exc:
241
+ # When a check fails, the state machine is stopped
242
+ # The failure is already sent to the queue by the state machine
243
+ # Here we need to either exit or re-run the state machine with this failure marked as known
244
+ suite_status = Status.FAILURE
245
+ if engine.has_reached_the_failure_limit:
246
+ break # type: ignore[unreachable]
247
+ for failure in exc.exceptions:
248
+ ctx.mark_as_seen_in_run(failure)
249
+ continue
250
+ except Flaky:
251
+ suite_status = Status.FAILURE
252
+ if engine.has_reached_the_failure_limit:
253
+ break # type: ignore[unreachable]
254
+ # Mark all failures in this suite as seen to prevent them being re-discovered
255
+ ctx.mark_current_suite_as_seen_in_run()
256
+ continue
257
+ except Exception as exc:
258
+ if isinstance(exc, Unsatisfiable) and ctx.completed_scenarios > 0:
259
+ # Sometimes Hypothesis randomly gives up on generating some complex cases. However, if we know that
260
+ # values are possible to generate based on the previous observations, we retry the generation
261
+ if ctx.completed_scenarios >= config.execution.hypothesis_settings.max_examples:
262
+ # Avoid infinite restarts
263
+ break
264
+ continue
265
+ # Any other exception is an inner error and the test run should be stopped
266
+ suite_status = Status.ERROR
267
+ event_queue.put(
268
+ events.NonFatalError(
269
+ error=exc, phase=PhaseName.STATEFUL_TESTING, label="Stateful tests", related_to_operation=False
270
+ )
271
+ )
272
+ break
273
+ finally:
274
+ event_queue.put(
275
+ events.SuiteFinished(
276
+ id=suite_started.id,
277
+ phase=PhaseName.STATEFUL_TESTING,
278
+ status=suite_status,
279
+ )
280
+ )
281
+ ctx.reset()
282
+ # Exit on the first successful state machine execution
283
+ break
284
+
285
+
286
+ def validate_response(
287
+ *,
288
+ response: Response,
289
+ case: Case,
290
+ stateful_ctx: StatefulContext,
291
+ check_ctx: CheckContext,
292
+ control: ExecutionControl,
293
+ checks: list[CheckFunction],
294
+ recorder: ScenarioRecorder,
295
+ additional_checks: tuple[CheckFunction, ...] = (),
296
+ ) -> None:
297
+ """Validate the response against the provided checks."""
298
+
299
+ def on_failure(name: str, collected: set[Failure], failure: Failure) -> None:
300
+ if stateful_ctx.is_seen_in_suite(failure) or stateful_ctx.is_seen_in_run(failure):
301
+ return
302
+ failure_data = recorder.find_failure_data(parent_id=case.id, failure=failure)
303
+ recorder.record_check_failure(
304
+ name=name,
305
+ case_id=failure_data.case.id,
306
+ code_sample=failure_data.case.as_curl_command(headers=failure_data.headers, verify=failure_data.verify),
307
+ failure=failure,
308
+ )
309
+ control.count_failure()
310
+ stateful_ctx.mark_as_seen_in_suite(failure)
311
+ collected.add(failure)
312
+
313
+ def on_success(name: str, case: Case) -> None:
314
+ recorder.record_check_success(name=name, case_id=case.id)
315
+
316
+ failures = run_checks(
317
+ case=case,
318
+ response=response,
319
+ ctx=check_ctx,
320
+ checks=tuple(checks) + tuple(additional_checks),
321
+ on_failure=on_failure,
322
+ on_success=on_success,
323
+ )
324
+
325
+ if failures:
326
+ 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.targets import TargetMetricCollector
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: TargetMetricCollector = field(default_factory=TargetMetricCollector)
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,174 @@
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.result import Ok
15
+ from schemathesis.engine import Status, events
16
+ from schemathesis.engine.phases import PhaseName, PhaseSkipReason
17
+ from schemathesis.engine.recorder import ScenarioRecorder
18
+ from schemathesis.generation.hypothesis.builder import HypothesisTestConfig
19
+ from schemathesis.generation.hypothesis.reporting import ignore_hypothesis_output
20
+
21
+ from ._pool import TaskProducer, WorkerPool
22
+
23
+ if TYPE_CHECKING:
24
+ from schemathesis.engine.context import EngineContext
25
+ from schemathesis.engine.phases import Phase
26
+ from schemathesis.schemas import APIOperation
27
+
28
+ WORKER_TIMEOUT = 0.1
29
+
30
+
31
+ def execute(engine: EngineContext, phase: Phase) -> events.EventGenerator:
32
+ """Run a set of unit tests.
33
+
34
+ Implemented as a producer-consumer pattern via a task queue.
35
+ The main thread provides an iterator over API operations and worker threads create test functions and run them.
36
+ """
37
+ producer = TaskProducer(engine)
38
+ workers_num = engine.config.execution.workers_num
39
+
40
+ suite_started = events.SuiteStarted(phase=phase.name)
41
+
42
+ yield suite_started
43
+
44
+ status = None
45
+ is_executed = False
46
+
47
+ with WorkerPool(
48
+ workers_num=workers_num, producer=producer, worker_factory=worker_task, ctx=engine, suite_id=suite_started.id
49
+ ) as pool:
50
+ try:
51
+ while True:
52
+ try:
53
+ event = pool.events_queue.get(timeout=WORKER_TIMEOUT)
54
+ is_executed = True
55
+ if engine.is_interrupted:
56
+ raise KeyboardInterrupt
57
+ yield event
58
+ if isinstance(event, events.NonFatalError):
59
+ status = Status.ERROR
60
+ if isinstance(event, events.ScenarioFinished):
61
+ if event.status != Status.SKIP and (status is None or status < event.status):
62
+ status = event.status
63
+ if event.status in (Status.ERROR, Status.FAILURE):
64
+ engine.control.count_failure()
65
+ if isinstance(event, events.Interrupted) or engine.is_interrupted:
66
+ status = Status.INTERRUPTED
67
+ engine.stop()
68
+ if engine.has_to_stop:
69
+ break # type: ignore[unreachable]
70
+ except queue.Empty:
71
+ if all(not worker.is_alive() for worker in pool.workers):
72
+ break
73
+ continue
74
+ except KeyboardInterrupt:
75
+ engine.stop()
76
+ status = Status.INTERRUPTED
77
+ yield events.Interrupted(phase=PhaseName.UNIT_TESTING)
78
+
79
+ if not is_executed:
80
+ phase.skip_reason = PhaseSkipReason.NOTHING_TO_TEST
81
+ status = Status.SKIP
82
+ elif status is None:
83
+ status = Status.SKIP
84
+ # NOTE: Right now there is just one suite, hence two events go one after another
85
+ yield events.SuiteFinished(id=suite_started.id, phase=phase.name, status=status)
86
+ yield events.PhaseFinished(phase=phase, status=status, payload=None)
87
+
88
+
89
+ def worker_task(*, events_queue: Queue, producer: TaskProducer, ctx: EngineContext, suite_id: uuid.UUID) -> None:
90
+ from hypothesis.errors import HypothesisWarning
91
+
92
+ from schemathesis.generation.hypothesis.builder import create_test
93
+
94
+ from ._executor import run_test, test_func
95
+
96
+ warnings.filterwarnings("ignore", message="The recursion limit will not be reset", category=HypothesisWarning)
97
+ with ignore_hypothesis_output():
98
+ try:
99
+ while not ctx.has_to_stop:
100
+ result = producer.next_operation()
101
+ if result is None:
102
+ break
103
+
104
+ if isinstance(result, Ok):
105
+ operation = result.ok()
106
+ as_strategy_kwargs = get_strategy_kwargs(ctx, operation)
107
+ test_function = create_test(
108
+ operation=operation,
109
+ test_func=test_func,
110
+ config=HypothesisTestConfig(
111
+ settings=ctx.config.execution.hypothesis_settings,
112
+ seed=ctx.config.execution.seed,
113
+ generation=ctx.config.execution.generation,
114
+ as_strategy_kwargs=as_strategy_kwargs,
115
+ ),
116
+ )
117
+
118
+ # The test is blocking, meaning that even if CTRL-C comes to the main thread, this tasks will continue
119
+ # executing. However, as we set a stop event, it will be checked before the next network request.
120
+ # However, this is still suboptimal, as there could be slow requests and they will block for longer
121
+ for event in run_test(operation=operation, test_function=test_function, ctx=ctx, suite_id=suite_id):
122
+ events_queue.put(event)
123
+ else:
124
+ error = result.err()
125
+ if error.method:
126
+ label = f"{error.method.upper()} {error.full_path}"
127
+ scenario_started = events.ScenarioStarted(
128
+ label=label, phase=PhaseName.UNIT_TESTING, suite_id=suite_id
129
+ )
130
+ events_queue.put(scenario_started)
131
+
132
+ events_queue.put(
133
+ events.NonFatalError(
134
+ error=error, phase=PhaseName.UNIT_TESTING, label=label, related_to_operation=True
135
+ )
136
+ )
137
+
138
+ events_queue.put(
139
+ events.ScenarioFinished(
140
+ id=scenario_started.id,
141
+ suite_id=suite_id,
142
+ phase=PhaseName.UNIT_TESTING,
143
+ status=Status.ERROR,
144
+ recorder=ScenarioRecorder(label="Error"),
145
+ elapsed_time=0.0,
146
+ skip_reason=None,
147
+ is_final=True,
148
+ )
149
+ )
150
+ else:
151
+ assert error.full_path is not None
152
+ events_queue.put(
153
+ events.NonFatalError(
154
+ error=error,
155
+ phase=PhaseName.UNIT_TESTING,
156
+ label=error.full_path,
157
+ related_to_operation=False,
158
+ )
159
+ )
160
+ except KeyboardInterrupt:
161
+ events_queue.put(events.Interrupted(phase=PhaseName.UNIT_TESTING))
162
+
163
+
164
+ def get_strategy_kwargs(ctx: EngineContext, operation: APIOperation) -> dict[str, Any]:
165
+ kwargs = {}
166
+ if ctx.config.override is not None:
167
+ for location, entry in ctx.config.override.for_operation(operation).items():
168
+ if entry:
169
+ kwargs[location] = entry
170
+ if ctx.config.network.headers:
171
+ kwargs["headers"] = {
172
+ key: value for key, value in ctx.config.network.headers.items() if key.lower() != "user-agent"
173
+ }
174
+ return kwargs