schemathesis 3.39.7__py3-none-any.whl → 4.0.0a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +27 -65
- schemathesis/auths.py +26 -68
- schemathesis/checks.py +130 -60
- schemathesis/cli/__init__.py +5 -2105
- schemathesis/cli/commands/__init__.py +37 -0
- schemathesis/cli/commands/run/__init__.py +662 -0
- schemathesis/cli/commands/run/checks.py +80 -0
- schemathesis/cli/commands/run/context.py +117 -0
- schemathesis/cli/commands/run/events.py +30 -0
- schemathesis/cli/commands/run/executor.py +141 -0
- schemathesis/cli/commands/run/filters.py +202 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/{cassettes.py → commands/run/handlers/cassettes.py} +178 -247
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +1368 -0
- schemathesis/cli/commands/run/hypothesis.py +105 -0
- schemathesis/cli/commands/run/loaders.py +129 -0
- schemathesis/cli/{callbacks.py → commands/run/validation.py} +59 -175
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +17 -0
- schemathesis/cli/ext/fs.py +14 -0
- schemathesis/cli/ext/groups.py +55 -0
- schemathesis/cli/{options.py → ext/options.py} +37 -16
- schemathesis/cli/hooks.py +36 -0
- schemathesis/contrib/__init__.py +1 -3
- schemathesis/contrib/openapi/__init__.py +1 -3
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -7
- schemathesis/core/__init__.py +58 -0
- schemathesis/core/compat.py +25 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +370 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
- schemathesis/{internal/output.py → core/output/__init__.py} +1 -0
- schemathesis/core/output/sanitization.py +197 -0
- schemathesis/{throttling.py → core/rate_limit.py} +16 -17
- schemathesis/core/registries.py +31 -0
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +108 -0
- schemathesis/core/validation.py +38 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +30 -0
- schemathesis/engine/config.py +59 -0
- schemathesis/engine/context.py +119 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +157 -0
- schemathesis/engine/errors.py +394 -0
- schemathesis/engine/events.py +243 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{runner → engine/phases}/probes.py +49 -68
- schemathesis/engine/phases/stateful/__init__.py +66 -0
- schemathesis/engine/phases/stateful/_executor.py +301 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +175 -0
- schemathesis/engine/phases/unit/_executor.py +322 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +246 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +9 -40
- schemathesis/filters.py +7 -95
- schemathesis/generation/__init__.py +3 -3
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +22 -22
- schemathesis/{_patches.py → generation/hypothesis/__init__.py} +15 -6
- schemathesis/generation/hypothesis/builder.py +585 -0
- schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/modes.py +28 -0
- schemathesis/generation/overrides.py +96 -0
- schemathesis/generation/stateful/__init__.py +20 -0
- schemathesis/{stateful → generation/stateful}/state_machine.py +84 -109
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +17 -62
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +387 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +63 -0
- schemathesis/openapi/loaders.py +178 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +273 -0
- schemathesis/pytest/loaders.py +12 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +94 -107
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +456 -228
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +5 -3
- schemathesis/specs/graphql/schemas.py +122 -123
- schemathesis/specs/graphql/validation.py +11 -17
- schemathesis/specs/openapi/__init__.py +6 -1
- schemathesis/specs/openapi/_cache.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +97 -134
- schemathesis/specs/openapi/checks.py +238 -219
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +22 -20
- schemathesis/specs/openapi/expressions/__init__.py +11 -15
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/nodes.py +33 -32
- schemathesis/specs/openapi/formats.py +3 -2
- schemathesis/specs/openapi/links.py +123 -299
- schemathesis/specs/openapi/media_types.py +10 -12
- schemathesis/specs/openapi/negative/__init__.py +2 -1
- schemathesis/specs/openapi/negative/mutations.py +3 -2
- schemathesis/specs/openapi/parameters.py +8 -6
- schemathesis/specs/openapi/patterns.py +1 -1
- schemathesis/specs/openapi/references.py +11 -51
- schemathesis/specs/openapi/schemas.py +177 -191
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +10 -6
- schemathesis/specs/openapi/stateful/__init__.py +97 -91
- schemathesis/transport/__init__.py +104 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +99 -0
- schemathesis/transport/requests.py +221 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -7
- schemathesis/transport/wsgi.py +165 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +18 -14
- schemathesis-4.0.0a2.dist-info/RECORD +151 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -559
- schemathesis/_override.py +0 -50
- schemathesis/_rate_limiter.py +0 -7
- schemathesis/cli/context.py +0 -75
- schemathesis/cli/debug.py +0 -27
- schemathesis/cli/handlers.py +0 -19
- schemathesis/cli/junitxml.py +0 -124
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -936
- schemathesis/cli/output/short.py +0 -59
- schemathesis/cli/reporting.py +0 -79
- schemathesis/cli/sanitization.py +0 -26
- schemathesis/code_samples.py +0 -151
- schemathesis/constants.py +0 -56
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -16
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -571
- schemathesis/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -277
- schemathesis/fixups/__init__.py +0 -37
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -28
- schemathesis/generation/_methods.py +0 -44
- schemathesis/graphql.py +0 -3
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/checks.py +0 -84
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -38
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- schemathesis/internal/transformation.py +0 -26
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -474
- schemathesis/loaders.py +0 -122
- schemathesis/models.py +0 -1341
- schemathesis/parameters.py +0 -90
- schemathesis/runner/__init__.py +0 -605
- schemathesis/runner/events.py +0 -389
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/context.py +0 -104
- schemathesis/runner/impl/core.py +0 -1246
- schemathesis/runner/impl/solo.py +0 -80
- schemathesis/runner/impl/threadpool.py +0 -391
- schemathesis/runner/serialization.py +0 -544
- schemathesis/sanitization.py +0 -252
- schemathesis/serializers.py +0 -328
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -202
- schemathesis/service/client.py +0 -133
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -61
- schemathesis/service/extensions.py +0 -224
- schemathesis/service/hosts.py +0 -111
- schemathesis/service/metadata.py +0 -71
- schemathesis/service/models.py +0 -258
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -173
- schemathesis/service/usage.py +0 -66
- schemathesis/specs/graphql/loaders.py +0 -364
- schemathesis/specs/openapi/expressions/context.py +0 -16
- schemathesis/specs/openapi/loaders.py +0 -708
- schemathesis/specs/openapi/stateful/statistic.py +0 -198
- schemathesis/specs/openapi/stateful/types.py +0 -14
- schemathesis/specs/openapi/validation.py +0 -26
- schemathesis/stateful/__init__.py +0 -147
- schemathesis/stateful/config.py +0 -97
- schemathesis/stateful/context.py +0 -135
- schemathesis/stateful/events.py +0 -274
- schemathesis/stateful/runner.py +0 -309
- schemathesis/stateful/sink.py +0 -68
- schemathesis/stateful/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -359
- schemathesis/transports/asgi.py +0 -7
- schemathesis/transports/auth.py +0 -38
- schemathesis/transports/headers.py +0 -36
- schemathesis/transports/responses.py +0 -57
- schemathesis/types.py +0 -44
- schemathesis/utils.py +0 -164
- schemathesis-3.39.7.dist-info/RECORD +0 -160
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
- /schemathesis/{internal → core}/result.py +0 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
schemathesis/_hypothesis.py
DELETED
@@ -1,559 +0,0 @@
|
|
1
|
-
"""High-level API for creating Hypothesis tests."""
|
2
|
-
|
3
|
-
from __future__ import annotations
|
4
|
-
|
5
|
-
import asyncio
|
6
|
-
import json
|
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 . import _patches
|
19
|
-
from .auths import get_auth_storage_from_test
|
20
|
-
from .constants import DEFAULT_DEADLINE, NOT_SET
|
21
|
-
from .exceptions import OperationSchemaError, SerializationNotPossible
|
22
|
-
from .experimental import COVERAGE_PHASE
|
23
|
-
from .generation import DataGenerationMethod, GenerationConfig, combine_strategies, coverage, get_single_example
|
24
|
-
from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher
|
25
|
-
from .models import APIOperation, Case, GenerationMetadata, TestPhase
|
26
|
-
from .parameters import ParameterSet
|
27
|
-
from .transports.content_types import parse_content_type
|
28
|
-
from .transports.headers import has_invalid_characters, is_latin_1_encodable
|
29
|
-
from .types import NotSet
|
30
|
-
|
31
|
-
if TYPE_CHECKING:
|
32
|
-
from .utils import GivenInput
|
33
|
-
|
34
|
-
# Forcefully initializes Hypothesis' global PRNG to avoid races that initialize it
|
35
|
-
# if e.g. Schemathesis CLI is used with multiple workers
|
36
|
-
with deterministic_PRNG():
|
37
|
-
pass
|
38
|
-
|
39
|
-
_patches.install()
|
40
|
-
|
41
|
-
|
42
|
-
def create_test(
|
43
|
-
*,
|
44
|
-
operation: APIOperation,
|
45
|
-
test: Callable,
|
46
|
-
settings: hypothesis.settings | None = None,
|
47
|
-
seed: int | None = None,
|
48
|
-
data_generation_methods: list[DataGenerationMethod],
|
49
|
-
generation_config: GenerationConfig | None = None,
|
50
|
-
as_strategy_kwargs: dict[str, Any] | None = None,
|
51
|
-
keep_async_fn: bool = False,
|
52
|
-
_given_args: tuple[GivenInput, ...] = (),
|
53
|
-
_given_kwargs: dict[str, GivenInput] | None = None,
|
54
|
-
) -> Callable:
|
55
|
-
"""Create a Hypothesis test."""
|
56
|
-
hook_dispatcher = getattr(test, "_schemathesis_hooks", None)
|
57
|
-
auth_storage = get_auth_storage_from_test(test)
|
58
|
-
strategies = []
|
59
|
-
skip_on_not_negated = len(data_generation_methods) == 1 and DataGenerationMethod.negative in data_generation_methods
|
60
|
-
as_strategy_kwargs = as_strategy_kwargs or {}
|
61
|
-
as_strategy_kwargs.update(
|
62
|
-
{
|
63
|
-
"hooks": hook_dispatcher,
|
64
|
-
"auth_storage": auth_storage,
|
65
|
-
"generation_config": generation_config,
|
66
|
-
"skip_on_not_negated": skip_on_not_negated,
|
67
|
-
}
|
68
|
-
)
|
69
|
-
for data_generation_method in data_generation_methods:
|
70
|
-
strategies.append(operation.as_strategy(data_generation_method=data_generation_method, **as_strategy_kwargs))
|
71
|
-
strategy = combine_strategies(strategies)
|
72
|
-
_given_kwargs = (_given_kwargs or {}).copy()
|
73
|
-
_given_kwargs.setdefault("case", strategy)
|
74
|
-
|
75
|
-
# Each generated test should be a unique function. It is especially important for the case when Schemathesis runs
|
76
|
-
# tests in multiple threads because Hypothesis stores some internal attributes on function objects and re-writing
|
77
|
-
# them from different threads may lead to unpredictable side-effects.
|
78
|
-
|
79
|
-
@wraps(test)
|
80
|
-
def test_function(*args: Any, **kwargs: Any) -> Any:
|
81
|
-
__tracebackhide__ = True
|
82
|
-
return test(*args, **kwargs)
|
83
|
-
|
84
|
-
wrapped_test = hypothesis.given(*_given_args, **_given_kwargs)(test_function)
|
85
|
-
if seed is not None:
|
86
|
-
wrapped_test = hypothesis.seed(seed)(wrapped_test)
|
87
|
-
if asyncio.iscoroutinefunction(test):
|
88
|
-
# `pytest-trio` expects a coroutine function
|
89
|
-
if keep_async_fn:
|
90
|
-
wrapped_test.hypothesis.inner_test = test # type: ignore
|
91
|
-
else:
|
92
|
-
wrapped_test.hypothesis.inner_test = make_async_test(test) # type: ignore
|
93
|
-
setup_default_deadline(wrapped_test)
|
94
|
-
if settings is not None:
|
95
|
-
existing_settings = _get_hypothesis_settings(wrapped_test)
|
96
|
-
if existing_settings is not None:
|
97
|
-
# Merge the user-provided settings with the current ones
|
98
|
-
default = hypothesis.settings.default
|
99
|
-
wrapped_test._hypothesis_internal_use_settings = hypothesis.settings(
|
100
|
-
wrapped_test._hypothesis_internal_use_settings,
|
101
|
-
**{item: value for item, value in settings.__dict__.items() if value != getattr(default, item)},
|
102
|
-
)
|
103
|
-
else:
|
104
|
-
wrapped_test = settings(wrapped_test)
|
105
|
-
existing_settings = _get_hypothesis_settings(wrapped_test)
|
106
|
-
if existing_settings is not None:
|
107
|
-
existing_settings = remove_explain_phase(existing_settings)
|
108
|
-
wrapped_test._hypothesis_internal_use_settings = existing_settings # type: ignore
|
109
|
-
if Phase.explicit in existing_settings.phases:
|
110
|
-
wrapped_test = add_examples(
|
111
|
-
wrapped_test, operation, hook_dispatcher=hook_dispatcher, as_strategy_kwargs=as_strategy_kwargs
|
112
|
-
)
|
113
|
-
if COVERAGE_PHASE.is_enabled:
|
114
|
-
wrapped_test = add_coverage(wrapped_test, operation, data_generation_methods)
|
115
|
-
return wrapped_test
|
116
|
-
|
117
|
-
|
118
|
-
def setup_default_deadline(wrapped_test: Callable) -> None:
|
119
|
-
# Quite hacky, but it is the simplest way to set up the default deadline value without affecting non-Schemathesis
|
120
|
-
# tests globally
|
121
|
-
existing_settings = _get_hypothesis_settings(wrapped_test)
|
122
|
-
if existing_settings is not None and existing_settings.deadline == hypothesis.settings.default.deadline:
|
123
|
-
with warnings.catch_warnings():
|
124
|
-
warnings.simplefilter("ignore", HypothesisWarning)
|
125
|
-
new_settings = hypothesis.settings(existing_settings, deadline=DEFAULT_DEADLINE)
|
126
|
-
wrapped_test._hypothesis_internal_use_settings = new_settings # type: ignore
|
127
|
-
|
128
|
-
|
129
|
-
def remove_explain_phase(settings: hypothesis.settings) -> hypothesis.settings:
|
130
|
-
# The "explain" phase is not supported
|
131
|
-
if Phase.explain in settings.phases:
|
132
|
-
phases = tuple(phase for phase in settings.phases if phase != Phase.explain)
|
133
|
-
return hypothesis.settings(settings, phases=phases)
|
134
|
-
return settings
|
135
|
-
|
136
|
-
|
137
|
-
def _get_hypothesis_settings(test: Callable) -> hypothesis.settings | None:
|
138
|
-
return getattr(test, "_hypothesis_internal_use_settings", None)
|
139
|
-
|
140
|
-
|
141
|
-
def make_async_test(test: Callable) -> Callable:
|
142
|
-
def async_run(*args: Any, **kwargs: Any) -> None:
|
143
|
-
try:
|
144
|
-
loop = asyncio.get_event_loop()
|
145
|
-
except RuntimeError:
|
146
|
-
loop = asyncio.new_event_loop()
|
147
|
-
coro = test(*args, **kwargs)
|
148
|
-
future = asyncio.ensure_future(coro, loop=loop)
|
149
|
-
loop.run_until_complete(future)
|
150
|
-
|
151
|
-
return async_run
|
152
|
-
|
153
|
-
|
154
|
-
def add_examples(
|
155
|
-
test: Callable,
|
156
|
-
operation: APIOperation,
|
157
|
-
hook_dispatcher: HookDispatcher | None = None,
|
158
|
-
as_strategy_kwargs: dict[str, Any] | None = None,
|
159
|
-
) -> Callable:
|
160
|
-
"""Add examples to the Hypothesis test, if they are specified in the schema."""
|
161
|
-
from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
|
162
|
-
|
163
|
-
try:
|
164
|
-
examples: list[Case] = [
|
165
|
-
get_single_example(strategy)
|
166
|
-
for strategy in operation.get_strategies_from_examples(as_strategy_kwargs=as_strategy_kwargs)
|
167
|
-
]
|
168
|
-
except (
|
169
|
-
OperationSchemaError,
|
170
|
-
HypothesisRefResolutionError,
|
171
|
-
Unsatisfiable,
|
172
|
-
SerializationNotPossible,
|
173
|
-
SchemaError,
|
174
|
-
) as exc:
|
175
|
-
# Invalid schema:
|
176
|
-
# In this case, the user didn't pass `--validate-schema=false` and see an error in the output anyway,
|
177
|
-
# and no tests will be executed. For this reason, examples can be skipped
|
178
|
-
# Recursive references: This test will be skipped anyway
|
179
|
-
# Unsatisfiable:
|
180
|
-
# The underlying schema is not satisfiable and test will raise an error for the same reason.
|
181
|
-
# Skipping this exception here allows us to continue the testing process for other operations.
|
182
|
-
# Still, we allow running user-defined hooks
|
183
|
-
examples = []
|
184
|
-
if isinstance(exc, Unsatisfiable):
|
185
|
-
add_unsatisfied_example_mark(test, exc)
|
186
|
-
if isinstance(exc, SerializationNotPossible):
|
187
|
-
add_non_serializable_mark(test, exc)
|
188
|
-
if isinstance(exc, SchemaError):
|
189
|
-
add_invalid_regex_mark(test, exc)
|
190
|
-
context = HookContext(operation) # context should be passed here instead
|
191
|
-
GLOBAL_HOOK_DISPATCHER.dispatch("before_add_examples", context, examples)
|
192
|
-
operation.schema.hooks.dispatch("before_add_examples", context, examples)
|
193
|
-
if hook_dispatcher:
|
194
|
-
hook_dispatcher.dispatch("before_add_examples", context, examples)
|
195
|
-
original_test = test
|
196
|
-
for example in examples:
|
197
|
-
if example.headers is not None:
|
198
|
-
invalid_headers = dict(find_invalid_headers(example.headers))
|
199
|
-
if invalid_headers:
|
200
|
-
add_invalid_example_header_mark(original_test, invalid_headers)
|
201
|
-
continue
|
202
|
-
adjust_urlencoded_payload(example)
|
203
|
-
test = hypothesis.example(case=example)(test)
|
204
|
-
return test
|
205
|
-
|
206
|
-
|
207
|
-
def adjust_urlencoded_payload(case: Case) -> None:
|
208
|
-
if case.media_type is not None:
|
209
|
-
try:
|
210
|
-
media_type = parse_content_type(case.media_type)
|
211
|
-
if media_type == ("application", "x-www-form-urlencoded"):
|
212
|
-
case.body = prepare_urlencoded(case.body)
|
213
|
-
except ValueError:
|
214
|
-
pass
|
215
|
-
|
216
|
-
|
217
|
-
def add_coverage(
|
218
|
-
test: Callable, operation: APIOperation, data_generation_methods: list[DataGenerationMethod]
|
219
|
-
) -> Callable:
|
220
|
-
for example in _iter_coverage_cases(operation, data_generation_methods):
|
221
|
-
adjust_urlencoded_payload(example)
|
222
|
-
test = hypothesis.example(case=example)(test)
|
223
|
-
return test
|
224
|
-
|
225
|
-
|
226
|
-
def _iter_coverage_cases(
|
227
|
-
operation: APIOperation, data_generation_methods: list[DataGenerationMethod]
|
228
|
-
) -> Generator[Case, None, None]:
|
229
|
-
from .specs.openapi.constants import LOCATION_TO_CONTAINER
|
230
|
-
from .specs.openapi.examples import find_in_responses, find_matching_in_responses
|
231
|
-
|
232
|
-
def _stringify_value(val: Any, location: str) -> str | list[str]:
|
233
|
-
if isinstance(val, list):
|
234
|
-
if location == "query":
|
235
|
-
# Having a list here ensures there will be multiple query parameters wit the same name
|
236
|
-
return [json.dumps(item) for item in val]
|
237
|
-
# use comma-separated values style for arrays
|
238
|
-
return ",".join(json.dumps(sub) for sub in val)
|
239
|
-
return json.dumps(val)
|
240
|
-
|
241
|
-
generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
|
242
|
-
template: dict[str, Any] = {}
|
243
|
-
responses = find_in_responses(operation)
|
244
|
-
for parameter in operation.iter_parameters():
|
245
|
-
location = parameter.location
|
246
|
-
name = parameter.name
|
247
|
-
schema = parameter.as_json_schema(operation, update_quantifiers=False)
|
248
|
-
for value in find_matching_in_responses(responses, parameter.name):
|
249
|
-
schema.setdefault("examples", []).append(value)
|
250
|
-
gen = coverage.cover_schema_iter(
|
251
|
-
coverage.CoverageContext(location=location, data_generation_methods=data_generation_methods), schema
|
252
|
-
)
|
253
|
-
value = next(gen, NOT_SET)
|
254
|
-
if isinstance(value, NotSet):
|
255
|
-
continue
|
256
|
-
container = template.setdefault(LOCATION_TO_CONTAINER[location], {})
|
257
|
-
if location in ("header", "cookie", "path", "query") and not isinstance(value.value, str):
|
258
|
-
container[name] = _stringify_value(value.value, location)
|
259
|
-
else:
|
260
|
-
container[name] = value.value
|
261
|
-
generators[(location, name)] = gen
|
262
|
-
if operation.body:
|
263
|
-
for body in operation.body:
|
264
|
-
schema = body.as_json_schema(operation, update_quantifiers=False)
|
265
|
-
# Definition could be a list for Open API 2.0
|
266
|
-
definition = body.definition if isinstance(body.definition, dict) else {}
|
267
|
-
examples = [example["value"] for example in definition.get("examples", {}).values() if "value" in example]
|
268
|
-
if examples:
|
269
|
-
schema.setdefault("examples", []).extend(examples)
|
270
|
-
gen = coverage.cover_schema_iter(
|
271
|
-
coverage.CoverageContext(location="body", data_generation_methods=data_generation_methods), schema
|
272
|
-
)
|
273
|
-
value = next(gen, NOT_SET)
|
274
|
-
if isinstance(value, NotSet):
|
275
|
-
continue
|
276
|
-
if "body" not in template:
|
277
|
-
template["body"] = value.value
|
278
|
-
template["media_type"] = body.media_type
|
279
|
-
case = operation.make_case(**{**template, "body": value.value, "media_type": body.media_type})
|
280
|
-
case.data_generation_method = value.data_generation_method
|
281
|
-
case.meta = _make_meta(
|
282
|
-
description=value.description,
|
283
|
-
location=value.location,
|
284
|
-
parameter=body.media_type,
|
285
|
-
parameter_location="body",
|
286
|
-
)
|
287
|
-
yield case
|
288
|
-
for next_value in gen:
|
289
|
-
case = operation.make_case(**{**template, "body": next_value.value, "media_type": body.media_type})
|
290
|
-
case.data_generation_method = next_value.data_generation_method
|
291
|
-
case.meta = _make_meta(
|
292
|
-
description=next_value.description,
|
293
|
-
location=next_value.location,
|
294
|
-
parameter=body.media_type,
|
295
|
-
parameter_location="body",
|
296
|
-
)
|
297
|
-
yield case
|
298
|
-
elif DataGenerationMethod.positive in data_generation_methods:
|
299
|
-
case = operation.make_case(**template)
|
300
|
-
case.data_generation_method = DataGenerationMethod.positive
|
301
|
-
case.meta = _make_meta(description="Default positive test case")
|
302
|
-
yield case
|
303
|
-
|
304
|
-
for (location, name), gen in generators.items():
|
305
|
-
container_name = LOCATION_TO_CONTAINER[location]
|
306
|
-
container = template[container_name]
|
307
|
-
for value in gen:
|
308
|
-
if location in ("header", "cookie", "path", "query") and not isinstance(value.value, str):
|
309
|
-
generated = _stringify_value(value.value, location)
|
310
|
-
else:
|
311
|
-
generated = value.value
|
312
|
-
case = operation.make_case(**{**template, container_name: {**container, name: generated}})
|
313
|
-
case.data_generation_method = value.data_generation_method
|
314
|
-
case.meta = _make_meta(
|
315
|
-
description=value.description,
|
316
|
-
location=value.location,
|
317
|
-
parameter=name,
|
318
|
-
parameter_location=location,
|
319
|
-
)
|
320
|
-
yield case
|
321
|
-
if DataGenerationMethod.negative in data_generation_methods:
|
322
|
-
# Generate HTTP methods that are not specified in the spec
|
323
|
-
# NOTE: The HEAD method is excluded
|
324
|
-
methods = {"get", "put", "post", "delete", "options", "patch", "trace"} - set(operation.schema[operation.path])
|
325
|
-
for method in sorted(methods):
|
326
|
-
case = operation.make_case(**template)
|
327
|
-
case._explicit_method = method
|
328
|
-
case.data_generation_method = DataGenerationMethod.negative
|
329
|
-
case.meta = _make_meta(description=f"Unspecified HTTP method: {method.upper()}")
|
330
|
-
yield case
|
331
|
-
# Generate duplicate query parameters
|
332
|
-
if operation.query:
|
333
|
-
container = template["query"]
|
334
|
-
for parameter in operation.query:
|
335
|
-
value = container[parameter.name]
|
336
|
-
case = operation.make_case(**{**template, "query": {**container, parameter.name: [value, value]}})
|
337
|
-
case.data_generation_method = DataGenerationMethod.negative
|
338
|
-
case.meta = _make_meta(
|
339
|
-
description=f"Duplicate `{parameter.name}` query parameter",
|
340
|
-
location=None,
|
341
|
-
parameter=parameter.name,
|
342
|
-
parameter_location="query",
|
343
|
-
)
|
344
|
-
yield case
|
345
|
-
# Generate missing required parameters
|
346
|
-
for parameter in operation.iter_parameters():
|
347
|
-
if parameter.is_required and parameter.location != "path":
|
348
|
-
name = parameter.name
|
349
|
-
location = parameter.location
|
350
|
-
container_name = LOCATION_TO_CONTAINER[location]
|
351
|
-
container = template[container_name]
|
352
|
-
case = operation.make_case(
|
353
|
-
**{**template, container_name: {k: v for k, v in container.items() if k != name}}
|
354
|
-
)
|
355
|
-
case.data_generation_method = DataGenerationMethod.negative
|
356
|
-
case.meta = _make_meta(
|
357
|
-
description=f"Missing `{name}` at {location}",
|
358
|
-
location=None,
|
359
|
-
parameter=name,
|
360
|
-
parameter_location=location,
|
361
|
-
)
|
362
|
-
yield case
|
363
|
-
# Generate combinations for each location
|
364
|
-
for location, parameter_set in [
|
365
|
-
("query", operation.query),
|
366
|
-
("header", operation.headers),
|
367
|
-
("cookie", operation.cookies),
|
368
|
-
]:
|
369
|
-
if not parameter_set:
|
370
|
-
continue
|
371
|
-
|
372
|
-
container_name = LOCATION_TO_CONTAINER[location]
|
373
|
-
base_container = template.get(container_name, {})
|
374
|
-
|
375
|
-
# Get required and optional parameters
|
376
|
-
required = {p.name for p in parameter_set if p.is_required}
|
377
|
-
all_params = {p.name for p in parameter_set}
|
378
|
-
optional = sorted(all_params - required)
|
379
|
-
|
380
|
-
# Helper function to create and yield a case
|
381
|
-
def make_case(
|
382
|
-
container_values: dict,
|
383
|
-
description: str,
|
384
|
-
_location: str,
|
385
|
-
_container_name: str,
|
386
|
-
_parameter: str | None,
|
387
|
-
_data_generation_method: DataGenerationMethod,
|
388
|
-
) -> Case:
|
389
|
-
if _location in ("header", "cookie", "path", "query"):
|
390
|
-
container = {
|
391
|
-
name: _stringify_value(val, _location) if not isinstance(val, str) else val
|
392
|
-
for name, val in container_values.items()
|
393
|
-
}
|
394
|
-
else:
|
395
|
-
container = container_values
|
396
|
-
|
397
|
-
case = operation.make_case(**{**template, _container_name: container})
|
398
|
-
case.data_generation_method = _data_generation_method
|
399
|
-
case.meta = _make_meta(
|
400
|
-
description=description,
|
401
|
-
location=None,
|
402
|
-
parameter=_parameter,
|
403
|
-
parameter_location=_location,
|
404
|
-
)
|
405
|
-
return case
|
406
|
-
|
407
|
-
def _combination_schema(
|
408
|
-
combination: dict[str, Any], _required: set[str], _parameter_set: ParameterSet
|
409
|
-
) -> dict[str, Any]:
|
410
|
-
return {
|
411
|
-
"properties": {
|
412
|
-
parameter.name: parameter.as_json_schema(operation)
|
413
|
-
for parameter in _parameter_set
|
414
|
-
if parameter.name in combination
|
415
|
-
},
|
416
|
-
"required": list(_required),
|
417
|
-
"additionalProperties": False,
|
418
|
-
}
|
419
|
-
|
420
|
-
def _yield_negative(
|
421
|
-
subschema: dict[str, Any], _location: str, _container_name: str
|
422
|
-
) -> Generator[Case, None, None]:
|
423
|
-
for more in coverage.cover_schema_iter(
|
424
|
-
coverage.CoverageContext(location=_location, data_generation_methods=[DataGenerationMethod.negative]),
|
425
|
-
subschema,
|
426
|
-
):
|
427
|
-
yield make_case(
|
428
|
-
more.value,
|
429
|
-
more.description,
|
430
|
-
_location,
|
431
|
-
_container_name,
|
432
|
-
more.parameter,
|
433
|
-
DataGenerationMethod.negative,
|
434
|
-
)
|
435
|
-
|
436
|
-
# 1. Generate only required properties
|
437
|
-
if required and all_params != required:
|
438
|
-
only_required = {k: v for k, v in base_container.items() if k in required}
|
439
|
-
if DataGenerationMethod.positive in data_generation_methods:
|
440
|
-
yield make_case(
|
441
|
-
only_required,
|
442
|
-
"Only required properties",
|
443
|
-
location,
|
444
|
-
container_name,
|
445
|
-
None,
|
446
|
-
DataGenerationMethod.positive,
|
447
|
-
)
|
448
|
-
if DataGenerationMethod.negative in data_generation_methods:
|
449
|
-
subschema = _combination_schema(only_required, required, parameter_set)
|
450
|
-
for case in _yield_negative(subschema, location, container_name):
|
451
|
-
# Already generated in one of the blocks above
|
452
|
-
if location != "path" and not case.meta.description.startswith("Missing required property"):
|
453
|
-
yield case
|
454
|
-
|
455
|
-
# 2. Generate combinations with required properties and one optional property
|
456
|
-
for opt_param in optional:
|
457
|
-
combo = {k: v for k, v in base_container.items() if k in required or k == opt_param}
|
458
|
-
if combo != base_container and DataGenerationMethod.positive in data_generation_methods:
|
459
|
-
yield make_case(
|
460
|
-
combo,
|
461
|
-
f"All required properties and optional '{opt_param}'",
|
462
|
-
location,
|
463
|
-
container_name,
|
464
|
-
None,
|
465
|
-
DataGenerationMethod.positive,
|
466
|
-
)
|
467
|
-
if DataGenerationMethod.negative in data_generation_methods:
|
468
|
-
subschema = _combination_schema(combo, required, parameter_set)
|
469
|
-
for case in _yield_negative(subschema, location, container_name):
|
470
|
-
# Already generated in one of the blocks above
|
471
|
-
if location != "path" and not case.meta.description.startswith("Missing required property"):
|
472
|
-
yield case
|
473
|
-
|
474
|
-
# 3. Generate one combination for each size from 2 to N-1 of optional parameters
|
475
|
-
if len(optional) > 1 and DataGenerationMethod.positive in data_generation_methods:
|
476
|
-
for size in range(2, len(optional)):
|
477
|
-
for combination in combinations(optional, size):
|
478
|
-
combo = {k: v for k, v in base_container.items() if k in required or k in combination}
|
479
|
-
if combo != base_container:
|
480
|
-
yield make_case(
|
481
|
-
combo,
|
482
|
-
f"All required and {size} optional properties",
|
483
|
-
location,
|
484
|
-
container_name,
|
485
|
-
None,
|
486
|
-
DataGenerationMethod.positive,
|
487
|
-
)
|
488
|
-
|
489
|
-
|
490
|
-
def _make_meta(
|
491
|
-
*,
|
492
|
-
description: str,
|
493
|
-
location: str | None = None,
|
494
|
-
parameter: str | None = None,
|
495
|
-
parameter_location: str | None = None,
|
496
|
-
) -> GenerationMetadata:
|
497
|
-
return GenerationMetadata(
|
498
|
-
query=None,
|
499
|
-
path_parameters=None,
|
500
|
-
headers=None,
|
501
|
-
cookies=None,
|
502
|
-
body=None,
|
503
|
-
phase=TestPhase.COVERAGE,
|
504
|
-
description=description,
|
505
|
-
location=location,
|
506
|
-
parameter=parameter,
|
507
|
-
parameter_location=parameter_location,
|
508
|
-
)
|
509
|
-
|
510
|
-
|
511
|
-
def find_invalid_headers(headers: Mapping) -> Generator[tuple[str, str], None, None]:
|
512
|
-
for name, value in headers.items():
|
513
|
-
if not is_latin_1_encodable(value) or has_invalid_characters(name, value):
|
514
|
-
yield name, value
|
515
|
-
|
516
|
-
|
517
|
-
def prepare_urlencoded(data: Any) -> Any:
|
518
|
-
if isinstance(data, list):
|
519
|
-
output = []
|
520
|
-
for item in data:
|
521
|
-
if isinstance(item, dict):
|
522
|
-
for key, value in item.items():
|
523
|
-
output.append((key, value))
|
524
|
-
else:
|
525
|
-
output.append((item, "arbitrary-value"))
|
526
|
-
return output
|
527
|
-
return data
|
528
|
-
|
529
|
-
|
530
|
-
def add_unsatisfied_example_mark(test: Callable, exc: Unsatisfiable) -> None:
|
531
|
-
test._schemathesis_unsatisfied_example = exc # type: ignore
|
532
|
-
|
533
|
-
|
534
|
-
def has_unsatisfied_example_mark(test: Callable) -> bool:
|
535
|
-
return hasattr(test, "_schemathesis_unsatisfied_example")
|
536
|
-
|
537
|
-
|
538
|
-
def add_non_serializable_mark(test: Callable, exc: SerializationNotPossible) -> None:
|
539
|
-
test._schemathesis_non_serializable = exc # type: ignore
|
540
|
-
|
541
|
-
|
542
|
-
def get_non_serializable_mark(test: Callable) -> SerializationNotPossible | None:
|
543
|
-
return getattr(test, "_schemathesis_non_serializable", None)
|
544
|
-
|
545
|
-
|
546
|
-
def get_invalid_regex_mark(test: Callable) -> SchemaError | None:
|
547
|
-
return getattr(test, "_schemathesis_invalid_regex", None)
|
548
|
-
|
549
|
-
|
550
|
-
def add_invalid_regex_mark(test: Callable, exc: SchemaError) -> None:
|
551
|
-
test._schemathesis_invalid_regex = exc # type: ignore
|
552
|
-
|
553
|
-
|
554
|
-
def get_invalid_example_headers_mark(test: Callable) -> dict[str, str] | None:
|
555
|
-
return getattr(test, "_schemathesis_invalid_example_headers", None)
|
556
|
-
|
557
|
-
|
558
|
-
def add_invalid_example_header_mark(test: Callable, headers: dict[str, str]) -> None:
|
559
|
-
test._schemathesis_invalid_example_headers = headers # type: ignore
|
schemathesis/_override.py
DELETED
@@ -1,50 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from dataclasses import dataclass
|
4
|
-
from typing import TYPE_CHECKING
|
5
|
-
|
6
|
-
from .exceptions import UsageError
|
7
|
-
|
8
|
-
if TYPE_CHECKING:
|
9
|
-
from .models import APIOperation
|
10
|
-
from .parameters import ParameterSet
|
11
|
-
from .types import GenericTest
|
12
|
-
|
13
|
-
|
14
|
-
@dataclass
|
15
|
-
class CaseOverride:
|
16
|
-
"""Overrides for various parts of a test case."""
|
17
|
-
|
18
|
-
query: dict[str, str]
|
19
|
-
headers: dict[str, str]
|
20
|
-
cookies: dict[str, str]
|
21
|
-
path_parameters: dict[str, str]
|
22
|
-
|
23
|
-
def for_operation(self, operation: APIOperation) -> dict[str, dict[str, str]]:
|
24
|
-
return {
|
25
|
-
"query": (_for_parameters(self.query, operation.query)),
|
26
|
-
"headers": (_for_parameters(self.headers, operation.headers)),
|
27
|
-
"cookies": (_for_parameters(self.cookies, operation.cookies)),
|
28
|
-
"path_parameters": (_for_parameters(self.path_parameters, operation.path_parameters)),
|
29
|
-
}
|
30
|
-
|
31
|
-
|
32
|
-
def _for_parameters(overridden: dict[str, str], defined: ParameterSet) -> dict[str, str]:
|
33
|
-
output = {}
|
34
|
-
for param in defined:
|
35
|
-
if param.name in overridden:
|
36
|
-
output[param.name] = overridden[param.name]
|
37
|
-
return output
|
38
|
-
|
39
|
-
|
40
|
-
def get_override_from_mark(test: GenericTest) -> CaseOverride | None:
|
41
|
-
return getattr(test, "_schemathesis_override", None)
|
42
|
-
|
43
|
-
|
44
|
-
def set_override_mark(test: GenericTest, override: CaseOverride) -> None:
|
45
|
-
test._schemathesis_override = override # type: ignore[attr-defined]
|
46
|
-
|
47
|
-
|
48
|
-
def check_no_override_mark(test: GenericTest) -> None:
|
49
|
-
if hasattr(test, "_schemathesis_override"):
|
50
|
-
raise UsageError(f"`{test.__name__}` has already been decorated with `override`.")
|
schemathesis/_rate_limiter.py
DELETED
schemathesis/cli/context.py
DELETED
@@ -1,75 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import shutil
|
4
|
-
from dataclasses import dataclass, field
|
5
|
-
from typing import TYPE_CHECKING, Generator
|
6
|
-
|
7
|
-
from ..code_samples import CodeSampleStyle
|
8
|
-
from ..internal.deprecation import deprecated_property
|
9
|
-
from ..internal.output import OutputConfig
|
10
|
-
|
11
|
-
if TYPE_CHECKING:
|
12
|
-
import os
|
13
|
-
from queue import Queue
|
14
|
-
|
15
|
-
import hypothesis
|
16
|
-
|
17
|
-
from ..internal.result import Result
|
18
|
-
from ..runner.probes import ProbeRun
|
19
|
-
from ..runner.serialization import SerializedTestResult
|
20
|
-
from ..service.models import AnalysisResult
|
21
|
-
from ..stateful.sink import StateMachineSink
|
22
|
-
|
23
|
-
|
24
|
-
@dataclass
|
25
|
-
class ServiceReportContext:
|
26
|
-
queue: Queue
|
27
|
-
service_base_url: str
|
28
|
-
|
29
|
-
|
30
|
-
@dataclass
|
31
|
-
class FileReportContext:
|
32
|
-
queue: Queue
|
33
|
-
filename: str | None = None
|
34
|
-
|
35
|
-
|
36
|
-
@dataclass
|
37
|
-
class ExecutionContext:
|
38
|
-
"""Storage for the current context of the execution."""
|
39
|
-
|
40
|
-
hypothesis_settings: hypothesis.settings
|
41
|
-
hypothesis_output: list[str] = field(default_factory=list)
|
42
|
-
workers_num: int = 1
|
43
|
-
rate_limit: str | None = None
|
44
|
-
show_trace: bool = False
|
45
|
-
wait_for_schema: float | None = None
|
46
|
-
validate_schema: bool = True
|
47
|
-
operations_processed: int = 0
|
48
|
-
# It is set in runtime, from the `Initialized` event
|
49
|
-
operations_count: int | None = None
|
50
|
-
seed: int | None = None
|
51
|
-
current_line_length: int = 0
|
52
|
-
terminal_size: os.terminal_size = field(default_factory=shutil.get_terminal_size)
|
53
|
-
results: list[SerializedTestResult] = field(default_factory=list)
|
54
|
-
cassette_path: str | None = None
|
55
|
-
junit_xml_file: str | None = None
|
56
|
-
is_interrupted: bool = False
|
57
|
-
verbosity: int = 0
|
58
|
-
code_sample_style: CodeSampleStyle = CodeSampleStyle.default()
|
59
|
-
report: ServiceReportContext | FileReportContext | None = None
|
60
|
-
probes: list[ProbeRun] | None = None
|
61
|
-
analysis: Result[AnalysisResult, Exception] | None = None
|
62
|
-
output_config: OutputConfig = field(default_factory=OutputConfig)
|
63
|
-
state_machine_sink: StateMachineSink | None = None
|
64
|
-
initialization_lines: list[str | Generator[str, None, None]] = field(default_factory=list)
|
65
|
-
summary_lines: list[str | Generator[str, None, None]] = field(default_factory=list)
|
66
|
-
|
67
|
-
@deprecated_property(removed_in="4.0", replacement="show_trace")
|
68
|
-
def show_errors_tracebacks(self) -> bool:
|
69
|
-
return self.show_trace
|
70
|
-
|
71
|
-
def add_initialization_line(self, line: str | Generator[str, None, None]) -> None:
|
72
|
-
self.initialization_lines.append(line)
|
73
|
-
|
74
|
-
def add_summary_line(self, line: str | Generator[str, None, None]) -> None:
|
75
|
-
self.summary_lines.append(line)
|