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

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