schemathesis 3.25.5__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 -1766
  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/{cli → engine/phases}/probes.py +63 -70
  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 +153 -39
  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 +483 -367
  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.5.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
  144. {schemathesis-3.25.5.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 -55
  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 -765
  156. schemathesis/cli/output/short.py +0 -40
  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 -1231
  182. schemathesis/parameters.py +0 -86
  183. schemathesis/runner/__init__.py +0 -555
  184. schemathesis/runner/events.py +0 -309
  185. schemathesis/runner/impl/__init__.py +0 -3
  186. schemathesis/runner/impl/core.py +0 -986
  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 -315
  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 -184
  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.5.dist-info/METADATA +0 -356
  219. schemathesis-3.25.5.dist-info/RECORD +0 -134
  220. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  221. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,394 @@
1
+ """Handling of recoverable errors in Schemathesis Engine.
2
+
3
+ This module provides utilities for analyzing, classifying, and formatting exceptions
4
+ that occur during test execution via Schemathesis Engine.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import enum
10
+ import re
11
+ from functools import cached_property
12
+ from typing import TYPE_CHECKING, Callable, Iterator, Sequence, cast
13
+
14
+ from schemathesis import errors
15
+ from schemathesis.core.errors import (
16
+ RECURSIVE_REFERENCE_ERROR_MESSAGE,
17
+ SerializationNotPossible,
18
+ format_exception,
19
+ get_request_error_extras,
20
+ get_request_error_message,
21
+ split_traceback,
22
+ )
23
+
24
+ if TYPE_CHECKING:
25
+ import hypothesis.errors
26
+
27
+ __all__ = ["EngineErrorInfo", "DeadlineExceeded", "UnsupportedRecursiveReference", "UnexpectedError"]
28
+
29
+
30
+ class DeadlineExceeded(errors.SchemathesisError):
31
+ """Test took too long to run."""
32
+
33
+ @classmethod
34
+ def from_exc(cls, exc: hypothesis.errors.DeadlineExceeded) -> DeadlineExceeded:
35
+ runtime = exc.runtime.total_seconds() * 1000
36
+ deadline = exc.deadline.total_seconds() * 1000
37
+ return cls(
38
+ f"Test running time is too slow! It took {runtime:.2f}ms, which exceeds the deadline of {deadline:.2f}ms.\n"
39
+ )
40
+
41
+
42
+ class UnsupportedRecursiveReference(errors.SchemathesisError):
43
+ """Recursive reference is impossible to resolve due to current limitations."""
44
+
45
+ def __init__(self) -> None:
46
+ super().__init__(RECURSIVE_REFERENCE_ERROR_MESSAGE)
47
+
48
+
49
+ class UnexpectedError(errors.SchemathesisError):
50
+ """An unexpected error during the engine execution.
51
+
52
+ Used primarily to not let Hypothesis consider the test as flaky or detect multiple failures as we handle it
53
+ on our side.
54
+ """
55
+
56
+
57
+ class EngineErrorInfo:
58
+ """Extended information about errors that happen during engine execution.
59
+
60
+ It serves as a caching wrapper around exceptions to avoid repeated computations.
61
+ """
62
+
63
+ def __init__(self, error: Exception) -> None:
64
+ self._error = error
65
+
66
+ def __str__(self) -> str:
67
+ return self._error_repr
68
+
69
+ @cached_property
70
+ def _kind(self) -> RuntimeErrorKind:
71
+ """Error kind."""
72
+ return _classify(error=self._error)
73
+
74
+ @property
75
+ def title(self) -> str:
76
+ """A general error description."""
77
+ import requests
78
+
79
+ if isinstance(self._error, requests.RequestException):
80
+ return "Network Error"
81
+
82
+ if self._kind in (
83
+ RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE,
84
+ RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH,
85
+ RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW,
86
+ RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE,
87
+ ):
88
+ return "Failed Health Check"
89
+
90
+ if self._kind in (
91
+ RuntimeErrorKind.SCHEMA_INVALID_REGULAR_EXPRESSION,
92
+ RuntimeErrorKind.SCHEMA_GENERIC,
93
+ RuntimeErrorKind.HYPOTHESIS_UNSATISFIABLE,
94
+ ):
95
+ return "Schema Error"
96
+
97
+ return {
98
+ RuntimeErrorKind.SCHEMA_UNSUPPORTED: "Unsupported Schema",
99
+ RuntimeErrorKind.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR: "Unknown GraphQL Scalar",
100
+ RuntimeErrorKind.SERIALIZATION_UNBOUNDED_PREFIX: "XML serialization error",
101
+ RuntimeErrorKind.SERIALIZATION_NOT_POSSIBLE: "Serialization not possible",
102
+ }.get(self._kind, "Runtime Error")
103
+
104
+ @property
105
+ def message(self) -> str:
106
+ """Detailed error description."""
107
+ import hypothesis.errors
108
+ import requests
109
+
110
+ if isinstance(self._error, requests.RequestException):
111
+ return get_request_error_message(self._error)
112
+
113
+ if self._kind == RuntimeErrorKind.SCHEMA_UNSUPPORTED:
114
+ return str(self._error).strip()
115
+
116
+ if self._kind == RuntimeErrorKind.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR and isinstance(
117
+ self._error, hypothesis.errors.InvalidArgument
118
+ ):
119
+ scalar_name = scalar_name_from_error(self._error)
120
+ return f"Scalar type '{scalar_name}' is not recognized"
121
+
122
+ if self._kind == RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE:
123
+ return HEALTH_CHECK_MESSAGE_DATA_TOO_LARGE
124
+ if self._kind == RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH:
125
+ return HEALTH_CHECK_MESSAGE_FILTER_TOO_MUCH
126
+ if self._kind == RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW:
127
+ return HEALTH_CHECK_MESSAGE_TOO_SLOW
128
+ if self._kind == RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE:
129
+ return HEALTH_CHECK_MESSAGE_LARGE_BASE_EXAMPLE
130
+
131
+ if self._kind == RuntimeErrorKind.HYPOTHESIS_UNSATISFIABLE:
132
+ return f"{self._error}. Possible reasons:"
133
+
134
+ if self._kind in (
135
+ RuntimeErrorKind.SCHEMA_INVALID_REGULAR_EXPRESSION,
136
+ RuntimeErrorKind.SCHEMA_GENERIC,
137
+ ):
138
+ return self._error.message # type: ignore
139
+
140
+ return str(self._error)
141
+
142
+ @cached_property
143
+ def extras(self) -> list[str]:
144
+ """Additional context about the error."""
145
+ import requests
146
+
147
+ if isinstance(self._error, requests.RequestException):
148
+ return get_request_error_extras(self._error)
149
+
150
+ if self._kind == RuntimeErrorKind.HYPOTHESIS_UNSATISFIABLE:
151
+ return [
152
+ "- Contradictory schema constraints, such as a minimum value exceeding the maximum.",
153
+ "- Invalid schema definitions for headers or cookies, for example allowing for non-ASCII characters.",
154
+ "- Excessive schema complexity, which hinders parameter generation.",
155
+ ]
156
+
157
+ return []
158
+
159
+ @cached_property
160
+ def _error_repr(self) -> str:
161
+ return format_exception(self._error, with_traceback=False)
162
+
163
+ @property
164
+ def has_useful_traceback(self) -> bool:
165
+ return self._kind not in (
166
+ RuntimeErrorKind.SCHEMA_INVALID_REGULAR_EXPRESSION,
167
+ RuntimeErrorKind.SCHEMA_UNSUPPORTED,
168
+ RuntimeErrorKind.SCHEMA_GENERIC,
169
+ RuntimeErrorKind.SERIALIZATION_NOT_POSSIBLE,
170
+ RuntimeErrorKind.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR,
171
+ RuntimeErrorKind.HYPOTHESIS_UNSATISFIABLE,
172
+ RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE,
173
+ RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW,
174
+ RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE,
175
+ RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH,
176
+ RuntimeErrorKind.NETWORK_OTHER,
177
+ )
178
+
179
+ @cached_property
180
+ def traceback(self) -> str:
181
+ return format_exception(self._error, with_traceback=True)
182
+
183
+ def format(self, *, bold: Callable[[str], str] = str, indent: str = " ") -> str:
184
+ """Format error message with optional styling and traceback."""
185
+ message = []
186
+
187
+ # Title
188
+ if self._kind == RuntimeErrorKind.SCHEMA_GENERIC:
189
+ title = "Schema Error"
190
+ else:
191
+ title = self.title
192
+ if title:
193
+ message.append(f"{title}\n")
194
+
195
+ # Main message
196
+ body = self.message or str(self._error)
197
+ message.append(body)
198
+
199
+ # Extras
200
+ if self.extras:
201
+ extras = self.extras
202
+ elif self.has_useful_traceback:
203
+ extras = split_traceback(self.traceback)
204
+ else:
205
+ extras = []
206
+
207
+ if extras:
208
+ message.append("") # Empty line before extras
209
+ message.extend(f"{indent}{extra}" for extra in extras)
210
+
211
+ # Suggestion
212
+ suggestion = get_runtime_error_suggestion(self._kind, bold=bold)
213
+ if suggestion is not None:
214
+ message.append(f"\nTip: {suggestion}")
215
+
216
+ return "\n".join(message)
217
+
218
+
219
+ def scalar_name_from_error(exception: hypothesis.errors.InvalidArgument) -> str:
220
+ # This one is always available as the format is checked upfront
221
+ match = re.search(r"Scalar '(\w+)' is not supported", str(exception))
222
+ match = cast(re.Match, match)
223
+ return match.group(1)
224
+
225
+
226
+ def extract_health_check_error(error: hypothesis.errors.FailedHealthCheck) -> hypothesis.HealthCheck | None:
227
+ from hypothesis import HealthCheck
228
+
229
+ match = re.search(r"add HealthCheck\.(\w+) to the suppress_health_check ", str(error))
230
+ if match:
231
+ return {
232
+ "data_too_large": HealthCheck.data_too_large,
233
+ "filter_too_much": HealthCheck.filter_too_much,
234
+ "too_slow": HealthCheck.too_slow,
235
+ "large_base_example": HealthCheck.large_base_example,
236
+ }.get(match.group(1))
237
+ return None
238
+
239
+
240
+ def get_runtime_error_suggestion(error_type: RuntimeErrorKind, bold: Callable[[str], str] = str) -> str | None:
241
+ """Get a user-friendly suggestion for handling the error."""
242
+
243
+ def _format_health_check_suggestion(label: str) -> str:
244
+ return f"Bypass this health check using {bold(f'`--suppress-health-check={label}`')}."
245
+
246
+ return {
247
+ RuntimeErrorKind.CONNECTION_SSL: f"Bypass SSL verification with {bold('`--request-tls-verify=false`')}.",
248
+ RuntimeErrorKind.HYPOTHESIS_UNSATISFIABLE: "Examine the schema for inconsistencies and consider simplifying it.",
249
+ RuntimeErrorKind.SCHEMA_INVALID_REGULAR_EXPRESSION: "Ensure your regex is compatible with Python's syntax.\n"
250
+ "For guidance, visit: https://docs.python.org/3/library/re.html",
251
+ RuntimeErrorKind.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR: "Define a custom strategy for it.\n"
252
+ "For guidance, visit: https://schemathesis.readthedocs.io/en/stable/graphql.html#custom-scalars",
253
+ RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE: _format_health_check_suggestion("data_too_large"),
254
+ RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH: _format_health_check_suggestion("filter_too_much"),
255
+ RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW: _format_health_check_suggestion("too_slow"),
256
+ RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE: _format_health_check_suggestion(
257
+ "large_base_example"
258
+ ),
259
+ }.get(error_type)
260
+
261
+
262
+ HEALTH_CHECK_MESSAGE_DATA_TOO_LARGE = """There's a notable occurrence of examples surpassing the maximum size limit.
263
+ Typically, generating excessively large examples can compromise the quality of test outcomes.
264
+
265
+ Consider revising the schema to more accurately represent typical use cases
266
+ or applying constraints to reduce the data size."""
267
+ HEALTH_CHECK_MESSAGE_FILTER_TOO_MUCH = """A significant number of generated examples are being filtered out, indicating
268
+ that the schema's constraints may be too complex.
269
+
270
+ This level of filtration can slow down testing and affect the distribution
271
+ of generated data. Review and simplify the schema constraints where
272
+ possible to mitigate this issue."""
273
+ HEALTH_CHECK_MESSAGE_TOO_SLOW = "Data generation is extremely slow. Consider reducing the complexity of the schema."
274
+ HEALTH_CHECK_MESSAGE_LARGE_BASE_EXAMPLE = """A health check has identified that the smallest example derived from the schema
275
+ is excessively large, potentially leading to inefficient test execution.
276
+
277
+ This is commonly due to schemas that specify large-scale data structures by
278
+ default, such as an array with an extensive number of elements.
279
+
280
+ Consider revising the schema to more accurately represent typical use cases
281
+ or applying constraints to reduce the data size."""
282
+
283
+
284
+ @enum.unique
285
+ class RuntimeErrorKind(str, enum.Enum):
286
+ """Classification of runtime errors."""
287
+
288
+ # Connection related issues
289
+ CONNECTION_SSL = "connection_ssl"
290
+ CONNECTION_OTHER = "connection_other"
291
+ NETWORK_OTHER = "network_other"
292
+
293
+ # Hypothesis issues
294
+ HYPOTHESIS_UNSATISFIABLE = "hypothesis_unsatisfiable"
295
+ HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR = "hypothesis_unsupported_graphql_scalar"
296
+ HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE = "hypothesis_health_check_data_too_large"
297
+ HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH = "hypothesis_health_check_filter_too_much"
298
+ HYPOTHESIS_HEALTH_CHECK_TOO_SLOW = "hypothesis_health_check_too_slow"
299
+ HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE = "hypothesis_health_check_large_base_example"
300
+
301
+ SCHEMA_INVALID_REGULAR_EXPRESSION = "schema_invalid_regular_expression"
302
+ SCHEMA_UNSUPPORTED = "schema_unsupported"
303
+ SCHEMA_GENERIC = "schema_generic"
304
+
305
+ SERIALIZATION_NOT_POSSIBLE = "serialization_not_possible"
306
+ SERIALIZATION_UNBOUNDED_PREFIX = "serialization_unbounded_prefix"
307
+
308
+ UNCLASSIFIED = "unclassified"
309
+
310
+
311
+ def _classify(*, error: Exception) -> RuntimeErrorKind:
312
+ """Classify an error."""
313
+ import hypothesis.errors
314
+ import requests
315
+ from hypothesis import HealthCheck
316
+
317
+ # Network-related errors
318
+ if isinstance(error, requests.RequestException):
319
+ if isinstance(error, requests.exceptions.SSLError):
320
+ return RuntimeErrorKind.CONNECTION_SSL
321
+ if isinstance(error, requests.exceptions.ConnectionError):
322
+ return RuntimeErrorKind.CONNECTION_OTHER
323
+ return RuntimeErrorKind.NETWORK_OTHER
324
+
325
+ # Hypothesis errors
326
+ if (
327
+ isinstance(error, hypothesis.errors.InvalidArgument)
328
+ and str(error).endswith("larger than Hypothesis is designed to handle")
329
+ or "can never generate an example, because min_size is larger than Hypothesis supports" in str(error)
330
+ ):
331
+ return RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE
332
+ if isinstance(error, hypothesis.errors.Unsatisfiable):
333
+ return RuntimeErrorKind.HYPOTHESIS_UNSATISFIABLE
334
+ if isinstance(error, hypothesis.errors.FailedHealthCheck):
335
+ health_check = extract_health_check_error(error)
336
+ if health_check is not None:
337
+ return {
338
+ HealthCheck.data_too_large: RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE,
339
+ HealthCheck.filter_too_much: RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH,
340
+ HealthCheck.too_slow: RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW,
341
+ HealthCheck.large_base_example: RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE,
342
+ }[health_check]
343
+ return RuntimeErrorKind.UNCLASSIFIED
344
+ if isinstance(error, hypothesis.errors.InvalidArgument) and str(error).startswith("Scalar "):
345
+ # Comes from `hypothesis-graphql`
346
+ return RuntimeErrorKind.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR
347
+
348
+ # Schema errors
349
+ if isinstance(error, errors.InvalidSchema):
350
+ if isinstance(error, errors.InvalidRegexPattern):
351
+ return RuntimeErrorKind.SCHEMA_INVALID_REGULAR_EXPRESSION
352
+ return RuntimeErrorKind.SCHEMA_GENERIC
353
+ if isinstance(error, UnsupportedRecursiveReference):
354
+ # Recursive references are not supported right now
355
+ return RuntimeErrorKind.SCHEMA_UNSUPPORTED
356
+ if isinstance(error, errors.SerializationError):
357
+ if isinstance(error, errors.UnboundPrefix):
358
+ return RuntimeErrorKind.SERIALIZATION_UNBOUNDED_PREFIX
359
+ return RuntimeErrorKind.SERIALIZATION_NOT_POSSIBLE
360
+ return RuntimeErrorKind.UNCLASSIFIED
361
+
362
+
363
+ def deduplicate_errors(errors: Sequence[Exception]) -> Iterator[Exception]:
364
+ """Deduplicate a list of errors."""
365
+ seen = set()
366
+ serialization_media_types = set()
367
+
368
+ for error in errors:
369
+ # Collect media types
370
+ if isinstance(error, SerializationNotPossible):
371
+ for media_type in error.media_types:
372
+ serialization_media_types.add(media_type)
373
+ continue
374
+
375
+ message = canonicalize_error_message(error)
376
+ if message not in seen:
377
+ seen.add(message)
378
+ yield error
379
+
380
+ if serialization_media_types:
381
+ yield SerializationNotPossible.from_media_types(*sorted(serialization_media_types))
382
+
383
+
384
+ MEMORY_ADDRESS_RE = re.compile("0x[0-9a-fA-F]+")
385
+ URL_IN_ERROR_MESSAGE_RE = re.compile(r"Max retries exceeded with url: .*? \(Caused by")
386
+
387
+
388
+ def canonicalize_error_message(error: Exception, with_traceback: bool = True) -> str:
389
+ """Canonicalize error messages by removing dynamic components."""
390
+ message = format_exception(error, with_traceback=with_traceback)
391
+ # Replace memory addresses
392
+ message = MEMORY_ADDRESS_RE.sub("0xbaaaaaaaaaad", message)
393
+ # Remove URL information
394
+ return URL_IN_ERROR_MESSAGE_RE.sub("", message)