schemathesis 3.39.15__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.
- schemathesis/__init__.py +41 -79
- schemathesis/auths.py +111 -122
- schemathesis/checks.py +169 -60
- schemathesis/cli/__init__.py +15 -2117
- schemathesis/cli/commands/__init__.py +85 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +590 -0
- schemathesis/cli/commands/run/context.py +204 -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 +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
- schemathesis/cli/commands/run/handlers/output.py +1628 -0
- schemathesis/cli/commands/run/loaders.py +114 -0
- schemathesis/cli/commands/run/validation.py +246 -0
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +84 -0
- schemathesis/cli/{options.py → ext/options.py} +36 -34
- schemathesis/config/__init__.py +189 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +99 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +156 -0
- schemathesis/config/_generation.py +149 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +327 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +187 -0
- schemathesis/config/_projects.py +527 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +885 -0
- schemathesis/core/__init__.py +67 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +459 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/{throttling.py → core/rate_limit.py} +16 -17
- schemathesis/core/registries.py +31 -0
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +54 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +118 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +169 -0
- schemathesis/engine/errors.py +464 -0
- schemathesis/engine/events.py +258 -0
- schemathesis/engine/phases/__init__.py +88 -0
- schemathesis/{runner → engine/phases}/probes.py +52 -68
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +356 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +212 -0
- schemathesis/engine/phases/unit/_executor.py +416 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +247 -0
- schemathesis/errors.py +43 -0
- schemathesis/filters.py +17 -98
- schemathesis/generation/__init__.py +5 -33
- schemathesis/generation/case.py +317 -0
- schemathesis/generation/coverage.py +282 -175
- schemathesis/generation/hypothesis/__init__.py +36 -0
- schemathesis/generation/hypothesis/builder.py +800 -0
- schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +116 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +278 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +284 -0
- schemathesis/hooks.py +80 -101
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +455 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +313 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +281 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -273
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +42 -6
- schemathesis/specs/graphql/schemas.py +141 -137
- schemathesis/specs/graphql/validation.py +11 -17
- schemathesis/specs/openapi/__init__.py +6 -1
- schemathesis/specs/openapi/_cache.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +142 -156
- schemathesis/specs/openapi/checks.py +368 -257
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +23 -21
- schemathesis/specs/openapi/expressions/__init__.py +31 -19
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/lexer.py +1 -1
- schemathesis/specs/openapi/expressions/nodes.py +36 -41
- schemathesis/specs/openapi/expressions/parser.py +1 -1
- schemathesis/specs/openapi/formats.py +35 -7
- schemathesis/specs/openapi/media_types.py +53 -12
- schemathesis/specs/openapi/negative/__init__.py +7 -4
- schemathesis/specs/openapi/negative/mutations.py +6 -5
- schemathesis/specs/openapi/parameters.py +7 -10
- schemathesis/specs/openapi/patterns.py +94 -31
- schemathesis/specs/openapi/references.py +12 -53
- schemathesis/specs/openapi/schemas.py +238 -308
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +12 -6
- schemathesis/specs/openapi/stateful/__init__.py +268 -133
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/links.py +209 -0
- schemathesis/transport/__init__.py +142 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +124 -0
- schemathesis/transport/requests.py +244 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -11
- schemathesis/transport/wsgi.py +171 -0
- schemathesis-4.0.0.dist-info/METADATA +204 -0
- schemathesis-4.0.0.dist-info/RECORD +164 -0
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -712
- schemathesis/_override.py +0 -50
- schemathesis/_patches.py +0 -21
- schemathesis/_rate_limiter.py +0 -7
- schemathesis/cli/callbacks.py +0 -466
- schemathesis/cli/cassettes.py +0 -561
- schemathesis/cli/context.py +0 -75
- schemathesis/cli/debug.py +0 -27
- schemathesis/cli/handlers.py +0 -19
- schemathesis/cli/junitxml.py +0 -124
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -920
- schemathesis/cli/output/short.py +0 -59
- schemathesis/cli/reporting.py +0 -79
- schemathesis/cli/sanitization.py +0 -26
- schemathesis/code_samples.py +0 -151
- schemathesis/constants.py +0 -54
- schemathesis/contrib/__init__.py +0 -11
- schemathesis/contrib/openapi/__init__.py +0 -11
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -16
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -571
- schemathesis/experimental/__init__.py +0 -109
- schemathesis/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -284
- schemathesis/fixups/__init__.py +0 -37
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -28
- schemathesis/generation/_methods.py +0 -44
- schemathesis/graphql.py +0 -3
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/checks.py +0 -86
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -37
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- schemathesis/internal/output.py +0 -68
- schemathesis/internal/transformation.py +0 -26
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -474
- schemathesis/loaders.py +0 -122
- schemathesis/models.py +0 -1341
- schemathesis/parameters.py +0 -90
- schemathesis/runner/__init__.py +0 -605
- schemathesis/runner/events.py +0 -389
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/context.py +0 -88
- schemathesis/runner/impl/core.py +0 -1280
- schemathesis/runner/impl/solo.py +0 -80
- schemathesis/runner/impl/threadpool.py +0 -391
- schemathesis/runner/serialization.py +0 -544
- schemathesis/sanitization.py +0 -252
- schemathesis/serializers.py +0 -328
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -202
- schemathesis/service/client.py +0 -133
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -61
- schemathesis/service/extensions.py +0 -224
- schemathesis/service/hosts.py +0 -111
- schemathesis/service/metadata.py +0 -71
- schemathesis/service/models.py +0 -258
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -173
- schemathesis/service/usage.py +0 -66
- schemathesis/specs/graphql/loaders.py +0 -364
- schemathesis/specs/openapi/expressions/context.py +0 -16
- schemathesis/specs/openapi/links.py +0 -389
- schemathesis/specs/openapi/loaders.py +0 -707
- schemathesis/specs/openapi/stateful/statistic.py +0 -198
- schemathesis/specs/openapi/stateful/types.py +0 -14
- schemathesis/specs/openapi/validation.py +0 -26
- schemathesis/stateful/__init__.py +0 -147
- schemathesis/stateful/config.py +0 -97
- schemathesis/stateful/context.py +0 -135
- schemathesis/stateful/events.py +0 -274
- schemathesis/stateful/runner.py +0 -309
- schemathesis/stateful/sink.py +0 -68
- schemathesis/stateful/state_machine.py +0 -328
- schemathesis/stateful/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -369
- schemathesis/transports/asgi.py +0 -7
- schemathesis/transports/auth.py +0 -38
- schemathesis/transports/headers.py +0 -36
- schemathesis/transports/responses.py +0 -57
- schemathesis/types.py +0 -44
- schemathesis/utils.py +0 -164
- schemathesis-3.39.15.dist-info/METADATA +0 -293
- schemathesis-3.39.15.dist-info/RECORD +0 -160
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
- /schemathesis/{internal → core}/result.py +0 -0
- {schemathesis-3.39.15.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
|