schemathesis 3.25.5__py3-none-any.whl → 3.39.7__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 +6 -6
- schemathesis/_compat.py +2 -2
- schemathesis/_dependency_versions.py +4 -2
- schemathesis/_hypothesis.py +369 -56
- schemathesis/_lazy_import.py +1 -0
- schemathesis/_override.py +5 -4
- schemathesis/_patches.py +21 -0
- schemathesis/_rate_limiter.py +7 -0
- schemathesis/_xml.py +75 -22
- schemathesis/auths.py +78 -16
- schemathesis/checks.py +21 -9
- schemathesis/cli/__init__.py +793 -448
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/callbacks.py +58 -13
- schemathesis/cli/cassettes.py +233 -47
- schemathesis/cli/constants.py +8 -2
- schemathesis/cli/context.py +24 -4
- schemathesis/cli/debug.py +2 -1
- schemathesis/cli/handlers.py +4 -1
- schemathesis/cli/junitxml.py +103 -22
- schemathesis/cli/options.py +15 -4
- schemathesis/cli/output/default.py +286 -115
- schemathesis/cli/output/short.py +25 -6
- schemathesis/cli/reporting.py +79 -0
- schemathesis/cli/sanitization.py +6 -0
- schemathesis/code_samples.py +5 -3
- schemathesis/constants.py +1 -0
- schemathesis/contrib/openapi/__init__.py +1 -1
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
- schemathesis/contrib/openapi/formats/uuid.py +2 -1
- schemathesis/contrib/unique_data.py +3 -3
- schemathesis/exceptions.py +76 -65
- schemathesis/experimental/__init__.py +35 -0
- schemathesis/extra/_aiohttp.py +1 -0
- schemathesis/extra/_flask.py +4 -1
- schemathesis/extra/_server.py +1 -0
- schemathesis/extra/pytest_plugin.py +17 -25
- schemathesis/failures.py +77 -9
- schemathesis/filters.py +185 -8
- schemathesis/fixups/__init__.py +1 -0
- schemathesis/fixups/fast_api.py +2 -2
- schemathesis/fixups/utf8_bom.py +1 -2
- schemathesis/generation/__init__.py +20 -36
- schemathesis/generation/_hypothesis.py +59 -0
- schemathesis/generation/_methods.py +44 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/graphql.py +0 -1
- schemathesis/hooks.py +89 -12
- schemathesis/internal/checks.py +84 -0
- schemathesis/internal/copy.py +22 -3
- schemathesis/internal/deprecation.py +6 -2
- schemathesis/internal/diff.py +15 -0
- schemathesis/internal/extensions.py +27 -0
- schemathesis/internal/jsonschema.py +2 -1
- schemathesis/internal/output.py +68 -0
- schemathesis/internal/result.py +1 -1
- schemathesis/internal/transformation.py +11 -0
- schemathesis/lazy.py +138 -25
- schemathesis/loaders.py +7 -5
- schemathesis/models.py +323 -213
- schemathesis/parameters.py +4 -0
- schemathesis/runner/__init__.py +72 -22
- schemathesis/runner/events.py +86 -6
- schemathesis/runner/impl/context.py +104 -0
- schemathesis/runner/impl/core.py +447 -187
- schemathesis/runner/impl/solo.py +19 -29
- schemathesis/runner/impl/threadpool.py +70 -79
- schemathesis/{cli → runner}/probes.py +37 -25
- schemathesis/runner/serialization.py +150 -17
- schemathesis/sanitization.py +5 -1
- schemathesis/schemas.py +170 -102
- schemathesis/serializers.py +17 -4
- schemathesis/service/ci.py +1 -0
- schemathesis/service/client.py +39 -6
- schemathesis/service/events.py +5 -1
- schemathesis/service/extensions.py +224 -0
- schemathesis/service/hosts.py +6 -2
- schemathesis/service/metadata.py +25 -0
- schemathesis/service/models.py +211 -2
- schemathesis/service/report.py +6 -6
- schemathesis/service/serialization.py +60 -71
- schemathesis/service/usage.py +1 -0
- schemathesis/specs/graphql/_cache.py +26 -0
- schemathesis/specs/graphql/loaders.py +25 -5
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +2 -2
- schemathesis/specs/graphql/schemas.py +130 -100
- schemathesis/specs/graphql/validation.py +1 -2
- schemathesis/specs/openapi/__init__.py +1 -0
- schemathesis/specs/openapi/_cache.py +123 -0
- schemathesis/specs/openapi/_hypothesis.py +79 -61
- schemathesis/specs/openapi/checks.py +504 -25
- schemathesis/specs/openapi/converter.py +31 -4
- schemathesis/specs/openapi/definitions.py +10 -17
- schemathesis/specs/openapi/examples.py +143 -31
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +1 -1
- schemathesis/specs/openapi/expressions/extractors.py +26 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +29 -6
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +44 -0
- schemathesis/specs/openapi/links.py +125 -42
- schemathesis/specs/openapi/loaders.py +77 -36
- schemathesis/specs/openapi/media_types.py +34 -0
- schemathesis/specs/openapi/negative/__init__.py +6 -3
- schemathesis/specs/openapi/negative/mutations.py +21 -6
- schemathesis/specs/openapi/parameters.py +39 -25
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +37 -7
- schemathesis/specs/openapi/schemas.py +368 -242
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +1 -0
- schemathesis/specs/openapi/stateful/__init__.py +198 -70
- schemathesis/specs/openapi/stateful/statistic.py +198 -0
- schemathesis/specs/openapi/stateful/types.py +14 -0
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/specs/openapi/validation.py +1 -0
- schemathesis/stateful/__init__.py +35 -21
- schemathesis/stateful/config.py +97 -0
- schemathesis/stateful/context.py +135 -0
- schemathesis/stateful/events.py +274 -0
- schemathesis/stateful/runner.py +309 -0
- schemathesis/stateful/sink.py +68 -0
- schemathesis/stateful/state_machine.py +67 -38
- schemathesis/stateful/statistic.py +22 -0
- schemathesis/stateful/validation.py +100 -0
- schemathesis/targets.py +33 -1
- schemathesis/throttling.py +25 -5
- schemathesis/transports/__init__.py +354 -0
- schemathesis/transports/asgi.py +7 -0
- schemathesis/transports/auth.py +25 -2
- schemathesis/transports/content_types.py +3 -1
- schemathesis/transports/headers.py +2 -1
- schemathesis/transports/responses.py +9 -4
- schemathesis/types.py +9 -0
- schemathesis/utils.py +11 -16
- schemathesis-3.39.7.dist-info/METADATA +293 -0
- schemathesis-3.39.7.dist-info/RECORD +160 -0
- {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis-3.25.5.dist-info/METADATA +0 -356
- schemathesis-3.25.5.dist-info/RECORD +0 -134
- {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
schemathesis/__init__.py
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
+
|
2
3
|
from typing import Any
|
3
4
|
|
4
|
-
from . import auths, checks,
|
5
|
+
from . import auths, checks, contrib, experimental, fixups, graphql, hooks, runner, serializers, targets
|
5
6
|
from ._lazy_import import lazy_import
|
6
|
-
from .
|
7
|
-
from .
|
8
|
-
from .models import Case
|
9
|
-
from .specs import openapi
|
10
|
-
|
7
|
+
from .constants import SCHEMATHESIS_VERSION
|
8
|
+
from .generation import DataGenerationMethod, GenerationConfig, HeaderConfig
|
9
|
+
from .models import Case
|
10
|
+
from .specs import openapi
|
11
11
|
|
12
12
|
__version__ = SCHEMATHESIS_VERSION
|
13
13
|
|
schemathesis/_compat.py
CHANGED
@@ -1,17 +1,19 @@
|
|
1
1
|
"""Compatibility flags based on installed dependency versions."""
|
2
|
-
from packaging import version
|
3
2
|
|
4
3
|
from importlib import metadata
|
5
4
|
|
5
|
+
from packaging import version
|
6
6
|
|
7
7
|
WERKZEUG_VERSION = version.parse(metadata.version("werkzeug"))
|
8
8
|
IS_WERKZEUG_ABOVE_3 = WERKZEUG_VERSION >= version.parse("3.0")
|
9
9
|
IS_WERKZEUG_BELOW_2_1 = WERKZEUG_VERSION < version.parse("2.1.0")
|
10
10
|
|
11
11
|
PYTEST_VERSION = version.parse(metadata.version("pytest"))
|
12
|
-
IS_PYTEST_ABOVE_54 = PYTEST_VERSION >= version.parse("5.4.0")
|
13
12
|
IS_PYTEST_ABOVE_7 = PYTEST_VERSION >= version.parse("7.0.0")
|
14
13
|
IS_PYTEST_ABOVE_8 = PYTEST_VERSION >= version.parse("8.0.0")
|
15
14
|
|
16
15
|
HYPOTHESIS_VERSION = version.parse(metadata.version("hypothesis"))
|
17
16
|
HYPOTHESIS_HAS_STATEFUL_NAMING_IMPROVEMENTS = HYPOTHESIS_VERSION >= version.parse("6.98.14")
|
17
|
+
|
18
|
+
PYRATE_LIMITER_VERSION = version.parse(metadata.version("pyrate-limiter"))
|
19
|
+
IS_PYRATE_LIMITER_ABOVE_3 = PYRATE_LIMITER_VERSION >= version.parse("3.0")
|
schemathesis/_hypothesis.py
CHANGED
@@ -1,26 +1,42 @@
|
|
1
1
|
"""High-level API for creating Hypothesis tests."""
|
2
|
+
|
2
3
|
from __future__ import annotations
|
3
4
|
|
4
5
|
import asyncio
|
6
|
+
import json
|
5
7
|
import warnings
|
6
|
-
from
|
8
|
+
from functools import wraps
|
9
|
+
from itertools import combinations
|
10
|
+
from typing import TYPE_CHECKING, Any, Callable, Generator, Mapping
|
7
11
|
|
8
12
|
import hypothesis
|
9
13
|
from hypothesis import Phase
|
10
|
-
from hypothesis import strategies as st
|
11
14
|
from hypothesis.errors import HypothesisWarning, Unsatisfiable
|
12
|
-
from hypothesis.internal.
|
15
|
+
from hypothesis.internal.entropy import deterministic_PRNG
|
13
16
|
from jsonschema.exceptions import SchemaError
|
14
17
|
|
18
|
+
from . import _patches
|
15
19
|
from .auths import get_auth_storage_from_test
|
16
|
-
from .constants import DEFAULT_DEADLINE
|
20
|
+
from .constants import DEFAULT_DEADLINE, NOT_SET
|
17
21
|
from .exceptions import OperationSchemaError, SerializationNotPossible
|
18
|
-
from .
|
22
|
+
from .experimental import COVERAGE_PHASE
|
23
|
+
from .generation import DataGenerationMethod, GenerationConfig, combine_strategies, coverage, get_single_example
|
19
24
|
from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher
|
20
|
-
from .models import APIOperation, Case
|
25
|
+
from .models import APIOperation, Case, GenerationMetadata, TestPhase
|
26
|
+
from .parameters import ParameterSet
|
21
27
|
from .transports.content_types import parse_content_type
|
22
28
|
from .transports.headers import has_invalid_characters, is_latin_1_encodable
|
23
|
-
from .
|
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()
|
24
40
|
|
25
41
|
|
26
42
|
def create_test(
|
@@ -41,17 +57,17 @@ def create_test(
|
|
41
57
|
auth_storage = get_auth_storage_from_test(test)
|
42
58
|
strategies = []
|
43
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
|
+
)
|
44
69
|
for data_generation_method in data_generation_methods:
|
45
|
-
strategies.append(
|
46
|
-
operation.as_strategy(
|
47
|
-
hooks=hook_dispatcher,
|
48
|
-
auth_storage=auth_storage,
|
49
|
-
data_generation_method=data_generation_method,
|
50
|
-
generation_config=generation_config,
|
51
|
-
skip_on_not_negated=skip_on_not_negated,
|
52
|
-
**(as_strategy_kwargs or {}),
|
53
|
-
)
|
54
|
-
)
|
70
|
+
strategies.append(operation.as_strategy(data_generation_method=data_generation_method, **as_strategy_kwargs))
|
55
71
|
strategy = combine_strategies(strategies)
|
56
72
|
_given_kwargs = (_given_kwargs or {}).copy()
|
57
73
|
_given_kwargs.setdefault("case", strategy)
|
@@ -60,7 +76,7 @@ def create_test(
|
|
60
76
|
# tests in multiple threads because Hypothesis stores some internal attributes on function objects and re-writing
|
61
77
|
# them from different threads may lead to unpredictable side-effects.
|
62
78
|
|
63
|
-
@
|
79
|
+
@wraps(test)
|
64
80
|
def test_function(*args: Any, **kwargs: Any) -> Any:
|
65
81
|
__tracebackhide__ = True
|
66
82
|
return test(*args, **kwargs)
|
@@ -76,13 +92,26 @@ def create_test(
|
|
76
92
|
wrapped_test.hypothesis.inner_test = make_async_test(test) # type: ignore
|
77
93
|
setup_default_deadline(wrapped_test)
|
78
94
|
if settings is not None:
|
79
|
-
|
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)
|
80
105
|
existing_settings = _get_hypothesis_settings(wrapped_test)
|
81
106
|
if existing_settings is not None:
|
82
107
|
existing_settings = remove_explain_phase(existing_settings)
|
83
108
|
wrapped_test._hypothesis_internal_use_settings = existing_settings # type: ignore
|
84
109
|
if Phase.explicit in existing_settings.phases:
|
85
|
-
wrapped_test = add_examples(
|
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)
|
86
115
|
return wrapped_test
|
87
116
|
|
88
117
|
|
@@ -122,12 +151,20 @@ def make_async_test(test: Callable) -> Callable:
|
|
122
151
|
return async_run
|
123
152
|
|
124
153
|
|
125
|
-
def add_examples(
|
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:
|
126
160
|
"""Add examples to the Hypothesis test, if they are specified in the schema."""
|
127
161
|
from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
|
128
162
|
|
129
163
|
try:
|
130
|
-
examples: list[Case] = [
|
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
|
+
]
|
131
168
|
except (
|
132
169
|
OperationSchemaError,
|
133
170
|
HypothesisRefResolutionError,
|
@@ -162,18 +199,316 @@ def add_examples(test: Callable, operation: APIOperation, hook_dispatcher: HookD
|
|
162
199
|
if invalid_headers:
|
163
200
|
add_invalid_example_header_mark(original_test, invalid_headers)
|
164
201
|
continue
|
165
|
-
|
166
|
-
try:
|
167
|
-
media_type = parse_content_type(example.media_type)
|
168
|
-
if media_type == ("application", "x-www-form-urlencoded"):
|
169
|
-
example.body = prepare_urlencoded(example.body)
|
170
|
-
except ValueError:
|
171
|
-
pass
|
202
|
+
adjust_urlencoded_payload(example)
|
172
203
|
test = hypothesis.example(case=example)(test)
|
173
204
|
return test
|
174
205
|
|
175
206
|
|
176
|
-
def
|
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]:
|
177
512
|
for name, value in headers.items():
|
178
513
|
if not is_latin_1_encodable(value) or has_invalid_characters(name, value):
|
179
514
|
yield name, value
|
@@ -187,7 +522,7 @@ def prepare_urlencoded(data: Any) -> Any:
|
|
187
522
|
for key, value in item.items():
|
188
523
|
output.append((key, value))
|
189
524
|
else:
|
190
|
-
output.append(item)
|
525
|
+
output.append((item, "arbitrary-value"))
|
191
526
|
return output
|
192
527
|
return data
|
193
528
|
|
@@ -204,11 +539,11 @@ def add_non_serializable_mark(test: Callable, exc: SerializationNotPossible) ->
|
|
204
539
|
test._schemathesis_non_serializable = exc # type: ignore
|
205
540
|
|
206
541
|
|
207
|
-
def get_non_serializable_mark(test: Callable) ->
|
542
|
+
def get_non_serializable_mark(test: Callable) -> SerializationNotPossible | None:
|
208
543
|
return getattr(test, "_schemathesis_non_serializable", None)
|
209
544
|
|
210
545
|
|
211
|
-
def get_invalid_regex_mark(test: Callable) ->
|
546
|
+
def get_invalid_regex_mark(test: Callable) -> SchemaError | None:
|
212
547
|
return getattr(test, "_schemathesis_invalid_regex", None)
|
213
548
|
|
214
549
|
|
@@ -216,31 +551,9 @@ def add_invalid_regex_mark(test: Callable, exc: SchemaError) -> None:
|
|
216
551
|
test._schemathesis_invalid_regex = exc # type: ignore
|
217
552
|
|
218
553
|
|
219
|
-
def get_invalid_example_headers_mark(test: Callable) ->
|
554
|
+
def get_invalid_example_headers_mark(test: Callable) -> dict[str, str] | None:
|
220
555
|
return getattr(test, "_schemathesis_invalid_example_headers", None)
|
221
556
|
|
222
557
|
|
223
558
|
def add_invalid_example_header_mark(test: Callable, headers: dict[str, str]) -> None:
|
224
559
|
test._schemathesis_invalid_example_headers = headers # type: ignore
|
225
|
-
|
226
|
-
|
227
|
-
def get_single_example(strategy: st.SearchStrategy[Case]) -> Case:
|
228
|
-
examples: list[Case] = []
|
229
|
-
add_single_example(strategy, examples)
|
230
|
-
return examples[0]
|
231
|
-
|
232
|
-
|
233
|
-
def add_single_example(strategy: st.SearchStrategy[Case], examples: list[Case]) -> None:
|
234
|
-
@hypothesis.given(strategy) # type: ignore
|
235
|
-
@hypothesis.settings( # type: ignore
|
236
|
-
database=None,
|
237
|
-
max_examples=1,
|
238
|
-
deadline=None,
|
239
|
-
verbosity=hypothesis.Verbosity.quiet,
|
240
|
-
phases=(hypothesis.Phase.generate,),
|
241
|
-
suppress_health_check=list(hypothesis.HealthCheck),
|
242
|
-
)
|
243
|
-
def example_generating_inner_function(ex: Case) -> None:
|
244
|
-
examples.append(ex)
|
245
|
-
|
246
|
-
example_generating_inner_function()
|
schemathesis/_lazy_import.py
CHANGED
schemathesis/_override.py
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
+
|
2
3
|
from dataclasses import dataclass
|
3
|
-
from typing import TYPE_CHECKING
|
4
|
+
from typing import TYPE_CHECKING
|
4
5
|
|
5
6
|
from .exceptions import UsageError
|
6
|
-
from .parameters import ParameterSet
|
7
|
-
from .types import GenericTest
|
8
7
|
|
9
8
|
if TYPE_CHECKING:
|
10
9
|
from .models import APIOperation
|
10
|
+
from .parameters import ParameterSet
|
11
|
+
from .types import GenericTest
|
11
12
|
|
12
13
|
|
13
14
|
@dataclass
|
@@ -36,7 +37,7 @@ def _for_parameters(overridden: dict[str, str], defined: ParameterSet) -> dict[s
|
|
36
37
|
return output
|
37
38
|
|
38
39
|
|
39
|
-
def get_override_from_mark(test: GenericTest) ->
|
40
|
+
def get_override_from_mark(test: GenericTest) -> CaseOverride | None:
|
40
41
|
return getattr(test, "_schemathesis_override", None)
|
41
42
|
|
42
43
|
|
schemathesis/_patches.py
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
"""A set of performance-related patches."""
|
2
|
+
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
|
6
|
+
def install() -> None:
|
7
|
+
from hypothesis.internal.reflection import is_first_param_referenced_in_function
|
8
|
+
from hypothesis.strategies._internal import core
|
9
|
+
from hypothesis_jsonschema import _from_schema, _resolve
|
10
|
+
|
11
|
+
from .internal.copy import fast_deepcopy
|
12
|
+
|
13
|
+
# This one is used a lot, and under the hood it re-parses the AST of the same function
|
14
|
+
def _is_first_param_referenced_in_function(f: Any) -> bool:
|
15
|
+
if f.__name__ == "from_object_schema" and f.__module__ == "hypothesis_jsonschema._from_schema":
|
16
|
+
return True
|
17
|
+
return is_first_param_referenced_in_function(f)
|
18
|
+
|
19
|
+
core.is_first_param_referenced_in_function = _is_first_param_referenced_in_function # type: ignore
|
20
|
+
_resolve.deepcopy = fast_deepcopy # type: ignore
|
21
|
+
_from_schema.deepcopy = fast_deepcopy # type: ignore
|