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
@@ -1,544 +0,0 @@
|
|
1
|
-
"""Transformation from Schemathesis-specific data structures to ones that can be serialized and sent over network.
|
2
|
-
|
3
|
-
They all consist of primitive types and don't have references to schemas, app, etc.
|
4
|
-
"""
|
5
|
-
|
6
|
-
from __future__ import annotations
|
7
|
-
|
8
|
-
import logging
|
9
|
-
import re
|
10
|
-
import textwrap
|
11
|
-
from dataclasses import asdict, dataclass, field
|
12
|
-
from typing import TYPE_CHECKING, Any, cast
|
13
|
-
|
14
|
-
from ..code_samples import get_excluded_headers
|
15
|
-
from ..exceptions import (
|
16
|
-
BodyInGetRequestError,
|
17
|
-
DeadlineExceeded,
|
18
|
-
InternalError,
|
19
|
-
InvalidRegularExpression,
|
20
|
-
OperationSchemaError,
|
21
|
-
RecursiveReferenceError,
|
22
|
-
RuntimeErrorType,
|
23
|
-
SerializationError,
|
24
|
-
UnboundPrefixError,
|
25
|
-
extract_requests_exception_details,
|
26
|
-
format_exception,
|
27
|
-
make_unique_by_key,
|
28
|
-
)
|
29
|
-
from ..models import Case, Check, Interaction, Request, Response, Status, TestPhase, TestResult, TransitionId
|
30
|
-
from ..transports import deserialize_payload, serialize_payload
|
31
|
-
|
32
|
-
if TYPE_CHECKING:
|
33
|
-
import hypothesis.errors
|
34
|
-
from requests.structures import CaseInsensitiveDict
|
35
|
-
|
36
|
-
from ..failures import FailureContext
|
37
|
-
from ..generation import DataGenerationMethod
|
38
|
-
|
39
|
-
|
40
|
-
@dataclass
|
41
|
-
class SerializedCase:
|
42
|
-
# Case data
|
43
|
-
id: str
|
44
|
-
generation_time: float
|
45
|
-
path_parameters: dict[str, Any] | None
|
46
|
-
headers: dict[str, Any] | None
|
47
|
-
cookies: dict[str, Any] | None
|
48
|
-
query: dict[str, Any] | None
|
49
|
-
body: str | None
|
50
|
-
media_type: str | None
|
51
|
-
data_generation_method: str | None
|
52
|
-
# Operation data
|
53
|
-
method: str
|
54
|
-
url: str
|
55
|
-
path_template: str
|
56
|
-
full_path: str
|
57
|
-
verbose_name: str
|
58
|
-
transition_id: TransitionId | None
|
59
|
-
# Transport info
|
60
|
-
verify: bool
|
61
|
-
# Headers coming from sources outside data generation
|
62
|
-
extra_headers: dict[str, Any]
|
63
|
-
|
64
|
-
@classmethod
|
65
|
-
def from_case(cls, case: Case, headers: dict[str, Any] | None, verify: bool) -> SerializedCase:
|
66
|
-
# `headers` include not only explicitly provided headers but also ones added by hooks, custom auth, etc.
|
67
|
-
request_data = case.prepare_code_sample_data(headers)
|
68
|
-
serialized_body = _serialize_body(request_data.body)
|
69
|
-
return cls(
|
70
|
-
id=case.id,
|
71
|
-
generation_time=case.generation_time,
|
72
|
-
path_parameters=case.path_parameters,
|
73
|
-
headers=dict(case.headers) if case.headers is not None else None,
|
74
|
-
cookies=case.cookies,
|
75
|
-
query=case.query,
|
76
|
-
body=serialized_body,
|
77
|
-
media_type=case.media_type,
|
78
|
-
data_generation_method=case.data_generation_method.as_short_name()
|
79
|
-
if case.data_generation_method is not None
|
80
|
-
else None,
|
81
|
-
method=case.method,
|
82
|
-
url=request_data.url,
|
83
|
-
path_template=case.path,
|
84
|
-
full_path=case.full_path,
|
85
|
-
verbose_name=case.operation.verbose_name,
|
86
|
-
transition_id=case.source.transition_id if case.source is not None else None,
|
87
|
-
verify=verify,
|
88
|
-
extra_headers=request_data.headers,
|
89
|
-
)
|
90
|
-
|
91
|
-
def deserialize_body(self) -> bytes | None:
|
92
|
-
"""Deserialize the test case body.
|
93
|
-
|
94
|
-
`SerializedCase` should be serializable to JSON, therefore body is encoded as base64 string
|
95
|
-
to support arbitrary binary data.
|
96
|
-
"""
|
97
|
-
return deserialize_payload(self.body)
|
98
|
-
|
99
|
-
|
100
|
-
def _serialize_body(body: str | bytes | None) -> str | None:
|
101
|
-
if body is None:
|
102
|
-
return None
|
103
|
-
if isinstance(body, str):
|
104
|
-
body = body.encode("utf-8")
|
105
|
-
return serialize_payload(body)
|
106
|
-
|
107
|
-
|
108
|
-
@dataclass
|
109
|
-
class SerializedCheck:
|
110
|
-
# Check name
|
111
|
-
name: str
|
112
|
-
# Check result
|
113
|
-
value: Status
|
114
|
-
request: Request
|
115
|
-
response: Response | None
|
116
|
-
# Generated example
|
117
|
-
example: SerializedCase
|
118
|
-
# Message could be absent for plain `assert` statements
|
119
|
-
message: str | None = None
|
120
|
-
# Failure-specific context
|
121
|
-
context: FailureContext | None = None
|
122
|
-
# Cases & responses that were made before this one
|
123
|
-
history: list[SerializedHistoryEntry] = field(default_factory=list)
|
124
|
-
|
125
|
-
@classmethod
|
126
|
-
def from_check(cls, check: Check) -> SerializedCheck:
|
127
|
-
import requests
|
128
|
-
|
129
|
-
from ..transports.responses import WSGIResponse
|
130
|
-
|
131
|
-
if check.response is not None:
|
132
|
-
request = Request.from_prepared_request(check.response.request)
|
133
|
-
elif check.request is not None:
|
134
|
-
# Response is not available, but it is not an error (only time-out behaves this way at the moment)
|
135
|
-
request = Request.from_prepared_request(check.request)
|
136
|
-
else:
|
137
|
-
raise InternalError("Can not find request data")
|
138
|
-
|
139
|
-
response: Response | None
|
140
|
-
if isinstance(check.response, requests.Response):
|
141
|
-
response = Response.from_requests(check.response)
|
142
|
-
elif isinstance(check.response, WSGIResponse):
|
143
|
-
response = Response.from_wsgi(check.response, check.elapsed)
|
144
|
-
else:
|
145
|
-
response = None
|
146
|
-
headers = _get_headers(request.headers)
|
147
|
-
history = get_serialized_history(check.example)
|
148
|
-
return cls(
|
149
|
-
name=check.name,
|
150
|
-
value=check.value,
|
151
|
-
example=SerializedCase.from_case(
|
152
|
-
check.example, headers, verify=response.verify if response is not None else True
|
153
|
-
),
|
154
|
-
message=check.message,
|
155
|
-
request=request,
|
156
|
-
response=response,
|
157
|
-
context=check.context,
|
158
|
-
history=history,
|
159
|
-
)
|
160
|
-
|
161
|
-
@property
|
162
|
-
def title(self) -> str:
|
163
|
-
if self.context is not None:
|
164
|
-
return self.context.title
|
165
|
-
return f"Custom check failed: `{self.name}`"
|
166
|
-
|
167
|
-
@property
|
168
|
-
def formatted_message(self) -> str | None:
|
169
|
-
if self.context is not None:
|
170
|
-
if self.context.message:
|
171
|
-
message = self.context.message
|
172
|
-
else:
|
173
|
-
message = None
|
174
|
-
else:
|
175
|
-
message = self.message
|
176
|
-
if message is not None:
|
177
|
-
message = textwrap.indent(message, prefix=" ")
|
178
|
-
return message
|
179
|
-
|
180
|
-
|
181
|
-
def _get_headers(headers: dict[str, Any] | CaseInsensitiveDict) -> dict[str, str]:
|
182
|
-
return {
|
183
|
-
key: value[0] if isinstance(value, list) else value
|
184
|
-
for key, value in headers.items()
|
185
|
-
if key not in get_excluded_headers()
|
186
|
-
}
|
187
|
-
|
188
|
-
|
189
|
-
@dataclass
|
190
|
-
class SerializedHistoryEntry:
|
191
|
-
case: SerializedCase
|
192
|
-
response: Response
|
193
|
-
|
194
|
-
|
195
|
-
def get_serialized_history(case: Case) -> list[SerializedHistoryEntry]:
|
196
|
-
import requests
|
197
|
-
|
198
|
-
history = []
|
199
|
-
while case.source is not None:
|
200
|
-
history_request = case.source.response.request
|
201
|
-
headers = _get_headers(history_request.headers)
|
202
|
-
if isinstance(case.source.response, requests.Response):
|
203
|
-
history_response = Response.from_requests(case.source.response)
|
204
|
-
verify = history_response.verify
|
205
|
-
else:
|
206
|
-
history_response = Response.from_wsgi(case.source.response, case.source.elapsed)
|
207
|
-
verify = True
|
208
|
-
entry = SerializedHistoryEntry(
|
209
|
-
case=SerializedCase.from_case(case.source.case, headers, verify=verify), response=history_response
|
210
|
-
)
|
211
|
-
history.append(entry)
|
212
|
-
case = case.source.case
|
213
|
-
return history
|
214
|
-
|
215
|
-
|
216
|
-
@dataclass
|
217
|
-
class SerializedError:
|
218
|
-
type: RuntimeErrorType
|
219
|
-
title: str | None
|
220
|
-
message: str | None
|
221
|
-
extras: list[str]
|
222
|
-
|
223
|
-
# Exception info
|
224
|
-
exception: str
|
225
|
-
exception_with_traceback: str
|
226
|
-
|
227
|
-
@classmethod
|
228
|
-
def with_exception(
|
229
|
-
cls,
|
230
|
-
type_: RuntimeErrorType,
|
231
|
-
title: str | None,
|
232
|
-
message: str | None,
|
233
|
-
extras: list[str],
|
234
|
-
exception: Exception,
|
235
|
-
) -> SerializedError:
|
236
|
-
return cls(
|
237
|
-
type=type_,
|
238
|
-
title=title,
|
239
|
-
message=message,
|
240
|
-
extras=extras,
|
241
|
-
exception=format_exception(exception),
|
242
|
-
exception_with_traceback=format_exception(exception, True),
|
243
|
-
)
|
244
|
-
|
245
|
-
@classmethod
|
246
|
-
def from_exception(cls, exception: Exception) -> SerializedError:
|
247
|
-
import hypothesis.errors
|
248
|
-
import requests
|
249
|
-
from hypothesis import HealthCheck
|
250
|
-
|
251
|
-
title = "Runtime Error"
|
252
|
-
message: str | None
|
253
|
-
if isinstance(exception, requests.RequestException):
|
254
|
-
if isinstance(exception, requests.exceptions.SSLError):
|
255
|
-
type_ = RuntimeErrorType.CONNECTION_SSL
|
256
|
-
elif isinstance(exception, requests.exceptions.ConnectionError):
|
257
|
-
type_ = RuntimeErrorType.CONNECTION_OTHER
|
258
|
-
else:
|
259
|
-
type_ = RuntimeErrorType.NETWORK_OTHER
|
260
|
-
message, extras = extract_requests_exception_details(exception)
|
261
|
-
title = "Network Error"
|
262
|
-
elif isinstance(exception, DeadlineExceeded):
|
263
|
-
type_ = RuntimeErrorType.HYPOTHESIS_DEADLINE_EXCEEDED
|
264
|
-
message = str(exception).strip()
|
265
|
-
extras = []
|
266
|
-
elif isinstance(exception, RecursiveReferenceError):
|
267
|
-
type_ = RuntimeErrorType.SCHEMA_UNSUPPORTED
|
268
|
-
message = str(exception).strip()
|
269
|
-
extras = []
|
270
|
-
title = "Unsupported Schema"
|
271
|
-
elif isinstance(exception, hypothesis.errors.InvalidArgument) and str(exception).startswith("Scalar "):
|
272
|
-
# Comes from `hypothesis-graphql`
|
273
|
-
scalar_name = _scalar_name_from_error(exception)
|
274
|
-
type_ = RuntimeErrorType.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR
|
275
|
-
message = f"Scalar type '{scalar_name}' is not recognized"
|
276
|
-
extras = []
|
277
|
-
title = "Unknown GraphQL Scalar"
|
278
|
-
elif (
|
279
|
-
isinstance(exception, hypothesis.errors.InvalidArgument)
|
280
|
-
and str(exception).endswith("larger than Hypothesis is designed to handle")
|
281
|
-
or "can never generate an example, because min_size is larger than Hypothesis supports" in str(exception)
|
282
|
-
):
|
283
|
-
type_ = RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE
|
284
|
-
message = HEALTH_CHECK_MESSAGE_LARGE_BASE_EXAMPLE
|
285
|
-
extras = []
|
286
|
-
title = "Failed Health Check"
|
287
|
-
elif isinstance(exception, hypothesis.errors.Unsatisfiable):
|
288
|
-
type_ = RuntimeErrorType.HYPOTHESIS_UNSATISFIABLE
|
289
|
-
message = f"{exception}. Possible reasons:"
|
290
|
-
extras = [
|
291
|
-
"- Contradictory schema constraints, such as a minimum value exceeding the maximum.",
|
292
|
-
"- Invalid schema definitions for headers or cookies, for example allowing for non-ASCII characters.",
|
293
|
-
"- Excessive schema complexity, which hinders parameter generation.",
|
294
|
-
]
|
295
|
-
title = "Schema Error"
|
296
|
-
elif isinstance(exception, hypothesis.errors.FailedHealthCheck):
|
297
|
-
health_check = _health_check_from_error(exception)
|
298
|
-
if health_check is not None:
|
299
|
-
message, type_ = {
|
300
|
-
HealthCheck.data_too_large: (
|
301
|
-
HEALTH_CHECK_MESSAGE_DATA_TOO_LARGE,
|
302
|
-
RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE,
|
303
|
-
),
|
304
|
-
HealthCheck.filter_too_much: (
|
305
|
-
HEALTH_CHECK_MESSAGE_FILTER_TOO_MUCH,
|
306
|
-
RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH,
|
307
|
-
),
|
308
|
-
HealthCheck.too_slow: (
|
309
|
-
HEALTH_CHECK_MESSAGE_TOO_SLOW,
|
310
|
-
RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW,
|
311
|
-
),
|
312
|
-
HealthCheck.large_base_example: (
|
313
|
-
HEALTH_CHECK_MESSAGE_LARGE_BASE_EXAMPLE,
|
314
|
-
RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE,
|
315
|
-
),
|
316
|
-
}[health_check]
|
317
|
-
else:
|
318
|
-
type_ = RuntimeErrorType.UNCLASSIFIED
|
319
|
-
message = str(exception)
|
320
|
-
extras = []
|
321
|
-
title = "Failed Health Check"
|
322
|
-
elif isinstance(exception, OperationSchemaError):
|
323
|
-
if isinstance(exception, BodyInGetRequestError):
|
324
|
-
type_ = RuntimeErrorType.SCHEMA_BODY_IN_GET_REQUEST
|
325
|
-
elif isinstance(exception, InvalidRegularExpression) and exception.is_valid_type:
|
326
|
-
type_ = RuntimeErrorType.SCHEMA_INVALID_REGULAR_EXPRESSION
|
327
|
-
else:
|
328
|
-
type_ = RuntimeErrorType.SCHEMA_GENERIC
|
329
|
-
message = exception.message
|
330
|
-
extras = []
|
331
|
-
title = "Schema Error"
|
332
|
-
elif isinstance(exception, SerializationError):
|
333
|
-
if isinstance(exception, UnboundPrefixError):
|
334
|
-
type_ = RuntimeErrorType.SERIALIZATION_UNBOUNDED_PREFIX
|
335
|
-
title = "XML serialization error"
|
336
|
-
else:
|
337
|
-
title = "Serialization not possible"
|
338
|
-
type_ = RuntimeErrorType.SERIALIZATION_NOT_POSSIBLE
|
339
|
-
message = str(exception)
|
340
|
-
extras = []
|
341
|
-
else:
|
342
|
-
type_ = RuntimeErrorType.UNCLASSIFIED
|
343
|
-
message = str(exception)
|
344
|
-
extras = []
|
345
|
-
return cls.with_exception(type_=type_, exception=exception, title=title, message=message, extras=extras)
|
346
|
-
|
347
|
-
|
348
|
-
HEALTH_CHECK_MESSAGE_DATA_TOO_LARGE = """There's a notable occurrence of examples surpassing the maximum size limit.
|
349
|
-
Typically, generating excessively large examples can compromise the quality of test outcomes.
|
350
|
-
|
351
|
-
Consider revising the schema to more accurately represent typical use cases
|
352
|
-
or applying constraints to reduce the data size."""
|
353
|
-
HEALTH_CHECK_MESSAGE_FILTER_TOO_MUCH = """A significant number of generated examples are being filtered out, indicating
|
354
|
-
that the schema's constraints may be too complex.
|
355
|
-
|
356
|
-
This level of filtration can slow down testing and affect the distribution
|
357
|
-
of generated data. Review and simplify the schema constraints where
|
358
|
-
possible to mitigate this issue."""
|
359
|
-
HEALTH_CHECK_MESSAGE_TOO_SLOW = "Data generation is extremely slow. Consider reducing the complexity of the schema."
|
360
|
-
HEALTH_CHECK_MESSAGE_LARGE_BASE_EXAMPLE = """A health check has identified that the smallest example derived from the schema
|
361
|
-
is excessively large, potentially leading to inefficient test execution.
|
362
|
-
|
363
|
-
This is commonly due to schemas that specify large-scale data structures by
|
364
|
-
default, such as an array with an extensive number of elements.
|
365
|
-
|
366
|
-
Consider revising the schema to more accurately represent typical use cases
|
367
|
-
or applying constraints to reduce the data size."""
|
368
|
-
|
369
|
-
|
370
|
-
def _health_check_from_error(exception: hypothesis.errors.FailedHealthCheck) -> hypothesis.HealthCheck | None:
|
371
|
-
from hypothesis import HealthCheck
|
372
|
-
|
373
|
-
match = re.search(r"add HealthCheck\.(\w+) to the suppress_health_check ", str(exception))
|
374
|
-
if match:
|
375
|
-
return {
|
376
|
-
"data_too_large": HealthCheck.data_too_large,
|
377
|
-
"filter_too_much": HealthCheck.filter_too_much,
|
378
|
-
"too_slow": HealthCheck.too_slow,
|
379
|
-
"large_base_example": HealthCheck.large_base_example,
|
380
|
-
}.get(match.group(1))
|
381
|
-
return None
|
382
|
-
|
383
|
-
|
384
|
-
def _scalar_name_from_error(exception: hypothesis.errors.InvalidArgument) -> str:
|
385
|
-
# This one is always available as the format is checked upfront
|
386
|
-
match = re.search(r"Scalar '(\w+)' is not supported", str(exception))
|
387
|
-
match = cast(re.Match, match)
|
388
|
-
return match.group(1)
|
389
|
-
|
390
|
-
|
391
|
-
@dataclass
|
392
|
-
class SerializedInteraction:
|
393
|
-
request: Request
|
394
|
-
response: Response | None
|
395
|
-
checks: list[SerializedCheck]
|
396
|
-
status: Status
|
397
|
-
data_generation_method: DataGenerationMethod
|
398
|
-
phase: TestPhase | None
|
399
|
-
description: str | None
|
400
|
-
location: str | None
|
401
|
-
parameter: str | None
|
402
|
-
parameter_location: str | None
|
403
|
-
recorded_at: str
|
404
|
-
|
405
|
-
@classmethod
|
406
|
-
def from_interaction(cls, interaction: Interaction) -> SerializedInteraction:
|
407
|
-
return cls(
|
408
|
-
request=interaction.request,
|
409
|
-
response=interaction.response,
|
410
|
-
checks=[SerializedCheck.from_check(check) for check in interaction.checks],
|
411
|
-
status=interaction.status,
|
412
|
-
data_generation_method=interaction.data_generation_method,
|
413
|
-
phase=interaction.phase,
|
414
|
-
description=interaction.description,
|
415
|
-
location=interaction.location,
|
416
|
-
parameter=interaction.parameter,
|
417
|
-
parameter_location=interaction.parameter_location,
|
418
|
-
recorded_at=interaction.recorded_at,
|
419
|
-
)
|
420
|
-
|
421
|
-
|
422
|
-
@dataclass
|
423
|
-
class SerializedTestResult:
|
424
|
-
method: str
|
425
|
-
path: str
|
426
|
-
verbose_name: str
|
427
|
-
has_failures: bool
|
428
|
-
has_errors: bool
|
429
|
-
has_logs: bool
|
430
|
-
is_errored: bool
|
431
|
-
is_flaky: bool
|
432
|
-
is_skipped: bool
|
433
|
-
skip_reason: str | None
|
434
|
-
seed: int | None
|
435
|
-
data_generation_method: list[str]
|
436
|
-
checks: list[SerializedCheck]
|
437
|
-
logs: list[str]
|
438
|
-
errors: list[SerializedError]
|
439
|
-
interactions: list[SerializedInteraction]
|
440
|
-
|
441
|
-
@classmethod
|
442
|
-
def from_test_result(cls, result: TestResult) -> SerializedTestResult:
|
443
|
-
formatter = logging.Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s")
|
444
|
-
return cls(
|
445
|
-
method=result.method,
|
446
|
-
path=result.path,
|
447
|
-
verbose_name=result.verbose_name,
|
448
|
-
has_failures=result.has_failures,
|
449
|
-
has_errors=result.has_errors,
|
450
|
-
has_logs=result.has_logs,
|
451
|
-
is_errored=result.is_errored,
|
452
|
-
is_flaky=result.is_flaky,
|
453
|
-
is_skipped=result.is_skipped,
|
454
|
-
skip_reason=result.skip_reason,
|
455
|
-
seed=result.seed,
|
456
|
-
data_generation_method=[m.as_short_name() for m in result.data_generation_method],
|
457
|
-
checks=[SerializedCheck.from_check(check) for check in result.checks],
|
458
|
-
logs=[formatter.format(record) for record in result.logs],
|
459
|
-
errors=[SerializedError.from_exception(error) for error in result.errors],
|
460
|
-
interactions=[SerializedInteraction.from_interaction(interaction) for interaction in result.interactions],
|
461
|
-
)
|
462
|
-
|
463
|
-
|
464
|
-
def deduplicate_failures(checks: list[SerializedCheck]) -> list[SerializedCheck]:
|
465
|
-
"""Return only unique checks that should be displayed in the output."""
|
466
|
-
seen: set[tuple[str | None, ...]] = set()
|
467
|
-
unique_checks = []
|
468
|
-
for check in reversed(checks):
|
469
|
-
# There are also could be checks that didn't fail
|
470
|
-
if check.value == Status.failure:
|
471
|
-
key = make_unique_by_key(check.name, check.message, check.context)
|
472
|
-
if key not in seen:
|
473
|
-
unique_checks.append(check)
|
474
|
-
seen.add(key)
|
475
|
-
return unique_checks
|
476
|
-
|
477
|
-
|
478
|
-
def _serialize_case(case: SerializedCase) -> dict[str, Any]:
|
479
|
-
return {
|
480
|
-
"id": case.id,
|
481
|
-
"generation_time": case.generation_time,
|
482
|
-
"verbose_name": case.verbose_name,
|
483
|
-
"path_template": case.path_template,
|
484
|
-
"path_parameters": stringify_path_parameters(case.path_parameters),
|
485
|
-
"query": prepare_query(case.query),
|
486
|
-
"cookies": case.cookies,
|
487
|
-
"media_type": case.media_type,
|
488
|
-
}
|
489
|
-
|
490
|
-
|
491
|
-
def _serialize_response(response: Response) -> dict[str, Any]:
|
492
|
-
return {
|
493
|
-
"status_code": response.status_code,
|
494
|
-
"headers": response.headers,
|
495
|
-
"body": response.body,
|
496
|
-
"encoding": response.encoding,
|
497
|
-
"elapsed": response.elapsed,
|
498
|
-
}
|
499
|
-
|
500
|
-
|
501
|
-
def _serialize_check(check: SerializedCheck) -> dict[str, Any]:
|
502
|
-
return {
|
503
|
-
"name": check.name,
|
504
|
-
"value": check.value,
|
505
|
-
"request": {
|
506
|
-
"method": check.request.method,
|
507
|
-
"uri": check.request.uri,
|
508
|
-
"body": check.request.body,
|
509
|
-
"headers": check.request.headers,
|
510
|
-
},
|
511
|
-
"response": _serialize_response(check.response) if check.response is not None else None,
|
512
|
-
"example": _serialize_case(check.example),
|
513
|
-
"message": check.message,
|
514
|
-
"context": asdict(check.context) if check.context is not None else None, # type: ignore
|
515
|
-
"history": [
|
516
|
-
{"case": _serialize_case(entry.case), "response": _serialize_response(entry.response)}
|
517
|
-
for entry in check.history
|
518
|
-
],
|
519
|
-
}
|
520
|
-
|
521
|
-
|
522
|
-
def stringify_path_parameters(path_parameters: dict[str, Any] | None) -> dict[str, str]:
|
523
|
-
"""Cast all path parameter values to strings.
|
524
|
-
|
525
|
-
Path parameter values may be of arbitrary type, but to display them properly they should be casted to strings.
|
526
|
-
"""
|
527
|
-
return {key: str(value) for key, value in (path_parameters or {}).items()}
|
528
|
-
|
529
|
-
|
530
|
-
def prepare_query(query: dict[str, Any] | None) -> dict[str, list[str]]:
|
531
|
-
"""Convert all query values to list of strings.
|
532
|
-
|
533
|
-
Query parameters may be generated in different shapes, including integers, strings, list of strings, etc.
|
534
|
-
It can also be an object, if the schema contains an object, but `style` and `explode` combo is not applicable.
|
535
|
-
"""
|
536
|
-
|
537
|
-
def to_list_of_strings(value: Any) -> list[str]:
|
538
|
-
if isinstance(value, list):
|
539
|
-
return list(map(str, value))
|
540
|
-
if isinstance(value, str):
|
541
|
-
return [value]
|
542
|
-
return [str(value)]
|
543
|
-
|
544
|
-
return {key: to_list_of_strings(value) for key, value in (query or {}).items()}
|