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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. schemathesis/__init__.py +53 -25
  2. schemathesis/auths.py +507 -0
  3. schemathesis/checks.py +190 -25
  4. schemathesis/cli/__init__.py +27 -1219
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +133 -0
  7. schemathesis/cli/commands/data.py +10 -0
  8. schemathesis/cli/commands/run/__init__.py +602 -0
  9. schemathesis/cli/commands/run/context.py +228 -0
  10. schemathesis/cli/commands/run/events.py +60 -0
  11. schemathesis/cli/commands/run/executor.py +157 -0
  12. schemathesis/cli/commands/run/filters.py +53 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +45 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
  17. schemathesis/cli/commands/run/handlers/output.py +1750 -0
  18. schemathesis/cli/commands/run/loaders.py +118 -0
  19. schemathesis/cli/commands/run/validation.py +256 -0
  20. schemathesis/cli/constants.py +5 -0
  21. schemathesis/cli/core.py +19 -0
  22. schemathesis/cli/ext/fs.py +16 -0
  23. schemathesis/cli/ext/groups.py +203 -0
  24. schemathesis/cli/ext/options.py +81 -0
  25. schemathesis/config/__init__.py +202 -0
  26. schemathesis/config/_auth.py +51 -0
  27. schemathesis/config/_checks.py +268 -0
  28. schemathesis/config/_diff_base.py +101 -0
  29. schemathesis/config/_env.py +21 -0
  30. schemathesis/config/_error.py +163 -0
  31. schemathesis/config/_generation.py +157 -0
  32. schemathesis/config/_health_check.py +24 -0
  33. schemathesis/config/_operations.py +335 -0
  34. schemathesis/config/_output.py +171 -0
  35. schemathesis/config/_parameters.py +19 -0
  36. schemathesis/config/_phases.py +253 -0
  37. schemathesis/config/_projects.py +543 -0
  38. schemathesis/config/_rate_limit.py +17 -0
  39. schemathesis/config/_report.py +120 -0
  40. schemathesis/config/_validator.py +9 -0
  41. schemathesis/config/_warnings.py +89 -0
  42. schemathesis/config/schema.json +975 -0
  43. schemathesis/core/__init__.py +72 -0
  44. schemathesis/core/adapter.py +34 -0
  45. schemathesis/core/compat.py +32 -0
  46. schemathesis/core/control.py +2 -0
  47. schemathesis/core/curl.py +100 -0
  48. schemathesis/core/deserialization.py +210 -0
  49. schemathesis/core/errors.py +588 -0
  50. schemathesis/core/failures.py +316 -0
  51. schemathesis/core/fs.py +19 -0
  52. schemathesis/core/hooks.py +20 -0
  53. schemathesis/core/jsonschema/__init__.py +13 -0
  54. schemathesis/core/jsonschema/bundler.py +183 -0
  55. schemathesis/core/jsonschema/keywords.py +40 -0
  56. schemathesis/core/jsonschema/references.py +222 -0
  57. schemathesis/core/jsonschema/types.py +41 -0
  58. schemathesis/core/lazy_import.py +15 -0
  59. schemathesis/core/loaders.py +107 -0
  60. schemathesis/core/marks.py +66 -0
  61. schemathesis/core/media_types.py +79 -0
  62. schemathesis/core/output/__init__.py +46 -0
  63. schemathesis/core/output/sanitization.py +54 -0
  64. schemathesis/core/parameters.py +45 -0
  65. schemathesis/core/rate_limit.py +60 -0
  66. schemathesis/core/registries.py +34 -0
  67. schemathesis/core/result.py +27 -0
  68. schemathesis/core/schema_analysis.py +17 -0
  69. schemathesis/core/shell.py +203 -0
  70. schemathesis/core/transforms.py +144 -0
  71. schemathesis/core/transport.py +223 -0
  72. schemathesis/core/validation.py +73 -0
  73. schemathesis/core/version.py +7 -0
  74. schemathesis/engine/__init__.py +28 -0
  75. schemathesis/engine/context.py +152 -0
  76. schemathesis/engine/control.py +44 -0
  77. schemathesis/engine/core.py +201 -0
  78. schemathesis/engine/errors.py +446 -0
  79. schemathesis/engine/events.py +284 -0
  80. schemathesis/engine/observations.py +42 -0
  81. schemathesis/engine/phases/__init__.py +108 -0
  82. schemathesis/engine/phases/analysis.py +28 -0
  83. schemathesis/engine/phases/probes.py +172 -0
  84. schemathesis/engine/phases/stateful/__init__.py +68 -0
  85. schemathesis/engine/phases/stateful/_executor.py +364 -0
  86. schemathesis/engine/phases/stateful/context.py +85 -0
  87. schemathesis/engine/phases/unit/__init__.py +220 -0
  88. schemathesis/engine/phases/unit/_executor.py +459 -0
  89. schemathesis/engine/phases/unit/_pool.py +82 -0
  90. schemathesis/engine/recorder.py +254 -0
  91. schemathesis/errors.py +47 -0
  92. schemathesis/filters.py +395 -0
  93. schemathesis/generation/__init__.py +25 -0
  94. schemathesis/generation/case.py +478 -0
  95. schemathesis/generation/coverage.py +1528 -0
  96. schemathesis/generation/hypothesis/__init__.py +121 -0
  97. schemathesis/generation/hypothesis/builder.py +992 -0
  98. schemathesis/generation/hypothesis/examples.py +56 -0
  99. schemathesis/generation/hypothesis/given.py +66 -0
  100. schemathesis/generation/hypothesis/reporting.py +285 -0
  101. schemathesis/generation/meta.py +227 -0
  102. schemathesis/generation/metrics.py +93 -0
  103. schemathesis/generation/modes.py +20 -0
  104. schemathesis/generation/overrides.py +127 -0
  105. schemathesis/generation/stateful/__init__.py +37 -0
  106. schemathesis/generation/stateful/state_machine.py +294 -0
  107. schemathesis/graphql/__init__.py +15 -0
  108. schemathesis/graphql/checks.py +109 -0
  109. schemathesis/graphql/loaders.py +285 -0
  110. schemathesis/hooks.py +270 -91
  111. schemathesis/openapi/__init__.py +13 -0
  112. schemathesis/openapi/checks.py +467 -0
  113. schemathesis/openapi/generation/__init__.py +0 -0
  114. schemathesis/openapi/generation/filters.py +72 -0
  115. schemathesis/openapi/loaders.py +315 -0
  116. schemathesis/pytest/__init__.py +5 -0
  117. schemathesis/pytest/control_flow.py +7 -0
  118. schemathesis/pytest/lazy.py +341 -0
  119. schemathesis/pytest/loaders.py +36 -0
  120. schemathesis/pytest/plugin.py +357 -0
  121. schemathesis/python/__init__.py +0 -0
  122. schemathesis/python/asgi.py +12 -0
  123. schemathesis/python/wsgi.py +12 -0
  124. schemathesis/schemas.py +682 -257
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +26 -2
  127. schemathesis/specs/graphql/scalars.py +77 -12
  128. schemathesis/specs/graphql/schemas.py +367 -148
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +555 -318
  132. schemathesis/specs/openapi/adapter/__init__.py +10 -0
  133. schemathesis/specs/openapi/adapter/parameters.py +729 -0
  134. schemathesis/specs/openapi/adapter/protocol.py +59 -0
  135. schemathesis/specs/openapi/adapter/references.py +19 -0
  136. schemathesis/specs/openapi/adapter/responses.py +368 -0
  137. schemathesis/specs/openapi/adapter/security.py +144 -0
  138. schemathesis/specs/openapi/adapter/v2.py +30 -0
  139. schemathesis/specs/openapi/adapter/v3_0.py +30 -0
  140. schemathesis/specs/openapi/adapter/v3_1.py +30 -0
  141. schemathesis/specs/openapi/analysis.py +96 -0
  142. schemathesis/specs/openapi/checks.py +748 -82
  143. schemathesis/specs/openapi/converter.py +176 -37
  144. schemathesis/specs/openapi/definitions.py +599 -4
  145. schemathesis/specs/openapi/examples.py +581 -165
  146. schemathesis/specs/openapi/expressions/__init__.py +52 -5
  147. schemathesis/specs/openapi/expressions/extractors.py +25 -0
  148. schemathesis/specs/openapi/expressions/lexer.py +34 -31
  149. schemathesis/specs/openapi/expressions/nodes.py +97 -46
  150. schemathesis/specs/openapi/expressions/parser.py +35 -13
  151. schemathesis/specs/openapi/formats.py +122 -0
  152. schemathesis/specs/openapi/media_types.py +75 -0
  153. schemathesis/specs/openapi/negative/__init__.py +93 -73
  154. schemathesis/specs/openapi/negative/mutations.py +294 -103
  155. schemathesis/specs/openapi/negative/utils.py +0 -9
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +647 -666
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +403 -68
  161. schemathesis/specs/openapi/stateful/control.py +87 -0
  162. schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
  163. schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
  164. schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
  165. schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
  166. schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
  167. schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
  168. schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
  169. schemathesis/specs/openapi/stateful/inference.py +254 -0
  170. schemathesis/specs/openapi/stateful/links.py +219 -78
  171. schemathesis/specs/openapi/types/__init__.py +3 -0
  172. schemathesis/specs/openapi/types/common.py +23 -0
  173. schemathesis/specs/openapi/types/v2.py +129 -0
  174. schemathesis/specs/openapi/types/v3.py +134 -0
  175. schemathesis/specs/openapi/utils.py +7 -6
  176. schemathesis/specs/openapi/warnings.py +75 -0
  177. schemathesis/transport/__init__.py +224 -0
  178. schemathesis/transport/asgi.py +26 -0
  179. schemathesis/transport/prepare.py +126 -0
  180. schemathesis/transport/requests.py +278 -0
  181. schemathesis/transport/serialization.py +329 -0
  182. schemathesis/transport/wsgi.py +175 -0
  183. schemathesis-4.4.2.dist-info/METADATA +213 -0
  184. schemathesis-4.4.2.dist-info/RECORD +192 -0
  185. {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
  186. schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
  187. {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -57
  189. schemathesis/_hypothesis.py +0 -123
  190. schemathesis/auth.py +0 -214
  191. schemathesis/cli/callbacks.py +0 -240
  192. schemathesis/cli/cassettes.py +0 -351
  193. schemathesis/cli/context.py +0 -38
  194. schemathesis/cli/debug.py +0 -21
  195. schemathesis/cli/handlers.py +0 -11
  196. schemathesis/cli/junitxml.py +0 -41
  197. schemathesis/cli/options.py +0 -70
  198. schemathesis/cli/output/__init__.py +0 -1
  199. schemathesis/cli/output/default.py +0 -521
  200. schemathesis/cli/output/short.py +0 -40
  201. schemathesis/constants.py +0 -88
  202. schemathesis/exceptions.py +0 -257
  203. schemathesis/extra/_aiohttp.py +0 -27
  204. schemathesis/extra/_flask.py +0 -10
  205. schemathesis/extra/_server.py +0 -16
  206. schemathesis/extra/pytest_plugin.py +0 -251
  207. schemathesis/failures.py +0 -145
  208. schemathesis/fixups/__init__.py +0 -29
  209. schemathesis/fixups/fast_api.py +0 -30
  210. schemathesis/graphql.py +0 -5
  211. schemathesis/internal.py +0 -6
  212. schemathesis/lazy.py +0 -301
  213. schemathesis/models.py +0 -1113
  214. schemathesis/parameters.py +0 -91
  215. schemathesis/runner/__init__.py +0 -470
  216. schemathesis/runner/events.py +0 -242
  217. schemathesis/runner/impl/__init__.py +0 -3
  218. schemathesis/runner/impl/core.py +0 -791
  219. schemathesis/runner/impl/solo.py +0 -85
  220. schemathesis/runner/impl/threadpool.py +0 -367
  221. schemathesis/runner/serialization.py +0 -206
  222. schemathesis/serializers.py +0 -253
  223. schemathesis/service/__init__.py +0 -18
  224. schemathesis/service/auth.py +0 -10
  225. schemathesis/service/client.py +0 -62
  226. schemathesis/service/constants.py +0 -25
  227. schemathesis/service/events.py +0 -39
  228. schemathesis/service/handler.py +0 -46
  229. schemathesis/service/hosts.py +0 -74
  230. schemathesis/service/metadata.py +0 -42
  231. schemathesis/service/models.py +0 -21
  232. schemathesis/service/serialization.py +0 -184
  233. schemathesis/service/worker.py +0 -39
  234. schemathesis/specs/graphql/loaders.py +0 -215
  235. schemathesis/specs/openapi/constants.py +0 -7
  236. schemathesis/specs/openapi/expressions/context.py +0 -12
  237. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  238. schemathesis/specs/openapi/filters.py +0 -44
  239. schemathesis/specs/openapi/links.py +0 -303
  240. schemathesis/specs/openapi/loaders.py +0 -453
  241. schemathesis/specs/openapi/parameters.py +0 -430
  242. schemathesis/specs/openapi/security.py +0 -129
  243. schemathesis/specs/openapi/validation.py +0 -24
  244. schemathesis/stateful.py +0 -358
  245. schemathesis/targets.py +0 -32
  246. schemathesis/types.py +0 -38
  247. schemathesis/utils.py +0 -475
  248. schemathesis-3.15.4.dist-info/METADATA +0 -202
  249. schemathesis-3.15.4.dist-info/RECORD +0 -99
  250. schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
  251. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
@@ -0,0 +1,459 @@
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 jsonschema.exceptions import SchemaError as JsonSchemaError
12
+ from jsonschema.exceptions import ValidationError
13
+ from requests.exceptions import ChunkedEncodingError
14
+ from requests.structures import CaseInsensitiveDict
15
+
16
+ from schemathesis.checks import CheckContext, run_checks
17
+ from schemathesis.config._generation import GenerationConfig
18
+ from schemathesis.core.compat import BaseExceptionGroup
19
+ from schemathesis.core.control import SkipTest
20
+ from schemathesis.core.errors import (
21
+ SERIALIZERS_SUGGESTION_MESSAGE,
22
+ AuthenticationError,
23
+ InternalError,
24
+ InvalidHeadersExample,
25
+ InvalidRegexPattern,
26
+ InvalidRegexType,
27
+ InvalidSchema,
28
+ MalformedMediaType,
29
+ SchemaLocation,
30
+ SerializationNotPossible,
31
+ )
32
+ from schemathesis.core.failures import Failure, FailureGroup
33
+ from schemathesis.core.transport import Response
34
+ from schemathesis.engine import Status, events
35
+ from schemathesis.engine.context import EngineContext
36
+ from schemathesis.engine.errors import (
37
+ DeadlineExceeded,
38
+ TestingState,
39
+ UnexpectedError,
40
+ UnrecoverableNetworkError,
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
+ InfiniteRecursiveReferenceMark,
51
+ InvalidHeadersExampleMark,
52
+ InvalidRegexMark,
53
+ MissingPathParameters,
54
+ NonSerializableMark,
55
+ UnresolvableReferenceMark,
56
+ UnsatisfiableExampleMark,
57
+ )
58
+ from schemathesis.generation.hypothesis.reporting import (
59
+ build_health_check_error,
60
+ build_unsatisfiable_error,
61
+ ignore_hypothesis_output,
62
+ )
63
+
64
+ if TYPE_CHECKING:
65
+ from schemathesis.schemas import APIOperation
66
+
67
+
68
+ def run_test(
69
+ *,
70
+ operation: APIOperation,
71
+ test_function: Callable,
72
+ ctx: EngineContext,
73
+ phase: PhaseName,
74
+ suite_id: uuid.UUID,
75
+ scenario_id: uuid.UUID,
76
+ ) -> events.EventGenerator:
77
+ """A single test run with all error handling needed."""
78
+ import hypothesis.errors
79
+
80
+ errors: list[Exception] = []
81
+ skip_reason = None
82
+ error: Exception
83
+ test_start_time = time.monotonic()
84
+ recorder = ScenarioRecorder(label=operation.label)
85
+ state = TestingState()
86
+
87
+ def non_fatal_error(error: Exception, code_sample: str | None = None) -> events.NonFatalError:
88
+ return events.NonFatalError(
89
+ error=error, phase=phase, label=operation.label, related_to_operation=True, code_sample=code_sample
90
+ )
91
+
92
+ def scenario_finished(status: Status) -> events.ScenarioFinished:
93
+ return events.ScenarioFinished(
94
+ id=scenario_id,
95
+ suite_id=suite_id,
96
+ phase=phase,
97
+ label=operation.label,
98
+ recorder=recorder,
99
+ status=status,
100
+ elapsed_time=time.monotonic() - test_start_time,
101
+ skip_reason=skip_reason,
102
+ is_final=False,
103
+ )
104
+
105
+ phase_name = phase.value.lower()
106
+ assert phase_name in ("examples", "coverage", "fuzzing", "stateful")
107
+
108
+ operation_config = ctx.config.operations.get_for_operation(operation)
109
+ continue_on_failure = operation_config.continue_on_failure or ctx.config.continue_on_failure or False
110
+ generation = ctx.config.generation_for(operation=operation, phase=phase_name)
111
+ override = overrides.for_operation(ctx.config, operation=operation)
112
+ auth = ctx.config.auth_for(operation=operation)
113
+ headers = ctx.config.headers_for(operation=operation)
114
+ transport_kwargs = ctx.get_transport_kwargs(operation=operation)
115
+ check_ctx = CheckContext(
116
+ override=override,
117
+ auth=auth,
118
+ headers=CaseInsensitiveDict(headers) if headers else None,
119
+ config=ctx.config.checks_config_for(operation=operation, phase=phase_name),
120
+ transport_kwargs=transport_kwargs,
121
+ recorder=recorder,
122
+ )
123
+
124
+ try:
125
+ setup_hypothesis_database_key(test_function, operation)
126
+ with catch_warnings(record=True) as warnings, ignore_hypothesis_output():
127
+ test_function(
128
+ ctx=ctx,
129
+ state=state,
130
+ errors=errors,
131
+ check_ctx=check_ctx,
132
+ recorder=recorder,
133
+ generation=generation,
134
+ transport_kwargs=transport_kwargs,
135
+ continue_on_failure=continue_on_failure,
136
+ )
137
+ # Test body was not executed at all - Hypothesis did not generate any tests, but there is no error
138
+ status = Status.SUCCESS
139
+ except (SkipTest, unittest.case.SkipTest) as exc:
140
+ status = Status.SKIP
141
+ skip_reason = {"Hypothesis has been told to run no examples for this test.": "No examples in schema"}.get(
142
+ str(exc), str(exc)
143
+ )
144
+ except (FailureGroup, Failure):
145
+ status = Status.FAILURE
146
+ except UnexpectedError:
147
+ # It could be an error in user-defined extensions, network errors or internal Schemathesis errors
148
+ status = Status.ERROR
149
+ for idx, err in enumerate(errors):
150
+ if isinstance(err, MalformedMediaType):
151
+ errors[idx] = InvalidSchema(str(err))
152
+ except hypothesis.errors.Flaky as exc:
153
+ if isinstance(exc.__cause__, hypothesis.errors.DeadlineExceeded):
154
+ status = Status.ERROR
155
+ yield non_fatal_error(DeadlineExceeded.from_exc(exc.__cause__))
156
+ elif isinstance(exc, hypothesis.errors.FlakyFailure) and any(
157
+ isinstance(subexc, hypothesis.errors.DeadlineExceeded) for subexc in exc.exceptions
158
+ ):
159
+ for sub_exc in exc.exceptions:
160
+ if isinstance(sub_exc, hypothesis.errors.DeadlineExceeded):
161
+ yield non_fatal_error(DeadlineExceeded.from_exc(sub_exc))
162
+ status = Status.ERROR
163
+ elif errors:
164
+ status = Status.ERROR
165
+ else:
166
+ status = Status.FAILURE
167
+ except BaseExceptionGroup:
168
+ status = Status.ERROR
169
+ except hypothesis.errors.FailedHealthCheck as exc:
170
+ status = Status.ERROR
171
+ yield non_fatal_error(build_health_check_error(operation, exc, with_tip=False))
172
+ except hypothesis.errors.Unsatisfiable:
173
+ # We need more clear error message here
174
+ status = Status.ERROR
175
+ yield non_fatal_error(build_unsatisfiable_error(operation, with_tip=False))
176
+ except AuthenticationError as exc:
177
+ status = Status.ERROR
178
+ yield non_fatal_error(exc)
179
+ except KeyboardInterrupt:
180
+ yield scenario_finished(Status.INTERRUPTED)
181
+ yield events.Interrupted(phase=phase)
182
+ return
183
+ except AssertionError as exc: # May come from `hypothesis-jsonschema` or `hypothesis`
184
+ status = Status.ERROR
185
+ try:
186
+ operation.schema.validate()
187
+ # JSON Schema validation can miss it if there is `$ref` adjacent to `type` on older specifications
188
+ if str(exc).startswith("Unknown type"):
189
+ yield non_fatal_error(
190
+ InvalidSchema(
191
+ message=str(exc),
192
+ path=operation.path,
193
+ method=operation.method,
194
+ )
195
+ )
196
+ else:
197
+ msg = "Unexpected error during testing of this API operation"
198
+ exc_msg = str(exc)
199
+ if exc_msg:
200
+ msg += f": {exc_msg}"
201
+ try:
202
+ raise InternalError(msg) from exc
203
+ except InternalError as exc:
204
+ yield non_fatal_error(exc)
205
+ except ValidationError as exc:
206
+ yield non_fatal_error(
207
+ InvalidSchema.from_jsonschema_error(
208
+ exc,
209
+ path=operation.path,
210
+ method=operation.method,
211
+ config=ctx.config.output,
212
+ location=SchemaLocation.maybe_from_error_path(
213
+ list(exc.absolute_path), ctx.schema.specification.version
214
+ ),
215
+ )
216
+ )
217
+ except InvalidArgument as exc:
218
+ status = Status.ERROR
219
+ message = get_invalid_regular_expression_message(warnings)
220
+ if message:
221
+ # `hypothesis-jsonschema` emits a warning on invalid regular expression syntax
222
+ yield non_fatal_error(InvalidRegexPattern.from_hypothesis_jsonschema_message(message))
223
+ else:
224
+ health_check = build_health_check_error(operation, exc, with_tip=False)
225
+ if isinstance(health_check, hypothesis.errors.FailedHealthCheck):
226
+ yield non_fatal_error(health_check)
227
+ else:
228
+ yield non_fatal_error(exc)
229
+ except hypothesis.errors.DeadlineExceeded as exc:
230
+ status = Status.ERROR
231
+ yield non_fatal_error(DeadlineExceeded.from_exc(exc))
232
+ except JsonSchemaError as exc:
233
+ status = Status.ERROR
234
+ yield non_fatal_error(InvalidRegexPattern.from_schema_error(exc, from_examples=False))
235
+ except Exception as exc:
236
+ status = Status.ERROR
237
+ clear_hypothesis_notes(exc)
238
+ # Likely a YAML parsing issue. E.g. `00:00:00.00` (without quotes) is parsed as float `0.0`
239
+ if str(exc) == "first argument must be string or compiled pattern":
240
+ yield non_fatal_error(
241
+ InvalidRegexType(
242
+ "Invalid `pattern` value: expected a string. "
243
+ "If your schema is in YAML, ensure `pattern` values are quoted",
244
+ )
245
+ )
246
+ else:
247
+ code_sample: str | None = None
248
+ if state.unrecoverable_network_error is not None and state.unrecoverable_network_error.error is exc:
249
+ code_sample = state.unrecoverable_network_error.code_sample
250
+ yield non_fatal_error(exc, code_sample=code_sample)
251
+ if (
252
+ status == Status.SUCCESS
253
+ and continue_on_failure
254
+ and any(check.status == Status.FAILURE for checks in recorder.checks.values() for check in checks)
255
+ ):
256
+ status = Status.FAILURE
257
+
258
+ # Check for various errors during generation (tests may still have been generated)
259
+
260
+ if UnsatisfiableExampleMark.is_set(test_function):
261
+ status = Status.ERROR
262
+ yield non_fatal_error(
263
+ hypothesis.errors.Unsatisfiable("Failed to generate test cases from examples for this API operation")
264
+ )
265
+
266
+ non_serializable = NonSerializableMark.get(test_function)
267
+ if non_serializable is not None and status != Status.ERROR:
268
+ status = Status.ERROR
269
+ media_types = ", ".join(non_serializable.media_types)
270
+ yield non_fatal_error(
271
+ SerializationNotPossible(
272
+ "Failed to generate test cases from examples for this API operation because of"
273
+ f" unsupported payload media types: {media_types}\n{SERIALIZERS_SUGGESTION_MESSAGE}",
274
+ media_types=non_serializable.media_types,
275
+ )
276
+ )
277
+
278
+ invalid_regex = InvalidRegexMark.get(test_function)
279
+ if invalid_regex is not None and status != Status.ERROR:
280
+ status = Status.ERROR
281
+ yield non_fatal_error(InvalidRegexPattern.from_schema_error(invalid_regex, from_examples=True))
282
+
283
+ invalid_headers = InvalidHeadersExampleMark.get(test_function)
284
+ if invalid_headers:
285
+ status = Status.ERROR
286
+ yield non_fatal_error(InvalidHeadersExample.from_headers(invalid_headers))
287
+
288
+ missing_path_parameters = MissingPathParameters.get(test_function)
289
+ if missing_path_parameters:
290
+ status = Status.ERROR
291
+ yield non_fatal_error(missing_path_parameters)
292
+
293
+ infinite_recursive_reference = InfiniteRecursiveReferenceMark.get(test_function)
294
+ if infinite_recursive_reference:
295
+ status = Status.ERROR
296
+ yield non_fatal_error(infinite_recursive_reference)
297
+
298
+ unresolvable_reference = UnresolvableReferenceMark.get(test_function)
299
+ if unresolvable_reference:
300
+ status = Status.ERROR
301
+ yield non_fatal_error(unresolvable_reference)
302
+
303
+ for error in deduplicate_errors(errors):
304
+ yield non_fatal_error(error)
305
+
306
+ yield scenario_finished(status)
307
+
308
+
309
+ def setup_hypothesis_database_key(test: Callable, operation: APIOperation) -> None:
310
+ """Make Hypothesis use separate database entries for every API operation.
311
+
312
+ It increases the effectiveness of the Hypothesis database in the CLI.
313
+ """
314
+ test.hypothesis.inner_test._hypothesis_internal_add_digest = operation.label.encode("utf8") # type: ignore[attr-defined]
315
+
316
+
317
+ def get_invalid_regular_expression_message(warnings: list[WarningMessage]) -> str | None:
318
+ for warning in warnings:
319
+ message = str(warning.message)
320
+ if "is not valid syntax for a Python regular expression" in message:
321
+ return message
322
+ return None
323
+
324
+
325
+ def cached_test_func(f: Callable) -> Callable:
326
+ def wrapped(
327
+ *,
328
+ ctx: EngineContext,
329
+ state: TestingState,
330
+ case: Case,
331
+ errors: list[Exception],
332
+ check_ctx: CheckContext,
333
+ recorder: ScenarioRecorder,
334
+ generation: GenerationConfig,
335
+ transport_kwargs: dict[str, Any],
336
+ continue_on_failure: bool,
337
+ ) -> None:
338
+ try:
339
+ if ctx.has_to_stop:
340
+ raise KeyboardInterrupt
341
+ if generation.unique_inputs:
342
+ cached = ctx.get_cached_outcome(case)
343
+ if isinstance(cached, BaseException):
344
+ raise cached
345
+ elif cached is None:
346
+ return None
347
+ try:
348
+ f(
349
+ case=case,
350
+ check_ctx=check_ctx,
351
+ recorder=recorder,
352
+ generation=generation,
353
+ transport_kwargs=transport_kwargs,
354
+ continue_on_failure=continue_on_failure,
355
+ )
356
+ except BaseException as exc:
357
+ ctx.cache_outcome(case, exc)
358
+ raise
359
+ else:
360
+ ctx.cache_outcome(case, None)
361
+ else:
362
+ f(
363
+ case=case,
364
+ check_ctx=check_ctx,
365
+ recorder=recorder,
366
+ generation=generation,
367
+ transport_kwargs=transport_kwargs,
368
+ continue_on_failure=continue_on_failure,
369
+ )
370
+ except (KeyboardInterrupt, Failure):
371
+ raise
372
+ except Exception as exc:
373
+ if isinstance(
374
+ exc, (requests.ConnectionError, ChunkedEncodingError, requests.Timeout)
375
+ ) and is_unrecoverable_network_error(exc):
376
+ # Server likely has crashed and does not accept any connections at all
377
+ # Don't report these error - only the original crash should be reported
378
+ if exc.request is not None:
379
+ headers = {key: value[0] for key, value in exc.request.headers.items()}
380
+ else:
381
+ headers = {**dict(case.headers or {}), **transport_kwargs.get("headers", {})}
382
+ verify = transport_kwargs.get("verify", True)
383
+ state.unrecoverable_network_error = UnrecoverableNetworkError(
384
+ error=exc,
385
+ code_sample=case.as_curl_command(headers=headers, verify=verify),
386
+ )
387
+ raise
388
+ errors.append(exc)
389
+ raise UnexpectedError from None
390
+
391
+ wrapped.__name__ = f.__name__
392
+
393
+ return wrapped
394
+
395
+
396
+ @cached_test_func
397
+ def test_func(
398
+ *,
399
+ case: Case,
400
+ check_ctx: CheckContext,
401
+ recorder: ScenarioRecorder,
402
+ generation: GenerationConfig,
403
+ transport_kwargs: dict[str, Any],
404
+ continue_on_failure: bool,
405
+ ) -> None:
406
+ recorder.record_case(parent_id=None, case=case, transition=None, is_transition_applied=False)
407
+ try:
408
+ response = case.call(**transport_kwargs)
409
+ except (requests.Timeout, requests.ConnectionError, ChunkedEncodingError) as error:
410
+ if isinstance(error.request, requests.Request):
411
+ recorder.record_request(case_id=case.id, request=error.request.prepare())
412
+ elif isinstance(error.request, requests.PreparedRequest):
413
+ recorder.record_request(case_id=case.id, request=error.request)
414
+ raise
415
+ recorder.record_response(case_id=case.id, response=response)
416
+ metrics.maximize(generation.maximize, case=case, response=response)
417
+ validate_response(
418
+ case=case,
419
+ ctx=check_ctx,
420
+ response=response,
421
+ continue_on_failure=continue_on_failure,
422
+ recorder=recorder,
423
+ )
424
+
425
+
426
+ def validate_response(
427
+ *,
428
+ case: Case,
429
+ ctx: CheckContext,
430
+ response: Response,
431
+ continue_on_failure: bool,
432
+ recorder: ScenarioRecorder,
433
+ ) -> None:
434
+ failures = set()
435
+
436
+ def on_failure(name: str, collected: set[Failure], failure: Failure) -> None:
437
+ collected.add(failure)
438
+ failure_data = recorder.find_failure_data(parent_id=case.id, failure=failure)
439
+ recorder.record_check_failure(
440
+ name=name,
441
+ case_id=failure_data.case.id,
442
+ code_sample=failure_data.case.as_curl_command(headers=failure_data.headers, verify=failure_data.verify),
443
+ failure=failure,
444
+ )
445
+
446
+ def on_success(name: str, _case: Case) -> None:
447
+ recorder.record_check_success(name=name, case_id=_case.id)
448
+
449
+ failures = run_checks(
450
+ case=case,
451
+ response=response,
452
+ ctx=ctx,
453
+ checks=ctx._checks,
454
+ on_failure=on_failure,
455
+ on_success=on_success,
456
+ )
457
+
458
+ if failures and not continue_on_failure:
459
+ 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()