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
schemathesis/exceptions.py
DELETED
@@ -1,571 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import enum
|
4
|
-
import re
|
5
|
-
import traceback
|
6
|
-
from dataclasses import dataclass, field
|
7
|
-
from hashlib import sha1
|
8
|
-
from typing import TYPE_CHECKING, Any, Callable, Generator, NoReturn
|
9
|
-
|
10
|
-
from .constants import SERIALIZERS_SUGGESTION_MESSAGE
|
11
|
-
from .internal.output import truncate_json
|
12
|
-
|
13
|
-
if TYPE_CHECKING:
|
14
|
-
from json import JSONDecodeError
|
15
|
-
from types import TracebackType
|
16
|
-
|
17
|
-
import hypothesis.errors
|
18
|
-
from graphql.error import GraphQLFormattedError
|
19
|
-
from jsonschema import RefResolutionError, ValidationError
|
20
|
-
from jsonschema import SchemaError as JsonSchemaError
|
21
|
-
from requests import RequestException
|
22
|
-
|
23
|
-
from .failures import FailureContext
|
24
|
-
from .transports.responses import GenericResponse
|
25
|
-
|
26
|
-
|
27
|
-
class CheckFailed(AssertionError):
|
28
|
-
"""Custom error type to distinguish from arbitrary AssertionError that may happen in the dependent libraries."""
|
29
|
-
|
30
|
-
__module__ = "builtins"
|
31
|
-
context: FailureContext | None
|
32
|
-
causes: tuple[CheckFailed | AssertionError, ...] | None
|
33
|
-
|
34
|
-
def __init__(
|
35
|
-
self,
|
36
|
-
*args: Any,
|
37
|
-
context: FailureContext | None = None,
|
38
|
-
causes: tuple[CheckFailed | AssertionError, ...] | None = None,
|
39
|
-
):
|
40
|
-
super().__init__(*args)
|
41
|
-
self.context = context
|
42
|
-
self.causes = causes
|
43
|
-
|
44
|
-
|
45
|
-
def make_unique_by_key(
|
46
|
-
check_name: str, check_message: str | None, context: FailureContext | None
|
47
|
-
) -> tuple[str | None, ...]:
|
48
|
-
"""A key to distinguish different failed checks.
|
49
|
-
|
50
|
-
It is not only based on `FailureContext`, because the end-user may raise plain `AssertionError` in their custom
|
51
|
-
checks, and those won't have any context attached.
|
52
|
-
"""
|
53
|
-
if context is not None:
|
54
|
-
return context.unique_by_key(check_message)
|
55
|
-
return check_name, check_message
|
56
|
-
|
57
|
-
|
58
|
-
def deduplicate_failed_checks(
|
59
|
-
checks: list[CheckFailed | AssertionError],
|
60
|
-
) -> Generator[CheckFailed | AssertionError, None, None]:
|
61
|
-
"""Keep only unique failed checks."""
|
62
|
-
seen = set()
|
63
|
-
for check in checks:
|
64
|
-
check_message = check.args[0]
|
65
|
-
if isinstance(check, CheckFailed) and check.context is not None:
|
66
|
-
key = check.context.unique_by_key(check_message)
|
67
|
-
else:
|
68
|
-
key = check_message
|
69
|
-
if key not in seen:
|
70
|
-
yield check
|
71
|
-
seen.add(key)
|
72
|
-
|
73
|
-
|
74
|
-
CACHE: dict[str | int, type[CheckFailed]] = {}
|
75
|
-
|
76
|
-
|
77
|
-
def get_exception(name: str) -> type[CheckFailed]:
|
78
|
-
"""Create a new exception class with provided name or fetch one from the cache."""
|
79
|
-
if name in CACHE:
|
80
|
-
exception_class = CACHE[name]
|
81
|
-
else:
|
82
|
-
exception_class = type(name, (CheckFailed,), {})
|
83
|
-
exception_class.__qualname__ = CheckFailed.__name__
|
84
|
-
exception_class.__name__ = CheckFailed.__name__
|
85
|
-
CACHE[name] = exception_class
|
86
|
-
return exception_class
|
87
|
-
|
88
|
-
|
89
|
-
def _get_hashed_exception(prefix: str, message: str) -> type[CheckFailed]:
|
90
|
-
"""Give different exceptions for different error messages."""
|
91
|
-
messages_digest = sha1(message.encode("utf-8")).hexdigest()
|
92
|
-
name = f"{prefix}{messages_digest}"
|
93
|
-
return get_exception(name)
|
94
|
-
|
95
|
-
|
96
|
-
def get_grouped_exception(prefix: str, *exceptions: AssertionError) -> type[CheckFailed]:
|
97
|
-
# The prefix is needed to distinguish multiple operations with the same error messages
|
98
|
-
# that are coming from different operations
|
99
|
-
messages = [exception.args[0] for exception in exceptions]
|
100
|
-
message = "".join(messages)
|
101
|
-
return _get_hashed_exception("GroupedException", f"{prefix}{message}")
|
102
|
-
|
103
|
-
|
104
|
-
def get_server_error(prefix: str, status_code: int) -> type[CheckFailed]:
|
105
|
-
"""Return new exception for the Internal Server Error cases."""
|
106
|
-
name = f"ServerError{prefix}{status_code}"
|
107
|
-
return get_exception(name)
|
108
|
-
|
109
|
-
|
110
|
-
def get_status_code_error(prefix: str, status_code: int) -> type[CheckFailed]:
|
111
|
-
"""Return new exception for an unexpected status code."""
|
112
|
-
name = f"StatusCodeError{prefix}{status_code}"
|
113
|
-
return get_exception(name)
|
114
|
-
|
115
|
-
|
116
|
-
def get_response_type_error(prefix: str, expected: str, received: str) -> type[CheckFailed]:
|
117
|
-
"""Return new exception for an unexpected response type."""
|
118
|
-
name = f"SchemaValidationError{prefix}{expected}_{received}"
|
119
|
-
return get_exception(name)
|
120
|
-
|
121
|
-
|
122
|
-
def get_malformed_media_type_error(prefix: str, media_type: str) -> type[CheckFailed]:
|
123
|
-
name = f"MalformedMediaType{prefix}{media_type}"
|
124
|
-
return get_exception(name)
|
125
|
-
|
126
|
-
|
127
|
-
def get_missing_content_type_error(prefix: str) -> type[CheckFailed]:
|
128
|
-
"""Return new exception for a missing Content-Type header."""
|
129
|
-
return get_exception(f"MissingContentTypeError{prefix}")
|
130
|
-
|
131
|
-
|
132
|
-
def get_schema_validation_error(prefix: str, exception: ValidationError) -> type[CheckFailed]:
|
133
|
-
"""Return new exception for schema validation error."""
|
134
|
-
return _get_hashed_exception(f"SchemaValidationError{prefix}", str(exception))
|
135
|
-
|
136
|
-
|
137
|
-
def get_response_parsing_error(prefix: str, exception: JSONDecodeError) -> type[CheckFailed]:
|
138
|
-
"""Return new exception for response parsing error."""
|
139
|
-
return _get_hashed_exception(f"ResponseParsingError{prefix}", str(exception))
|
140
|
-
|
141
|
-
|
142
|
-
def get_headers_error(prefix: str, message: str) -> type[CheckFailed]:
|
143
|
-
"""Return new exception for missing headers."""
|
144
|
-
return _get_hashed_exception(f"MissingHeadersError{prefix}", message)
|
145
|
-
|
146
|
-
|
147
|
-
def get_negative_rejection_error(prefix: str, status: int) -> type[CheckFailed]:
|
148
|
-
return _get_hashed_exception(f"AcceptedNegativeDataError{prefix}", str(status))
|
149
|
-
|
150
|
-
|
151
|
-
def get_positive_acceptance_error(prefix: str, status: int) -> type[CheckFailed]:
|
152
|
-
return _get_hashed_exception(f"RejectedPositiveDataError{prefix}", str(status))
|
153
|
-
|
154
|
-
|
155
|
-
def get_use_after_free_error(free: str) -> type[CheckFailed]:
|
156
|
-
return _get_hashed_exception("UseAfterFreeError", free)
|
157
|
-
|
158
|
-
|
159
|
-
def get_ensure_resource_availability_error(operation: str) -> type[CheckFailed]:
|
160
|
-
return _get_hashed_exception("EnsureResourceAvailabilityError", operation)
|
161
|
-
|
162
|
-
|
163
|
-
def get_ignored_auth_error(operation: str) -> type[CheckFailed]:
|
164
|
-
return _get_hashed_exception("IgnoredAuthError", operation)
|
165
|
-
|
166
|
-
|
167
|
-
def get_timeout_error(prefix: str, deadline: float | int) -> type[CheckFailed]:
|
168
|
-
"""Request took too long."""
|
169
|
-
return _get_hashed_exception(f"TimeoutError{prefix}", str(deadline))
|
170
|
-
|
171
|
-
|
172
|
-
def get_unexpected_graphql_response_error(type_: type) -> type[CheckFailed]:
|
173
|
-
"""When GraphQL response is not a JSON object."""
|
174
|
-
return get_exception(f"UnexpectedGraphQLResponseError:{type_}")
|
175
|
-
|
176
|
-
|
177
|
-
def get_grouped_graphql_error(errors: list[GraphQLFormattedError]) -> type[CheckFailed]:
|
178
|
-
# Canonicalize GraphQL errors by serializing them uniformly and sorting the outcomes
|
179
|
-
entries = []
|
180
|
-
for error in errors:
|
181
|
-
message = error["message"]
|
182
|
-
if "locations" in error:
|
183
|
-
message += ";locations:"
|
184
|
-
for location in sorted(error["locations"]):
|
185
|
-
message += f"({location['line'], location['column']})"
|
186
|
-
if "path" in error:
|
187
|
-
message += ";path:"
|
188
|
-
for chunk in error["path"]:
|
189
|
-
message += str(chunk)
|
190
|
-
entries.append(message)
|
191
|
-
entries.sort()
|
192
|
-
return _get_hashed_exception("GraphQLErrors", "".join(entries))
|
193
|
-
|
194
|
-
|
195
|
-
SCHEMA_ERROR_SUGGESTION = "Ensure that the definition complies with the OpenAPI specification"
|
196
|
-
|
197
|
-
|
198
|
-
@dataclass
|
199
|
-
class OperationSchemaError(Exception):
|
200
|
-
"""Schema associated with an API operation contains an error."""
|
201
|
-
|
202
|
-
__module__ = "builtins"
|
203
|
-
message: str | None = None
|
204
|
-
path: str | None = None
|
205
|
-
method: str | None = None
|
206
|
-
full_path: str | None = None
|
207
|
-
|
208
|
-
@classmethod
|
209
|
-
def from_jsonschema_error(
|
210
|
-
cls, error: ValidationError, path: str | None, method: str | None, full_path: str | None
|
211
|
-
) -> OperationSchemaError:
|
212
|
-
if error.absolute_path:
|
213
|
-
part = error.absolute_path[-1]
|
214
|
-
if isinstance(part, int) and len(error.absolute_path) > 1:
|
215
|
-
parent = error.absolute_path[-2]
|
216
|
-
message = f"Invalid definition for element at index {part} in `{parent}`"
|
217
|
-
else:
|
218
|
-
message = f"Invalid `{part}` definition"
|
219
|
-
else:
|
220
|
-
message = "Invalid schema definition"
|
221
|
-
error_path = " -> ".join(str(entry) for entry in error.path) or "[root]"
|
222
|
-
message += f"\n\nLocation:\n {error_path}"
|
223
|
-
instance = truncate_json(error.instance)
|
224
|
-
message += f"\n\nProblematic definition:\n{instance}"
|
225
|
-
message += "\n\nError details:\n "
|
226
|
-
# This default message contains the instance which we already printed
|
227
|
-
if "is not valid under any of the given schemas" in error.message:
|
228
|
-
message += "The provided definition doesn't match any of the expected formats or types."
|
229
|
-
else:
|
230
|
-
message += error.message
|
231
|
-
message += f"\n\n{SCHEMA_ERROR_SUGGESTION}"
|
232
|
-
return cls(message, path=path, method=method, full_path=full_path)
|
233
|
-
|
234
|
-
@classmethod
|
235
|
-
def from_reference_resolution_error(
|
236
|
-
cls, error: RefResolutionError, path: str | None, method: str | None, full_path: str | None
|
237
|
-
) -> OperationSchemaError:
|
238
|
-
notes = getattr(error, "__notes__", [])
|
239
|
-
# Some exceptions don't have the actual reference in them, hence we add it manually via notes
|
240
|
-
pointer = f"'{notes[0]}'"
|
241
|
-
message = "Unresolvable JSON pointer in the schema"
|
242
|
-
# Get the pointer value from "Unresolvable JSON pointer: 'components/UnknownParameter'"
|
243
|
-
message += f"\n\nError details:\n JSON pointer: {pointer}"
|
244
|
-
message += "\n This typically means that the schema is referencing a component that doesn't exist."
|
245
|
-
message += f"\n\n{SCHEMA_ERROR_SUGGESTION}"
|
246
|
-
return cls(message, path=path, method=method, full_path=full_path)
|
247
|
-
|
248
|
-
def as_failing_test_function(self) -> Callable:
|
249
|
-
"""Create a test function that will fail.
|
250
|
-
|
251
|
-
This approach allows us to use default pytest reporting style for operation-level schema errors.
|
252
|
-
"""
|
253
|
-
|
254
|
-
def actual_test(*args: Any, **kwargs: Any) -> NoReturn:
|
255
|
-
__tracebackhide__ = True
|
256
|
-
raise self
|
257
|
-
|
258
|
-
return actual_test
|
259
|
-
|
260
|
-
|
261
|
-
@dataclass
|
262
|
-
class BodyInGetRequestError(OperationSchemaError):
|
263
|
-
__module__ = "builtins"
|
264
|
-
|
265
|
-
|
266
|
-
@dataclass
|
267
|
-
class OperationNotFound(KeyError):
|
268
|
-
message: str
|
269
|
-
item: str
|
270
|
-
__module__ = "builtins"
|
271
|
-
|
272
|
-
def __str__(self) -> str:
|
273
|
-
return self.message
|
274
|
-
|
275
|
-
|
276
|
-
@dataclass
|
277
|
-
class InvalidRegularExpression(OperationSchemaError):
|
278
|
-
is_valid_type: bool = True
|
279
|
-
__module__ = "builtins"
|
280
|
-
|
281
|
-
@classmethod
|
282
|
-
def from_hypothesis_jsonschema_message(cls, message: str) -> InvalidRegularExpression:
|
283
|
-
match = re.search(r"pattern='(.*?)'.*?\((.*?)\)", message)
|
284
|
-
if match:
|
285
|
-
message = f"Invalid regular expression. Pattern `{match.group(1)}` is not recognized - `{match.group(2)}`"
|
286
|
-
return cls(message)
|
287
|
-
|
288
|
-
@classmethod
|
289
|
-
def from_schema_error(cls, error: JsonSchemaError, *, from_examples: bool) -> InvalidRegularExpression:
|
290
|
-
if from_examples:
|
291
|
-
message = (
|
292
|
-
"Failed to generate test cases from examples for this API operation because of "
|
293
|
-
f"unsupported regular expression `{error.instance}`"
|
294
|
-
)
|
295
|
-
else:
|
296
|
-
message = (
|
297
|
-
"Failed to generate test cases for this API operation because of "
|
298
|
-
f"unsupported regular expression `{error.instance}`"
|
299
|
-
)
|
300
|
-
return cls(message)
|
301
|
-
|
302
|
-
|
303
|
-
class InvalidHeadersExample(OperationSchemaError):
|
304
|
-
__module__ = "builtins"
|
305
|
-
|
306
|
-
@classmethod
|
307
|
-
def from_headers(cls, headers: dict[str, str]) -> InvalidHeadersExample:
|
308
|
-
message = (
|
309
|
-
"Failed to generate test cases from examples for this API operation because of "
|
310
|
-
"some header examples are invalid:\n"
|
311
|
-
)
|
312
|
-
for key, value in headers.items():
|
313
|
-
message += f"\n - {key!r}={value!r}"
|
314
|
-
message += "\n\nEnsure the header examples comply with RFC 7230, Section 3.2"
|
315
|
-
return cls(message)
|
316
|
-
|
317
|
-
|
318
|
-
class DeadlineExceeded(Exception):
|
319
|
-
"""Test took too long to run."""
|
320
|
-
|
321
|
-
__module__ = "builtins"
|
322
|
-
|
323
|
-
@classmethod
|
324
|
-
def from_exc(cls, exc: hypothesis.errors.DeadlineExceeded) -> DeadlineExceeded:
|
325
|
-
runtime = exc.runtime.total_seconds() * 1000
|
326
|
-
deadline = exc.deadline.total_seconds() * 1000
|
327
|
-
return cls(
|
328
|
-
f"Test running time is too slow! It took {runtime:.2f}ms, which exceeds the deadline of {deadline:.2f}ms.\n"
|
329
|
-
)
|
330
|
-
|
331
|
-
|
332
|
-
class RecursiveReferenceError(Exception):
|
333
|
-
"""Recursive reference is impossible to resolve due to current limitations."""
|
334
|
-
|
335
|
-
__module__ = "builtins"
|
336
|
-
|
337
|
-
|
338
|
-
@enum.unique
|
339
|
-
class RuntimeErrorType(str, enum.Enum):
|
340
|
-
# Connection related issues
|
341
|
-
CONNECTION_SSL = "connection_ssl"
|
342
|
-
CONNECTION_OTHER = "connection_other"
|
343
|
-
NETWORK_OTHER = "network_other"
|
344
|
-
|
345
|
-
# Hypothesis issues
|
346
|
-
HYPOTHESIS_DEADLINE_EXCEEDED = "hypothesis_deadline_exceeded"
|
347
|
-
HYPOTHESIS_UNSATISFIABLE = "hypothesis_unsatisfiable"
|
348
|
-
HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR = "hypothesis_unsupported_graphql_scalar"
|
349
|
-
HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE = "hypothesis_health_check_data_too_large"
|
350
|
-
HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH = "hypothesis_health_check_filter_too_much"
|
351
|
-
HYPOTHESIS_HEALTH_CHECK_TOO_SLOW = "hypothesis_health_check_too_slow"
|
352
|
-
HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE = "hypothesis_health_check_large_base_example"
|
353
|
-
|
354
|
-
SCHEMA_BODY_IN_GET_REQUEST = "schema_body_in_get_request"
|
355
|
-
SCHEMA_INVALID_REGULAR_EXPRESSION = "schema_invalid_regular_expression"
|
356
|
-
SCHEMA_UNSUPPORTED = "schema_unsupported"
|
357
|
-
SCHEMA_GENERIC = "schema_generic"
|
358
|
-
|
359
|
-
SERIALIZATION_NOT_POSSIBLE = "serialization_not_possible"
|
360
|
-
SERIALIZATION_UNBOUNDED_PREFIX = "serialization_unbounded_prefix"
|
361
|
-
|
362
|
-
# Unclassified
|
363
|
-
UNCLASSIFIED = "unclassified"
|
364
|
-
|
365
|
-
@property
|
366
|
-
def has_useful_traceback(self) -> bool:
|
367
|
-
return self not in (
|
368
|
-
RuntimeErrorType.SCHEMA_BODY_IN_GET_REQUEST,
|
369
|
-
RuntimeErrorType.SCHEMA_INVALID_REGULAR_EXPRESSION,
|
370
|
-
RuntimeErrorType.SCHEMA_UNSUPPORTED,
|
371
|
-
RuntimeErrorType.SCHEMA_GENERIC,
|
372
|
-
RuntimeErrorType.SERIALIZATION_NOT_POSSIBLE,
|
373
|
-
)
|
374
|
-
|
375
|
-
|
376
|
-
@enum.unique
|
377
|
-
class SchemaErrorType(str, enum.Enum):
|
378
|
-
# Connection related issues
|
379
|
-
CONNECTION_SSL = "connection_ssl"
|
380
|
-
CONNECTION_OTHER = "connection_other"
|
381
|
-
NETWORK_OTHER = "network_other"
|
382
|
-
|
383
|
-
# HTTP error codes
|
384
|
-
HTTP_SERVER_ERROR = "http_server_error"
|
385
|
-
HTTP_CLIENT_ERROR = "http_client_error"
|
386
|
-
HTTP_NOT_FOUND = "http_not_found"
|
387
|
-
HTTP_FORBIDDEN = "http_forbidden"
|
388
|
-
|
389
|
-
# Content decoding issues
|
390
|
-
SYNTAX_ERROR = "syntax_error"
|
391
|
-
UNEXPECTED_CONTENT_TYPE = "unexpected_content_type"
|
392
|
-
YAML_NUMERIC_STATUS_CODES = "yaml_numeric_status_codes"
|
393
|
-
YAML_NON_STRING_KEYS = "yaml_non_string_keys"
|
394
|
-
|
395
|
-
# Open API validation
|
396
|
-
OPEN_API_INVALID_SCHEMA = "open_api_invalid_schema"
|
397
|
-
OPEN_API_UNSPECIFIED_VERSION = "open_api_unspecified_version"
|
398
|
-
OPEN_API_UNSUPPORTED_VERSION = "open_api_unsupported_version"
|
399
|
-
OPEN_API_EXPERIMENTAL_VERSION = "open_api_experimental_version"
|
400
|
-
|
401
|
-
# GraphQL validation
|
402
|
-
GRAPHQL_INVALID_SCHEMA = "graphql_invalid_schema"
|
403
|
-
|
404
|
-
# Unclassified
|
405
|
-
UNCLASSIFIED = "unclassified"
|
406
|
-
|
407
|
-
|
408
|
-
@dataclass
|
409
|
-
class SchemaError(RuntimeError):
|
410
|
-
"""Failed to load an API schema."""
|
411
|
-
|
412
|
-
type: SchemaErrorType
|
413
|
-
message: str
|
414
|
-
url: str | None = None
|
415
|
-
response: GenericResponse | None = None
|
416
|
-
extras: list[str] = field(default_factory=list)
|
417
|
-
|
418
|
-
def __str__(self) -> str:
|
419
|
-
return self.message
|
420
|
-
|
421
|
-
|
422
|
-
class NonCheckError(Exception):
|
423
|
-
"""An error happened in side the runner, but is not related to failed checks.
|
424
|
-
|
425
|
-
Used primarily to not let Hypothesis consider the test as flaky or detect multiple failures as we handle it
|
426
|
-
on our side.
|
427
|
-
"""
|
428
|
-
|
429
|
-
__module__ = "builtins"
|
430
|
-
|
431
|
-
|
432
|
-
class InternalError(Exception):
|
433
|
-
"""Internal error in Schemathesis."""
|
434
|
-
|
435
|
-
__module__ = "builtins"
|
436
|
-
|
437
|
-
|
438
|
-
class SkipTest(BaseException):
|
439
|
-
"""Raises when a test should be skipped and return control to the execution engine (own Schemathesis' or pytest)."""
|
440
|
-
|
441
|
-
__module__ = "builtins"
|
442
|
-
|
443
|
-
|
444
|
-
SERIALIZATION_NOT_POSSIBLE_MESSAGE = (
|
445
|
-
f"Schemathesis can't serialize data to any of the defined media types: {{}} \n{SERIALIZERS_SUGGESTION_MESSAGE}"
|
446
|
-
)
|
447
|
-
NAMESPACE_DEFINITION_URL = "https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#xmlNamespace"
|
448
|
-
UNBOUND_PREFIX_MESSAGE_TEMPLATE = (
|
449
|
-
"Unbound prefix: `{prefix}`. "
|
450
|
-
"You need to define this namespace in your API schema via the `xml.namespace` keyword. "
|
451
|
-
f"See more at {NAMESPACE_DEFINITION_URL}"
|
452
|
-
)
|
453
|
-
|
454
|
-
SERIALIZATION_FOR_TYPE_IS_NOT_POSSIBLE_MESSAGE = (
|
455
|
-
f"Schemathesis can't serialize data to {{}} \n{SERIALIZERS_SUGGESTION_MESSAGE}"
|
456
|
-
)
|
457
|
-
|
458
|
-
|
459
|
-
class SerializationError(Exception):
|
460
|
-
"""Serialization can not be done."""
|
461
|
-
|
462
|
-
__module__ = "builtins"
|
463
|
-
|
464
|
-
|
465
|
-
class UnboundPrefixError(SerializationError):
|
466
|
-
"""XML serialization error.
|
467
|
-
|
468
|
-
It happens when the schema does not define a namespace that is used by some of its parts.
|
469
|
-
"""
|
470
|
-
|
471
|
-
def __init__(self, prefix: str):
|
472
|
-
super().__init__(UNBOUND_PREFIX_MESSAGE_TEMPLATE.format(prefix=prefix))
|
473
|
-
|
474
|
-
|
475
|
-
@dataclass
|
476
|
-
class SerializationNotPossible(SerializationError):
|
477
|
-
"""Not possible to serialize to any of the media types defined for some API operation.
|
478
|
-
|
479
|
-
Usually, there is still `application/json` along with less common ones, but this error happens when there is no
|
480
|
-
media type that Schemathesis knows how to serialize data to.
|
481
|
-
"""
|
482
|
-
|
483
|
-
message: str
|
484
|
-
media_types: list[str]
|
485
|
-
|
486
|
-
__module__ = "builtins"
|
487
|
-
|
488
|
-
def __str__(self) -> str:
|
489
|
-
return self.message
|
490
|
-
|
491
|
-
@classmethod
|
492
|
-
def from_media_types(cls, *media_types: str) -> SerializationNotPossible:
|
493
|
-
return cls(SERIALIZATION_NOT_POSSIBLE_MESSAGE.format(", ".join(media_types)), media_types=list(media_types))
|
494
|
-
|
495
|
-
@classmethod
|
496
|
-
def for_media_type(cls, media_type: str) -> SerializationNotPossible:
|
497
|
-
return cls(SERIALIZATION_FOR_TYPE_IS_NOT_POSSIBLE_MESSAGE.format(media_type), media_types=[media_type])
|
498
|
-
|
499
|
-
|
500
|
-
class UsageError(Exception):
|
501
|
-
"""Incorrect usage of Schemathesis functions."""
|
502
|
-
|
503
|
-
|
504
|
-
def maybe_set_assertion_message(exc: AssertionError, check_name: str) -> str:
|
505
|
-
message = str(exc)
|
506
|
-
title = f"Custom check failed: `{check_name}`"
|
507
|
-
if not message:
|
508
|
-
exc.args = (title, None)
|
509
|
-
else:
|
510
|
-
exc.args = (title, message)
|
511
|
-
return message
|
512
|
-
|
513
|
-
|
514
|
-
def format_exception(error: Exception, include_traceback: bool = False) -> str:
|
515
|
-
"""Format exception as text."""
|
516
|
-
error_type = type(error)
|
517
|
-
if include_traceback:
|
518
|
-
lines = traceback.format_exception(error_type, error, error.__traceback__)
|
519
|
-
else:
|
520
|
-
lines = traceback.format_exception_only(error_type, error)
|
521
|
-
return "".join(lines).strip()
|
522
|
-
|
523
|
-
|
524
|
-
def extract_nth_traceback(trace: TracebackType | None, n: int) -> TracebackType | None:
|
525
|
-
depth = 0
|
526
|
-
while depth < n and trace is not None:
|
527
|
-
trace = trace.tb_next
|
528
|
-
depth += 1
|
529
|
-
return trace
|
530
|
-
|
531
|
-
|
532
|
-
def remove_ssl_line_number(text: str) -> str:
|
533
|
-
return re.sub(r"\(_ssl\.c:\d+\)", "", text)
|
534
|
-
|
535
|
-
|
536
|
-
def _clean_inner_request_message(message: Any) -> str:
|
537
|
-
if isinstance(message, str) and message.startswith("HTTPConnectionPool"):
|
538
|
-
return re.sub(r"HTTPConnectionPool\(.+?\): ", "", message).rstrip(".")
|
539
|
-
return str(message)
|
540
|
-
|
541
|
-
|
542
|
-
def extract_requests_exception_details(exc: RequestException) -> tuple[str, list[str]]:
|
543
|
-
from requests.exceptions import ChunkedEncodingError, ConnectionError, SSLError
|
544
|
-
from urllib3.exceptions import MaxRetryError
|
545
|
-
|
546
|
-
if isinstance(exc, SSLError):
|
547
|
-
message = "SSL verification problem"
|
548
|
-
reason = str(exc.args[0].reason)
|
549
|
-
extra = [remove_ssl_line_number(reason).strip()]
|
550
|
-
elif isinstance(exc, ConnectionError):
|
551
|
-
message = "Connection failed"
|
552
|
-
inner = exc.args[0]
|
553
|
-
if isinstance(inner, MaxRetryError) and inner.reason is not None:
|
554
|
-
arg = inner.reason.args[0]
|
555
|
-
if isinstance(arg, str):
|
556
|
-
if ":" not in arg:
|
557
|
-
reason = arg
|
558
|
-
else:
|
559
|
-
_, reason = arg.split(":", maxsplit=1)
|
560
|
-
else:
|
561
|
-
reason = f"Max retries exceeded with url: {inner.url}"
|
562
|
-
extra = [reason.strip()]
|
563
|
-
else:
|
564
|
-
extra = [" ".join(map(_clean_inner_request_message, inner.args))]
|
565
|
-
elif isinstance(exc, ChunkedEncodingError):
|
566
|
-
message = "Connection broken. The server declared chunked encoding but sent an invalid chunk"
|
567
|
-
extra = [str(exc.args[0].args[1])]
|
568
|
-
else:
|
569
|
-
message = str(exc)
|
570
|
-
extra = []
|
571
|
-
return message, extra
|
schemathesis/extra/_aiohttp.py
DELETED
@@ -1,28 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import asyncio
|
4
|
-
|
5
|
-
from aiohttp import web
|
6
|
-
|
7
|
-
from . import _server
|
8
|
-
|
9
|
-
|
10
|
-
def _run_server(app: web.Application, port: int) -> None:
|
11
|
-
"""Run the given app on the given port.
|
12
|
-
|
13
|
-
Intended to be called as a target for a separate thread.
|
14
|
-
NOTE. `aiohttp.web.run_app` works only in the main thread and can't be used here (or maybe can we some tuning)
|
15
|
-
"""
|
16
|
-
# Set a loop for a new thread (there is no by default for non-main threads)
|
17
|
-
loop = asyncio.new_event_loop()
|
18
|
-
asyncio.set_event_loop(loop)
|
19
|
-
runner = web.AppRunner(app)
|
20
|
-
loop.run_until_complete(runner.setup())
|
21
|
-
site = web.TCPSite(runner, "127.0.0.1", port)
|
22
|
-
loop.run_until_complete(site.start())
|
23
|
-
loop.run_forever()
|
24
|
-
|
25
|
-
|
26
|
-
def run_server(app: web.Application, port: int | None = None, timeout: float = 0.05) -> int:
|
27
|
-
"""Start a thread with the given aiohttp application."""
|
28
|
-
return _server.run(_run_server, app=app, port=port, timeout=timeout)
|
schemathesis/extra/_flask.py
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from typing import TYPE_CHECKING
|
4
|
-
|
5
|
-
from . import _server
|
6
|
-
|
7
|
-
if TYPE_CHECKING:
|
8
|
-
from flask import Flask
|
9
|
-
|
10
|
-
|
11
|
-
def run_server(app: Flask, port: int | None = None, timeout: float = 0.05) -> int:
|
12
|
-
"""Start a thread with the given aiohttp application."""
|
13
|
-
return _server.run(app.run, port=port, timeout=timeout)
|
schemathesis/extra/_server.py
DELETED
@@ -1,18 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import threading
|
4
|
-
from time import sleep
|
5
|
-
from typing import Any, Callable
|
6
|
-
|
7
|
-
from aiohttp.test_utils import unused_port
|
8
|
-
|
9
|
-
|
10
|
-
def run(target: Callable, port: int | None = None, timeout: float = 0.05, **kwargs: Any) -> int:
|
11
|
-
"""Start a thread with the given aiohttp application."""
|
12
|
-
if port is None:
|
13
|
-
port = unused_port()
|
14
|
-
server_thread = threading.Thread(target=target, kwargs={"port": port, **kwargs})
|
15
|
-
server_thread.daemon = True
|
16
|
-
server_thread.start()
|
17
|
-
sleep(timeout)
|
18
|
-
return port
|