schemathesis 3.25.6__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 -1760
- 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/{runner → engine/phases}/probes.py +50 -67
- 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 +139 -23
- 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 +478 -369
- 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.6.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
- {schemathesis-3.25.6.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 -58
- 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 -790
- schemathesis/cli/output/short.py +0 -44
- 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 -1234
- schemathesis/parameters.py +0 -86
- schemathesis/runner/__init__.py +0 -570
- schemathesis/runner/events.py +0 -329
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -1035
- 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 -323
- 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 -199
- 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.6.dist-info/METADATA +0 -356
- schemathesis-3.25.6.dist-info/RECORD +0 -134
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
schemathesis/_hypothesis.py
DELETED
@@ -1,246 +0,0 @@
|
|
1
|
-
"""High-level API for creating Hypothesis tests."""
|
2
|
-
from __future__ import annotations
|
3
|
-
|
4
|
-
import asyncio
|
5
|
-
import warnings
|
6
|
-
from typing import Any, Callable, Generator, Mapping, Optional, Tuple
|
7
|
-
|
8
|
-
import hypothesis
|
9
|
-
from hypothesis import Phase
|
10
|
-
from hypothesis import strategies as st
|
11
|
-
from hypothesis.errors import HypothesisWarning, Unsatisfiable
|
12
|
-
from hypothesis.internal.reflection import proxies
|
13
|
-
from jsonschema.exceptions import SchemaError
|
14
|
-
|
15
|
-
from .auths import get_auth_storage_from_test
|
16
|
-
from .constants import DEFAULT_DEADLINE
|
17
|
-
from .exceptions import OperationSchemaError, SerializationNotPossible
|
18
|
-
from .generation import DataGenerationMethod, GenerationConfig
|
19
|
-
from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher
|
20
|
-
from .models import APIOperation, Case
|
21
|
-
from .transports.content_types import parse_content_type
|
22
|
-
from .transports.headers import has_invalid_characters, is_latin_1_encodable
|
23
|
-
from .utils import GivenInput, combine_strategies
|
24
|
-
|
25
|
-
|
26
|
-
def create_test(
|
27
|
-
*,
|
28
|
-
operation: APIOperation,
|
29
|
-
test: Callable,
|
30
|
-
settings: hypothesis.settings | None = None,
|
31
|
-
seed: int | None = None,
|
32
|
-
data_generation_methods: list[DataGenerationMethod],
|
33
|
-
generation_config: GenerationConfig | None = None,
|
34
|
-
as_strategy_kwargs: dict[str, Any] | None = None,
|
35
|
-
keep_async_fn: bool = False,
|
36
|
-
_given_args: tuple[GivenInput, ...] = (),
|
37
|
-
_given_kwargs: dict[str, GivenInput] | None = None,
|
38
|
-
) -> Callable:
|
39
|
-
"""Create a Hypothesis test."""
|
40
|
-
hook_dispatcher = getattr(test, "_schemathesis_hooks", None)
|
41
|
-
auth_storage = get_auth_storage_from_test(test)
|
42
|
-
strategies = []
|
43
|
-
skip_on_not_negated = len(data_generation_methods) == 1 and DataGenerationMethod.negative in data_generation_methods
|
44
|
-
for data_generation_method in data_generation_methods:
|
45
|
-
strategies.append(
|
46
|
-
operation.as_strategy(
|
47
|
-
hooks=hook_dispatcher,
|
48
|
-
auth_storage=auth_storage,
|
49
|
-
data_generation_method=data_generation_method,
|
50
|
-
generation_config=generation_config,
|
51
|
-
skip_on_not_negated=skip_on_not_negated,
|
52
|
-
**(as_strategy_kwargs or {}),
|
53
|
-
)
|
54
|
-
)
|
55
|
-
strategy = combine_strategies(strategies)
|
56
|
-
_given_kwargs = (_given_kwargs or {}).copy()
|
57
|
-
_given_kwargs.setdefault("case", strategy)
|
58
|
-
|
59
|
-
# Each generated test should be a unique function. It is especially important for the case when Schemathesis runs
|
60
|
-
# tests in multiple threads because Hypothesis stores some internal attributes on function objects and re-writing
|
61
|
-
# them from different threads may lead to unpredictable side-effects.
|
62
|
-
|
63
|
-
@proxies(test) # type: ignore
|
64
|
-
def test_function(*args: Any, **kwargs: Any) -> Any:
|
65
|
-
__tracebackhide__ = True
|
66
|
-
return test(*args, **kwargs)
|
67
|
-
|
68
|
-
wrapped_test = hypothesis.given(*_given_args, **_given_kwargs)(test_function)
|
69
|
-
if seed is not None:
|
70
|
-
wrapped_test = hypothesis.seed(seed)(wrapped_test)
|
71
|
-
if asyncio.iscoroutinefunction(test):
|
72
|
-
# `pytest-trio` expects a coroutine function
|
73
|
-
if keep_async_fn:
|
74
|
-
wrapped_test.hypothesis.inner_test = test # type: ignore
|
75
|
-
else:
|
76
|
-
wrapped_test.hypothesis.inner_test = make_async_test(test) # type: ignore
|
77
|
-
setup_default_deadline(wrapped_test)
|
78
|
-
if settings is not None:
|
79
|
-
wrapped_test = settings(wrapped_test)
|
80
|
-
existing_settings = _get_hypothesis_settings(wrapped_test)
|
81
|
-
if existing_settings is not None:
|
82
|
-
existing_settings = remove_explain_phase(existing_settings)
|
83
|
-
wrapped_test._hypothesis_internal_use_settings = existing_settings # type: ignore
|
84
|
-
if Phase.explicit in existing_settings.phases:
|
85
|
-
wrapped_test = add_examples(wrapped_test, operation, hook_dispatcher=hook_dispatcher)
|
86
|
-
return wrapped_test
|
87
|
-
|
88
|
-
|
89
|
-
def setup_default_deadline(wrapped_test: Callable) -> None:
|
90
|
-
# Quite hacky, but it is the simplest way to set up the default deadline value without affecting non-Schemathesis
|
91
|
-
# tests globally
|
92
|
-
existing_settings = _get_hypothesis_settings(wrapped_test)
|
93
|
-
if existing_settings is not None and existing_settings.deadline == hypothesis.settings.default.deadline:
|
94
|
-
with warnings.catch_warnings():
|
95
|
-
warnings.simplefilter("ignore", HypothesisWarning)
|
96
|
-
new_settings = hypothesis.settings(existing_settings, deadline=DEFAULT_DEADLINE)
|
97
|
-
wrapped_test._hypothesis_internal_use_settings = new_settings # type: ignore
|
98
|
-
|
99
|
-
|
100
|
-
def remove_explain_phase(settings: hypothesis.settings) -> hypothesis.settings:
|
101
|
-
# The "explain" phase is not supported
|
102
|
-
if Phase.explain in settings.phases:
|
103
|
-
phases = tuple(phase for phase in settings.phases if phase != Phase.explain)
|
104
|
-
return hypothesis.settings(settings, phases=phases)
|
105
|
-
return settings
|
106
|
-
|
107
|
-
|
108
|
-
def _get_hypothesis_settings(test: Callable) -> hypothesis.settings | None:
|
109
|
-
return getattr(test, "_hypothesis_internal_use_settings", None)
|
110
|
-
|
111
|
-
|
112
|
-
def make_async_test(test: Callable) -> Callable:
|
113
|
-
def async_run(*args: Any, **kwargs: Any) -> None:
|
114
|
-
try:
|
115
|
-
loop = asyncio.get_event_loop()
|
116
|
-
except RuntimeError:
|
117
|
-
loop = asyncio.new_event_loop()
|
118
|
-
coro = test(*args, **kwargs)
|
119
|
-
future = asyncio.ensure_future(coro, loop=loop)
|
120
|
-
loop.run_until_complete(future)
|
121
|
-
|
122
|
-
return async_run
|
123
|
-
|
124
|
-
|
125
|
-
def add_examples(test: Callable, operation: APIOperation, hook_dispatcher: HookDispatcher | None = None) -> Callable:
|
126
|
-
"""Add examples to the Hypothesis test, if they are specified in the schema."""
|
127
|
-
from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
|
128
|
-
|
129
|
-
try:
|
130
|
-
examples: list[Case] = [get_single_example(strategy) for strategy in operation.get_strategies_from_examples()]
|
131
|
-
except (
|
132
|
-
OperationSchemaError,
|
133
|
-
HypothesisRefResolutionError,
|
134
|
-
Unsatisfiable,
|
135
|
-
SerializationNotPossible,
|
136
|
-
SchemaError,
|
137
|
-
) as exc:
|
138
|
-
# Invalid schema:
|
139
|
-
# In this case, the user didn't pass `--validate-schema=false` and see an error in the output anyway,
|
140
|
-
# and no tests will be executed. For this reason, examples can be skipped
|
141
|
-
# Recursive references: This test will be skipped anyway
|
142
|
-
# Unsatisfiable:
|
143
|
-
# The underlying schema is not satisfiable and test will raise an error for the same reason.
|
144
|
-
# Skipping this exception here allows us to continue the testing process for other operations.
|
145
|
-
# Still, we allow running user-defined hooks
|
146
|
-
examples = []
|
147
|
-
if isinstance(exc, Unsatisfiable):
|
148
|
-
add_unsatisfied_example_mark(test, exc)
|
149
|
-
if isinstance(exc, SerializationNotPossible):
|
150
|
-
add_non_serializable_mark(test, exc)
|
151
|
-
if isinstance(exc, SchemaError):
|
152
|
-
add_invalid_regex_mark(test, exc)
|
153
|
-
context = HookContext(operation) # context should be passed here instead
|
154
|
-
GLOBAL_HOOK_DISPATCHER.dispatch("before_add_examples", context, examples)
|
155
|
-
operation.schema.hooks.dispatch("before_add_examples", context, examples)
|
156
|
-
if hook_dispatcher:
|
157
|
-
hook_dispatcher.dispatch("before_add_examples", context, examples)
|
158
|
-
original_test = test
|
159
|
-
for example in examples:
|
160
|
-
if example.headers is not None:
|
161
|
-
invalid_headers = dict(find_invalid_headers(example.headers))
|
162
|
-
if invalid_headers:
|
163
|
-
add_invalid_example_header_mark(original_test, invalid_headers)
|
164
|
-
continue
|
165
|
-
if example.media_type is not None:
|
166
|
-
try:
|
167
|
-
media_type = parse_content_type(example.media_type)
|
168
|
-
if media_type == ("application", "x-www-form-urlencoded"):
|
169
|
-
example.body = prepare_urlencoded(example.body)
|
170
|
-
except ValueError:
|
171
|
-
pass
|
172
|
-
test = hypothesis.example(case=example)(test)
|
173
|
-
return test
|
174
|
-
|
175
|
-
|
176
|
-
def find_invalid_headers(headers: Mapping) -> Generator[Tuple[str, str], None, None]:
|
177
|
-
for name, value in headers.items():
|
178
|
-
if not is_latin_1_encodable(value) or has_invalid_characters(name, value):
|
179
|
-
yield name, value
|
180
|
-
|
181
|
-
|
182
|
-
def prepare_urlencoded(data: Any) -> Any:
|
183
|
-
if isinstance(data, list):
|
184
|
-
output = []
|
185
|
-
for item in data:
|
186
|
-
if isinstance(item, dict):
|
187
|
-
for key, value in item.items():
|
188
|
-
output.append((key, value))
|
189
|
-
else:
|
190
|
-
output.append(item)
|
191
|
-
return output
|
192
|
-
return data
|
193
|
-
|
194
|
-
|
195
|
-
def add_unsatisfied_example_mark(test: Callable, exc: Unsatisfiable) -> None:
|
196
|
-
test._schemathesis_unsatisfied_example = exc # type: ignore
|
197
|
-
|
198
|
-
|
199
|
-
def has_unsatisfied_example_mark(test: Callable) -> bool:
|
200
|
-
return hasattr(test, "_schemathesis_unsatisfied_example")
|
201
|
-
|
202
|
-
|
203
|
-
def add_non_serializable_mark(test: Callable, exc: SerializationNotPossible) -> None:
|
204
|
-
test._schemathesis_non_serializable = exc # type: ignore
|
205
|
-
|
206
|
-
|
207
|
-
def get_non_serializable_mark(test: Callable) -> Optional[SerializationNotPossible]:
|
208
|
-
return getattr(test, "_schemathesis_non_serializable", None)
|
209
|
-
|
210
|
-
|
211
|
-
def get_invalid_regex_mark(test: Callable) -> Optional[SchemaError]:
|
212
|
-
return getattr(test, "_schemathesis_invalid_regex", None)
|
213
|
-
|
214
|
-
|
215
|
-
def add_invalid_regex_mark(test: Callable, exc: SchemaError) -> None:
|
216
|
-
test._schemathesis_invalid_regex = exc # type: ignore
|
217
|
-
|
218
|
-
|
219
|
-
def get_invalid_example_headers_mark(test: Callable) -> Optional[dict[str, str]]:
|
220
|
-
return getattr(test, "_schemathesis_invalid_example_headers", None)
|
221
|
-
|
222
|
-
|
223
|
-
def add_invalid_example_header_mark(test: Callable, headers: dict[str, str]) -> None:
|
224
|
-
test._schemathesis_invalid_example_headers = headers # type: ignore
|
225
|
-
|
226
|
-
|
227
|
-
def get_single_example(strategy: st.SearchStrategy[Case]) -> Case:
|
228
|
-
examples: list[Case] = []
|
229
|
-
add_single_example(strategy, examples)
|
230
|
-
return examples[0]
|
231
|
-
|
232
|
-
|
233
|
-
def add_single_example(strategy: st.SearchStrategy[Case], examples: list[Case]) -> None:
|
234
|
-
@hypothesis.given(strategy) # type: ignore
|
235
|
-
@hypothesis.settings( # type: ignore
|
236
|
-
database=None,
|
237
|
-
max_examples=1,
|
238
|
-
deadline=None,
|
239
|
-
verbosity=hypothesis.Verbosity.quiet,
|
240
|
-
phases=(hypothesis.Phase.generate,),
|
241
|
-
suppress_health_check=list(hypothesis.HealthCheck),
|
242
|
-
)
|
243
|
-
def example_generating_inner_function(ex: Case) -> None:
|
244
|
-
examples.append(ex)
|
245
|
-
|
246
|
-
example_generating_inner_function()
|
schemathesis/_override.py
DELETED
@@ -1,49 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
from dataclasses import dataclass
|
3
|
-
from typing import TYPE_CHECKING, Optional
|
4
|
-
|
5
|
-
from .exceptions import UsageError
|
6
|
-
from .parameters import ParameterSet
|
7
|
-
from .types import GenericTest
|
8
|
-
|
9
|
-
if TYPE_CHECKING:
|
10
|
-
from .models import APIOperation
|
11
|
-
|
12
|
-
|
13
|
-
@dataclass
|
14
|
-
class CaseOverride:
|
15
|
-
"""Overrides for various parts of a test case."""
|
16
|
-
|
17
|
-
query: dict[str, str]
|
18
|
-
headers: dict[str, str]
|
19
|
-
cookies: dict[str, str]
|
20
|
-
path_parameters: dict[str, str]
|
21
|
-
|
22
|
-
def for_operation(self, operation: APIOperation) -> dict[str, dict[str, str]]:
|
23
|
-
return {
|
24
|
-
"query": (_for_parameters(self.query, operation.query)),
|
25
|
-
"headers": (_for_parameters(self.headers, operation.headers)),
|
26
|
-
"cookies": (_for_parameters(self.cookies, operation.cookies)),
|
27
|
-
"path_parameters": (_for_parameters(self.path_parameters, operation.path_parameters)),
|
28
|
-
}
|
29
|
-
|
30
|
-
|
31
|
-
def _for_parameters(overridden: dict[str, str], defined: ParameterSet) -> dict[str, str]:
|
32
|
-
output = {}
|
33
|
-
for param in defined:
|
34
|
-
if param.name in overridden:
|
35
|
-
output[param.name] = overridden[param.name]
|
36
|
-
return output
|
37
|
-
|
38
|
-
|
39
|
-
def get_override_from_mark(test: GenericTest) -> Optional[CaseOverride]:
|
40
|
-
return getattr(test, "_schemathesis_override", None)
|
41
|
-
|
42
|
-
|
43
|
-
def set_override_mark(test: GenericTest, override: CaseOverride) -> None:
|
44
|
-
test._schemathesis_override = override # type: ignore[attr-defined]
|
45
|
-
|
46
|
-
|
47
|
-
def check_no_override_mark(test: GenericTest) -> None:
|
48
|
-
if hasattr(test, "_schemathesis_override"):
|
49
|
-
raise UsageError(f"`{test.__name__}` has already been decorated with `override`.")
|
schemathesis/cli/cassettes.py
DELETED
@@ -1,375 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
import base64
|
3
|
-
import json
|
4
|
-
import re
|
5
|
-
import sys
|
6
|
-
import threading
|
7
|
-
from dataclasses import dataclass, field
|
8
|
-
from queue import Queue
|
9
|
-
from typing import IO, Any, Generator, Iterator, cast, TYPE_CHECKING
|
10
|
-
|
11
|
-
from ..constants import SCHEMATHESIS_VERSION
|
12
|
-
from ..runner import events
|
13
|
-
from ..types import RequestCert
|
14
|
-
from .handlers import EventHandler
|
15
|
-
|
16
|
-
if TYPE_CHECKING:
|
17
|
-
import click
|
18
|
-
import requests
|
19
|
-
from ..models import Request, Response
|
20
|
-
from ..runner.serialization import SerializedCheck, SerializedInteraction
|
21
|
-
from .context import ExecutionContext
|
22
|
-
from ..generation import DataGenerationMethod
|
23
|
-
|
24
|
-
# Wait until the worker terminates
|
25
|
-
WRITER_WORKER_JOIN_TIMEOUT = 1
|
26
|
-
|
27
|
-
|
28
|
-
@dataclass
|
29
|
-
class CassetteWriter(EventHandler):
|
30
|
-
"""Write interactions in a YAML cassette.
|
31
|
-
|
32
|
-
A low-level interface is used to write data to YAML file during the test run and reduce the delay at
|
33
|
-
the end of the test run.
|
34
|
-
"""
|
35
|
-
|
36
|
-
file_handle: click.utils.LazyFile
|
37
|
-
preserve_exact_body_bytes: bool
|
38
|
-
queue: Queue = field(default_factory=Queue)
|
39
|
-
worker: threading.Thread = field(init=False)
|
40
|
-
|
41
|
-
def __post_init__(self) -> None:
|
42
|
-
self.worker = threading.Thread(
|
43
|
-
target=worker,
|
44
|
-
kwargs={
|
45
|
-
"file_handle": self.file_handle,
|
46
|
-
"preserve_exact_body_bytes": self.preserve_exact_body_bytes,
|
47
|
-
"queue": self.queue,
|
48
|
-
},
|
49
|
-
)
|
50
|
-
self.worker.start()
|
51
|
-
|
52
|
-
def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
|
53
|
-
if isinstance(event, events.Initialized):
|
54
|
-
# In the beginning we write metadata and start `http_interactions` list
|
55
|
-
self.queue.put(Initialize())
|
56
|
-
if isinstance(event, events.AfterExecution):
|
57
|
-
# Seed is always present at this point, the original Optional[int] type is there because `TestResult`
|
58
|
-
# instance is created before `seed` is generated on the hypothesis side
|
59
|
-
seed = cast(int, event.result.seed)
|
60
|
-
self.queue.put(
|
61
|
-
Process(
|
62
|
-
seed=seed,
|
63
|
-
correlation_id=event.correlation_id,
|
64
|
-
thread_id=event.thread_id,
|
65
|
-
# NOTE: For backward compatibility reasons AfterExecution stores a list of data generation methods
|
66
|
-
# The list always contains one element - the method that was actually used for generation
|
67
|
-
# This will change in the future
|
68
|
-
data_generation_method=event.data_generation_method[0],
|
69
|
-
interactions=event.result.interactions,
|
70
|
-
)
|
71
|
-
)
|
72
|
-
if isinstance(event, events.Finished):
|
73
|
-
self.shutdown()
|
74
|
-
|
75
|
-
def shutdown(self) -> None:
|
76
|
-
self.queue.put(Finalize())
|
77
|
-
self._stop_worker()
|
78
|
-
|
79
|
-
def _stop_worker(self) -> None:
|
80
|
-
self.worker.join(WRITER_WORKER_JOIN_TIMEOUT)
|
81
|
-
|
82
|
-
|
83
|
-
@dataclass
|
84
|
-
class Initialize:
|
85
|
-
"""Start up, the first message to make preparations before proceeding the input data."""
|
86
|
-
|
87
|
-
|
88
|
-
@dataclass
|
89
|
-
class Process:
|
90
|
-
"""A new chunk of data should be processed."""
|
91
|
-
|
92
|
-
seed: int
|
93
|
-
correlation_id: str
|
94
|
-
thread_id: int
|
95
|
-
data_generation_method: DataGenerationMethod
|
96
|
-
interactions: list[SerializedInteraction]
|
97
|
-
|
98
|
-
|
99
|
-
@dataclass
|
100
|
-
class Finalize:
|
101
|
-
"""The work is done and there will be no more messages to process."""
|
102
|
-
|
103
|
-
|
104
|
-
def get_command_representation() -> str:
|
105
|
-
"""Get how Schemathesis was run."""
|
106
|
-
# It is supposed to be executed from Schemathesis CLI, not via Click's `command.invoke`
|
107
|
-
if not sys.argv[0].endswith(("schemathesis", "st")):
|
108
|
-
return "<unknown entrypoint>"
|
109
|
-
args = " ".join(sys.argv[1:])
|
110
|
-
return f"st {args}"
|
111
|
-
|
112
|
-
|
113
|
-
def worker(file_handle: click.utils.LazyFile, preserve_exact_body_bytes: bool, queue: Queue) -> None:
|
114
|
-
"""Write YAML to a file in an incremental manner.
|
115
|
-
|
116
|
-
This implementation doesn't use `pyyaml` package and composes YAML manually as string due to the following reasons:
|
117
|
-
- It is much faster. The string-based approach gives only ~2.5% time overhead when `yaml.CDumper` has ~11.2%;
|
118
|
-
- Implementation complexity. We have a quite simple format where almost all values are strings, and it is much
|
119
|
-
simpler to implement it with string composition rather than with adjusting `yaml.Serializer` to emit explicit
|
120
|
-
types. Another point is that with `pyyaml` we need to emit events and handle some low-level details like
|
121
|
-
providing tags, anchors to have incremental writing, with primitive types it is much simpler.
|
122
|
-
"""
|
123
|
-
current_id = 1
|
124
|
-
stream = file_handle.open()
|
125
|
-
|
126
|
-
def format_header_values(values: list[str]) -> str:
|
127
|
-
return "\n".join(f" - {json.dumps(v)}" for v in values)
|
128
|
-
|
129
|
-
def format_headers(headers: dict[str, list[str]]) -> str:
|
130
|
-
return "\n".join(f' "{name}":\n{format_header_values(values)}' for name, values in headers.items())
|
131
|
-
|
132
|
-
def format_check_message(message: str | None) -> str:
|
133
|
-
return "~" if message is None else f"{repr(message)}"
|
134
|
-
|
135
|
-
def format_checks(checks: list[SerializedCheck]) -> str:
|
136
|
-
return "\n".join(
|
137
|
-
f" - name: '{check.name}'\n status: '{check.value.name.upper()}'\n message: {format_check_message(check.message)}"
|
138
|
-
for check in checks
|
139
|
-
)
|
140
|
-
|
141
|
-
if preserve_exact_body_bytes:
|
142
|
-
|
143
|
-
def format_request_body(output: IO, request: Request) -> None:
|
144
|
-
if request.body is not None:
|
145
|
-
output.write(
|
146
|
-
f"""
|
147
|
-
body:
|
148
|
-
encoding: 'utf-8'
|
149
|
-
base64_string: '{request.body}'"""
|
150
|
-
)
|
151
|
-
|
152
|
-
def format_response_body(output: IO, response: Response) -> None:
|
153
|
-
if response.body is not None:
|
154
|
-
output.write(
|
155
|
-
f""" body:
|
156
|
-
encoding: '{response.encoding}'
|
157
|
-
base64_string: '{response.body}'"""
|
158
|
-
)
|
159
|
-
|
160
|
-
else:
|
161
|
-
|
162
|
-
def format_request_body(output: IO, request: Request) -> None:
|
163
|
-
if request.body is not None:
|
164
|
-
string = _safe_decode(request.body, "utf8")
|
165
|
-
output.write(
|
166
|
-
"""
|
167
|
-
body:
|
168
|
-
encoding: 'utf-8'
|
169
|
-
string: """
|
170
|
-
)
|
171
|
-
write_double_quoted(output, string)
|
172
|
-
|
173
|
-
def format_response_body(output: IO, response: Response) -> None:
|
174
|
-
if response.body is not None:
|
175
|
-
encoding = response.encoding or "utf8"
|
176
|
-
string = _safe_decode(response.body, encoding)
|
177
|
-
output.write(
|
178
|
-
f""" body:
|
179
|
-
encoding: '{encoding}'
|
180
|
-
string: """
|
181
|
-
)
|
182
|
-
write_double_quoted(output, string)
|
183
|
-
|
184
|
-
while True:
|
185
|
-
item = queue.get()
|
186
|
-
if isinstance(item, Initialize):
|
187
|
-
stream.write(
|
188
|
-
f"""command: '{get_command_representation()}'
|
189
|
-
recorded_with: 'Schemathesis {SCHEMATHESIS_VERSION}'
|
190
|
-
http_interactions:"""
|
191
|
-
)
|
192
|
-
elif isinstance(item, Process):
|
193
|
-
for interaction in item.interactions:
|
194
|
-
status = interaction.status.name.upper()
|
195
|
-
# Body payloads are handled via separate `stream.write` calls to avoid some allocations
|
196
|
-
stream.write(
|
197
|
-
f"""\n- id: '{current_id}'
|
198
|
-
status: '{status}'
|
199
|
-
seed: '{item.seed}'
|
200
|
-
thread_id: {item.thread_id}
|
201
|
-
correlation_id: '{item.correlation_id}'
|
202
|
-
data_generation_method: '{item.data_generation_method.value}'
|
203
|
-
elapsed: '{interaction.response.elapsed}'
|
204
|
-
recorded_at: '{interaction.recorded_at}'
|
205
|
-
checks:
|
206
|
-
{format_checks(interaction.checks)}
|
207
|
-
request:
|
208
|
-
uri: '{interaction.request.uri}'
|
209
|
-
method: '{interaction.request.method}'
|
210
|
-
headers:
|
211
|
-
{format_headers(interaction.request.headers)}"""
|
212
|
-
)
|
213
|
-
format_request_body(stream, interaction.request)
|
214
|
-
stream.write(
|
215
|
-
f"""
|
216
|
-
response:
|
217
|
-
status:
|
218
|
-
code: '{interaction.response.status_code}'
|
219
|
-
message: {json.dumps(interaction.response.message)}
|
220
|
-
headers:
|
221
|
-
{format_headers(interaction.response.headers)}
|
222
|
-
"""
|
223
|
-
)
|
224
|
-
format_response_body(stream, interaction.response)
|
225
|
-
stream.write(
|
226
|
-
f"""
|
227
|
-
http_version: '{interaction.response.http_version}'"""
|
228
|
-
)
|
229
|
-
current_id += 1
|
230
|
-
else:
|
231
|
-
break
|
232
|
-
file_handle.close()
|
233
|
-
|
234
|
-
|
235
|
-
def _safe_decode(value: str, encoding: str) -> str:
|
236
|
-
"""Decode base64-encoded body bytes as a string."""
|
237
|
-
return base64.b64decode(value).decode(encoding, "replace")
|
238
|
-
|
239
|
-
|
240
|
-
def write_double_quoted(stream: IO, text: str) -> None:
|
241
|
-
"""Writes a valid YAML string enclosed in double quotes."""
|
242
|
-
from yaml.emitter import Emitter
|
243
|
-
|
244
|
-
# Adapted from `yaml.Emitter.write_double_quoted`:
|
245
|
-
# - Doesn't split the string, therefore doesn't track the current column
|
246
|
-
# - Doesn't encode the input
|
247
|
-
# - Allows Unicode unconditionally
|
248
|
-
stream.write('"')
|
249
|
-
start = end = 0
|
250
|
-
length = len(text)
|
251
|
-
while end <= length:
|
252
|
-
ch = None
|
253
|
-
if end < length:
|
254
|
-
ch = text[end]
|
255
|
-
if (
|
256
|
-
ch is None
|
257
|
-
or ch in '"\\\x85\u2028\u2029\uFEFF'
|
258
|
-
or not ("\x20" <= ch <= "\x7E" or ("\xA0" <= ch <= "\uD7FF" or "\uE000" <= ch <= "\uFFFD"))
|
259
|
-
):
|
260
|
-
if start < end:
|
261
|
-
stream.write(text[start:end])
|
262
|
-
start = end
|
263
|
-
if ch is not None:
|
264
|
-
# Escape character
|
265
|
-
if ch in Emitter.ESCAPE_REPLACEMENTS:
|
266
|
-
data = "\\" + Emitter.ESCAPE_REPLACEMENTS[ch]
|
267
|
-
elif ch <= "\xFF":
|
268
|
-
data = "\\x%02X" % ord(ch)
|
269
|
-
elif ch <= "\uFFFF":
|
270
|
-
data = "\\u%04X" % ord(ch)
|
271
|
-
else:
|
272
|
-
data = "\\U%08X" % ord(ch)
|
273
|
-
stream.write(data)
|
274
|
-
start = end + 1
|
275
|
-
end += 1
|
276
|
-
stream.write('"')
|
277
|
-
|
278
|
-
|
279
|
-
@dataclass
|
280
|
-
class Replayed:
|
281
|
-
interaction: dict[str, Any]
|
282
|
-
response: requests.Response
|
283
|
-
|
284
|
-
|
285
|
-
def replay(
|
286
|
-
cassette: dict[str, Any],
|
287
|
-
id_: str | None = None,
|
288
|
-
status: str | None = None,
|
289
|
-
uri: str | None = None,
|
290
|
-
method: str | None = None,
|
291
|
-
request_tls_verify: bool = True,
|
292
|
-
request_cert: RequestCert | None = None,
|
293
|
-
request_proxy: str | None = None,
|
294
|
-
) -> Generator[Replayed, None, None]:
|
295
|
-
"""Replay saved interactions."""
|
296
|
-
import requests
|
297
|
-
|
298
|
-
session = requests.Session()
|
299
|
-
session.verify = request_tls_verify
|
300
|
-
session.cert = request_cert
|
301
|
-
kwargs = {}
|
302
|
-
if request_proxy is not None:
|
303
|
-
kwargs["proxies"] = {"all": request_proxy}
|
304
|
-
for interaction in filter_cassette(cassette["http_interactions"], id_, status, uri, method):
|
305
|
-
request = get_prepared_request(interaction["request"])
|
306
|
-
response = session.send(request, **kwargs) # type: ignore
|
307
|
-
yield Replayed(interaction, response)
|
308
|
-
|
309
|
-
|
310
|
-
def filter_cassette(
|
311
|
-
interactions: list[dict[str, Any]],
|
312
|
-
id_: str | None = None,
|
313
|
-
status: str | None = None,
|
314
|
-
uri: str | None = None,
|
315
|
-
method: str | None = None,
|
316
|
-
) -> Iterator[dict[str, Any]]:
|
317
|
-
filters = []
|
318
|
-
|
319
|
-
def id_filter(item: dict[str, Any]) -> bool:
|
320
|
-
return item["id"] == id_
|
321
|
-
|
322
|
-
def status_filter(item: dict[str, Any]) -> bool:
|
323
|
-
status_ = cast(str, status)
|
324
|
-
return item["status"].upper() == status_.upper()
|
325
|
-
|
326
|
-
def uri_filter(item: dict[str, Any]) -> bool:
|
327
|
-
uri_ = cast(str, uri)
|
328
|
-
return bool(re.search(uri_, item["request"]["uri"]))
|
329
|
-
|
330
|
-
def method_filter(item: dict[str, Any]) -> bool:
|
331
|
-
method_ = cast(str, method)
|
332
|
-
return bool(re.search(method_, item["request"]["method"]))
|
333
|
-
|
334
|
-
if id_ is not None:
|
335
|
-
filters.append(id_filter)
|
336
|
-
|
337
|
-
if status is not None:
|
338
|
-
filters.append(status_filter)
|
339
|
-
|
340
|
-
if uri is not None:
|
341
|
-
filters.append(uri_filter)
|
342
|
-
|
343
|
-
if method is not None:
|
344
|
-
filters.append(method_filter)
|
345
|
-
|
346
|
-
def is_match(interaction: dict[str, Any]) -> bool:
|
347
|
-
return all(filter_(interaction) for filter_ in filters)
|
348
|
-
|
349
|
-
return filter(is_match, interactions)
|
350
|
-
|
351
|
-
|
352
|
-
def get_prepared_request(data: dict[str, Any]) -> requests.PreparedRequest:
|
353
|
-
"""Create a `requests.PreparedRequest` from a serialized one."""
|
354
|
-
from requests.structures import CaseInsensitiveDict
|
355
|
-
from requests.cookies import RequestsCookieJar
|
356
|
-
import requests
|
357
|
-
|
358
|
-
prepared = requests.PreparedRequest()
|
359
|
-
prepared.method = data["method"]
|
360
|
-
prepared.url = data["uri"]
|
361
|
-
prepared._cookies = RequestsCookieJar() # type: ignore
|
362
|
-
if "body" in data:
|
363
|
-
body = data["body"]
|
364
|
-
if "base64_string" in body:
|
365
|
-
content = body["base64_string"]
|
366
|
-
if content:
|
367
|
-
prepared.body = base64.b64decode(content)
|
368
|
-
else:
|
369
|
-
content = body["string"]
|
370
|
-
if content:
|
371
|
-
prepared.body = content.encode("utf8")
|
372
|
-
# There is always 1 value in a request
|
373
|
-
headers = [(key, value[0]) for key, value in data["headers"].items()]
|
374
|
-
prepared.headers = CaseInsensitiveDict(headers)
|
375
|
-
return prepared
|