schemathesis 3.25.5__py3-none-any.whl → 4.0.0a1__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 +102 -82
- schemathesis/checks.py +126 -46
- schemathesis/cli/__init__.py +11 -1766
- schemathesis/cli/__main__.py +4 -0
- 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 +35 -0
- schemathesis/cli/commands/run/executor.py +138 -0
- schemathesis/cli/commands/run/filters.py +194 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +746 -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} +103 -174
- schemathesis/cli/constants.py +5 -52
- 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} +39 -10
- 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 -5
- 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 +285 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
- schemathesis/core/output/__init__.py +69 -0
- schemathesis/core/output/sanitization.py +197 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +31 -0
- schemathesis/{internal → core}/result.py +1 -1
- 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 +337 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{cli → engine/phases}/probes.py +63 -70
- schemathesis/engine/phases/stateful/__init__.py +65 -0
- schemathesis/engine/phases/stateful/_executor.py +326 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +174 -0
- schemathesis/engine/phases/unit/_executor.py +321 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +241 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +18 -14
- schemathesis/filters.py +103 -14
- schemathesis/generation/__init__.py +21 -37
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/generation/hypothesis/__init__.py +30 -0
- schemathesis/generation/hypothesis/builder.py +585 -0
- schemathesis/generation/hypothesis/examples.py +50 -0
- 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 +68 -81
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +115 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +99 -67
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +412 -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} +106 -127
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -261
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +25 -0
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +7 -5
- schemathesis/specs/graphql/schemas.py +215 -187
- schemathesis/specs/graphql/validation.py +11 -18
- schemathesis/specs/openapi/__init__.py +7 -1
- schemathesis/specs/openapi/_cache.py +122 -0
- schemathesis/specs/openapi/_hypothesis.py +146 -165
- schemathesis/specs/openapi/checks.py +565 -67
- schemathesis/specs/openapi/converter.py +33 -6
- schemathesis/specs/openapi/definitions.py +11 -18
- schemathesis/specs/openapi/examples.py +153 -39
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +4 -6
- schemathesis/specs/openapi/expressions/extractors.py +23 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +38 -14
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +45 -0
- schemathesis/specs/openapi/links.py +65 -165
- schemathesis/specs/openapi/media_types.py +32 -0
- schemathesis/specs/openapi/negative/__init__.py +7 -3
- schemathesis/specs/openapi/negative/mutations.py +24 -8
- schemathesis/specs/openapi/parameters.py +46 -30
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +47 -57
- schemathesis/specs/openapi/schemas.py +483 -367
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +11 -6
- schemathesis/specs/openapi/stateful/__init__.py +185 -73
- schemathesis/specs/openapi/utils.py +6 -1
- 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} +143 -28
- schemathesis/transport/wsgi.py +165 -0
- schemathesis-4.0.0a1.dist-info/METADATA +297 -0
- schemathesis-4.0.0a1.dist-info/RECORD +152 -0
- {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
- {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -17
- schemathesis/_hypothesis.py +0 -246
- schemathesis/_override.py +0 -49
- schemathesis/cli/cassettes.py +0 -375
- schemathesis/cli/context.py +0 -55
- schemathesis/cli/debug.py +0 -26
- schemathesis/cli/handlers.py +0 -16
- schemathesis/cli/junitxml.py +0 -43
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -765
- schemathesis/cli/output/short.py +0 -40
- schemathesis/cli/sanitization.py +0 -20
- schemathesis/code_samples.py +0 -149
- schemathesis/constants.py +0 -55
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -15
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -560
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -17
- schemathesis/failures.py +0 -209
- schemathesis/fixups/__init__.py +0 -36
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -29
- schemathesis/graphql.py +0 -4
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/copy.py +0 -13
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -34
- schemathesis/internal/jsonschema.py +0 -35
- schemathesis/internal/transformation.py +0 -15
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -361
- schemathesis/loaders.py +0 -120
- schemathesis/models.py +0 -1231
- schemathesis/parameters.py +0 -86
- schemathesis/runner/__init__.py +0 -555
- schemathesis/runner/events.py +0 -309
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -986
- schemathesis/runner/impl/solo.py +0 -90
- schemathesis/runner/impl/threadpool.py +0 -400
- schemathesis/runner/serialization.py +0 -411
- schemathesis/sanitization.py +0 -248
- schemathesis/serializers.py +0 -315
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -201
- schemathesis/service/client.py +0 -100
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -57
- schemathesis/service/hosts.py +0 -107
- schemathesis/service/metadata.py +0 -46
- schemathesis/service/models.py +0 -49
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -184
- schemathesis/service/usage.py +0 -65
- schemathesis/specs/graphql/loaders.py +0 -344
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/loaders.py +0 -667
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis/specs/openapi/validation.py +0 -25
- schemathesis/stateful/__init__.py +0 -133
- schemathesis/targets.py +0 -45
- schemathesis/throttling.py +0 -41
- schemathesis/transports/__init__.py +0 -5
- schemathesis/transports/auth.py +0 -15
- schemathesis/transports/headers.py +0 -35
- schemathesis/transports/responses.py +0 -52
- schemathesis/types.py +0 -35
- schemathesis/utils.py +0 -169
- schemathesis-3.25.5.dist-info/METADATA +0 -356
- schemathesis-3.25.5.dist-info/RECORD +0 -134
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.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,55 +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
|
-
from typing import Any, Callable, Generator, Type,
|
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
|
-
from _pytest.fixtures import FuncFixtureInfo
|
12
|
-
from _pytest.nodes import Node
|
13
11
|
from _pytest.python import Class, Function, FunctionDefinition, Metafunc, Module, PyCollector
|
14
|
-
from hypothesis import reporting
|
15
12
|
from hypothesis.errors import InvalidArgument, Unsatisfiable
|
16
13
|
from jsonschema.exceptions import SchemaError
|
17
14
|
|
18
|
-
from
|
19
|
-
from
|
20
|
-
from ..constants import (
|
21
|
-
GIVEN_AND_EXPLICIT_EXAMPLES_ERROR_MESSAGE,
|
15
|
+
from schemathesis.core.control import SkipTest
|
16
|
+
from schemathesis.core.errors import (
|
22
17
|
RECURSIVE_REFERENCE_ERROR_MESSAGE,
|
23
18
|
SERIALIZERS_SUGGESTION_MESSAGE,
|
24
|
-
|
25
|
-
from ..exceptions import (
|
19
|
+
IncorrectUsage,
|
26
20
|
InvalidHeadersExample,
|
27
|
-
|
28
|
-
|
21
|
+
InvalidRegexPattern,
|
22
|
+
InvalidSchema,
|
29
23
|
SerializationNotPossible,
|
30
|
-
SkipTest,
|
31
|
-
UsageError,
|
32
24
|
)
|
33
|
-
from
|
34
|
-
from
|
35
|
-
from
|
36
|
-
|
37
|
-
|
38
|
-
get_given_args,
|
39
|
-
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,
|
40
30
|
is_given_applied,
|
41
|
-
is_schemathesis_test,
|
42
31
|
merge_given_args,
|
43
32
|
validate_given_args,
|
44
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
|
38
|
+
|
39
|
+
if TYPE_CHECKING:
|
40
|
+
from _pytest.fixtures import FuncFixtureInfo
|
41
|
+
|
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
|
+
|
45
49
|
|
46
|
-
|
50
|
+
def _is_schema(value: object) -> bool:
|
51
|
+
from schemathesis.schemas import BaseSchema
|
47
52
|
|
53
|
+
return isinstance(value, BaseSchema)
|
48
54
|
|
49
|
-
|
50
|
-
|
51
|
-
return cls.from_parent(*args, **kwargs) # type: ignore
|
52
|
-
return cls(*args, **kwargs)
|
55
|
+
|
56
|
+
SchemaHandleMark = Mark["BaseSchema"](attr_name="schema", check=_is_schema)
|
53
57
|
|
54
58
|
|
55
59
|
class SchemathesisFunction(Function):
|
@@ -64,21 +68,15 @@ class SchemathesisFunction(Function):
|
|
64
68
|
self.test_function = test_func
|
65
69
|
self.test_name = test_name
|
66
70
|
|
67
|
-
if not IS_PYTEST_ABOVE_7:
|
68
|
-
# On pytest 7, `self.obj` is already `partial`
|
69
|
-
def _getobj(self) -> partial:
|
70
|
-
"""Tests defined as methods require `self` as the first argument.
|
71
|
-
|
72
|
-
This method is called only for this case.
|
73
|
-
"""
|
74
|
-
return partial(self.obj, self.parent.obj) # type: ignore
|
75
|
-
|
76
71
|
|
77
72
|
class SchemathesisCase(PyCollector):
|
78
|
-
def __init__(self, test_function: Callable, *args: Any, **kwargs: Any) -> None:
|
79
|
-
self.given_kwargs: dict[str, Any]
|
80
|
-
given_args =
|
81
|
-
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
|
82
80
|
|
83
81
|
def _init_with_valid_test(_test_function: Callable, _args: tuple, _kwargs: dict[str, Any]) -> None:
|
84
82
|
self.test_function = _test_function
|
@@ -90,20 +88,18 @@ class SchemathesisCase(PyCollector):
|
|
90
88
|
if failing_test is not None:
|
91
89
|
self.test_function = failing_test
|
92
90
|
self.is_invalid_test = True
|
93
|
-
self.given_kwargs =
|
91
|
+
self.given_kwargs = {}
|
94
92
|
else:
|
95
93
|
_init_with_valid_test(test_function, given_args, given_kwargs)
|
96
94
|
else:
|
97
95
|
_init_with_valid_test(test_function, given_args, given_kwargs)
|
98
|
-
self.
|
96
|
+
self.schema = schema
|
99
97
|
super().__init__(*args, **kwargs)
|
100
98
|
|
101
99
|
def _get_test_name(self, operation: APIOperation) -> str:
|
102
|
-
return f"{self.name}[{operation.
|
100
|
+
return f"{self.name}[{operation.label}]"
|
103
101
|
|
104
|
-
def _gen_items(
|
105
|
-
self, result: Result[APIOperation, OperationSchemaError]
|
106
|
-
) -> Generator[SchemathesisFunction, None, None]:
|
102
|
+
def _gen_items(self, result: Result[APIOperation, InvalidSchema]) -> Generator[SchemathesisFunction, None, None]:
|
107
103
|
"""Generate all tests for the given API operation.
|
108
104
|
|
109
105
|
Could produce more than one test item if
|
@@ -112,7 +108,7 @@ class SchemathesisCase(PyCollector):
|
|
112
108
|
This implementation is based on the original one in pytest, but with slight adjustments
|
113
109
|
to produce tests out of hypothesis ones.
|
114
110
|
"""
|
115
|
-
from
|
111
|
+
from schemathesis.generation.hypothesis.builder import HypothesisTestConfig, create_test
|
116
112
|
|
117
113
|
is_trio_test = False
|
118
114
|
for mark in getattr(self.test_function, "pytestmark", []):
|
@@ -125,24 +121,29 @@ class SchemathesisCase(PyCollector):
|
|
125
121
|
if self.is_invalid_test:
|
126
122
|
funcobj = self.test_function
|
127
123
|
else:
|
128
|
-
override =
|
129
|
-
as_strategy_kwargs: dict | None
|
124
|
+
override = OverrideMark.get(self.test_function)
|
130
125
|
if override is not None:
|
131
126
|
as_strategy_kwargs = {}
|
132
127
|
for location, entry in override.for_operation(operation).items():
|
133
128
|
if entry:
|
134
129
|
as_strategy_kwargs[location] = entry
|
135
130
|
else:
|
136
|
-
as_strategy_kwargs =
|
131
|
+
as_strategy_kwargs = {}
|
137
132
|
funcobj = create_test(
|
138
133
|
operation=operation,
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
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
|
+
),
|
145
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
|
146
147
|
name = self._get_test_name(operation)
|
147
148
|
else:
|
148
149
|
error = result.err()
|
@@ -155,7 +156,9 @@ class SchemathesisCase(PyCollector):
|
|
155
156
|
name += f"[{error.full_path}]"
|
156
157
|
|
157
158
|
cls = self._get_class_parent()
|
158
|
-
definition: FunctionDefinition =
|
159
|
+
definition: FunctionDefinition = FunctionDefinition.from_parent(
|
160
|
+
name=self.name, parent=self.parent, callobj=funcobj
|
161
|
+
)
|
159
162
|
fixturemanager = self.session._fixturemanager
|
160
163
|
fixtureinfo = fixturemanager.getfixtureinfo(definition, funcobj, cls)
|
161
164
|
|
@@ -166,8 +169,7 @@ class SchemathesisCase(PyCollector):
|
|
166
169
|
funcobj = partial(funcobj, self.parent.obj)
|
167
170
|
|
168
171
|
if not metafunc._calls:
|
169
|
-
yield
|
170
|
-
SchemathesisFunction,
|
172
|
+
yield SchemathesisFunction.from_parent(
|
171
173
|
name=name,
|
172
174
|
parent=self.parent,
|
173
175
|
callobj=funcobj,
|
@@ -176,15 +178,12 @@ class SchemathesisCase(PyCollector):
|
|
176
178
|
originalname=self.name,
|
177
179
|
)
|
178
180
|
else:
|
179
|
-
if not IS_PYTEST_ABOVE_8:
|
180
|
-
fixtures.add_funcarg_pseudo_fixture_def(self.parent, metafunc, fixturemanager) # type: ignore[arg-type]
|
181
181
|
fixtureinfo.prune_dependency_tree()
|
182
182
|
for callspec in metafunc._calls:
|
183
183
|
subname = f"{name}[{callspec.id}]"
|
184
|
-
yield
|
185
|
-
|
184
|
+
yield SchemathesisFunction.from_parent(
|
185
|
+
self.parent,
|
186
186
|
name=subname,
|
187
|
-
parent=self.parent,
|
188
187
|
callspec=callspec,
|
189
188
|
callobj=funcobj,
|
190
189
|
fixtureinfo=fixtureinfo,
|
@@ -200,13 +199,10 @@ class SchemathesisCase(PyCollector):
|
|
200
199
|
def _parametrize(self, cls: type | None, definition: FunctionDefinition, fixtureinfo: FuncFixtureInfo) -> Metafunc:
|
201
200
|
parent = self.getparent(Module)
|
202
201
|
module = parent.obj if parent is not None else parent
|
203
|
-
|
204
|
-
|
205
|
-
# Avoiding `Metafunc` is quite problematic for now, as there are quite a lot of internals we rely on
|
206
|
-
kwargs["_ispytest"] = True
|
207
|
-
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)
|
208
204
|
methods = []
|
209
|
-
if hasattr(module, "pytest_generate_tests"):
|
205
|
+
if module is not None and hasattr(module, "pytest_generate_tests"):
|
210
206
|
methods.append(module.pytest_generate_tests)
|
211
207
|
if hasattr(cls, "pytest_generate_tests"):
|
212
208
|
cls = cast(Type, cls)
|
@@ -217,13 +213,7 @@ class SchemathesisCase(PyCollector):
|
|
217
213
|
def collect(self) -> list[Function]: # type: ignore
|
218
214
|
"""Generate different test items for all API operations available in the given schema."""
|
219
215
|
try:
|
220
|
-
items = [
|
221
|
-
item
|
222
|
-
for operation in self.schemathesis_case.get_all_operations(
|
223
|
-
hooks=getattr(self.test_function, "_schemathesis_hooks", None)
|
224
|
-
)
|
225
|
-
for item in self._gen_items(operation)
|
226
|
-
]
|
216
|
+
items = [item for operation in self.schema.get_all_operations() for item in self._gen_items(operation)]
|
227
217
|
if not items:
|
228
218
|
fail_on_no_matches(self.nodeid)
|
229
219
|
return items
|
@@ -231,37 +221,32 @@ class SchemathesisCase(PyCollector):
|
|
231
221
|
pytest.fail("Error during collection")
|
232
222
|
|
233
223
|
|
234
|
-
|
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
|
235
238
|
def pytest_pycollect_makeitem(collector: nodes.Collector, name: str, obj: Any) -> Generator[None, Any, None]:
|
236
239
|
"""Switch to a different collector if the test is parametrized marked by schemathesis."""
|
237
240
|
outcome = yield
|
238
|
-
|
239
|
-
|
240
|
-
|
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:
|
241
246
|
outcome.get_result()
|
242
247
|
|
243
248
|
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
def _should_ignore_entry(value: str) -> bool:
|
248
|
-
return value.startswith(IGNORED_HYPOTHESIS_OUTPUT)
|
249
|
-
|
250
|
-
|
251
|
-
def hypothesis_reporter(value: str) -> None:
|
252
|
-
if _should_ignore_entry(value):
|
253
|
-
return
|
254
|
-
reporting.default(value)
|
255
|
-
|
256
|
-
|
257
|
-
@contextmanager
|
258
|
-
def skip_unnecessary_hypothesis_output() -> Generator:
|
259
|
-
"""Avoid printing Hypothesis output that is not necessary in Schemathesis' pytest plugin."""
|
260
|
-
with reporting.with_reporter(hypothesis_reporter): # type: ignore
|
261
|
-
yield
|
262
|
-
|
263
|
-
|
264
|
-
@hookimpl(hookwrapper=True)
|
249
|
+
@hookimpl(wrapper=True)
|
265
250
|
def pytest_pyfunc_call(pyfuncitem): # type:ignore
|
266
251
|
"""It is possible to have a Hypothesis exception in runtime.
|
267
252
|
|
@@ -269,29 +254,28 @@ def pytest_pyfunc_call(pyfuncitem): # type:ignore
|
|
269
254
|
"""
|
270
255
|
from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
|
271
256
|
|
272
|
-
from
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
257
|
+
from schemathesis.generation.hypothesis.builder import (
|
258
|
+
InvalidHeadersExampleMark,
|
259
|
+
InvalidRegexMark,
|
260
|
+
NonSerializableMark,
|
261
|
+
UnsatisfiableExampleMark,
|
277
262
|
)
|
278
263
|
|
279
264
|
__tracebackhide__ = True
|
280
265
|
if isinstance(pyfuncitem, SchemathesisFunction):
|
281
|
-
with skip_unnecessary_hypothesis_output():
|
282
|
-
outcome = yield
|
283
266
|
try:
|
284
|
-
|
267
|
+
with ignore_hypothesis_output():
|
268
|
+
yield
|
285
269
|
except InvalidArgument as exc:
|
286
270
|
if "Inconsistent args" in str(exc) and "@example()" in str(exc):
|
287
|
-
raise
|
288
|
-
raise
|
271
|
+
raise IncorrectUsage(GIVEN_AND_EXPLICIT_EXAMPLES_ERROR_MESSAGE) from None
|
272
|
+
raise InvalidSchema(exc.args[0]) from None
|
289
273
|
except HypothesisRefResolutionError:
|
290
274
|
pytest.skip(RECURSIVE_REFERENCE_ERROR_MESSAGE)
|
291
275
|
except (SkipTest, unittest.SkipTest) as exc:
|
292
|
-
if
|
276
|
+
if UnsatisfiableExampleMark.is_set(pyfuncitem.obj):
|
293
277
|
raise Unsatisfiable("Failed to generate test cases from examples for this API operation") from None
|
294
|
-
non_serializable =
|
278
|
+
non_serializable = NonSerializableMark.get(pyfuncitem.obj)
|
295
279
|
if non_serializable is not None:
|
296
280
|
media_types = ", ".join(non_serializable.media_types)
|
297
281
|
raise SerializationNotPossible(
|
@@ -299,22 +283,17 @@ def pytest_pyfunc_call(pyfuncitem): # type:ignore
|
|
299
283
|
f" unsupported payload media types: {media_types}\n{SERIALIZERS_SUGGESTION_MESSAGE}",
|
300
284
|
media_types=non_serializable.media_types,
|
301
285
|
) from None
|
302
|
-
invalid_regex =
|
286
|
+
invalid_regex = InvalidRegexMark.get(pyfuncitem.obj)
|
303
287
|
if invalid_regex is not None:
|
304
|
-
raise
|
305
|
-
invalid_headers =
|
288
|
+
raise InvalidRegexPattern.from_schema_error(invalid_regex, from_examples=True) from None
|
289
|
+
invalid_headers = InvalidHeadersExampleMark.get(pyfuncitem.obj)
|
306
290
|
if invalid_headers is not None:
|
307
291
|
raise InvalidHeadersExample.from_headers(invalid_headers) from None
|
308
292
|
pytest.skip(exc.args[0])
|
309
293
|
except SchemaError as exc:
|
310
|
-
raise
|
311
|
-
|
312
|
-
if hasattr(exc, "__notes__"):
|
313
|
-
exc.__notes__ = [note for note in exc.__notes__ if not _should_ignore_entry(note)] # type: ignore
|
314
|
-
raise
|
315
|
-
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)
|
316
296
|
if invalid_headers is not None:
|
317
297
|
raise InvalidHeadersExample.from_headers(invalid_headers) from None
|
318
298
|
else:
|
319
|
-
|
320
|
-
outcome.get_result()
|
299
|
+
yield
|
File without changes
|