schemathesis 3.39.16__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 +233 -307
- 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.16.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
- {schemathesis-3.39.16.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 -717
- 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.16.dist-info/METADATA +0 -293
- schemathesis-3.39.16.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.16.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,247 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import base64
|
4
|
+
import time
|
5
|
+
from dataclasses import dataclass
|
6
|
+
from typing import TYPE_CHECKING, Iterator, cast
|
7
|
+
|
8
|
+
from schemathesis.core.failures import Failure
|
9
|
+
from schemathesis.core.transport import Response
|
10
|
+
from schemathesis.engine import Status
|
11
|
+
from schemathesis.generation.case import Case
|
12
|
+
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
import requests
|
15
|
+
|
16
|
+
from schemathesis.generation.stateful.state_machine import Transition
|
17
|
+
|
18
|
+
|
19
|
+
@dataclass
|
20
|
+
class ScenarioRecorder:
|
21
|
+
"""Tracks and organizes all data related to a logical block of testing.
|
22
|
+
|
23
|
+
Records test cases, their hierarchy, API interactions, and results of checks performed during execution.
|
24
|
+
"""
|
25
|
+
|
26
|
+
# Human-readable label
|
27
|
+
label: str
|
28
|
+
|
29
|
+
# Recorded test cases
|
30
|
+
cases: dict[str, CaseNode]
|
31
|
+
# Results of checks categorized by test case ID
|
32
|
+
checks: dict[str, list[CheckNode]]
|
33
|
+
# Network interactions by test case ID
|
34
|
+
interactions: dict[str, Interaction]
|
35
|
+
|
36
|
+
__slots__ = ("label", "status", "roots", "cases", "checks", "interactions")
|
37
|
+
|
38
|
+
def __init__(self, *, label: str) -> None:
|
39
|
+
self.label = label
|
40
|
+
self.cases = {}
|
41
|
+
self.checks = {}
|
42
|
+
self.interactions = {}
|
43
|
+
|
44
|
+
def record_case(self, *, parent_id: str | None, transition: Transition | None, case: Case) -> None:
|
45
|
+
"""Record a test case and its relationship to a parent, if applicable."""
|
46
|
+
self.cases[case.id] = CaseNode(value=case, parent_id=parent_id, transition=transition)
|
47
|
+
|
48
|
+
def record_response(self, *, case_id: str, response: Response) -> None:
|
49
|
+
"""Record the API response for a given test case."""
|
50
|
+
request = Request.from_prepared_request(response.request)
|
51
|
+
self.interactions[case_id] = Interaction(request=request, response=response)
|
52
|
+
|
53
|
+
def record_request(self, *, case_id: str, request: requests.PreparedRequest) -> None:
|
54
|
+
"""Record a network-level error for a given test case."""
|
55
|
+
self.interactions[case_id] = Interaction(request=Request.from_prepared_request(request), response=None)
|
56
|
+
|
57
|
+
def record_check_failure(self, *, name: str, case_id: str, code_sample: str, failure: Failure) -> None:
|
58
|
+
"""Record a failure of a check for a given test case."""
|
59
|
+
self.checks.setdefault(case_id, []).append(
|
60
|
+
CheckNode(
|
61
|
+
name=name,
|
62
|
+
status=Status.FAILURE,
|
63
|
+
failure_info=CheckFailureInfo(code_sample=code_sample, failure=failure),
|
64
|
+
)
|
65
|
+
)
|
66
|
+
|
67
|
+
def record_check_success(self, *, name: str, case_id: str) -> None:
|
68
|
+
"""Record a successful pass of a check for a given test case."""
|
69
|
+
self.checks.setdefault(case_id, []).append(CheckNode(name=name, status=Status.SUCCESS, failure_info=None))
|
70
|
+
|
71
|
+
def find_failure_data(self, *, parent_id: str, failure: Failure) -> FailureData:
|
72
|
+
"""Retrieve the relevant test case & interaction data for a failure.
|
73
|
+
|
74
|
+
It may happen that a failure comes from a different test case if a check generated some additional
|
75
|
+
test cases & interactions.
|
76
|
+
"""
|
77
|
+
case_id = failure.case_id or parent_id
|
78
|
+
case = self.cases[case_id].value
|
79
|
+
request = self.interactions[case_id].request
|
80
|
+
response = self.interactions[case_id].response
|
81
|
+
assert isinstance(response, Response)
|
82
|
+
headers = {key: value[0] for key, value in request.headers.items()}
|
83
|
+
return FailureData(case=case, headers=headers, verify=response.verify)
|
84
|
+
|
85
|
+
def find_parent(self, *, case_id: str) -> Case | None:
|
86
|
+
"""Find the parent case of a given test case, if it exists."""
|
87
|
+
case = self.cases.get(case_id)
|
88
|
+
if case is not None and case.parent_id is not None:
|
89
|
+
parent = self.cases.get(case.parent_id)
|
90
|
+
# The recorder state should always be consistent
|
91
|
+
assert parent is not None, "Parent does not exist"
|
92
|
+
return parent.value
|
93
|
+
return None
|
94
|
+
|
95
|
+
def find_related(self, *, case_id: str) -> Iterator[Case]:
|
96
|
+
"""Iterate over all cases in the tree, starting from the root."""
|
97
|
+
seen = {case_id}
|
98
|
+
|
99
|
+
# First, find the root by going up
|
100
|
+
current_id = case_id
|
101
|
+
while True:
|
102
|
+
current_node = self.cases.get(current_id)
|
103
|
+
if current_node is None or current_node.parent_id is None:
|
104
|
+
root_id = current_id
|
105
|
+
break
|
106
|
+
current_id = current_node.parent_id
|
107
|
+
|
108
|
+
# Then traverse the whole tree from root
|
109
|
+
def traverse(node_id: str) -> Iterator[Case]:
|
110
|
+
# Get all children
|
111
|
+
for case_id, node in self.cases.items():
|
112
|
+
if node.parent_id == node_id and case_id not in seen:
|
113
|
+
seen.add(case_id)
|
114
|
+
yield node.value
|
115
|
+
# Recurse into children
|
116
|
+
yield from traverse(case_id)
|
117
|
+
|
118
|
+
# Start traversal from root
|
119
|
+
root_node = self.cases.get(root_id)
|
120
|
+
if root_node and root_id not in seen:
|
121
|
+
seen.add(root_id)
|
122
|
+
yield root_node.value
|
123
|
+
yield from traverse(root_id)
|
124
|
+
|
125
|
+
def find_response(self, *, case_id: str) -> Response | None:
|
126
|
+
"""Retrieve the API response for a given test case, if available."""
|
127
|
+
interaction = self.interactions.get(case_id)
|
128
|
+
if interaction is None or interaction.response is None:
|
129
|
+
return None
|
130
|
+
return interaction.response
|
131
|
+
|
132
|
+
|
133
|
+
@dataclass
|
134
|
+
class CaseNode:
|
135
|
+
"""Represents a test case and its parent-child relationship."""
|
136
|
+
|
137
|
+
value: Case
|
138
|
+
parent_id: str | None
|
139
|
+
# Transition may be absent if `parent_id` is present for cases when a case is derived inside a check
|
140
|
+
# and outside of the implemented transition logic (e.g. Open API links)
|
141
|
+
transition: Transition | None
|
142
|
+
|
143
|
+
__slots__ = ("value", "parent_id", "transition")
|
144
|
+
|
145
|
+
|
146
|
+
@dataclass
|
147
|
+
class CheckNode:
|
148
|
+
name: str
|
149
|
+
status: Status
|
150
|
+
failure_info: CheckFailureInfo | None
|
151
|
+
|
152
|
+
__slots__ = ("name", "status", "failure_info")
|
153
|
+
|
154
|
+
|
155
|
+
@dataclass
|
156
|
+
class CheckFailureInfo:
|
157
|
+
code_sample: str
|
158
|
+
failure: Failure
|
159
|
+
|
160
|
+
__slots__ = ("code_sample", "failure")
|
161
|
+
|
162
|
+
|
163
|
+
def serialize_payload(payload: bytes) -> str:
|
164
|
+
return base64.b64encode(payload).decode()
|
165
|
+
|
166
|
+
|
167
|
+
@dataclass(repr=False)
|
168
|
+
class Request:
|
169
|
+
"""Request data extracted from `Case`."""
|
170
|
+
|
171
|
+
method: str
|
172
|
+
uri: str
|
173
|
+
body: bytes | None
|
174
|
+
body_size: int | None
|
175
|
+
headers: dict[str, list[str]]
|
176
|
+
|
177
|
+
__slots__ = ("method", "uri", "body", "body_size", "headers", "_encoded_body_cache")
|
178
|
+
|
179
|
+
def __init__(
|
180
|
+
self,
|
181
|
+
method: str,
|
182
|
+
uri: str,
|
183
|
+
body: bytes | None,
|
184
|
+
body_size: int | None,
|
185
|
+
headers: dict[str, list[str]],
|
186
|
+
):
|
187
|
+
self.method = method
|
188
|
+
self.uri = uri
|
189
|
+
self.body = body
|
190
|
+
self.body_size = body_size
|
191
|
+
self.headers = headers
|
192
|
+
self._encoded_body_cache: str | None = None
|
193
|
+
|
194
|
+
@classmethod
|
195
|
+
def from_prepared_request(cls, prepared: requests.PreparedRequest) -> Request:
|
196
|
+
"""A prepared request version is already stored in `requests.Response`."""
|
197
|
+
body = prepared.body
|
198
|
+
|
199
|
+
if isinstance(body, str):
|
200
|
+
# can be a string for `application/x-www-form-urlencoded`
|
201
|
+
body = body.encode("utf-8")
|
202
|
+
|
203
|
+
# these values have `str` type at this point
|
204
|
+
uri = cast(str, prepared.url)
|
205
|
+
method = cast(str, prepared.method)
|
206
|
+
return cls(
|
207
|
+
uri=uri,
|
208
|
+
method=method,
|
209
|
+
headers={key: [value] for (key, value) in prepared.headers.items()},
|
210
|
+
body=body,
|
211
|
+
body_size=len(body) if body is not None else None,
|
212
|
+
)
|
213
|
+
|
214
|
+
@property
|
215
|
+
def encoded_body(self) -> str | None:
|
216
|
+
if self.body is not None:
|
217
|
+
if self._encoded_body_cache is None:
|
218
|
+
self._encoded_body_cache = serialize_payload(self.body)
|
219
|
+
return self._encoded_body_cache
|
220
|
+
return None
|
221
|
+
|
222
|
+
|
223
|
+
@dataclass
|
224
|
+
class Interaction:
|
225
|
+
"""Represents a single interaction with the tested application."""
|
226
|
+
|
227
|
+
request: Request
|
228
|
+
response: Response | None
|
229
|
+
timestamp: float
|
230
|
+
|
231
|
+
__slots__ = ("request", "response", "timestamp")
|
232
|
+
|
233
|
+
def __init__(self, request: Request, response: Response | None) -> None:
|
234
|
+
self.request = request
|
235
|
+
self.response = response
|
236
|
+
self.timestamp = time.time()
|
237
|
+
|
238
|
+
|
239
|
+
@dataclass
|
240
|
+
class FailureData:
|
241
|
+
"""Details about a test failure, including the case and its context."""
|
242
|
+
|
243
|
+
case: Case
|
244
|
+
headers: dict[str, str]
|
245
|
+
verify: bool
|
246
|
+
|
247
|
+
__slots__ = ("case", "headers", "verify")
|
schemathesis/errors.py
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
"""Public Schemathesis errors."""
|
2
|
+
|
3
|
+
from schemathesis.core.errors import (
|
4
|
+
HookError,
|
5
|
+
IncorrectUsage,
|
6
|
+
InternalError,
|
7
|
+
InvalidHeadersExample,
|
8
|
+
InvalidRateLimit,
|
9
|
+
InvalidRegexPattern,
|
10
|
+
InvalidRegexType,
|
11
|
+
InvalidSchema,
|
12
|
+
InvalidStateMachine,
|
13
|
+
InvalidTransition,
|
14
|
+
LoaderError,
|
15
|
+
NoLinksFound,
|
16
|
+
OperationNotFound,
|
17
|
+
SchemathesisError,
|
18
|
+
SerializationError,
|
19
|
+
SerializationNotPossible,
|
20
|
+
TransitionValidationError,
|
21
|
+
UnboundPrefix,
|
22
|
+
)
|
23
|
+
|
24
|
+
__all__ = [
|
25
|
+
"HookError",
|
26
|
+
"IncorrectUsage",
|
27
|
+
"InternalError",
|
28
|
+
"InvalidHeadersExample",
|
29
|
+
"InvalidRateLimit",
|
30
|
+
"InvalidRegexPattern",
|
31
|
+
"InvalidRegexType",
|
32
|
+
"InvalidSchema",
|
33
|
+
"InvalidStateMachine",
|
34
|
+
"InvalidTransition",
|
35
|
+
"LoaderError",
|
36
|
+
"OperationNotFound",
|
37
|
+
"NoLinksFound",
|
38
|
+
"SchemathesisError",
|
39
|
+
"SerializationError",
|
40
|
+
"SerializationNotPossible",
|
41
|
+
"TransitionValidationError",
|
42
|
+
"UnboundPrefix",
|
43
|
+
]
|
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,26 +150,8 @@ class FilterSet:
|
|
151
150
|
def clone(self) -> FilterSet:
|
152
151
|
return FilterSet(_includes=self._includes.copy(), _excludes=self._excludes.copy())
|
153
152
|
|
154
|
-
def
|
155
|
-
|
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
|
-
def apply_to(self, operations: list[APIOperation]) -> list[APIOperation]:
|
172
|
-
"""Get a filtered list of the given operations that match the filters."""
|
173
|
-
return [operation for operation in operations if self.match(SimpleNamespace(operation=operation))]
|
153
|
+
def applies_to(self, operation: APIOperation) -> bool:
|
154
|
+
return self.match(SimpleNamespace(operation=operation))
|
174
155
|
|
175
156
|
def match(self, ctx: HasAPIOperation) -> bool:
|
176
157
|
"""Determines whether the given operation should be included based on the defined filters.
|
@@ -276,7 +257,7 @@ class FilterSet:
|
|
276
257
|
if func is not None:
|
277
258
|
matchers.append(Matcher.for_function(func))
|
278
259
|
for attribute, expected, regex in (
|
279
|
-
("
|
260
|
+
("label", name, name_regex),
|
280
261
|
("method", method, method_regex),
|
281
262
|
("path", path, path_regex),
|
282
263
|
("tag", tag, tag_regex),
|
@@ -284,23 +265,31 @@ class FilterSet:
|
|
284
265
|
):
|
285
266
|
if expected is not None and regex is not None:
|
286
267
|
# To match anything the regex should match the expected value, hence passing them together is useless
|
287
|
-
raise
|
268
|
+
raise IncorrectUsage(ERROR_EXPECTED_AND_REGEX)
|
288
269
|
if expected is not None:
|
270
|
+
if attribute == "method":
|
271
|
+
expected = _normalize_method(expected)
|
289
272
|
matchers.append(Matcher.for_value(attribute, expected))
|
290
273
|
if regex is not None:
|
291
274
|
matchers.append(Matcher.for_regex(attribute, regex))
|
292
275
|
|
293
276
|
if not matchers:
|
294
|
-
raise
|
277
|
+
raise IncorrectUsage(ERROR_EMPTY_FILTER)
|
295
278
|
filter_ = Filter(matchers=tuple(matchers))
|
296
279
|
if filter_ in self._includes or filter_ in self._excludes:
|
297
|
-
raise
|
280
|
+
raise IncorrectUsage(ERROR_FILTER_EXISTS)
|
298
281
|
if include:
|
299
282
|
self._includes.add(filter_)
|
300
283
|
else:
|
301
284
|
self._excludes.add(filter_)
|
302
285
|
|
303
286
|
|
287
|
+
def _normalize_method(value: FilterValue) -> FilterValue:
|
288
|
+
if isinstance(value, list):
|
289
|
+
return [item.upper() for item in value]
|
290
|
+
return value.upper()
|
291
|
+
|
292
|
+
|
304
293
|
def attach_filter_chain(
|
305
294
|
target: Callable,
|
306
295
|
attribute: str,
|
@@ -358,74 +347,6 @@ def is_deprecated(ctx: HasAPIOperation) -> bool:
|
|
358
347
|
return ctx.operation.definition.raw.get("deprecated") is True
|
359
348
|
|
360
349
|
|
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
350
|
def parse_expression(expression: str) -> tuple[str, str, Any]:
|
430
351
|
expression = expression.strip()
|
431
352
|
|
@@ -452,8 +373,6 @@ def parse_expression(expression: str) -> tuple[str, str, Any]:
|
|
452
373
|
|
453
374
|
|
454
375
|
def expression_to_filter_function(expression: str) -> Callable[[HasAPIOperation], bool]:
|
455
|
-
from .specs.openapi.references import resolve_pointer
|
456
|
-
|
457
376
|
pointer, op, value = parse_expression(expression)
|
458
377
|
|
459
378
|
if op == "==":
|
@@ -1,17 +1,13 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import random
|
4
|
-
from dataclasses import dataclass, field
|
5
|
-
from typing import TYPE_CHECKING
|
6
4
|
|
7
|
-
from .
|
8
|
-
from ._methods import DataGenerationMethod, DataGenerationMethodInput
|
5
|
+
from schemathesis.generation.modes import GenerationMode
|
9
6
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
DEFAULT_DATA_GENERATION_METHODS = (DataGenerationMethod.default(),)
|
7
|
+
__all__ = [
|
8
|
+
"GenerationMode",
|
9
|
+
"generate_random_case_id",
|
10
|
+
]
|
15
11
|
|
16
12
|
|
17
13
|
CASE_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
@@ -27,27 +23,3 @@ def generate_random_case_id(length: int = 6) -> str:
|
|
27
23
|
number, rem = divmod(number, BASE)
|
28
24
|
output += CASE_ID_ALPHABET[rem]
|
29
25
|
return output
|
30
|
-
|
31
|
-
|
32
|
-
@dataclass
|
33
|
-
class HeaderConfig:
|
34
|
-
"""Configuration for generating headers."""
|
35
|
-
|
36
|
-
strategy: SearchStrategy[str] | None = None
|
37
|
-
|
38
|
-
|
39
|
-
@dataclass
|
40
|
-
class GenerationConfig:
|
41
|
-
"""Holds various configuration options relevant for data generation."""
|
42
|
-
|
43
|
-
# Allow generating `\x00` bytes in strings
|
44
|
-
allow_x00: bool = True
|
45
|
-
# Allowing using `null` for optional arguments in GraphQL queries
|
46
|
-
graphql_allow_null: bool = True
|
47
|
-
# Generate strings using the given codec
|
48
|
-
codec: str | None = "utf-8"
|
49
|
-
# Whether to generate security parameters
|
50
|
-
with_security_parameters: bool = True
|
51
|
-
# Header generation configuration
|
52
|
-
headers: HeaderConfig = field(default_factory=HeaderConfig)
|
53
|
-
unexpected_methods: set[str] | None = None
|