schemathesis 3.39.7__py3-none-any.whl → 4.0.0a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +27 -65
- schemathesis/auths.py +26 -68
- schemathesis/checks.py +130 -60
- schemathesis/cli/__init__.py +5 -2105
- schemathesis/cli/commands/__init__.py +37 -0
- schemathesis/cli/commands/run/__init__.py +662 -0
- schemathesis/cli/commands/run/checks.py +80 -0
- schemathesis/cli/commands/run/context.py +117 -0
- schemathesis/cli/commands/run/events.py +30 -0
- schemathesis/cli/commands/run/executor.py +141 -0
- schemathesis/cli/commands/run/filters.py +202 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/{cassettes.py → commands/run/handlers/cassettes.py} +178 -247
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +1368 -0
- schemathesis/cli/commands/run/hypothesis.py +105 -0
- schemathesis/cli/commands/run/loaders.py +129 -0
- schemathesis/cli/{callbacks.py → commands/run/validation.py} +59 -175
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +17 -0
- schemathesis/cli/ext/fs.py +14 -0
- schemathesis/cli/ext/groups.py +55 -0
- schemathesis/cli/{options.py → ext/options.py} +37 -16
- schemathesis/cli/hooks.py +36 -0
- schemathesis/contrib/__init__.py +1 -3
- schemathesis/contrib/openapi/__init__.py +1 -3
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -7
- schemathesis/core/__init__.py +58 -0
- schemathesis/core/compat.py +25 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +370 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
- schemathesis/{internal/output.py → core/output/__init__.py} +1 -0
- schemathesis/core/output/sanitization.py +197 -0
- schemathesis/{throttling.py → core/rate_limit.py} +16 -17
- schemathesis/core/registries.py +31 -0
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +108 -0
- schemathesis/core/validation.py +38 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +30 -0
- schemathesis/engine/config.py +59 -0
- schemathesis/engine/context.py +119 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +157 -0
- schemathesis/engine/errors.py +394 -0
- schemathesis/engine/events.py +243 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{runner → engine/phases}/probes.py +49 -68
- schemathesis/engine/phases/stateful/__init__.py +66 -0
- schemathesis/engine/phases/stateful/_executor.py +301 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +175 -0
- schemathesis/engine/phases/unit/_executor.py +322 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +246 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +9 -40
- schemathesis/filters.py +7 -95
- schemathesis/generation/__init__.py +3 -3
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +22 -22
- schemathesis/{_patches.py → generation/hypothesis/__init__.py} +15 -6
- schemathesis/generation/hypothesis/builder.py +585 -0
- schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/modes.py +28 -0
- schemathesis/generation/overrides.py +96 -0
- schemathesis/generation/stateful/__init__.py +20 -0
- schemathesis/{stateful → generation/stateful}/state_machine.py +84 -109
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +17 -62
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +387 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +63 -0
- schemathesis/openapi/loaders.py +178 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +273 -0
- schemathesis/pytest/loaders.py +12 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +94 -107
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +456 -228
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +5 -3
- schemathesis/specs/graphql/schemas.py +122 -123
- schemathesis/specs/graphql/validation.py +11 -17
- schemathesis/specs/openapi/__init__.py +6 -1
- schemathesis/specs/openapi/_cache.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +97 -134
- schemathesis/specs/openapi/checks.py +238 -219
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +22 -20
- schemathesis/specs/openapi/expressions/__init__.py +11 -15
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/nodes.py +33 -32
- schemathesis/specs/openapi/formats.py +3 -2
- schemathesis/specs/openapi/links.py +123 -299
- schemathesis/specs/openapi/media_types.py +10 -12
- schemathesis/specs/openapi/negative/__init__.py +2 -1
- schemathesis/specs/openapi/negative/mutations.py +3 -2
- schemathesis/specs/openapi/parameters.py +8 -6
- schemathesis/specs/openapi/patterns.py +1 -1
- schemathesis/specs/openapi/references.py +11 -51
- schemathesis/specs/openapi/schemas.py +177 -191
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +10 -6
- schemathesis/specs/openapi/stateful/__init__.py +97 -91
- schemathesis/transport/__init__.py +104 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +99 -0
- schemathesis/transport/requests.py +221 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -7
- schemathesis/transport/wsgi.py +165 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +18 -14
- schemathesis-4.0.0a2.dist-info/RECORD +151 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -559
- schemathesis/_override.py +0 -50
- schemathesis/_rate_limiter.py +0 -7
- schemathesis/cli/context.py +0 -75
- schemathesis/cli/debug.py +0 -27
- schemathesis/cli/handlers.py +0 -19
- schemathesis/cli/junitxml.py +0 -124
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -936
- schemathesis/cli/output/short.py +0 -59
- schemathesis/cli/reporting.py +0 -79
- schemathesis/cli/sanitization.py +0 -26
- schemathesis/code_samples.py +0 -151
- schemathesis/constants.py +0 -56
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -16
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -571
- schemathesis/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -277
- schemathesis/fixups/__init__.py +0 -37
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -28
- schemathesis/generation/_methods.py +0 -44
- schemathesis/graphql.py +0 -3
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/checks.py +0 -84
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -38
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- schemathesis/internal/transformation.py +0 -26
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -474
- schemathesis/loaders.py +0 -122
- schemathesis/models.py +0 -1341
- schemathesis/parameters.py +0 -90
- schemathesis/runner/__init__.py +0 -605
- schemathesis/runner/events.py +0 -389
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/context.py +0 -104
- schemathesis/runner/impl/core.py +0 -1246
- schemathesis/runner/impl/solo.py +0 -80
- schemathesis/runner/impl/threadpool.py +0 -391
- schemathesis/runner/serialization.py +0 -544
- schemathesis/sanitization.py +0 -252
- schemathesis/serializers.py +0 -328
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -202
- schemathesis/service/client.py +0 -133
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -61
- schemathesis/service/extensions.py +0 -224
- schemathesis/service/hosts.py +0 -111
- schemathesis/service/metadata.py +0 -71
- schemathesis/service/models.py +0 -258
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -173
- schemathesis/service/usage.py +0 -66
- schemathesis/specs/graphql/loaders.py +0 -364
- schemathesis/specs/openapi/expressions/context.py +0 -16
- schemathesis/specs/openapi/loaders.py +0 -708
- schemathesis/specs/openapi/stateful/statistic.py +0 -198
- schemathesis/specs/openapi/stateful/types.py +0 -14
- schemathesis/specs/openapi/validation.py +0 -26
- schemathesis/stateful/__init__.py +0 -147
- schemathesis/stateful/config.py +0 -97
- schemathesis/stateful/context.py +0 -135
- schemathesis/stateful/events.py +0 -274
- schemathesis/stateful/runner.py +0 -309
- schemathesis/stateful/sink.py +0 -68
- schemathesis/stateful/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -359
- schemathesis/transports/asgi.py +0 -7
- schemathesis/transports/auth.py +0 -38
- schemathesis/transports/headers.py +0 -36
- schemathesis/transports/responses.py +0 -57
- schemathesis/types.py +0 -44
- schemathesis/utils.py +0 -164
- schemathesis-3.39.7.dist-info/RECORD +0 -160
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
- /schemathesis/{internal → core}/result.py +0 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,273 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from contextlib import nullcontext
|
4
|
+
from dataclasses import dataclass, field
|
5
|
+
from inspect import signature
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable, Generator, Type
|
7
|
+
|
8
|
+
import pytest
|
9
|
+
from hypothesis.core import HypothesisHandle
|
10
|
+
from pytest_subtests import SubTests
|
11
|
+
|
12
|
+
from schemathesis.core.errors import InvalidSchema
|
13
|
+
from schemathesis.core.result import Ok
|
14
|
+
from schemathesis.filters import FilterSet, FilterValue, MatcherFunc, RegexValue, is_deprecated
|
15
|
+
from schemathesis.generation.hypothesis.builder import get_all_tests
|
16
|
+
from schemathesis.generation.hypothesis.given import (
|
17
|
+
GivenArgsMark,
|
18
|
+
GivenInput,
|
19
|
+
GivenKwargsMark,
|
20
|
+
given_proxy,
|
21
|
+
is_given_applied,
|
22
|
+
merge_given_args,
|
23
|
+
validate_given_args,
|
24
|
+
)
|
25
|
+
from schemathesis.generation.overrides import Override, OverrideMark, check_no_override_mark
|
26
|
+
from schemathesis.pytest.control_flow import fail_on_no_matches
|
27
|
+
from schemathesis.schemas import BaseSchema
|
28
|
+
|
29
|
+
if TYPE_CHECKING:
|
30
|
+
from _pytest.fixtures import FixtureRequest
|
31
|
+
|
32
|
+
from schemathesis.schemas import APIOperation
|
33
|
+
|
34
|
+
|
35
|
+
@dataclass
|
36
|
+
class LazySchema:
|
37
|
+
fixture_name: str
|
38
|
+
filter_set: FilterSet = field(default_factory=FilterSet)
|
39
|
+
|
40
|
+
def include(
|
41
|
+
self,
|
42
|
+
func: MatcherFunc | None = None,
|
43
|
+
*,
|
44
|
+
name: FilterValue | None = None,
|
45
|
+
name_regex: str | None = None,
|
46
|
+
method: FilterValue | None = None,
|
47
|
+
method_regex: str | None = None,
|
48
|
+
path: FilterValue | None = None,
|
49
|
+
path_regex: str | None = None,
|
50
|
+
tag: FilterValue | None = None,
|
51
|
+
tag_regex: RegexValue | None = None,
|
52
|
+
operation_id: FilterValue | None = None,
|
53
|
+
operation_id_regex: RegexValue | None = None,
|
54
|
+
) -> LazySchema:
|
55
|
+
"""Include only operations that match the given filters."""
|
56
|
+
filter_set = self.filter_set.clone()
|
57
|
+
filter_set.include(
|
58
|
+
func,
|
59
|
+
name=name,
|
60
|
+
name_regex=name_regex,
|
61
|
+
method=method,
|
62
|
+
method_regex=method_regex,
|
63
|
+
path=path,
|
64
|
+
path_regex=path_regex,
|
65
|
+
tag=tag,
|
66
|
+
tag_regex=tag_regex,
|
67
|
+
operation_id=operation_id,
|
68
|
+
operation_id_regex=operation_id_regex,
|
69
|
+
)
|
70
|
+
return self.__class__(fixture_name=self.fixture_name, filter_set=filter_set)
|
71
|
+
|
72
|
+
def exclude(
|
73
|
+
self,
|
74
|
+
func: MatcherFunc | None = None,
|
75
|
+
*,
|
76
|
+
name: FilterValue | None = None,
|
77
|
+
name_regex: str | None = None,
|
78
|
+
method: FilterValue | None = None,
|
79
|
+
method_regex: str | None = None,
|
80
|
+
path: FilterValue | None = None,
|
81
|
+
path_regex: str | None = None,
|
82
|
+
tag: FilterValue | None = None,
|
83
|
+
tag_regex: RegexValue | None = None,
|
84
|
+
operation_id: FilterValue | None = None,
|
85
|
+
operation_id_regex: RegexValue | None = None,
|
86
|
+
deprecated: bool = False,
|
87
|
+
) -> LazySchema:
|
88
|
+
"""Exclude operations that match the given filters."""
|
89
|
+
filter_set = self.filter_set.clone()
|
90
|
+
if deprecated:
|
91
|
+
if func is None:
|
92
|
+
func = is_deprecated
|
93
|
+
else:
|
94
|
+
filter_set.exclude(is_deprecated)
|
95
|
+
filter_set.exclude(
|
96
|
+
func,
|
97
|
+
name=name,
|
98
|
+
name_regex=name_regex,
|
99
|
+
method=method,
|
100
|
+
method_regex=method_regex,
|
101
|
+
path=path,
|
102
|
+
path_regex=path_regex,
|
103
|
+
tag=tag,
|
104
|
+
tag_regex=tag_regex,
|
105
|
+
operation_id=operation_id,
|
106
|
+
operation_id_regex=operation_id_regex,
|
107
|
+
)
|
108
|
+
return self.__class__(fixture_name=self.fixture_name, filter_set=filter_set)
|
109
|
+
|
110
|
+
def parametrize(self) -> Callable:
|
111
|
+
def wrapper(test_func: Callable) -> Callable:
|
112
|
+
if is_given_applied(test_func):
|
113
|
+
# The user wrapped the test function with `@schema.given`
|
114
|
+
# These args & kwargs go as extra to the underlying test generator
|
115
|
+
given_args = GivenArgsMark.get(test_func)
|
116
|
+
given_kwargs = GivenKwargsMark.get(test_func)
|
117
|
+
assert given_args is not None
|
118
|
+
assert given_kwargs is not None
|
119
|
+
test_function = validate_given_args(test_func, given_args, given_kwargs)
|
120
|
+
if test_function is not None:
|
121
|
+
return test_function
|
122
|
+
given_kwargs = merge_given_args(test_func, given_args, given_kwargs)
|
123
|
+
del given_args
|
124
|
+
else:
|
125
|
+
given_kwargs = {}
|
126
|
+
|
127
|
+
def wrapped_test(request: FixtureRequest) -> None:
|
128
|
+
"""The actual test, which is executed by pytest."""
|
129
|
+
__tracebackhide__ = True
|
130
|
+
schema = get_schema(
|
131
|
+
request=request,
|
132
|
+
name=self.fixture_name,
|
133
|
+
test_function=test_func,
|
134
|
+
filter_set=self.filter_set,
|
135
|
+
)
|
136
|
+
fixtures = get_fixtures(test_func, request, given_kwargs)
|
137
|
+
# Changing the node id is required for better reporting - the method and path will appear there
|
138
|
+
node_id = request.node._nodeid
|
139
|
+
settings = getattr(wrapped_test, "_hypothesis_internal_use_settings", None)
|
140
|
+
|
141
|
+
as_strategy_kwargs: Callable[[APIOperation], dict[str, Any]] | None = None
|
142
|
+
|
143
|
+
override = OverrideMark.get(test_func)
|
144
|
+
if override is not None:
|
145
|
+
|
146
|
+
def as_strategy_kwargs(_operation: APIOperation) -> dict[str, Any]:
|
147
|
+
nonlocal override
|
148
|
+
|
149
|
+
return {
|
150
|
+
location: entry for location, entry in override.for_operation(_operation).items() if entry
|
151
|
+
}
|
152
|
+
|
153
|
+
tests = list(
|
154
|
+
get_all_tests(
|
155
|
+
schema=schema,
|
156
|
+
test_func=test_func,
|
157
|
+
settings=settings,
|
158
|
+
generation_config=schema.generation_config,
|
159
|
+
as_strategy_kwargs=as_strategy_kwargs,
|
160
|
+
given_kwargs=given_kwargs,
|
161
|
+
)
|
162
|
+
)
|
163
|
+
if not tests:
|
164
|
+
fail_on_no_matches(node_id)
|
165
|
+
request.session.testscollected += len(tests)
|
166
|
+
suspend_capture_ctx = _get_capturemanager(request)
|
167
|
+
subtests = SubTests(request.node.ihook, suspend_capture_ctx, request)
|
168
|
+
for result in tests:
|
169
|
+
if isinstance(result, Ok):
|
170
|
+
operation, sub_test = result.ok()
|
171
|
+
subtests.item._nodeid = f"{node_id}[{operation.method.upper()} {operation.full_path}]"
|
172
|
+
run_subtest(operation, fixtures, sub_test, subtests)
|
173
|
+
else:
|
174
|
+
_schema_error(subtests, result.err(), node_id)
|
175
|
+
subtests.item._nodeid = node_id
|
176
|
+
|
177
|
+
wrapped_test = pytest.mark.usefixtures(self.fixture_name)(wrapped_test)
|
178
|
+
_copy_marks(test_func, wrapped_test)
|
179
|
+
|
180
|
+
# Needed to prevent a failure when settings are applied to the test function
|
181
|
+
wrapped_test.is_hypothesis_test = True # type: ignore
|
182
|
+
wrapped_test.hypothesis = HypothesisHandle(test_func, wrapped_test, given_kwargs) # type: ignore
|
183
|
+
|
184
|
+
return wrapped_test
|
185
|
+
|
186
|
+
return wrapper
|
187
|
+
|
188
|
+
def given(self, *args: GivenInput, **kwargs: GivenInput) -> Callable:
|
189
|
+
return given_proxy(*args, **kwargs)
|
190
|
+
|
191
|
+
def override(
|
192
|
+
self,
|
193
|
+
*,
|
194
|
+
query: dict[str, str] | None = None,
|
195
|
+
headers: dict[str, str] | None = None,
|
196
|
+
cookies: dict[str, str] | None = None,
|
197
|
+
path_parameters: dict[str, str] | None = None,
|
198
|
+
) -> Callable[[Callable], Callable]:
|
199
|
+
"""Override Open API parameters with fixed values."""
|
200
|
+
|
201
|
+
def _add_override(test: Callable) -> Callable:
|
202
|
+
check_no_override_mark(test)
|
203
|
+
override = Override(
|
204
|
+
query=query or {}, headers=headers or {}, cookies=cookies or {}, path_parameters=path_parameters or {}
|
205
|
+
)
|
206
|
+
OverrideMark.set(test, override)
|
207
|
+
return test
|
208
|
+
|
209
|
+
return _add_override
|
210
|
+
|
211
|
+
|
212
|
+
def _copy_marks(source: Callable, target: Callable) -> None:
|
213
|
+
marks = getattr(source, "pytestmark", [])
|
214
|
+
# Pytest adds this attribute in `usefixtures`
|
215
|
+
target.pytestmark.extend(marks) # type: ignore
|
216
|
+
|
217
|
+
|
218
|
+
def _get_capturemanager(request: FixtureRequest) -> Generator | Type[nullcontext]:
|
219
|
+
capturemanager = request.node.config.pluginmanager.get_plugin("capturemanager")
|
220
|
+
if capturemanager is not None:
|
221
|
+
return capturemanager.global_and_fixture_disabled
|
222
|
+
return nullcontext
|
223
|
+
|
224
|
+
|
225
|
+
def run_subtest(operation: APIOperation, fixtures: dict[str, Any], sub_test: Callable, subtests: SubTests) -> None:
|
226
|
+
"""Run the given subtest with pytest fixtures."""
|
227
|
+
__tracebackhide__ = True
|
228
|
+
|
229
|
+
with subtests.test(label=operation.label):
|
230
|
+
sub_test(**fixtures)
|
231
|
+
|
232
|
+
|
233
|
+
SEPARATOR = "\n===================="
|
234
|
+
|
235
|
+
|
236
|
+
def _schema_error(subtests: SubTests, error: InvalidSchema, node_id: str) -> None:
|
237
|
+
"""Run a failing test, that will show the underlying problem."""
|
238
|
+
sub_test = error.as_failing_test_function()
|
239
|
+
# `full_path` is always available in this case
|
240
|
+
kwargs = {"path": error.full_path}
|
241
|
+
if error.method:
|
242
|
+
kwargs["method"] = error.method.upper()
|
243
|
+
subtests.item._nodeid = _get_partial_node_name(node_id, **kwargs)
|
244
|
+
__tracebackhide__ = True
|
245
|
+
with subtests.test(**kwargs):
|
246
|
+
sub_test()
|
247
|
+
|
248
|
+
|
249
|
+
def _get_partial_node_name(node_id: str, **kwargs: Any) -> str:
|
250
|
+
"""Make a test node name for failing tests caused by schema errors."""
|
251
|
+
name = node_id
|
252
|
+
if "method" in kwargs:
|
253
|
+
name += f"[{kwargs['method']} {kwargs['path']}]"
|
254
|
+
else:
|
255
|
+
name += f"[{kwargs['path']}]"
|
256
|
+
return name
|
257
|
+
|
258
|
+
|
259
|
+
def get_schema(*, request: FixtureRequest, name: str, filter_set: FilterSet, test_function: Callable) -> BaseSchema:
|
260
|
+
"""Loads a schema from the fixture."""
|
261
|
+
schema = request.getfixturevalue(name)
|
262
|
+
if not isinstance(schema, BaseSchema):
|
263
|
+
raise ValueError(f"The given schema must be an instance of BaseSchema, got: {type(schema)}")
|
264
|
+
|
265
|
+
return schema.clone(filter_set=filter_set, test_function=test_function)
|
266
|
+
|
267
|
+
|
268
|
+
def get_fixtures(func: Callable, request: FixtureRequest, given_kwargs: dict[str, Any]) -> dict[str, Any]:
|
269
|
+
"""Load fixtures, needed for the test function."""
|
270
|
+
sig = signature(func)
|
271
|
+
return {
|
272
|
+
name: request.getfixturevalue(name) for name in sig.parameters if name != "case" and name not in given_kwargs
|
273
|
+
}
|
@@ -0,0 +1,12 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import TYPE_CHECKING
|
4
|
+
|
5
|
+
if TYPE_CHECKING:
|
6
|
+
from schemathesis.pytest.lazy import LazySchema
|
7
|
+
|
8
|
+
|
9
|
+
def from_fixture(name: str) -> LazySchema:
|
10
|
+
from schemathesis.pytest.lazy import LazySchema
|
11
|
+
|
12
|
+
return LazySchema(name)
|
@@ -1,49 +1,59 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
import asyncio
|
3
4
|
import unittest
|
4
|
-
from contextlib import contextmanager
|
5
5
|
from functools import partial
|
6
6
|
from typing import TYPE_CHECKING, Any, Callable, Generator, Type, cast
|
7
7
|
|
8
8
|
import pytest
|
9
|
-
from _pytest import
|
9
|
+
from _pytest import nodes
|
10
10
|
from _pytest.config import hookimpl
|
11
11
|
from _pytest.python import Class, Function, FunctionDefinition, Metafunc, Module, PyCollector
|
12
|
-
from hypothesis import reporting
|
13
12
|
from hypothesis.errors import InvalidArgument, Unsatisfiable
|
14
13
|
from jsonschema.exceptions import SchemaError
|
15
14
|
|
16
|
-
from
|
17
|
-
from
|
18
|
-
from ..constants import (
|
19
|
-
GIVEN_AND_EXPLICIT_EXAMPLES_ERROR_MESSAGE,
|
15
|
+
from schemathesis.core.control import SkipTest
|
16
|
+
from schemathesis.core.errors import (
|
20
17
|
RECURSIVE_REFERENCE_ERROR_MESSAGE,
|
21
18
|
SERIALIZERS_SUGGESTION_MESSAGE,
|
22
|
-
|
23
|
-
from ..exceptions import (
|
19
|
+
IncorrectUsage,
|
24
20
|
InvalidHeadersExample,
|
25
|
-
|
26
|
-
|
21
|
+
InvalidRegexPattern,
|
22
|
+
InvalidSchema,
|
27
23
|
SerializationNotPossible,
|
28
|
-
SkipTest,
|
29
|
-
UsageError,
|
30
24
|
)
|
31
|
-
from
|
32
|
-
from
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
get_given_kwargs,
|
25
|
+
from schemathesis.core.marks import Mark
|
26
|
+
from schemathesis.core.result import Ok, Result
|
27
|
+
from schemathesis.generation.hypothesis.given import (
|
28
|
+
GivenArgsMark,
|
29
|
+
GivenKwargsMark,
|
37
30
|
is_given_applied,
|
38
|
-
is_schemathesis_test,
|
39
31
|
merge_given_args,
|
40
32
|
validate_given_args,
|
41
33
|
)
|
34
|
+
from schemathesis.generation.hypothesis.reporting import ignore_hypothesis_output
|
35
|
+
from schemathesis.generation.overrides import OverrideMark
|
36
|
+
from schemathesis.pytest.control_flow import fail_on_no_matches
|
37
|
+
from schemathesis.schemas import APIOperation
|
42
38
|
|
43
39
|
if TYPE_CHECKING:
|
44
40
|
from _pytest.fixtures import FuncFixtureInfo
|
45
41
|
|
46
|
-
from
|
42
|
+
from schemathesis.schemas import BaseSchema
|
43
|
+
|
44
|
+
GIVEN_AND_EXPLICIT_EXAMPLES_ERROR_MESSAGE = (
|
45
|
+
"Unsupported test setup. Tests using `@schema.given` cannot be combined with explicit schema examples in the same "
|
46
|
+
"function. Separate these tests into distinct functions to avoid conflicts."
|
47
|
+
)
|
48
|
+
|
49
|
+
|
50
|
+
def _is_schema(value: object) -> bool:
|
51
|
+
from schemathesis.schemas import BaseSchema
|
52
|
+
|
53
|
+
return isinstance(value, BaseSchema)
|
54
|
+
|
55
|
+
|
56
|
+
SchemaHandleMark = Mark["BaseSchema"](attr_name="schema", check=_is_schema)
|
47
57
|
|
48
58
|
|
49
59
|
class SchemathesisFunction(Function):
|
@@ -58,21 +68,15 @@ class SchemathesisFunction(Function):
|
|
58
68
|
self.test_function = test_func
|
59
69
|
self.test_name = test_name
|
60
70
|
|
61
|
-
if not IS_PYTEST_ABOVE_7:
|
62
|
-
# On pytest 7, `self.obj` is already `partial`
|
63
|
-
def _getobj(self) -> partial:
|
64
|
-
"""Tests defined as methods require `self` as the first argument.
|
65
|
-
|
66
|
-
This method is called only for this case.
|
67
|
-
"""
|
68
|
-
return partial(self.obj, self.parent.obj) # type: ignore
|
69
|
-
|
70
71
|
|
71
72
|
class SchemathesisCase(PyCollector):
|
72
|
-
def __init__(self, test_function: Callable, *args: Any, **kwargs: Any) -> None:
|
73
|
-
self.given_kwargs: dict[str, Any]
|
74
|
-
given_args =
|
75
|
-
given_kwargs =
|
73
|
+
def __init__(self, test_function: Callable, schema: BaseSchema, *args: Any, **kwargs: Any) -> None:
|
74
|
+
self.given_kwargs: dict[str, Any]
|
75
|
+
given_args = GivenArgsMark.get(test_function)
|
76
|
+
given_kwargs = GivenKwargsMark.get(test_function)
|
77
|
+
|
78
|
+
assert given_args is not None
|
79
|
+
assert given_kwargs is not None
|
76
80
|
|
77
81
|
def _init_with_valid_test(_test_function: Callable, _args: tuple, _kwargs: dict[str, Any]) -> None:
|
78
82
|
self.test_function = _test_function
|
@@ -84,20 +88,18 @@ class SchemathesisCase(PyCollector):
|
|
84
88
|
if failing_test is not None:
|
85
89
|
self.test_function = failing_test
|
86
90
|
self.is_invalid_test = True
|
87
|
-
self.given_kwargs =
|
91
|
+
self.given_kwargs = {}
|
88
92
|
else:
|
89
93
|
_init_with_valid_test(test_function, given_args, given_kwargs)
|
90
94
|
else:
|
91
95
|
_init_with_valid_test(test_function, given_args, given_kwargs)
|
92
|
-
self.
|
96
|
+
self.schema = schema
|
93
97
|
super().__init__(*args, **kwargs)
|
94
98
|
|
95
99
|
def _get_test_name(self, operation: APIOperation) -> str:
|
96
|
-
return f"{self.name}[{operation.
|
100
|
+
return f"{self.name}[{operation.label}]"
|
97
101
|
|
98
|
-
def _gen_items(
|
99
|
-
self, result: Result[APIOperation, OperationSchemaError]
|
100
|
-
) -> Generator[SchemathesisFunction, None, None]:
|
102
|
+
def _gen_items(self, result: Result[APIOperation, InvalidSchema]) -> Generator[SchemathesisFunction, None, None]:
|
101
103
|
"""Generate all tests for the given API operation.
|
102
104
|
|
103
105
|
Could produce more than one test item if
|
@@ -106,7 +108,7 @@ class SchemathesisCase(PyCollector):
|
|
106
108
|
This implementation is based on the original one in pytest, but with slight adjustments
|
107
109
|
to produce tests out of hypothesis ones.
|
108
110
|
"""
|
109
|
-
from
|
111
|
+
from schemathesis.generation.hypothesis.builder import HypothesisTestConfig, create_test
|
110
112
|
|
111
113
|
is_trio_test = False
|
112
114
|
for mark in getattr(self.test_function, "pytestmark", []):
|
@@ -119,24 +121,29 @@ class SchemathesisCase(PyCollector):
|
|
119
121
|
if self.is_invalid_test:
|
120
122
|
funcobj = self.test_function
|
121
123
|
else:
|
122
|
-
override =
|
123
|
-
as_strategy_kwargs: dict | None
|
124
|
+
override = OverrideMark.get(self.test_function)
|
124
125
|
if override is not None:
|
125
126
|
as_strategy_kwargs = {}
|
126
127
|
for location, entry in override.for_operation(operation).items():
|
127
128
|
if entry:
|
128
129
|
as_strategy_kwargs[location] = entry
|
129
130
|
else:
|
130
|
-
as_strategy_kwargs =
|
131
|
+
as_strategy_kwargs = {}
|
131
132
|
funcobj = create_test(
|
132
133
|
operation=operation,
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
134
|
+
test_func=self.test_function,
|
135
|
+
config=HypothesisTestConfig(
|
136
|
+
given_kwargs=self.given_kwargs,
|
137
|
+
generation=self.schema.generation_config,
|
138
|
+
as_strategy_kwargs=as_strategy_kwargs,
|
139
|
+
),
|
139
140
|
)
|
141
|
+
if asyncio.iscoroutinefunction(self.test_function):
|
142
|
+
# `pytest-trio` expects a coroutine function
|
143
|
+
if is_trio_test:
|
144
|
+
funcobj.hypothesis.inner_test = self.test_function # type: ignore
|
145
|
+
else:
|
146
|
+
funcobj.hypothesis.inner_test = make_async_test(self.test_function) # type: ignore
|
140
147
|
name = self._get_test_name(operation)
|
141
148
|
else:
|
142
149
|
error = result.err()
|
@@ -171,8 +178,6 @@ class SchemathesisCase(PyCollector):
|
|
171
178
|
originalname=self.name,
|
172
179
|
)
|
173
180
|
else:
|
174
|
-
if not IS_PYTEST_ABOVE_8:
|
175
|
-
fixtures.add_funcarg_pseudo_fixture_def(self.parent, metafunc, fixturemanager) # type: ignore[arg-type]
|
176
181
|
fixtureinfo.prune_dependency_tree()
|
177
182
|
for callspec in metafunc._calls:
|
178
183
|
subname = f"{name}[{callspec.id}]"
|
@@ -194,11 +199,8 @@ class SchemathesisCase(PyCollector):
|
|
194
199
|
def _parametrize(self, cls: type | None, definition: FunctionDefinition, fixtureinfo: FuncFixtureInfo) -> Metafunc:
|
195
200
|
parent = self.getparent(Module)
|
196
201
|
module = parent.obj if parent is not None else parent
|
197
|
-
|
198
|
-
|
199
|
-
# Avoiding `Metafunc` is quite problematic for now, as there are quite a lot of internals we rely on
|
200
|
-
kwargs["_ispytest"] = True
|
201
|
-
metafunc = Metafunc(definition, fixtureinfo, self.config, **kwargs)
|
202
|
+
# Avoiding `Metafunc` is quite problematic for now, as there are quite a lot of internals we rely on
|
203
|
+
metafunc = Metafunc(definition, fixtureinfo, self.config, cls=cls, module=module, _ispytest=True)
|
202
204
|
methods = []
|
203
205
|
if module is not None and hasattr(module, "pytest_generate_tests"):
|
204
206
|
methods.append(module.pytest_generate_tests)
|
@@ -211,13 +213,7 @@ class SchemathesisCase(PyCollector):
|
|
211
213
|
def collect(self) -> list[Function]: # type: ignore
|
212
214
|
"""Generate different test items for all API operations available in the given schema."""
|
213
215
|
try:
|
214
|
-
items = [
|
215
|
-
item
|
216
|
-
for operation in self.schemathesis_case.get_all_operations(
|
217
|
-
hooks=getattr(self.test_function, "_schemathesis_hooks", None)
|
218
|
-
)
|
219
|
-
for item in self._gen_items(operation)
|
220
|
-
]
|
216
|
+
items = [item for operation in self.schema.get_all_operations() for item in self._gen_items(operation)]
|
221
217
|
if not items:
|
222
218
|
fail_on_no_matches(self.nodeid)
|
223
219
|
return items
|
@@ -225,36 +221,31 @@ class SchemathesisCase(PyCollector):
|
|
225
221
|
pytest.fail("Error during collection")
|
226
222
|
|
227
223
|
|
228
|
-
|
224
|
+
def make_async_test(test: Callable) -> Callable:
|
225
|
+
def async_run(*args: Any, **kwargs: Any) -> None:
|
226
|
+
try:
|
227
|
+
loop = asyncio.get_event_loop()
|
228
|
+
except RuntimeError:
|
229
|
+
loop = asyncio.new_event_loop()
|
230
|
+
coro = test(*args, **kwargs)
|
231
|
+
future = asyncio.ensure_future(coro, loop=loop)
|
232
|
+
loop.run_until_complete(future)
|
233
|
+
|
234
|
+
return async_run
|
235
|
+
|
236
|
+
|
237
|
+
@hookimpl(hookwrapper=True) # type:ignore
|
229
238
|
def pytest_pycollect_makeitem(collector: nodes.Collector, name: str, obj: Any) -> Generator[None, Any, None]:
|
230
239
|
"""Switch to a different collector if the test is parametrized marked by schemathesis."""
|
231
240
|
outcome = yield
|
232
|
-
|
233
|
-
|
234
|
-
|
241
|
+
try:
|
242
|
+
schema = SchemaHandleMark.get(obj)
|
243
|
+
assert schema is not None
|
244
|
+
outcome.force_result(SchemathesisCase.from_parent(collector, test_function=obj, name=name, schema=schema))
|
245
|
+
except Exception:
|
235
246
|
outcome.get_result()
|
236
247
|
|
237
248
|
|
238
|
-
IGNORED_HYPOTHESIS_OUTPUT = ("Falsifying example",)
|
239
|
-
|
240
|
-
|
241
|
-
def _should_ignore_entry(value: str) -> bool:
|
242
|
-
return value.startswith(IGNORED_HYPOTHESIS_OUTPUT)
|
243
|
-
|
244
|
-
|
245
|
-
def hypothesis_reporter(value: str) -> None:
|
246
|
-
if _should_ignore_entry(value):
|
247
|
-
return
|
248
|
-
reporting.default(value)
|
249
|
-
|
250
|
-
|
251
|
-
@contextmanager
|
252
|
-
def skip_unnecessary_hypothesis_output() -> Generator:
|
253
|
-
"""Avoid printing Hypothesis output that is not necessary in Schemathesis' pytest plugin."""
|
254
|
-
with reporting.with_reporter(hypothesis_reporter): # type: ignore
|
255
|
-
yield
|
256
|
-
|
257
|
-
|
258
249
|
@hookimpl(wrapper=True)
|
259
250
|
def pytest_pyfunc_call(pyfuncitem): # type:ignore
|
260
251
|
"""It is possible to have a Hypothesis exception in runtime.
|
@@ -263,28 +254,28 @@ def pytest_pyfunc_call(pyfuncitem): # type:ignore
|
|
263
254
|
"""
|
264
255
|
from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
|
265
256
|
|
266
|
-
from
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
257
|
+
from schemathesis.generation.hypothesis.builder import (
|
258
|
+
InvalidHeadersExampleMark,
|
259
|
+
InvalidRegexMark,
|
260
|
+
NonSerializableMark,
|
261
|
+
UnsatisfiableExampleMark,
|
271
262
|
)
|
272
263
|
|
273
264
|
__tracebackhide__ = True
|
274
265
|
if isinstance(pyfuncitem, SchemathesisFunction):
|
275
266
|
try:
|
276
|
-
with
|
267
|
+
with ignore_hypothesis_output():
|
277
268
|
yield
|
278
269
|
except InvalidArgument as exc:
|
279
270
|
if "Inconsistent args" in str(exc) and "@example()" in str(exc):
|
280
|
-
raise
|
281
|
-
raise
|
271
|
+
raise IncorrectUsage(GIVEN_AND_EXPLICIT_EXAMPLES_ERROR_MESSAGE) from None
|
272
|
+
raise InvalidSchema(exc.args[0]) from None
|
282
273
|
except HypothesisRefResolutionError:
|
283
274
|
pytest.skip(RECURSIVE_REFERENCE_ERROR_MESSAGE)
|
284
275
|
except (SkipTest, unittest.SkipTest) as exc:
|
285
|
-
if
|
276
|
+
if UnsatisfiableExampleMark.is_set(pyfuncitem.obj):
|
286
277
|
raise Unsatisfiable("Failed to generate test cases from examples for this API operation") from None
|
287
|
-
non_serializable =
|
278
|
+
non_serializable = NonSerializableMark.get(pyfuncitem.obj)
|
288
279
|
if non_serializable is not None:
|
289
280
|
media_types = ", ".join(non_serializable.media_types)
|
290
281
|
raise SerializationNotPossible(
|
@@ -292,20 +283,16 @@ def pytest_pyfunc_call(pyfuncitem): # type:ignore
|
|
292
283
|
f" unsupported payload media types: {media_types}\n{SERIALIZERS_SUGGESTION_MESSAGE}",
|
293
284
|
media_types=non_serializable.media_types,
|
294
285
|
) from None
|
295
|
-
invalid_regex =
|
286
|
+
invalid_regex = InvalidRegexMark.get(pyfuncitem.obj)
|
296
287
|
if invalid_regex is not None:
|
297
|
-
raise
|
298
|
-
invalid_headers =
|
288
|
+
raise InvalidRegexPattern.from_schema_error(invalid_regex, from_examples=True) from None
|
289
|
+
invalid_headers = InvalidHeadersExampleMark.get(pyfuncitem.obj)
|
299
290
|
if invalid_headers is not None:
|
300
291
|
raise InvalidHeadersExample.from_headers(invalid_headers) from None
|
301
292
|
pytest.skip(exc.args[0])
|
302
293
|
except SchemaError as exc:
|
303
|
-
raise
|
304
|
-
|
305
|
-
if hasattr(exc, "__notes__"):
|
306
|
-
exc.__notes__ = [note for note in exc.__notes__ if not _should_ignore_entry(note)] # type: ignore
|
307
|
-
raise
|
308
|
-
invalid_headers = get_invalid_example_headers_mark(pyfuncitem.obj)
|
294
|
+
raise InvalidRegexPattern.from_schema_error(exc, from_examples=False) from exc
|
295
|
+
invalid_headers = InvalidHeadersExampleMark.get(pyfuncitem.obj)
|
309
296
|
if invalid_headers is not None:
|
310
297
|
raise InvalidHeadersExample.from_headers(invalid_headers) from None
|
311
298
|
else:
|
File without changes
|
@@ -0,0 +1,12 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import TYPE_CHECKING, Any
|
4
|
+
|
5
|
+
if TYPE_CHECKING:
|
6
|
+
from starlette_testclient import TestClient as ASGIClient
|
7
|
+
|
8
|
+
|
9
|
+
def get_client(app: Any) -> ASGIClient:
|
10
|
+
from starlette_testclient import TestClient as ASGIClient
|
11
|
+
|
12
|
+
return ASGIClient(app)
|