schemathesis 3.39.16__py3-none-any.whl → 4.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. schemathesis/__init__.py +41 -79
  2. schemathesis/auths.py +111 -122
  3. schemathesis/checks.py +169 -60
  4. schemathesis/cli/__init__.py +15 -2117
  5. schemathesis/cli/commands/__init__.py +85 -0
  6. schemathesis/cli/commands/data.py +10 -0
  7. schemathesis/cli/commands/run/__init__.py +590 -0
  8. schemathesis/cli/commands/run/context.py +204 -0
  9. schemathesis/cli/commands/run/events.py +60 -0
  10. schemathesis/cli/commands/run/executor.py +157 -0
  11. schemathesis/cli/commands/run/filters.py +53 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
  15. schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1628 -0
  17. schemathesis/cli/commands/run/loaders.py +114 -0
  18. schemathesis/cli/commands/run/validation.py +246 -0
  19. schemathesis/cli/constants.py +5 -58
  20. schemathesis/cli/core.py +19 -0
  21. schemathesis/cli/ext/fs.py +16 -0
  22. schemathesis/cli/ext/groups.py +84 -0
  23. schemathesis/cli/{options.py → ext/options.py} +36 -34
  24. schemathesis/config/__init__.py +189 -0
  25. schemathesis/config/_auth.py +51 -0
  26. schemathesis/config/_checks.py +268 -0
  27. schemathesis/config/_diff_base.py +99 -0
  28. schemathesis/config/_env.py +21 -0
  29. schemathesis/config/_error.py +156 -0
  30. schemathesis/config/_generation.py +149 -0
  31. schemathesis/config/_health_check.py +24 -0
  32. schemathesis/config/_operations.py +327 -0
  33. schemathesis/config/_output.py +171 -0
  34. schemathesis/config/_parameters.py +19 -0
  35. schemathesis/config/_phases.py +187 -0
  36. schemathesis/config/_projects.py +527 -0
  37. schemathesis/config/_rate_limit.py +17 -0
  38. schemathesis/config/_report.py +120 -0
  39. schemathesis/config/_validator.py +9 -0
  40. schemathesis/config/_warnings.py +25 -0
  41. schemathesis/config/schema.json +885 -0
  42. schemathesis/core/__init__.py +67 -0
  43. schemathesis/core/compat.py +32 -0
  44. schemathesis/core/control.py +2 -0
  45. schemathesis/core/curl.py +58 -0
  46. schemathesis/core/deserialization.py +65 -0
  47. schemathesis/core/errors.py +459 -0
  48. schemathesis/core/failures.py +315 -0
  49. schemathesis/core/fs.py +19 -0
  50. schemathesis/core/hooks.py +20 -0
  51. schemathesis/core/loaders.py +104 -0
  52. schemathesis/core/marks.py +66 -0
  53. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  54. schemathesis/core/output/__init__.py +46 -0
  55. schemathesis/core/output/sanitization.py +54 -0
  56. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  57. schemathesis/core/registries.py +31 -0
  58. schemathesis/core/transforms.py +113 -0
  59. schemathesis/core/transport.py +223 -0
  60. schemathesis/core/validation.py +54 -0
  61. schemathesis/core/version.py +7 -0
  62. schemathesis/engine/__init__.py +28 -0
  63. schemathesis/engine/context.py +118 -0
  64. schemathesis/engine/control.py +36 -0
  65. schemathesis/engine/core.py +169 -0
  66. schemathesis/engine/errors.py +464 -0
  67. schemathesis/engine/events.py +258 -0
  68. schemathesis/engine/phases/__init__.py +88 -0
  69. schemathesis/{runner → engine/phases}/probes.py +52 -68
  70. schemathesis/engine/phases/stateful/__init__.py +68 -0
  71. schemathesis/engine/phases/stateful/_executor.py +356 -0
  72. schemathesis/engine/phases/stateful/context.py +85 -0
  73. schemathesis/engine/phases/unit/__init__.py +212 -0
  74. schemathesis/engine/phases/unit/_executor.py +416 -0
  75. schemathesis/engine/phases/unit/_pool.py +82 -0
  76. schemathesis/engine/recorder.py +247 -0
  77. schemathesis/errors.py +43 -0
  78. schemathesis/filters.py +17 -98
  79. schemathesis/generation/__init__.py +5 -33
  80. schemathesis/generation/case.py +317 -0
  81. schemathesis/generation/coverage.py +282 -175
  82. schemathesis/generation/hypothesis/__init__.py +36 -0
  83. schemathesis/generation/hypothesis/builder.py +800 -0
  84. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  85. schemathesis/generation/hypothesis/given.py +66 -0
  86. schemathesis/generation/hypothesis/reporting.py +14 -0
  87. schemathesis/generation/hypothesis/strategies.py +16 -0
  88. schemathesis/generation/meta.py +115 -0
  89. schemathesis/generation/metrics.py +93 -0
  90. schemathesis/generation/modes.py +20 -0
  91. schemathesis/generation/overrides.py +116 -0
  92. schemathesis/generation/stateful/__init__.py +37 -0
  93. schemathesis/generation/stateful/state_machine.py +278 -0
  94. schemathesis/graphql/__init__.py +15 -0
  95. schemathesis/graphql/checks.py +109 -0
  96. schemathesis/graphql/loaders.py +284 -0
  97. schemathesis/hooks.py +80 -101
  98. schemathesis/openapi/__init__.py +13 -0
  99. schemathesis/openapi/checks.py +455 -0
  100. schemathesis/openapi/generation/__init__.py +0 -0
  101. schemathesis/openapi/generation/filters.py +72 -0
  102. schemathesis/openapi/loaders.py +313 -0
  103. schemathesis/pytest/__init__.py +5 -0
  104. schemathesis/pytest/control_flow.py +7 -0
  105. schemathesis/pytest/lazy.py +281 -0
  106. schemathesis/pytest/loaders.py +36 -0
  107. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
  108. schemathesis/python/__init__.py +0 -0
  109. schemathesis/python/asgi.py +12 -0
  110. schemathesis/python/wsgi.py +12 -0
  111. schemathesis/schemas.py +537 -273
  112. schemathesis/specs/graphql/__init__.py +0 -1
  113. schemathesis/specs/graphql/_cache.py +1 -2
  114. schemathesis/specs/graphql/scalars.py +42 -6
  115. schemathesis/specs/graphql/schemas.py +141 -137
  116. schemathesis/specs/graphql/validation.py +11 -17
  117. schemathesis/specs/openapi/__init__.py +6 -1
  118. schemathesis/specs/openapi/_cache.py +1 -2
  119. schemathesis/specs/openapi/_hypothesis.py +142 -156
  120. schemathesis/specs/openapi/checks.py +368 -257
  121. schemathesis/specs/openapi/converter.py +4 -4
  122. schemathesis/specs/openapi/definitions.py +1 -1
  123. schemathesis/specs/openapi/examples.py +23 -21
  124. schemathesis/specs/openapi/expressions/__init__.py +31 -19
  125. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  126. schemathesis/specs/openapi/expressions/lexer.py +1 -1
  127. schemathesis/specs/openapi/expressions/nodes.py +36 -41
  128. schemathesis/specs/openapi/expressions/parser.py +1 -1
  129. schemathesis/specs/openapi/formats.py +35 -7
  130. schemathesis/specs/openapi/media_types.py +53 -12
  131. schemathesis/specs/openapi/negative/__init__.py +7 -4
  132. schemathesis/specs/openapi/negative/mutations.py +6 -5
  133. schemathesis/specs/openapi/parameters.py +7 -10
  134. schemathesis/specs/openapi/patterns.py +94 -31
  135. schemathesis/specs/openapi/references.py +12 -53
  136. schemathesis/specs/openapi/schemas.py +233 -307
  137. schemathesis/specs/openapi/security.py +1 -1
  138. schemathesis/specs/openapi/serialization.py +12 -6
  139. schemathesis/specs/openapi/stateful/__init__.py +268 -133
  140. schemathesis/specs/openapi/stateful/control.py +87 -0
  141. schemathesis/specs/openapi/stateful/links.py +209 -0
  142. schemathesis/transport/__init__.py +142 -0
  143. schemathesis/transport/asgi.py +26 -0
  144. schemathesis/transport/prepare.py +124 -0
  145. schemathesis/transport/requests.py +244 -0
  146. schemathesis/{_xml.py → transport/serialization.py} +69 -11
  147. schemathesis/transport/wsgi.py +171 -0
  148. schemathesis-4.0.0.dist-info/METADATA +204 -0
  149. schemathesis-4.0.0.dist-info/RECORD +164 -0
  150. {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
  151. {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
  152. schemathesis/_compat.py +0 -74
  153. schemathesis/_dependency_versions.py +0 -19
  154. schemathesis/_hypothesis.py +0 -717
  155. schemathesis/_override.py +0 -50
  156. schemathesis/_patches.py +0 -21
  157. schemathesis/_rate_limiter.py +0 -7
  158. schemathesis/cli/callbacks.py +0 -466
  159. schemathesis/cli/cassettes.py +0 -561
  160. schemathesis/cli/context.py +0 -75
  161. schemathesis/cli/debug.py +0 -27
  162. schemathesis/cli/handlers.py +0 -19
  163. schemathesis/cli/junitxml.py +0 -124
  164. schemathesis/cli/output/__init__.py +0 -1
  165. schemathesis/cli/output/default.py +0 -920
  166. schemathesis/cli/output/short.py +0 -59
  167. schemathesis/cli/reporting.py +0 -79
  168. schemathesis/cli/sanitization.py +0 -26
  169. schemathesis/code_samples.py +0 -151
  170. schemathesis/constants.py +0 -54
  171. schemathesis/contrib/__init__.py +0 -11
  172. schemathesis/contrib/openapi/__init__.py +0 -11
  173. schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
  174. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  175. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  176. schemathesis/contrib/unique_data.py +0 -41
  177. schemathesis/exceptions.py +0 -571
  178. schemathesis/experimental/__init__.py +0 -109
  179. schemathesis/extra/_aiohttp.py +0 -28
  180. schemathesis/extra/_flask.py +0 -13
  181. schemathesis/extra/_server.py +0 -18
  182. schemathesis/failures.py +0 -284
  183. schemathesis/fixups/__init__.py +0 -37
  184. schemathesis/fixups/fast_api.py +0 -41
  185. schemathesis/fixups/utf8_bom.py +0 -28
  186. schemathesis/generation/_methods.py +0 -44
  187. schemathesis/graphql.py +0 -3
  188. schemathesis/internal/__init__.py +0 -7
  189. schemathesis/internal/checks.py +0 -86
  190. schemathesis/internal/copy.py +0 -32
  191. schemathesis/internal/datetime.py +0 -5
  192. schemathesis/internal/deprecation.py +0 -37
  193. schemathesis/internal/diff.py +0 -15
  194. schemathesis/internal/extensions.py +0 -27
  195. schemathesis/internal/jsonschema.py +0 -36
  196. schemathesis/internal/output.py +0 -68
  197. schemathesis/internal/transformation.py +0 -26
  198. schemathesis/internal/validation.py +0 -34
  199. schemathesis/lazy.py +0 -474
  200. schemathesis/loaders.py +0 -122
  201. schemathesis/models.py +0 -1341
  202. schemathesis/parameters.py +0 -90
  203. schemathesis/runner/__init__.py +0 -605
  204. schemathesis/runner/events.py +0 -389
  205. schemathesis/runner/impl/__init__.py +0 -3
  206. schemathesis/runner/impl/context.py +0 -88
  207. schemathesis/runner/impl/core.py +0 -1280
  208. schemathesis/runner/impl/solo.py +0 -80
  209. schemathesis/runner/impl/threadpool.py +0 -391
  210. schemathesis/runner/serialization.py +0 -544
  211. schemathesis/sanitization.py +0 -252
  212. schemathesis/serializers.py +0 -328
  213. schemathesis/service/__init__.py +0 -18
  214. schemathesis/service/auth.py +0 -11
  215. schemathesis/service/ci.py +0 -202
  216. schemathesis/service/client.py +0 -133
  217. schemathesis/service/constants.py +0 -38
  218. schemathesis/service/events.py +0 -61
  219. schemathesis/service/extensions.py +0 -224
  220. schemathesis/service/hosts.py +0 -111
  221. schemathesis/service/metadata.py +0 -71
  222. schemathesis/service/models.py +0 -258
  223. schemathesis/service/report.py +0 -255
  224. schemathesis/service/serialization.py +0 -173
  225. schemathesis/service/usage.py +0 -66
  226. schemathesis/specs/graphql/loaders.py +0 -364
  227. schemathesis/specs/openapi/expressions/context.py +0 -16
  228. schemathesis/specs/openapi/links.py +0 -389
  229. schemathesis/specs/openapi/loaders.py +0 -707
  230. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  231. schemathesis/specs/openapi/stateful/types.py +0 -14
  232. schemathesis/specs/openapi/validation.py +0 -26
  233. schemathesis/stateful/__init__.py +0 -147
  234. schemathesis/stateful/config.py +0 -97
  235. schemathesis/stateful/context.py +0 -135
  236. schemathesis/stateful/events.py +0 -274
  237. schemathesis/stateful/runner.py +0 -309
  238. schemathesis/stateful/sink.py +0 -68
  239. schemathesis/stateful/state_machine.py +0 -328
  240. schemathesis/stateful/statistic.py +0 -22
  241. schemathesis/stateful/validation.py +0 -100
  242. schemathesis/targets.py +0 -77
  243. schemathesis/transports/__init__.py +0 -369
  244. schemathesis/transports/asgi.py +0 -7
  245. schemathesis/transports/auth.py +0 -38
  246. schemathesis/transports/headers.py +0 -36
  247. schemathesis/transports/responses.py +0 -57
  248. schemathesis/types.py +0 -44
  249. schemathesis/utils.py +0 -164
  250. schemathesis-3.39.16.dist-info/METADATA +0 -293
  251. schemathesis-3.39.16.dist-info/RECORD +0 -160
  252. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  253. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  254. /schemathesis/{internal → core}/result.py +0 -0
  255. {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,169 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ from dataclasses import dataclass
5
+ from typing import Sequence
6
+
7
+ from schemathesis.auths import unregister as unregister_auth
8
+ from schemathesis.core import SpecificationFeature
9
+ from schemathesis.engine import Status, events, phases
10
+ from schemathesis.schemas import BaseSchema
11
+
12
+ from .context import EngineContext
13
+ from .events import EventGenerator
14
+ from .phases import Phase, PhaseName, PhaseSkipReason
15
+
16
+
17
+ @dataclass
18
+ class Engine:
19
+ schema: BaseSchema
20
+
21
+ def execute(self) -> EventStream:
22
+ """Execute all test phases."""
23
+ # Unregister auth if explicitly provided
24
+ if self.schema.config.auth.is_defined:
25
+ unregister_auth()
26
+
27
+ ctx = EngineContext(schema=self.schema, stop_event=threading.Event())
28
+ plan = self._create_execution_plan()
29
+ return EventStream(plan.execute(ctx), ctx.control.stop_event)
30
+
31
+ def _create_execution_plan(self) -> ExecutionPlan:
32
+ """Create execution plan based on configuration."""
33
+ phases = [
34
+ self.get_phase_config(PhaseName.PROBING, is_supported=True, requires_links=False),
35
+ self.get_phase_config(
36
+ PhaseName.EXAMPLES,
37
+ is_supported=self.schema.specification.supports_feature(SpecificationFeature.EXAMPLES),
38
+ requires_links=False,
39
+ ),
40
+ self.get_phase_config(
41
+ PhaseName.COVERAGE,
42
+ is_supported=self.schema.specification.supports_feature(SpecificationFeature.COVERAGE),
43
+ requires_links=False,
44
+ ),
45
+ self.get_phase_config(PhaseName.FUZZING, is_supported=True, requires_links=False),
46
+ self.get_phase_config(
47
+ PhaseName.STATEFUL_TESTING,
48
+ is_supported=self.schema.specification.supports_feature(SpecificationFeature.STATEFUL_TESTING),
49
+ requires_links=True,
50
+ ),
51
+ ]
52
+ return ExecutionPlan(phases)
53
+
54
+ def get_phase_config(
55
+ self,
56
+ phase_name: PhaseName,
57
+ *,
58
+ is_supported: bool = True,
59
+ requires_links: bool = False,
60
+ ) -> Phase:
61
+ """Helper to determine phase configuration with proper skip reasons."""
62
+ # Check if feature is supported by the schema
63
+ if not is_supported:
64
+ return Phase(
65
+ name=phase_name,
66
+ is_supported=False,
67
+ is_enabled=False,
68
+ skip_reason=PhaseSkipReason.NOT_SUPPORTED,
69
+ )
70
+
71
+ phase = phase_name.value.lower()
72
+ if (
73
+ phase in ("examples", "coverage", "fuzzing", "stateful")
74
+ and not self.schema.config.phases.get_by_name(name=phase).enabled
75
+ ):
76
+ return Phase(
77
+ name=phase_name,
78
+ is_supported=True,
79
+ is_enabled=False,
80
+ skip_reason=PhaseSkipReason.DISABLED,
81
+ )
82
+
83
+ if requires_links and self.schema.statistic.links.total == 0:
84
+ return Phase(
85
+ name=phase_name,
86
+ is_supported=True,
87
+ is_enabled=False,
88
+ skip_reason=PhaseSkipReason.NOT_APPLICABLE,
89
+ )
90
+
91
+ # Phase can be executed
92
+ return Phase(
93
+ name=phase_name,
94
+ is_supported=True,
95
+ is_enabled=True,
96
+ skip_reason=None,
97
+ )
98
+
99
+
100
+ @dataclass
101
+ class ExecutionPlan:
102
+ """Manages test execution phases."""
103
+
104
+ phases: Sequence[Phase]
105
+
106
+ def execute(self, engine: EngineContext) -> EventGenerator:
107
+ """Execute all phases in sequence."""
108
+ yield events.EngineStarted()
109
+ try:
110
+ if engine.is_interrupted:
111
+ yield from self._finish(engine)
112
+ return
113
+ if engine.is_interrupted:
114
+ yield from self._finish(engine) # type: ignore[unreachable]
115
+ return
116
+
117
+ # Run main phases
118
+ for phase in self.phases:
119
+ if engine.has_reached_the_failure_limit:
120
+ phase.skip_reason = PhaseSkipReason.FAILURE_LIMIT_REACHED
121
+ yield events.PhaseStarted(phase=phase)
122
+ if phase.should_execute(engine):
123
+ yield from phases.execute(engine, phase)
124
+ else:
125
+ if engine.has_reached_the_failure_limit:
126
+ phase.skip_reason = PhaseSkipReason.FAILURE_LIMIT_REACHED
127
+ yield events.PhaseFinished(phase=phase, status=Status.SKIP, payload=None)
128
+ if engine.is_interrupted:
129
+ break # type: ignore[unreachable]
130
+
131
+ except KeyboardInterrupt:
132
+ engine.stop()
133
+ yield events.Interrupted(phase=None)
134
+
135
+ # Always finish
136
+ yield from self._finish(engine)
137
+
138
+ def _finish(self, ctx: EngineContext) -> EventGenerator:
139
+ """Finish the test run."""
140
+ yield events.EngineFinished(running_time=ctx.running_time)
141
+
142
+
143
+ @dataclass
144
+ class EventStream:
145
+ """Schemathesis event stream.
146
+
147
+ Provides an API to control the execution flow.
148
+ """
149
+
150
+ generator: EventGenerator
151
+ stop_event: threading.Event
152
+
153
+ def __next__(self) -> events.EngineEvent:
154
+ return next(self.generator)
155
+
156
+ def __iter__(self) -> EventGenerator:
157
+ return self.generator
158
+
159
+ def stop(self) -> None:
160
+ """Stop the event stream.
161
+
162
+ Its next value will be the last one (Finished).
163
+ """
164
+ self.stop_event.set()
165
+
166
+ def finish(self) -> events.EngineEvent:
167
+ """Stop the event stream & return the last event."""
168
+ self.stop()
169
+ return next(self)
@@ -0,0 +1,464 @@
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 dataclasses import dataclass
12
+ from functools import cached_property
13
+ from typing import TYPE_CHECKING, Callable, Iterator, Sequence, cast
14
+
15
+ from schemathesis import errors
16
+ from schemathesis.core.errors import (
17
+ RECURSIVE_REFERENCE_ERROR_MESSAGE,
18
+ InvalidTransition,
19
+ SerializationNotPossible,
20
+ format_exception,
21
+ get_request_error_extras,
22
+ get_request_error_message,
23
+ split_traceback,
24
+ )
25
+
26
+ if TYPE_CHECKING:
27
+ import hypothesis.errors
28
+ import requests
29
+ from requests.exceptions import ChunkedEncodingError
30
+
31
+ __all__ = ["EngineErrorInfo", "DeadlineExceeded", "UnsupportedRecursiveReference", "UnexpectedError"]
32
+
33
+
34
+ class DeadlineExceeded(errors.SchemathesisError):
35
+ """Test took too long to run."""
36
+
37
+ @classmethod
38
+ def from_exc(cls, exc: hypothesis.errors.DeadlineExceeded) -> DeadlineExceeded:
39
+ runtime = exc.runtime.total_seconds() * 1000
40
+ deadline = exc.deadline.total_seconds() * 1000
41
+ return cls(
42
+ f"Test running time is too slow! It took {runtime:.2f}ms, which exceeds the deadline of {deadline:.2f}ms.\n"
43
+ )
44
+
45
+
46
+ class UnsupportedRecursiveReference(errors.SchemathesisError):
47
+ """Recursive reference is impossible to resolve due to current limitations."""
48
+
49
+ def __init__(self) -> None:
50
+ super().__init__(RECURSIVE_REFERENCE_ERROR_MESSAGE)
51
+
52
+
53
+ class UnexpectedError(errors.SchemathesisError):
54
+ """An unexpected error during the engine execution.
55
+
56
+ Used primarily to not let Hypothesis consider the test as flaky or detect multiple failures as we handle it
57
+ on our side.
58
+ """
59
+
60
+
61
+ class EngineErrorInfo:
62
+ """Extended information about errors that happen during engine execution.
63
+
64
+ It serves as a caching wrapper around exceptions to avoid repeated computations.
65
+ """
66
+
67
+ def __init__(self, error: Exception, code_sample: str | None = None) -> None:
68
+ self._error = error
69
+ self._code_sample = code_sample
70
+
71
+ def __str__(self) -> str:
72
+ return self._error_repr
73
+
74
+ @cached_property
75
+ def _kind(self) -> RuntimeErrorKind:
76
+ """Error kind."""
77
+ return _classify(error=self._error)
78
+
79
+ @property
80
+ def title(self) -> str:
81
+ """A general error description."""
82
+ import requests
83
+
84
+ if isinstance(self._error, InvalidTransition):
85
+ return "Invalid Link Definition"
86
+
87
+ if isinstance(self._error, requests.RequestException):
88
+ return "Network Error"
89
+
90
+ if self._kind in (
91
+ RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE,
92
+ RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH,
93
+ RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW,
94
+ RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE,
95
+ ):
96
+ return "Failed Health Check"
97
+
98
+ if self._kind in (
99
+ RuntimeErrorKind.SCHEMA_INVALID_REGULAR_EXPRESSION,
100
+ RuntimeErrorKind.SCHEMA_GENERIC,
101
+ RuntimeErrorKind.HYPOTHESIS_UNSATISFIABLE,
102
+ ):
103
+ return "Schema Error"
104
+
105
+ return {
106
+ RuntimeErrorKind.SCHEMA_UNSUPPORTED: "Unsupported Schema",
107
+ RuntimeErrorKind.SCHEMA_NO_LINKS_FOUND: "Missing Open API links",
108
+ RuntimeErrorKind.SCHEMA_INVALID_STATE_MACHINE: "Invalid OpenAPI Links Definition",
109
+ RuntimeErrorKind.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR: "Unknown GraphQL Scalar",
110
+ RuntimeErrorKind.SERIALIZATION_UNBOUNDED_PREFIX: "XML serialization error",
111
+ RuntimeErrorKind.SERIALIZATION_NOT_POSSIBLE: "Serialization not possible",
112
+ }.get(self._kind, "Runtime Error")
113
+
114
+ @property
115
+ def message(self) -> str:
116
+ """Detailed error description."""
117
+ import hypothesis.errors
118
+ import requests
119
+
120
+ if isinstance(self._error, requests.RequestException):
121
+ return get_request_error_message(self._error)
122
+
123
+ if self._kind == RuntimeErrorKind.SCHEMA_UNSUPPORTED:
124
+ return str(self._error).strip()
125
+
126
+ if self._kind == RuntimeErrorKind.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR and isinstance(
127
+ self._error, hypothesis.errors.InvalidArgument
128
+ ):
129
+ scalar_name = scalar_name_from_error(self._error)
130
+ return f"Scalar type '{scalar_name}' is not recognized"
131
+
132
+ if self._kind == RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE:
133
+ return HEALTH_CHECK_MESSAGE_DATA_TOO_LARGE
134
+ if self._kind == RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH:
135
+ return HEALTH_CHECK_MESSAGE_FILTER_TOO_MUCH
136
+ if self._kind == RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW:
137
+ return HEALTH_CHECK_MESSAGE_TOO_SLOW
138
+ if self._kind == RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE:
139
+ return HEALTH_CHECK_MESSAGE_LARGE_BASE_EXAMPLE
140
+
141
+ if self._kind == RuntimeErrorKind.HYPOTHESIS_UNSATISFIABLE:
142
+ return f"{self._error}. Possible reasons:"
143
+
144
+ if self._kind in (
145
+ RuntimeErrorKind.SCHEMA_INVALID_REGULAR_EXPRESSION,
146
+ RuntimeErrorKind.SCHEMA_GENERIC,
147
+ ):
148
+ return self._error.message # type: ignore
149
+
150
+ return str(self._error)
151
+
152
+ @cached_property
153
+ def extras(self) -> list[str]:
154
+ """Additional context about the error."""
155
+ import requests
156
+
157
+ if isinstance(self._error, requests.RequestException):
158
+ return get_request_error_extras(self._error)
159
+
160
+ if self._kind == RuntimeErrorKind.HYPOTHESIS_UNSATISFIABLE:
161
+ return [
162
+ "- Contradictory schema constraints, such as a minimum value exceeding the maximum.",
163
+ "- Invalid schema definitions for headers or cookies, for example allowing for non-ASCII characters.",
164
+ "- Excessive schema complexity, which hinders parameter generation.",
165
+ ]
166
+
167
+ return []
168
+
169
+ @cached_property
170
+ def _error_repr(self) -> str:
171
+ return format_exception(self._error, with_traceback=False)
172
+
173
+ @property
174
+ def has_useful_traceback(self) -> bool:
175
+ return self._kind not in (
176
+ RuntimeErrorKind.SCHEMA_INVALID_REGULAR_EXPRESSION,
177
+ RuntimeErrorKind.SCHEMA_INVALID_STATE_MACHINE,
178
+ RuntimeErrorKind.SCHEMA_UNSUPPORTED,
179
+ RuntimeErrorKind.SCHEMA_GENERIC,
180
+ RuntimeErrorKind.SCHEMA_NO_LINKS_FOUND,
181
+ RuntimeErrorKind.SERIALIZATION_NOT_POSSIBLE,
182
+ RuntimeErrorKind.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR,
183
+ RuntimeErrorKind.HYPOTHESIS_UNSATISFIABLE,
184
+ RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE,
185
+ RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW,
186
+ RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE,
187
+ RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH,
188
+ RuntimeErrorKind.NETWORK_OTHER,
189
+ )
190
+
191
+ @cached_property
192
+ def traceback(self) -> str:
193
+ return format_exception(self._error, with_traceback=True)
194
+
195
+ def format(self, *, bold: Callable[[str], str] = str, indent: str = " ") -> str:
196
+ """Format error message with optional styling and traceback."""
197
+ message = []
198
+
199
+ title = self.title
200
+ if title:
201
+ message.append(f"{title}\n")
202
+
203
+ # Main message
204
+ body = self.message or str(self._error)
205
+ message.append(body)
206
+
207
+ # Extras
208
+ if self.extras:
209
+ extras = self.extras
210
+ elif self.has_useful_traceback:
211
+ extras = split_traceback(self.traceback)
212
+ else:
213
+ extras = []
214
+
215
+ if extras:
216
+ message.append("") # Empty line before extras
217
+ message.extend(f"{indent}{extra}" for extra in extras)
218
+
219
+ if self._code_sample is not None:
220
+ message.append(f"\nReproduce with: \n\n {self._code_sample}")
221
+
222
+ # Suggestion
223
+ suggestion = get_runtime_error_suggestion(self._kind, bold=bold)
224
+ if suggestion is not None:
225
+ message.append(f"\nTip: {suggestion}")
226
+
227
+ return "\n".join(message)
228
+
229
+
230
+ def scalar_name_from_error(exception: hypothesis.errors.InvalidArgument) -> str:
231
+ # This one is always available as the format is checked upfront
232
+ match = re.search(r"Scalar '(\w+)' is not supported", str(exception))
233
+ match = cast(re.Match, match)
234
+ return match.group(1)
235
+
236
+
237
+ def extract_health_check_error(error: hypothesis.errors.FailedHealthCheck) -> hypothesis.HealthCheck | None:
238
+ from hypothesis import HealthCheck
239
+
240
+ match = re.search(r"add HealthCheck\.(\w+) to the suppress_health_check ", str(error))
241
+ if match:
242
+ return {
243
+ "data_too_large": HealthCheck.data_too_large,
244
+ "filter_too_much": HealthCheck.filter_too_much,
245
+ "too_slow": HealthCheck.too_slow,
246
+ "large_base_example": HealthCheck.large_base_example,
247
+ }.get(match.group(1))
248
+ return None
249
+
250
+
251
+ def get_runtime_error_suggestion(error_type: RuntimeErrorKind, bold: Callable[[str], str] = str) -> str | None:
252
+ """Get a user-friendly suggestion for handling the error."""
253
+
254
+ def _format_health_check_suggestion(label: str) -> str:
255
+ return f"Bypass this health check using {bold(f'`--suppress-health-check={label}`')}."
256
+
257
+ return {
258
+ RuntimeErrorKind.CONNECTION_SSL: f"Bypass SSL verification with {bold('`--tls-verify=false`')}.",
259
+ RuntimeErrorKind.HYPOTHESIS_UNSATISFIABLE: "Examine the schema for inconsistencies and consider simplifying it.",
260
+ RuntimeErrorKind.SCHEMA_NO_LINKS_FOUND: "Review your endpoint filters to include linked operations",
261
+ RuntimeErrorKind.SCHEMA_INVALID_REGULAR_EXPRESSION: "Ensure your regex is compatible with Python's syntax.\n"
262
+ "For guidance, visit: https://docs.python.org/3/library/re.html",
263
+ RuntimeErrorKind.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR: "Define a custom strategy for it.\n"
264
+ "For guidance, visit: https://schemathesis.readthedocs.io/en/stable/guides/graphql-custom-scalars/",
265
+ RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE: _format_health_check_suggestion("data_too_large"),
266
+ RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH: _format_health_check_suggestion("filter_too_much"),
267
+ RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW: _format_health_check_suggestion("too_slow"),
268
+ RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE: _format_health_check_suggestion(
269
+ "large_base_example"
270
+ ),
271
+ }.get(error_type)
272
+
273
+
274
+ HEALTH_CHECK_MESSAGE_DATA_TOO_LARGE = """There's a notable occurrence of examples surpassing the maximum size limit.
275
+ Typically, generating excessively large examples can compromise the quality of test outcomes.
276
+
277
+ Consider revising the schema to more accurately represent typical use cases
278
+ or applying constraints to reduce the data size."""
279
+ HEALTH_CHECK_MESSAGE_FILTER_TOO_MUCH = """A significant number of generated examples are being filtered out, indicating
280
+ that the schema's constraints may be too complex.
281
+
282
+ This level of filtration can slow down testing and affect the distribution
283
+ of generated data. Review and simplify the schema constraints where
284
+ possible to mitigate this issue."""
285
+ HEALTH_CHECK_MESSAGE_TOO_SLOW = "Data generation is extremely slow. Consider reducing the complexity of the schema."
286
+ HEALTH_CHECK_MESSAGE_LARGE_BASE_EXAMPLE = """A health check has identified that the smallest example derived from the schema
287
+ is excessively large, potentially leading to inefficient test execution.
288
+
289
+ This is commonly due to schemas that specify large-scale data structures by
290
+ default, such as an array with an extensive number of elements.
291
+
292
+ Consider revising the schema to more accurately represent typical use cases
293
+ or applying constraints to reduce the data size."""
294
+
295
+
296
+ @enum.unique
297
+ class RuntimeErrorKind(str, enum.Enum):
298
+ """Classification of runtime errors."""
299
+
300
+ # Connection related issues
301
+ CONNECTION_SSL = "connection_ssl"
302
+ CONNECTION_OTHER = "connection_other"
303
+ NETWORK_OTHER = "network_other"
304
+
305
+ # Hypothesis issues
306
+ HYPOTHESIS_UNSATISFIABLE = "hypothesis_unsatisfiable"
307
+ HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR = "hypothesis_unsupported_graphql_scalar"
308
+ HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE = "hypothesis_health_check_data_too_large"
309
+ HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH = "hypothesis_health_check_filter_too_much"
310
+ HYPOTHESIS_HEALTH_CHECK_TOO_SLOW = "hypothesis_health_check_too_slow"
311
+ HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE = "hypothesis_health_check_large_base_example"
312
+
313
+ SCHEMA_INVALID_REGULAR_EXPRESSION = "schema_invalid_regular_expression"
314
+ SCHEMA_INVALID_STATE_MACHINE = "schema_invalid_state_machine"
315
+ SCHEMA_NO_LINKS_FOUND = "schema_no_links_found"
316
+ SCHEMA_UNSUPPORTED = "schema_unsupported"
317
+ SCHEMA_GENERIC = "schema_generic"
318
+
319
+ SERIALIZATION_NOT_POSSIBLE = "serialization_not_possible"
320
+ SERIALIZATION_UNBOUNDED_PREFIX = "serialization_unbounded_prefix"
321
+
322
+ UNCLASSIFIED = "unclassified"
323
+
324
+
325
+ def _classify(*, error: Exception) -> RuntimeErrorKind:
326
+ """Classify an error."""
327
+ import hypothesis.errors
328
+ import requests
329
+ from hypothesis import HealthCheck
330
+
331
+ # Network-related errors
332
+ if isinstance(error, requests.RequestException):
333
+ if isinstance(error, requests.exceptions.SSLError):
334
+ return RuntimeErrorKind.CONNECTION_SSL
335
+ if isinstance(error, requests.exceptions.ConnectionError):
336
+ return RuntimeErrorKind.CONNECTION_OTHER
337
+ return RuntimeErrorKind.NETWORK_OTHER
338
+
339
+ # Hypothesis errors
340
+ if (
341
+ isinstance(error, hypothesis.errors.InvalidArgument)
342
+ and str(error).endswith("larger than Hypothesis is designed to handle")
343
+ or "can never generate an example, because min_size is larger than Hypothesis supports" in str(error)
344
+ ):
345
+ return RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE
346
+ if isinstance(error, hypothesis.errors.Unsatisfiable):
347
+ return RuntimeErrorKind.HYPOTHESIS_UNSATISFIABLE
348
+ if isinstance(error, hypothesis.errors.FailedHealthCheck):
349
+ health_check = extract_health_check_error(error)
350
+ if health_check is not None:
351
+ return {
352
+ HealthCheck.data_too_large: RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE,
353
+ HealthCheck.filter_too_much: RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH,
354
+ HealthCheck.too_slow: RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW,
355
+ HealthCheck.large_base_example: RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE,
356
+ }[health_check]
357
+ return RuntimeErrorKind.UNCLASSIFIED
358
+ if isinstance(error, hypothesis.errors.InvalidArgument) and str(error).startswith("Scalar "):
359
+ # Comes from `hypothesis-graphql`
360
+ return RuntimeErrorKind.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR
361
+
362
+ # Schema errors
363
+ if isinstance(error, errors.InvalidSchema):
364
+ if isinstance(error, errors.InvalidRegexPattern):
365
+ return RuntimeErrorKind.SCHEMA_INVALID_REGULAR_EXPRESSION
366
+ return RuntimeErrorKind.SCHEMA_GENERIC
367
+ if isinstance(error, errors.InvalidStateMachine):
368
+ return RuntimeErrorKind.SCHEMA_INVALID_STATE_MACHINE
369
+ if isinstance(error, errors.NoLinksFound):
370
+ return RuntimeErrorKind.SCHEMA_NO_LINKS_FOUND
371
+ if isinstance(error, UnsupportedRecursiveReference):
372
+ # Recursive references are not supported right now
373
+ return RuntimeErrorKind.SCHEMA_UNSUPPORTED
374
+ if isinstance(error, errors.SerializationError):
375
+ if isinstance(error, errors.UnboundPrefix):
376
+ return RuntimeErrorKind.SERIALIZATION_UNBOUNDED_PREFIX
377
+ return RuntimeErrorKind.SERIALIZATION_NOT_POSSIBLE
378
+ return RuntimeErrorKind.UNCLASSIFIED
379
+
380
+
381
+ def deduplicate_errors(errors: Sequence[Exception]) -> Iterator[Exception]:
382
+ """Deduplicate a list of errors."""
383
+ seen = set()
384
+ serialization_media_types = set()
385
+
386
+ for error in errors:
387
+ # Collect media types
388
+ if isinstance(error, SerializationNotPossible):
389
+ for media_type in error.media_types:
390
+ serialization_media_types.add(media_type)
391
+ continue
392
+
393
+ message = canonicalize_error_message(error)
394
+ if message not in seen:
395
+ seen.add(message)
396
+ yield error
397
+
398
+ if serialization_media_types:
399
+ yield SerializationNotPossible.from_media_types(*sorted(serialization_media_types))
400
+
401
+
402
+ MEMORY_ADDRESS_RE = re.compile("0x[0-9a-fA-F]+")
403
+ URL_IN_ERROR_MESSAGE_RE = re.compile(r"Max retries exceeded with url: .*? \(Caused by")
404
+
405
+
406
+ def canonicalize_error_message(error: Exception, with_traceback: bool = True) -> str:
407
+ """Canonicalize error messages by removing dynamic components."""
408
+ message = format_exception(error, with_traceback=with_traceback)
409
+ # Replace memory addresses
410
+ message = MEMORY_ADDRESS_RE.sub("0xbaaaaaaaaaad", message)
411
+ # Remove URL information
412
+ return URL_IN_ERROR_MESSAGE_RE.sub("", message)
413
+
414
+
415
+ def clear_hypothesis_notes(exc: Exception) -> None:
416
+ notes = getattr(exc, "__notes__", [])
417
+ if any("while generating" in note for note in notes):
418
+ notes.clear()
419
+
420
+
421
+ def is_unrecoverable_network_error(exc: Exception) -> bool:
422
+ from http.client import RemoteDisconnected
423
+
424
+ from urllib3.exceptions import ProtocolError
425
+
426
+ def has_connection_reset(inner: BaseException) -> bool:
427
+ exc_str = str(inner)
428
+ if any(pattern in exc_str for pattern in ["Connection reset by peer", "[Errno 104]", "ECONNRESET"]):
429
+ return True
430
+
431
+ if inner.__context__ is not None:
432
+ return has_connection_reset(inner.__context__)
433
+
434
+ return False
435
+
436
+ if isinstance(exc.__context__, ProtocolError):
437
+ if len(exc.__context__.args) == 2 and isinstance(exc.__context__.args[1], RemoteDisconnected):
438
+ return True
439
+ if len(exc.__context__.args) == 1 and exc.__context__.args[0] == "Response ended prematurely":
440
+ return True
441
+
442
+ return has_connection_reset(exc)
443
+
444
+
445
+ @dataclass()
446
+ class UnrecoverableNetworkError:
447
+ error: requests.ConnectionError | ChunkedEncodingError
448
+ code_sample: str
449
+
450
+ __slots__ = ("error", "code_sample")
451
+
452
+ def __init__(self, error: requests.ConnectionError | ChunkedEncodingError, code_sample: str) -> None:
453
+ self.error = error
454
+ self.code_sample = code_sample
455
+
456
+
457
+ @dataclass
458
+ class TestingState:
459
+ unrecoverable_network_error: UnrecoverableNetworkError | None
460
+
461
+ __slots__ = ("unrecoverable_network_error",)
462
+
463
+ def __init__(self) -> None:
464
+ self.unrecoverable_network_error = None