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,246 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import base64
|
4
|
+
import time
|
5
|
+
import uuid
|
6
|
+
from dataclasses import dataclass
|
7
|
+
from typing import TYPE_CHECKING, Iterator, cast
|
8
|
+
|
9
|
+
from schemathesis.core.failures import Failure
|
10
|
+
from schemathesis.core.transport import Response
|
11
|
+
from schemathesis.engine import Status
|
12
|
+
from schemathesis.generation.case import Case
|
13
|
+
|
14
|
+
if TYPE_CHECKING:
|
15
|
+
import requests
|
16
|
+
|
17
|
+
from schemathesis.generation.stateful.state_machine import Transition
|
18
|
+
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class ScenarioRecorder:
|
22
|
+
"""Tracks and organizes all data related to a logical block of testing.
|
23
|
+
|
24
|
+
Records test cases, their hierarchy, API interactions, and results of checks performed during execution.
|
25
|
+
"""
|
26
|
+
|
27
|
+
id: uuid.UUID
|
28
|
+
# Human-readable label
|
29
|
+
label: str
|
30
|
+
|
31
|
+
# Recorded test cases
|
32
|
+
cases: dict[str, CaseNode]
|
33
|
+
# Results of checks categorized by test case ID
|
34
|
+
checks: dict[str, list[CheckNode]]
|
35
|
+
# Network interactions by test case ID
|
36
|
+
interactions: dict[str, Interaction]
|
37
|
+
|
38
|
+
__slots__ = ("id", "label", "status", "roots", "cases", "checks", "interactions")
|
39
|
+
|
40
|
+
def __init__(self, *, label: str) -> None:
|
41
|
+
self.id = uuid.uuid4()
|
42
|
+
self.label = label
|
43
|
+
self.cases = {}
|
44
|
+
self.checks = {}
|
45
|
+
self.interactions = {}
|
46
|
+
|
47
|
+
def record_case(self, *, parent_id: str | None, transition: Transition | None, case: Case) -> None:
|
48
|
+
"""Record a test case and its relationship to a parent, if applicable."""
|
49
|
+
self.cases[case.id] = CaseNode(value=case, parent_id=parent_id, transition=transition)
|
50
|
+
|
51
|
+
def record_response(self, *, case_id: str, response: Response) -> None:
|
52
|
+
"""Record the API response for a given test case."""
|
53
|
+
request = Request.from_prepared_request(response.request)
|
54
|
+
self.interactions[case_id] = Interaction(request=request, response=response)
|
55
|
+
|
56
|
+
def record_request(self, *, case_id: str, request: requests.PreparedRequest) -> None:
|
57
|
+
"""Record a network-level error for a given test case."""
|
58
|
+
self.interactions[case_id] = Interaction(request=Request.from_prepared_request(request), response=None)
|
59
|
+
|
60
|
+
def record_check_failure(self, *, name: str, case_id: str, code_sample: str, failure: Failure) -> None:
|
61
|
+
"""Record a failure of a check for a given test case."""
|
62
|
+
self.checks.setdefault(case_id, []).append(
|
63
|
+
CheckNode(
|
64
|
+
name=name,
|
65
|
+
status=Status.FAILURE,
|
66
|
+
failure_info=CheckFailureInfo(code_sample=code_sample, failure=failure),
|
67
|
+
)
|
68
|
+
)
|
69
|
+
|
70
|
+
def record_check_success(self, *, name: str, case_id: str) -> None:
|
71
|
+
"""Record a successful pass of a check for a given test case."""
|
72
|
+
self.checks.setdefault(case_id, []).append(CheckNode(name=name, status=Status.SUCCESS, failure_info=None))
|
73
|
+
|
74
|
+
def find_failure_data(self, *, parent_id: str, failure: Failure) -> FailureData:
|
75
|
+
"""Retrieve the relevant test case & interaction data for a failure.
|
76
|
+
|
77
|
+
It may happen that a failure comes from a different test case if a check generated some additional
|
78
|
+
test cases & interactions.
|
79
|
+
"""
|
80
|
+
case_id = failure.case_id or parent_id
|
81
|
+
case = self.cases[case_id].value
|
82
|
+
request = self.interactions[case_id].request
|
83
|
+
response = self.interactions[case_id].response
|
84
|
+
assert isinstance(response, Response)
|
85
|
+
headers = {key: value[0] for key, value in request.headers.items()}
|
86
|
+
return FailureData(case=case, headers=headers, verify=response.verify)
|
87
|
+
|
88
|
+
def find_parent(self, *, case_id: str) -> Case | None:
|
89
|
+
"""Find the parent case of a given test case, if it exists."""
|
90
|
+
case = self.cases.get(case_id)
|
91
|
+
if case is not None and case.parent_id is not None:
|
92
|
+
parent = self.cases.get(case.parent_id)
|
93
|
+
# The recorder state should always be consistent
|
94
|
+
assert parent is not None, "Parent does not exist"
|
95
|
+
return parent.value
|
96
|
+
return None
|
97
|
+
|
98
|
+
def find_related(self, *, case_id: str) -> Iterator[Case]:
|
99
|
+
"""Iterate over all ancestors and their children for a given case."""
|
100
|
+
current_id = case_id
|
101
|
+
seen = {current_id}
|
102
|
+
|
103
|
+
while True:
|
104
|
+
current_node = self.cases.get(current_id)
|
105
|
+
if current_node is None or current_node.parent_id is None:
|
106
|
+
break
|
107
|
+
|
108
|
+
# Get all children of the parent (siblings of the current case)
|
109
|
+
parent_id = current_node.parent_id
|
110
|
+
for case_id, maybe_child in self.cases.items():
|
111
|
+
# If this case has the same parent and we haven't seen it yet
|
112
|
+
if parent_id == maybe_child.parent_id and case_id not in seen:
|
113
|
+
seen.add(case_id)
|
114
|
+
yield maybe_child.value
|
115
|
+
|
116
|
+
# Move up to the parent
|
117
|
+
current_id = parent_id
|
118
|
+
if current_id not in seen:
|
119
|
+
seen.add(current_id)
|
120
|
+
parent_node = self.cases.get(current_id)
|
121
|
+
if parent_node:
|
122
|
+
yield parent_node.value
|
123
|
+
|
124
|
+
def find_response(self, *, case_id: str) -> Response | None:
|
125
|
+
"""Retrieve the API response for a given test case, if available."""
|
126
|
+
interaction = self.interactions.get(case_id)
|
127
|
+
if interaction is None or interaction.response is None:
|
128
|
+
return None
|
129
|
+
return interaction.response
|
130
|
+
|
131
|
+
|
132
|
+
@dataclass
|
133
|
+
class CaseNode:
|
134
|
+
"""Represents a test case and its parent-child relationship."""
|
135
|
+
|
136
|
+
value: Case
|
137
|
+
parent_id: str | None
|
138
|
+
# Transition may be absent if `parent_id` is present for cases when a case is derived inside a check
|
139
|
+
# and outside of the implemented transition logic (e.g. Open API links)
|
140
|
+
transition: Transition | None
|
141
|
+
|
142
|
+
__slots__ = ("value", "parent_id", "transition")
|
143
|
+
|
144
|
+
|
145
|
+
@dataclass
|
146
|
+
class CheckNode:
|
147
|
+
name: str
|
148
|
+
status: Status
|
149
|
+
failure_info: CheckFailureInfo | None
|
150
|
+
|
151
|
+
__slots__ = ("name", "status", "failure_info")
|
152
|
+
|
153
|
+
|
154
|
+
@dataclass
|
155
|
+
class CheckFailureInfo:
|
156
|
+
code_sample: str
|
157
|
+
failure: Failure
|
158
|
+
|
159
|
+
__slots__ = ("code_sample", "failure")
|
160
|
+
|
161
|
+
|
162
|
+
def serialize_payload(payload: bytes) -> str:
|
163
|
+
return base64.b64encode(payload).decode()
|
164
|
+
|
165
|
+
|
166
|
+
@dataclass(repr=False)
|
167
|
+
class Request:
|
168
|
+
"""Request data extracted from `Case`."""
|
169
|
+
|
170
|
+
method: str
|
171
|
+
uri: str
|
172
|
+
body: bytes | None
|
173
|
+
body_size: int | None
|
174
|
+
headers: dict[str, list[str]]
|
175
|
+
|
176
|
+
__slots__ = ("method", "uri", "body", "body_size", "headers", "_encoded_body_cache")
|
177
|
+
|
178
|
+
def __init__(
|
179
|
+
self,
|
180
|
+
method: str,
|
181
|
+
uri: str,
|
182
|
+
body: bytes | None,
|
183
|
+
body_size: int | None,
|
184
|
+
headers: dict[str, list[str]],
|
185
|
+
):
|
186
|
+
self.method = method
|
187
|
+
self.uri = uri
|
188
|
+
self.body = body
|
189
|
+
self.body_size = body_size
|
190
|
+
self.headers = headers
|
191
|
+
self._encoded_body_cache: str | None = None
|
192
|
+
|
193
|
+
@classmethod
|
194
|
+
def from_prepared_request(cls, prepared: requests.PreparedRequest) -> Request:
|
195
|
+
"""A prepared request version is already stored in `requests.Response`."""
|
196
|
+
body = prepared.body
|
197
|
+
|
198
|
+
if isinstance(body, str):
|
199
|
+
# can be a string for `application/x-www-form-urlencoded`
|
200
|
+
body = body.encode("utf-8")
|
201
|
+
|
202
|
+
# these values have `str` type at this point
|
203
|
+
uri = cast(str, prepared.url)
|
204
|
+
method = cast(str, prepared.method)
|
205
|
+
return cls(
|
206
|
+
uri=uri,
|
207
|
+
method=method,
|
208
|
+
headers={key: [value] for (key, value) in prepared.headers.items()},
|
209
|
+
body=body,
|
210
|
+
body_size=len(body) if body is not None else None,
|
211
|
+
)
|
212
|
+
|
213
|
+
@property
|
214
|
+
def encoded_body(self) -> str | None:
|
215
|
+
if self.body is not None:
|
216
|
+
if self._encoded_body_cache is None:
|
217
|
+
self._encoded_body_cache = serialize_payload(self.body)
|
218
|
+
return self._encoded_body_cache
|
219
|
+
return None
|
220
|
+
|
221
|
+
|
222
|
+
@dataclass
|
223
|
+
class Interaction:
|
224
|
+
"""Represents a single interaction with the tested application."""
|
225
|
+
|
226
|
+
request: Request
|
227
|
+
response: Response | None
|
228
|
+
timestamp: float
|
229
|
+
|
230
|
+
__slots__ = ("request", "response", "timestamp")
|
231
|
+
|
232
|
+
def __init__(self, request: Request, response: Response | None) -> None:
|
233
|
+
self.request = request
|
234
|
+
self.response = response
|
235
|
+
self.timestamp = time.time()
|
236
|
+
|
237
|
+
|
238
|
+
@dataclass
|
239
|
+
class FailureData:
|
240
|
+
"""Details about a test failure, including the case and its context."""
|
241
|
+
|
242
|
+
case: Case
|
243
|
+
headers: dict[str, str]
|
244
|
+
verify: bool
|
245
|
+
|
246
|
+
__slots__ = ("case", "headers", "verify")
|
schemathesis/errors.py
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
"""Public Schemathesis errors."""
|
2
|
+
|
3
|
+
from schemathesis.core.errors import IncorrectUsage as IncorrectUsage
|
4
|
+
from schemathesis.core.errors import InternalError as InternalError
|
5
|
+
from schemathesis.core.errors import InvalidHeadersExample as InvalidHeadersExample
|
6
|
+
from schemathesis.core.errors import InvalidRateLimit as InvalidRateLimit
|
7
|
+
from schemathesis.core.errors import InvalidRegexPattern as InvalidRegexPattern
|
8
|
+
from schemathesis.core.errors import InvalidRegexType as InvalidRegexType
|
9
|
+
from schemathesis.core.errors import InvalidSchema as InvalidSchema
|
10
|
+
from schemathesis.core.errors import LoaderError as LoaderError
|
11
|
+
from schemathesis.core.errors import OperationNotFound as OperationNotFound
|
12
|
+
from schemathesis.core.errors import SchemathesisError as SchemathesisError
|
13
|
+
from schemathesis.core.errors import SerializationError as SerializationError
|
14
|
+
from schemathesis.core.errors import SerializationNotPossible as SerializationNotPossible
|
15
|
+
from schemathesis.core.errors import UnboundPrefix as UnboundPrefix
|
16
|
+
|
17
|
+
__all__ = [
|
18
|
+
"IncorrectUsage",
|
19
|
+
"InternalError",
|
20
|
+
"InvalidHeadersExample",
|
21
|
+
"InvalidRateLimit",
|
22
|
+
"InvalidRegexPattern",
|
23
|
+
"InvalidRegexType",
|
24
|
+
"InvalidSchema",
|
25
|
+
"LoaderError",
|
26
|
+
"OperationNotFound",
|
27
|
+
"SchemathesisError",
|
28
|
+
"SerializationError",
|
29
|
+
"SerializationNotPossible",
|
30
|
+
"UnboundPrefix",
|
31
|
+
]
|
@@ -1,18 +1,21 @@
|
|
1
1
|
import os
|
2
2
|
from dataclasses import dataclass, field
|
3
3
|
|
4
|
-
from
|
4
|
+
from schemathesis.core import string_to_boolean
|
5
5
|
|
6
6
|
|
7
7
|
@dataclass(eq=False)
|
8
8
|
class Experiment:
|
9
9
|
name: str
|
10
|
-
verbose_name: str
|
11
10
|
env_var: str
|
12
11
|
description: str
|
13
12
|
discussion_url: str
|
14
13
|
_storage: "ExperimentSet" = field(repr=False)
|
15
14
|
|
15
|
+
@property
|
16
|
+
def label(self) -> str:
|
17
|
+
return self.name.lower().replace(" ", "-")
|
18
|
+
|
16
19
|
def enable(self) -> None:
|
17
20
|
self._storage.enable(self)
|
18
21
|
|
@@ -25,7 +28,7 @@ class Experiment:
|
|
25
28
|
|
26
29
|
@property
|
27
30
|
def is_env_var_set(self) -> bool:
|
28
|
-
return os.getenv(self.env_var, "")
|
31
|
+
return string_to_boolean(os.getenv(self.env_var, "")) is True
|
29
32
|
|
30
33
|
|
31
34
|
@dataclass
|
@@ -33,12 +36,9 @@ class ExperimentSet:
|
|
33
36
|
available: set = field(default_factory=set)
|
34
37
|
enabled: set = field(default_factory=set)
|
35
38
|
|
36
|
-
def create_experiment(
|
37
|
-
self, name: str, verbose_name: str, env_var: str, description: str, discussion_url: str
|
38
|
-
) -> Experiment:
|
39
|
+
def create_experiment(self, name: str, env_var: str, description: str, discussion_url: str) -> Experiment:
|
39
40
|
instance = Experiment(
|
40
41
|
name=name,
|
41
|
-
verbose_name=verbose_name,
|
42
42
|
env_var=f"{ENV_PREFIX}_{env_var}",
|
43
43
|
description=description,
|
44
44
|
discussion_url=discussion_url,
|
@@ -64,45 +64,14 @@ class ExperimentSet:
|
|
64
64
|
|
65
65
|
ENV_PREFIX = "SCHEMATHESIS_EXPERIMENTAL"
|
66
66
|
GLOBAL_EXPERIMENTS = ExperimentSet()
|
67
|
-
|
68
|
-
OPEN_API_3_1 = GLOBAL_EXPERIMENTS.create_experiment(
|
69
|
-
name="openapi-3.1",
|
70
|
-
verbose_name="OpenAPI 3.1",
|
71
|
-
env_var="OPENAPI_3_1",
|
72
|
-
description="Support for response validation",
|
73
|
-
discussion_url="https://github.com/schemathesis/schemathesis/discussions/1822",
|
74
|
-
)
|
75
|
-
SCHEMA_ANALYSIS = GLOBAL_EXPERIMENTS.create_experiment(
|
76
|
-
name="schema-analysis",
|
77
|
-
verbose_name="Schema Analysis",
|
78
|
-
env_var="SCHEMA_ANALYSIS",
|
79
|
-
description="Analyzing API schemas via Schemathesis.io",
|
80
|
-
discussion_url="https://github.com/schemathesis/schemathesis/discussions/2056",
|
81
|
-
)
|
82
|
-
STATEFUL_TEST_RUNNER = GLOBAL_EXPERIMENTS.create_experiment(
|
83
|
-
name="stateful-test-runner",
|
84
|
-
verbose_name="New Stateful Test Runner",
|
85
|
-
env_var="STATEFUL_TEST_RUNNER",
|
86
|
-
description="State machine-based runner for stateful tests in CLI",
|
87
|
-
discussion_url="https://github.com/schemathesis/schemathesis/discussions/2262",
|
88
|
-
)
|
89
|
-
STATEFUL_ONLY = GLOBAL_EXPERIMENTS.create_experiment(
|
90
|
-
name="stateful-only",
|
91
|
-
verbose_name="Stateful Only",
|
92
|
-
env_var="STATEFUL_ONLY",
|
93
|
-
description="Run only stateful tests",
|
94
|
-
discussion_url="https://github.com/schemathesis/schemathesis/discussions/2262",
|
95
|
-
)
|
96
67
|
COVERAGE_PHASE = GLOBAL_EXPERIMENTS.create_experiment(
|
97
|
-
name="
|
98
|
-
verbose_name="Coverage phase",
|
68
|
+
name="Coverage phase",
|
99
69
|
env_var="COVERAGE_PHASE",
|
100
70
|
description="Generate covering test cases",
|
101
71
|
discussion_url="https://github.com/schemathesis/schemathesis/discussions/2418",
|
102
72
|
)
|
103
73
|
POSITIVE_DATA_ACCEPTANCE = GLOBAL_EXPERIMENTS.create_experiment(
|
104
|
-
name="
|
105
|
-
verbose_name="Positive Data Acceptance",
|
74
|
+
name="Positive Data Acceptance",
|
106
75
|
env_var="POSITIVE_DATA_ACCEPTANCE",
|
107
76
|
description="Verifying schema-conformant data is accepted",
|
108
77
|
discussion_url="https://github.com/schemathesis/schemathesis/discussions/2499",
|
schemathesis/filters.py
CHANGED
@@ -9,12 +9,11 @@ from functools import partial
|
|
9
9
|
from types import SimpleNamespace
|
10
10
|
from typing import TYPE_CHECKING, Any, Callable, List, Protocol, Union
|
11
11
|
|
12
|
-
from .
|
13
|
-
from .
|
14
|
-
from .types import NotSet
|
12
|
+
from schemathesis.core.errors import IncorrectUsage
|
13
|
+
from schemathesis.core.transforms import resolve_pointer
|
15
14
|
|
16
15
|
if TYPE_CHECKING:
|
17
|
-
from .
|
16
|
+
from schemathesis.schemas import APIOperation
|
18
17
|
|
19
18
|
|
20
19
|
class HasAPIOperation(Protocol):
|
@@ -151,23 +150,6 @@ class FilterSet:
|
|
151
150
|
def clone(self) -> FilterSet:
|
152
151
|
return FilterSet(_includes=self._includes.copy(), _excludes=self._excludes.copy())
|
153
152
|
|
154
|
-
def merge(self, other: FilterSet) -> FilterSet:
|
155
|
-
def _merge(lhs: set[Filter], rhs: set[Filter]) -> set[Filter]:
|
156
|
-
result = lhs.copy()
|
157
|
-
for new in rhs:
|
158
|
-
for old in lhs:
|
159
|
-
for new_matcher in new.matchers:
|
160
|
-
for old_matcher in old.matchers:
|
161
|
-
if "=" in new_matcher.label and "=" in old_matcher.label:
|
162
|
-
if new_matcher.label.split("=")[0] == old_matcher.label.split("=")[0]:
|
163
|
-
result.remove(old)
|
164
|
-
result.add(new)
|
165
|
-
return result
|
166
|
-
|
167
|
-
return FilterSet(
|
168
|
-
_includes=_merge(self._includes, other._includes), _excludes=_merge(self._excludes, other._excludes)
|
169
|
-
)
|
170
|
-
|
171
153
|
def apply_to(self, operations: list[APIOperation]) -> list[APIOperation]:
|
172
154
|
"""Get a filtered list of the given operations that match the filters."""
|
173
155
|
return [operation for operation in operations if self.match(SimpleNamespace(operation=operation))]
|
@@ -276,7 +258,7 @@ class FilterSet:
|
|
276
258
|
if func is not None:
|
277
259
|
matchers.append(Matcher.for_function(func))
|
278
260
|
for attribute, expected, regex in (
|
279
|
-
("
|
261
|
+
("label", name, name_regex),
|
280
262
|
("method", method, method_regex),
|
281
263
|
("path", path, path_regex),
|
282
264
|
("tag", tag, tag_regex),
|
@@ -284,17 +266,17 @@ class FilterSet:
|
|
284
266
|
):
|
285
267
|
if expected is not None and regex is not None:
|
286
268
|
# To match anything the regex should match the expected value, hence passing them together is useless
|
287
|
-
raise
|
269
|
+
raise IncorrectUsage(ERROR_EXPECTED_AND_REGEX)
|
288
270
|
if expected is not None:
|
289
271
|
matchers.append(Matcher.for_value(attribute, expected))
|
290
272
|
if regex is not None:
|
291
273
|
matchers.append(Matcher.for_regex(attribute, regex))
|
292
274
|
|
293
275
|
if not matchers:
|
294
|
-
raise
|
276
|
+
raise IncorrectUsage(ERROR_EMPTY_FILTER)
|
295
277
|
filter_ = Filter(matchers=tuple(matchers))
|
296
278
|
if filter_ in self._includes or filter_ in self._excludes:
|
297
|
-
raise
|
279
|
+
raise IncorrectUsage(ERROR_FILTER_EXISTS)
|
298
280
|
if include:
|
299
281
|
self._includes.add(filter_)
|
300
282
|
else:
|
@@ -358,74 +340,6 @@ def is_deprecated(ctx: HasAPIOperation) -> bool:
|
|
358
340
|
return ctx.operation.definition.raw.get("deprecated") is True
|
359
341
|
|
360
342
|
|
361
|
-
def filter_set_from_components(
|
362
|
-
*,
|
363
|
-
include: bool,
|
364
|
-
method: FilterType | None = None,
|
365
|
-
endpoint: FilterType | None = None,
|
366
|
-
tag: FilterType | None = None,
|
367
|
-
operation_id: FilterType | None = None,
|
368
|
-
skip_deprecated_operations: bool | None | NotSet = None,
|
369
|
-
parent: FilterSet | None = None,
|
370
|
-
) -> FilterSet:
|
371
|
-
def _is_defined(x: FilterType | None) -> bool:
|
372
|
-
return x is not None and not isinstance(x, NotSet)
|
373
|
-
|
374
|
-
def _prepare_filter(filter_: FilterType | None) -> RegexValue | None:
|
375
|
-
if filter_ is None or isinstance(filter_, NotSet):
|
376
|
-
return None
|
377
|
-
if isinstance(filter_, str):
|
378
|
-
return filter_
|
379
|
-
return "|".join(f"({f})" for f in filter_)
|
380
|
-
|
381
|
-
new = FilterSet()
|
382
|
-
|
383
|
-
if _is_defined(method) or _is_defined(endpoint) or _is_defined(tag) or _is_defined(operation_id):
|
384
|
-
new._add_filter(
|
385
|
-
include,
|
386
|
-
method_regex=_prepare_filter(method),
|
387
|
-
path_regex=_prepare_filter(endpoint),
|
388
|
-
tag_regex=_prepare_filter(tag),
|
389
|
-
operation_id_regex=_prepare_filter(operation_id),
|
390
|
-
)
|
391
|
-
if skip_deprecated_operations is True and not any(
|
392
|
-
matcher.label == is_deprecated.__name__ for exclude_ in new._excludes for matcher in exclude_.matchers
|
393
|
-
):
|
394
|
-
new.exclude(func=is_deprecated)
|
395
|
-
# Merge with the parent filter set
|
396
|
-
if parent is not None:
|
397
|
-
for include_ in parent._includes:
|
398
|
-
matchers = include_.matchers
|
399
|
-
ids = []
|
400
|
-
for idx, matcher in enumerate(matchers):
|
401
|
-
label = matcher.label
|
402
|
-
if (
|
403
|
-
(not isinstance(method, NotSet) and label.startswith("method_regex="))
|
404
|
-
or (not isinstance(endpoint, NotSet) and label.startswith("path_regex="))
|
405
|
-
or (not isinstance(tag, NotSet) and matcher.label.startswith("tag_regex="))
|
406
|
-
or (not isinstance(operation_id, NotSet) and matcher.label.startswith("operation_id_regex="))
|
407
|
-
):
|
408
|
-
ids.append(idx)
|
409
|
-
if ids:
|
410
|
-
matchers = tuple(matcher for idx, matcher in enumerate(matchers) if idx not in ids)
|
411
|
-
if matchers:
|
412
|
-
if new._includes:
|
413
|
-
existing = new._includes.pop()
|
414
|
-
matchers = existing.matchers + matchers
|
415
|
-
new._includes.add(Filter(matchers=matchers))
|
416
|
-
for exclude_ in parent._excludes:
|
417
|
-
matchers = exclude_.matchers
|
418
|
-
ids = []
|
419
|
-
for idx, matcher in enumerate(exclude_.matchers):
|
420
|
-
if skip_deprecated_operations is False and matcher.label == is_deprecated.__name__:
|
421
|
-
ids.append(idx)
|
422
|
-
if ids:
|
423
|
-
matchers = tuple(matcher for idx, matcher in enumerate(matchers) if idx not in ids)
|
424
|
-
if matchers:
|
425
|
-
new._excludes.add(exclude_)
|
426
|
-
return new
|
427
|
-
|
428
|
-
|
429
343
|
def parse_expression(expression: str) -> tuple[str, str, Any]:
|
430
344
|
expression = expression.strip()
|
431
345
|
|
@@ -452,8 +366,6 @@ def parse_expression(expression: str) -> tuple[str, str, Any]:
|
|
452
366
|
|
453
367
|
|
454
368
|
def expression_to_filter_function(expression: str) -> Callable[[HasAPIOperation], bool]:
|
455
|
-
from .specs.openapi.references import resolve_pointer
|
456
|
-
|
457
369
|
pointer, op, value = parse_expression(expression)
|
458
370
|
|
459
371
|
if op == "==":
|
@@ -4,14 +4,13 @@ import random
|
|
4
4
|
from dataclasses import dataclass, field
|
5
5
|
from typing import TYPE_CHECKING
|
6
6
|
|
7
|
-
from .
|
8
|
-
from ._methods import DataGenerationMethod, DataGenerationMethodInput
|
7
|
+
from schemathesis.generation.modes import GenerationMode as GenerationMode
|
9
8
|
|
10
9
|
if TYPE_CHECKING:
|
11
10
|
from hypothesis.strategies import SearchStrategy
|
12
11
|
|
13
12
|
|
14
|
-
|
13
|
+
DEFAULT_GENERATOR_MODES = (GenerationMode.default(),)
|
15
14
|
|
16
15
|
|
17
16
|
CASE_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
@@ -40,6 +39,7 @@ class HeaderConfig:
|
|
40
39
|
class GenerationConfig:
|
41
40
|
"""Holds various configuration options relevant for data generation."""
|
42
41
|
|
42
|
+
modes: list[GenerationMode] = field(default_factory=lambda: [GenerationMode.default()])
|
43
43
|
# Allow generating `\x00` bytes in strings
|
44
44
|
allow_x00: bool = True
|
45
45
|
# Allowing using `null` for optional arguments in GraphQL queries
|