schemathesis 3.15.4__py3-none-any.whl → 4.4.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +53 -25
- schemathesis/auths.py +507 -0
- schemathesis/checks.py +190 -25
- schemathesis/cli/__init__.py +27 -1219
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +133 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +602 -0
- schemathesis/cli/commands/run/context.py +228 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +45 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
- schemathesis/cli/commands/run/handlers/output.py +1750 -0
- schemathesis/cli/commands/run/loaders.py +118 -0
- schemathesis/cli/commands/run/validation.py +256 -0
- schemathesis/cli/constants.py +5 -0
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +203 -0
- schemathesis/cli/ext/options.py +81 -0
- schemathesis/config/__init__.py +202 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +101 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +163 -0
- schemathesis/config/_generation.py +157 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +335 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +253 -0
- schemathesis/config/_projects.py +543 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +89 -0
- schemathesis/config/schema.json +975 -0
- schemathesis/core/__init__.py +72 -0
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +100 -0
- schemathesis/core/deserialization.py +210 -0
- schemathesis/core/errors.py +588 -0
- schemathesis/core/failures.py +316 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +183 -0
- schemathesis/core/jsonschema/keywords.py +40 -0
- schemathesis/core/jsonschema/references.py +222 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/lazy_import.py +15 -0
- schemathesis/core/loaders.py +107 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/core/media_types.py +79 -0
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/core/parameters.py +45 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +34 -0
- schemathesis/core/result.py +27 -0
- schemathesis/core/schema_analysis.py +17 -0
- schemathesis/core/shell.py +203 -0
- schemathesis/core/transforms.py +144 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +73 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +152 -0
- schemathesis/engine/control.py +44 -0
- schemathesis/engine/core.py +201 -0
- schemathesis/engine/errors.py +446 -0
- schemathesis/engine/events.py +284 -0
- schemathesis/engine/observations.py +42 -0
- schemathesis/engine/phases/__init__.py +108 -0
- schemathesis/engine/phases/analysis.py +28 -0
- schemathesis/engine/phases/probes.py +172 -0
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +364 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +220 -0
- schemathesis/engine/phases/unit/_executor.py +459 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +254 -0
- schemathesis/errors.py +47 -0
- schemathesis/filters.py +395 -0
- schemathesis/generation/__init__.py +25 -0
- schemathesis/generation/case.py +478 -0
- schemathesis/generation/coverage.py +1528 -0
- schemathesis/generation/hypothesis/__init__.py +121 -0
- schemathesis/generation/hypothesis/builder.py +992 -0
- schemathesis/generation/hypothesis/examples.py +56 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +285 -0
- schemathesis/generation/meta.py +227 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +127 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +294 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +285 -0
- schemathesis/hooks.py +270 -91
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +467 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +315 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +341 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/pytest/plugin.py +357 -0
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +682 -257
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/nodes.py +26 -2
- schemathesis/specs/graphql/scalars.py +77 -12
- schemathesis/specs/graphql/schemas.py +367 -148
- schemathesis/specs/graphql/validation.py +33 -0
- schemathesis/specs/openapi/__init__.py +9 -1
- schemathesis/specs/openapi/_hypothesis.py +555 -318
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +729 -0
- schemathesis/specs/openapi/adapter/protocol.py +59 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +368 -0
- schemathesis/specs/openapi/adapter/security.py +144 -0
- schemathesis/specs/openapi/adapter/v2.py +30 -0
- schemathesis/specs/openapi/adapter/v3_0.py +30 -0
- schemathesis/specs/openapi/adapter/v3_1.py +30 -0
- schemathesis/specs/openapi/analysis.py +96 -0
- schemathesis/specs/openapi/checks.py +748 -82
- schemathesis/specs/openapi/converter.py +176 -37
- schemathesis/specs/openapi/definitions.py +599 -4
- schemathesis/specs/openapi/examples.py +581 -165
- schemathesis/specs/openapi/expressions/__init__.py +52 -5
- schemathesis/specs/openapi/expressions/extractors.py +25 -0
- schemathesis/specs/openapi/expressions/lexer.py +34 -31
- schemathesis/specs/openapi/expressions/nodes.py +97 -46
- schemathesis/specs/openapi/expressions/parser.py +35 -13
- schemathesis/specs/openapi/formats.py +122 -0
- schemathesis/specs/openapi/media_types.py +75 -0
- schemathesis/specs/openapi/negative/__init__.py +93 -73
- schemathesis/specs/openapi/negative/mutations.py +294 -103
- schemathesis/specs/openapi/negative/utils.py +0 -9
- schemathesis/specs/openapi/patterns.py +458 -0
- schemathesis/specs/openapi/references.py +60 -81
- schemathesis/specs/openapi/schemas.py +647 -666
- schemathesis/specs/openapi/serialization.py +53 -30
- schemathesis/specs/openapi/stateful/__init__.py +403 -68
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
- schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
- schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
- schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
- schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
- schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
- schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
- schemathesis/specs/openapi/stateful/inference.py +254 -0
- schemathesis/specs/openapi/stateful/links.py +219 -78
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/common.py +23 -0
- schemathesis/specs/openapi/types/v2.py +129 -0
- schemathesis/specs/openapi/types/v3.py +134 -0
- schemathesis/specs/openapi/utils.py +7 -6
- schemathesis/specs/openapi/warnings.py +75 -0
- schemathesis/transport/__init__.py +224 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +126 -0
- schemathesis/transport/requests.py +278 -0
- schemathesis/transport/serialization.py +329 -0
- schemathesis/transport/wsgi.py +175 -0
- schemathesis-4.4.2.dist-info/METADATA +213 -0
- schemathesis-4.4.2.dist-info/RECORD +192 -0
- {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
- schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
- {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
- schemathesis/_compat.py +0 -57
- schemathesis/_hypothesis.py +0 -123
- schemathesis/auth.py +0 -214
- schemathesis/cli/callbacks.py +0 -240
- schemathesis/cli/cassettes.py +0 -351
- schemathesis/cli/context.py +0 -38
- schemathesis/cli/debug.py +0 -21
- schemathesis/cli/handlers.py +0 -11
- schemathesis/cli/junitxml.py +0 -41
- schemathesis/cli/options.py +0 -70
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -521
- schemathesis/cli/output/short.py +0 -40
- schemathesis/constants.py +0 -88
- schemathesis/exceptions.py +0 -257
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -16
- schemathesis/extra/pytest_plugin.py +0 -251
- schemathesis/failures.py +0 -145
- schemathesis/fixups/__init__.py +0 -29
- schemathesis/fixups/fast_api.py +0 -30
- schemathesis/graphql.py +0 -5
- schemathesis/internal.py +0 -6
- schemathesis/lazy.py +0 -301
- schemathesis/models.py +0 -1113
- schemathesis/parameters.py +0 -91
- schemathesis/runner/__init__.py +0 -470
- schemathesis/runner/events.py +0 -242
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -791
- schemathesis/runner/impl/solo.py +0 -85
- schemathesis/runner/impl/threadpool.py +0 -367
- schemathesis/runner/serialization.py +0 -206
- schemathesis/serializers.py +0 -253
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -10
- schemathesis/service/client.py +0 -62
- schemathesis/service/constants.py +0 -25
- schemathesis/service/events.py +0 -39
- schemathesis/service/handler.py +0 -46
- schemathesis/service/hosts.py +0 -74
- schemathesis/service/metadata.py +0 -42
- schemathesis/service/models.py +0 -21
- schemathesis/service/serialization.py +0 -184
- schemathesis/service/worker.py +0 -39
- schemathesis/specs/graphql/loaders.py +0 -215
- schemathesis/specs/openapi/constants.py +0 -7
- schemathesis/specs/openapi/expressions/context.py +0 -12
- schemathesis/specs/openapi/expressions/pointers.py +0 -29
- schemathesis/specs/openapi/filters.py +0 -44
- schemathesis/specs/openapi/links.py +0 -303
- schemathesis/specs/openapi/loaders.py +0 -453
- schemathesis/specs/openapi/parameters.py +0 -430
- schemathesis/specs/openapi/security.py +0 -129
- schemathesis/specs/openapi/validation.py +0 -24
- schemathesis/stateful.py +0 -358
- schemathesis/targets.py +0 -32
- schemathesis/types.py +0 -38
- schemathesis/utils.py +0 -475
- schemathesis-3.15.4.dist-info/METADATA +0 -202
- schemathesis-3.15.4.dist-info/RECORD +0 -99
- schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
- /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
|