schemathesis 3.39.16__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 +233 -307
- 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.16.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
- {schemathesis-3.39.16.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 -717
- 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.16.dist-info/METADATA +0 -293
- schemathesis-3.39.16.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.16.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,800 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
from dataclasses import dataclass, field
|
5
|
+
from enum import Enum
|
6
|
+
from functools import wraps
|
7
|
+
from itertools import combinations
|
8
|
+
from time import perf_counter
|
9
|
+
from typing import Any, Callable, Generator, Mapping
|
10
|
+
|
11
|
+
import hypothesis
|
12
|
+
from hypothesis import Phase, Verbosity
|
13
|
+
from hypothesis import strategies as st
|
14
|
+
from hypothesis._settings import all_settings
|
15
|
+
from hypothesis.errors import Unsatisfiable
|
16
|
+
from jsonschema.exceptions import SchemaError
|
17
|
+
from requests.models import CaseInsensitiveDict
|
18
|
+
|
19
|
+
from schemathesis import auths
|
20
|
+
from schemathesis.auths import AuthStorage, AuthStorageMark
|
21
|
+
from schemathesis.config import ProjectConfig
|
22
|
+
from schemathesis.core import NOT_SET, NotSet, SpecificationFeature, media_types
|
23
|
+
from schemathesis.core.errors import InvalidSchema, SerializationNotPossible
|
24
|
+
from schemathesis.core.marks import Mark
|
25
|
+
from schemathesis.core.transport import prepare_urlencoded
|
26
|
+
from schemathesis.core.validation import has_invalid_characters, is_latin_1_encodable
|
27
|
+
from schemathesis.generation import GenerationMode, coverage
|
28
|
+
from schemathesis.generation.case import Case
|
29
|
+
from schemathesis.generation.hypothesis import DEFAULT_DEADLINE, examples, setup, strategies
|
30
|
+
from schemathesis.generation.hypothesis.examples import add_single_example
|
31
|
+
from schemathesis.generation.hypothesis.given import GivenInput
|
32
|
+
from schemathesis.generation.meta import (
|
33
|
+
CaseMetadata,
|
34
|
+
ComponentInfo,
|
35
|
+
ComponentKind,
|
36
|
+
CoveragePhaseData,
|
37
|
+
GenerationInfo,
|
38
|
+
PhaseInfo,
|
39
|
+
)
|
40
|
+
from schemathesis.hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookDispatcherMark
|
41
|
+
from schemathesis.schemas import APIOperation, ParameterSet
|
42
|
+
|
43
|
+
setup()
|
44
|
+
|
45
|
+
|
46
|
+
class HypothesisTestMode(str, Enum):
|
47
|
+
EXAMPLES = "examples"
|
48
|
+
COVERAGE = "coverage"
|
49
|
+
FUZZING = "fuzzing"
|
50
|
+
|
51
|
+
|
52
|
+
@dataclass
|
53
|
+
class HypothesisTestConfig:
|
54
|
+
project: ProjectConfig
|
55
|
+
modes: list[HypothesisTestMode]
|
56
|
+
settings: hypothesis.settings | None = None
|
57
|
+
seed: int | None = None
|
58
|
+
as_strategy_kwargs: dict[str, Any] = field(default_factory=dict)
|
59
|
+
given_args: tuple[GivenInput, ...] = ()
|
60
|
+
given_kwargs: dict[str, GivenInput] = field(default_factory=dict)
|
61
|
+
|
62
|
+
|
63
|
+
def create_test(
|
64
|
+
*,
|
65
|
+
operation: APIOperation,
|
66
|
+
test_func: Callable,
|
67
|
+
config: HypothesisTestConfig,
|
68
|
+
) -> Callable:
|
69
|
+
"""Create a Hypothesis test."""
|
70
|
+
hook_dispatcher = HookDispatcherMark.get(test_func)
|
71
|
+
auth_storage = AuthStorageMark.get(test_func)
|
72
|
+
|
73
|
+
strategy_kwargs = {
|
74
|
+
"hooks": hook_dispatcher,
|
75
|
+
"auth_storage": auth_storage,
|
76
|
+
**config.as_strategy_kwargs,
|
77
|
+
}
|
78
|
+
generation = config.project.generation_for(operation=operation)
|
79
|
+
strategy = strategies.combine(
|
80
|
+
[operation.as_strategy(generation_mode=mode, **strategy_kwargs) for mode in generation.modes]
|
81
|
+
)
|
82
|
+
|
83
|
+
hypothesis_test = create_base_test(
|
84
|
+
test_function=test_func,
|
85
|
+
strategy=strategy,
|
86
|
+
args=config.given_args,
|
87
|
+
kwargs=config.given_kwargs,
|
88
|
+
)
|
89
|
+
|
90
|
+
if config.seed is not None:
|
91
|
+
hypothesis_test = hypothesis.seed(config.seed)(hypothesis_test)
|
92
|
+
|
93
|
+
default = hypothesis.settings.default
|
94
|
+
settings = getattr(hypothesis_test, SETTINGS_ATTRIBUTE_NAME, None)
|
95
|
+
assert settings is not None
|
96
|
+
|
97
|
+
if settings.deadline == default.deadline:
|
98
|
+
settings = hypothesis.settings(settings, deadline=DEFAULT_DEADLINE)
|
99
|
+
|
100
|
+
if settings.verbosity == default.verbosity:
|
101
|
+
settings = hypothesis.settings(settings, verbosity=Verbosity.quiet)
|
102
|
+
|
103
|
+
if config.settings is not None:
|
104
|
+
# Merge the user-provided settings with the current ones
|
105
|
+
settings = hypothesis.settings(
|
106
|
+
config.settings,
|
107
|
+
**{
|
108
|
+
item: getattr(settings, item)
|
109
|
+
for item in all_settings
|
110
|
+
if getattr(settings, item) != getattr(default, item)
|
111
|
+
},
|
112
|
+
)
|
113
|
+
|
114
|
+
if Phase.explain in settings.phases:
|
115
|
+
phases = tuple(phase for phase in settings.phases if phase != Phase.explain)
|
116
|
+
settings = hypothesis.settings(settings, phases=phases)
|
117
|
+
|
118
|
+
# Remove `reuse` & `generate` phases to avoid yielding any test cases if we don't do fuzzing
|
119
|
+
if HypothesisTestMode.FUZZING not in config.modes and (
|
120
|
+
Phase.generate in settings.phases or Phase.reuse in settings.phases
|
121
|
+
):
|
122
|
+
phases = tuple(phase for phase in settings.phases if phase not in (Phase.reuse, Phase.generate))
|
123
|
+
settings = hypothesis.settings(settings, phases=phases)
|
124
|
+
|
125
|
+
specification = operation.schema.specification
|
126
|
+
|
127
|
+
# Add examples if explicit phase is enabled
|
128
|
+
if (
|
129
|
+
HypothesisTestMode.EXAMPLES in config.modes
|
130
|
+
and Phase.explicit in settings.phases
|
131
|
+
and specification.supports_feature(SpecificationFeature.EXAMPLES)
|
132
|
+
):
|
133
|
+
phases_config = config.project.phases_for(operation=operation)
|
134
|
+
hypothesis_test = add_examples(
|
135
|
+
hypothesis_test,
|
136
|
+
operation,
|
137
|
+
fill_missing=phases_config.examples.fill_missing,
|
138
|
+
hook_dispatcher=hook_dispatcher,
|
139
|
+
**strategy_kwargs,
|
140
|
+
)
|
141
|
+
|
142
|
+
if (
|
143
|
+
HypothesisTestMode.COVERAGE in config.modes
|
144
|
+
and Phase.explicit in settings.phases
|
145
|
+
and specification.supports_feature(SpecificationFeature.COVERAGE)
|
146
|
+
and not config.given_args
|
147
|
+
and not config.given_kwargs
|
148
|
+
):
|
149
|
+
phases_config = config.project.phases_for(operation=operation)
|
150
|
+
hypothesis_test = add_coverage(
|
151
|
+
hypothesis_test,
|
152
|
+
operation,
|
153
|
+
generation.modes,
|
154
|
+
auth_storage,
|
155
|
+
config.as_strategy_kwargs,
|
156
|
+
generate_duplicate_query_parameters=phases_config.coverage.generate_duplicate_query_parameters,
|
157
|
+
unexpected_methods=phases_config.coverage.unexpected_methods,
|
158
|
+
)
|
159
|
+
|
160
|
+
setattr(hypothesis_test, SETTINGS_ATTRIBUTE_NAME, settings)
|
161
|
+
|
162
|
+
return hypothesis_test
|
163
|
+
|
164
|
+
|
165
|
+
SETTINGS_ATTRIBUTE_NAME = "_hypothesis_internal_use_settings"
|
166
|
+
|
167
|
+
|
168
|
+
def create_base_test(
|
169
|
+
*,
|
170
|
+
test_function: Callable,
|
171
|
+
strategy: st.SearchStrategy,
|
172
|
+
args: tuple[GivenInput, ...],
|
173
|
+
kwargs: dict[str, GivenInput],
|
174
|
+
) -> Callable:
|
175
|
+
"""Create the basic Hypothesis test with the given strategy."""
|
176
|
+
|
177
|
+
@wraps(test_function)
|
178
|
+
def test_wrapper(*args: Any, **kwargs: Any) -> Any:
|
179
|
+
__tracebackhide__ = True
|
180
|
+
return test_function(*args, **kwargs)
|
181
|
+
|
182
|
+
funcobj = hypothesis.given(*args, **{**kwargs, "case": strategy})(test_wrapper)
|
183
|
+
|
184
|
+
if asyncio.iscoroutinefunction(test_function):
|
185
|
+
funcobj.hypothesis.inner_test = make_async_test(test_function) # type: ignore
|
186
|
+
return funcobj
|
187
|
+
|
188
|
+
|
189
|
+
def make_async_test(test: Callable) -> Callable:
|
190
|
+
def async_run(*args: Any, **kwargs: Any) -> None:
|
191
|
+
try:
|
192
|
+
loop = asyncio.get_event_loop()
|
193
|
+
except RuntimeError:
|
194
|
+
loop = asyncio.new_event_loop()
|
195
|
+
coro = test(*args, **kwargs)
|
196
|
+
future = asyncio.ensure_future(coro, loop=loop)
|
197
|
+
loop.run_until_complete(future)
|
198
|
+
|
199
|
+
return async_run
|
200
|
+
|
201
|
+
|
202
|
+
def add_examples(
|
203
|
+
test: Callable,
|
204
|
+
operation: APIOperation,
|
205
|
+
fill_missing: bool,
|
206
|
+
hook_dispatcher: HookDispatcher | None = None,
|
207
|
+
**kwargs: Any,
|
208
|
+
) -> Callable:
|
209
|
+
"""Add examples to the Hypothesis test, if they are specified in the schema."""
|
210
|
+
from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
|
211
|
+
|
212
|
+
try:
|
213
|
+
result: list[Case] = [
|
214
|
+
examples.generate_one(strategy) for strategy in operation.get_strategies_from_examples(**kwargs)
|
215
|
+
]
|
216
|
+
except (
|
217
|
+
InvalidSchema,
|
218
|
+
HypothesisRefResolutionError,
|
219
|
+
Unsatisfiable,
|
220
|
+
SerializationNotPossible,
|
221
|
+
SchemaError,
|
222
|
+
) as exc:
|
223
|
+
result = []
|
224
|
+
if isinstance(exc, Unsatisfiable):
|
225
|
+
UnsatisfiableExampleMark.set(test, exc)
|
226
|
+
if isinstance(exc, SerializationNotPossible):
|
227
|
+
NonSerializableMark.set(test, exc)
|
228
|
+
if isinstance(exc, SchemaError):
|
229
|
+
InvalidRegexMark.set(test, exc)
|
230
|
+
|
231
|
+
if fill_missing and not result:
|
232
|
+
strategy = operation.as_strategy()
|
233
|
+
add_single_example(strategy, result)
|
234
|
+
|
235
|
+
context = HookContext(operation=operation) # context should be passed here instead
|
236
|
+
GLOBAL_HOOK_DISPATCHER.dispatch("before_add_examples", context, result)
|
237
|
+
operation.schema.hooks.dispatch("before_add_examples", context, result)
|
238
|
+
if hook_dispatcher:
|
239
|
+
hook_dispatcher.dispatch("before_add_examples", context, result)
|
240
|
+
original_test = test
|
241
|
+
for example in result:
|
242
|
+
if example.headers is not None:
|
243
|
+
invalid_headers = dict(find_invalid_headers(example.headers))
|
244
|
+
if invalid_headers:
|
245
|
+
InvalidHeadersExampleMark.set(original_test, invalid_headers)
|
246
|
+
continue
|
247
|
+
adjust_urlencoded_payload(example)
|
248
|
+
test = hypothesis.example(case=example)(test)
|
249
|
+
|
250
|
+
return test
|
251
|
+
|
252
|
+
|
253
|
+
def adjust_urlencoded_payload(case: Case) -> None:
|
254
|
+
if case.media_type is not None:
|
255
|
+
try:
|
256
|
+
media_type = media_types.parse(case.media_type)
|
257
|
+
if media_type == ("application", "x-www-form-urlencoded"):
|
258
|
+
case.body = prepare_urlencoded(case.body)
|
259
|
+
except ValueError:
|
260
|
+
pass
|
261
|
+
|
262
|
+
|
263
|
+
def add_coverage(
|
264
|
+
test: Callable,
|
265
|
+
operation: APIOperation,
|
266
|
+
generation_modes: list[GenerationMode],
|
267
|
+
auth_storage: AuthStorage | None,
|
268
|
+
as_strategy_kwargs: dict[str, Any],
|
269
|
+
generate_duplicate_query_parameters: bool,
|
270
|
+
unexpected_methods: set[str] | None = None,
|
271
|
+
) -> Callable:
|
272
|
+
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
273
|
+
|
274
|
+
auth_context = auths.AuthContext(
|
275
|
+
operation=operation,
|
276
|
+
app=operation.app,
|
277
|
+
)
|
278
|
+
overrides = {
|
279
|
+
container: as_strategy_kwargs[container]
|
280
|
+
for container in LOCATION_TO_CONTAINER.values()
|
281
|
+
if container in as_strategy_kwargs
|
282
|
+
}
|
283
|
+
for case in _iter_coverage_cases(
|
284
|
+
operation, generation_modes, generate_duplicate_query_parameters, unexpected_methods
|
285
|
+
):
|
286
|
+
if case.media_type and operation.schema.transport.get_first_matching_media_type(case.media_type) is None:
|
287
|
+
continue
|
288
|
+
adjust_urlencoded_payload(case)
|
289
|
+
auths.set_on_case(case, auth_context, auth_storage)
|
290
|
+
for container_name, value in overrides.items():
|
291
|
+
container = getattr(case, container_name)
|
292
|
+
if container is None:
|
293
|
+
setattr(case, container_name, value)
|
294
|
+
else:
|
295
|
+
container.update(value)
|
296
|
+
|
297
|
+
test = hypothesis.example(case=case)(test)
|
298
|
+
return test
|
299
|
+
|
300
|
+
|
301
|
+
class Instant:
|
302
|
+
__slots__ = ("start",)
|
303
|
+
|
304
|
+
def __init__(self) -> None:
|
305
|
+
self.start = perf_counter()
|
306
|
+
|
307
|
+
@property
|
308
|
+
def elapsed(self) -> float:
|
309
|
+
return perf_counter() - self.start
|
310
|
+
|
311
|
+
|
312
|
+
class Template:
|
313
|
+
__slots__ = ("_components", "_template", "_serializers")
|
314
|
+
|
315
|
+
def __init__(self, serializers: dict[str, Callable]) -> None:
|
316
|
+
self._components: dict[ComponentKind, ComponentInfo] = {}
|
317
|
+
self._template: dict[str, Any] = {}
|
318
|
+
self._serializers = serializers
|
319
|
+
|
320
|
+
def __contains__(self, key: str) -> bool:
|
321
|
+
return key in self._template
|
322
|
+
|
323
|
+
def __getitem__(self, key: str) -> dict:
|
324
|
+
return self._template[key]
|
325
|
+
|
326
|
+
def get(self, key: str, default: Any = None) -> dict:
|
327
|
+
return self._template.get(key, default)
|
328
|
+
|
329
|
+
def add_parameter(self, location: str, name: str, value: coverage.GeneratedValue) -> None:
|
330
|
+
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
331
|
+
|
332
|
+
component_name = LOCATION_TO_CONTAINER[location]
|
333
|
+
kind = ComponentKind(component_name)
|
334
|
+
info = self._components.get(kind)
|
335
|
+
if info is None:
|
336
|
+
self._components[kind] = ComponentInfo(mode=value.generation_mode)
|
337
|
+
elif value.generation_mode == GenerationMode.NEGATIVE:
|
338
|
+
info.mode = GenerationMode.NEGATIVE
|
339
|
+
|
340
|
+
container = self._template.setdefault(component_name, {})
|
341
|
+
container[name] = value.value
|
342
|
+
|
343
|
+
def set_body(self, body: coverage.GeneratedValue, media_type: str) -> None:
|
344
|
+
self._template["body"] = body.value
|
345
|
+
self._template["media_type"] = media_type
|
346
|
+
self._components[ComponentKind.BODY] = ComponentInfo(mode=body.generation_mode)
|
347
|
+
|
348
|
+
def _serialize(self, kwargs: dict[str, Any]) -> dict[str, Any]:
|
349
|
+
from schemathesis.specs.openapi._hypothesis import quote_all
|
350
|
+
|
351
|
+
output = {}
|
352
|
+
for container_name, value in kwargs.items():
|
353
|
+
serializer = self._serializers.get(container_name)
|
354
|
+
if container_name in ("headers", "cookies") and isinstance(value, dict):
|
355
|
+
value = _stringify_value(value, container_name)
|
356
|
+
if serializer is not None:
|
357
|
+
value = serializer(value)
|
358
|
+
if container_name == "query" and isinstance(value, dict):
|
359
|
+
value = _stringify_value(value, container_name)
|
360
|
+
if container_name == "path_parameters" and isinstance(value, dict):
|
361
|
+
value = _stringify_value(quote_all(value), container_name)
|
362
|
+
output[container_name] = value
|
363
|
+
return output
|
364
|
+
|
365
|
+
def unmodified(self) -> TemplateValue:
|
366
|
+
kwargs = self._template.copy()
|
367
|
+
kwargs = self._serialize(kwargs)
|
368
|
+
return TemplateValue(kwargs=kwargs, components=self._components.copy())
|
369
|
+
|
370
|
+
def with_body(self, *, media_type: str, value: coverage.GeneratedValue) -> TemplateValue:
|
371
|
+
kwargs = {**self._template, "media_type": media_type, "body": value.value}
|
372
|
+
kwargs = self._serialize(kwargs)
|
373
|
+
components = {**self._components, ComponentKind.BODY: ComponentInfo(mode=value.generation_mode)}
|
374
|
+
return TemplateValue(kwargs=kwargs, components=components)
|
375
|
+
|
376
|
+
def with_parameter(self, *, location: str, name: str, value: coverage.GeneratedValue) -> TemplateValue:
|
377
|
+
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
378
|
+
|
379
|
+
container_name = LOCATION_TO_CONTAINER[location]
|
380
|
+
container = self._template[container_name]
|
381
|
+
return self.with_container(
|
382
|
+
container_name=container_name, value={**container, name: value.value}, generation_mode=value.generation_mode
|
383
|
+
)
|
384
|
+
|
385
|
+
def with_container(self, *, container_name: str, value: Any, generation_mode: GenerationMode) -> TemplateValue:
|
386
|
+
kwargs = {**self._template, container_name: value}
|
387
|
+
components = {**self._components, ComponentKind(container_name): ComponentInfo(mode=generation_mode)}
|
388
|
+
kwargs = self._serialize(kwargs)
|
389
|
+
return TemplateValue(kwargs=kwargs, components=components)
|
390
|
+
|
391
|
+
|
392
|
+
@dataclass
|
393
|
+
class TemplateValue:
|
394
|
+
kwargs: dict[str, Any]
|
395
|
+
components: dict[ComponentKind, ComponentInfo]
|
396
|
+
__slots__ = ("kwargs", "components")
|
397
|
+
|
398
|
+
|
399
|
+
def _stringify_value(val: Any, container_name: str) -> Any:
|
400
|
+
if val is None:
|
401
|
+
return "null"
|
402
|
+
if val is True:
|
403
|
+
return "true"
|
404
|
+
if val is False:
|
405
|
+
return "false"
|
406
|
+
if isinstance(val, (int, float)):
|
407
|
+
return str(val)
|
408
|
+
if isinstance(val, list):
|
409
|
+
if container_name == "query":
|
410
|
+
# Having a list here ensures there will be multiple query parameters wit the same name
|
411
|
+
return [_stringify_value(item, container_name) for item in val]
|
412
|
+
# use comma-separated values style for arrays
|
413
|
+
return ",".join(str(_stringify_value(sub, container_name)) for sub in val)
|
414
|
+
if isinstance(val, dict):
|
415
|
+
return {key: _stringify_value(sub, container_name) for key, sub in val.items()}
|
416
|
+
return val
|
417
|
+
|
418
|
+
|
419
|
+
def _iter_coverage_cases(
|
420
|
+
operation: APIOperation,
|
421
|
+
generation_modes: list[GenerationMode],
|
422
|
+
generate_duplicate_query_parameters: bool,
|
423
|
+
unexpected_methods: set[str] | None = None,
|
424
|
+
) -> Generator[Case, None, None]:
|
425
|
+
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
426
|
+
from schemathesis.specs.openapi.examples import find_in_responses, find_matching_in_responses
|
427
|
+
from schemathesis.specs.openapi.serialization import get_serializers_for_operation
|
428
|
+
|
429
|
+
generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
|
430
|
+
serializers = get_serializers_for_operation(operation)
|
431
|
+
template = Template(serializers)
|
432
|
+
|
433
|
+
instant = Instant()
|
434
|
+
responses = find_in_responses(operation)
|
435
|
+
# NOTE: The HEAD method is excluded
|
436
|
+
unexpected_methods = unexpected_methods or {"get", "put", "post", "delete", "options", "patch", "trace"}
|
437
|
+
|
438
|
+
seen_negative = coverage.HashSet()
|
439
|
+
seen_positive = coverage.HashSet()
|
440
|
+
|
441
|
+
for parameter in operation.iter_parameters():
|
442
|
+
location = parameter.location
|
443
|
+
name = parameter.name
|
444
|
+
schema = parameter.as_json_schema(operation, update_quantifiers=False)
|
445
|
+
for value in find_matching_in_responses(responses, parameter.name):
|
446
|
+
schema.setdefault("examples", []).append(value)
|
447
|
+
gen = coverage.cover_schema_iter(
|
448
|
+
coverage.CoverageContext(location=location, generation_modes=generation_modes), schema
|
449
|
+
)
|
450
|
+
value = next(gen, NOT_SET)
|
451
|
+
if isinstance(value, NotSet):
|
452
|
+
continue
|
453
|
+
template.add_parameter(location, name, value)
|
454
|
+
generators[(location, name)] = gen
|
455
|
+
template_time = instant.elapsed
|
456
|
+
if operation.body:
|
457
|
+
for body in operation.body:
|
458
|
+
instant = Instant()
|
459
|
+
schema = body.as_json_schema(operation, update_quantifiers=False)
|
460
|
+
# Definition could be a list for Open API 2.0
|
461
|
+
definition = body.definition if isinstance(body.definition, dict) else {}
|
462
|
+
examples = [example["value"] for example in definition.get("examples", {}).values() if "value" in example]
|
463
|
+
if examples:
|
464
|
+
schema.setdefault("examples", []).extend(examples)
|
465
|
+
gen = coverage.cover_schema_iter(
|
466
|
+
coverage.CoverageContext(location="body", generation_modes=generation_modes), schema
|
467
|
+
)
|
468
|
+
value = next(gen, NOT_SET)
|
469
|
+
if isinstance(value, NotSet):
|
470
|
+
continue
|
471
|
+
elapsed = instant.elapsed
|
472
|
+
if "body" not in template:
|
473
|
+
template_time += elapsed
|
474
|
+
template.set_body(value, body.media_type)
|
475
|
+
data = template.with_body(value=value, media_type=body.media_type)
|
476
|
+
yield operation.Case(
|
477
|
+
**data.kwargs,
|
478
|
+
_meta=CaseMetadata(
|
479
|
+
generation=GenerationInfo(
|
480
|
+
time=elapsed,
|
481
|
+
mode=value.generation_mode,
|
482
|
+
),
|
483
|
+
components=data.components,
|
484
|
+
phase=PhaseInfo.coverage(
|
485
|
+
description=value.description,
|
486
|
+
location=value.location,
|
487
|
+
parameter=body.media_type,
|
488
|
+
parameter_location="body",
|
489
|
+
),
|
490
|
+
),
|
491
|
+
)
|
492
|
+
iterator = iter(gen)
|
493
|
+
while True:
|
494
|
+
instant = Instant()
|
495
|
+
try:
|
496
|
+
next_value = next(iterator)
|
497
|
+
data = template.with_body(value=next_value, media_type=body.media_type)
|
498
|
+
yield operation.Case(
|
499
|
+
**data.kwargs,
|
500
|
+
_meta=CaseMetadata(
|
501
|
+
generation=GenerationInfo(
|
502
|
+
time=instant.elapsed,
|
503
|
+
mode=next_value.generation_mode,
|
504
|
+
),
|
505
|
+
components=data.components,
|
506
|
+
phase=PhaseInfo.coverage(
|
507
|
+
description=next_value.description,
|
508
|
+
location=next_value.location,
|
509
|
+
parameter=body.media_type,
|
510
|
+
parameter_location="body",
|
511
|
+
),
|
512
|
+
),
|
513
|
+
)
|
514
|
+
except StopIteration:
|
515
|
+
break
|
516
|
+
elif GenerationMode.POSITIVE in generation_modes:
|
517
|
+
data = template.unmodified()
|
518
|
+
seen_positive.insert(data.kwargs)
|
519
|
+
yield operation.Case(
|
520
|
+
**data.kwargs,
|
521
|
+
_meta=CaseMetadata(
|
522
|
+
generation=GenerationInfo(
|
523
|
+
time=template_time,
|
524
|
+
mode=GenerationMode.POSITIVE,
|
525
|
+
),
|
526
|
+
components=data.components,
|
527
|
+
phase=PhaseInfo.coverage(description="Default positive test case"),
|
528
|
+
),
|
529
|
+
)
|
530
|
+
|
531
|
+
for (location, name), gen in generators.items():
|
532
|
+
iterator = iter(gen)
|
533
|
+
while True:
|
534
|
+
instant = Instant()
|
535
|
+
try:
|
536
|
+
value = next(iterator)
|
537
|
+
data = template.with_parameter(location=location, name=name, value=value)
|
538
|
+
except StopIteration:
|
539
|
+
break
|
540
|
+
|
541
|
+
if value.generation_mode == GenerationMode.NEGATIVE:
|
542
|
+
seen_negative.insert(data.kwargs)
|
543
|
+
elif value.generation_mode == GenerationMode.POSITIVE and not seen_positive.insert(data.kwargs):
|
544
|
+
# Was already generated before
|
545
|
+
continue
|
546
|
+
|
547
|
+
yield operation.Case(
|
548
|
+
**data.kwargs,
|
549
|
+
_meta=CaseMetadata(
|
550
|
+
generation=GenerationInfo(time=instant.elapsed, mode=value.generation_mode),
|
551
|
+
components=data.components,
|
552
|
+
phase=PhaseInfo.coverage(
|
553
|
+
description=value.description,
|
554
|
+
location=value.location,
|
555
|
+
parameter=name,
|
556
|
+
parameter_location=location,
|
557
|
+
),
|
558
|
+
),
|
559
|
+
)
|
560
|
+
if GenerationMode.NEGATIVE in generation_modes:
|
561
|
+
# Generate HTTP methods that are not specified in the spec
|
562
|
+
methods = unexpected_methods - set(operation.schema[operation.path])
|
563
|
+
for method in sorted(methods):
|
564
|
+
instant = Instant()
|
565
|
+
data = template.unmodified()
|
566
|
+
yield operation.Case(
|
567
|
+
**data.kwargs,
|
568
|
+
method=method.upper(),
|
569
|
+
_meta=CaseMetadata(
|
570
|
+
generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
|
571
|
+
components=data.components,
|
572
|
+
phase=PhaseInfo.coverage(description=f"Unspecified HTTP method: {method.upper()}"),
|
573
|
+
),
|
574
|
+
)
|
575
|
+
# Generate duplicate query parameters
|
576
|
+
if generate_duplicate_query_parameters and operation.query:
|
577
|
+
container = template["query"]
|
578
|
+
for parameter in operation.query:
|
579
|
+
instant = Instant()
|
580
|
+
# Could be absent if value schema can't be negated
|
581
|
+
# I.e. contains just `default` value without any other keywords
|
582
|
+
value = container.get(parameter.name, NOT_SET)
|
583
|
+
if value is not NOT_SET:
|
584
|
+
data = template.with_container(
|
585
|
+
container_name="query",
|
586
|
+
value={**container, parameter.name: [value, value]},
|
587
|
+
generation_mode=GenerationMode.NEGATIVE,
|
588
|
+
)
|
589
|
+
yield operation.Case(
|
590
|
+
**data.kwargs,
|
591
|
+
_meta=CaseMetadata(
|
592
|
+
generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
|
593
|
+
components=data.components,
|
594
|
+
phase=PhaseInfo.coverage(
|
595
|
+
description=f"Duplicate `{parameter.name}` query parameter",
|
596
|
+
parameter=parameter.name,
|
597
|
+
parameter_location="query",
|
598
|
+
),
|
599
|
+
),
|
600
|
+
)
|
601
|
+
# Generate missing required parameters
|
602
|
+
for parameter in operation.iter_parameters():
|
603
|
+
if parameter.is_required and parameter.location != "path":
|
604
|
+
instant = Instant()
|
605
|
+
name = parameter.name
|
606
|
+
location = parameter.location
|
607
|
+
container_name = LOCATION_TO_CONTAINER[location]
|
608
|
+
container = template[container_name]
|
609
|
+
data = template.with_container(
|
610
|
+
container_name=container_name,
|
611
|
+
value={k: v for k, v in container.items() if k != name},
|
612
|
+
generation_mode=GenerationMode.NEGATIVE,
|
613
|
+
)
|
614
|
+
yield operation.Case(
|
615
|
+
**data.kwargs,
|
616
|
+
_meta=CaseMetadata(
|
617
|
+
generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
|
618
|
+
components=data.components,
|
619
|
+
phase=PhaseInfo.coverage(
|
620
|
+
description=f"Missing `{name}` at {location}",
|
621
|
+
parameter=name,
|
622
|
+
parameter_location=location,
|
623
|
+
),
|
624
|
+
),
|
625
|
+
)
|
626
|
+
# Generate combinations for each location
|
627
|
+
for location, parameter_set in [
|
628
|
+
("query", operation.query),
|
629
|
+
("header", operation.headers),
|
630
|
+
("cookie", operation.cookies),
|
631
|
+
]:
|
632
|
+
if not parameter_set:
|
633
|
+
continue
|
634
|
+
|
635
|
+
container_name = LOCATION_TO_CONTAINER[location]
|
636
|
+
base_container = template.get(container_name, {})
|
637
|
+
|
638
|
+
# Get required and optional parameters
|
639
|
+
required = {p.name for p in parameter_set if p.is_required}
|
640
|
+
all_params = {p.name for p in parameter_set}
|
641
|
+
optional = sorted(all_params - required)
|
642
|
+
|
643
|
+
# Helper function to create and yield a case
|
644
|
+
def make_case(
|
645
|
+
container_values: dict,
|
646
|
+
description: str,
|
647
|
+
_location: str,
|
648
|
+
_container_name: str,
|
649
|
+
_parameter: str | None,
|
650
|
+
_generation_mode: GenerationMode,
|
651
|
+
_instant: Instant,
|
652
|
+
) -> Case:
|
653
|
+
data = template.with_container(
|
654
|
+
container_name=_container_name, value=container_values, generation_mode=_generation_mode
|
655
|
+
)
|
656
|
+
return operation.Case(
|
657
|
+
**data.kwargs,
|
658
|
+
_meta=CaseMetadata(
|
659
|
+
generation=GenerationInfo(
|
660
|
+
time=_instant.elapsed,
|
661
|
+
mode=_generation_mode,
|
662
|
+
),
|
663
|
+
components=data.components,
|
664
|
+
phase=PhaseInfo.coverage(
|
665
|
+
description=description,
|
666
|
+
parameter=_parameter,
|
667
|
+
parameter_location=_location,
|
668
|
+
),
|
669
|
+
),
|
670
|
+
)
|
671
|
+
|
672
|
+
def _combination_schema(
|
673
|
+
combination: dict[str, Any], _required: set[str], _parameter_set: ParameterSet
|
674
|
+
) -> dict[str, Any]:
|
675
|
+
return {
|
676
|
+
"properties": {
|
677
|
+
parameter.name: parameter.as_json_schema(operation)
|
678
|
+
for parameter in _parameter_set
|
679
|
+
if parameter.name in combination
|
680
|
+
},
|
681
|
+
"required": list(_required),
|
682
|
+
"additionalProperties": False,
|
683
|
+
}
|
684
|
+
|
685
|
+
def _yield_negative(
|
686
|
+
subschema: dict[str, Any], _location: str, _container_name: str
|
687
|
+
) -> Generator[Case, None, None]:
|
688
|
+
iterator = iter(
|
689
|
+
coverage.cover_schema_iter(
|
690
|
+
coverage.CoverageContext(location=_location, generation_modes=[GenerationMode.NEGATIVE]),
|
691
|
+
subschema,
|
692
|
+
)
|
693
|
+
)
|
694
|
+
while True:
|
695
|
+
instant = Instant()
|
696
|
+
try:
|
697
|
+
more = next(iterator)
|
698
|
+
yield make_case(
|
699
|
+
more.value,
|
700
|
+
more.description,
|
701
|
+
_location,
|
702
|
+
_container_name,
|
703
|
+
more.parameter,
|
704
|
+
GenerationMode.NEGATIVE,
|
705
|
+
instant,
|
706
|
+
)
|
707
|
+
except StopIteration:
|
708
|
+
break
|
709
|
+
|
710
|
+
# 1. Generate only required properties
|
711
|
+
if required and all_params != required:
|
712
|
+
only_required = {k: v for k, v in base_container.items() if k in required}
|
713
|
+
if GenerationMode.POSITIVE in generation_modes:
|
714
|
+
yield make_case(
|
715
|
+
only_required,
|
716
|
+
"Only required properties",
|
717
|
+
location,
|
718
|
+
container_name,
|
719
|
+
None,
|
720
|
+
GenerationMode.POSITIVE,
|
721
|
+
Instant(),
|
722
|
+
)
|
723
|
+
if GenerationMode.NEGATIVE in generation_modes:
|
724
|
+
subschema = _combination_schema(only_required, required, parameter_set)
|
725
|
+
for case in _yield_negative(subschema, location, container_name):
|
726
|
+
kwargs = _case_to_kwargs(case)
|
727
|
+
if not seen_negative.insert(kwargs):
|
728
|
+
continue
|
729
|
+
assert case.meta is not None
|
730
|
+
assert isinstance(case.meta.phase.data, CoveragePhaseData)
|
731
|
+
# Already generated in one of the blocks above
|
732
|
+
if location != "path" and not case.meta.phase.data.description.startswith(
|
733
|
+
"Missing required property"
|
734
|
+
):
|
735
|
+
yield case
|
736
|
+
|
737
|
+
# 2. Generate combinations with required properties and one optional property
|
738
|
+
for opt_param in optional:
|
739
|
+
combo = {k: v for k, v in base_container.items() if k in required or k == opt_param}
|
740
|
+
if combo != base_container and GenerationMode.POSITIVE in generation_modes:
|
741
|
+
yield make_case(
|
742
|
+
combo,
|
743
|
+
f"All required properties and optional '{opt_param}'",
|
744
|
+
location,
|
745
|
+
container_name,
|
746
|
+
None,
|
747
|
+
GenerationMode.POSITIVE,
|
748
|
+
Instant(),
|
749
|
+
)
|
750
|
+
if GenerationMode.NEGATIVE in generation_modes:
|
751
|
+
subschema = _combination_schema(combo, required, parameter_set)
|
752
|
+
for case in _yield_negative(subschema, location, container_name):
|
753
|
+
assert case.meta is not None
|
754
|
+
assert isinstance(case.meta.phase.data, CoveragePhaseData)
|
755
|
+
# Already generated in one of the blocks above
|
756
|
+
if location != "path" and not case.meta.phase.data.description.startswith(
|
757
|
+
"Missing required property"
|
758
|
+
):
|
759
|
+
yield case
|
760
|
+
|
761
|
+
# 3. Generate one combination for each size from 2 to N-1 of optional parameters
|
762
|
+
if len(optional) > 1 and GenerationMode.POSITIVE in generation_modes:
|
763
|
+
for size in range(2, len(optional)):
|
764
|
+
for combination in combinations(optional, size):
|
765
|
+
combo = {k: v for k, v in base_container.items() if k in required or k in combination}
|
766
|
+
if combo != base_container:
|
767
|
+
yield make_case(
|
768
|
+
combo,
|
769
|
+
f"All required and {size} optional properties",
|
770
|
+
location,
|
771
|
+
container_name,
|
772
|
+
None,
|
773
|
+
GenerationMode.POSITIVE,
|
774
|
+
Instant(),
|
775
|
+
)
|
776
|
+
|
777
|
+
|
778
|
+
def _case_to_kwargs(case: Case) -> dict:
|
779
|
+
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
780
|
+
|
781
|
+
kwargs = {}
|
782
|
+
for container_name in LOCATION_TO_CONTAINER.values():
|
783
|
+
value = getattr(case, container_name)
|
784
|
+
if isinstance(value, CaseInsensitiveDict) and value:
|
785
|
+
kwargs[container_name] = dict(value)
|
786
|
+
elif value and value is not NOT_SET:
|
787
|
+
kwargs[container_name] = value
|
788
|
+
return kwargs
|
789
|
+
|
790
|
+
|
791
|
+
def find_invalid_headers(headers: Mapping) -> Generator[tuple[str, str], None, None]:
|
792
|
+
for name, value in headers.items():
|
793
|
+
if not is_latin_1_encodable(value) or has_invalid_characters(name, value):
|
794
|
+
yield name, value
|
795
|
+
|
796
|
+
|
797
|
+
UnsatisfiableExampleMark = Mark[Unsatisfiable](attr_name="unsatisfiable_example")
|
798
|
+
NonSerializableMark = Mark[SerializationNotPossible](attr_name="non_serializable")
|
799
|
+
InvalidRegexMark = Mark[SchemaError](attr_name="invalid_regex")
|
800
|
+
InvalidHeadersExampleMark = Mark[dict[str, str]](attr_name="invalid_example_header")
|