schemathesis 3.39.15__py3-none-any.whl → 4.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +41 -79
- schemathesis/auths.py +111 -122
- schemathesis/checks.py +169 -60
- schemathesis/cli/__init__.py +15 -2117
- schemathesis/cli/commands/__init__.py +85 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +590 -0
- schemathesis/cli/commands/run/context.py +204 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
- schemathesis/cli/commands/run/handlers/output.py +1628 -0
- schemathesis/cli/commands/run/loaders.py +114 -0
- schemathesis/cli/commands/run/validation.py +246 -0
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +84 -0
- schemathesis/cli/{options.py → ext/options.py} +36 -34
- schemathesis/config/__init__.py +189 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +99 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +156 -0
- schemathesis/config/_generation.py +149 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +327 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +187 -0
- schemathesis/config/_projects.py +527 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +885 -0
- schemathesis/core/__init__.py +67 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +459 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/{throttling.py → core/rate_limit.py} +16 -17
- schemathesis/core/registries.py +31 -0
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +54 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +118 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +169 -0
- schemathesis/engine/errors.py +464 -0
- schemathesis/engine/events.py +258 -0
- schemathesis/engine/phases/__init__.py +88 -0
- schemathesis/{runner → engine/phases}/probes.py +52 -68
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +356 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +212 -0
- schemathesis/engine/phases/unit/_executor.py +416 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +247 -0
- schemathesis/errors.py +43 -0
- schemathesis/filters.py +17 -98
- schemathesis/generation/__init__.py +5 -33
- schemathesis/generation/case.py +317 -0
- schemathesis/generation/coverage.py +282 -175
- schemathesis/generation/hypothesis/__init__.py +36 -0
- schemathesis/generation/hypothesis/builder.py +800 -0
- schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +116 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +278 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +284 -0
- schemathesis/hooks.py +80 -101
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +455 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +313 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +281 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -273
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +42 -6
- schemathesis/specs/graphql/schemas.py +141 -137
- schemathesis/specs/graphql/validation.py +11 -17
- schemathesis/specs/openapi/__init__.py +6 -1
- schemathesis/specs/openapi/_cache.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +142 -156
- schemathesis/specs/openapi/checks.py +368 -257
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +23 -21
- schemathesis/specs/openapi/expressions/__init__.py +31 -19
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/lexer.py +1 -1
- schemathesis/specs/openapi/expressions/nodes.py +36 -41
- schemathesis/specs/openapi/expressions/parser.py +1 -1
- schemathesis/specs/openapi/formats.py +35 -7
- schemathesis/specs/openapi/media_types.py +53 -12
- schemathesis/specs/openapi/negative/__init__.py +7 -4
- schemathesis/specs/openapi/negative/mutations.py +6 -5
- schemathesis/specs/openapi/parameters.py +7 -10
- schemathesis/specs/openapi/patterns.py +94 -31
- schemathesis/specs/openapi/references.py +12 -53
- schemathesis/specs/openapi/schemas.py +238 -308
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +12 -6
- schemathesis/specs/openapi/stateful/__init__.py +268 -133
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/links.py +209 -0
- schemathesis/transport/__init__.py +142 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +124 -0
- schemathesis/transport/requests.py +244 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -11
- schemathesis/transport/wsgi.py +171 -0
- schemathesis-4.0.0.dist-info/METADATA +204 -0
- schemathesis-4.0.0.dist-info/RECORD +164 -0
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -712
- schemathesis/_override.py +0 -50
- schemathesis/_patches.py +0 -21
- schemathesis/_rate_limiter.py +0 -7
- schemathesis/cli/callbacks.py +0 -466
- schemathesis/cli/cassettes.py +0 -561
- schemathesis/cli/context.py +0 -75
- schemathesis/cli/debug.py +0 -27
- schemathesis/cli/handlers.py +0 -19
- schemathesis/cli/junitxml.py +0 -124
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -920
- schemathesis/cli/output/short.py +0 -59
- schemathesis/cli/reporting.py +0 -79
- schemathesis/cli/sanitization.py +0 -26
- schemathesis/code_samples.py +0 -151
- schemathesis/constants.py +0 -54
- schemathesis/contrib/__init__.py +0 -11
- schemathesis/contrib/openapi/__init__.py +0 -11
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -16
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -571
- schemathesis/experimental/__init__.py +0 -109
- schemathesis/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -284
- schemathesis/fixups/__init__.py +0 -37
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -28
- schemathesis/generation/_methods.py +0 -44
- schemathesis/graphql.py +0 -3
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/checks.py +0 -86
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -37
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- schemathesis/internal/output.py +0 -68
- schemathesis/internal/transformation.py +0 -26
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -474
- schemathesis/loaders.py +0 -122
- schemathesis/models.py +0 -1341
- schemathesis/parameters.py +0 -90
- schemathesis/runner/__init__.py +0 -605
- schemathesis/runner/events.py +0 -389
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/context.py +0 -88
- schemathesis/runner/impl/core.py +0 -1280
- schemathesis/runner/impl/solo.py +0 -80
- schemathesis/runner/impl/threadpool.py +0 -391
- schemathesis/runner/serialization.py +0 -544
- schemathesis/sanitization.py +0 -252
- schemathesis/serializers.py +0 -328
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -202
- schemathesis/service/client.py +0 -133
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -61
- schemathesis/service/extensions.py +0 -224
- schemathesis/service/hosts.py +0 -111
- schemathesis/service/metadata.py +0 -71
- schemathesis/service/models.py +0 -258
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -173
- schemathesis/service/usage.py +0 -66
- schemathesis/specs/graphql/loaders.py +0 -364
- schemathesis/specs/openapi/expressions/context.py +0 -16
- schemathesis/specs/openapi/links.py +0 -389
- schemathesis/specs/openapi/loaders.py +0 -707
- schemathesis/specs/openapi/stateful/statistic.py +0 -198
- schemathesis/specs/openapi/stateful/types.py +0 -14
- schemathesis/specs/openapi/validation.py +0 -26
- schemathesis/stateful/__init__.py +0 -147
- schemathesis/stateful/config.py +0 -97
- schemathesis/stateful/context.py +0 -135
- schemathesis/stateful/events.py +0 -274
- schemathesis/stateful/runner.py +0 -309
- schemathesis/stateful/sink.py +0 -68
- schemathesis/stateful/state_machine.py +0 -328
- schemathesis/stateful/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -369
- schemathesis/transports/asgi.py +0 -7
- schemathesis/transports/auth.py +0 -38
- schemathesis/transports/headers.py +0 -36
- schemathesis/transports/responses.py +0 -57
- schemathesis/types.py +0 -44
- schemathesis/utils.py +0 -164
- schemathesis-3.39.15.dist-info/METADATA +0 -293
- schemathesis-3.39.15.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.15.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
schemathesis/_hypothesis.py
DELETED
@@ -1,712 +0,0 @@
|
|
1
|
-
"""High-level API for creating Hypothesis tests."""
|
2
|
-
|
3
|
-
from __future__ import annotations
|
4
|
-
|
5
|
-
import asyncio
|
6
|
-
from dataclasses import dataclass
|
7
|
-
import warnings
|
8
|
-
from functools import wraps
|
9
|
-
from itertools import combinations
|
10
|
-
from typing import TYPE_CHECKING, Any, Callable, Generator, Mapping
|
11
|
-
|
12
|
-
import hypothesis
|
13
|
-
from hypothesis import Phase
|
14
|
-
from hypothesis.errors import HypothesisWarning, Unsatisfiable
|
15
|
-
from hypothesis.internal.entropy import deterministic_PRNG
|
16
|
-
from jsonschema.exceptions import SchemaError
|
17
|
-
|
18
|
-
from schemathesis.serializers import get_first_matching_media_type
|
19
|
-
|
20
|
-
from . import _patches
|
21
|
-
from .auths import AuthStorage, get_auth_storage_from_test
|
22
|
-
from .constants import DEFAULT_DEADLINE, NOT_SET
|
23
|
-
from .exceptions import OperationSchemaError, SerializationNotPossible
|
24
|
-
from .experimental import COVERAGE_PHASE
|
25
|
-
from .generation import DataGenerationMethod, GenerationConfig, combine_strategies, coverage, get_single_example
|
26
|
-
from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher
|
27
|
-
from .models import APIOperation, Case, GenerationMetadata, TestPhase
|
28
|
-
from .parameters import ParameterSet
|
29
|
-
from .transports.content_types import parse_content_type
|
30
|
-
from .transports.headers import has_invalid_characters, is_latin_1_encodable
|
31
|
-
from .types import NotSet
|
32
|
-
from schemathesis import auths
|
33
|
-
|
34
|
-
if TYPE_CHECKING:
|
35
|
-
from .utils import GivenInput
|
36
|
-
|
37
|
-
# Forcefully initializes Hypothesis' global PRNG to avoid races that initialize it
|
38
|
-
# if e.g. Schemathesis CLI is used with multiple workers
|
39
|
-
with deterministic_PRNG():
|
40
|
-
pass
|
41
|
-
|
42
|
-
_patches.install()
|
43
|
-
|
44
|
-
|
45
|
-
def create_test(
|
46
|
-
*,
|
47
|
-
operation: APIOperation,
|
48
|
-
test: Callable,
|
49
|
-
settings: hypothesis.settings | None = None,
|
50
|
-
seed: int | None = None,
|
51
|
-
data_generation_methods: list[DataGenerationMethod],
|
52
|
-
generation_config: GenerationConfig | None = None,
|
53
|
-
as_strategy_kwargs: dict[str, Any] | None = None,
|
54
|
-
keep_async_fn: bool = False,
|
55
|
-
_given_args: tuple[GivenInput, ...] = (),
|
56
|
-
_given_kwargs: dict[str, GivenInput] | None = None,
|
57
|
-
) -> Callable:
|
58
|
-
"""Create a Hypothesis test."""
|
59
|
-
hook_dispatcher = getattr(test, "_schemathesis_hooks", None)
|
60
|
-
auth_storage = get_auth_storage_from_test(test)
|
61
|
-
strategies = []
|
62
|
-
skip_on_not_negated = len(data_generation_methods) == 1 and DataGenerationMethod.negative in data_generation_methods
|
63
|
-
as_strategy_kwargs = as_strategy_kwargs or {}
|
64
|
-
as_strategy_kwargs.update(
|
65
|
-
{
|
66
|
-
"hooks": hook_dispatcher,
|
67
|
-
"auth_storage": auth_storage,
|
68
|
-
"generation_config": generation_config,
|
69
|
-
"skip_on_not_negated": skip_on_not_negated,
|
70
|
-
}
|
71
|
-
)
|
72
|
-
for data_generation_method in data_generation_methods:
|
73
|
-
strategies.append(operation.as_strategy(data_generation_method=data_generation_method, **as_strategy_kwargs))
|
74
|
-
strategy = combine_strategies(strategies)
|
75
|
-
_given_kwargs = (_given_kwargs or {}).copy()
|
76
|
-
_given_kwargs.setdefault("case", strategy)
|
77
|
-
|
78
|
-
# Each generated test should be a unique function. It is especially important for the case when Schemathesis runs
|
79
|
-
# tests in multiple threads because Hypothesis stores some internal attributes on function objects and re-writing
|
80
|
-
# them from different threads may lead to unpredictable side-effects.
|
81
|
-
|
82
|
-
@wraps(test)
|
83
|
-
def test_function(*args: Any, **kwargs: Any) -> Any:
|
84
|
-
__tracebackhide__ = True
|
85
|
-
return test(*args, **kwargs)
|
86
|
-
|
87
|
-
wrapped_test = hypothesis.given(*_given_args, **_given_kwargs)(test_function)
|
88
|
-
if seed is not None:
|
89
|
-
wrapped_test = hypothesis.seed(seed)(wrapped_test)
|
90
|
-
if asyncio.iscoroutinefunction(test):
|
91
|
-
# `pytest-trio` expects a coroutine function
|
92
|
-
if keep_async_fn:
|
93
|
-
wrapped_test.hypothesis.inner_test = test # type: ignore
|
94
|
-
else:
|
95
|
-
wrapped_test.hypothesis.inner_test = make_async_test(test) # type: ignore
|
96
|
-
setup_default_deadline(wrapped_test)
|
97
|
-
if settings is not None:
|
98
|
-
existing_settings = _get_hypothesis_settings(wrapped_test)
|
99
|
-
if existing_settings is not None:
|
100
|
-
# Merge the user-provided settings with the current ones
|
101
|
-
default = hypothesis.settings.default
|
102
|
-
wrapped_test._hypothesis_internal_use_settings = hypothesis.settings(
|
103
|
-
wrapped_test._hypothesis_internal_use_settings,
|
104
|
-
**{item: value for item, value in settings.__dict__.items() if value != getattr(default, item)},
|
105
|
-
)
|
106
|
-
else:
|
107
|
-
wrapped_test = settings(wrapped_test)
|
108
|
-
existing_settings = _get_hypothesis_settings(wrapped_test)
|
109
|
-
if existing_settings is not None:
|
110
|
-
existing_settings = remove_explain_phase(existing_settings)
|
111
|
-
wrapped_test._hypothesis_internal_use_settings = existing_settings # type: ignore
|
112
|
-
if Phase.explicit in existing_settings.phases:
|
113
|
-
wrapped_test = add_examples(
|
114
|
-
wrapped_test, operation, hook_dispatcher=hook_dispatcher, as_strategy_kwargs=as_strategy_kwargs
|
115
|
-
)
|
116
|
-
if COVERAGE_PHASE.is_enabled:
|
117
|
-
unexpected_methods = generation_config.unexpected_methods if generation_config else None
|
118
|
-
wrapped_test = add_coverage(
|
119
|
-
wrapped_test,
|
120
|
-
operation,
|
121
|
-
data_generation_methods,
|
122
|
-
auth_storage,
|
123
|
-
as_strategy_kwargs,
|
124
|
-
unexpected_methods,
|
125
|
-
)
|
126
|
-
return wrapped_test
|
127
|
-
|
128
|
-
|
129
|
-
def setup_default_deadline(wrapped_test: Callable) -> None:
|
130
|
-
# Quite hacky, but it is the simplest way to set up the default deadline value without affecting non-Schemathesis
|
131
|
-
# tests globally
|
132
|
-
existing_settings = _get_hypothesis_settings(wrapped_test)
|
133
|
-
if existing_settings is not None and existing_settings.deadline == hypothesis.settings.default.deadline:
|
134
|
-
with warnings.catch_warnings():
|
135
|
-
warnings.simplefilter("ignore", HypothesisWarning)
|
136
|
-
new_settings = hypothesis.settings(existing_settings, deadline=DEFAULT_DEADLINE)
|
137
|
-
wrapped_test._hypothesis_internal_use_settings = new_settings # type: ignore
|
138
|
-
|
139
|
-
|
140
|
-
def remove_explain_phase(settings: hypothesis.settings) -> hypothesis.settings:
|
141
|
-
# The "explain" phase is not supported
|
142
|
-
if Phase.explain in settings.phases:
|
143
|
-
phases = tuple(phase for phase in settings.phases if phase != Phase.explain)
|
144
|
-
return hypothesis.settings(settings, phases=phases)
|
145
|
-
return settings
|
146
|
-
|
147
|
-
|
148
|
-
def _get_hypothesis_settings(test: Callable) -> hypothesis.settings | None:
|
149
|
-
return getattr(test, "_hypothesis_internal_use_settings", None)
|
150
|
-
|
151
|
-
|
152
|
-
def make_async_test(test: Callable) -> Callable:
|
153
|
-
def async_run(*args: Any, **kwargs: Any) -> None:
|
154
|
-
try:
|
155
|
-
loop = asyncio.get_event_loop()
|
156
|
-
except RuntimeError:
|
157
|
-
loop = asyncio.new_event_loop()
|
158
|
-
coro = test(*args, **kwargs)
|
159
|
-
future = asyncio.ensure_future(coro, loop=loop)
|
160
|
-
loop.run_until_complete(future)
|
161
|
-
|
162
|
-
return async_run
|
163
|
-
|
164
|
-
|
165
|
-
def add_examples(
|
166
|
-
test: Callable,
|
167
|
-
operation: APIOperation,
|
168
|
-
hook_dispatcher: HookDispatcher | None = None,
|
169
|
-
as_strategy_kwargs: dict[str, Any] | None = None,
|
170
|
-
) -> Callable:
|
171
|
-
"""Add examples to the Hypothesis test, if they are specified in the schema."""
|
172
|
-
from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
|
173
|
-
|
174
|
-
try:
|
175
|
-
examples: list[Case] = [
|
176
|
-
get_single_example(strategy)
|
177
|
-
for strategy in operation.get_strategies_from_examples(as_strategy_kwargs=as_strategy_kwargs)
|
178
|
-
]
|
179
|
-
except (
|
180
|
-
OperationSchemaError,
|
181
|
-
HypothesisRefResolutionError,
|
182
|
-
Unsatisfiable,
|
183
|
-
SerializationNotPossible,
|
184
|
-
SchemaError,
|
185
|
-
) as exc:
|
186
|
-
# Invalid schema:
|
187
|
-
# In this case, the user didn't pass `--validate-schema=false` and see an error in the output anyway,
|
188
|
-
# and no tests will be executed. For this reason, examples can be skipped
|
189
|
-
# Recursive references: This test will be skipped anyway
|
190
|
-
# Unsatisfiable:
|
191
|
-
# The underlying schema is not satisfiable and test will raise an error for the same reason.
|
192
|
-
# Skipping this exception here allows us to continue the testing process for other operations.
|
193
|
-
# Still, we allow running user-defined hooks
|
194
|
-
examples = []
|
195
|
-
if isinstance(exc, Unsatisfiable):
|
196
|
-
add_unsatisfied_example_mark(test, exc)
|
197
|
-
if isinstance(exc, SerializationNotPossible):
|
198
|
-
add_non_serializable_mark(test, exc)
|
199
|
-
if isinstance(exc, SchemaError):
|
200
|
-
add_invalid_regex_mark(test, exc)
|
201
|
-
context = HookContext(operation) # context should be passed here instead
|
202
|
-
GLOBAL_HOOK_DISPATCHER.dispatch("before_add_examples", context, examples)
|
203
|
-
operation.schema.hooks.dispatch("before_add_examples", context, examples)
|
204
|
-
if hook_dispatcher:
|
205
|
-
hook_dispatcher.dispatch("before_add_examples", context, examples)
|
206
|
-
original_test = test
|
207
|
-
for example in examples:
|
208
|
-
if example.headers is not None:
|
209
|
-
invalid_headers = dict(find_invalid_headers(example.headers))
|
210
|
-
if invalid_headers:
|
211
|
-
add_invalid_example_header_mark(original_test, invalid_headers)
|
212
|
-
continue
|
213
|
-
adjust_urlencoded_payload(example)
|
214
|
-
test = hypothesis.example(case=example)(test)
|
215
|
-
return test
|
216
|
-
|
217
|
-
|
218
|
-
def adjust_urlencoded_payload(case: Case) -> None:
|
219
|
-
if case.media_type is not None:
|
220
|
-
try:
|
221
|
-
media_type = parse_content_type(case.media_type)
|
222
|
-
if media_type == ("application", "x-www-form-urlencoded"):
|
223
|
-
case.body = prepare_urlencoded(case.body)
|
224
|
-
except ValueError:
|
225
|
-
pass
|
226
|
-
|
227
|
-
|
228
|
-
def add_coverage(
|
229
|
-
test: Callable,
|
230
|
-
operation: APIOperation,
|
231
|
-
data_generation_methods: list[DataGenerationMethod],
|
232
|
-
auth_storage: AuthStorage | None,
|
233
|
-
as_strategy_kwargs: dict[str, Any],
|
234
|
-
unexpected_methods: set[str] | None = None,
|
235
|
-
) -> Callable:
|
236
|
-
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
237
|
-
|
238
|
-
auth_context = auths.AuthContext(
|
239
|
-
operation=operation,
|
240
|
-
app=operation.app,
|
241
|
-
)
|
242
|
-
overrides = {
|
243
|
-
container: as_strategy_kwargs[container]
|
244
|
-
for container in LOCATION_TO_CONTAINER.values()
|
245
|
-
if container in as_strategy_kwargs
|
246
|
-
}
|
247
|
-
for case in _iter_coverage_cases(operation, data_generation_methods, unexpected_methods):
|
248
|
-
if case.media_type and get_first_matching_media_type(case.media_type) is None:
|
249
|
-
continue
|
250
|
-
adjust_urlencoded_payload(case)
|
251
|
-
auths.set_on_case(case, auth_context, auth_storage)
|
252
|
-
for container_name, value in overrides.items():
|
253
|
-
container = getattr(case, container_name)
|
254
|
-
if container is None:
|
255
|
-
setattr(case, container_name, value)
|
256
|
-
else:
|
257
|
-
container.update(value)
|
258
|
-
test = hypothesis.example(case=case)(test)
|
259
|
-
return test
|
260
|
-
|
261
|
-
|
262
|
-
class Template:
|
263
|
-
__slots__ = ("_components", "_template", "_serializers")
|
264
|
-
|
265
|
-
def __init__(self, serializers: dict[str, Callable]) -> None:
|
266
|
-
self._components: dict[str, DataGenerationMethod] = {}
|
267
|
-
self._template: dict[str, Any] = {}
|
268
|
-
self._serializers = serializers
|
269
|
-
|
270
|
-
def __contains__(self, key: str) -> bool:
|
271
|
-
return key in self._template
|
272
|
-
|
273
|
-
def __getitem__(self, key: str) -> dict:
|
274
|
-
return self._template[key]
|
275
|
-
|
276
|
-
def get(self, key: str, default: Any = None) -> dict:
|
277
|
-
return self._template.get(key, default)
|
278
|
-
|
279
|
-
def add_parameter(self, location: str, name: str, value: coverage.GeneratedValue) -> None:
|
280
|
-
from .specs.openapi.constants import LOCATION_TO_CONTAINER
|
281
|
-
|
282
|
-
component_name = LOCATION_TO_CONTAINER[location]
|
283
|
-
method = self._components.get(component_name)
|
284
|
-
if method is None:
|
285
|
-
self._components[component_name] = value.data_generation_method
|
286
|
-
elif value.data_generation_method == DataGenerationMethod.negative:
|
287
|
-
self._components[component_name] = DataGenerationMethod.negative
|
288
|
-
|
289
|
-
container = self._template.setdefault(component_name, {})
|
290
|
-
container[name] = value.value
|
291
|
-
|
292
|
-
def set_body(self, body: coverage.GeneratedValue, media_type: str) -> None:
|
293
|
-
self._template["body"] = body.value
|
294
|
-
self._template["media_type"] = media_type
|
295
|
-
self._components["body"] = body.data_generation_method
|
296
|
-
|
297
|
-
def _serialize(self, kwargs: dict[str, Any]) -> dict[str, Any]:
|
298
|
-
from schemathesis.specs.openapi._hypothesis import quote_all
|
299
|
-
|
300
|
-
output = {}
|
301
|
-
for container_name, value in kwargs.items():
|
302
|
-
serializer = self._serializers.get(container_name)
|
303
|
-
if container_name in ("headers", "cookies") and isinstance(value, dict):
|
304
|
-
value = _stringify_value(value, container_name)
|
305
|
-
if serializer is not None:
|
306
|
-
value = serializer(value)
|
307
|
-
if container_name == "query" and isinstance(value, dict):
|
308
|
-
value = _stringify_value(value, container_name)
|
309
|
-
if container_name == "path_parameters" and isinstance(value, dict):
|
310
|
-
value = _stringify_value(quote_all(value), container_name)
|
311
|
-
output[container_name] = value
|
312
|
-
return output
|
313
|
-
|
314
|
-
def unmodified(self) -> TemplateValue:
|
315
|
-
kwargs = self._template.copy()
|
316
|
-
kwargs = self._serialize(kwargs)
|
317
|
-
return TemplateValue(kwargs=kwargs, components=self._components.copy())
|
318
|
-
|
319
|
-
def with_body(self, *, media_type: str, value: coverage.GeneratedValue) -> TemplateValue:
|
320
|
-
kwargs = {**self._template, "media_type": media_type, "body": value.value}
|
321
|
-
kwargs = self._serialize(kwargs)
|
322
|
-
components = {**self._components, "body": value.data_generation_method}
|
323
|
-
return TemplateValue(kwargs=kwargs, components=components)
|
324
|
-
|
325
|
-
def with_parameter(self, *, location: str, name: str, value: coverage.GeneratedValue) -> TemplateValue:
|
326
|
-
from .specs.openapi.constants import LOCATION_TO_CONTAINER
|
327
|
-
|
328
|
-
container_name = LOCATION_TO_CONTAINER[location]
|
329
|
-
container = self._template[container_name]
|
330
|
-
return self.with_container(
|
331
|
-
container_name=container_name,
|
332
|
-
value={**container, name: value.value},
|
333
|
-
data_generation_method=value.data_generation_method,
|
334
|
-
)
|
335
|
-
|
336
|
-
def with_container(
|
337
|
-
self, *, container_name: str, value: Any, data_generation_method: DataGenerationMethod
|
338
|
-
) -> TemplateValue:
|
339
|
-
kwargs = {**self._template, container_name: value}
|
340
|
-
kwargs = self._serialize(kwargs)
|
341
|
-
components = {**self._components, container_name: data_generation_method}
|
342
|
-
return TemplateValue(kwargs=kwargs, components=components)
|
343
|
-
|
344
|
-
|
345
|
-
@dataclass
|
346
|
-
class TemplateValue:
|
347
|
-
kwargs: dict[str, Any]
|
348
|
-
components: dict[str, DataGenerationMethod]
|
349
|
-
__slots__ = ("kwargs", "components")
|
350
|
-
|
351
|
-
|
352
|
-
def _stringify_value(val: Any, container_name: str) -> Any:
|
353
|
-
if val is None:
|
354
|
-
return "null"
|
355
|
-
if val is True:
|
356
|
-
return "true"
|
357
|
-
if val is False:
|
358
|
-
return "false"
|
359
|
-
if isinstance(val, (int, float)):
|
360
|
-
return str(val)
|
361
|
-
if isinstance(val, list):
|
362
|
-
if container_name == "query":
|
363
|
-
# Having a list here ensures there will be multiple query parameters wit the same name
|
364
|
-
return [_stringify_value(item, container_name) for item in val]
|
365
|
-
# use comma-separated values style for arrays
|
366
|
-
return ",".join(_stringify_value(sub, container_name) for sub in val)
|
367
|
-
if isinstance(val, dict):
|
368
|
-
return {key: _stringify_value(sub, container_name) for key, sub in val.items()}
|
369
|
-
return val
|
370
|
-
|
371
|
-
|
372
|
-
def _iter_coverage_cases(
|
373
|
-
operation: APIOperation,
|
374
|
-
data_generation_methods: list[DataGenerationMethod],
|
375
|
-
unexpected_methods: set[str] | None = None,
|
376
|
-
) -> Generator[Case, None, None]:
|
377
|
-
from .specs.openapi.constants import LOCATION_TO_CONTAINER
|
378
|
-
from .specs.openapi.examples import find_in_responses, find_matching_in_responses
|
379
|
-
from schemathesis.specs.openapi.serialization import get_serializers_for_operation
|
380
|
-
|
381
|
-
generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
|
382
|
-
serializers = get_serializers_for_operation(operation)
|
383
|
-
template = Template(serializers)
|
384
|
-
responses = find_in_responses(operation)
|
385
|
-
# NOTE: The HEAD method is excluded
|
386
|
-
unexpected_methods = unexpected_methods or {"get", "put", "post", "delete", "options", "patch", "trace"}
|
387
|
-
for parameter in operation.iter_parameters():
|
388
|
-
location = parameter.location
|
389
|
-
name = parameter.name
|
390
|
-
schema = parameter.as_json_schema(operation, update_quantifiers=False)
|
391
|
-
for value in find_matching_in_responses(responses, parameter.name):
|
392
|
-
schema.setdefault("examples", []).append(value)
|
393
|
-
gen = coverage.cover_schema_iter(
|
394
|
-
coverage.CoverageContext(location=location, data_generation_methods=data_generation_methods), schema
|
395
|
-
)
|
396
|
-
value = next(gen, NOT_SET)
|
397
|
-
if isinstance(value, NotSet):
|
398
|
-
continue
|
399
|
-
template.add_parameter(location, name, value)
|
400
|
-
generators[(location, name)] = gen
|
401
|
-
if operation.body:
|
402
|
-
for body in operation.body:
|
403
|
-
schema = body.as_json_schema(operation, update_quantifiers=False)
|
404
|
-
# Definition could be a list for Open API 2.0
|
405
|
-
definition = body.definition if isinstance(body.definition, dict) else {}
|
406
|
-
examples = [example["value"] for example in definition.get("examples", {}).values() if "value" in example]
|
407
|
-
if examples:
|
408
|
-
schema.setdefault("examples", []).extend(examples)
|
409
|
-
gen = coverage.cover_schema_iter(
|
410
|
-
coverage.CoverageContext(location="body", data_generation_methods=data_generation_methods), schema
|
411
|
-
)
|
412
|
-
value = next(gen, NOT_SET)
|
413
|
-
if isinstance(value, NotSet):
|
414
|
-
continue
|
415
|
-
if "body" not in template:
|
416
|
-
template.set_body(value, body.media_type)
|
417
|
-
data = template.with_body(value=value, media_type=body.media_type)
|
418
|
-
case = operation.make_case(**data.kwargs)
|
419
|
-
case.data_generation_method = value.data_generation_method
|
420
|
-
case.meta = _make_meta(
|
421
|
-
description=value.description,
|
422
|
-
location=value.location,
|
423
|
-
parameter=body.media_type,
|
424
|
-
parameter_location="body",
|
425
|
-
**data.components,
|
426
|
-
)
|
427
|
-
yield case
|
428
|
-
for next_value in gen:
|
429
|
-
data = template.with_body(value=next_value, media_type=body.media_type)
|
430
|
-
case = operation.make_case(**data.kwargs)
|
431
|
-
case.data_generation_method = next_value.data_generation_method
|
432
|
-
case.meta = _make_meta(
|
433
|
-
description=next_value.description,
|
434
|
-
location=next_value.location,
|
435
|
-
parameter=body.media_type,
|
436
|
-
parameter_location="body",
|
437
|
-
**data.components,
|
438
|
-
)
|
439
|
-
yield case
|
440
|
-
elif DataGenerationMethod.positive in data_generation_methods:
|
441
|
-
data = template.unmodified()
|
442
|
-
case = operation.make_case(**data.kwargs)
|
443
|
-
case.data_generation_method = DataGenerationMethod.positive
|
444
|
-
case.meta = _make_meta(description="Default positive test case", **data.components)
|
445
|
-
yield case
|
446
|
-
|
447
|
-
for (location, name), gen in generators.items():
|
448
|
-
for value in gen:
|
449
|
-
data = template.with_parameter(location=location, name=name, value=value)
|
450
|
-
case = operation.make_case(**data.kwargs)
|
451
|
-
case.data_generation_method = value.data_generation_method
|
452
|
-
case.meta = _make_meta(
|
453
|
-
description=value.description,
|
454
|
-
location=value.location,
|
455
|
-
parameter=name,
|
456
|
-
parameter_location=location,
|
457
|
-
**data.components,
|
458
|
-
)
|
459
|
-
yield case
|
460
|
-
if DataGenerationMethod.negative in data_generation_methods:
|
461
|
-
# Generate HTTP methods that are not specified in the spec
|
462
|
-
methods = unexpected_methods - set(operation.schema[operation.path])
|
463
|
-
for method in sorted(methods):
|
464
|
-
data = template.unmodified()
|
465
|
-
case = operation.make_case(**data.kwargs)
|
466
|
-
case._explicit_method = method
|
467
|
-
case.data_generation_method = DataGenerationMethod.negative
|
468
|
-
case.meta = _make_meta(description=f"Unspecified HTTP method: {method.upper()}", **data.components)
|
469
|
-
yield case
|
470
|
-
# Generate duplicate query parameters
|
471
|
-
if operation.query:
|
472
|
-
container = template["query"]
|
473
|
-
for parameter in operation.query:
|
474
|
-
# Could be absent if value schema can't be negated
|
475
|
-
# I.e. contains just `default` value without any other keywords
|
476
|
-
value = container.get(parameter.name, NOT_SET)
|
477
|
-
if value is not NOT_SET:
|
478
|
-
data = template.with_container(
|
479
|
-
container_name="query",
|
480
|
-
value={**container, parameter.name: [value, value]},
|
481
|
-
data_generation_method=DataGenerationMethod.negative,
|
482
|
-
)
|
483
|
-
case = operation.make_case(**data.kwargs)
|
484
|
-
case.data_generation_method = DataGenerationMethod.negative
|
485
|
-
case.meta = _make_meta(
|
486
|
-
description=f"Duplicate `{parameter.name}` query parameter",
|
487
|
-
location=None,
|
488
|
-
parameter=parameter.name,
|
489
|
-
parameter_location="query",
|
490
|
-
**data.components,
|
491
|
-
)
|
492
|
-
yield case
|
493
|
-
# Generate missing required parameters
|
494
|
-
for parameter in operation.iter_parameters():
|
495
|
-
if parameter.is_required and parameter.location != "path":
|
496
|
-
name = parameter.name
|
497
|
-
location = parameter.location
|
498
|
-
container_name = LOCATION_TO_CONTAINER[location]
|
499
|
-
container = template[container_name]
|
500
|
-
data = template.with_container(
|
501
|
-
container_name=container_name,
|
502
|
-
value={k: v for k, v in container.items() if k != name},
|
503
|
-
data_generation_method=DataGenerationMethod.negative,
|
504
|
-
)
|
505
|
-
case = operation.make_case(**data.kwargs)
|
506
|
-
case.data_generation_method = DataGenerationMethod.negative
|
507
|
-
case.meta = _make_meta(
|
508
|
-
description=f"Missing `{name}` at {location}",
|
509
|
-
location=None,
|
510
|
-
parameter=name,
|
511
|
-
parameter_location=location,
|
512
|
-
**data.components,
|
513
|
-
)
|
514
|
-
yield case
|
515
|
-
# Generate combinations for each location
|
516
|
-
for location, parameter_set in [
|
517
|
-
("query", operation.query),
|
518
|
-
("header", operation.headers),
|
519
|
-
("cookie", operation.cookies),
|
520
|
-
]:
|
521
|
-
if not parameter_set:
|
522
|
-
continue
|
523
|
-
|
524
|
-
container_name = LOCATION_TO_CONTAINER[location]
|
525
|
-
base_container = template.get(container_name, {})
|
526
|
-
|
527
|
-
# Get required and optional parameters
|
528
|
-
required = {p.name for p in parameter_set if p.is_required}
|
529
|
-
all_params = {p.name for p in parameter_set}
|
530
|
-
optional = sorted(all_params - required)
|
531
|
-
|
532
|
-
# Helper function to create and yield a case
|
533
|
-
def make_case(
|
534
|
-
container_values: dict,
|
535
|
-
description: str,
|
536
|
-
_location: str,
|
537
|
-
_container_name: str,
|
538
|
-
_parameter: str | None,
|
539
|
-
_data_generation_method: DataGenerationMethod,
|
540
|
-
) -> Case:
|
541
|
-
data = template.with_container(
|
542
|
-
container_name=_container_name, value=container_values, data_generation_method=_data_generation_method
|
543
|
-
)
|
544
|
-
case = operation.make_case(**data.kwargs)
|
545
|
-
case.data_generation_method = _data_generation_method
|
546
|
-
case.meta = _make_meta(
|
547
|
-
description=description,
|
548
|
-
location=None,
|
549
|
-
parameter=_parameter,
|
550
|
-
parameter_location=_location,
|
551
|
-
**data.components,
|
552
|
-
)
|
553
|
-
return case
|
554
|
-
|
555
|
-
def _combination_schema(
|
556
|
-
combination: dict[str, Any], _required: set[str], _parameter_set: ParameterSet
|
557
|
-
) -> dict[str, Any]:
|
558
|
-
return {
|
559
|
-
"properties": {
|
560
|
-
parameter.name: parameter.as_json_schema(operation)
|
561
|
-
for parameter in _parameter_set
|
562
|
-
if parameter.name in combination
|
563
|
-
},
|
564
|
-
"required": list(_required),
|
565
|
-
"additionalProperties": False,
|
566
|
-
}
|
567
|
-
|
568
|
-
def _yield_negative(
|
569
|
-
subschema: dict[str, Any], _location: str, _container_name: str
|
570
|
-
) -> Generator[Case, None, None]:
|
571
|
-
for more in coverage.cover_schema_iter(
|
572
|
-
coverage.CoverageContext(location=_location, data_generation_methods=[DataGenerationMethod.negative]),
|
573
|
-
subschema,
|
574
|
-
):
|
575
|
-
yield make_case(
|
576
|
-
more.value,
|
577
|
-
more.description,
|
578
|
-
_location,
|
579
|
-
_container_name,
|
580
|
-
more.parameter,
|
581
|
-
DataGenerationMethod.negative,
|
582
|
-
)
|
583
|
-
|
584
|
-
# 1. Generate only required properties
|
585
|
-
if required and all_params != required:
|
586
|
-
only_required = {k: v for k, v in base_container.items() if k in required}
|
587
|
-
if DataGenerationMethod.positive in data_generation_methods:
|
588
|
-
yield make_case(
|
589
|
-
only_required,
|
590
|
-
"Only required properties",
|
591
|
-
location,
|
592
|
-
container_name,
|
593
|
-
None,
|
594
|
-
DataGenerationMethod.positive,
|
595
|
-
)
|
596
|
-
if DataGenerationMethod.negative in data_generation_methods:
|
597
|
-
subschema = _combination_schema(only_required, required, parameter_set)
|
598
|
-
for case in _yield_negative(subschema, location, container_name):
|
599
|
-
# Already generated in one of the blocks above
|
600
|
-
if location != "path" and not case.meta.description.startswith("Missing required property"):
|
601
|
-
yield case
|
602
|
-
|
603
|
-
# 2. Generate combinations with required properties and one optional property
|
604
|
-
for opt_param in optional:
|
605
|
-
combo = {k: v for k, v in base_container.items() if k in required or k == opt_param}
|
606
|
-
if combo != base_container and DataGenerationMethod.positive in data_generation_methods:
|
607
|
-
yield make_case(
|
608
|
-
combo,
|
609
|
-
f"All required properties and optional '{opt_param}'",
|
610
|
-
location,
|
611
|
-
container_name,
|
612
|
-
None,
|
613
|
-
DataGenerationMethod.positive,
|
614
|
-
)
|
615
|
-
if DataGenerationMethod.negative in data_generation_methods:
|
616
|
-
subschema = _combination_schema(combo, required, parameter_set)
|
617
|
-
for case in _yield_negative(subschema, location, container_name):
|
618
|
-
# Already generated in one of the blocks above
|
619
|
-
if location != "path" and not case.meta.description.startswith("Missing required property"):
|
620
|
-
yield case
|
621
|
-
|
622
|
-
# 3. Generate one combination for each size from 2 to N-1 of optional parameters
|
623
|
-
if len(optional) > 1 and DataGenerationMethod.positive in data_generation_methods:
|
624
|
-
for size in range(2, len(optional)):
|
625
|
-
for combination in combinations(optional, size):
|
626
|
-
combo = {k: v for k, v in base_container.items() if k in required or k in combination}
|
627
|
-
if combo != base_container:
|
628
|
-
yield make_case(
|
629
|
-
combo,
|
630
|
-
f"All required and {size} optional properties",
|
631
|
-
location,
|
632
|
-
container_name,
|
633
|
-
None,
|
634
|
-
DataGenerationMethod.positive,
|
635
|
-
)
|
636
|
-
|
637
|
-
|
638
|
-
def _make_meta(
|
639
|
-
*,
|
640
|
-
description: str,
|
641
|
-
location: str | None = None,
|
642
|
-
parameter: str | None = None,
|
643
|
-
parameter_location: str | None = None,
|
644
|
-
query: DataGenerationMethod | None = None,
|
645
|
-
path_parameters: DataGenerationMethod | None = None,
|
646
|
-
headers: DataGenerationMethod | None = None,
|
647
|
-
cookies: DataGenerationMethod | None = None,
|
648
|
-
body: DataGenerationMethod | None = None,
|
649
|
-
) -> GenerationMetadata:
|
650
|
-
return GenerationMetadata(
|
651
|
-
query=query,
|
652
|
-
path_parameters=path_parameters,
|
653
|
-
headers=headers,
|
654
|
-
cookies=cookies,
|
655
|
-
body=body,
|
656
|
-
phase=TestPhase.COVERAGE,
|
657
|
-
description=description,
|
658
|
-
location=location,
|
659
|
-
parameter=parameter,
|
660
|
-
parameter_location=parameter_location,
|
661
|
-
)
|
662
|
-
|
663
|
-
|
664
|
-
def find_invalid_headers(headers: Mapping) -> Generator[tuple[str, str], None, None]:
|
665
|
-
for name, value in headers.items():
|
666
|
-
if not is_latin_1_encodable(value) or has_invalid_characters(name, value):
|
667
|
-
yield name, value
|
668
|
-
|
669
|
-
|
670
|
-
def prepare_urlencoded(data: Any) -> Any:
|
671
|
-
if isinstance(data, list):
|
672
|
-
output = []
|
673
|
-
for item in data:
|
674
|
-
if isinstance(item, dict):
|
675
|
-
for key, value in item.items():
|
676
|
-
output.append((key, value))
|
677
|
-
else:
|
678
|
-
output.append((item, "arbitrary-value"))
|
679
|
-
return output
|
680
|
-
return data
|
681
|
-
|
682
|
-
|
683
|
-
def add_unsatisfied_example_mark(test: Callable, exc: Unsatisfiable) -> None:
|
684
|
-
test._schemathesis_unsatisfied_example = exc # type: ignore
|
685
|
-
|
686
|
-
|
687
|
-
def has_unsatisfied_example_mark(test: Callable) -> bool:
|
688
|
-
return hasattr(test, "_schemathesis_unsatisfied_example")
|
689
|
-
|
690
|
-
|
691
|
-
def add_non_serializable_mark(test: Callable, exc: SerializationNotPossible) -> None:
|
692
|
-
test._schemathesis_non_serializable = exc # type: ignore
|
693
|
-
|
694
|
-
|
695
|
-
def get_non_serializable_mark(test: Callable) -> SerializationNotPossible | None:
|
696
|
-
return getattr(test, "_schemathesis_non_serializable", None)
|
697
|
-
|
698
|
-
|
699
|
-
def get_invalid_regex_mark(test: Callable) -> SchemaError | None:
|
700
|
-
return getattr(test, "_schemathesis_invalid_regex", None)
|
701
|
-
|
702
|
-
|
703
|
-
def add_invalid_regex_mark(test: Callable, exc: SchemaError) -> None:
|
704
|
-
test._schemathesis_invalid_regex = exc # type: ignore
|
705
|
-
|
706
|
-
|
707
|
-
def get_invalid_example_headers_mark(test: Callable) -> dict[str, str] | None:
|
708
|
-
return getattr(test, "_schemathesis_invalid_example_headers", None)
|
709
|
-
|
710
|
-
|
711
|
-
def add_invalid_example_header_mark(test: Callable, headers: dict[str, str]) -> None:
|
712
|
-
test._schemathesis_invalid_example_headers = headers # type: ignore
|