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,416 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ import unittest
5
+ import uuid
6
+ from typing import TYPE_CHECKING, Any, Callable
7
+ from warnings import WarningMessage, catch_warnings
8
+
9
+ import requests
10
+ from hypothesis.errors import InvalidArgument
11
+ from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
12
+ from jsonschema.exceptions import SchemaError as JsonSchemaError
13
+ from jsonschema.exceptions import ValidationError
14
+ from requests.exceptions import ChunkedEncodingError
15
+ from requests.structures import CaseInsensitiveDict
16
+
17
+ from schemathesis.checks import CheckContext, run_checks
18
+ from schemathesis.config._generation import GenerationConfig
19
+ from schemathesis.core.compat import BaseExceptionGroup
20
+ from schemathesis.core.control import SkipTest
21
+ from schemathesis.core.errors import (
22
+ SERIALIZERS_SUGGESTION_MESSAGE,
23
+ InternalError,
24
+ InvalidHeadersExample,
25
+ InvalidRegexPattern,
26
+ InvalidRegexType,
27
+ InvalidSchema,
28
+ MalformedMediaType,
29
+ SerializationNotPossible,
30
+ )
31
+ from schemathesis.core.failures import Failure, FailureGroup
32
+ from schemathesis.core.transport import Response
33
+ from schemathesis.engine import Status, events
34
+ from schemathesis.engine.context import EngineContext
35
+ from schemathesis.engine.errors import (
36
+ DeadlineExceeded,
37
+ TestingState,
38
+ UnexpectedError,
39
+ UnrecoverableNetworkError,
40
+ UnsupportedRecursiveReference,
41
+ clear_hypothesis_notes,
42
+ deduplicate_errors,
43
+ is_unrecoverable_network_error,
44
+ )
45
+ from schemathesis.engine.phases import PhaseName
46
+ from schemathesis.engine.recorder import ScenarioRecorder
47
+ from schemathesis.generation import metrics, overrides
48
+ from schemathesis.generation.case import Case
49
+ from schemathesis.generation.hypothesis.builder import (
50
+ InvalidHeadersExampleMark,
51
+ InvalidRegexMark,
52
+ NonSerializableMark,
53
+ UnsatisfiableExampleMark,
54
+ )
55
+ from schemathesis.generation.hypothesis.reporting import ignore_hypothesis_output
56
+
57
+ if TYPE_CHECKING:
58
+ from schemathesis.schemas import APIOperation
59
+
60
+
61
+ def run_test(
62
+ *,
63
+ operation: APIOperation,
64
+ test_function: Callable,
65
+ ctx: EngineContext,
66
+ phase: PhaseName,
67
+ suite_id: uuid.UUID,
68
+ ) -> events.EventGenerator:
69
+ """A single test run with all error handling needed."""
70
+ import hypothesis.errors
71
+
72
+ scenario_started = events.ScenarioStarted(label=operation.label, phase=phase, suite_id=suite_id)
73
+ yield scenario_started
74
+ errors: list[Exception] = []
75
+ skip_reason = None
76
+ test_start_time = time.monotonic()
77
+ recorder = ScenarioRecorder(label=operation.label)
78
+ state = TestingState()
79
+
80
+ def non_fatal_error(error: Exception, code_sample: str | None = None) -> events.NonFatalError:
81
+ return events.NonFatalError(
82
+ error=error, phase=phase, label=operation.label, related_to_operation=True, code_sample=code_sample
83
+ )
84
+
85
+ def scenario_finished(status: Status) -> events.ScenarioFinished:
86
+ return events.ScenarioFinished(
87
+ id=scenario_started.id,
88
+ suite_id=suite_id,
89
+ phase=phase,
90
+ label=operation.label,
91
+ recorder=recorder,
92
+ status=status,
93
+ elapsed_time=time.monotonic() - test_start_time,
94
+ skip_reason=skip_reason,
95
+ is_final=False,
96
+ )
97
+
98
+ phase_name = phase.value.lower()
99
+ assert phase_name in ("examples", "coverage", "fuzzing", "stateful")
100
+
101
+ operation_config = ctx.config.operations.get_for_operation(operation)
102
+ continue_on_failure = operation_config.continue_on_failure or ctx.config.continue_on_failure or False
103
+ generation = ctx.config.generation_for(operation=operation, phase=phase_name)
104
+ override = overrides.for_operation(ctx.config, operation=operation)
105
+ auth = ctx.config.auth_for(operation=operation)
106
+ headers = ctx.config.headers_for(operation=operation)
107
+ transport_kwargs = ctx.get_transport_kwargs(operation=operation)
108
+ check_ctx = CheckContext(
109
+ override=override,
110
+ auth=auth,
111
+ headers=CaseInsensitiveDict(headers) if headers else None,
112
+ config=ctx.config.checks_config_for(operation=operation, phase=phase_name),
113
+ transport_kwargs=transport_kwargs,
114
+ recorder=recorder,
115
+ )
116
+
117
+ try:
118
+ setup_hypothesis_database_key(test_function, operation)
119
+ with catch_warnings(record=True) as warnings, ignore_hypothesis_output():
120
+ test_function(
121
+ ctx=ctx,
122
+ state=state,
123
+ errors=errors,
124
+ check_ctx=check_ctx,
125
+ recorder=recorder,
126
+ generation=generation,
127
+ transport_kwargs=transport_kwargs,
128
+ continue_on_failure=continue_on_failure,
129
+ )
130
+ # Test body was not executed at all - Hypothesis did not generate any tests, but there is no error
131
+ status = Status.SUCCESS
132
+ except (SkipTest, unittest.case.SkipTest) as exc:
133
+ status = Status.SKIP
134
+ skip_reason = {"Hypothesis has been told to run no examples for this test.": "No examples in schema"}.get(
135
+ str(exc), str(exc)
136
+ )
137
+ except (FailureGroup, Failure):
138
+ status = Status.FAILURE
139
+ except UnexpectedError:
140
+ # It could be an error in user-defined extensions, network errors or internal Schemathesis errors
141
+ status = Status.ERROR
142
+ for idx, err in enumerate(errors):
143
+ if isinstance(err, MalformedMediaType):
144
+ errors[idx] = InvalidSchema(str(err))
145
+ except hypothesis.errors.Flaky as exc:
146
+ if isinstance(exc.__cause__, hypothesis.errors.DeadlineExceeded):
147
+ status = Status.ERROR
148
+ yield non_fatal_error(DeadlineExceeded.from_exc(exc.__cause__))
149
+ elif isinstance(exc, hypothesis.errors.FlakyFailure) and any(
150
+ isinstance(subexc, hypothesis.errors.DeadlineExceeded) for subexc in exc.exceptions
151
+ ):
152
+ for sub_exc in exc.exceptions:
153
+ if isinstance(sub_exc, hypothesis.errors.DeadlineExceeded):
154
+ yield non_fatal_error(DeadlineExceeded.from_exc(sub_exc))
155
+ status = Status.ERROR
156
+ elif errors:
157
+ status = Status.ERROR
158
+ else:
159
+ status = Status.FAILURE
160
+ except BaseExceptionGroup:
161
+ status = Status.ERROR
162
+ except hypothesis.errors.Unsatisfiable:
163
+ # We need more clear error message here
164
+ status = Status.ERROR
165
+ yield non_fatal_error(hypothesis.errors.Unsatisfiable("Failed to generate test cases for this API operation"))
166
+ except KeyboardInterrupt:
167
+ yield scenario_finished(Status.INTERRUPTED)
168
+ yield events.Interrupted(phase=phase)
169
+ return
170
+ except AssertionError as exc: # May come from `hypothesis-jsonschema` or `hypothesis`
171
+ status = Status.ERROR
172
+ try:
173
+ operation.schema.validate()
174
+ msg = "Unexpected error during testing of this API operation"
175
+ exc_msg = str(exc)
176
+ if exc_msg:
177
+ msg += f": {exc_msg}"
178
+ try:
179
+ raise InternalError(msg) from exc
180
+ except InternalError as exc:
181
+ yield non_fatal_error(exc)
182
+ except ValidationError as exc:
183
+ yield non_fatal_error(
184
+ InvalidSchema.from_jsonschema_error(
185
+ exc,
186
+ path=operation.path,
187
+ method=operation.method,
188
+ config=ctx.config.output,
189
+ )
190
+ )
191
+ except HypothesisRefResolutionError:
192
+ status = Status.ERROR
193
+ yield non_fatal_error(UnsupportedRecursiveReference())
194
+ except InvalidArgument as exc:
195
+ status = Status.ERROR
196
+ message = get_invalid_regular_expression_message(warnings)
197
+ if message:
198
+ # `hypothesis-jsonschema` emits a warning on invalid regular expression syntax
199
+ yield non_fatal_error(InvalidRegexPattern.from_hypothesis_jsonschema_message(message))
200
+ else:
201
+ yield non_fatal_error(exc)
202
+ except hypothesis.errors.DeadlineExceeded as exc:
203
+ status = Status.ERROR
204
+ yield non_fatal_error(DeadlineExceeded.from_exc(exc))
205
+ except JsonSchemaError as exc:
206
+ status = Status.ERROR
207
+ yield non_fatal_error(InvalidRegexPattern.from_schema_error(exc, from_examples=False))
208
+ except Exception as exc:
209
+ status = Status.ERROR
210
+ clear_hypothesis_notes(exc)
211
+ # Likely a YAML parsing issue. E.g. `00:00:00.00` (without quotes) is parsed as float `0.0`
212
+ if str(exc) == "first argument must be string or compiled pattern":
213
+ yield non_fatal_error(
214
+ InvalidRegexType(
215
+ "Invalid `pattern` value: expected a string. "
216
+ "If your schema is in YAML, ensure `pattern` values are quoted",
217
+ )
218
+ )
219
+ else:
220
+ code_sample: str | None = None
221
+ if state.unrecoverable_network_error is not None and state.unrecoverable_network_error.error is exc:
222
+ code_sample = state.unrecoverable_network_error.code_sample
223
+ yield non_fatal_error(exc, code_sample=code_sample)
224
+ if (
225
+ status == Status.SUCCESS
226
+ and continue_on_failure
227
+ and any(check.status == Status.FAILURE for checks in recorder.checks.values() for check in checks)
228
+ ):
229
+ status = Status.FAILURE
230
+ if UnsatisfiableExampleMark.is_set(test_function):
231
+ status = Status.ERROR
232
+ yield non_fatal_error(
233
+ hypothesis.errors.Unsatisfiable("Failed to generate test cases from examples for this API operation")
234
+ )
235
+ non_serializable = NonSerializableMark.get(test_function)
236
+ if non_serializable is not None and status != Status.ERROR:
237
+ status = Status.ERROR
238
+ media_types = ", ".join(non_serializable.media_types)
239
+ yield non_fatal_error(
240
+ SerializationNotPossible(
241
+ "Failed to generate test cases from examples for this API operation because of"
242
+ f" unsupported payload media types: {media_types}\n{SERIALIZERS_SUGGESTION_MESSAGE}",
243
+ media_types=non_serializable.media_types,
244
+ )
245
+ )
246
+
247
+ invalid_regex = InvalidRegexMark.get(test_function)
248
+ if invalid_regex is not None and status != Status.ERROR:
249
+ status = Status.ERROR
250
+ yield non_fatal_error(InvalidRegexPattern.from_schema_error(invalid_regex, from_examples=True))
251
+ invalid_headers = InvalidHeadersExampleMark.get(test_function)
252
+ if invalid_headers:
253
+ status = Status.ERROR
254
+ yield non_fatal_error(InvalidHeadersExample.from_headers(invalid_headers))
255
+ for error in deduplicate_errors(errors):
256
+ yield non_fatal_error(error)
257
+
258
+ yield scenario_finished(status)
259
+
260
+
261
+ def setup_hypothesis_database_key(test: Callable, operation: APIOperation) -> None:
262
+ """Make Hypothesis use separate database entries for every API operation.
263
+
264
+ It increases the effectiveness of the Hypothesis database in the CLI.
265
+ """
266
+ # Hypothesis's function digest depends on the test function signature. To reflect it for the web API case,
267
+ # we use all API operation parameters in the digest.
268
+ extra = operation.label.encode("utf8")
269
+ for parameter in operation.iter_parameters():
270
+ extra += parameter.serialize(operation).encode("utf8")
271
+ test.hypothesis.inner_test._hypothesis_internal_add_digest = extra # type: ignore
272
+
273
+
274
+ def get_invalid_regular_expression_message(warnings: list[WarningMessage]) -> str | None:
275
+ for warning in warnings:
276
+ message = str(warning.message)
277
+ if "is not valid syntax for a Python regular expression" in message:
278
+ return message
279
+ return None
280
+
281
+
282
+ def cached_test_func(f: Callable) -> Callable:
283
+ def wrapped(
284
+ *,
285
+ ctx: EngineContext,
286
+ state: TestingState,
287
+ case: Case,
288
+ errors: list[Exception],
289
+ check_ctx: CheckContext,
290
+ recorder: ScenarioRecorder,
291
+ generation: GenerationConfig,
292
+ transport_kwargs: dict[str, Any],
293
+ continue_on_failure: bool,
294
+ ) -> None:
295
+ try:
296
+ if ctx.has_to_stop:
297
+ raise KeyboardInterrupt
298
+ if generation.unique_inputs:
299
+ cached = ctx.get_cached_outcome(case)
300
+ if isinstance(cached, BaseException):
301
+ raise cached
302
+ elif cached is None:
303
+ return None
304
+ try:
305
+ f(
306
+ case=case,
307
+ check_ctx=check_ctx,
308
+ recorder=recorder,
309
+ generation=generation,
310
+ transport_kwargs=transport_kwargs,
311
+ continue_on_failure=continue_on_failure,
312
+ )
313
+ except BaseException as exc:
314
+ ctx.cache_outcome(case, exc)
315
+ raise
316
+ else:
317
+ ctx.cache_outcome(case, None)
318
+ else:
319
+ f(
320
+ case=case,
321
+ check_ctx=check_ctx,
322
+ recorder=recorder,
323
+ generation=generation,
324
+ transport_kwargs=transport_kwargs,
325
+ continue_on_failure=continue_on_failure,
326
+ )
327
+ except (KeyboardInterrupt, Failure):
328
+ raise
329
+ except Exception as exc:
330
+ if isinstance(exc, (requests.ConnectionError, ChunkedEncodingError)) and is_unrecoverable_network_error(
331
+ exc
332
+ ):
333
+ # Server likely has crashed and does not accept any connections at all
334
+ # Don't report these error - only the original crash should be reported
335
+ if exc.request is not None:
336
+ headers = {key: value[0] for key, value in exc.request.headers.items()}
337
+ else:
338
+ headers = {**dict(case.headers or {}), **transport_kwargs.get("headers", {})}
339
+ verify = transport_kwargs.get("verify", True)
340
+ state.unrecoverable_network_error = UnrecoverableNetworkError(
341
+ error=exc,
342
+ code_sample=case.as_curl_command(headers=headers, verify=verify),
343
+ )
344
+ raise
345
+ errors.append(exc)
346
+ raise UnexpectedError from None
347
+
348
+ wrapped.__name__ = f.__name__
349
+
350
+ return wrapped
351
+
352
+
353
+ @cached_test_func
354
+ def test_func(
355
+ *,
356
+ case: Case,
357
+ check_ctx: CheckContext,
358
+ recorder: ScenarioRecorder,
359
+ generation: GenerationConfig,
360
+ transport_kwargs: dict[str, Any],
361
+ continue_on_failure: bool,
362
+ ) -> None:
363
+ recorder.record_case(parent_id=None, transition=None, case=case)
364
+ try:
365
+ response = case.call(**transport_kwargs)
366
+ except (requests.Timeout, requests.ConnectionError, ChunkedEncodingError) as error:
367
+ if isinstance(error.request, requests.Request):
368
+ recorder.record_request(case_id=case.id, request=error.request.prepare())
369
+ elif isinstance(error.request, requests.PreparedRequest):
370
+ recorder.record_request(case_id=case.id, request=error.request)
371
+ raise
372
+ recorder.record_response(case_id=case.id, response=response)
373
+ metrics.maximize(generation.maximize, case=case, response=response)
374
+ validate_response(
375
+ case=case,
376
+ ctx=check_ctx,
377
+ response=response,
378
+ continue_on_failure=continue_on_failure,
379
+ recorder=recorder,
380
+ )
381
+
382
+
383
+ def validate_response(
384
+ *,
385
+ case: Case,
386
+ ctx: CheckContext,
387
+ response: Response,
388
+ continue_on_failure: bool,
389
+ recorder: ScenarioRecorder,
390
+ ) -> None:
391
+ failures = set()
392
+
393
+ def on_failure(name: str, collected: set[Failure], failure: Failure) -> None:
394
+ collected.add(failure)
395
+ failure_data = recorder.find_failure_data(parent_id=case.id, failure=failure)
396
+ recorder.record_check_failure(
397
+ name=name,
398
+ case_id=failure_data.case.id,
399
+ code_sample=failure_data.case.as_curl_command(headers=failure_data.headers, verify=failure_data.verify),
400
+ failure=failure,
401
+ )
402
+
403
+ def on_success(name: str, _case: Case) -> None:
404
+ recorder.record_check_success(name=name, case_id=_case.id)
405
+
406
+ failures = run_checks(
407
+ case=case,
408
+ response=response,
409
+ ctx=ctx,
410
+ checks=ctx._checks,
411
+ on_failure=on_failure,
412
+ on_success=on_success,
413
+ )
414
+
415
+ if failures and not continue_on_failure:
416
+ raise FailureGroup(list(failures)) from None
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ import uuid
5
+ from queue import Queue
6
+ from types import TracebackType
7
+ from typing import TYPE_CHECKING, Callable
8
+
9
+ from schemathesis.core.result import Result
10
+ from schemathesis.engine.phases import PhaseName
11
+
12
+ if TYPE_CHECKING:
13
+ from schemathesis.engine.context import EngineContext
14
+ from schemathesis.generation.hypothesis.builder import HypothesisTestMode
15
+
16
+
17
+ class TaskProducer:
18
+ """Produces test tasks for workers to execute."""
19
+
20
+ def __init__(self, ctx: EngineContext) -> None:
21
+ self.operations = ctx.schema.get_all_operations()
22
+ self.lock = threading.Lock()
23
+
24
+ def next_operation(self) -> Result | None:
25
+ """Get next API operation in a thread-safe manner."""
26
+ with self.lock:
27
+ return next(self.operations, None)
28
+
29
+
30
+ class WorkerPool:
31
+ """Manages a pool of worker threads."""
32
+
33
+ def __init__(
34
+ self,
35
+ workers_num: int,
36
+ producer: TaskProducer,
37
+ worker_factory: Callable,
38
+ ctx: EngineContext,
39
+ mode: HypothesisTestMode,
40
+ phase: PhaseName,
41
+ suite_id: uuid.UUID,
42
+ ) -> None:
43
+ self.workers_num = workers_num
44
+ self.producer = producer
45
+ self.worker_factory = worker_factory
46
+ self.ctx = ctx
47
+ self.mode = mode
48
+ self.phase = phase
49
+ self.suite_id = suite_id
50
+ self.workers: list[threading.Thread] = []
51
+ self.events_queue: Queue = Queue()
52
+
53
+ def start(self) -> None:
54
+ """Start all worker threads."""
55
+ for i in range(self.workers_num):
56
+ worker = threading.Thread(
57
+ target=self.worker_factory,
58
+ kwargs={
59
+ "ctx": self.ctx,
60
+ "mode": self.mode,
61
+ "phase": self.phase,
62
+ "events_queue": self.events_queue,
63
+ "producer": self.producer,
64
+ "suite_id": self.suite_id,
65
+ },
66
+ name=f"schemathesis_unit_tests_{i}",
67
+ daemon=True,
68
+ )
69
+ self.workers.append(worker)
70
+ worker.start()
71
+
72
+ def stop(self) -> None:
73
+ """Stop all workers gracefully."""
74
+ for worker in self.workers:
75
+ worker.join()
76
+
77
+ def __enter__(self) -> WorkerPool:
78
+ self.start()
79
+ return self
80
+
81
+ def __exit__(self, ty: type[BaseException] | None, value: BaseException | None, tb: TracebackType | None) -> None:
82
+ self.stop()