schemathesis 3.39.7__py3-none-any.whl → 4.0.0a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +27 -65
- schemathesis/auths.py +26 -68
- schemathesis/checks.py +130 -60
- schemathesis/cli/__init__.py +5 -2105
- schemathesis/cli/commands/__init__.py +37 -0
- schemathesis/cli/commands/run/__init__.py +662 -0
- schemathesis/cli/commands/run/checks.py +80 -0
- schemathesis/cli/commands/run/context.py +117 -0
- schemathesis/cli/commands/run/events.py +30 -0
- schemathesis/cli/commands/run/executor.py +141 -0
- schemathesis/cli/commands/run/filters.py +202 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/{cassettes.py → commands/run/handlers/cassettes.py} +178 -247
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +1368 -0
- schemathesis/cli/commands/run/hypothesis.py +105 -0
- schemathesis/cli/commands/run/loaders.py +129 -0
- schemathesis/cli/{callbacks.py → commands/run/validation.py} +59 -175
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +17 -0
- schemathesis/cli/ext/fs.py +14 -0
- schemathesis/cli/ext/groups.py +55 -0
- schemathesis/cli/{options.py → ext/options.py} +37 -16
- schemathesis/cli/hooks.py +36 -0
- schemathesis/contrib/__init__.py +1 -3
- schemathesis/contrib/openapi/__init__.py +1 -3
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -7
- schemathesis/core/__init__.py +58 -0
- schemathesis/core/compat.py +25 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +370 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -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/{internal/output.py → core/output/__init__.py} +1 -0
- schemathesis/core/output/sanitization.py +197 -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 +108 -0
- schemathesis/core/validation.py +38 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +30 -0
- schemathesis/engine/config.py +59 -0
- schemathesis/engine/context.py +119 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +157 -0
- schemathesis/engine/errors.py +394 -0
- schemathesis/engine/events.py +243 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{runner → engine/phases}/probes.py +49 -68
- schemathesis/engine/phases/stateful/__init__.py +66 -0
- schemathesis/engine/phases/stateful/_executor.py +301 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +175 -0
- schemathesis/engine/phases/unit/_executor.py +322 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +246 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +9 -40
- schemathesis/filters.py +7 -95
- schemathesis/generation/__init__.py +3 -3
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +22 -22
- schemathesis/{_patches.py → generation/hypothesis/__init__.py} +15 -6
- schemathesis/generation/hypothesis/builder.py +585 -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/modes.py +28 -0
- schemathesis/generation/overrides.py +96 -0
- schemathesis/generation/stateful/__init__.py +20 -0
- schemathesis/{stateful → generation/stateful}/state_machine.py +84 -109
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +17 -62
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +387 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +63 -0
- schemathesis/openapi/loaders.py +178 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +273 -0
- schemathesis/pytest/loaders.py +12 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +94 -107
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +456 -228
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +5 -3
- schemathesis/specs/graphql/schemas.py +122 -123
- 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 +97 -134
- schemathesis/specs/openapi/checks.py +238 -219
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +22 -20
- schemathesis/specs/openapi/expressions/__init__.py +11 -15
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/nodes.py +33 -32
- schemathesis/specs/openapi/formats.py +3 -2
- schemathesis/specs/openapi/links.py +123 -299
- schemathesis/specs/openapi/media_types.py +10 -12
- schemathesis/specs/openapi/negative/__init__.py +2 -1
- schemathesis/specs/openapi/negative/mutations.py +3 -2
- schemathesis/specs/openapi/parameters.py +8 -6
- schemathesis/specs/openapi/patterns.py +1 -1
- schemathesis/specs/openapi/references.py +11 -51
- schemathesis/specs/openapi/schemas.py +177 -191
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +10 -6
- schemathesis/specs/openapi/stateful/__init__.py +97 -91
- schemathesis/transport/__init__.py +104 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +99 -0
- schemathesis/transport/requests.py +221 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -7
- schemathesis/transport/wsgi.py +165 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +18 -14
- schemathesis-4.0.0a2.dist-info/RECORD +151 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -559
- schemathesis/_override.py +0 -50
- schemathesis/_rate_limiter.py +0 -7
- 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 -936
- 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 -56
- 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/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -277
- 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 -84
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -38
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- 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 -104
- schemathesis/runner/impl/core.py +0 -1246
- 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/loaders.py +0 -708
- 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/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -359
- 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.7.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.7.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,394 @@
|
|
1
|
+
"""Handling of recoverable errors in Schemathesis Engine.
|
2
|
+
|
3
|
+
This module provides utilities for analyzing, classifying, and formatting exceptions
|
4
|
+
that occur during test execution via Schemathesis Engine.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from __future__ import annotations
|
8
|
+
|
9
|
+
import enum
|
10
|
+
import re
|
11
|
+
from functools import cached_property
|
12
|
+
from typing import TYPE_CHECKING, Callable, Iterator, Sequence, cast
|
13
|
+
|
14
|
+
from schemathesis import errors
|
15
|
+
from schemathesis.core.errors import (
|
16
|
+
RECURSIVE_REFERENCE_ERROR_MESSAGE,
|
17
|
+
SerializationNotPossible,
|
18
|
+
format_exception,
|
19
|
+
get_request_error_extras,
|
20
|
+
get_request_error_message,
|
21
|
+
split_traceback,
|
22
|
+
)
|
23
|
+
|
24
|
+
if TYPE_CHECKING:
|
25
|
+
import hypothesis.errors
|
26
|
+
|
27
|
+
__all__ = ["EngineErrorInfo", "DeadlineExceeded", "UnsupportedRecursiveReference", "UnexpectedError"]
|
28
|
+
|
29
|
+
|
30
|
+
class DeadlineExceeded(errors.SchemathesisError):
|
31
|
+
"""Test took too long to run."""
|
32
|
+
|
33
|
+
@classmethod
|
34
|
+
def from_exc(cls, exc: hypothesis.errors.DeadlineExceeded) -> DeadlineExceeded:
|
35
|
+
runtime = exc.runtime.total_seconds() * 1000
|
36
|
+
deadline = exc.deadline.total_seconds() * 1000
|
37
|
+
return cls(
|
38
|
+
f"Test running time is too slow! It took {runtime:.2f}ms, which exceeds the deadline of {deadline:.2f}ms.\n"
|
39
|
+
)
|
40
|
+
|
41
|
+
|
42
|
+
class UnsupportedRecursiveReference(errors.SchemathesisError):
|
43
|
+
"""Recursive reference is impossible to resolve due to current limitations."""
|
44
|
+
|
45
|
+
def __init__(self) -> None:
|
46
|
+
super().__init__(RECURSIVE_REFERENCE_ERROR_MESSAGE)
|
47
|
+
|
48
|
+
|
49
|
+
class UnexpectedError(errors.SchemathesisError):
|
50
|
+
"""An unexpected error during the engine execution.
|
51
|
+
|
52
|
+
Used primarily to not let Hypothesis consider the test as flaky or detect multiple failures as we handle it
|
53
|
+
on our side.
|
54
|
+
"""
|
55
|
+
|
56
|
+
|
57
|
+
class EngineErrorInfo:
|
58
|
+
"""Extended information about errors that happen during engine execution.
|
59
|
+
|
60
|
+
It serves as a caching wrapper around exceptions to avoid repeated computations.
|
61
|
+
"""
|
62
|
+
|
63
|
+
def __init__(self, error: Exception) -> None:
|
64
|
+
self._error = error
|
65
|
+
|
66
|
+
def __str__(self) -> str:
|
67
|
+
return self._error_repr
|
68
|
+
|
69
|
+
@cached_property
|
70
|
+
def _kind(self) -> RuntimeErrorKind:
|
71
|
+
"""Error kind."""
|
72
|
+
return _classify(error=self._error)
|
73
|
+
|
74
|
+
@property
|
75
|
+
def title(self) -> str:
|
76
|
+
"""A general error description."""
|
77
|
+
import requests
|
78
|
+
|
79
|
+
if isinstance(self._error, requests.RequestException):
|
80
|
+
return "Network Error"
|
81
|
+
|
82
|
+
if self._kind in (
|
83
|
+
RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE,
|
84
|
+
RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH,
|
85
|
+
RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW,
|
86
|
+
RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE,
|
87
|
+
):
|
88
|
+
return "Failed Health Check"
|
89
|
+
|
90
|
+
if self._kind in (
|
91
|
+
RuntimeErrorKind.SCHEMA_INVALID_REGULAR_EXPRESSION,
|
92
|
+
RuntimeErrorKind.SCHEMA_GENERIC,
|
93
|
+
RuntimeErrorKind.HYPOTHESIS_UNSATISFIABLE,
|
94
|
+
):
|
95
|
+
return "Schema Error"
|
96
|
+
|
97
|
+
return {
|
98
|
+
RuntimeErrorKind.SCHEMA_UNSUPPORTED: "Unsupported Schema",
|
99
|
+
RuntimeErrorKind.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR: "Unknown GraphQL Scalar",
|
100
|
+
RuntimeErrorKind.SERIALIZATION_UNBOUNDED_PREFIX: "XML serialization error",
|
101
|
+
RuntimeErrorKind.SERIALIZATION_NOT_POSSIBLE: "Serialization not possible",
|
102
|
+
}.get(self._kind, "Runtime Error")
|
103
|
+
|
104
|
+
@property
|
105
|
+
def message(self) -> str:
|
106
|
+
"""Detailed error description."""
|
107
|
+
import hypothesis.errors
|
108
|
+
import requests
|
109
|
+
|
110
|
+
if isinstance(self._error, requests.RequestException):
|
111
|
+
return get_request_error_message(self._error)
|
112
|
+
|
113
|
+
if self._kind == RuntimeErrorKind.SCHEMA_UNSUPPORTED:
|
114
|
+
return str(self._error).strip()
|
115
|
+
|
116
|
+
if self._kind == RuntimeErrorKind.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR and isinstance(
|
117
|
+
self._error, hypothesis.errors.InvalidArgument
|
118
|
+
):
|
119
|
+
scalar_name = scalar_name_from_error(self._error)
|
120
|
+
return f"Scalar type '{scalar_name}' is not recognized"
|
121
|
+
|
122
|
+
if self._kind == RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE:
|
123
|
+
return HEALTH_CHECK_MESSAGE_DATA_TOO_LARGE
|
124
|
+
if self._kind == RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH:
|
125
|
+
return HEALTH_CHECK_MESSAGE_FILTER_TOO_MUCH
|
126
|
+
if self._kind == RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW:
|
127
|
+
return HEALTH_CHECK_MESSAGE_TOO_SLOW
|
128
|
+
if self._kind == RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE:
|
129
|
+
return HEALTH_CHECK_MESSAGE_LARGE_BASE_EXAMPLE
|
130
|
+
|
131
|
+
if self._kind == RuntimeErrorKind.HYPOTHESIS_UNSATISFIABLE:
|
132
|
+
return f"{self._error}. Possible reasons:"
|
133
|
+
|
134
|
+
if self._kind in (
|
135
|
+
RuntimeErrorKind.SCHEMA_INVALID_REGULAR_EXPRESSION,
|
136
|
+
RuntimeErrorKind.SCHEMA_GENERIC,
|
137
|
+
):
|
138
|
+
return self._error.message # type: ignore
|
139
|
+
|
140
|
+
return str(self._error)
|
141
|
+
|
142
|
+
@cached_property
|
143
|
+
def extras(self) -> list[str]:
|
144
|
+
"""Additional context about the error."""
|
145
|
+
import requests
|
146
|
+
|
147
|
+
if isinstance(self._error, requests.RequestException):
|
148
|
+
return get_request_error_extras(self._error)
|
149
|
+
|
150
|
+
if self._kind == RuntimeErrorKind.HYPOTHESIS_UNSATISFIABLE:
|
151
|
+
return [
|
152
|
+
"- Contradictory schema constraints, such as a minimum value exceeding the maximum.",
|
153
|
+
"- Invalid schema definitions for headers or cookies, for example allowing for non-ASCII characters.",
|
154
|
+
"- Excessive schema complexity, which hinders parameter generation.",
|
155
|
+
]
|
156
|
+
|
157
|
+
return []
|
158
|
+
|
159
|
+
@cached_property
|
160
|
+
def _error_repr(self) -> str:
|
161
|
+
return format_exception(self._error, with_traceback=False)
|
162
|
+
|
163
|
+
@property
|
164
|
+
def has_useful_traceback(self) -> bool:
|
165
|
+
return self._kind not in (
|
166
|
+
RuntimeErrorKind.SCHEMA_INVALID_REGULAR_EXPRESSION,
|
167
|
+
RuntimeErrorKind.SCHEMA_UNSUPPORTED,
|
168
|
+
RuntimeErrorKind.SCHEMA_GENERIC,
|
169
|
+
RuntimeErrorKind.SERIALIZATION_NOT_POSSIBLE,
|
170
|
+
RuntimeErrorKind.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR,
|
171
|
+
RuntimeErrorKind.HYPOTHESIS_UNSATISFIABLE,
|
172
|
+
RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE,
|
173
|
+
RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW,
|
174
|
+
RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE,
|
175
|
+
RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH,
|
176
|
+
RuntimeErrorKind.NETWORK_OTHER,
|
177
|
+
)
|
178
|
+
|
179
|
+
@cached_property
|
180
|
+
def traceback(self) -> str:
|
181
|
+
return format_exception(self._error, with_traceback=True)
|
182
|
+
|
183
|
+
def format(self, *, bold: Callable[[str], str] = str, indent: str = " ") -> str:
|
184
|
+
"""Format error message with optional styling and traceback."""
|
185
|
+
message = []
|
186
|
+
|
187
|
+
# Title
|
188
|
+
if self._kind == RuntimeErrorKind.SCHEMA_GENERIC:
|
189
|
+
title = "Schema Error"
|
190
|
+
else:
|
191
|
+
title = self.title
|
192
|
+
if title:
|
193
|
+
message.append(f"{title}\n")
|
194
|
+
|
195
|
+
# Main message
|
196
|
+
body = self.message or str(self._error)
|
197
|
+
message.append(body)
|
198
|
+
|
199
|
+
# Extras
|
200
|
+
if self.extras:
|
201
|
+
extras = self.extras
|
202
|
+
elif self.has_useful_traceback:
|
203
|
+
extras = split_traceback(self.traceback)
|
204
|
+
else:
|
205
|
+
extras = []
|
206
|
+
|
207
|
+
if extras:
|
208
|
+
message.append("") # Empty line before extras
|
209
|
+
message.extend(f"{indent}{extra}" for extra in extras)
|
210
|
+
|
211
|
+
# Suggestion
|
212
|
+
suggestion = get_runtime_error_suggestion(self._kind, bold=bold)
|
213
|
+
if suggestion is not None:
|
214
|
+
message.append(f"\nTip: {suggestion}")
|
215
|
+
|
216
|
+
return "\n".join(message)
|
217
|
+
|
218
|
+
|
219
|
+
def scalar_name_from_error(exception: hypothesis.errors.InvalidArgument) -> str:
|
220
|
+
# This one is always available as the format is checked upfront
|
221
|
+
match = re.search(r"Scalar '(\w+)' is not supported", str(exception))
|
222
|
+
match = cast(re.Match, match)
|
223
|
+
return match.group(1)
|
224
|
+
|
225
|
+
|
226
|
+
def extract_health_check_error(error: hypothesis.errors.FailedHealthCheck) -> hypothesis.HealthCheck | None:
|
227
|
+
from hypothesis import HealthCheck
|
228
|
+
|
229
|
+
match = re.search(r"add HealthCheck\.(\w+) to the suppress_health_check ", str(error))
|
230
|
+
if match:
|
231
|
+
return {
|
232
|
+
"data_too_large": HealthCheck.data_too_large,
|
233
|
+
"filter_too_much": HealthCheck.filter_too_much,
|
234
|
+
"too_slow": HealthCheck.too_slow,
|
235
|
+
"large_base_example": HealthCheck.large_base_example,
|
236
|
+
}.get(match.group(1))
|
237
|
+
return None
|
238
|
+
|
239
|
+
|
240
|
+
def get_runtime_error_suggestion(error_type: RuntimeErrorKind, bold: Callable[[str], str] = str) -> str | None:
|
241
|
+
"""Get a user-friendly suggestion for handling the error."""
|
242
|
+
|
243
|
+
def _format_health_check_suggestion(label: str) -> str:
|
244
|
+
return f"Bypass this health check using {bold(f'`--suppress-health-check={label}`')}."
|
245
|
+
|
246
|
+
return {
|
247
|
+
RuntimeErrorKind.CONNECTION_SSL: f"Bypass SSL verification with {bold('`--request-tls-verify=false`')}.",
|
248
|
+
RuntimeErrorKind.HYPOTHESIS_UNSATISFIABLE: "Examine the schema for inconsistencies and consider simplifying it.",
|
249
|
+
RuntimeErrorKind.SCHEMA_INVALID_REGULAR_EXPRESSION: "Ensure your regex is compatible with Python's syntax.\n"
|
250
|
+
"For guidance, visit: https://docs.python.org/3/library/re.html",
|
251
|
+
RuntimeErrorKind.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR: "Define a custom strategy for it.\n"
|
252
|
+
"For guidance, visit: https://schemathesis.readthedocs.io/en/stable/graphql.html#custom-scalars",
|
253
|
+
RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE: _format_health_check_suggestion("data_too_large"),
|
254
|
+
RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH: _format_health_check_suggestion("filter_too_much"),
|
255
|
+
RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW: _format_health_check_suggestion("too_slow"),
|
256
|
+
RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE: _format_health_check_suggestion(
|
257
|
+
"large_base_example"
|
258
|
+
),
|
259
|
+
}.get(error_type)
|
260
|
+
|
261
|
+
|
262
|
+
HEALTH_CHECK_MESSAGE_DATA_TOO_LARGE = """There's a notable occurrence of examples surpassing the maximum size limit.
|
263
|
+
Typically, generating excessively large examples can compromise the quality of test outcomes.
|
264
|
+
|
265
|
+
Consider revising the schema to more accurately represent typical use cases
|
266
|
+
or applying constraints to reduce the data size."""
|
267
|
+
HEALTH_CHECK_MESSAGE_FILTER_TOO_MUCH = """A significant number of generated examples are being filtered out, indicating
|
268
|
+
that the schema's constraints may be too complex.
|
269
|
+
|
270
|
+
This level of filtration can slow down testing and affect the distribution
|
271
|
+
of generated data. Review and simplify the schema constraints where
|
272
|
+
possible to mitigate this issue."""
|
273
|
+
HEALTH_CHECK_MESSAGE_TOO_SLOW = "Data generation is extremely slow. Consider reducing the complexity of the schema."
|
274
|
+
HEALTH_CHECK_MESSAGE_LARGE_BASE_EXAMPLE = """A health check has identified that the smallest example derived from the schema
|
275
|
+
is excessively large, potentially leading to inefficient test execution.
|
276
|
+
|
277
|
+
This is commonly due to schemas that specify large-scale data structures by
|
278
|
+
default, such as an array with an extensive number of elements.
|
279
|
+
|
280
|
+
Consider revising the schema to more accurately represent typical use cases
|
281
|
+
or applying constraints to reduce the data size."""
|
282
|
+
|
283
|
+
|
284
|
+
@enum.unique
|
285
|
+
class RuntimeErrorKind(str, enum.Enum):
|
286
|
+
"""Classification of runtime errors."""
|
287
|
+
|
288
|
+
# Connection related issues
|
289
|
+
CONNECTION_SSL = "connection_ssl"
|
290
|
+
CONNECTION_OTHER = "connection_other"
|
291
|
+
NETWORK_OTHER = "network_other"
|
292
|
+
|
293
|
+
# Hypothesis issues
|
294
|
+
HYPOTHESIS_UNSATISFIABLE = "hypothesis_unsatisfiable"
|
295
|
+
HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR = "hypothesis_unsupported_graphql_scalar"
|
296
|
+
HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE = "hypothesis_health_check_data_too_large"
|
297
|
+
HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH = "hypothesis_health_check_filter_too_much"
|
298
|
+
HYPOTHESIS_HEALTH_CHECK_TOO_SLOW = "hypothesis_health_check_too_slow"
|
299
|
+
HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE = "hypothesis_health_check_large_base_example"
|
300
|
+
|
301
|
+
SCHEMA_INVALID_REGULAR_EXPRESSION = "schema_invalid_regular_expression"
|
302
|
+
SCHEMA_UNSUPPORTED = "schema_unsupported"
|
303
|
+
SCHEMA_GENERIC = "schema_generic"
|
304
|
+
|
305
|
+
SERIALIZATION_NOT_POSSIBLE = "serialization_not_possible"
|
306
|
+
SERIALIZATION_UNBOUNDED_PREFIX = "serialization_unbounded_prefix"
|
307
|
+
|
308
|
+
UNCLASSIFIED = "unclassified"
|
309
|
+
|
310
|
+
|
311
|
+
def _classify(*, error: Exception) -> RuntimeErrorKind:
|
312
|
+
"""Classify an error."""
|
313
|
+
import hypothesis.errors
|
314
|
+
import requests
|
315
|
+
from hypothesis import HealthCheck
|
316
|
+
|
317
|
+
# Network-related errors
|
318
|
+
if isinstance(error, requests.RequestException):
|
319
|
+
if isinstance(error, requests.exceptions.SSLError):
|
320
|
+
return RuntimeErrorKind.CONNECTION_SSL
|
321
|
+
if isinstance(error, requests.exceptions.ConnectionError):
|
322
|
+
return RuntimeErrorKind.CONNECTION_OTHER
|
323
|
+
return RuntimeErrorKind.NETWORK_OTHER
|
324
|
+
|
325
|
+
# Hypothesis errors
|
326
|
+
if (
|
327
|
+
isinstance(error, hypothesis.errors.InvalidArgument)
|
328
|
+
and str(error).endswith("larger than Hypothesis is designed to handle")
|
329
|
+
or "can never generate an example, because min_size is larger than Hypothesis supports" in str(error)
|
330
|
+
):
|
331
|
+
return RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE
|
332
|
+
if isinstance(error, hypothesis.errors.Unsatisfiable):
|
333
|
+
return RuntimeErrorKind.HYPOTHESIS_UNSATISFIABLE
|
334
|
+
if isinstance(error, hypothesis.errors.FailedHealthCheck):
|
335
|
+
health_check = extract_health_check_error(error)
|
336
|
+
if health_check is not None:
|
337
|
+
return {
|
338
|
+
HealthCheck.data_too_large: RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE,
|
339
|
+
HealthCheck.filter_too_much: RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH,
|
340
|
+
HealthCheck.too_slow: RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW,
|
341
|
+
HealthCheck.large_base_example: RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE,
|
342
|
+
}[health_check]
|
343
|
+
return RuntimeErrorKind.UNCLASSIFIED
|
344
|
+
if isinstance(error, hypothesis.errors.InvalidArgument) and str(error).startswith("Scalar "):
|
345
|
+
# Comes from `hypothesis-graphql`
|
346
|
+
return RuntimeErrorKind.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR
|
347
|
+
|
348
|
+
# Schema errors
|
349
|
+
if isinstance(error, errors.InvalidSchema):
|
350
|
+
if isinstance(error, errors.InvalidRegexPattern):
|
351
|
+
return RuntimeErrorKind.SCHEMA_INVALID_REGULAR_EXPRESSION
|
352
|
+
return RuntimeErrorKind.SCHEMA_GENERIC
|
353
|
+
if isinstance(error, UnsupportedRecursiveReference):
|
354
|
+
# Recursive references are not supported right now
|
355
|
+
return RuntimeErrorKind.SCHEMA_UNSUPPORTED
|
356
|
+
if isinstance(error, errors.SerializationError):
|
357
|
+
if isinstance(error, errors.UnboundPrefix):
|
358
|
+
return RuntimeErrorKind.SERIALIZATION_UNBOUNDED_PREFIX
|
359
|
+
return RuntimeErrorKind.SERIALIZATION_NOT_POSSIBLE
|
360
|
+
return RuntimeErrorKind.UNCLASSIFIED
|
361
|
+
|
362
|
+
|
363
|
+
def deduplicate_errors(errors: Sequence[Exception]) -> Iterator[Exception]:
|
364
|
+
"""Deduplicate a list of errors."""
|
365
|
+
seen = set()
|
366
|
+
serialization_media_types = set()
|
367
|
+
|
368
|
+
for error in errors:
|
369
|
+
# Collect media types
|
370
|
+
if isinstance(error, SerializationNotPossible):
|
371
|
+
for media_type in error.media_types:
|
372
|
+
serialization_media_types.add(media_type)
|
373
|
+
continue
|
374
|
+
|
375
|
+
message = canonicalize_error_message(error)
|
376
|
+
if message not in seen:
|
377
|
+
seen.add(message)
|
378
|
+
yield error
|
379
|
+
|
380
|
+
if serialization_media_types:
|
381
|
+
yield SerializationNotPossible.from_media_types(*sorted(serialization_media_types))
|
382
|
+
|
383
|
+
|
384
|
+
MEMORY_ADDRESS_RE = re.compile("0x[0-9a-fA-F]+")
|
385
|
+
URL_IN_ERROR_MESSAGE_RE = re.compile(r"Max retries exceeded with url: .*? \(Caused by")
|
386
|
+
|
387
|
+
|
388
|
+
def canonicalize_error_message(error: Exception, with_traceback: bool = True) -> str:
|
389
|
+
"""Canonicalize error messages by removing dynamic components."""
|
390
|
+
message = format_exception(error, with_traceback=with_traceback)
|
391
|
+
# Replace memory addresses
|
392
|
+
message = MEMORY_ADDRESS_RE.sub("0xbaaaaaaaaaad", message)
|
393
|
+
# Remove URL information
|
394
|
+
return URL_IN_ERROR_MESSAGE_RE.sub("", message)
|
@@ -0,0 +1,243 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import time
|
4
|
+
import uuid
|
5
|
+
from dataclasses import dataclass
|
6
|
+
from typing import TYPE_CHECKING, Generator
|
7
|
+
|
8
|
+
from schemathesis.core.result import Result
|
9
|
+
from schemathesis.engine.errors import EngineErrorInfo
|
10
|
+
from schemathesis.engine.phases import Phase, PhaseName
|
11
|
+
from schemathesis.engine.recorder import ScenarioRecorder
|
12
|
+
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
from schemathesis.engine import Status
|
15
|
+
from schemathesis.engine.phases.probes import ProbePayload
|
16
|
+
|
17
|
+
EventGenerator = Generator["EngineEvent", None, None]
|
18
|
+
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class EngineEvent:
|
22
|
+
"""An event within the engine's lifecycle."""
|
23
|
+
|
24
|
+
id: uuid.UUID
|
25
|
+
timestamp: float
|
26
|
+
# Indicates whether this event is the last in the event stream
|
27
|
+
is_terminal = False
|
28
|
+
|
29
|
+
|
30
|
+
@dataclass
|
31
|
+
class EngineStarted(EngineEvent):
|
32
|
+
"""Start of an engine."""
|
33
|
+
|
34
|
+
__slots__ = ("id", "timestamp")
|
35
|
+
|
36
|
+
def __init__(self) -> None:
|
37
|
+
self.id = uuid.uuid4()
|
38
|
+
self.timestamp = time.time()
|
39
|
+
|
40
|
+
|
41
|
+
@dataclass
|
42
|
+
class PhaseEvent(EngineEvent):
|
43
|
+
"""Event associated with a specific execution phase."""
|
44
|
+
|
45
|
+
phase: Phase
|
46
|
+
|
47
|
+
|
48
|
+
@dataclass
|
49
|
+
class PhaseStarted(PhaseEvent):
|
50
|
+
"""Start of an execution phase."""
|
51
|
+
|
52
|
+
__slots__ = ("id", "timestamp", "phase")
|
53
|
+
|
54
|
+
def __init__(self, *, phase: Phase) -> None:
|
55
|
+
self.id = uuid.uuid4()
|
56
|
+
self.timestamp = time.time()
|
57
|
+
self.phase = phase
|
58
|
+
|
59
|
+
|
60
|
+
@dataclass
|
61
|
+
class PhaseFinished(PhaseEvent):
|
62
|
+
"""End of an execution phase."""
|
63
|
+
|
64
|
+
status: Status
|
65
|
+
payload: Result[ProbePayload, Exception] | None
|
66
|
+
|
67
|
+
__slots__ = ("id", "timestamp", "phase", "status", "payload")
|
68
|
+
|
69
|
+
def __init__(self, *, phase: Phase, status: Status, payload: Result[ProbePayload, Exception] | None) -> None:
|
70
|
+
self.id = uuid.uuid4()
|
71
|
+
self.timestamp = time.time()
|
72
|
+
self.phase = phase
|
73
|
+
self.status = status
|
74
|
+
self.payload = payload
|
75
|
+
|
76
|
+
|
77
|
+
@dataclass
|
78
|
+
class TestEvent(EngineEvent):
|
79
|
+
phase: PhaseName
|
80
|
+
|
81
|
+
|
82
|
+
@dataclass
|
83
|
+
class SuiteStarted(TestEvent):
|
84
|
+
"""Before executing a set of scenarios."""
|
85
|
+
|
86
|
+
__slots__ = ("id", "timestamp", "phase")
|
87
|
+
|
88
|
+
def __init__(self, *, phase: PhaseName) -> None:
|
89
|
+
self.id = uuid.uuid4()
|
90
|
+
self.timestamp = time.time()
|
91
|
+
self.phase = phase
|
92
|
+
|
93
|
+
|
94
|
+
@dataclass
|
95
|
+
class SuiteFinished(TestEvent):
|
96
|
+
"""After executing a set of test scenarios."""
|
97
|
+
|
98
|
+
status: Status
|
99
|
+
|
100
|
+
__slots__ = ("id", "timestamp", "phase", "status")
|
101
|
+
|
102
|
+
def __init__(self, *, id: uuid.UUID, phase: PhaseName, status: Status) -> None:
|
103
|
+
self.id = id
|
104
|
+
self.timestamp = time.time()
|
105
|
+
self.phase = phase
|
106
|
+
self.status = status
|
107
|
+
|
108
|
+
|
109
|
+
@dataclass
|
110
|
+
class ScenarioEvent(TestEvent):
|
111
|
+
suite_id: uuid.UUID
|
112
|
+
|
113
|
+
|
114
|
+
@dataclass
|
115
|
+
class ScenarioStarted(ScenarioEvent):
|
116
|
+
"""Before executing a grouped set of test steps."""
|
117
|
+
|
118
|
+
__slots__ = ("id", "timestamp", "phase", "suite_id", "label")
|
119
|
+
|
120
|
+
def __init__(self, *, phase: PhaseName, suite_id: uuid.UUID, label: str | None) -> None:
|
121
|
+
self.id = uuid.uuid4()
|
122
|
+
self.timestamp = time.time()
|
123
|
+
self.phase = phase
|
124
|
+
self.suite_id = suite_id
|
125
|
+
self.label = label
|
126
|
+
|
127
|
+
|
128
|
+
@dataclass
|
129
|
+
class ScenarioFinished(ScenarioEvent):
|
130
|
+
"""After executing a grouped set of test steps."""
|
131
|
+
|
132
|
+
status: Status
|
133
|
+
recorder: ScenarioRecorder
|
134
|
+
elapsed_time: float
|
135
|
+
skip_reason: str | None
|
136
|
+
# Whether this is a scenario that tries to reproduce a failure
|
137
|
+
is_final: bool
|
138
|
+
|
139
|
+
__slots__ = (
|
140
|
+
"id",
|
141
|
+
"timestamp",
|
142
|
+
"phase",
|
143
|
+
"suite_id",
|
144
|
+
"label",
|
145
|
+
"status",
|
146
|
+
"recorder",
|
147
|
+
"elapsed_time",
|
148
|
+
"skip_reason",
|
149
|
+
"is_final",
|
150
|
+
)
|
151
|
+
|
152
|
+
def __init__(
|
153
|
+
self,
|
154
|
+
*,
|
155
|
+
id: uuid.UUID,
|
156
|
+
phase: PhaseName,
|
157
|
+
suite_id: uuid.UUID,
|
158
|
+
label: str | None,
|
159
|
+
status: Status,
|
160
|
+
recorder: ScenarioRecorder,
|
161
|
+
elapsed_time: float,
|
162
|
+
skip_reason: str | None,
|
163
|
+
is_final: bool,
|
164
|
+
) -> None:
|
165
|
+
self.id = id
|
166
|
+
self.timestamp = time.time()
|
167
|
+
self.phase = phase
|
168
|
+
self.suite_id = suite_id
|
169
|
+
self.label = label
|
170
|
+
self.status = status
|
171
|
+
self.recorder = recorder
|
172
|
+
self.elapsed_time = elapsed_time
|
173
|
+
self.skip_reason = skip_reason
|
174
|
+
self.is_final = is_final
|
175
|
+
|
176
|
+
|
177
|
+
@dataclass
|
178
|
+
class Interrupted(EngineEvent):
|
179
|
+
"""If execution was interrupted by Ctrl-C, or a received SIGTERM."""
|
180
|
+
|
181
|
+
phase: PhaseName | None
|
182
|
+
|
183
|
+
__slots__ = ("id", "timestamp", "phase")
|
184
|
+
|
185
|
+
def __init__(self, *, phase: PhaseName | None) -> None:
|
186
|
+
self.id = uuid.uuid4()
|
187
|
+
self.timestamp = time.time()
|
188
|
+
self.phase = phase
|
189
|
+
|
190
|
+
|
191
|
+
@dataclass
|
192
|
+
class NonFatalError(EngineEvent):
|
193
|
+
"""Error that doesn't halt execution but should be reported."""
|
194
|
+
|
195
|
+
info: EngineErrorInfo
|
196
|
+
value: Exception
|
197
|
+
phase: PhaseName
|
198
|
+
label: str
|
199
|
+
related_to_operation: bool
|
200
|
+
|
201
|
+
__slots__ = ("id", "timestamp", "info", "value", "phase", "label", "related_to_operation")
|
202
|
+
|
203
|
+
def __init__(self, *, error: Exception, phase: PhaseName, label: str, related_to_operation: bool) -> None:
|
204
|
+
self.id = uuid.uuid4()
|
205
|
+
self.timestamp = time.time()
|
206
|
+
self.info = EngineErrorInfo(error=error)
|
207
|
+
self.value = error
|
208
|
+
self.phase = phase
|
209
|
+
self.label = label
|
210
|
+
self.related_to_operation = related_to_operation
|
211
|
+
|
212
|
+
|
213
|
+
@dataclass
|
214
|
+
class FatalError(EngineEvent):
|
215
|
+
"""Internal error in the engine."""
|
216
|
+
|
217
|
+
exception: Exception
|
218
|
+
is_terminal = True
|
219
|
+
|
220
|
+
__slots__ = ("id", "timestamp", "exception")
|
221
|
+
|
222
|
+
def __init__(self, *, exception: Exception) -> None:
|
223
|
+
self.id = uuid.uuid4()
|
224
|
+
self.timestamp = time.time()
|
225
|
+
self.exception = exception
|
226
|
+
|
227
|
+
|
228
|
+
@dataclass
|
229
|
+
class EngineFinished(EngineEvent):
|
230
|
+
"""The final event of the run.
|
231
|
+
|
232
|
+
No more events after this point.
|
233
|
+
"""
|
234
|
+
|
235
|
+
is_terminal = True
|
236
|
+
running_time: float
|
237
|
+
|
238
|
+
__slots__ = ("id", "timestamp", "running_time")
|
239
|
+
|
240
|
+
def __init__(self, *, running_time: float) -> None:
|
241
|
+
self.id = uuid.uuid4()
|
242
|
+
self.timestamp = time.time()
|
243
|
+
self.running_time = running_time
|
@@ -0,0 +1,66 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import enum
|
4
|
+
import warnings
|
5
|
+
from dataclasses import dataclass
|
6
|
+
from typing import TYPE_CHECKING
|
7
|
+
|
8
|
+
if TYPE_CHECKING:
|
9
|
+
from schemathesis.engine.context import EngineContext
|
10
|
+
from schemathesis.engine.events import EventGenerator
|
11
|
+
|
12
|
+
|
13
|
+
class PhaseName(enum.Enum):
|
14
|
+
"""Available execution phases."""
|
15
|
+
|
16
|
+
PROBING = "API probing"
|
17
|
+
UNIT_TESTING = "Unit testing"
|
18
|
+
STATEFUL_TESTING = "Stateful testing"
|
19
|
+
|
20
|
+
@classmethod
|
21
|
+
def from_str(cls, value: str) -> PhaseName:
|
22
|
+
return {
|
23
|
+
"probing": cls.PROBING,
|
24
|
+
"unit": cls.UNIT_TESTING,
|
25
|
+
"stateful": cls.STATEFUL_TESTING,
|
26
|
+
}[value.lower()]
|
27
|
+
|
28
|
+
|
29
|
+
class PhaseSkipReason(str, enum.Enum):
|
30
|
+
"""Reasons why a phase might not be executed."""
|
31
|
+
|
32
|
+
DISABLED = "disabled" # Explicitly disabled via config
|
33
|
+
NOT_SUPPORTED = "not supported" # Feature not supported by schema
|
34
|
+
NOT_APPLICABLE = "not applicable" # No relevant data (e.g., no links for stateful)
|
35
|
+
FAILURE_LIMIT_REACHED = "failure limit reached"
|
36
|
+
NOTHING_TO_TEST = "nothing to test"
|
37
|
+
|
38
|
+
|
39
|
+
@dataclass
|
40
|
+
class Phase:
|
41
|
+
"""A logically separate engine execution phase."""
|
42
|
+
|
43
|
+
name: PhaseName
|
44
|
+
is_supported: bool
|
45
|
+
is_enabled: bool = True
|
46
|
+
skip_reason: PhaseSkipReason | None = None
|
47
|
+
|
48
|
+
def should_execute(self, ctx: EngineContext) -> bool:
|
49
|
+
"""Determine if phase should run based on context & configuration."""
|
50
|
+
return self.is_enabled and not ctx.has_to_stop
|
51
|
+
|
52
|
+
|
53
|
+
def execute(ctx: EngineContext, phase: Phase) -> EventGenerator:
|
54
|
+
from urllib3.exceptions import InsecureRequestWarning
|
55
|
+
|
56
|
+
from . import probes, stateful, unit
|
57
|
+
|
58
|
+
with warnings.catch_warnings():
|
59
|
+
warnings.simplefilter("ignore", InsecureRequestWarning)
|
60
|
+
|
61
|
+
if phase.name == PhaseName.PROBING:
|
62
|
+
yield from probes.execute(ctx, phase)
|
63
|
+
elif phase.name == PhaseName.UNIT_TESTING:
|
64
|
+
yield from unit.execute(ctx, phase)
|
65
|
+
elif phase.name == PhaseName.STATEFUL_TESTING:
|
66
|
+
yield from stateful.execute(ctx, phase)
|