schemathesis 3.15.4__py3-none-any.whl → 4.4.2__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 +53 -25
- schemathesis/auths.py +507 -0
- schemathesis/checks.py +190 -25
- schemathesis/cli/__init__.py +27 -1219
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +133 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +602 -0
- schemathesis/cli/commands/run/context.py +228 -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 +45 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
- schemathesis/cli/commands/run/handlers/output.py +1750 -0
- schemathesis/cli/commands/run/loaders.py +118 -0
- schemathesis/cli/commands/run/validation.py +256 -0
- schemathesis/cli/constants.py +5 -0
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +203 -0
- schemathesis/cli/ext/options.py +81 -0
- schemathesis/config/__init__.py +202 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +101 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +163 -0
- schemathesis/config/_generation.py +157 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +335 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +253 -0
- schemathesis/config/_projects.py +543 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +89 -0
- schemathesis/config/schema.json +975 -0
- schemathesis/core/__init__.py +72 -0
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +100 -0
- schemathesis/core/deserialization.py +210 -0
- schemathesis/core/errors.py +588 -0
- schemathesis/core/failures.py +316 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +183 -0
- schemathesis/core/jsonschema/keywords.py +40 -0
- schemathesis/core/jsonschema/references.py +222 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/lazy_import.py +15 -0
- schemathesis/core/loaders.py +107 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/core/media_types.py +79 -0
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/core/parameters.py +45 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +34 -0
- schemathesis/core/result.py +27 -0
- schemathesis/core/schema_analysis.py +17 -0
- schemathesis/core/shell.py +203 -0
- schemathesis/core/transforms.py +144 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +73 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +152 -0
- schemathesis/engine/control.py +44 -0
- schemathesis/engine/core.py +201 -0
- schemathesis/engine/errors.py +446 -0
- schemathesis/engine/events.py +284 -0
- schemathesis/engine/observations.py +42 -0
- schemathesis/engine/phases/__init__.py +108 -0
- schemathesis/engine/phases/analysis.py +28 -0
- schemathesis/engine/phases/probes.py +172 -0
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +364 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +220 -0
- schemathesis/engine/phases/unit/_executor.py +459 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +254 -0
- schemathesis/errors.py +47 -0
- schemathesis/filters.py +395 -0
- schemathesis/generation/__init__.py +25 -0
- schemathesis/generation/case.py +478 -0
- schemathesis/generation/coverage.py +1528 -0
- schemathesis/generation/hypothesis/__init__.py +121 -0
- schemathesis/generation/hypothesis/builder.py +992 -0
- schemathesis/generation/hypothesis/examples.py +56 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +285 -0
- schemathesis/generation/meta.py +227 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +127 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +294 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +285 -0
- schemathesis/hooks.py +270 -91
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +467 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +315 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +341 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/pytest/plugin.py +357 -0
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +682 -257
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/nodes.py +26 -2
- schemathesis/specs/graphql/scalars.py +77 -12
- schemathesis/specs/graphql/schemas.py +367 -148
- schemathesis/specs/graphql/validation.py +33 -0
- schemathesis/specs/openapi/__init__.py +9 -1
- schemathesis/specs/openapi/_hypothesis.py +555 -318
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +729 -0
- schemathesis/specs/openapi/adapter/protocol.py +59 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +368 -0
- schemathesis/specs/openapi/adapter/security.py +144 -0
- schemathesis/specs/openapi/adapter/v2.py +30 -0
- schemathesis/specs/openapi/adapter/v3_0.py +30 -0
- schemathesis/specs/openapi/adapter/v3_1.py +30 -0
- schemathesis/specs/openapi/analysis.py +96 -0
- schemathesis/specs/openapi/checks.py +748 -82
- schemathesis/specs/openapi/converter.py +176 -37
- schemathesis/specs/openapi/definitions.py +599 -4
- schemathesis/specs/openapi/examples.py +581 -165
- schemathesis/specs/openapi/expressions/__init__.py +52 -5
- schemathesis/specs/openapi/expressions/extractors.py +25 -0
- schemathesis/specs/openapi/expressions/lexer.py +34 -31
- schemathesis/specs/openapi/expressions/nodes.py +97 -46
- schemathesis/specs/openapi/expressions/parser.py +35 -13
- schemathesis/specs/openapi/formats.py +122 -0
- schemathesis/specs/openapi/media_types.py +75 -0
- schemathesis/specs/openapi/negative/__init__.py +93 -73
- schemathesis/specs/openapi/negative/mutations.py +294 -103
- schemathesis/specs/openapi/negative/utils.py +0 -9
- schemathesis/specs/openapi/patterns.py +458 -0
- schemathesis/specs/openapi/references.py +60 -81
- schemathesis/specs/openapi/schemas.py +647 -666
- schemathesis/specs/openapi/serialization.py +53 -30
- schemathesis/specs/openapi/stateful/__init__.py +403 -68
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
- schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
- schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
- schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
- schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
- schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
- schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
- schemathesis/specs/openapi/stateful/inference.py +254 -0
- schemathesis/specs/openapi/stateful/links.py +219 -78
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/common.py +23 -0
- schemathesis/specs/openapi/types/v2.py +129 -0
- schemathesis/specs/openapi/types/v3.py +134 -0
- schemathesis/specs/openapi/utils.py +7 -6
- schemathesis/specs/openapi/warnings.py +75 -0
- schemathesis/transport/__init__.py +224 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +126 -0
- schemathesis/transport/requests.py +278 -0
- schemathesis/transport/serialization.py +329 -0
- schemathesis/transport/wsgi.py +175 -0
- schemathesis-4.4.2.dist-info/METADATA +213 -0
- schemathesis-4.4.2.dist-info/RECORD +192 -0
- {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
- schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
- {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
- schemathesis/_compat.py +0 -57
- schemathesis/_hypothesis.py +0 -123
- schemathesis/auth.py +0 -214
- schemathesis/cli/callbacks.py +0 -240
- schemathesis/cli/cassettes.py +0 -351
- schemathesis/cli/context.py +0 -38
- schemathesis/cli/debug.py +0 -21
- schemathesis/cli/handlers.py +0 -11
- schemathesis/cli/junitxml.py +0 -41
- schemathesis/cli/options.py +0 -70
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -521
- schemathesis/cli/output/short.py +0 -40
- schemathesis/constants.py +0 -88
- schemathesis/exceptions.py +0 -257
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -16
- schemathesis/extra/pytest_plugin.py +0 -251
- schemathesis/failures.py +0 -145
- schemathesis/fixups/__init__.py +0 -29
- schemathesis/fixups/fast_api.py +0 -30
- schemathesis/graphql.py +0 -5
- schemathesis/internal.py +0 -6
- schemathesis/lazy.py +0 -301
- schemathesis/models.py +0 -1113
- schemathesis/parameters.py +0 -91
- schemathesis/runner/__init__.py +0 -470
- schemathesis/runner/events.py +0 -242
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -791
- schemathesis/runner/impl/solo.py +0 -85
- schemathesis/runner/impl/threadpool.py +0 -367
- schemathesis/runner/serialization.py +0 -206
- schemathesis/serializers.py +0 -253
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -10
- schemathesis/service/client.py +0 -62
- schemathesis/service/constants.py +0 -25
- schemathesis/service/events.py +0 -39
- schemathesis/service/handler.py +0 -46
- schemathesis/service/hosts.py +0 -74
- schemathesis/service/metadata.py +0 -42
- schemathesis/service/models.py +0 -21
- schemathesis/service/serialization.py +0 -184
- schemathesis/service/worker.py +0 -39
- schemathesis/specs/graphql/loaders.py +0 -215
- schemathesis/specs/openapi/constants.py +0 -7
- schemathesis/specs/openapi/expressions/context.py +0 -12
- schemathesis/specs/openapi/expressions/pointers.py +0 -29
- schemathesis/specs/openapi/filters.py +0 -44
- schemathesis/specs/openapi/links.py +0 -303
- schemathesis/specs/openapi/loaders.py +0 -453
- schemathesis/specs/openapi/parameters.py +0 -430
- schemathesis/specs/openapi/security.py +0 -129
- schemathesis/specs/openapi/validation.py +0 -24
- schemathesis/stateful.py +0 -358
- schemathesis/targets.py +0 -32
- schemathesis/types.py +0 -38
- schemathesis/utils.py +0 -475
- schemathesis-3.15.4.dist-info/METADATA +0 -202
- schemathesis-3.15.4.dist-info/RECORD +0 -99
- schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
|
@@ -0,0 +1,1528 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import re
|
|
5
|
+
from contextlib import contextmanager, suppress
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from functools import lru_cache, partial
|
|
8
|
+
from itertools import combinations
|
|
9
|
+
|
|
10
|
+
from schemathesis.core.jsonschema.bundler import BUNDLE_STORAGE_KEY
|
|
11
|
+
from schemathesis.core.jsonschema.keywords import ALL_KEYWORDS
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from json.encoder import _make_iterencode # type: ignore[attr-defined]
|
|
15
|
+
except ImportError:
|
|
16
|
+
_make_iterencode = None
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from json.encoder import c_make_encoder # type: ignore[attr-defined]
|
|
20
|
+
except ImportError:
|
|
21
|
+
c_make_encoder = None
|
|
22
|
+
|
|
23
|
+
from json.encoder import JSONEncoder, encode_basestring_ascii
|
|
24
|
+
from typing import Any, Callable, Generator, Iterator, TypeVar, cast
|
|
25
|
+
from urllib.parse import quote_plus
|
|
26
|
+
|
|
27
|
+
import jsonschema.protocols
|
|
28
|
+
from hypothesis import strategies as st
|
|
29
|
+
from hypothesis.errors import InvalidArgument, Unsatisfiable
|
|
30
|
+
from hypothesis_jsonschema import from_schema
|
|
31
|
+
from hypothesis_jsonschema._canonicalise import canonicalish
|
|
32
|
+
from hypothesis_jsonschema._from_schema import STRING_FORMATS as BUILT_IN_STRING_FORMATS
|
|
33
|
+
|
|
34
|
+
from schemathesis.core import INTERNAL_BUFFER_SIZE, NOT_SET
|
|
35
|
+
from schemathesis.core.compat import RefResolutionError, RefResolver
|
|
36
|
+
from schemathesis.core.parameters import ParameterLocation
|
|
37
|
+
from schemathesis.core.transforms import deepclone
|
|
38
|
+
from schemathesis.core.validation import contains_unicode_surrogate_pair, has_invalid_characters, is_latin_1_encodable
|
|
39
|
+
from schemathesis.generation import GenerationMode
|
|
40
|
+
from schemathesis.generation.hypothesis import examples
|
|
41
|
+
from schemathesis.generation.meta import CoverageScenario
|
|
42
|
+
from schemathesis.openapi.generation.filters import is_invalid_path_parameter
|
|
43
|
+
|
|
44
|
+
from ..specs.openapi.converter import update_pattern_in_schema
|
|
45
|
+
from ..specs.openapi.formats import STRING_FORMATS, get_default_format_strategies
|
|
46
|
+
from ..specs.openapi.patterns import update_quantifier
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _replace_zero_with_nonzero(x: float) -> float:
|
|
50
|
+
return x or 0.0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def json_recursive_strategy(strategy: st.SearchStrategy) -> st.SearchStrategy:
|
|
54
|
+
return st.lists(strategy, max_size=2) | st.dictionaries(st.text(), strategy, max_size=2)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
NEGATIVE_MODE_MAX_LENGTH_WITH_PATTERN = 100
|
|
58
|
+
NEGATIVE_MODE_MAX_ITEMS = 15
|
|
59
|
+
FLOAT_STRATEGY: st.SearchStrategy = st.floats(allow_nan=False, allow_infinity=False).map(_replace_zero_with_nonzero)
|
|
60
|
+
NUMERIC_STRATEGY: st.SearchStrategy = st.integers() | FLOAT_STRATEGY
|
|
61
|
+
JSON_STRATEGY: st.SearchStrategy = st.recursive(
|
|
62
|
+
st.none() | st.booleans() | NUMERIC_STRATEGY | st.text(max_size=16),
|
|
63
|
+
json_recursive_strategy,
|
|
64
|
+
max_leaves=2,
|
|
65
|
+
)
|
|
66
|
+
ARRAY_STRATEGY: st.SearchStrategy = st.lists(JSON_STRATEGY, min_size=2, max_size=3)
|
|
67
|
+
OBJECT_STRATEGY: st.SearchStrategy = st.dictionaries(st.text(max_size=16), JSON_STRATEGY, max_size=2)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
STRATEGIES_FOR_TYPE = {
|
|
71
|
+
"integer": st.integers(),
|
|
72
|
+
"number": NUMERIC_STRATEGY,
|
|
73
|
+
"boolean": st.booleans(),
|
|
74
|
+
"null": st.none(),
|
|
75
|
+
"string": st.text(),
|
|
76
|
+
"array": ARRAY_STRATEGY,
|
|
77
|
+
"object": OBJECT_STRATEGY,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_strategy_for_type(ty: str | list[str]) -> st.SearchStrategy:
|
|
82
|
+
if isinstance(ty, str):
|
|
83
|
+
return STRATEGIES_FOR_TYPE[ty]
|
|
84
|
+
return st.one_of(STRATEGIES_FOR_TYPE[t] for t in ty if t in STRATEGIES_FOR_TYPE)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
FORMAT_STRATEGIES = {**BUILT_IN_STRING_FORMATS, **get_default_format_strategies(), **STRING_FORMATS}
|
|
88
|
+
|
|
89
|
+
UNKNOWN_PROPERTY_KEY = "x-schemathesis-unknown-property"
|
|
90
|
+
UNKNOWN_PROPERTY_VALUE = 42
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class GeneratedValue:
|
|
95
|
+
value: Any
|
|
96
|
+
generation_mode: GenerationMode
|
|
97
|
+
scenario: CoverageScenario
|
|
98
|
+
description: str
|
|
99
|
+
parameter: str | None
|
|
100
|
+
location: str | None
|
|
101
|
+
|
|
102
|
+
__slots__ = ("value", "generation_mode", "scenario", "description", "parameter", "location")
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def with_positive(cls, value: Any, *, scenario: CoverageScenario, description: str) -> GeneratedValue:
|
|
106
|
+
return cls(
|
|
107
|
+
value=value,
|
|
108
|
+
generation_mode=GenerationMode.POSITIVE,
|
|
109
|
+
scenario=scenario,
|
|
110
|
+
description=description,
|
|
111
|
+
location=None,
|
|
112
|
+
parameter=None,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def with_negative(
|
|
117
|
+
cls, value: Any, *, scenario: CoverageScenario, description: str, location: str, parameter: str | None = None
|
|
118
|
+
) -> GeneratedValue:
|
|
119
|
+
return cls(
|
|
120
|
+
value=value,
|
|
121
|
+
generation_mode=GenerationMode.NEGATIVE,
|
|
122
|
+
scenario=scenario,
|
|
123
|
+
description=description,
|
|
124
|
+
location=location,
|
|
125
|
+
parameter=parameter,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
PositiveValue = GeneratedValue.with_positive
|
|
130
|
+
NegativeValue = GeneratedValue.with_negative
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@lru_cache(maxsize=128)
|
|
134
|
+
def cached_draw(strategy: st.SearchStrategy) -> Any:
|
|
135
|
+
return examples.generate_one(strategy)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass
|
|
139
|
+
class CoverageContext:
|
|
140
|
+
root_schema: dict[str, Any]
|
|
141
|
+
generation_modes: list[GenerationMode]
|
|
142
|
+
location: ParameterLocation
|
|
143
|
+
media_type: tuple[str, str] | None
|
|
144
|
+
is_required: bool
|
|
145
|
+
path: list[str | int]
|
|
146
|
+
custom_formats: dict[str, st.SearchStrategy]
|
|
147
|
+
validator_cls: type[jsonschema.protocols.Validator]
|
|
148
|
+
_resolver: RefResolver | None
|
|
149
|
+
allow_extra_parameters: bool
|
|
150
|
+
|
|
151
|
+
__slots__ = (
|
|
152
|
+
"root_schema",
|
|
153
|
+
"location",
|
|
154
|
+
"media_type",
|
|
155
|
+
"generation_modes",
|
|
156
|
+
"is_required",
|
|
157
|
+
"path",
|
|
158
|
+
"custom_formats",
|
|
159
|
+
"validator_cls",
|
|
160
|
+
"_resolver",
|
|
161
|
+
"allow_extra_parameters",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def __init__(
|
|
165
|
+
self,
|
|
166
|
+
*,
|
|
167
|
+
root_schema: dict[str, Any],
|
|
168
|
+
location: ParameterLocation,
|
|
169
|
+
media_type: tuple[str, str] | None,
|
|
170
|
+
generation_modes: list[GenerationMode] | None = None,
|
|
171
|
+
is_required: bool,
|
|
172
|
+
path: list[str | int] | None = None,
|
|
173
|
+
custom_formats: dict[str, st.SearchStrategy],
|
|
174
|
+
validator_cls: type[jsonschema.protocols.Validator],
|
|
175
|
+
_resolver: RefResolver | None = None,
|
|
176
|
+
allow_extra_parameters: bool = True,
|
|
177
|
+
) -> None:
|
|
178
|
+
self.root_schema = root_schema
|
|
179
|
+
self.location = location
|
|
180
|
+
self.media_type = media_type
|
|
181
|
+
self.generation_modes = generation_modes if generation_modes is not None else list(GenerationMode)
|
|
182
|
+
self.is_required = is_required
|
|
183
|
+
self.path = path or []
|
|
184
|
+
self.custom_formats = custom_formats
|
|
185
|
+
self.validator_cls = validator_cls
|
|
186
|
+
self._resolver = _resolver
|
|
187
|
+
self.allow_extra_parameters = allow_extra_parameters
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def resolver(self) -> RefResolver:
|
|
191
|
+
"""Lazy-initialized cached resolver."""
|
|
192
|
+
if self._resolver is None:
|
|
193
|
+
self._resolver = RefResolver.from_schema(self.root_schema)
|
|
194
|
+
return cast(RefResolver, self._resolver)
|
|
195
|
+
|
|
196
|
+
def resolve_ref(self, ref: str) -> dict | bool:
|
|
197
|
+
"""Resolve a $ref to its schema definition."""
|
|
198
|
+
_, resolved = self.resolver.resolve(ref)
|
|
199
|
+
return resolved
|
|
200
|
+
|
|
201
|
+
@contextmanager
|
|
202
|
+
def at(self, key: str | int) -> Generator[None, None, None]:
|
|
203
|
+
self.path.append(key)
|
|
204
|
+
try:
|
|
205
|
+
yield
|
|
206
|
+
finally:
|
|
207
|
+
self.path.pop()
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def current_path(self) -> str:
|
|
211
|
+
return "/" + "/".join(str(key) for key in self.path)
|
|
212
|
+
|
|
213
|
+
def with_positive(self) -> CoverageContext:
|
|
214
|
+
return CoverageContext(
|
|
215
|
+
root_schema=self.root_schema,
|
|
216
|
+
location=self.location,
|
|
217
|
+
media_type=self.media_type,
|
|
218
|
+
generation_modes=[GenerationMode.POSITIVE],
|
|
219
|
+
is_required=self.is_required,
|
|
220
|
+
path=self.path,
|
|
221
|
+
custom_formats=self.custom_formats,
|
|
222
|
+
validator_cls=self.validator_cls,
|
|
223
|
+
_resolver=self._resolver,
|
|
224
|
+
allow_extra_parameters=self.allow_extra_parameters,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def with_negative(self) -> CoverageContext:
|
|
228
|
+
return CoverageContext(
|
|
229
|
+
root_schema=self.root_schema,
|
|
230
|
+
location=self.location,
|
|
231
|
+
media_type=self.media_type,
|
|
232
|
+
generation_modes=[GenerationMode.NEGATIVE],
|
|
233
|
+
is_required=self.is_required,
|
|
234
|
+
path=self.path,
|
|
235
|
+
custom_formats=self.custom_formats,
|
|
236
|
+
validator_cls=self.validator_cls,
|
|
237
|
+
_resolver=self._resolver,
|
|
238
|
+
allow_extra_parameters=self.allow_extra_parameters,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def is_valid_for_location(self, value: Any) -> bool:
|
|
242
|
+
if self.location in ("header", "cookie") and isinstance(value, str):
|
|
243
|
+
return not value or (is_latin_1_encodable(value) and not has_invalid_characters("", value))
|
|
244
|
+
elif self.location == "path":
|
|
245
|
+
return not is_invalid_path_parameter(value)
|
|
246
|
+
return True
|
|
247
|
+
|
|
248
|
+
def leads_to_negative_test_case(self, value: Any) -> bool:
|
|
249
|
+
if self.location == "query":
|
|
250
|
+
# Some values will not be serialized into the query string
|
|
251
|
+
if isinstance(value, list) and not self.is_required:
|
|
252
|
+
# Optional parameters should be present
|
|
253
|
+
return any(item not in [{}, []] for item in value)
|
|
254
|
+
return True
|
|
255
|
+
|
|
256
|
+
def will_be_serialized_to_string(self) -> bool:
|
|
257
|
+
return self.location in ("query", "path", "header", "cookie") or (
|
|
258
|
+
self.location == "body"
|
|
259
|
+
and self.media_type
|
|
260
|
+
in frozenset(
|
|
261
|
+
[
|
|
262
|
+
("multipart", "form-data"),
|
|
263
|
+
("application", "x-www-form-urlencoded"),
|
|
264
|
+
]
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
def can_be_negated(self, schema: dict[str, Any]) -> bool:
|
|
269
|
+
# Path, query, header, and cookie parameters will be stringified anyway
|
|
270
|
+
# If there are no constraints, then anything will match the original schema after serialization
|
|
271
|
+
if self.will_be_serialized_to_string():
|
|
272
|
+
cleaned = {
|
|
273
|
+
k: v
|
|
274
|
+
for k, v in schema.items()
|
|
275
|
+
if not k.startswith("x-") and k not in ["description", "example", "examples"]
|
|
276
|
+
}
|
|
277
|
+
return cleaned not in [{}, {"type": "string"}]
|
|
278
|
+
return True
|
|
279
|
+
|
|
280
|
+
def generate_from(self, strategy: st.SearchStrategy) -> Any:
|
|
281
|
+
return cached_draw(strategy)
|
|
282
|
+
|
|
283
|
+
def generate_from_schema(self, schema: dict | bool) -> Any:
|
|
284
|
+
if isinstance(schema, dict) and "$ref" in schema:
|
|
285
|
+
reference = schema["$ref"]
|
|
286
|
+
# Deep clone to avoid circular references in Python objects
|
|
287
|
+
schema = deepclone(self.resolve_ref(reference))
|
|
288
|
+
if isinstance(schema, bool):
|
|
289
|
+
return 0
|
|
290
|
+
keys = sorted([k for k in schema if not k.startswith("x-") and k not in ["description", "example", "examples"]])
|
|
291
|
+
if keys == ["type"]:
|
|
292
|
+
return cached_draw(get_strategy_for_type(schema["type"]))
|
|
293
|
+
if keys == ["format", "type"]:
|
|
294
|
+
if schema["type"] != "string":
|
|
295
|
+
return cached_draw(get_strategy_for_type(schema["type"]))
|
|
296
|
+
elif schema["format"] in FORMAT_STRATEGIES:
|
|
297
|
+
return cached_draw(FORMAT_STRATEGIES[schema["format"]])
|
|
298
|
+
if (keys == ["maxLength", "minLength", "type"] or keys == ["maxLength", "type"]) and schema["type"] == "string":
|
|
299
|
+
return cached_draw(st.text(min_size=schema.get("minLength", 0), max_size=schema["maxLength"]))
|
|
300
|
+
if (
|
|
301
|
+
keys == ["properties", "required", "type"]
|
|
302
|
+
or keys == ["properties", "required"]
|
|
303
|
+
or keys == ["properties", "type"]
|
|
304
|
+
or keys == ["properties"]
|
|
305
|
+
):
|
|
306
|
+
obj = {}
|
|
307
|
+
for key, sub_schema in schema["properties"].items():
|
|
308
|
+
if isinstance(sub_schema, dict) and "const" in sub_schema:
|
|
309
|
+
obj[key] = sub_schema["const"]
|
|
310
|
+
else:
|
|
311
|
+
obj[key] = self.generate_from_schema(sub_schema)
|
|
312
|
+
return obj
|
|
313
|
+
if (
|
|
314
|
+
keys == ["maximum", "minimum", "type"] or keys == ["maximum", "type"] or keys == ["minimum", "type"]
|
|
315
|
+
) and schema["type"] == "integer":
|
|
316
|
+
return cached_draw(st.integers(min_value=schema.get("minimum"), max_value=schema.get("maximum")))
|
|
317
|
+
if "enum" in schema:
|
|
318
|
+
return cached_draw(st.sampled_from(schema["enum"]))
|
|
319
|
+
if keys == ["multipleOf", "type"] and schema["type"] in ("integer", "number"):
|
|
320
|
+
step = schema["multipleOf"]
|
|
321
|
+
return cached_draw(st.integers().map(step.__mul__))
|
|
322
|
+
if "pattern" in schema:
|
|
323
|
+
pattern = schema["pattern"]
|
|
324
|
+
try:
|
|
325
|
+
re.compile(pattern)
|
|
326
|
+
except re.error:
|
|
327
|
+
raise Unsatisfiable from None
|
|
328
|
+
if "minLength" in schema or "maxLength" in schema:
|
|
329
|
+
min_length = schema.get("minLength")
|
|
330
|
+
max_length = schema.get("maxLength")
|
|
331
|
+
pattern = update_quantifier(pattern, min_length, max_length)
|
|
332
|
+
return cached_draw(st.from_regex(pattern))
|
|
333
|
+
if (keys == ["items", "type"] or keys == ["items", "minItems", "type"]) and isinstance(schema["items"], dict):
|
|
334
|
+
items = schema["items"]
|
|
335
|
+
min_items = schema.get("minItems", 0)
|
|
336
|
+
if "enum" in items:
|
|
337
|
+
return cached_draw(st.lists(st.sampled_from(items["enum"]), min_size=min_items))
|
|
338
|
+
sub_keys = sorted([k for k in items if not k.startswith("x-") and k not in ["description", "example"]])
|
|
339
|
+
if sub_keys == ["type"] and items["type"] == "string":
|
|
340
|
+
return cached_draw(st.lists(st.text(), min_size=min_items))
|
|
341
|
+
if (
|
|
342
|
+
sub_keys == ["properties", "required", "type"]
|
|
343
|
+
or sub_keys == ["properties", "type"]
|
|
344
|
+
or sub_keys == ["properties"]
|
|
345
|
+
):
|
|
346
|
+
return cached_draw(
|
|
347
|
+
st.lists(
|
|
348
|
+
st.fixed_dictionaries(
|
|
349
|
+
{
|
|
350
|
+
key: from_schema(sub_schema, custom_formats=self.custom_formats)
|
|
351
|
+
for key, sub_schema in items["properties"].items()
|
|
352
|
+
}
|
|
353
|
+
),
|
|
354
|
+
min_size=min_items,
|
|
355
|
+
)
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
if keys == ["allOf"]:
|
|
359
|
+
for idx, sub_schema in enumerate(schema["allOf"]):
|
|
360
|
+
if "$ref" in sub_schema:
|
|
361
|
+
schema["allOf"][idx] = self.resolve_ref(sub_schema["$ref"])
|
|
362
|
+
|
|
363
|
+
schema = canonicalish(schema)
|
|
364
|
+
if isinstance(schema, dict) and "allOf" not in schema:
|
|
365
|
+
return self.generate_from_schema(schema)
|
|
366
|
+
|
|
367
|
+
if isinstance(schema, dict) and "examples" in schema:
|
|
368
|
+
# Examples may contain binary data which will fail the canonicalisation process in `hypothesis-jsonschema`
|
|
369
|
+
schema = {key: value for key, value in schema.items() if key != "examples"}
|
|
370
|
+
# Prevent some hard to satisfy schemas
|
|
371
|
+
if isinstance(schema, dict) and schema.get("additionalProperties") is False and "required" in schema:
|
|
372
|
+
# Set required properties to any value to simplify generation
|
|
373
|
+
schema = dict(schema)
|
|
374
|
+
properties = schema.setdefault("properties", {})
|
|
375
|
+
for key in schema["required"]:
|
|
376
|
+
properties.setdefault(key, {})
|
|
377
|
+
|
|
378
|
+
# Add bundled schemas if any
|
|
379
|
+
if isinstance(schema, dict) and BUNDLE_STORAGE_KEY in self.root_schema:
|
|
380
|
+
schema = dict(schema)
|
|
381
|
+
schema[BUNDLE_STORAGE_KEY] = self.root_schema[BUNDLE_STORAGE_KEY]
|
|
382
|
+
|
|
383
|
+
return self.generate_from(from_schema(schema, custom_formats=self.custom_formats))
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
T = TypeVar("T")
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
if c_make_encoder is not None:
|
|
390
|
+
_iterencode = c_make_encoder(None, None, encode_basestring_ascii, None, ":", ",", True, False, False)
|
|
391
|
+
elif _make_iterencode is not None:
|
|
392
|
+
_iterencode = _make_iterencode(
|
|
393
|
+
None, None, encode_basestring_ascii, None, float.__repr__, ":", ",", True, False, True
|
|
394
|
+
)
|
|
395
|
+
else:
|
|
396
|
+
encoder = JSONEncoder(skipkeys=False, sort_keys=False, indent=None, separators=(":", ","))
|
|
397
|
+
_iterencode = encoder.iterencode
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _encode(o: Any) -> str:
|
|
401
|
+
return "".join(_iterencode(o, False))
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _to_hashable_key(value: T, _encode: Callable = _encode) -> tuple[type, str | T]:
|
|
405
|
+
if isinstance(value, (dict, list)):
|
|
406
|
+
serialized = _encode(value)
|
|
407
|
+
return (type(value), serialized)
|
|
408
|
+
return (type(value), value)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
class HashSet:
|
|
412
|
+
"""Helper to track already generated values."""
|
|
413
|
+
|
|
414
|
+
__slots__ = ("_data",)
|
|
415
|
+
|
|
416
|
+
def __init__(self) -> None:
|
|
417
|
+
self._data: set[tuple] = set()
|
|
418
|
+
|
|
419
|
+
def insert(self, value: Any) -> bool:
|
|
420
|
+
key = _to_hashable_key(value)
|
|
421
|
+
before = len(self._data)
|
|
422
|
+
self._data.add(key)
|
|
423
|
+
return len(self._data) > before
|
|
424
|
+
|
|
425
|
+
def clear(self) -> None:
|
|
426
|
+
self._data.clear()
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _cover_positive_for_type(
|
|
430
|
+
ctx: CoverageContext, schema: dict, ty: str | None, seen: HashSet | None = None
|
|
431
|
+
) -> Generator[GeneratedValue, None, None]:
|
|
432
|
+
if ty == "object" or ty == "array":
|
|
433
|
+
template_schema = _get_template_schema(schema, ty)
|
|
434
|
+
template = ctx.generate_from_schema(template_schema)
|
|
435
|
+
elif "properties" in schema or "required" in schema:
|
|
436
|
+
template_schema = _get_template_schema(schema, "object")
|
|
437
|
+
template = ctx.generate_from_schema(template_schema)
|
|
438
|
+
else:
|
|
439
|
+
template = None
|
|
440
|
+
if GenerationMode.POSITIVE in ctx.generation_modes:
|
|
441
|
+
ctx = ctx.with_positive()
|
|
442
|
+
enum = schema.get("enum", NOT_SET)
|
|
443
|
+
const = schema.get("const", NOT_SET)
|
|
444
|
+
for key in ("anyOf", "oneOf"):
|
|
445
|
+
sub_schemas = schema.get(key)
|
|
446
|
+
if sub_schemas is not None:
|
|
447
|
+
for sub_schema in sub_schemas:
|
|
448
|
+
yield from cover_schema_iter(ctx, sub_schema)
|
|
449
|
+
all_of = schema.get("allOf")
|
|
450
|
+
if all_of is not None:
|
|
451
|
+
if len(all_of) == 1:
|
|
452
|
+
yield from cover_schema_iter(ctx, all_of[0])
|
|
453
|
+
else:
|
|
454
|
+
with suppress(jsonschema.SchemaError):
|
|
455
|
+
for idx, sub_schema in enumerate(all_of):
|
|
456
|
+
if "$ref" in sub_schema:
|
|
457
|
+
all_of[idx] = ctx.resolve_ref(sub_schema["$ref"])
|
|
458
|
+
canonical = canonicalish(schema)
|
|
459
|
+
yield from cover_schema_iter(ctx, canonical)
|
|
460
|
+
if enum is not NOT_SET:
|
|
461
|
+
for value in enum:
|
|
462
|
+
yield PositiveValue(value, scenario=CoverageScenario.ENUM_VALUE, description="Enum value")
|
|
463
|
+
elif const is not NOT_SET:
|
|
464
|
+
yield PositiveValue(const, scenario=CoverageScenario.CONST_VALUE, description="Const value")
|
|
465
|
+
elif ty is not None:
|
|
466
|
+
if ty == "null":
|
|
467
|
+
yield PositiveValue(None, scenario=CoverageScenario.NULL_VALUE, description="Value null value")
|
|
468
|
+
elif ty == "boolean":
|
|
469
|
+
yield PositiveValue(True, scenario=CoverageScenario.VALID_BOOLEAN, description="Valid boolean value")
|
|
470
|
+
yield PositiveValue(False, scenario=CoverageScenario.VALID_BOOLEAN, description="Valid boolean value")
|
|
471
|
+
elif ty == "string":
|
|
472
|
+
yield from _positive_string(ctx, schema)
|
|
473
|
+
elif ty == "integer" or ty == "number":
|
|
474
|
+
yield from _positive_number(ctx, schema)
|
|
475
|
+
elif ty == "array":
|
|
476
|
+
yield from _positive_array(ctx, schema, cast(list, template))
|
|
477
|
+
elif ty == "object":
|
|
478
|
+
yield from _positive_object(ctx, schema, cast(dict, template))
|
|
479
|
+
elif "properties" in schema or "required" in schema:
|
|
480
|
+
yield from _positive_object(ctx, schema, cast(dict, template))
|
|
481
|
+
elif "not" in schema and isinstance(schema["not"], (dict, bool)):
|
|
482
|
+
# For 'not' schemas: generate negative cases of inner schema (violations)
|
|
483
|
+
# These violations are positive for the outer schema, so flip the mode
|
|
484
|
+
nctx = ctx.with_negative()
|
|
485
|
+
yield from _flip_generation_mode_for_not(cover_schema_iter(nctx, schema["not"], seen))
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
@contextmanager
|
|
489
|
+
def _ignore_unfixable(
|
|
490
|
+
*,
|
|
491
|
+
# Cache exception types here as `jsonschema` uses a custom `__getattr__` on the module level
|
|
492
|
+
# and it may cause errors during the interpreter shutdown
|
|
493
|
+
ref_error: type[Exception] = RefResolutionError,
|
|
494
|
+
schema_error: type[Exception] = jsonschema.SchemaError,
|
|
495
|
+
) -> Generator:
|
|
496
|
+
try:
|
|
497
|
+
yield
|
|
498
|
+
except (Unsatisfiable, ref_error, schema_error):
|
|
499
|
+
pass
|
|
500
|
+
except InvalidArgument as exc:
|
|
501
|
+
message = str(exc)
|
|
502
|
+
if "Cannot create non-empty" not in message and "is not in the specified alphabet" not in message:
|
|
503
|
+
raise
|
|
504
|
+
except TypeError as exc:
|
|
505
|
+
if "first argument must be string or compiled pattern" not in str(exc):
|
|
506
|
+
raise
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def cover_schema_iter(
|
|
510
|
+
ctx: CoverageContext, schema: dict | bool, seen: HashSet | None = None
|
|
511
|
+
) -> Generator[GeneratedValue, None, None]:
|
|
512
|
+
if seen is None:
|
|
513
|
+
seen = HashSet()
|
|
514
|
+
|
|
515
|
+
if isinstance(schema, dict) and "$ref" in schema:
|
|
516
|
+
reference = schema["$ref"]
|
|
517
|
+
try:
|
|
518
|
+
resolved = ctx.resolve_ref(reference)
|
|
519
|
+
if isinstance(resolved, dict):
|
|
520
|
+
schema = {**resolved, **{k: v for k, v in schema.items() if k != "$ref"}}
|
|
521
|
+
yield from cover_schema_iter(ctx, schema, seen)
|
|
522
|
+
else:
|
|
523
|
+
yield from cover_schema_iter(ctx, resolved, seen)
|
|
524
|
+
return
|
|
525
|
+
except RefResolutionError:
|
|
526
|
+
# Can't resolve a reference - at this point, we can't generate anything useful as `$ref` is in the current schema root
|
|
527
|
+
return
|
|
528
|
+
|
|
529
|
+
if schema is True:
|
|
530
|
+
types = ["null", "boolean", "string", "number", "array", "object"]
|
|
531
|
+
schema = {}
|
|
532
|
+
elif schema is False:
|
|
533
|
+
types = []
|
|
534
|
+
schema = {"not": {}}
|
|
535
|
+
elif not any(k in ALL_KEYWORDS for k in schema):
|
|
536
|
+
types = ["null", "boolean", "string", "number", "array", "object"]
|
|
537
|
+
else:
|
|
538
|
+
types = schema.get("type", [])
|
|
539
|
+
push_examples_to_properties(schema)
|
|
540
|
+
if not isinstance(types, list):
|
|
541
|
+
types = [types] # type: ignore[unreachable]
|
|
542
|
+
if not types:
|
|
543
|
+
with _ignore_unfixable():
|
|
544
|
+
yield from _cover_positive_for_type(ctx, schema, None)
|
|
545
|
+
for ty in types:
|
|
546
|
+
with _ignore_unfixable():
|
|
547
|
+
yield from _cover_positive_for_type(ctx, schema, ty)
|
|
548
|
+
if GenerationMode.NEGATIVE in ctx.generation_modes:
|
|
549
|
+
template = None
|
|
550
|
+
if not ctx.can_be_negated(schema):
|
|
551
|
+
return
|
|
552
|
+
for key, value in schema.items():
|
|
553
|
+
with _ignore_unfixable(), ctx.at(key):
|
|
554
|
+
if key == "enum":
|
|
555
|
+
yield from _negative_enum(ctx, value, seen)
|
|
556
|
+
elif key == "const":
|
|
557
|
+
for value_ in _negative_enum(ctx, [value], seen):
|
|
558
|
+
yield value_
|
|
559
|
+
elif key == "type":
|
|
560
|
+
yield from _negative_type(ctx, value, seen, schema)
|
|
561
|
+
elif key == "properties":
|
|
562
|
+
template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
|
|
563
|
+
yield from _negative_properties(ctx, template, value)
|
|
564
|
+
elif key == "patternProperties":
|
|
565
|
+
template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
|
|
566
|
+
yield from _negative_pattern_properties(ctx, template, value)
|
|
567
|
+
elif key == "items" and isinstance(value, dict):
|
|
568
|
+
yield from _negative_items(ctx, value)
|
|
569
|
+
elif key == "pattern":
|
|
570
|
+
min_length = schema.get("minLength")
|
|
571
|
+
max_length = schema.get("maxLength")
|
|
572
|
+
yield from _negative_pattern(ctx, value, min_length=min_length, max_length=max_length)
|
|
573
|
+
elif key == "format" and ("string" in types or not types):
|
|
574
|
+
yield from _negative_format(ctx, schema, value)
|
|
575
|
+
elif key == "maximum":
|
|
576
|
+
next = value + 1
|
|
577
|
+
if seen.insert(next):
|
|
578
|
+
yield NegativeValue(
|
|
579
|
+
next,
|
|
580
|
+
scenario=CoverageScenario.VALUE_ABOVE_MAXIMUM,
|
|
581
|
+
description="Value greater than maximum",
|
|
582
|
+
location=ctx.current_path,
|
|
583
|
+
)
|
|
584
|
+
elif key == "minimum":
|
|
585
|
+
next = value - 1
|
|
586
|
+
if seen.insert(next):
|
|
587
|
+
yield NegativeValue(
|
|
588
|
+
next,
|
|
589
|
+
scenario=CoverageScenario.VALUE_BELOW_MINIMUM,
|
|
590
|
+
description="Value smaller than minimum",
|
|
591
|
+
location=ctx.current_path,
|
|
592
|
+
)
|
|
593
|
+
elif key == "exclusiveMaximum" or key == "exclusiveMinimum" and seen.insert(value):
|
|
594
|
+
verb = "greater" if key == "exclusiveMaximum" else "smaller"
|
|
595
|
+
limit = "maximum" if key == "exclusiveMaximum" else "minimum"
|
|
596
|
+
scenario = (
|
|
597
|
+
CoverageScenario.VALUE_ABOVE_MAXIMUM
|
|
598
|
+
if key == "exclusiveMaximum"
|
|
599
|
+
else CoverageScenario.VALUE_BELOW_MINIMUM
|
|
600
|
+
)
|
|
601
|
+
yield NegativeValue(
|
|
602
|
+
value, scenario=scenario, description=f"Value {verb} than {limit}", location=ctx.current_path
|
|
603
|
+
)
|
|
604
|
+
elif key == "multipleOf":
|
|
605
|
+
for value_ in _negative_multiple_of(ctx, schema, value):
|
|
606
|
+
if seen.insert(value_.value):
|
|
607
|
+
yield value_
|
|
608
|
+
elif key == "minLength" and 0 < value < INTERNAL_BUFFER_SIZE:
|
|
609
|
+
if value == 1:
|
|
610
|
+
# In this case, the only possible negative string is an empty one
|
|
611
|
+
# The `pattern` value may require an non-empty one and the generation will fail
|
|
612
|
+
# However, it is fine to violate `pattern` here as it is negative string generation anyway
|
|
613
|
+
value = ""
|
|
614
|
+
if ctx.is_valid_for_location(value) and seen.insert(value):
|
|
615
|
+
yield NegativeValue(
|
|
616
|
+
value,
|
|
617
|
+
scenario=CoverageScenario.STRING_BELOW_MIN_LENGTH,
|
|
618
|
+
description="String smaller than minLength",
|
|
619
|
+
location=ctx.current_path,
|
|
620
|
+
)
|
|
621
|
+
else:
|
|
622
|
+
with suppress(InvalidArgument):
|
|
623
|
+
min_length = max_length = value - 1
|
|
624
|
+
new_schema = {**schema, "minLength": min_length, "maxLength": max_length}
|
|
625
|
+
new_schema.setdefault("type", "string")
|
|
626
|
+
if "pattern" in new_schema:
|
|
627
|
+
new_schema["pattern"] = update_quantifier(schema["pattern"], min_length, max_length)
|
|
628
|
+
if new_schema["pattern"] == schema["pattern"]:
|
|
629
|
+
# Pattern wasn't updated, try to generate a valid value then shrink the string to the required length
|
|
630
|
+
del new_schema["minLength"]
|
|
631
|
+
del new_schema["maxLength"]
|
|
632
|
+
value = ctx.generate_from_schema(new_schema)[:max_length]
|
|
633
|
+
else:
|
|
634
|
+
value = ctx.generate_from_schema(new_schema)
|
|
635
|
+
else:
|
|
636
|
+
value = ctx.generate_from_schema(new_schema)
|
|
637
|
+
if ctx.is_valid_for_location(value) and seen.insert(value):
|
|
638
|
+
yield NegativeValue(
|
|
639
|
+
value,
|
|
640
|
+
scenario=CoverageScenario.STRING_BELOW_MIN_LENGTH,
|
|
641
|
+
description="String smaller than minLength",
|
|
642
|
+
location=ctx.current_path,
|
|
643
|
+
)
|
|
644
|
+
elif key == "maxLength" and value < INTERNAL_BUFFER_SIZE:
|
|
645
|
+
try:
|
|
646
|
+
min_length = max_length = value + 1
|
|
647
|
+
new_schema = {**schema, "minLength": min_length, "maxLength": max_length}
|
|
648
|
+
new_schema.setdefault("type", "string")
|
|
649
|
+
if "pattern" in new_schema:
|
|
650
|
+
if value > NEGATIVE_MODE_MAX_LENGTH_WITH_PATTERN:
|
|
651
|
+
# Large `maxLength` value can be extremely slow to generate when combined with `pattern`
|
|
652
|
+
del new_schema["pattern"]
|
|
653
|
+
value = ctx.generate_from_schema(new_schema)
|
|
654
|
+
else:
|
|
655
|
+
new_schema["pattern"] = update_quantifier(schema["pattern"], min_length, max_length)
|
|
656
|
+
if new_schema["pattern"] == schema["pattern"]:
|
|
657
|
+
# Pattern wasn't updated, try to generate a valid value then extend the string to the required length
|
|
658
|
+
del new_schema["minLength"]
|
|
659
|
+
del new_schema["maxLength"]
|
|
660
|
+
value = ctx.generate_from_schema(new_schema).ljust(max_length, "0")
|
|
661
|
+
else:
|
|
662
|
+
value = ctx.generate_from_schema(new_schema)
|
|
663
|
+
else:
|
|
664
|
+
value = ctx.generate_from_schema(new_schema)
|
|
665
|
+
if seen.insert(value):
|
|
666
|
+
yield NegativeValue(
|
|
667
|
+
value,
|
|
668
|
+
scenario=CoverageScenario.STRING_ABOVE_MAX_LENGTH,
|
|
669
|
+
description="String larger than maxLength",
|
|
670
|
+
location=ctx.current_path,
|
|
671
|
+
)
|
|
672
|
+
except (InvalidArgument, Unsatisfiable):
|
|
673
|
+
pass
|
|
674
|
+
elif key == "uniqueItems" and value:
|
|
675
|
+
yield from _negative_unique_items(ctx, schema)
|
|
676
|
+
elif key == "required":
|
|
677
|
+
template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
|
|
678
|
+
yield from _negative_required(ctx, template, value)
|
|
679
|
+
elif key == "maxItems" and isinstance(value, int) and value < INTERNAL_BUFFER_SIZE:
|
|
680
|
+
if value > NEGATIVE_MODE_MAX_ITEMS:
|
|
681
|
+
# It could be extremely slow to generate large arrays
|
|
682
|
+
# Generate values up to the limit and reuse them to construct the final array
|
|
683
|
+
new_schema = {
|
|
684
|
+
**schema,
|
|
685
|
+
"minItems": NEGATIVE_MODE_MAX_ITEMS,
|
|
686
|
+
"maxItems": NEGATIVE_MODE_MAX_ITEMS,
|
|
687
|
+
"type": "array",
|
|
688
|
+
}
|
|
689
|
+
if "items" in schema and isinstance(schema["items"], dict):
|
|
690
|
+
# The schema may have another large array nested, therefore generate covering cases
|
|
691
|
+
# and use them to build an array for the current schema
|
|
692
|
+
negative = [case.value for case in cover_schema_iter(ctx, schema["items"])]
|
|
693
|
+
positive = [case.value for case in cover_schema_iter(ctx.with_positive(), schema["items"])]
|
|
694
|
+
# Interleave positive & negative values
|
|
695
|
+
array_value = [value for pair in zip(positive, negative) for value in pair][
|
|
696
|
+
:NEGATIVE_MODE_MAX_ITEMS
|
|
697
|
+
]
|
|
698
|
+
else:
|
|
699
|
+
array_value = ctx.generate_from_schema(new_schema)
|
|
700
|
+
|
|
701
|
+
# Extend the array to be of length value + 1 by repeating its own elements
|
|
702
|
+
diff = value + 1 - len(array_value)
|
|
703
|
+
if diff > 0 and array_value:
|
|
704
|
+
array_value += (
|
|
705
|
+
array_value * (diff // len(array_value)) + array_value[: diff % len(array_value)]
|
|
706
|
+
)
|
|
707
|
+
if seen.insert(array_value):
|
|
708
|
+
yield NegativeValue(
|
|
709
|
+
array_value,
|
|
710
|
+
scenario=CoverageScenario.ARRAY_ABOVE_MAX_ITEMS,
|
|
711
|
+
description="Array with more items than allowed by maxItems",
|
|
712
|
+
location=ctx.current_path,
|
|
713
|
+
)
|
|
714
|
+
else:
|
|
715
|
+
try:
|
|
716
|
+
# Force the array to have one more item than allowed
|
|
717
|
+
new_schema = {**schema, "minItems": value + 1, "maxItems": value + 1, "type": "array"}
|
|
718
|
+
array_value = ctx.generate_from_schema(new_schema)
|
|
719
|
+
if seen.insert(array_value):
|
|
720
|
+
yield NegativeValue(
|
|
721
|
+
array_value,
|
|
722
|
+
scenario=CoverageScenario.ARRAY_ABOVE_MAX_ITEMS,
|
|
723
|
+
description="Array with more items than allowed by maxItems",
|
|
724
|
+
location=ctx.current_path,
|
|
725
|
+
)
|
|
726
|
+
except (InvalidArgument, Unsatisfiable):
|
|
727
|
+
pass
|
|
728
|
+
elif key == "minItems" and isinstance(value, int) and value > 0:
|
|
729
|
+
try:
|
|
730
|
+
# Force the array to have one less item than the minimum
|
|
731
|
+
new_schema = {**schema, "minItems": value - 1, "maxItems": value - 1, "type": "array"}
|
|
732
|
+
array_value = ctx.generate_from_schema(new_schema)
|
|
733
|
+
if seen.insert(array_value):
|
|
734
|
+
yield NegativeValue(
|
|
735
|
+
array_value,
|
|
736
|
+
scenario=CoverageScenario.ARRAY_BELOW_MIN_ITEMS,
|
|
737
|
+
description="Array with fewer items than allowed by minItems",
|
|
738
|
+
location=ctx.current_path,
|
|
739
|
+
)
|
|
740
|
+
except (InvalidArgument, Unsatisfiable):
|
|
741
|
+
pass
|
|
742
|
+
elif (
|
|
743
|
+
key == "additionalProperties"
|
|
744
|
+
and not value
|
|
745
|
+
and "pattern" not in schema
|
|
746
|
+
and schema.get("type") in ["object", None]
|
|
747
|
+
):
|
|
748
|
+
if not ctx.allow_extra_parameters and ctx.location in (
|
|
749
|
+
ParameterLocation.QUERY,
|
|
750
|
+
ParameterLocation.HEADER,
|
|
751
|
+
ParameterLocation.COOKIE,
|
|
752
|
+
):
|
|
753
|
+
continue
|
|
754
|
+
template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
|
|
755
|
+
yield NegativeValue(
|
|
756
|
+
{**template, UNKNOWN_PROPERTY_KEY: UNKNOWN_PROPERTY_VALUE},
|
|
757
|
+
scenario=CoverageScenario.OBJECT_UNEXPECTED_PROPERTIES,
|
|
758
|
+
description="Object with unexpected properties",
|
|
759
|
+
location=ctx.current_path,
|
|
760
|
+
)
|
|
761
|
+
elif key == "allOf":
|
|
762
|
+
nctx = ctx.with_negative()
|
|
763
|
+
if len(value) == 1:
|
|
764
|
+
with nctx.at(0):
|
|
765
|
+
yield from cover_schema_iter(nctx, value[0], seen)
|
|
766
|
+
else:
|
|
767
|
+
with _ignore_unfixable():
|
|
768
|
+
canonical = canonicalish(schema)
|
|
769
|
+
yield from cover_schema_iter(nctx, canonical, seen)
|
|
770
|
+
elif key == "anyOf":
|
|
771
|
+
nctx = ctx.with_negative()
|
|
772
|
+
validators = [jsonschema.validators.validator_for(sub_schema)(sub_schema) for sub_schema in value]
|
|
773
|
+
for idx, sub_schema in enumerate(value):
|
|
774
|
+
with nctx.at(idx):
|
|
775
|
+
for value in cover_schema_iter(nctx, sub_schema, seen):
|
|
776
|
+
# Negative value for this schema could be a positive value for another one
|
|
777
|
+
if is_valid_for_others(value.value, idx, validators):
|
|
778
|
+
continue
|
|
779
|
+
yield value
|
|
780
|
+
elif key == "oneOf":
|
|
781
|
+
nctx = ctx.with_negative()
|
|
782
|
+
validators = [jsonschema.validators.validator_for(sub_schema)(sub_schema) for sub_schema in value]
|
|
783
|
+
for idx, sub_schema in enumerate(value):
|
|
784
|
+
with nctx.at(idx):
|
|
785
|
+
for value in cover_schema_iter(nctx, sub_schema, seen):
|
|
786
|
+
if is_invalid_for_oneOf(value.value, idx, validators):
|
|
787
|
+
yield value
|
|
788
|
+
elif key == "not" and isinstance(value, (dict, bool)):
|
|
789
|
+
# For 'not' schemas: generate positive cases of inner schema (valid values)
|
|
790
|
+
# These valid values are negative for the outer schema, so flip the mode
|
|
791
|
+
pctx = ctx.with_positive()
|
|
792
|
+
yield from _flip_generation_mode_for_not(cover_schema_iter(pctx, value, seen))
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def is_valid_for_others(value: Any, idx: int, validators: list[jsonschema.Validator]) -> bool:
|
|
796
|
+
for vidx, validator in enumerate(validators):
|
|
797
|
+
if idx == vidx:
|
|
798
|
+
# This one is being negated
|
|
799
|
+
continue
|
|
800
|
+
if validator.is_valid(value):
|
|
801
|
+
return True
|
|
802
|
+
return False
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def is_invalid_for_oneOf(value: Any, idx: int, validators: list[jsonschema.Validator]) -> bool:
|
|
806
|
+
valid_count = 0
|
|
807
|
+
for vidx, validator in enumerate(validators):
|
|
808
|
+
if idx == vidx:
|
|
809
|
+
# This one is being negated
|
|
810
|
+
continue
|
|
811
|
+
if validator.is_valid(value):
|
|
812
|
+
valid_count += 1
|
|
813
|
+
# Should circuit - no need to validate more, it is already invalid
|
|
814
|
+
if valid_count > 1:
|
|
815
|
+
return True
|
|
816
|
+
# No matching at all - we successfully generated invalid value
|
|
817
|
+
return valid_count == 0
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def _get_properties(schema: dict | bool) -> dict | bool:
|
|
821
|
+
if isinstance(schema, dict):
|
|
822
|
+
if "example" in schema:
|
|
823
|
+
return {"const": schema["example"]}
|
|
824
|
+
if "default" in schema:
|
|
825
|
+
return {"const": schema["default"]}
|
|
826
|
+
if schema.get("examples"):
|
|
827
|
+
return {"enum": schema["examples"]}
|
|
828
|
+
if schema.get("type") == "object":
|
|
829
|
+
return _get_template_schema(schema, "object")
|
|
830
|
+
_schema = deepclone(schema)
|
|
831
|
+
update_pattern_in_schema(_schema)
|
|
832
|
+
return _schema
|
|
833
|
+
return schema
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def _get_template_schema(schema: dict, ty: str) -> dict:
|
|
837
|
+
if ty == "object":
|
|
838
|
+
properties = schema.get("properties")
|
|
839
|
+
if properties is not None:
|
|
840
|
+
return {
|
|
841
|
+
**schema,
|
|
842
|
+
"required": list(properties),
|
|
843
|
+
"type": ty,
|
|
844
|
+
"properties": {k: _get_properties(v) for k, v in properties.items()},
|
|
845
|
+
}
|
|
846
|
+
return {**schema, "type": ty}
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def _ensure_valid_path_parameter_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
850
|
+
# Path parameters should have at least 1 character length and don't contain any characters with special treatment
|
|
851
|
+
# on the transport level.
|
|
852
|
+
# The implementation below sneaks into `not` to avoid clashing with existing `pattern` keyword
|
|
853
|
+
not_ = schema.get("not", {}).copy()
|
|
854
|
+
not_["pattern"] = r"[/{}]"
|
|
855
|
+
return {**schema, "minLength": 1, "not": not_}
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def _ensure_valid_headers_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
859
|
+
# Reject any character that is not A-Z, a-z, or 0-9 for simplicity
|
|
860
|
+
not_ = schema.get("not", {}).copy()
|
|
861
|
+
not_["pattern"] = r"[^A-Za-z0-9]"
|
|
862
|
+
return {**schema, "not": not_}
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
|
|
866
|
+
"""Generate positive string values."""
|
|
867
|
+
# Boundary and near boundary values
|
|
868
|
+
schema = {"type": "string", **schema}
|
|
869
|
+
min_length = schema.get("minLength")
|
|
870
|
+
if min_length == 0:
|
|
871
|
+
min_length = None
|
|
872
|
+
max_length = schema.get("maxLength")
|
|
873
|
+
if ctx.location == "path":
|
|
874
|
+
schema = _ensure_valid_path_parameter_schema(schema)
|
|
875
|
+
elif ctx.location in ("header", "cookie") and not ("format" in schema and schema["format"] in FORMAT_STRATEGIES):
|
|
876
|
+
# Don't apply it for known formats - they will insure the correct format during generation
|
|
877
|
+
schema = _ensure_valid_headers_schema(schema)
|
|
878
|
+
|
|
879
|
+
example = schema.get("example")
|
|
880
|
+
examples = schema.get("examples")
|
|
881
|
+
default = schema.get("default")
|
|
882
|
+
|
|
883
|
+
# Two-layer check to avoid potentially expensive data generation using schema constraints as a key
|
|
884
|
+
seen_values = HashSet()
|
|
885
|
+
seen_constraints: set[tuple] = set()
|
|
886
|
+
|
|
887
|
+
if example or examples or default:
|
|
888
|
+
has_valid_example = False
|
|
889
|
+
if example and ctx.is_valid_for_location(example) and seen_values.insert(example):
|
|
890
|
+
has_valid_example = True
|
|
891
|
+
yield PositiveValue(example, scenario=CoverageScenario.EXAMPLE_VALUE, description="Example value")
|
|
892
|
+
if examples:
|
|
893
|
+
for example in examples:
|
|
894
|
+
if ctx.is_valid_for_location(example) and seen_values.insert(example):
|
|
895
|
+
has_valid_example = True
|
|
896
|
+
yield PositiveValue(example, scenario=CoverageScenario.EXAMPLE_VALUE, description="Example value")
|
|
897
|
+
if (
|
|
898
|
+
default
|
|
899
|
+
and not (example is not None and default == example)
|
|
900
|
+
and not (examples is not None and any(default == ex for ex in examples))
|
|
901
|
+
and ctx.is_valid_for_location(default)
|
|
902
|
+
and seen_values.insert(default)
|
|
903
|
+
):
|
|
904
|
+
has_valid_example = True
|
|
905
|
+
yield PositiveValue(default, scenario=CoverageScenario.DEFAULT_VALUE, description="Default value")
|
|
906
|
+
if not has_valid_example:
|
|
907
|
+
if not min_length and not max_length or "pattern" in schema:
|
|
908
|
+
value = ctx.generate_from_schema(schema)
|
|
909
|
+
seen_values.insert(value)
|
|
910
|
+
seen_constraints.add((min_length, max_length))
|
|
911
|
+
yield PositiveValue(value, scenario=CoverageScenario.VALID_STRING, description="Valid string")
|
|
912
|
+
elif not min_length and not max_length or "pattern" in schema:
|
|
913
|
+
value = ctx.generate_from_schema(schema)
|
|
914
|
+
seen_values.insert(value)
|
|
915
|
+
seen_constraints.add((min_length, max_length))
|
|
916
|
+
yield PositiveValue(value, scenario=CoverageScenario.VALID_STRING, description="Valid string")
|
|
917
|
+
|
|
918
|
+
if min_length is not None and min_length < INTERNAL_BUFFER_SIZE:
|
|
919
|
+
# Exactly the minimum length
|
|
920
|
+
key = (min_length, min_length)
|
|
921
|
+
if key not in seen_constraints:
|
|
922
|
+
seen_constraints.add(key)
|
|
923
|
+
value = ctx.generate_from_schema({**schema, "maxLength": min_length})
|
|
924
|
+
if seen_values.insert(value):
|
|
925
|
+
yield PositiveValue(
|
|
926
|
+
value, scenario=CoverageScenario.MINIMUM_LENGTH_STRING, description="Minimum length string"
|
|
927
|
+
)
|
|
928
|
+
|
|
929
|
+
# One character more than minimum if possible
|
|
930
|
+
larger = min_length + 1
|
|
931
|
+
key = (larger, larger)
|
|
932
|
+
if larger < INTERNAL_BUFFER_SIZE and key not in seen_constraints and (not max_length or larger <= max_length):
|
|
933
|
+
seen_constraints.add(key)
|
|
934
|
+
value = ctx.generate_from_schema({**schema, "minLength": larger, "maxLength": larger})
|
|
935
|
+
if seen_values.insert(value):
|
|
936
|
+
yield PositiveValue(
|
|
937
|
+
value,
|
|
938
|
+
scenario=CoverageScenario.NEAR_BOUNDARY_LENGTH_STRING,
|
|
939
|
+
description="Near-boundary length string",
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
if max_length is not None:
|
|
943
|
+
# Exactly the maximum length
|
|
944
|
+
key = (max_length, max_length)
|
|
945
|
+
if max_length < INTERNAL_BUFFER_SIZE and key not in seen_constraints:
|
|
946
|
+
seen_constraints.add(key)
|
|
947
|
+
value = ctx.generate_from_schema({**schema, "minLength": max_length, "maxLength": max_length})
|
|
948
|
+
if seen_values.insert(value):
|
|
949
|
+
yield PositiveValue(
|
|
950
|
+
value, scenario=CoverageScenario.MAXIMUM_LENGTH_STRING, description="Maximum length string"
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
# One character less than maximum if possible
|
|
954
|
+
smaller = max_length - 1
|
|
955
|
+
key = (smaller, smaller)
|
|
956
|
+
if (
|
|
957
|
+
smaller < INTERNAL_BUFFER_SIZE
|
|
958
|
+
and key not in seen_constraints
|
|
959
|
+
and (smaller > 0 and (min_length is None or smaller >= min_length))
|
|
960
|
+
):
|
|
961
|
+
seen_constraints.add(key)
|
|
962
|
+
value = ctx.generate_from_schema({**schema, "minLength": smaller, "maxLength": smaller})
|
|
963
|
+
if seen_values.insert(value):
|
|
964
|
+
yield PositiveValue(
|
|
965
|
+
value,
|
|
966
|
+
scenario=CoverageScenario.NEAR_BOUNDARY_LENGTH_STRING,
|
|
967
|
+
description="Near-boundary length string",
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def closest_multiple_greater_than(y: int, x: int) -> int:
|
|
972
|
+
"""Find the closest multiple of X that is greater than Y."""
|
|
973
|
+
quotient, remainder = divmod(y, x)
|
|
974
|
+
if remainder == 0:
|
|
975
|
+
return y
|
|
976
|
+
return x * (quotient + 1)
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
|
|
980
|
+
"""Generate positive integer values."""
|
|
981
|
+
# Boundary and near boundary values
|
|
982
|
+
schema = {"type": "number", **schema}
|
|
983
|
+
minimum = schema.get("minimum")
|
|
984
|
+
maximum = schema.get("maximum")
|
|
985
|
+
exclusive_minimum = schema.get("exclusiveMinimum")
|
|
986
|
+
exclusive_maximum = schema.get("exclusiveMaximum")
|
|
987
|
+
if exclusive_minimum is not None:
|
|
988
|
+
minimum = exclusive_minimum + 1
|
|
989
|
+
if exclusive_maximum is not None:
|
|
990
|
+
maximum = exclusive_maximum - 1
|
|
991
|
+
multiple_of = schema.get("multipleOf")
|
|
992
|
+
example = schema.get("example")
|
|
993
|
+
examples = schema.get("examples")
|
|
994
|
+
default = schema.get("default")
|
|
995
|
+
|
|
996
|
+
seen = HashSet()
|
|
997
|
+
|
|
998
|
+
if example or examples or default:
|
|
999
|
+
if example and seen.insert(example):
|
|
1000
|
+
yield PositiveValue(example, scenario=CoverageScenario.EXAMPLE_VALUE, description="Example value")
|
|
1001
|
+
if examples:
|
|
1002
|
+
for example in examples:
|
|
1003
|
+
if seen.insert(example):
|
|
1004
|
+
yield PositiveValue(example, scenario=CoverageScenario.EXAMPLE_VALUE, description="Example value")
|
|
1005
|
+
if (
|
|
1006
|
+
default
|
|
1007
|
+
and not (example is not None and default == example)
|
|
1008
|
+
and not (examples is not None and any(default == ex for ex in examples))
|
|
1009
|
+
and seen.insert(default)
|
|
1010
|
+
):
|
|
1011
|
+
yield PositiveValue(default, scenario=CoverageScenario.DEFAULT_VALUE, description="Default value")
|
|
1012
|
+
elif not minimum and not maximum:
|
|
1013
|
+
value = ctx.generate_from_schema(schema)
|
|
1014
|
+
seen.insert(value)
|
|
1015
|
+
yield PositiveValue(value, scenario=CoverageScenario.VALID_NUMBER, description="Valid number")
|
|
1016
|
+
|
|
1017
|
+
if minimum is not None:
|
|
1018
|
+
# Exactly the minimum
|
|
1019
|
+
if multiple_of is not None:
|
|
1020
|
+
smallest = closest_multiple_greater_than(minimum, multiple_of)
|
|
1021
|
+
else:
|
|
1022
|
+
smallest = minimum
|
|
1023
|
+
if seen.insert(smallest):
|
|
1024
|
+
yield PositiveValue(smallest, scenario=CoverageScenario.MINIMUM_VALUE, description="Minimum value")
|
|
1025
|
+
|
|
1026
|
+
# One more than minimum if possible
|
|
1027
|
+
if multiple_of is not None:
|
|
1028
|
+
larger = smallest + multiple_of
|
|
1029
|
+
else:
|
|
1030
|
+
larger = minimum + 1
|
|
1031
|
+
if (not maximum or larger <= maximum) and seen.insert(larger):
|
|
1032
|
+
yield PositiveValue(
|
|
1033
|
+
larger, scenario=CoverageScenario.NEAR_BOUNDARY_NUMBER, description="Near-boundary number"
|
|
1034
|
+
)
|
|
1035
|
+
|
|
1036
|
+
if maximum is not None:
|
|
1037
|
+
# Exactly the maximum
|
|
1038
|
+
if multiple_of is not None:
|
|
1039
|
+
largest = maximum - (maximum % multiple_of)
|
|
1040
|
+
else:
|
|
1041
|
+
largest = maximum
|
|
1042
|
+
if seen.insert(largest):
|
|
1043
|
+
yield PositiveValue(largest, scenario=CoverageScenario.MAXIMUM_VALUE, description="Maximum value")
|
|
1044
|
+
|
|
1045
|
+
# One less than maximum if possible
|
|
1046
|
+
if multiple_of is not None:
|
|
1047
|
+
smaller = largest - multiple_of
|
|
1048
|
+
else:
|
|
1049
|
+
smaller = maximum - 1
|
|
1050
|
+
if (minimum is None or smaller >= minimum) and seen.insert(smaller):
|
|
1051
|
+
yield PositiveValue(
|
|
1052
|
+
smaller, scenario=CoverageScenario.NEAR_BOUNDARY_NUMBER, description="Near-boundary number"
|
|
1053
|
+
)
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Generator[GeneratedValue, None, None]:
|
|
1057
|
+
example = schema.get("example")
|
|
1058
|
+
examples = schema.get("examples")
|
|
1059
|
+
default = schema.get("default")
|
|
1060
|
+
|
|
1061
|
+
seen = HashSet()
|
|
1062
|
+
seen_constraints: set[tuple] = set()
|
|
1063
|
+
|
|
1064
|
+
if example or examples or default:
|
|
1065
|
+
if example and seen.insert(example):
|
|
1066
|
+
yield PositiveValue(example, scenario=CoverageScenario.EXAMPLE_VALUE, description="Example value")
|
|
1067
|
+
if examples:
|
|
1068
|
+
for example in examples:
|
|
1069
|
+
if seen.insert(example):
|
|
1070
|
+
yield PositiveValue(example, scenario=CoverageScenario.EXAMPLE_VALUE, description="Example value")
|
|
1071
|
+
if (
|
|
1072
|
+
default
|
|
1073
|
+
and not (example is not None and default == example)
|
|
1074
|
+
and not (examples is not None and any(default == ex for ex in examples))
|
|
1075
|
+
and seen.insert(default)
|
|
1076
|
+
):
|
|
1077
|
+
yield PositiveValue(default, scenario=CoverageScenario.DEFAULT_VALUE, description="Default value")
|
|
1078
|
+
elif seen.insert(template):
|
|
1079
|
+
yield PositiveValue(template, scenario=CoverageScenario.VALID_ARRAY, description="Valid array")
|
|
1080
|
+
|
|
1081
|
+
# Boundary and near-boundary sizes
|
|
1082
|
+
min_items = schema.get("minItems")
|
|
1083
|
+
max_items = schema.get("maxItems")
|
|
1084
|
+
if min_items is not None:
|
|
1085
|
+
# Do not generate an array with `minItems` length, because it is already covered by `template`
|
|
1086
|
+
# One item more than minimum if possible
|
|
1087
|
+
larger = min_items + 1
|
|
1088
|
+
if (max_items is None or larger <= max_items) and larger not in seen_constraints:
|
|
1089
|
+
seen_constraints.add(larger)
|
|
1090
|
+
value = ctx.generate_from_schema({**schema, "minItems": larger, "maxItems": larger})
|
|
1091
|
+
if seen.insert(value):
|
|
1092
|
+
yield PositiveValue(
|
|
1093
|
+
value, scenario=CoverageScenario.NEAR_BOUNDARY_ITEMS_ARRAY, description="Near-boundary items array"
|
|
1094
|
+
)
|
|
1095
|
+
|
|
1096
|
+
if max_items is not None:
|
|
1097
|
+
if max_items < INTERNAL_BUFFER_SIZE and max_items not in seen_constraints:
|
|
1098
|
+
seen_constraints.add(max_items)
|
|
1099
|
+
value = ctx.generate_from_schema({**schema, "minItems": max_items})
|
|
1100
|
+
if seen.insert(value):
|
|
1101
|
+
yield PositiveValue(
|
|
1102
|
+
value, scenario=CoverageScenario.MAXIMUM_ITEMS_ARRAY, description="Maximum items array"
|
|
1103
|
+
)
|
|
1104
|
+
|
|
1105
|
+
# One item smaller than maximum if possible
|
|
1106
|
+
smaller = max_items - 1
|
|
1107
|
+
if (
|
|
1108
|
+
smaller < INTERNAL_BUFFER_SIZE
|
|
1109
|
+
and smaller > 0
|
|
1110
|
+
and (min_items is None or smaller >= min_items)
|
|
1111
|
+
and smaller not in seen_constraints
|
|
1112
|
+
):
|
|
1113
|
+
value = ctx.generate_from_schema({**schema, "minItems": smaller, "maxItems": smaller})
|
|
1114
|
+
if seen.insert(value):
|
|
1115
|
+
yield PositiveValue(
|
|
1116
|
+
value, scenario=CoverageScenario.NEAR_BOUNDARY_ITEMS_ARRAY, description="Near-boundary items array"
|
|
1117
|
+
)
|
|
1118
|
+
|
|
1119
|
+
if "items" in schema and "enum" in schema["items"] and isinstance(schema["items"]["enum"], list) and max_items != 0:
|
|
1120
|
+
# Ensure there is enough items to pass `minItems` if it is specified
|
|
1121
|
+
length = min_items or 1
|
|
1122
|
+
for variant in schema["items"]["enum"]:
|
|
1123
|
+
value = [variant] * length
|
|
1124
|
+
if seen.insert(value):
|
|
1125
|
+
yield PositiveValue(
|
|
1126
|
+
value,
|
|
1127
|
+
scenario=CoverageScenario.ENUM_VALUE_ITEMS_ARRAY,
|
|
1128
|
+
description="Enum value from available for items array",
|
|
1129
|
+
)
|
|
1130
|
+
elif min_items is None and max_items is None and "items" in schema and isinstance(schema["items"], dict):
|
|
1131
|
+
# Otherwise only an empty array is generated
|
|
1132
|
+
sub_schema = schema["items"]
|
|
1133
|
+
for item in cover_schema_iter(ctx, sub_schema):
|
|
1134
|
+
yield PositiveValue(
|
|
1135
|
+
[item.value],
|
|
1136
|
+
scenario=CoverageScenario.VALID_ARRAY,
|
|
1137
|
+
description=f"Single-item array: {item.description}",
|
|
1138
|
+
)
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Generator[GeneratedValue, None, None]:
|
|
1142
|
+
example = schema.get("example")
|
|
1143
|
+
examples = schema.get("examples")
|
|
1144
|
+
default = schema.get("default")
|
|
1145
|
+
|
|
1146
|
+
if example or examples or default:
|
|
1147
|
+
if example:
|
|
1148
|
+
yield PositiveValue(example, scenario=CoverageScenario.EXAMPLE_VALUE, description="Example value")
|
|
1149
|
+
if examples:
|
|
1150
|
+
for example in examples:
|
|
1151
|
+
yield PositiveValue(example, scenario=CoverageScenario.EXAMPLE_VALUE, description="Example value")
|
|
1152
|
+
if (
|
|
1153
|
+
default
|
|
1154
|
+
and not (example is not None and default == example)
|
|
1155
|
+
and not (examples is not None and any(default == ex for ex in examples))
|
|
1156
|
+
):
|
|
1157
|
+
yield PositiveValue(default, scenario=CoverageScenario.DEFAULT_VALUE, description="Default value")
|
|
1158
|
+
else:
|
|
1159
|
+
yield PositiveValue(template, scenario=CoverageScenario.VALID_OBJECT, description="Valid object")
|
|
1160
|
+
|
|
1161
|
+
properties = schema.get("properties", {})
|
|
1162
|
+
required = set(schema.get("required", []))
|
|
1163
|
+
optional = list(set(properties) - required)
|
|
1164
|
+
optional.sort()
|
|
1165
|
+
|
|
1166
|
+
# Generate combinations with required properties and one optional property
|
|
1167
|
+
for name in optional:
|
|
1168
|
+
combo = {k: v for k, v in template.items() if k in required or k == name}
|
|
1169
|
+
if combo != template:
|
|
1170
|
+
yield PositiveValue(
|
|
1171
|
+
combo,
|
|
1172
|
+
scenario=CoverageScenario.OBJECT_REQUIRED_AND_OPTIONAL,
|
|
1173
|
+
description=f"Object with all required properties and '{name}'",
|
|
1174
|
+
)
|
|
1175
|
+
# Generate one combination for each size from 2 to N-1
|
|
1176
|
+
for selection in select_combinations(optional):
|
|
1177
|
+
combo = {k: v for k, v in template.items() if k in required or k in selection}
|
|
1178
|
+
yield PositiveValue(
|
|
1179
|
+
combo,
|
|
1180
|
+
scenario=CoverageScenario.OBJECT_REQUIRED_AND_OPTIONAL,
|
|
1181
|
+
description="Object with all required and a subset of optional properties",
|
|
1182
|
+
)
|
|
1183
|
+
# Generate only required properties
|
|
1184
|
+
if set(properties) != required:
|
|
1185
|
+
only_required = {k: v for k, v in template.items() if k in required}
|
|
1186
|
+
yield PositiveValue(
|
|
1187
|
+
only_required,
|
|
1188
|
+
scenario=CoverageScenario.OBJECT_ONLY_REQUIRED,
|
|
1189
|
+
description="Object with only required properties",
|
|
1190
|
+
)
|
|
1191
|
+
seen = HashSet()
|
|
1192
|
+
for name, sub_schema in properties.items():
|
|
1193
|
+
seen.insert(template.get(name))
|
|
1194
|
+
for new in cover_schema_iter(ctx, sub_schema):
|
|
1195
|
+
if seen.insert(new.value):
|
|
1196
|
+
yield PositiveValue(
|
|
1197
|
+
{**template, name: new.value},
|
|
1198
|
+
scenario=CoverageScenario.VALID_OBJECT,
|
|
1199
|
+
description=f"Object with valid '{name}' value: {new.description}",
|
|
1200
|
+
)
|
|
1201
|
+
seen.clear()
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
def select_combinations(optional: list[str]) -> Iterator[tuple[str, ...]]:
|
|
1205
|
+
for size in range(2, len(optional)):
|
|
1206
|
+
yield next(combinations(optional, size))
|
|
1207
|
+
|
|
1208
|
+
|
|
1209
|
+
def _negative_enum(ctx: CoverageContext, value: list, seen: HashSet) -> Generator[GeneratedValue, None, None]:
|
|
1210
|
+
def is_not_in_value(x: Any) -> bool:
|
|
1211
|
+
if x in value or not ctx.is_valid_for_location(x):
|
|
1212
|
+
return False
|
|
1213
|
+
return seen.insert(x)
|
|
1214
|
+
|
|
1215
|
+
strategy = (
|
|
1216
|
+
st.text(alphabet=st.characters(min_codepoint=65, max_codepoint=122, categories=["L"]), min_size=3)
|
|
1217
|
+
| st.none()
|
|
1218
|
+
| st.booleans()
|
|
1219
|
+
| NUMERIC_STRATEGY
|
|
1220
|
+
).filter(is_not_in_value)
|
|
1221
|
+
yield NegativeValue(
|
|
1222
|
+
ctx.generate_from(strategy),
|
|
1223
|
+
scenario=CoverageScenario.INVALID_ENUM_VALUE,
|
|
1224
|
+
description="Invalid enum value",
|
|
1225
|
+
location=ctx.current_path,
|
|
1226
|
+
)
|
|
1227
|
+
|
|
1228
|
+
|
|
1229
|
+
def _negative_properties(
|
|
1230
|
+
ctx: CoverageContext, template: dict, properties: dict
|
|
1231
|
+
) -> Generator[GeneratedValue, None, None]:
|
|
1232
|
+
nctx = ctx.with_negative()
|
|
1233
|
+
for key, sub_schema in properties.items():
|
|
1234
|
+
with nctx.at(key):
|
|
1235
|
+
for value in cover_schema_iter(nctx, sub_schema):
|
|
1236
|
+
yield NegativeValue(
|
|
1237
|
+
{**template, key: value.value},
|
|
1238
|
+
scenario=value.scenario,
|
|
1239
|
+
description=f"Object with invalid '{key}' value: {value.description}",
|
|
1240
|
+
location=nctx.current_path,
|
|
1241
|
+
parameter=key,
|
|
1242
|
+
)
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
def _negative_pattern_properties(
|
|
1246
|
+
ctx: CoverageContext, template: dict, pattern_properties: dict
|
|
1247
|
+
) -> Generator[GeneratedValue, None, None]:
|
|
1248
|
+
nctx = ctx.with_negative()
|
|
1249
|
+
for pattern, sub_schema in pattern_properties.items():
|
|
1250
|
+
try:
|
|
1251
|
+
key = ctx.generate_from(st.from_regex(pattern))
|
|
1252
|
+
except re.error:
|
|
1253
|
+
continue
|
|
1254
|
+
with nctx.at(pattern):
|
|
1255
|
+
for value in cover_schema_iter(nctx, sub_schema):
|
|
1256
|
+
yield NegativeValue(
|
|
1257
|
+
{**template, key: value.value},
|
|
1258
|
+
scenario=value.scenario,
|
|
1259
|
+
description=f"Object with invalid pattern key '{key}' ('{pattern}') value: {value.description}",
|
|
1260
|
+
location=nctx.current_path,
|
|
1261
|
+
)
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
def _negative_items(ctx: CoverageContext, schema: dict[str, Any] | bool) -> Generator[GeneratedValue, None, None]:
|
|
1265
|
+
"""Arrays not matching the schema."""
|
|
1266
|
+
nctx = ctx.with_negative()
|
|
1267
|
+
for value in cover_schema_iter(nctx, schema):
|
|
1268
|
+
items = [value.value]
|
|
1269
|
+
if ctx.leads_to_negative_test_case(items):
|
|
1270
|
+
yield NegativeValue(
|
|
1271
|
+
items,
|
|
1272
|
+
scenario=value.scenario,
|
|
1273
|
+
description=f"Array with invalid items: {value.description}",
|
|
1274
|
+
location=nctx.current_path,
|
|
1275
|
+
)
|
|
1276
|
+
|
|
1277
|
+
|
|
1278
|
+
def _not_matching_pattern(value: str, pattern: re.Pattern) -> bool:
|
|
1279
|
+
return pattern.search(value) is None
|
|
1280
|
+
|
|
1281
|
+
|
|
1282
|
+
def _negative_pattern(
|
|
1283
|
+
ctx: CoverageContext, pattern: str, min_length: int | None = None, max_length: int | None = None
|
|
1284
|
+
) -> Generator[GeneratedValue, None, None]:
|
|
1285
|
+
try:
|
|
1286
|
+
compiled = re.compile(pattern)
|
|
1287
|
+
except re.error:
|
|
1288
|
+
return
|
|
1289
|
+
yield NegativeValue(
|
|
1290
|
+
ctx.generate_from(
|
|
1291
|
+
st.text(min_size=min_length or 0, max_size=max_length)
|
|
1292
|
+
.filter(partial(_not_matching_pattern, pattern=compiled))
|
|
1293
|
+
.filter(ctx.is_valid_for_location)
|
|
1294
|
+
),
|
|
1295
|
+
scenario=CoverageScenario.INVALID_PATTERN,
|
|
1296
|
+
description=f"Value not matching the '{pattern}' pattern",
|
|
1297
|
+
location=ctx.current_path,
|
|
1298
|
+
)
|
|
1299
|
+
|
|
1300
|
+
|
|
1301
|
+
def _with_negated_key(schema: dict, key: str, value: Any) -> dict:
|
|
1302
|
+
return {"allOf": [{k: v for k, v in schema.items() if k != key}, {"not": {key: value}}]}
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
def _negative_multiple_of(
|
|
1306
|
+
ctx: CoverageContext, schema: dict, multiple_of: int | float
|
|
1307
|
+
) -> Generator[GeneratedValue, None, None]:
|
|
1308
|
+
yield NegativeValue(
|
|
1309
|
+
ctx.generate_from_schema(_with_negated_key(schema, "multipleOf", multiple_of)),
|
|
1310
|
+
scenario=CoverageScenario.NOT_MULTIPLE_OF,
|
|
1311
|
+
description=f"Non-multiple of {multiple_of}",
|
|
1312
|
+
location=ctx.current_path,
|
|
1313
|
+
)
|
|
1314
|
+
|
|
1315
|
+
|
|
1316
|
+
def _negative_unique_items(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
|
|
1317
|
+
unique = jsonify(ctx.generate_from_schema({**schema, "type": "array", "minItems": 1, "maxItems": 1}))
|
|
1318
|
+
yield NegativeValue(
|
|
1319
|
+
unique + unique,
|
|
1320
|
+
scenario=CoverageScenario.NON_UNIQUE_ITEMS,
|
|
1321
|
+
description="Non-unique items",
|
|
1322
|
+
location=ctx.current_path,
|
|
1323
|
+
)
|
|
1324
|
+
|
|
1325
|
+
|
|
1326
|
+
def _negative_required(
|
|
1327
|
+
ctx: CoverageContext, template: dict, required: list[str]
|
|
1328
|
+
) -> Generator[GeneratedValue, None, None]:
|
|
1329
|
+
for key in required:
|
|
1330
|
+
yield NegativeValue(
|
|
1331
|
+
{k: v for k, v in template.items() if k != key},
|
|
1332
|
+
scenario=CoverageScenario.OBJECT_MISSING_REQUIRED_PROPERTY,
|
|
1333
|
+
description=f"Missing required property: {key}",
|
|
1334
|
+
location=ctx.current_path,
|
|
1335
|
+
parameter=key,
|
|
1336
|
+
)
|
|
1337
|
+
|
|
1338
|
+
|
|
1339
|
+
def _is_invalid_hostname(v: Any) -> bool:
|
|
1340
|
+
return v == "" or not jsonschema.Draft202012Validator.FORMAT_CHECKER.conforms(v, "hostname")
|
|
1341
|
+
|
|
1342
|
+
|
|
1343
|
+
def _is_invalid_format(v: Any, format: str) -> bool:
|
|
1344
|
+
return not jsonschema.Draft202012Validator.FORMAT_CHECKER.conforms(v, format)
|
|
1345
|
+
|
|
1346
|
+
|
|
1347
|
+
def _negative_format(ctx: CoverageContext, schema: dict, format: str) -> Generator[GeneratedValue, None, None]:
|
|
1348
|
+
# Hypothesis-jsonschema does not canonicalise it properly right now, which leads to unsatisfiable schema
|
|
1349
|
+
without_format = {k: v for k, v in schema.items() if k != "format"}
|
|
1350
|
+
without_format.setdefault("type", "string")
|
|
1351
|
+
if ctx.location == "path":
|
|
1352
|
+
# Empty path parameters are invalid
|
|
1353
|
+
without_format["minLength"] = 1
|
|
1354
|
+
strategy = from_schema(without_format)
|
|
1355
|
+
if format in jsonschema.Draft202012Validator.FORMAT_CHECKER.checkers:
|
|
1356
|
+
if format == "hostname":
|
|
1357
|
+
strategy = strategy.filter(_is_invalid_hostname)
|
|
1358
|
+
else:
|
|
1359
|
+
strategy = strategy.filter(functools.partial(_is_invalid_format, format=format))
|
|
1360
|
+
yield NegativeValue(
|
|
1361
|
+
ctx.generate_from(strategy),
|
|
1362
|
+
scenario=CoverageScenario.INVALID_FORMAT,
|
|
1363
|
+
description=f"Value not matching the '{format}' format",
|
|
1364
|
+
location=ctx.current_path,
|
|
1365
|
+
)
|
|
1366
|
+
|
|
1367
|
+
|
|
1368
|
+
def _is_non_integer_float(x: float) -> bool:
|
|
1369
|
+
return x != int(x)
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
def is_valid_header_value(value: Any) -> bool:
|
|
1373
|
+
value = str(value)
|
|
1374
|
+
if not is_latin_1_encodable(value):
|
|
1375
|
+
return False
|
|
1376
|
+
if has_invalid_characters("A", value):
|
|
1377
|
+
return False
|
|
1378
|
+
return True
|
|
1379
|
+
|
|
1380
|
+
|
|
1381
|
+
def jsonify(value: Any) -> Any:
|
|
1382
|
+
if isinstance(value, bool):
|
|
1383
|
+
return "true" if value else "false"
|
|
1384
|
+
elif value is None:
|
|
1385
|
+
return "null"
|
|
1386
|
+
|
|
1387
|
+
stack: list = [value]
|
|
1388
|
+
while stack:
|
|
1389
|
+
item = stack.pop()
|
|
1390
|
+
if isinstance(item, dict):
|
|
1391
|
+
for key, sub_item in item.items():
|
|
1392
|
+
if isinstance(sub_item, bool):
|
|
1393
|
+
item[key] = "true" if sub_item else "false"
|
|
1394
|
+
elif sub_item is None:
|
|
1395
|
+
item[key] = "null"
|
|
1396
|
+
elif isinstance(sub_item, dict):
|
|
1397
|
+
stack.append(sub_item)
|
|
1398
|
+
elif isinstance(sub_item, list):
|
|
1399
|
+
stack.extend(item)
|
|
1400
|
+
elif isinstance(item, list):
|
|
1401
|
+
for idx, sub_item in enumerate(item):
|
|
1402
|
+
if isinstance(sub_item, bool):
|
|
1403
|
+
item[idx] = "true" if sub_item else "false"
|
|
1404
|
+
elif sub_item is None:
|
|
1405
|
+
item[idx] = "null"
|
|
1406
|
+
else:
|
|
1407
|
+
stack.extend(item)
|
|
1408
|
+
return value
|
|
1409
|
+
|
|
1410
|
+
|
|
1411
|
+
def quote_path_parameter(value: Any) -> str:
|
|
1412
|
+
if isinstance(value, str):
|
|
1413
|
+
if value == ".":
|
|
1414
|
+
return "%2E"
|
|
1415
|
+
elif value == "..":
|
|
1416
|
+
return "%2E%2E"
|
|
1417
|
+
else:
|
|
1418
|
+
return quote_plus(value)
|
|
1419
|
+
if isinstance(value, list):
|
|
1420
|
+
return ",".join(map(str, value))
|
|
1421
|
+
return str(value)
|
|
1422
|
+
|
|
1423
|
+
|
|
1424
|
+
def _negative_type(
|
|
1425
|
+
ctx: CoverageContext, ty: str | list[str], seen: HashSet, schema: dict[str, Any]
|
|
1426
|
+
) -> Generator[GeneratedValue, None, None]:
|
|
1427
|
+
if isinstance(ty, str):
|
|
1428
|
+
types = [ty]
|
|
1429
|
+
else:
|
|
1430
|
+
types = ty
|
|
1431
|
+
strategies = {ty: strategy for ty, strategy in STRATEGIES_FOR_TYPE.items() if ty not in types}
|
|
1432
|
+
|
|
1433
|
+
filter_func = {
|
|
1434
|
+
"path": lambda x: not is_invalid_path_parameter(x),
|
|
1435
|
+
"header": is_valid_header_value,
|
|
1436
|
+
"cookie": is_valid_header_value,
|
|
1437
|
+
"query": lambda x: not contains_unicode_surrogate_pair(x),
|
|
1438
|
+
}.get(ctx.location)
|
|
1439
|
+
|
|
1440
|
+
if "number" in types:
|
|
1441
|
+
strategies.pop("integer", None)
|
|
1442
|
+
if "integer" in types:
|
|
1443
|
+
strategies["number"] = FLOAT_STRATEGY.filter(_is_non_integer_float)
|
|
1444
|
+
if ctx.location == ParameterLocation.QUERY:
|
|
1445
|
+
strategies.pop("object", None)
|
|
1446
|
+
if filter_func is not None:
|
|
1447
|
+
for ty, strategy in strategies.items():
|
|
1448
|
+
strategies[ty] = strategy.filter(filter_func)
|
|
1449
|
+
|
|
1450
|
+
pattern = schema.get("pattern")
|
|
1451
|
+
if pattern is not None:
|
|
1452
|
+
try:
|
|
1453
|
+
re.compile(pattern)
|
|
1454
|
+
except re.error:
|
|
1455
|
+
schema = schema.copy()
|
|
1456
|
+
del schema["pattern"]
|
|
1457
|
+
return
|
|
1458
|
+
|
|
1459
|
+
validator = ctx.validator_cls(
|
|
1460
|
+
schema,
|
|
1461
|
+
format_checker=jsonschema.Draft202012Validator.FORMAT_CHECKER,
|
|
1462
|
+
)
|
|
1463
|
+
is_valid = validator.is_valid
|
|
1464
|
+
try:
|
|
1465
|
+
is_valid(None)
|
|
1466
|
+
apply_validation = True
|
|
1467
|
+
except Exception:
|
|
1468
|
+
# Schema is not correct and we can't validate the generated instances.
|
|
1469
|
+
# In such a scenario it is better to generate at least something with some chances to have a false
|
|
1470
|
+
# positive failure
|
|
1471
|
+
apply_validation = False
|
|
1472
|
+
|
|
1473
|
+
def _does_not_match_the_original_schema(value: Any) -> bool:
|
|
1474
|
+
return not is_valid(str(value))
|
|
1475
|
+
|
|
1476
|
+
if ctx.location == ParameterLocation.PATH:
|
|
1477
|
+
for ty, strategy in strategies.items():
|
|
1478
|
+
strategies[ty] = strategy.map(jsonify).map(quote_path_parameter)
|
|
1479
|
+
elif ctx.location == ParameterLocation.QUERY:
|
|
1480
|
+
for ty, strategy in strategies.items():
|
|
1481
|
+
strategies[ty] = strategy.map(jsonify)
|
|
1482
|
+
|
|
1483
|
+
if apply_validation and ctx.will_be_serialized_to_string():
|
|
1484
|
+
for ty, strategy in strategies.items():
|
|
1485
|
+
strategies[ty] = strategy.filter(_does_not_match_the_original_schema)
|
|
1486
|
+
for strategy in strategies.values():
|
|
1487
|
+
value = ctx.generate_from(strategy)
|
|
1488
|
+
if seen.insert(value) and ctx.is_valid_for_location(value):
|
|
1489
|
+
yield NegativeValue(
|
|
1490
|
+
value, scenario=CoverageScenario.INCORRECT_TYPE, description="Incorrect type", location=ctx.current_path
|
|
1491
|
+
)
|
|
1492
|
+
|
|
1493
|
+
|
|
1494
|
+
def _flip_generation_mode_for_not(
|
|
1495
|
+
values: Generator[GeneratedValue, None, None],
|
|
1496
|
+
) -> Generator[GeneratedValue, None, None]:
|
|
1497
|
+
"""Flip generation mode for values from 'not' schemas.
|
|
1498
|
+
|
|
1499
|
+
For 'not' schemas, the semantic is inverted:
|
|
1500
|
+
- Positive values for the inner schema are negative for the outer schema
|
|
1501
|
+
- Negative values for the inner schema are positive for the outer schema
|
|
1502
|
+
"""
|
|
1503
|
+
for value in values:
|
|
1504
|
+
flipped_mode = (
|
|
1505
|
+
GenerationMode.NEGATIVE if value.generation_mode == GenerationMode.POSITIVE else GenerationMode.POSITIVE
|
|
1506
|
+
)
|
|
1507
|
+
yield GeneratedValue(
|
|
1508
|
+
value=value.value,
|
|
1509
|
+
generation_mode=flipped_mode,
|
|
1510
|
+
scenario=value.scenario,
|
|
1511
|
+
description=value.description,
|
|
1512
|
+
location=value.location,
|
|
1513
|
+
parameter=value.parameter,
|
|
1514
|
+
)
|
|
1515
|
+
|
|
1516
|
+
|
|
1517
|
+
def push_examples_to_properties(schema: dict[str, Any]) -> None:
|
|
1518
|
+
"""Push examples from the top-level 'examples' field to the corresponding properties."""
|
|
1519
|
+
if "examples" in schema and "properties" in schema:
|
|
1520
|
+
properties = schema["properties"]
|
|
1521
|
+
for example in schema["examples"]:
|
|
1522
|
+
if isinstance(example, dict):
|
|
1523
|
+
for prop, value in example.items():
|
|
1524
|
+
if prop in properties:
|
|
1525
|
+
if "examples" not in properties[prop]:
|
|
1526
|
+
properties[prop]["examples"] = []
|
|
1527
|
+
if value not in schema["properties"][prop]["examples"]:
|
|
1528
|
+
properties[prop]["examples"].append(value)
|