schemathesis 3.39.7__py3-none-any.whl → 4.0.0a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +27 -65
- schemathesis/auths.py +26 -68
- schemathesis/checks.py +130 -60
- schemathesis/cli/__init__.py +5 -2105
- schemathesis/cli/commands/__init__.py +37 -0
- schemathesis/cli/commands/run/__init__.py +662 -0
- schemathesis/cli/commands/run/checks.py +80 -0
- schemathesis/cli/commands/run/context.py +117 -0
- schemathesis/cli/commands/run/events.py +30 -0
- schemathesis/cli/commands/run/executor.py +141 -0
- schemathesis/cli/commands/run/filters.py +202 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/{cassettes.py → commands/run/handlers/cassettes.py} +178 -247
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +1368 -0
- schemathesis/cli/commands/run/hypothesis.py +105 -0
- schemathesis/cli/commands/run/loaders.py +129 -0
- schemathesis/cli/{callbacks.py → commands/run/validation.py} +59 -175
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +17 -0
- schemathesis/cli/ext/fs.py +14 -0
- schemathesis/cli/ext/groups.py +55 -0
- schemathesis/cli/{options.py → ext/options.py} +37 -16
- schemathesis/cli/hooks.py +36 -0
- schemathesis/contrib/__init__.py +1 -3
- schemathesis/contrib/openapi/__init__.py +1 -3
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -7
- schemathesis/core/__init__.py +58 -0
- schemathesis/core/compat.py +25 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +370 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
- schemathesis/{internal/output.py → core/output/__init__.py} +1 -0
- schemathesis/core/output/sanitization.py +197 -0
- schemathesis/{throttling.py → core/rate_limit.py} +16 -17
- schemathesis/core/registries.py +31 -0
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +108 -0
- schemathesis/core/validation.py +38 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +30 -0
- schemathesis/engine/config.py +59 -0
- schemathesis/engine/context.py +119 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +157 -0
- schemathesis/engine/errors.py +394 -0
- schemathesis/engine/events.py +243 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{runner → engine/phases}/probes.py +49 -68
- schemathesis/engine/phases/stateful/__init__.py +66 -0
- schemathesis/engine/phases/stateful/_executor.py +301 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +175 -0
- schemathesis/engine/phases/unit/_executor.py +322 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +246 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +9 -40
- schemathesis/filters.py +7 -95
- schemathesis/generation/__init__.py +3 -3
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +22 -22
- schemathesis/{_patches.py → generation/hypothesis/__init__.py} +15 -6
- schemathesis/generation/hypothesis/builder.py +585 -0
- schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/modes.py +28 -0
- schemathesis/generation/overrides.py +96 -0
- schemathesis/generation/stateful/__init__.py +20 -0
- schemathesis/{stateful → generation/stateful}/state_machine.py +84 -109
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +17 -62
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +387 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +63 -0
- schemathesis/openapi/loaders.py +178 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +273 -0
- schemathesis/pytest/loaders.py +12 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +94 -107
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +456 -228
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +5 -3
- schemathesis/specs/graphql/schemas.py +122 -123
- schemathesis/specs/graphql/validation.py +11 -17
- schemathesis/specs/openapi/__init__.py +6 -1
- schemathesis/specs/openapi/_cache.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +97 -134
- schemathesis/specs/openapi/checks.py +238 -219
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +22 -20
- schemathesis/specs/openapi/expressions/__init__.py +11 -15
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/nodes.py +33 -32
- schemathesis/specs/openapi/formats.py +3 -2
- schemathesis/specs/openapi/links.py +123 -299
- schemathesis/specs/openapi/media_types.py +10 -12
- schemathesis/specs/openapi/negative/__init__.py +2 -1
- schemathesis/specs/openapi/negative/mutations.py +3 -2
- schemathesis/specs/openapi/parameters.py +8 -6
- schemathesis/specs/openapi/patterns.py +1 -1
- schemathesis/specs/openapi/references.py +11 -51
- schemathesis/specs/openapi/schemas.py +177 -191
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +10 -6
- schemathesis/specs/openapi/stateful/__init__.py +97 -91
- schemathesis/transport/__init__.py +104 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +99 -0
- schemathesis/transport/requests.py +221 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -7
- schemathesis/transport/wsgi.py +165 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +18 -14
- schemathesis-4.0.0a2.dist-info/RECORD +151 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -559
- schemathesis/_override.py +0 -50
- schemathesis/_rate_limiter.py +0 -7
- schemathesis/cli/context.py +0 -75
- schemathesis/cli/debug.py +0 -27
- schemathesis/cli/handlers.py +0 -19
- schemathesis/cli/junitxml.py +0 -124
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -936
- schemathesis/cli/output/short.py +0 -59
- schemathesis/cli/reporting.py +0 -79
- schemathesis/cli/sanitization.py +0 -26
- schemathesis/code_samples.py +0 -151
- schemathesis/constants.py +0 -56
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -16
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -571
- schemathesis/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -277
- schemathesis/fixups/__init__.py +0 -37
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -28
- schemathesis/generation/_methods.py +0 -44
- schemathesis/graphql.py +0 -3
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/checks.py +0 -84
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -38
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- schemathesis/internal/transformation.py +0 -26
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -474
- schemathesis/loaders.py +0 -122
- schemathesis/models.py +0 -1341
- schemathesis/parameters.py +0 -90
- schemathesis/runner/__init__.py +0 -605
- schemathesis/runner/events.py +0 -389
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/context.py +0 -104
- schemathesis/runner/impl/core.py +0 -1246
- schemathesis/runner/impl/solo.py +0 -80
- schemathesis/runner/impl/threadpool.py +0 -391
- schemathesis/runner/serialization.py +0 -544
- schemathesis/sanitization.py +0 -252
- schemathesis/serializers.py +0 -328
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -202
- schemathesis/service/client.py +0 -133
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -61
- schemathesis/service/extensions.py +0 -224
- schemathesis/service/hosts.py +0 -111
- schemathesis/service/metadata.py +0 -71
- schemathesis/service/models.py +0 -258
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -173
- schemathesis/service/usage.py +0 -66
- schemathesis/specs/graphql/loaders.py +0 -364
- schemathesis/specs/openapi/expressions/context.py +0 -16
- schemathesis/specs/openapi/loaders.py +0 -708
- schemathesis/specs/openapi/stateful/statistic.py +0 -198
- schemathesis/specs/openapi/stateful/types.py +0 -14
- schemathesis/specs/openapi/validation.py +0 -26
- schemathesis/stateful/__init__.py +0 -147
- schemathesis/stateful/config.py +0 -97
- schemathesis/stateful/context.py +0 -135
- schemathesis/stateful/events.py +0 -274
- schemathesis/stateful/runner.py +0 -309
- schemathesis/stateful/sink.py +0 -68
- schemathesis/stateful/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -359
- schemathesis/transports/asgi.py +0 -7
- schemathesis/transports/auth.py +0 -38
- schemathesis/transports/headers.py +0 -36
- schemathesis/transports/responses.py +0 -57
- schemathesis/types.py +0 -44
- schemathesis/utils.py +0 -164
- schemathesis-3.39.7.dist-info/RECORD +0 -160
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
- /schemathesis/{internal → core}/result.py +0 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
schemathesis/models.py
DELETED
@@ -1,1341 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import datetime
|
4
|
-
import inspect
|
5
|
-
import textwrap
|
6
|
-
from collections import Counter
|
7
|
-
from contextlib import contextmanager
|
8
|
-
from dataclasses import dataclass, field
|
9
|
-
from enum import Enum
|
10
|
-
from functools import lru_cache, partial
|
11
|
-
from itertools import chain
|
12
|
-
from typing import (
|
13
|
-
TYPE_CHECKING,
|
14
|
-
Any,
|
15
|
-
Callable,
|
16
|
-
Generator,
|
17
|
-
Generic,
|
18
|
-
Iterator,
|
19
|
-
Literal,
|
20
|
-
NoReturn,
|
21
|
-
Sequence,
|
22
|
-
Type,
|
23
|
-
TypeVar,
|
24
|
-
cast,
|
25
|
-
)
|
26
|
-
from urllib.parse import quote, unquote, urljoin, urlsplit, urlunsplit
|
27
|
-
|
28
|
-
from . import serializers
|
29
|
-
from ._dependency_versions import IS_WERKZEUG_ABOVE_3
|
30
|
-
from ._override import CaseOverride
|
31
|
-
from .code_samples import CodeSampleStyle
|
32
|
-
from .constants import (
|
33
|
-
NOT_SET,
|
34
|
-
SCHEMATHESIS_TEST_CASE_HEADER,
|
35
|
-
SERIALIZERS_SUGGESTION_MESSAGE,
|
36
|
-
USER_AGENT,
|
37
|
-
)
|
38
|
-
from .exceptions import (
|
39
|
-
CheckFailed,
|
40
|
-
OperationSchemaError,
|
41
|
-
SerializationNotPossible,
|
42
|
-
SkipTest,
|
43
|
-
UsageError,
|
44
|
-
deduplicate_failed_checks,
|
45
|
-
get_grouped_exception,
|
46
|
-
maybe_set_assertion_message,
|
47
|
-
)
|
48
|
-
from .generation import DataGenerationMethod, GenerationConfig, generate_random_case_id
|
49
|
-
from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, dispatch
|
50
|
-
from .internal.checks import CheckContext
|
51
|
-
from .internal.copy import fast_deepcopy
|
52
|
-
from .internal.deprecation import deprecated_function, deprecated_property
|
53
|
-
from .internal.diff import diff
|
54
|
-
from .internal.output import prepare_response_payload
|
55
|
-
from .parameters import Parameter, ParameterSet, PayloadAlternatives
|
56
|
-
from .sanitization import sanitize_request, sanitize_response
|
57
|
-
from .transports import ASGITransport, RequestsTransport, WSGITransport, deserialize_payload, serialize_payload
|
58
|
-
from .types import Body, Cookies, FormData, Headers, NotSet, PathParameters, Query
|
59
|
-
|
60
|
-
if TYPE_CHECKING:
|
61
|
-
import unittest
|
62
|
-
from logging import LogRecord
|
63
|
-
|
64
|
-
import requests.auth
|
65
|
-
import werkzeug
|
66
|
-
from hypothesis import strategies as st
|
67
|
-
from requests.structures import CaseInsensitiveDict
|
68
|
-
|
69
|
-
from .auths import AuthStorage
|
70
|
-
from .failures import FailureContext
|
71
|
-
from .internal.checks import CheckFunction
|
72
|
-
from .schemas import BaseSchema
|
73
|
-
from .serializers import Serializer
|
74
|
-
from .stateful import Stateful, StatefulTest
|
75
|
-
from .transports.responses import GenericResponse, WSGIResponse
|
76
|
-
|
77
|
-
|
78
|
-
@dataclass
|
79
|
-
class TransitionId:
|
80
|
-
name: str
|
81
|
-
status_code: str
|
82
|
-
|
83
|
-
__slots__ = ("name", "status_code")
|
84
|
-
|
85
|
-
|
86
|
-
@dataclass
|
87
|
-
class CaseSource:
|
88
|
-
"""Data sources, used to generate a test case."""
|
89
|
-
|
90
|
-
case: Case
|
91
|
-
response: GenericResponse
|
92
|
-
elapsed: float
|
93
|
-
overrides_all_parameters: bool
|
94
|
-
transition_id: TransitionId
|
95
|
-
|
96
|
-
def partial_deepcopy(self) -> CaseSource:
|
97
|
-
return self.__class__(
|
98
|
-
case=self.case.partial_deepcopy(),
|
99
|
-
response=self.response,
|
100
|
-
elapsed=self.elapsed,
|
101
|
-
overrides_all_parameters=self.overrides_all_parameters,
|
102
|
-
transition_id=self.transition_id,
|
103
|
-
)
|
104
|
-
|
105
|
-
|
106
|
-
def cant_serialize(media_type: str) -> NoReturn: # type: ignore
|
107
|
-
"""Reject the current example if we don't know how to send this data to the application."""
|
108
|
-
from hypothesis import event, note, reject
|
109
|
-
|
110
|
-
event_text = f"Can't serialize data to `{media_type}`."
|
111
|
-
note(f"{event_text} {SERIALIZERS_SUGGESTION_MESSAGE}")
|
112
|
-
event(event_text)
|
113
|
-
reject() # type: ignore
|
114
|
-
|
115
|
-
|
116
|
-
@lru_cache
|
117
|
-
def get_request_signature() -> inspect.Signature:
|
118
|
-
import requests
|
119
|
-
|
120
|
-
return inspect.signature(requests.Request)
|
121
|
-
|
122
|
-
|
123
|
-
@dataclass()
|
124
|
-
class PreparedRequestData:
|
125
|
-
method: str
|
126
|
-
url: str
|
127
|
-
body: str | bytes | None
|
128
|
-
headers: Headers
|
129
|
-
|
130
|
-
|
131
|
-
def prepare_request_data(kwargs: dict[str, Any]) -> PreparedRequestData:
|
132
|
-
"""Prepare request data for generating code samples."""
|
133
|
-
import requests
|
134
|
-
|
135
|
-
kwargs = {key: value for key, value in kwargs.items() if key in get_request_signature().parameters}
|
136
|
-
request = requests.Request(**kwargs).prepare()
|
137
|
-
return PreparedRequestData(
|
138
|
-
method=str(request.method), url=str(request.url), body=request.body, headers=dict(request.headers)
|
139
|
-
)
|
140
|
-
|
141
|
-
|
142
|
-
class TestPhase(str, Enum):
|
143
|
-
__test__ = False
|
144
|
-
|
145
|
-
EXPLICIT = "explicit"
|
146
|
-
COVERAGE = "coverage"
|
147
|
-
GENERATE = "generate"
|
148
|
-
|
149
|
-
|
150
|
-
@dataclass
|
151
|
-
class GenerationMetadata:
|
152
|
-
"""Stores various information about how data is generated."""
|
153
|
-
|
154
|
-
query: DataGenerationMethod | None
|
155
|
-
path_parameters: DataGenerationMethod | None
|
156
|
-
headers: DataGenerationMethod | None
|
157
|
-
cookies: DataGenerationMethod | None
|
158
|
-
body: DataGenerationMethod | None
|
159
|
-
phase: TestPhase
|
160
|
-
# Temporary attributes to carry info specific to the coverage phase
|
161
|
-
description: str | None
|
162
|
-
location: str | None
|
163
|
-
parameter: str | None
|
164
|
-
parameter_location: str | None
|
165
|
-
|
166
|
-
__slots__ = (
|
167
|
-
"query",
|
168
|
-
"path_parameters",
|
169
|
-
"headers",
|
170
|
-
"cookies",
|
171
|
-
"body",
|
172
|
-
"phase",
|
173
|
-
"description",
|
174
|
-
"location",
|
175
|
-
"parameter",
|
176
|
-
"parameter_location",
|
177
|
-
)
|
178
|
-
|
179
|
-
|
180
|
-
@dataclass(repr=False)
|
181
|
-
class Case:
|
182
|
-
"""A single test case parameters."""
|
183
|
-
|
184
|
-
operation: APIOperation
|
185
|
-
# Time spent on generation of this test case
|
186
|
-
generation_time: float
|
187
|
-
# Unique test case identifier
|
188
|
-
id: str = field(default_factory=generate_random_case_id, compare=False)
|
189
|
-
path_parameters: PathParameters | None = None
|
190
|
-
headers: CaseInsensitiveDict | None = None
|
191
|
-
cookies: Cookies | None = None
|
192
|
-
query: Query | None = None
|
193
|
-
# By default, there is no body, but we can't use `None` as the default value because it clashes with `null`
|
194
|
-
# which is a valid payload.
|
195
|
-
body: Body | NotSet = NOT_SET
|
196
|
-
# The media type for cases with a payload. For example, "application/json"
|
197
|
-
media_type: str | None = None
|
198
|
-
source: CaseSource | None = None
|
199
|
-
|
200
|
-
meta: GenerationMetadata | None = None
|
201
|
-
|
202
|
-
# The way the case was generated (None for manually crafted ones)
|
203
|
-
data_generation_method: DataGenerationMethod | None = None
|
204
|
-
_auth: requests.auth.AuthBase | None = None
|
205
|
-
_has_explicit_auth: bool = False
|
206
|
-
_explicit_method: str | None = None
|
207
|
-
|
208
|
-
def __post_init__(self) -> None:
|
209
|
-
self._original_path_parameters = self.path_parameters.copy() if self.path_parameters else None
|
210
|
-
self._original_headers = self.headers.copy() if self.headers else None
|
211
|
-
self._original_cookies = self.cookies.copy() if self.cookies else None
|
212
|
-
self._original_query = self.query.copy() if self.query else None
|
213
|
-
|
214
|
-
def _has_generated_component(self, name: str) -> bool:
|
215
|
-
assert name in ["path_parameters", "headers", "cookies", "query"]
|
216
|
-
if self.meta is None:
|
217
|
-
return False
|
218
|
-
return getattr(self.meta, name) is not None
|
219
|
-
|
220
|
-
def _get_diff(self, component: Literal["path_parameters", "headers", "query", "cookies"]) -> dict[str, Any]:
|
221
|
-
original = getattr(self, f"_original_{component}")
|
222
|
-
current = getattr(self, component)
|
223
|
-
if not (current and original):
|
224
|
-
return {}
|
225
|
-
original_value = original if self._has_generated_component(component) else {}
|
226
|
-
return diff(original_value, current)
|
227
|
-
|
228
|
-
def __repr__(self) -> str:
|
229
|
-
parts = [f"{self.__class__.__name__}("]
|
230
|
-
first = True
|
231
|
-
for name in ("path_parameters", "headers", "cookies", "query", "body"):
|
232
|
-
value = getattr(self, name)
|
233
|
-
if value is not None and not isinstance(value, NotSet):
|
234
|
-
if first:
|
235
|
-
first = False
|
236
|
-
else:
|
237
|
-
parts.append(", ")
|
238
|
-
parts.extend((name, "=", repr(value)))
|
239
|
-
return "".join(parts) + ")"
|
240
|
-
|
241
|
-
def __hash__(self) -> int:
|
242
|
-
return hash(self.as_curl_command({SCHEMATHESIS_TEST_CASE_HEADER: "0"}))
|
243
|
-
|
244
|
-
@property
|
245
|
-
def _override(self) -> CaseOverride:
|
246
|
-
return CaseOverride(
|
247
|
-
path_parameters=self._get_diff("path_parameters"),
|
248
|
-
headers=self._get_diff("headers"),
|
249
|
-
query=self._get_diff("query"),
|
250
|
-
cookies=self._get_diff("cookies"),
|
251
|
-
)
|
252
|
-
|
253
|
-
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
254
|
-
|
255
|
-
@deprecated_property(removed_in="4.0", replacement="`operation`")
|
256
|
-
def endpoint(self) -> APIOperation:
|
257
|
-
return self.operation
|
258
|
-
|
259
|
-
@property
|
260
|
-
def path(self) -> str:
|
261
|
-
return self.operation.path
|
262
|
-
|
263
|
-
@property
|
264
|
-
def full_path(self) -> str:
|
265
|
-
return self.operation.full_path
|
266
|
-
|
267
|
-
@property
|
268
|
-
def method(self) -> str:
|
269
|
-
return self._explicit_method.upper() if self._explicit_method else self.operation.method.upper()
|
270
|
-
|
271
|
-
@property
|
272
|
-
def base_url(self) -> str | None:
|
273
|
-
return self.operation.base_url
|
274
|
-
|
275
|
-
@property
|
276
|
-
def app(self) -> Any:
|
277
|
-
return self.operation.app
|
278
|
-
|
279
|
-
def set_source(
|
280
|
-
self,
|
281
|
-
response: GenericResponse,
|
282
|
-
case: Case,
|
283
|
-
elapsed: float,
|
284
|
-
overrides_all_parameters: bool,
|
285
|
-
transition_id: TransitionId,
|
286
|
-
) -> None:
|
287
|
-
self.source = CaseSource(
|
288
|
-
case=case,
|
289
|
-
response=response,
|
290
|
-
elapsed=elapsed,
|
291
|
-
overrides_all_parameters=overrides_all_parameters,
|
292
|
-
transition_id=transition_id,
|
293
|
-
)
|
294
|
-
|
295
|
-
@property
|
296
|
-
def formatted_path(self) -> str:
|
297
|
-
try:
|
298
|
-
return self.path.format(**self.path_parameters or {})
|
299
|
-
except KeyError as exc:
|
300
|
-
# This may happen when a path template has a placeholder for variable "X", but parameter "X" is not defined
|
301
|
-
# in the parameters list.
|
302
|
-
# When `exc` is formatted, it is the missing key name in quotes. E.g. 'id'
|
303
|
-
raise OperationSchemaError(f"Path parameter {exc} is not defined") from exc
|
304
|
-
except (IndexError, ValueError) as exc:
|
305
|
-
# A single unmatched `}` inside the path template may cause this
|
306
|
-
raise OperationSchemaError(f"Malformed path template: `{self.path}`\n\n {exc}") from exc
|
307
|
-
|
308
|
-
def get_full_base_url(self) -> str | None:
|
309
|
-
"""Create a full base url, adding "localhost" for WSGI apps."""
|
310
|
-
parts = urlsplit(self.base_url)
|
311
|
-
if not parts.hostname:
|
312
|
-
path = cast(str, parts.path or "")
|
313
|
-
return urlunsplit(("http", "localhost", path or "", "", ""))
|
314
|
-
return self.base_url
|
315
|
-
|
316
|
-
def prepare_code_sample_data(self, headers: dict[str, Any] | None) -> PreparedRequestData:
|
317
|
-
base_url = self.get_full_base_url()
|
318
|
-
kwargs = RequestsTransport().serialize_case(self, base_url=base_url, headers=headers)
|
319
|
-
return prepare_request_data(kwargs)
|
320
|
-
|
321
|
-
def get_code_to_reproduce(
|
322
|
-
self,
|
323
|
-
headers: dict[str, Any] | None = None,
|
324
|
-
request: requests.PreparedRequest | None = None,
|
325
|
-
verify: bool = True,
|
326
|
-
) -> str:
|
327
|
-
"""Construct a Python code to reproduce this case with `requests`."""
|
328
|
-
if request is not None:
|
329
|
-
request_data = prepare_request_data(
|
330
|
-
{
|
331
|
-
"method": request.method,
|
332
|
-
"url": request.url,
|
333
|
-
"headers": request.headers,
|
334
|
-
"data": request.body,
|
335
|
-
}
|
336
|
-
)
|
337
|
-
else:
|
338
|
-
request_data = self.prepare_code_sample_data(headers)
|
339
|
-
return CodeSampleStyle.python.generate(
|
340
|
-
method=request_data.method,
|
341
|
-
url=request_data.url,
|
342
|
-
body=request_data.body,
|
343
|
-
headers=dict(self.headers) if self.headers is not None else None,
|
344
|
-
verify=verify,
|
345
|
-
extra_headers=request_data.headers,
|
346
|
-
)
|
347
|
-
|
348
|
-
def as_curl_command(self, headers: dict[str, Any] | None = None, verify: bool = True) -> str:
|
349
|
-
"""Construct a curl command for a given case."""
|
350
|
-
request_data = self.prepare_code_sample_data(headers)
|
351
|
-
return CodeSampleStyle.curl.generate(
|
352
|
-
method=request_data.method,
|
353
|
-
url=request_data.url,
|
354
|
-
body=request_data.body,
|
355
|
-
headers=dict(self.headers) if self.headers is not None else None,
|
356
|
-
verify=verify,
|
357
|
-
extra_headers=request_data.headers,
|
358
|
-
)
|
359
|
-
|
360
|
-
def _get_base_url(self, base_url: str | None = None) -> str:
|
361
|
-
if base_url is None:
|
362
|
-
if self.base_url is not None:
|
363
|
-
base_url = self.base_url
|
364
|
-
else:
|
365
|
-
raise ValueError(
|
366
|
-
"Base URL is required as `base_url` argument in `call` or should be specified "
|
367
|
-
"in the schema constructor as a part of Schema URL."
|
368
|
-
)
|
369
|
-
return base_url
|
370
|
-
|
371
|
-
def _get_headers(self, headers: dict[str, str] | None = None) -> CaseInsensitiveDict:
|
372
|
-
from requests.structures import CaseInsensitiveDict
|
373
|
-
|
374
|
-
final_headers = self.headers.copy() if self.headers is not None else CaseInsensitiveDict()
|
375
|
-
if headers:
|
376
|
-
final_headers.update(headers)
|
377
|
-
final_headers.setdefault("User-Agent", USER_AGENT)
|
378
|
-
final_headers.setdefault(SCHEMATHESIS_TEST_CASE_HEADER, self.id)
|
379
|
-
return final_headers
|
380
|
-
|
381
|
-
def _get_serializer(self, media_type: str | None = None) -> Serializer | None:
|
382
|
-
"""Get a serializer for the payload, if there is any."""
|
383
|
-
input_media_type = media_type or self.media_type
|
384
|
-
if input_media_type is not None:
|
385
|
-
media_type = serializers.get_first_matching_media_type(input_media_type)
|
386
|
-
if media_type is None:
|
387
|
-
# This media type is set manually. Otherwise, it should have been rejected during the data generation
|
388
|
-
raise SerializationNotPossible.for_media_type(input_media_type)
|
389
|
-
# SAFETY: It is safe to assume that serializer will be found, because `media_type` returned above
|
390
|
-
# is registered. This intentionally ignores cases with concurrent serializers registry modification.
|
391
|
-
cls = cast(Type[serializers.Serializer], serializers.get(media_type))
|
392
|
-
return cls()
|
393
|
-
return None
|
394
|
-
|
395
|
-
def _get_body(self) -> Body | NotSet:
|
396
|
-
return self.body
|
397
|
-
|
398
|
-
@deprecated_function(removed_in="4.0", replacement="Case.as_transport_kwargs")
|
399
|
-
def as_requests_kwargs(self, base_url: str | None = None, headers: dict[str, str] | None = None) -> dict[str, Any]:
|
400
|
-
"""Convert the case into a dictionary acceptable by requests."""
|
401
|
-
return RequestsTransport().serialize_case(self, base_url=base_url, headers=headers)
|
402
|
-
|
403
|
-
def as_transport_kwargs(self, base_url: str | None = None, headers: dict[str, str] | None = None) -> dict[str, Any]:
|
404
|
-
"""Convert the test case into a dictionary acceptable by the underlying transport call."""
|
405
|
-
return self.operation.schema.transport.serialize_case(self, base_url=base_url, headers=headers)
|
406
|
-
|
407
|
-
def call(
|
408
|
-
self,
|
409
|
-
base_url: str | None = None,
|
410
|
-
session: requests.Session | None = None,
|
411
|
-
headers: dict[str, Any] | None = None,
|
412
|
-
params: dict[str, Any] | None = None,
|
413
|
-
cookies: dict[str, Any] | None = None,
|
414
|
-
**kwargs: Any,
|
415
|
-
) -> GenericResponse:
|
416
|
-
hook_context = HookContext(operation=self.operation)
|
417
|
-
dispatch("before_call", hook_context, self)
|
418
|
-
response = self.operation.schema.transport.send(
|
419
|
-
self, session=session, base_url=base_url, headers=headers, params=params, cookies=cookies, **kwargs
|
420
|
-
)
|
421
|
-
dispatch("after_call", hook_context, self, response)
|
422
|
-
return response
|
423
|
-
|
424
|
-
@deprecated_function(removed_in="4.0", replacement="Case.as_transport_kwargs")
|
425
|
-
def as_werkzeug_kwargs(self, headers: dict[str, str] | None = None) -> dict[str, Any]:
|
426
|
-
"""Convert the case into a dictionary acceptable by werkzeug.Client."""
|
427
|
-
return WSGITransport(self.app).serialize_case(self, headers=headers)
|
428
|
-
|
429
|
-
@deprecated_function(removed_in="4.0", replacement="Case.call")
|
430
|
-
def call_wsgi(
|
431
|
-
self,
|
432
|
-
app: Any = None,
|
433
|
-
headers: dict[str, str] | None = None,
|
434
|
-
query_string: dict[str, str] | None = None,
|
435
|
-
**kwargs: Any,
|
436
|
-
) -> WSGIResponse:
|
437
|
-
application = app or self.app
|
438
|
-
if application is None:
|
439
|
-
raise RuntimeError(
|
440
|
-
"WSGI application instance is required. "
|
441
|
-
"Please, set `app` argument in the schema constructor or pass it to `call_wsgi`"
|
442
|
-
)
|
443
|
-
hook_context = HookContext(operation=self.operation)
|
444
|
-
dispatch("before_call", hook_context, self)
|
445
|
-
response = WSGITransport(application).send(self, headers=headers, params=query_string, **kwargs)
|
446
|
-
dispatch("after_call", hook_context, self, response)
|
447
|
-
return response
|
448
|
-
|
449
|
-
@deprecated_function(removed_in="4.0", replacement="Case.call")
|
450
|
-
def call_asgi(
|
451
|
-
self,
|
452
|
-
app: Any = None,
|
453
|
-
base_url: str | None = None,
|
454
|
-
headers: dict[str, str] | None = None,
|
455
|
-
**kwargs: Any,
|
456
|
-
) -> requests.Response:
|
457
|
-
application = app or self.app
|
458
|
-
if application is None:
|
459
|
-
raise RuntimeError(
|
460
|
-
"ASGI application instance is required. "
|
461
|
-
"Please, set `app` argument in the schema constructor or pass it to `call_asgi`"
|
462
|
-
)
|
463
|
-
hook_context = HookContext(operation=self.operation)
|
464
|
-
dispatch("before_call", hook_context, self)
|
465
|
-
response = ASGITransport(application).send(self, base_url=base_url, headers=headers, **kwargs)
|
466
|
-
dispatch("after_call", hook_context, self, response)
|
467
|
-
return response
|
468
|
-
|
469
|
-
def validate_response(
|
470
|
-
self,
|
471
|
-
response: GenericResponse,
|
472
|
-
checks: tuple[CheckFunction, ...] = (),
|
473
|
-
additional_checks: tuple[CheckFunction, ...] = (),
|
474
|
-
excluded_checks: tuple[CheckFunction, ...] = (),
|
475
|
-
code_sample_style: str | None = None,
|
476
|
-
headers: dict[str, Any] | None = None,
|
477
|
-
transport_kwargs: dict[str, Any] | None = None,
|
478
|
-
) -> None:
|
479
|
-
"""Validate application response.
|
480
|
-
|
481
|
-
By default, all available checks will be applied.
|
482
|
-
|
483
|
-
:param response: Application response.
|
484
|
-
:param checks: A tuple of check functions that accept ``response`` and ``case``.
|
485
|
-
:param additional_checks: A tuple of additional checks that will be executed after ones from the ``checks``
|
486
|
-
argument.
|
487
|
-
:param excluded_checks: Checks excluded from the default ones.
|
488
|
-
:param code_sample_style: Controls the style of code samples for failure reproduction.
|
489
|
-
"""
|
490
|
-
__tracebackhide__ = True
|
491
|
-
from requests.structures import CaseInsensitiveDict
|
492
|
-
|
493
|
-
from .checks import ALL_CHECKS
|
494
|
-
from .internal.checks import wrap_check
|
495
|
-
from .transports.responses import get_payload, get_reason
|
496
|
-
|
497
|
-
if checks:
|
498
|
-
_checks = tuple(wrap_check(check) for check in checks)
|
499
|
-
else:
|
500
|
-
_checks = checks
|
501
|
-
if additional_checks:
|
502
|
-
_additional_checks = tuple(wrap_check(check) for check in additional_checks)
|
503
|
-
else:
|
504
|
-
_additional_checks = additional_checks
|
505
|
-
|
506
|
-
checks = _checks or ALL_CHECKS
|
507
|
-
checks = tuple(check for check in checks if check not in excluded_checks)
|
508
|
-
additional_checks = tuple(check for check in _additional_checks if check not in excluded_checks)
|
509
|
-
failed_checks = []
|
510
|
-
ctx = CheckContext(
|
511
|
-
override=self._override,
|
512
|
-
auth=None,
|
513
|
-
headers=CaseInsensitiveDict(headers) if headers else None,
|
514
|
-
transport_kwargs=transport_kwargs,
|
515
|
-
)
|
516
|
-
for check in chain(checks, additional_checks):
|
517
|
-
copied_case = self.partial_deepcopy()
|
518
|
-
try:
|
519
|
-
check(ctx, response, copied_case)
|
520
|
-
except AssertionError as exc:
|
521
|
-
maybe_set_assertion_message(exc, check.__name__)
|
522
|
-
failed_checks.append(exc)
|
523
|
-
failed_checks = list(deduplicate_failed_checks(failed_checks))
|
524
|
-
if failed_checks:
|
525
|
-
exception_cls = get_grouped_exception(self.operation.verbose_name, *failed_checks)
|
526
|
-
formatted = ""
|
527
|
-
for idx, failed in enumerate(failed_checks, 1):
|
528
|
-
if isinstance(failed, CheckFailed) and failed.context is not None:
|
529
|
-
title = failed.context.title
|
530
|
-
if failed.context.message:
|
531
|
-
message = failed.context.message
|
532
|
-
else:
|
533
|
-
message = None
|
534
|
-
else:
|
535
|
-
title, message = failed.args
|
536
|
-
formatted += "\n\n"
|
537
|
-
formatted += f"{idx}. {title}"
|
538
|
-
if message is not None:
|
539
|
-
formatted += "\n\n"
|
540
|
-
formatted += textwrap.indent(message, prefix=" ")
|
541
|
-
|
542
|
-
status_code = response.status_code
|
543
|
-
reason = get_reason(status_code)
|
544
|
-
formatted += f"\n\n[{response.status_code}] {reason}:"
|
545
|
-
payload = get_payload(response)
|
546
|
-
if not payload:
|
547
|
-
formatted += "\n\n <EMPTY>"
|
548
|
-
else:
|
549
|
-
payload = prepare_response_payload(payload, config=self.operation.schema.output_config)
|
550
|
-
payload = textwrap.indent(f"\n`{payload}`", prefix=" ")
|
551
|
-
formatted += f"\n{payload}"
|
552
|
-
code_sample_style = (
|
553
|
-
CodeSampleStyle.from_str(code_sample_style)
|
554
|
-
if code_sample_style is not None
|
555
|
-
else self.operation.schema.code_sample_style
|
556
|
-
)
|
557
|
-
verify = getattr(response, "verify", True)
|
558
|
-
if self.operation.schema.sanitize_output:
|
559
|
-
sanitize_request(response.request)
|
560
|
-
sanitize_response(response)
|
561
|
-
code_message = self._get_code_message(code_sample_style, response.request, verify=verify)
|
562
|
-
raise exception_cls(
|
563
|
-
f"{formatted}\n\n" f"{code_message}",
|
564
|
-
causes=tuple(failed_checks),
|
565
|
-
)
|
566
|
-
|
567
|
-
def _get_code_message(
|
568
|
-
self, code_sample_style: CodeSampleStyle, request: requests.PreparedRequest, verify: bool
|
569
|
-
) -> str:
|
570
|
-
if code_sample_style == CodeSampleStyle.python:
|
571
|
-
code = self.get_code_to_reproduce(request=request, verify=verify)
|
572
|
-
elif code_sample_style == CodeSampleStyle.curl:
|
573
|
-
code = self.as_curl_command(headers=dict(request.headers), verify=verify)
|
574
|
-
else:
|
575
|
-
raise ValueError(f"Unknown code sample style: {code_sample_style.name}")
|
576
|
-
return f"Reproduce with: \n\n {code}\n"
|
577
|
-
|
578
|
-
def call_and_validate(
|
579
|
-
self,
|
580
|
-
base_url: str | None = None,
|
581
|
-
session: requests.Session | None = None,
|
582
|
-
headers: dict[str, Any] | None = None,
|
583
|
-
checks: tuple[CheckFunction, ...] = (),
|
584
|
-
additional_checks: tuple[CheckFunction, ...] = (),
|
585
|
-
excluded_checks: tuple[CheckFunction, ...] = (),
|
586
|
-
code_sample_style: str | None = None,
|
587
|
-
**kwargs: Any,
|
588
|
-
) -> requests.Response:
|
589
|
-
__tracebackhide__ = True
|
590
|
-
response = self.call(base_url, session, headers, **kwargs)
|
591
|
-
self.validate_response(
|
592
|
-
response,
|
593
|
-
checks,
|
594
|
-
code_sample_style=code_sample_style,
|
595
|
-
headers=headers,
|
596
|
-
additional_checks=additional_checks,
|
597
|
-
excluded_checks=excluded_checks,
|
598
|
-
transport_kwargs=kwargs,
|
599
|
-
)
|
600
|
-
return response
|
601
|
-
|
602
|
-
def _get_url(self, base_url: str | None) -> str:
|
603
|
-
base_url = self._get_base_url(base_url)
|
604
|
-
formatted_path = self.formatted_path.lstrip("/")
|
605
|
-
if not base_url.endswith("/"):
|
606
|
-
base_url += "/"
|
607
|
-
return unquote(urljoin(base_url, quote(formatted_path)))
|
608
|
-
|
609
|
-
def get_full_url(self) -> str:
|
610
|
-
"""Make a full URL to the current API operation, including query parameters."""
|
611
|
-
import requests
|
612
|
-
|
613
|
-
base_url = self.base_url or "http://127.0.0.1"
|
614
|
-
kwargs = RequestsTransport().serialize_case(self, base_url=base_url)
|
615
|
-
request = requests.Request(**kwargs)
|
616
|
-
prepared = requests.Session().prepare_request(request) # type: ignore
|
617
|
-
return cast(str, prepared.url)
|
618
|
-
|
619
|
-
def partial_deepcopy(self) -> Case:
|
620
|
-
return self.__class__(
|
621
|
-
operation=self.operation.partial_deepcopy(),
|
622
|
-
data_generation_method=self.data_generation_method,
|
623
|
-
media_type=self.media_type,
|
624
|
-
source=self.source if self.source is None else self.source.partial_deepcopy(),
|
625
|
-
path_parameters=fast_deepcopy(self.path_parameters),
|
626
|
-
headers=fast_deepcopy(self.headers),
|
627
|
-
cookies=fast_deepcopy(self.cookies),
|
628
|
-
query=fast_deepcopy(self.query),
|
629
|
-
body=fast_deepcopy(self.body),
|
630
|
-
meta=self.meta,
|
631
|
-
generation_time=self.generation_time,
|
632
|
-
id=self.id,
|
633
|
-
_auth=self._auth,
|
634
|
-
_has_explicit_auth=self._has_explicit_auth,
|
635
|
-
_explicit_method=self._explicit_method,
|
636
|
-
)
|
637
|
-
|
638
|
-
|
639
|
-
@contextmanager
|
640
|
-
def cookie_handler(client: werkzeug.Client, cookies: Cookies | None) -> Generator[None, None, None]:
|
641
|
-
"""Set cookies required for a call."""
|
642
|
-
if not cookies:
|
643
|
-
yield
|
644
|
-
else:
|
645
|
-
for key, value in cookies.items():
|
646
|
-
if IS_WERKZEUG_ABOVE_3:
|
647
|
-
client.set_cookie(key=key, value=value, domain="localhost")
|
648
|
-
else:
|
649
|
-
client.set_cookie("localhost", key=key, value=value)
|
650
|
-
yield
|
651
|
-
for key in cookies:
|
652
|
-
if IS_WERKZEUG_ABOVE_3:
|
653
|
-
client.delete_cookie(key=key, domain="localhost")
|
654
|
-
else:
|
655
|
-
client.delete_cookie("localhost", key=key)
|
656
|
-
|
657
|
-
|
658
|
-
P = TypeVar("P", bound=Parameter)
|
659
|
-
D = TypeVar("D", bound=dict)
|
660
|
-
|
661
|
-
|
662
|
-
@dataclass
|
663
|
-
class OperationDefinition(Generic[D]):
|
664
|
-
"""A wrapper to store not resolved API operation definitions.
|
665
|
-
|
666
|
-
To prevent recursion errors we need to store definitions without resolving references. But operation definitions
|
667
|
-
itself can be behind a reference (when there is a ``$ref`` in ``paths`` values), therefore we need to store this
|
668
|
-
scope change to have a proper reference resolving later.
|
669
|
-
"""
|
670
|
-
|
671
|
-
raw: D
|
672
|
-
resolved: D
|
673
|
-
scope: str
|
674
|
-
|
675
|
-
__slots__ = ("raw", "resolved", "scope")
|
676
|
-
|
677
|
-
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
678
|
-
|
679
|
-
|
680
|
-
C = TypeVar("C", bound=Case)
|
681
|
-
|
682
|
-
|
683
|
-
@dataclass(eq=False)
|
684
|
-
class APIOperation(Generic[P, C]):
|
685
|
-
"""A single operation defined in an API.
|
686
|
-
|
687
|
-
You can get one via a ``schema`` instance.
|
688
|
-
|
689
|
-
.. code-block:: python
|
690
|
-
|
691
|
-
# Get the POST /items operation
|
692
|
-
operation = schema["/items"]["POST"]
|
693
|
-
|
694
|
-
"""
|
695
|
-
|
696
|
-
# `path` does not contain `basePath`
|
697
|
-
# Example <scheme>://<host>/<basePath>/users - "/users" is path
|
698
|
-
# https://swagger.io/docs/specification/2-0/api-host-and-base-path/
|
699
|
-
path: str
|
700
|
-
method: str
|
701
|
-
definition: OperationDefinition = field(repr=False)
|
702
|
-
schema: BaseSchema
|
703
|
-
verbose_name: str = None # type: ignore
|
704
|
-
app: Any = None
|
705
|
-
base_url: str | None = None
|
706
|
-
path_parameters: ParameterSet[P] = field(default_factory=ParameterSet)
|
707
|
-
headers: ParameterSet[P] = field(default_factory=ParameterSet)
|
708
|
-
cookies: ParameterSet[P] = field(default_factory=ParameterSet)
|
709
|
-
query: ParameterSet[P] = field(default_factory=ParameterSet)
|
710
|
-
body: PayloadAlternatives[P] = field(default_factory=PayloadAlternatives)
|
711
|
-
case_cls: type[C] = Case # type: ignore
|
712
|
-
|
713
|
-
def __post_init__(self) -> None:
|
714
|
-
if self.verbose_name is None:
|
715
|
-
self.verbose_name = f"{self.method.upper()} {self.full_path}" # type: ignore
|
716
|
-
|
717
|
-
@property
|
718
|
-
def full_path(self) -> str:
|
719
|
-
return self.schema.get_full_path(self.path)
|
720
|
-
|
721
|
-
@property
|
722
|
-
def links(self) -> dict[str, dict[str, Any]]:
|
723
|
-
return self.schema.get_links(self)
|
724
|
-
|
725
|
-
@property
|
726
|
-
def tags(self) -> list[str] | None:
|
727
|
-
return self.schema.get_tags(self)
|
728
|
-
|
729
|
-
def iter_parameters(self) -> Iterator[P]:
|
730
|
-
"""Iterate over all operation's parameters."""
|
731
|
-
return chain(self.path_parameters, self.headers, self.cookies, self.query)
|
732
|
-
|
733
|
-
def _lookup_container(self, location: str) -> ParameterSet[P] | PayloadAlternatives[P] | None:
|
734
|
-
return {
|
735
|
-
"path": self.path_parameters,
|
736
|
-
"header": self.headers,
|
737
|
-
"cookie": self.cookies,
|
738
|
-
"query": self.query,
|
739
|
-
"body": self.body,
|
740
|
-
}.get(location)
|
741
|
-
|
742
|
-
def add_parameter(self, parameter: P) -> None:
|
743
|
-
"""Add a new processed parameter to an API operation.
|
744
|
-
|
745
|
-
:param parameter: A parameter that will be used with this operation.
|
746
|
-
:rtype: None
|
747
|
-
"""
|
748
|
-
# If the parameter has a typo, then by default, there will be an error from `jsonschema` earlier.
|
749
|
-
# But if the user wants to skip schema validation, we choose to ignore a malformed parameter.
|
750
|
-
# In this case, we still might generate some tests for an API operation, but without this parameter,
|
751
|
-
# which is better than skip the whole operation from testing.
|
752
|
-
container = self._lookup_container(parameter.location)
|
753
|
-
if container is not None:
|
754
|
-
container.add(parameter)
|
755
|
-
|
756
|
-
def get_parameter(self, name: str, location: str) -> P | None:
|
757
|
-
container = self._lookup_container(location)
|
758
|
-
if container is not None:
|
759
|
-
return container.get(name)
|
760
|
-
return None
|
761
|
-
|
762
|
-
def as_strategy(
|
763
|
-
self,
|
764
|
-
hooks: HookDispatcher | None = None,
|
765
|
-
auth_storage: AuthStorage | None = None,
|
766
|
-
data_generation_method: DataGenerationMethod = DataGenerationMethod.default(),
|
767
|
-
generation_config: GenerationConfig | None = None,
|
768
|
-
**kwargs: Any,
|
769
|
-
) -> st.SearchStrategy:
|
770
|
-
"""Turn this API operation into a Hypothesis strategy."""
|
771
|
-
strategy = self.schema.get_case_strategy(
|
772
|
-
self, hooks, auth_storage, data_generation_method, generation_config=generation_config, **kwargs
|
773
|
-
)
|
774
|
-
|
775
|
-
def _apply_hooks(dispatcher: HookDispatcher, _strategy: st.SearchStrategy[Case]) -> st.SearchStrategy[Case]:
|
776
|
-
context = HookContext(self)
|
777
|
-
for hook in dispatcher.get_all_by_name("before_generate_case"):
|
778
|
-
_strategy = hook(context, _strategy)
|
779
|
-
for hook in dispatcher.get_all_by_name("filter_case"):
|
780
|
-
hook = partial(hook, context)
|
781
|
-
_strategy = _strategy.filter(hook)
|
782
|
-
for hook in dispatcher.get_all_by_name("map_case"):
|
783
|
-
hook = partial(hook, context)
|
784
|
-
_strategy = _strategy.map(hook)
|
785
|
-
for hook in dispatcher.get_all_by_name("flatmap_case"):
|
786
|
-
hook = partial(hook, context)
|
787
|
-
_strategy = _strategy.flatmap(hook)
|
788
|
-
return _strategy
|
789
|
-
|
790
|
-
strategy = _apply_hooks(GLOBAL_HOOK_DISPATCHER, strategy)
|
791
|
-
strategy = _apply_hooks(self.schema.hooks, strategy)
|
792
|
-
if hooks is not None:
|
793
|
-
strategy = _apply_hooks(hooks, strategy)
|
794
|
-
return strategy
|
795
|
-
|
796
|
-
def get_security_requirements(self) -> list[str]:
|
797
|
-
return self.schema.get_security_requirements(self)
|
798
|
-
|
799
|
-
def get_strategies_from_examples(
|
800
|
-
self, as_strategy_kwargs: dict[str, Any] | None = None
|
801
|
-
) -> list[st.SearchStrategy[Case]]:
|
802
|
-
"""Get examples from the API operation."""
|
803
|
-
return self.schema.get_strategies_from_examples(self, as_strategy_kwargs=as_strategy_kwargs)
|
804
|
-
|
805
|
-
def get_stateful_tests(self, response: GenericResponse, stateful: Stateful | None) -> Sequence[StatefulTest]:
|
806
|
-
return self.schema.get_stateful_tests(response, self, stateful)
|
807
|
-
|
808
|
-
def get_parameter_serializer(self, location: str) -> Callable | None:
|
809
|
-
"""Get a function that serializes parameters for the given location.
|
810
|
-
|
811
|
-
It handles serializing data into various `collectionFormat` options and similar.
|
812
|
-
Note that payload is handled by this function - it is handled by serializers.
|
813
|
-
"""
|
814
|
-
return self.schema.get_parameter_serializer(self, location)
|
815
|
-
|
816
|
-
def prepare_multipart(self, form_data: FormData) -> tuple[list | None, dict[str, Any] | None]:
|
817
|
-
return self.schema.prepare_multipart(form_data, self)
|
818
|
-
|
819
|
-
def get_request_payload_content_types(self) -> list[str]:
|
820
|
-
return self.schema.get_request_payload_content_types(self)
|
821
|
-
|
822
|
-
def _get_default_media_type(self) -> str:
|
823
|
-
# If the user wants to send payload, then there should be a media type, otherwise the payload is ignored
|
824
|
-
media_types = self.get_request_payload_content_types()
|
825
|
-
if len(media_types) == 1:
|
826
|
-
# The only available option
|
827
|
-
return media_types[0]
|
828
|
-
media_types_repr = ", ".join(media_types)
|
829
|
-
raise UsageError(
|
830
|
-
"Can not detect appropriate media type. "
|
831
|
-
"You can either specify one of the defined media types "
|
832
|
-
f"or pass any other media type available for serialization. Defined media types: {media_types_repr}"
|
833
|
-
)
|
834
|
-
|
835
|
-
def partial_deepcopy(self) -> APIOperation:
|
836
|
-
return self.__class__(
|
837
|
-
path=self.path, # string, immutable
|
838
|
-
method=self.method, # string, immutable
|
839
|
-
definition=fast_deepcopy(self.definition),
|
840
|
-
schema=self.schema.clone(), # shallow copy
|
841
|
-
verbose_name=self.verbose_name, # string, immutable
|
842
|
-
app=self.app, # not deepcopyable
|
843
|
-
base_url=self.base_url, # string, immutable
|
844
|
-
path_parameters=fast_deepcopy(self.path_parameters),
|
845
|
-
headers=fast_deepcopy(self.headers),
|
846
|
-
cookies=fast_deepcopy(self.cookies),
|
847
|
-
query=fast_deepcopy(self.query),
|
848
|
-
body=fast_deepcopy(self.body),
|
849
|
-
)
|
850
|
-
|
851
|
-
def clone(self, **components: Any) -> APIOperation:
|
852
|
-
"""Create a new instance of this API operation with updated components."""
|
853
|
-
return self.__class__(
|
854
|
-
path=self.path,
|
855
|
-
method=self.method,
|
856
|
-
verbose_name=self.verbose_name,
|
857
|
-
definition=self.definition,
|
858
|
-
schema=self.schema,
|
859
|
-
app=self.app,
|
860
|
-
base_url=self.base_url,
|
861
|
-
path_parameters=components["path_parameters"],
|
862
|
-
query=components["query"],
|
863
|
-
headers=components["headers"],
|
864
|
-
cookies=components["cookies"],
|
865
|
-
body=components["body"],
|
866
|
-
)
|
867
|
-
|
868
|
-
def make_case(
|
869
|
-
self,
|
870
|
-
*,
|
871
|
-
path_parameters: PathParameters | None = None,
|
872
|
-
headers: Headers | None = None,
|
873
|
-
cookies: Cookies | None = None,
|
874
|
-
query: Query | None = None,
|
875
|
-
body: Body | NotSet = NOT_SET,
|
876
|
-
media_type: str | None = None,
|
877
|
-
) -> C:
|
878
|
-
"""Create a new example for this API operation.
|
879
|
-
|
880
|
-
The main use case is constructing Case instances completely manually, without data generation.
|
881
|
-
"""
|
882
|
-
return self.schema.make_case(
|
883
|
-
case_cls=self.case_cls,
|
884
|
-
operation=self,
|
885
|
-
path_parameters=path_parameters,
|
886
|
-
headers=headers,
|
887
|
-
cookies=cookies,
|
888
|
-
query=query,
|
889
|
-
body=body,
|
890
|
-
media_type=media_type,
|
891
|
-
)
|
892
|
-
|
893
|
-
@property
|
894
|
-
def operation_reference(self) -> str:
|
895
|
-
path = self.path.replace("~", "~0").replace("/", "~1")
|
896
|
-
return f"#/paths/{path}/{self.method}"
|
897
|
-
|
898
|
-
def validate_response(self, response: GenericResponse) -> bool | None:
|
899
|
-
"""Validate API response for conformance.
|
900
|
-
|
901
|
-
:raises CheckFailed: If the response does not conform to the API schema.
|
902
|
-
"""
|
903
|
-
return self.schema.validate_response(self, response)
|
904
|
-
|
905
|
-
def is_response_valid(self, response: GenericResponse) -> bool:
|
906
|
-
"""Validate API response for conformance."""
|
907
|
-
try:
|
908
|
-
self.validate_response(response)
|
909
|
-
return True
|
910
|
-
except CheckFailed:
|
911
|
-
return False
|
912
|
-
|
913
|
-
def get_raw_payload_schema(self, media_type: str) -> dict[str, Any] | None:
|
914
|
-
return self.schema._get_payload_schema(self.definition.raw, media_type)
|
915
|
-
|
916
|
-
def get_resolved_payload_schema(self, media_type: str) -> dict[str, Any] | None:
|
917
|
-
return self.schema._get_payload_schema(self.definition.resolved, media_type)
|
918
|
-
|
919
|
-
|
920
|
-
# backward-compatibility
|
921
|
-
Endpoint = APIOperation
|
922
|
-
|
923
|
-
|
924
|
-
class Status(str, Enum):
|
925
|
-
"""Status of an action or multiple actions."""
|
926
|
-
|
927
|
-
success = "success"
|
928
|
-
failure = "failure"
|
929
|
-
error = "error"
|
930
|
-
skip = "skip"
|
931
|
-
|
932
|
-
|
933
|
-
@dataclass(repr=False)
|
934
|
-
class Check:
|
935
|
-
"""Single check run result."""
|
936
|
-
|
937
|
-
name: str
|
938
|
-
value: Status
|
939
|
-
response: GenericResponse | None
|
940
|
-
elapsed: float
|
941
|
-
example: Case
|
942
|
-
message: str | None = None
|
943
|
-
# Failure-specific context
|
944
|
-
context: FailureContext | None = None
|
945
|
-
request: requests.PreparedRequest | None = None
|
946
|
-
|
947
|
-
|
948
|
-
@dataclass(repr=False)
|
949
|
-
class Request:
|
950
|
-
"""Request data extracted from `Case`."""
|
951
|
-
|
952
|
-
method: str
|
953
|
-
uri: str
|
954
|
-
body: str | None
|
955
|
-
body_size: int | None
|
956
|
-
headers: Headers
|
957
|
-
|
958
|
-
@classmethod
|
959
|
-
def from_case(cls, case: Case, session: requests.Session) -> Request:
|
960
|
-
"""Create a new `Request` instance from `Case`."""
|
961
|
-
import requests
|
962
|
-
|
963
|
-
base_url = case.get_full_base_url()
|
964
|
-
kwargs = RequestsTransport().serialize_case(case, base_url=base_url)
|
965
|
-
request = requests.Request(**kwargs)
|
966
|
-
prepared = session.prepare_request(request) # type: ignore
|
967
|
-
return cls.from_prepared_request(prepared)
|
968
|
-
|
969
|
-
@classmethod
|
970
|
-
def from_prepared_request(cls, prepared: requests.PreparedRequest) -> Request:
|
971
|
-
"""A prepared request version is already stored in `requests.Response`."""
|
972
|
-
body = prepared.body
|
973
|
-
|
974
|
-
if isinstance(body, str):
|
975
|
-
# can be a string for `application/x-www-form-urlencoded`
|
976
|
-
body = body.encode("utf-8")
|
977
|
-
|
978
|
-
# these values have `str` type at this point
|
979
|
-
uri = cast(str, prepared.url)
|
980
|
-
method = cast(str, prepared.method)
|
981
|
-
return cls(
|
982
|
-
uri=uri,
|
983
|
-
method=method,
|
984
|
-
headers={key: [value] for (key, value) in prepared.headers.items()},
|
985
|
-
body=serialize_payload(body) if body is not None else body,
|
986
|
-
body_size=len(body) if body is not None else None,
|
987
|
-
)
|
988
|
-
|
989
|
-
def deserialize_body(self) -> bytes | None:
|
990
|
-
"""Deserialize the request body.
|
991
|
-
|
992
|
-
`Request` should be serializable to JSON, therefore body is encoded as base64 string
|
993
|
-
to support arbitrary binary data.
|
994
|
-
"""
|
995
|
-
return deserialize_payload(self.body)
|
996
|
-
|
997
|
-
|
998
|
-
@dataclass(repr=False)
|
999
|
-
class Response:
|
1000
|
-
"""Unified response data."""
|
1001
|
-
|
1002
|
-
status_code: int
|
1003
|
-
message: str
|
1004
|
-
headers: dict[str, list[str]]
|
1005
|
-
body: str | None
|
1006
|
-
body_size: int | None
|
1007
|
-
encoding: str | None
|
1008
|
-
http_version: str
|
1009
|
-
elapsed: float
|
1010
|
-
verify: bool
|
1011
|
-
|
1012
|
-
@classmethod
|
1013
|
-
def from_requests(cls, response: requests.Response) -> Response:
|
1014
|
-
"""Create a response from requests.Response."""
|
1015
|
-
raw = response.raw
|
1016
|
-
raw_headers = raw.headers if raw is not None else {}
|
1017
|
-
headers = {name: response.raw.headers.getlist(name) for name in raw_headers.keys()}
|
1018
|
-
# Similar to http.client:319 (HTTP version detection in stdlib's `http` package)
|
1019
|
-
version = raw.version if raw is not None else 10
|
1020
|
-
http_version = "1.0" if version == 10 else "1.1"
|
1021
|
-
|
1022
|
-
def is_empty(_response: requests.Response) -> bool:
|
1023
|
-
# Assume the response is empty if:
|
1024
|
-
# - no `Content-Length` header
|
1025
|
-
# - no chunks when iterating over its content
|
1026
|
-
return "Content-Length" not in headers and list(_response.iter_content()) == []
|
1027
|
-
|
1028
|
-
body = None if is_empty(response) else serialize_payload(response.content)
|
1029
|
-
return cls(
|
1030
|
-
status_code=response.status_code,
|
1031
|
-
message=response.reason,
|
1032
|
-
body=body,
|
1033
|
-
body_size=len(response.content) if body is not None else None,
|
1034
|
-
encoding=response.encoding,
|
1035
|
-
headers=headers,
|
1036
|
-
http_version=http_version,
|
1037
|
-
elapsed=response.elapsed.total_seconds(),
|
1038
|
-
verify=getattr(response, "verify", True),
|
1039
|
-
)
|
1040
|
-
|
1041
|
-
@classmethod
|
1042
|
-
def from_wsgi(cls, response: WSGIResponse, elapsed: float) -> Response:
|
1043
|
-
"""Create a response from WSGI response."""
|
1044
|
-
from .transports.responses import get_reason
|
1045
|
-
|
1046
|
-
message = get_reason(response.status_code)
|
1047
|
-
headers = {name: response.headers.getlist(name) for name in response.headers.keys()}
|
1048
|
-
# Note, this call ensures that `response.response` is a sequence, which is needed for comparison
|
1049
|
-
data = response.get_data()
|
1050
|
-
body = None if response.response == [] else serialize_payload(data)
|
1051
|
-
encoding: str | None
|
1052
|
-
if body is not None:
|
1053
|
-
# Werkzeug <3.0 had `charset` attr, newer versions always have UTF-8
|
1054
|
-
encoding = response.mimetype_params.get("charset", getattr(response, "charset", "utf-8"))
|
1055
|
-
else:
|
1056
|
-
encoding = None
|
1057
|
-
return cls(
|
1058
|
-
status_code=response.status_code,
|
1059
|
-
message=message,
|
1060
|
-
body=body,
|
1061
|
-
body_size=len(data) if body is not None else None,
|
1062
|
-
encoding=encoding,
|
1063
|
-
headers=headers,
|
1064
|
-
http_version="1.1",
|
1065
|
-
elapsed=elapsed,
|
1066
|
-
verify=True,
|
1067
|
-
)
|
1068
|
-
|
1069
|
-
def deserialize_body(self) -> bytes | None:
|
1070
|
-
"""Deserialize the response body.
|
1071
|
-
|
1072
|
-
`Response` should be serializable to JSON, therefore body is encoded as base64 string
|
1073
|
-
to support arbitrary binary data.
|
1074
|
-
"""
|
1075
|
-
return deserialize_payload(self.body)
|
1076
|
-
|
1077
|
-
|
1078
|
-
TIMEZONE = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo
|
1079
|
-
|
1080
|
-
|
1081
|
-
@dataclass
|
1082
|
-
class Interaction:
|
1083
|
-
"""A single interaction with the target app."""
|
1084
|
-
|
1085
|
-
request: Request
|
1086
|
-
response: Response | None
|
1087
|
-
checks: list[Check]
|
1088
|
-
status: Status
|
1089
|
-
data_generation_method: DataGenerationMethod
|
1090
|
-
phase: TestPhase | None
|
1091
|
-
# `description` & `location` are related to metadata about this interaction
|
1092
|
-
# NOTE: It will be better to keep it in a separate attribute
|
1093
|
-
description: str | None
|
1094
|
-
location: str | None
|
1095
|
-
parameter: str | None
|
1096
|
-
parameter_location: str | None
|
1097
|
-
recorded_at: str = field(default_factory=lambda: datetime.datetime.now(TIMEZONE).isoformat())
|
1098
|
-
|
1099
|
-
@classmethod
|
1100
|
-
def from_requests(
|
1101
|
-
cls,
|
1102
|
-
case: Case,
|
1103
|
-
response: requests.Response | None,
|
1104
|
-
status: Status,
|
1105
|
-
checks: list[Check],
|
1106
|
-
headers: dict[str, Any] | None,
|
1107
|
-
session: requests.Session | None,
|
1108
|
-
) -> Interaction:
|
1109
|
-
if response is not None:
|
1110
|
-
prepared = response.request
|
1111
|
-
request = Request.from_prepared_request(prepared)
|
1112
|
-
else:
|
1113
|
-
import requests
|
1114
|
-
|
1115
|
-
if session is None:
|
1116
|
-
session = requests.Session()
|
1117
|
-
session.headers.update(headers or {})
|
1118
|
-
request = Request.from_case(case, session)
|
1119
|
-
return cls(
|
1120
|
-
request=request,
|
1121
|
-
response=Response.from_requests(response) if response is not None else None,
|
1122
|
-
status=status,
|
1123
|
-
checks=checks,
|
1124
|
-
data_generation_method=cast(DataGenerationMethod, case.data_generation_method),
|
1125
|
-
phase=case.meta.phase if case.meta is not None else None,
|
1126
|
-
description=case.meta.description if case.meta is not None else None,
|
1127
|
-
location=case.meta.location if case.meta is not None else None,
|
1128
|
-
parameter=case.meta.parameter if case.meta is not None else None,
|
1129
|
-
parameter_location=case.meta.parameter_location if case.meta is not None else None,
|
1130
|
-
)
|
1131
|
-
|
1132
|
-
@classmethod
|
1133
|
-
def from_wsgi(
|
1134
|
-
cls,
|
1135
|
-
case: Case,
|
1136
|
-
response: WSGIResponse | None,
|
1137
|
-
headers: dict[str, Any],
|
1138
|
-
elapsed: float | None,
|
1139
|
-
status: Status,
|
1140
|
-
checks: list[Check],
|
1141
|
-
) -> Interaction:
|
1142
|
-
import requests
|
1143
|
-
|
1144
|
-
session = requests.Session()
|
1145
|
-
session.headers.update(headers)
|
1146
|
-
return cls(
|
1147
|
-
request=Request.from_case(case, session),
|
1148
|
-
response=Response.from_wsgi(response, elapsed) if response is not None and elapsed is not None else None,
|
1149
|
-
status=status,
|
1150
|
-
checks=checks,
|
1151
|
-
data_generation_method=cast(DataGenerationMethod, case.data_generation_method),
|
1152
|
-
phase=case.meta.phase if case.meta is not None else None,
|
1153
|
-
description=case.meta.description if case.meta is not None else None,
|
1154
|
-
location=case.meta.location if case.meta is not None else None,
|
1155
|
-
parameter=case.meta.parameter if case.meta is not None else None,
|
1156
|
-
parameter_location=case.meta.parameter_location if case.meta is not None else None,
|
1157
|
-
)
|
1158
|
-
|
1159
|
-
|
1160
|
-
@dataclass(repr=False)
|
1161
|
-
class TestResult:
|
1162
|
-
"""Result of a single test."""
|
1163
|
-
|
1164
|
-
__test__ = False
|
1165
|
-
|
1166
|
-
method: str
|
1167
|
-
path: str
|
1168
|
-
verbose_name: str
|
1169
|
-
data_generation_method: list[DataGenerationMethod]
|
1170
|
-
checks: list[Check] = field(default_factory=list)
|
1171
|
-
errors: list[Exception] = field(default_factory=list)
|
1172
|
-
interactions: list[Interaction] = field(default_factory=list)
|
1173
|
-
logs: list[LogRecord] = field(default_factory=list)
|
1174
|
-
is_errored: bool = False
|
1175
|
-
is_flaky: bool = False
|
1176
|
-
is_skipped: bool = False
|
1177
|
-
skip_reason: str | None = None
|
1178
|
-
is_executed: bool = False
|
1179
|
-
# DEPRECATED: Seed is the same per test run
|
1180
|
-
seed: int | None = None
|
1181
|
-
|
1182
|
-
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
1183
|
-
|
1184
|
-
def mark_errored(self) -> None:
|
1185
|
-
self.is_errored = True
|
1186
|
-
|
1187
|
-
def mark_flaky(self) -> None:
|
1188
|
-
self.is_flaky = True
|
1189
|
-
|
1190
|
-
def mark_skipped(self, exc: SkipTest | unittest.case.SkipTest | None) -> None:
|
1191
|
-
self.is_skipped = True
|
1192
|
-
if exc is not None:
|
1193
|
-
self.skip_reason = str(exc)
|
1194
|
-
|
1195
|
-
def mark_executed(self) -> None:
|
1196
|
-
self.is_executed = True
|
1197
|
-
|
1198
|
-
@property
|
1199
|
-
def has_errors(self) -> bool:
|
1200
|
-
return bool(self.errors)
|
1201
|
-
|
1202
|
-
@property
|
1203
|
-
def has_failures(self) -> bool:
|
1204
|
-
return any(check.value == Status.failure for check in self.checks)
|
1205
|
-
|
1206
|
-
@property
|
1207
|
-
def has_logs(self) -> bool:
|
1208
|
-
return bool(self.logs)
|
1209
|
-
|
1210
|
-
def add_success(self, name: str, example: Case, response: GenericResponse, elapsed: float) -> Check:
|
1211
|
-
check = Check(
|
1212
|
-
name=name, value=Status.success, response=response, elapsed=elapsed, example=example, request=None
|
1213
|
-
)
|
1214
|
-
self.checks.append(check)
|
1215
|
-
return check
|
1216
|
-
|
1217
|
-
def add_failure(
|
1218
|
-
self,
|
1219
|
-
name: str,
|
1220
|
-
example: Case,
|
1221
|
-
response: GenericResponse | None,
|
1222
|
-
elapsed: float,
|
1223
|
-
message: str,
|
1224
|
-
context: FailureContext | None,
|
1225
|
-
request: requests.PreparedRequest | None = None,
|
1226
|
-
) -> Check:
|
1227
|
-
check = Check(
|
1228
|
-
name=name,
|
1229
|
-
value=Status.failure,
|
1230
|
-
response=response,
|
1231
|
-
elapsed=elapsed,
|
1232
|
-
example=example,
|
1233
|
-
message=message,
|
1234
|
-
context=context,
|
1235
|
-
request=request,
|
1236
|
-
)
|
1237
|
-
self.checks.append(check)
|
1238
|
-
return check
|
1239
|
-
|
1240
|
-
def add_error(self, exception: Exception) -> None:
|
1241
|
-
self.errors.append(exception)
|
1242
|
-
|
1243
|
-
def store_requests_response(
|
1244
|
-
self,
|
1245
|
-
case: Case,
|
1246
|
-
response: requests.Response | None,
|
1247
|
-
status: Status,
|
1248
|
-
checks: list[Check],
|
1249
|
-
headers: dict[str, Any] | None,
|
1250
|
-
session: requests.Session | None,
|
1251
|
-
) -> None:
|
1252
|
-
self.interactions.append(Interaction.from_requests(case, response, status, checks, headers, session))
|
1253
|
-
|
1254
|
-
def store_wsgi_response(
|
1255
|
-
self,
|
1256
|
-
case: Case,
|
1257
|
-
response: WSGIResponse | None,
|
1258
|
-
headers: dict[str, Any],
|
1259
|
-
elapsed: float | None,
|
1260
|
-
status: Status,
|
1261
|
-
checks: list[Check],
|
1262
|
-
) -> None:
|
1263
|
-
self.interactions.append(Interaction.from_wsgi(case, response, headers, elapsed, status, checks))
|
1264
|
-
|
1265
|
-
|
1266
|
-
@dataclass(repr=False)
|
1267
|
-
class TestResultSet:
|
1268
|
-
"""Set of multiple test results."""
|
1269
|
-
|
1270
|
-
__test__ = False
|
1271
|
-
|
1272
|
-
seed: int | None
|
1273
|
-
results: list[TestResult] = field(default_factory=list)
|
1274
|
-
generic_errors: list[OperationSchemaError] = field(default_factory=list)
|
1275
|
-
warnings: list[str] = field(default_factory=list)
|
1276
|
-
|
1277
|
-
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
1278
|
-
|
1279
|
-
def __iter__(self) -> Iterator[TestResult]:
|
1280
|
-
return iter(self.results)
|
1281
|
-
|
1282
|
-
@property
|
1283
|
-
def is_empty(self) -> bool:
|
1284
|
-
"""If the result set contains no results."""
|
1285
|
-
return len(self.results) == 0 and len(self.generic_errors) == 0
|
1286
|
-
|
1287
|
-
@property
|
1288
|
-
def has_failures(self) -> bool:
|
1289
|
-
"""If any result has any failures."""
|
1290
|
-
return any(result.has_failures for result in self)
|
1291
|
-
|
1292
|
-
@property
|
1293
|
-
def has_errors(self) -> bool:
|
1294
|
-
"""If any result has any errors."""
|
1295
|
-
return self.errored_count > 0
|
1296
|
-
|
1297
|
-
@property
|
1298
|
-
def has_logs(self) -> bool:
|
1299
|
-
"""If any result has any captured logs."""
|
1300
|
-
return any(result.has_logs for result in self)
|
1301
|
-
|
1302
|
-
def _count(self, predicate: Callable) -> int:
|
1303
|
-
return sum(1 for result in self if predicate(result))
|
1304
|
-
|
1305
|
-
@property
|
1306
|
-
def passed_count(self) -> int:
|
1307
|
-
return self._count(lambda result: not result.has_errors and not result.is_skipped and not result.has_failures)
|
1308
|
-
|
1309
|
-
@property
|
1310
|
-
def skipped_count(self) -> int:
|
1311
|
-
return self._count(lambda result: result.is_skipped)
|
1312
|
-
|
1313
|
-
@property
|
1314
|
-
def failed_count(self) -> int:
|
1315
|
-
return self._count(lambda result: result.has_failures and not result.is_errored)
|
1316
|
-
|
1317
|
-
@property
|
1318
|
-
def errored_count(self) -> int:
|
1319
|
-
return self._count(lambda result: result.has_errors or result.is_errored) + len(self.generic_errors)
|
1320
|
-
|
1321
|
-
@property
|
1322
|
-
def total(self) -> dict[str, dict[str | Status, int]]:
|
1323
|
-
"""An aggregated statistic about test results."""
|
1324
|
-
output: dict[str, dict[str | Status, int]] = {}
|
1325
|
-
for item in self.results:
|
1326
|
-
for check in item.checks:
|
1327
|
-
output.setdefault(check.name, Counter())
|
1328
|
-
output[check.name][check.value] += 1
|
1329
|
-
output[check.name]["total"] += 1
|
1330
|
-
# Avoid using Counter, since its behavior could harm in other places:
|
1331
|
-
# `if not total["unknown"]:` - this will lead to the branch execution
|
1332
|
-
# It is better to let it fail if there is a wrong key
|
1333
|
-
return {key: dict(value) for key, value in output.items()}
|
1334
|
-
|
1335
|
-
def append(self, item: TestResult) -> None:
|
1336
|
-
"""Add a new item to the results list."""
|
1337
|
-
self.results.append(item)
|
1338
|
-
|
1339
|
-
def add_warning(self, warning: str) -> None:
|
1340
|
-
"""Add a new warning to the warnings list."""
|
1341
|
-
self.warnings.append(warning)
|