schemathesis 3.39.7__py3-none-any.whl → 4.0.0a2__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 (229) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +26 -68
  3. schemathesis/checks.py +130 -60
  4. schemathesis/cli/__init__.py +5 -2105
  5. schemathesis/cli/commands/__init__.py +37 -0
  6. schemathesis/cli/commands/run/__init__.py +662 -0
  7. schemathesis/cli/commands/run/checks.py +80 -0
  8. schemathesis/cli/commands/run/context.py +117 -0
  9. schemathesis/cli/commands/run/events.py +30 -0
  10. schemathesis/cli/commands/run/executor.py +141 -0
  11. schemathesis/cli/commands/run/filters.py +202 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/{cassettes.py → commands/run/handlers/cassettes.py} +178 -247
  15. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1368 -0
  17. schemathesis/cli/commands/run/hypothesis.py +105 -0
  18. schemathesis/cli/commands/run/loaders.py +129 -0
  19. schemathesis/cli/{callbacks.py → commands/run/validation.py} +59 -175
  20. schemathesis/cli/constants.py +5 -58
  21. schemathesis/cli/core.py +17 -0
  22. schemathesis/cli/ext/fs.py +14 -0
  23. schemathesis/cli/ext/groups.py +55 -0
  24. schemathesis/cli/{options.py → ext/options.py} +37 -16
  25. schemathesis/cli/hooks.py +36 -0
  26. schemathesis/contrib/__init__.py +1 -3
  27. schemathesis/contrib/openapi/__init__.py +1 -3
  28. schemathesis/contrib/openapi/fill_missing_examples.py +3 -7
  29. schemathesis/core/__init__.py +58 -0
  30. schemathesis/core/compat.py +25 -0
  31. schemathesis/core/control.py +2 -0
  32. schemathesis/core/curl.py +58 -0
  33. schemathesis/core/deserialization.py +65 -0
  34. schemathesis/core/errors.py +370 -0
  35. schemathesis/core/failures.py +315 -0
  36. schemathesis/core/fs.py +19 -0
  37. schemathesis/core/loaders.py +104 -0
  38. schemathesis/core/marks.py +66 -0
  39. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  40. schemathesis/{internal/output.py → core/output/__init__.py} +1 -0
  41. schemathesis/core/output/sanitization.py +197 -0
  42. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  43. schemathesis/core/registries.py +31 -0
  44. schemathesis/core/transforms.py +113 -0
  45. schemathesis/core/transport.py +108 -0
  46. schemathesis/core/validation.py +38 -0
  47. schemathesis/core/version.py +7 -0
  48. schemathesis/engine/__init__.py +30 -0
  49. schemathesis/engine/config.py +59 -0
  50. schemathesis/engine/context.py +119 -0
  51. schemathesis/engine/control.py +36 -0
  52. schemathesis/engine/core.py +157 -0
  53. schemathesis/engine/errors.py +394 -0
  54. schemathesis/engine/events.py +243 -0
  55. schemathesis/engine/phases/__init__.py +66 -0
  56. schemathesis/{runner → engine/phases}/probes.py +49 -68
  57. schemathesis/engine/phases/stateful/__init__.py +66 -0
  58. schemathesis/engine/phases/stateful/_executor.py +301 -0
  59. schemathesis/engine/phases/stateful/context.py +85 -0
  60. schemathesis/engine/phases/unit/__init__.py +175 -0
  61. schemathesis/engine/phases/unit/_executor.py +322 -0
  62. schemathesis/engine/phases/unit/_pool.py +74 -0
  63. schemathesis/engine/recorder.py +246 -0
  64. schemathesis/errors.py +31 -0
  65. schemathesis/experimental/__init__.py +9 -40
  66. schemathesis/filters.py +7 -95
  67. schemathesis/generation/__init__.py +3 -3
  68. schemathesis/generation/case.py +190 -0
  69. schemathesis/generation/coverage.py +22 -22
  70. schemathesis/{_patches.py → generation/hypothesis/__init__.py} +15 -6
  71. schemathesis/generation/hypothesis/builder.py +585 -0
  72. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  73. schemathesis/generation/hypothesis/given.py +66 -0
  74. schemathesis/generation/hypothesis/reporting.py +14 -0
  75. schemathesis/generation/hypothesis/strategies.py +16 -0
  76. schemathesis/generation/meta.py +115 -0
  77. schemathesis/generation/modes.py +28 -0
  78. schemathesis/generation/overrides.py +96 -0
  79. schemathesis/generation/stateful/__init__.py +20 -0
  80. schemathesis/{stateful → generation/stateful}/state_machine.py +84 -109
  81. schemathesis/generation/targets.py +69 -0
  82. schemathesis/graphql/__init__.py +15 -0
  83. schemathesis/graphql/checks.py +109 -0
  84. schemathesis/graphql/loaders.py +131 -0
  85. schemathesis/hooks.py +17 -62
  86. schemathesis/openapi/__init__.py +13 -0
  87. schemathesis/openapi/checks.py +387 -0
  88. schemathesis/openapi/generation/__init__.py +0 -0
  89. schemathesis/openapi/generation/filters.py +63 -0
  90. schemathesis/openapi/loaders.py +178 -0
  91. schemathesis/pytest/__init__.py +5 -0
  92. schemathesis/pytest/control_flow.py +7 -0
  93. schemathesis/pytest/lazy.py +273 -0
  94. schemathesis/pytest/loaders.py +12 -0
  95. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +94 -107
  96. schemathesis/python/__init__.py +0 -0
  97. schemathesis/python/asgi.py +12 -0
  98. schemathesis/python/wsgi.py +12 -0
  99. schemathesis/schemas.py +456 -228
  100. schemathesis/specs/graphql/__init__.py +0 -1
  101. schemathesis/specs/graphql/_cache.py +1 -2
  102. schemathesis/specs/graphql/scalars.py +5 -3
  103. schemathesis/specs/graphql/schemas.py +122 -123
  104. schemathesis/specs/graphql/validation.py +11 -17
  105. schemathesis/specs/openapi/__init__.py +6 -1
  106. schemathesis/specs/openapi/_cache.py +1 -2
  107. schemathesis/specs/openapi/_hypothesis.py +97 -134
  108. schemathesis/specs/openapi/checks.py +238 -219
  109. schemathesis/specs/openapi/converter.py +4 -4
  110. schemathesis/specs/openapi/definitions.py +1 -1
  111. schemathesis/specs/openapi/examples.py +22 -20
  112. schemathesis/specs/openapi/expressions/__init__.py +11 -15
  113. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  114. schemathesis/specs/openapi/expressions/nodes.py +33 -32
  115. schemathesis/specs/openapi/formats.py +3 -2
  116. schemathesis/specs/openapi/links.py +123 -299
  117. schemathesis/specs/openapi/media_types.py +10 -12
  118. schemathesis/specs/openapi/negative/__init__.py +2 -1
  119. schemathesis/specs/openapi/negative/mutations.py +3 -2
  120. schemathesis/specs/openapi/parameters.py +8 -6
  121. schemathesis/specs/openapi/patterns.py +1 -1
  122. schemathesis/specs/openapi/references.py +11 -51
  123. schemathesis/specs/openapi/schemas.py +177 -191
  124. schemathesis/specs/openapi/security.py +1 -1
  125. schemathesis/specs/openapi/serialization.py +10 -6
  126. schemathesis/specs/openapi/stateful/__init__.py +97 -91
  127. schemathesis/transport/__init__.py +104 -0
  128. schemathesis/transport/asgi.py +26 -0
  129. schemathesis/transport/prepare.py +99 -0
  130. schemathesis/transport/requests.py +221 -0
  131. schemathesis/{_xml.py → transport/serialization.py} +69 -7
  132. schemathesis/transport/wsgi.py +165 -0
  133. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +18 -14
  134. schemathesis-4.0.0a2.dist-info/RECORD +151 -0
  135. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +1 -1
  136. schemathesis/_compat.py +0 -74
  137. schemathesis/_dependency_versions.py +0 -19
  138. schemathesis/_hypothesis.py +0 -559
  139. schemathesis/_override.py +0 -50
  140. schemathesis/_rate_limiter.py +0 -7
  141. schemathesis/cli/context.py +0 -75
  142. schemathesis/cli/debug.py +0 -27
  143. schemathesis/cli/handlers.py +0 -19
  144. schemathesis/cli/junitxml.py +0 -124
  145. schemathesis/cli/output/__init__.py +0 -1
  146. schemathesis/cli/output/default.py +0 -936
  147. schemathesis/cli/output/short.py +0 -59
  148. schemathesis/cli/reporting.py +0 -79
  149. schemathesis/cli/sanitization.py +0 -26
  150. schemathesis/code_samples.py +0 -151
  151. schemathesis/constants.py +0 -56
  152. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  153. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  154. schemathesis/contrib/unique_data.py +0 -41
  155. schemathesis/exceptions.py +0 -571
  156. schemathesis/extra/_aiohttp.py +0 -28
  157. schemathesis/extra/_flask.py +0 -13
  158. schemathesis/extra/_server.py +0 -18
  159. schemathesis/failures.py +0 -277
  160. schemathesis/fixups/__init__.py +0 -37
  161. schemathesis/fixups/fast_api.py +0 -41
  162. schemathesis/fixups/utf8_bom.py +0 -28
  163. schemathesis/generation/_methods.py +0 -44
  164. schemathesis/graphql.py +0 -3
  165. schemathesis/internal/__init__.py +0 -7
  166. schemathesis/internal/checks.py +0 -84
  167. schemathesis/internal/copy.py +0 -32
  168. schemathesis/internal/datetime.py +0 -5
  169. schemathesis/internal/deprecation.py +0 -38
  170. schemathesis/internal/diff.py +0 -15
  171. schemathesis/internal/extensions.py +0 -27
  172. schemathesis/internal/jsonschema.py +0 -36
  173. schemathesis/internal/transformation.py +0 -26
  174. schemathesis/internal/validation.py +0 -34
  175. schemathesis/lazy.py +0 -474
  176. schemathesis/loaders.py +0 -122
  177. schemathesis/models.py +0 -1341
  178. schemathesis/parameters.py +0 -90
  179. schemathesis/runner/__init__.py +0 -605
  180. schemathesis/runner/events.py +0 -389
  181. schemathesis/runner/impl/__init__.py +0 -3
  182. schemathesis/runner/impl/context.py +0 -104
  183. schemathesis/runner/impl/core.py +0 -1246
  184. schemathesis/runner/impl/solo.py +0 -80
  185. schemathesis/runner/impl/threadpool.py +0 -391
  186. schemathesis/runner/serialization.py +0 -544
  187. schemathesis/sanitization.py +0 -252
  188. schemathesis/serializers.py +0 -328
  189. schemathesis/service/__init__.py +0 -18
  190. schemathesis/service/auth.py +0 -11
  191. schemathesis/service/ci.py +0 -202
  192. schemathesis/service/client.py +0 -133
  193. schemathesis/service/constants.py +0 -38
  194. schemathesis/service/events.py +0 -61
  195. schemathesis/service/extensions.py +0 -224
  196. schemathesis/service/hosts.py +0 -111
  197. schemathesis/service/metadata.py +0 -71
  198. schemathesis/service/models.py +0 -258
  199. schemathesis/service/report.py +0 -255
  200. schemathesis/service/serialization.py +0 -173
  201. schemathesis/service/usage.py +0 -66
  202. schemathesis/specs/graphql/loaders.py +0 -364
  203. schemathesis/specs/openapi/expressions/context.py +0 -16
  204. schemathesis/specs/openapi/loaders.py +0 -708
  205. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  206. schemathesis/specs/openapi/stateful/types.py +0 -14
  207. schemathesis/specs/openapi/validation.py +0 -26
  208. schemathesis/stateful/__init__.py +0 -147
  209. schemathesis/stateful/config.py +0 -97
  210. schemathesis/stateful/context.py +0 -135
  211. schemathesis/stateful/events.py +0 -274
  212. schemathesis/stateful/runner.py +0 -309
  213. schemathesis/stateful/sink.py +0 -68
  214. schemathesis/stateful/statistic.py +0 -22
  215. schemathesis/stateful/validation.py +0 -100
  216. schemathesis/targets.py +0 -77
  217. schemathesis/transports/__init__.py +0 -359
  218. schemathesis/transports/asgi.py +0 -7
  219. schemathesis/transports/auth.py +0 -38
  220. schemathesis/transports/headers.py +0 -36
  221. schemathesis/transports/responses.py +0 -57
  222. schemathesis/types.py +0 -44
  223. schemathesis/utils.py +0 -164
  224. schemathesis-3.39.7.dist-info/RECORD +0 -160
  225. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  226. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  227. /schemathesis/{internal → core}/result.py +0 -0
  228. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
  229. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.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)
@@ -0,0 +1,243 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ import uuid
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING, Generator
7
+
8
+ from schemathesis.core.result import Result
9
+ from schemathesis.engine.errors import EngineErrorInfo
10
+ from schemathesis.engine.phases import Phase, PhaseName
11
+ from schemathesis.engine.recorder import ScenarioRecorder
12
+
13
+ if TYPE_CHECKING:
14
+ from schemathesis.engine import Status
15
+ from schemathesis.engine.phases.probes import ProbePayload
16
+
17
+ EventGenerator = Generator["EngineEvent", None, None]
18
+
19
+
20
+ @dataclass
21
+ class EngineEvent:
22
+ """An event within the engine's lifecycle."""
23
+
24
+ id: uuid.UUID
25
+ timestamp: float
26
+ # Indicates whether this event is the last in the event stream
27
+ is_terminal = False
28
+
29
+
30
+ @dataclass
31
+ class EngineStarted(EngineEvent):
32
+ """Start of an engine."""
33
+
34
+ __slots__ = ("id", "timestamp")
35
+
36
+ def __init__(self) -> None:
37
+ self.id = uuid.uuid4()
38
+ self.timestamp = time.time()
39
+
40
+
41
+ @dataclass
42
+ class PhaseEvent(EngineEvent):
43
+ """Event associated with a specific execution phase."""
44
+
45
+ phase: Phase
46
+
47
+
48
+ @dataclass
49
+ class PhaseStarted(PhaseEvent):
50
+ """Start of an execution phase."""
51
+
52
+ __slots__ = ("id", "timestamp", "phase")
53
+
54
+ def __init__(self, *, phase: Phase) -> None:
55
+ self.id = uuid.uuid4()
56
+ self.timestamp = time.time()
57
+ self.phase = phase
58
+
59
+
60
+ @dataclass
61
+ class PhaseFinished(PhaseEvent):
62
+ """End of an execution phase."""
63
+
64
+ status: Status
65
+ payload: Result[ProbePayload, Exception] | None
66
+
67
+ __slots__ = ("id", "timestamp", "phase", "status", "payload")
68
+
69
+ def __init__(self, *, phase: Phase, status: Status, payload: Result[ProbePayload, Exception] | None) -> None:
70
+ self.id = uuid.uuid4()
71
+ self.timestamp = time.time()
72
+ self.phase = phase
73
+ self.status = status
74
+ self.payload = payload
75
+
76
+
77
+ @dataclass
78
+ class TestEvent(EngineEvent):
79
+ phase: PhaseName
80
+
81
+
82
+ @dataclass
83
+ class SuiteStarted(TestEvent):
84
+ """Before executing a set of scenarios."""
85
+
86
+ __slots__ = ("id", "timestamp", "phase")
87
+
88
+ def __init__(self, *, phase: PhaseName) -> None:
89
+ self.id = uuid.uuid4()
90
+ self.timestamp = time.time()
91
+ self.phase = phase
92
+
93
+
94
+ @dataclass
95
+ class SuiteFinished(TestEvent):
96
+ """After executing a set of test scenarios."""
97
+
98
+ status: Status
99
+
100
+ __slots__ = ("id", "timestamp", "phase", "status")
101
+
102
+ def __init__(self, *, id: uuid.UUID, phase: PhaseName, status: Status) -> None:
103
+ self.id = id
104
+ self.timestamp = time.time()
105
+ self.phase = phase
106
+ self.status = status
107
+
108
+
109
+ @dataclass
110
+ class ScenarioEvent(TestEvent):
111
+ suite_id: uuid.UUID
112
+
113
+
114
+ @dataclass
115
+ class ScenarioStarted(ScenarioEvent):
116
+ """Before executing a grouped set of test steps."""
117
+
118
+ __slots__ = ("id", "timestamp", "phase", "suite_id", "label")
119
+
120
+ def __init__(self, *, phase: PhaseName, suite_id: uuid.UUID, label: str | None) -> None:
121
+ self.id = uuid.uuid4()
122
+ self.timestamp = time.time()
123
+ self.phase = phase
124
+ self.suite_id = suite_id
125
+ self.label = label
126
+
127
+
128
+ @dataclass
129
+ class ScenarioFinished(ScenarioEvent):
130
+ """After executing a grouped set of test steps."""
131
+
132
+ status: Status
133
+ recorder: ScenarioRecorder
134
+ elapsed_time: float
135
+ skip_reason: str | None
136
+ # Whether this is a scenario that tries to reproduce a failure
137
+ is_final: bool
138
+
139
+ __slots__ = (
140
+ "id",
141
+ "timestamp",
142
+ "phase",
143
+ "suite_id",
144
+ "label",
145
+ "status",
146
+ "recorder",
147
+ "elapsed_time",
148
+ "skip_reason",
149
+ "is_final",
150
+ )
151
+
152
+ def __init__(
153
+ self,
154
+ *,
155
+ id: uuid.UUID,
156
+ phase: PhaseName,
157
+ suite_id: uuid.UUID,
158
+ label: str | None,
159
+ status: Status,
160
+ recorder: ScenarioRecorder,
161
+ elapsed_time: float,
162
+ skip_reason: str | None,
163
+ is_final: bool,
164
+ ) -> None:
165
+ self.id = id
166
+ self.timestamp = time.time()
167
+ self.phase = phase
168
+ self.suite_id = suite_id
169
+ self.label = label
170
+ self.status = status
171
+ self.recorder = recorder
172
+ self.elapsed_time = elapsed_time
173
+ self.skip_reason = skip_reason
174
+ self.is_final = is_final
175
+
176
+
177
+ @dataclass
178
+ class Interrupted(EngineEvent):
179
+ """If execution was interrupted by Ctrl-C, or a received SIGTERM."""
180
+
181
+ phase: PhaseName | None
182
+
183
+ __slots__ = ("id", "timestamp", "phase")
184
+
185
+ def __init__(self, *, phase: PhaseName | None) -> None:
186
+ self.id = uuid.uuid4()
187
+ self.timestamp = time.time()
188
+ self.phase = phase
189
+
190
+
191
+ @dataclass
192
+ class NonFatalError(EngineEvent):
193
+ """Error that doesn't halt execution but should be reported."""
194
+
195
+ info: EngineErrorInfo
196
+ value: Exception
197
+ phase: PhaseName
198
+ label: str
199
+ related_to_operation: bool
200
+
201
+ __slots__ = ("id", "timestamp", "info", "value", "phase", "label", "related_to_operation")
202
+
203
+ def __init__(self, *, error: Exception, phase: PhaseName, label: str, related_to_operation: bool) -> None:
204
+ self.id = uuid.uuid4()
205
+ self.timestamp = time.time()
206
+ self.info = EngineErrorInfo(error=error)
207
+ self.value = error
208
+ self.phase = phase
209
+ self.label = label
210
+ self.related_to_operation = related_to_operation
211
+
212
+
213
+ @dataclass
214
+ class FatalError(EngineEvent):
215
+ """Internal error in the engine."""
216
+
217
+ exception: Exception
218
+ is_terminal = True
219
+
220
+ __slots__ = ("id", "timestamp", "exception")
221
+
222
+ def __init__(self, *, exception: Exception) -> None:
223
+ self.id = uuid.uuid4()
224
+ self.timestamp = time.time()
225
+ self.exception = exception
226
+
227
+
228
+ @dataclass
229
+ class EngineFinished(EngineEvent):
230
+ """The final event of the run.
231
+
232
+ No more events after this point.
233
+ """
234
+
235
+ is_terminal = True
236
+ running_time: float
237
+
238
+ __slots__ = ("id", "timestamp", "running_time")
239
+
240
+ def __init__(self, *, running_time: float) -> None:
241
+ self.id = uuid.uuid4()
242
+ self.timestamp = time.time()
243
+ self.running_time = running_time
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+ import warnings
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from schemathesis.engine.context import EngineContext
10
+ from schemathesis.engine.events import EventGenerator
11
+
12
+
13
+ class PhaseName(enum.Enum):
14
+ """Available execution phases."""
15
+
16
+ PROBING = "API probing"
17
+ UNIT_TESTING = "Unit testing"
18
+ STATEFUL_TESTING = "Stateful testing"
19
+
20
+ @classmethod
21
+ def from_str(cls, value: str) -> PhaseName:
22
+ return {
23
+ "probing": cls.PROBING,
24
+ "unit": cls.UNIT_TESTING,
25
+ "stateful": cls.STATEFUL_TESTING,
26
+ }[value.lower()]
27
+
28
+
29
+ class PhaseSkipReason(str, enum.Enum):
30
+ """Reasons why a phase might not be executed."""
31
+
32
+ DISABLED = "disabled" # Explicitly disabled via config
33
+ NOT_SUPPORTED = "not supported" # Feature not supported by schema
34
+ NOT_APPLICABLE = "not applicable" # No relevant data (e.g., no links for stateful)
35
+ FAILURE_LIMIT_REACHED = "failure limit reached"
36
+ NOTHING_TO_TEST = "nothing to test"
37
+
38
+
39
+ @dataclass
40
+ class Phase:
41
+ """A logically separate engine execution phase."""
42
+
43
+ name: PhaseName
44
+ is_supported: bool
45
+ is_enabled: bool = True
46
+ skip_reason: PhaseSkipReason | None = None
47
+
48
+ def should_execute(self, ctx: EngineContext) -> bool:
49
+ """Determine if phase should run based on context & configuration."""
50
+ return self.is_enabled and not ctx.has_to_stop
51
+
52
+
53
+ def execute(ctx: EngineContext, phase: Phase) -> EventGenerator:
54
+ from urllib3.exceptions import InsecureRequestWarning
55
+
56
+ from . import probes, stateful, unit
57
+
58
+ with warnings.catch_warnings():
59
+ warnings.simplefilter("ignore", InsecureRequestWarning)
60
+
61
+ if phase.name == PhaseName.PROBING:
62
+ yield from probes.execute(ctx, phase)
63
+ elif phase.name == PhaseName.UNIT_TESTING:
64
+ yield from unit.execute(ctx, phase)
65
+ elif phase.name == PhaseName.STATEFUL_TESTING:
66
+ yield from stateful.execute(ctx, phase)