schemathesis 3.25.6__py3-none-any.whl → 4.0.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +27 -65
- schemathesis/auths.py +102 -82
- schemathesis/checks.py +126 -46
- schemathesis/cli/__init__.py +11 -1760
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +37 -0
- schemathesis/cli/commands/run/__init__.py +662 -0
- schemathesis/cli/commands/run/checks.py +80 -0
- schemathesis/cli/commands/run/context.py +117 -0
- schemathesis/cli/commands/run/events.py +35 -0
- schemathesis/cli/commands/run/executor.py +138 -0
- schemathesis/cli/commands/run/filters.py +194 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +746 -0
- schemathesis/cli/commands/run/hypothesis.py +105 -0
- schemathesis/cli/commands/run/loaders.py +129 -0
- schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
- schemathesis/cli/constants.py +5 -52
- schemathesis/cli/core.py +17 -0
- schemathesis/cli/ext/fs.py +14 -0
- schemathesis/cli/ext/groups.py +55 -0
- schemathesis/cli/{options.py → ext/options.py} +39 -10
- schemathesis/cli/hooks.py +36 -0
- schemathesis/contrib/__init__.py +1 -3
- schemathesis/contrib/openapi/__init__.py +1 -3
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
- schemathesis/core/__init__.py +58 -0
- schemathesis/core/compat.py +25 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +370 -0
- schemathesis/core/failures.py +285 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
- schemathesis/core/output/__init__.py +69 -0
- schemathesis/core/output/sanitization.py +197 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +31 -0
- schemathesis/{internal → core}/result.py +1 -1
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +108 -0
- schemathesis/core/validation.py +38 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +30 -0
- schemathesis/engine/config.py +59 -0
- schemathesis/engine/context.py +119 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +157 -0
- schemathesis/engine/errors.py +394 -0
- schemathesis/engine/events.py +337 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{runner → engine/phases}/probes.py +50 -67
- schemathesis/engine/phases/stateful/__init__.py +65 -0
- schemathesis/engine/phases/stateful/_executor.py +326 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +174 -0
- schemathesis/engine/phases/unit/_executor.py +321 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +241 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +18 -14
- schemathesis/filters.py +103 -14
- schemathesis/generation/__init__.py +21 -37
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/generation/hypothesis/__init__.py +30 -0
- schemathesis/generation/hypothesis/builder.py +585 -0
- schemathesis/generation/hypothesis/examples.py +50 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/modes.py +28 -0
- schemathesis/generation/overrides.py +96 -0
- schemathesis/generation/stateful/__init__.py +20 -0
- schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +115 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +99 -67
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +412 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +63 -0
- schemathesis/openapi/loaders.py +178 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +273 -0
- schemathesis/pytest/loaders.py +12 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -261
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +25 -0
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +7 -5
- schemathesis/specs/graphql/schemas.py +215 -187
- schemathesis/specs/graphql/validation.py +11 -18
- schemathesis/specs/openapi/__init__.py +7 -1
- schemathesis/specs/openapi/_cache.py +122 -0
- schemathesis/specs/openapi/_hypothesis.py +146 -165
- schemathesis/specs/openapi/checks.py +565 -67
- schemathesis/specs/openapi/converter.py +33 -6
- schemathesis/specs/openapi/definitions.py +11 -18
- schemathesis/specs/openapi/examples.py +139 -23
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +4 -6
- schemathesis/specs/openapi/expressions/extractors.py +23 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +38 -14
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +45 -0
- schemathesis/specs/openapi/links.py +65 -165
- schemathesis/specs/openapi/media_types.py +32 -0
- schemathesis/specs/openapi/negative/__init__.py +7 -3
- schemathesis/specs/openapi/negative/mutations.py +24 -8
- schemathesis/specs/openapi/parameters.py +46 -30
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +47 -57
- schemathesis/specs/openapi/schemas.py +478 -369
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +11 -6
- schemathesis/specs/openapi/stateful/__init__.py +185 -73
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/transport/__init__.py +104 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +99 -0
- schemathesis/transport/requests.py +221 -0
- schemathesis/{_xml.py → transport/serialization.py} +143 -28
- schemathesis/transport/wsgi.py +165 -0
- schemathesis-4.0.0a1.dist-info/METADATA +297 -0
- schemathesis-4.0.0a1.dist-info/RECORD +152 -0
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -17
- schemathesis/_hypothesis.py +0 -246
- schemathesis/_override.py +0 -49
- schemathesis/cli/cassettes.py +0 -375
- schemathesis/cli/context.py +0 -58
- schemathesis/cli/debug.py +0 -26
- schemathesis/cli/handlers.py +0 -16
- schemathesis/cli/junitxml.py +0 -43
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -790
- schemathesis/cli/output/short.py +0 -44
- schemathesis/cli/sanitization.py +0 -20
- schemathesis/code_samples.py +0 -149
- schemathesis/constants.py +0 -55
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -15
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -560
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -17
- schemathesis/failures.py +0 -209
- schemathesis/fixups/__init__.py +0 -36
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -29
- schemathesis/graphql.py +0 -4
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/copy.py +0 -13
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -34
- schemathesis/internal/jsonschema.py +0 -35
- schemathesis/internal/transformation.py +0 -15
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -361
- schemathesis/loaders.py +0 -120
- schemathesis/models.py +0 -1234
- schemathesis/parameters.py +0 -86
- schemathesis/runner/__init__.py +0 -570
- schemathesis/runner/events.py +0 -329
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -1035
- schemathesis/runner/impl/solo.py +0 -90
- schemathesis/runner/impl/threadpool.py +0 -400
- schemathesis/runner/serialization.py +0 -411
- schemathesis/sanitization.py +0 -248
- schemathesis/serializers.py +0 -323
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -201
- schemathesis/service/client.py +0 -100
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -57
- schemathesis/service/hosts.py +0 -107
- schemathesis/service/metadata.py +0 -46
- schemathesis/service/models.py +0 -49
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -199
- schemathesis/service/usage.py +0 -65
- schemathesis/specs/graphql/loaders.py +0 -344
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/loaders.py +0 -667
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis/specs/openapi/validation.py +0 -25
- schemathesis/stateful/__init__.py +0 -133
- schemathesis/targets.py +0 -45
- schemathesis/throttling.py +0 -41
- schemathesis/transports/__init__.py +0 -5
- schemathesis/transports/auth.py +0 -15
- schemathesis/transports/headers.py +0 -35
- schemathesis/transports/responses.py +0 -52
- schemathesis/types.py +0 -35
- schemathesis/utils.py +0 -169
- schemathesis-3.25.6.dist-info/METADATA +0 -356
- schemathesis-3.25.6.dist-info/RECORD +0 -134
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,931 @@
|
|
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
|
+
from json.encoder import _make_iterencode, c_make_encoder, encode_basestring_ascii # type: ignore
|
10
|
+
from typing import Any, Callable, Generator, Iterator, TypeVar, cast
|
11
|
+
|
12
|
+
import jsonschema
|
13
|
+
from hypothesis import strategies as st
|
14
|
+
from hypothesis.errors import InvalidArgument, Unsatisfiable
|
15
|
+
from hypothesis_jsonschema import from_schema
|
16
|
+
from hypothesis_jsonschema._canonicalise import canonicalish
|
17
|
+
from hypothesis_jsonschema._from_schema import STRING_FORMATS as BUILT_IN_STRING_FORMATS
|
18
|
+
|
19
|
+
from schemathesis.core import NOT_SET
|
20
|
+
from schemathesis.core.compat import RefResolutionError
|
21
|
+
from schemathesis.core.transforms import deepclone
|
22
|
+
from schemathesis.core.validation import has_invalid_characters, is_latin_1_encodable
|
23
|
+
from schemathesis.generation import GenerationMode
|
24
|
+
from schemathesis.generation.hypothesis import examples
|
25
|
+
|
26
|
+
from ..specs.openapi.converter import update_pattern_in_schema
|
27
|
+
from ..specs.openapi.formats import STRING_FORMATS, get_default_format_strategies
|
28
|
+
from ..specs.openapi.patterns import update_quantifier
|
29
|
+
|
30
|
+
|
31
|
+
def _replace_zero_with_nonzero(x: float) -> float:
|
32
|
+
return x or 0.0
|
33
|
+
|
34
|
+
|
35
|
+
def json_recursive_strategy(strategy: st.SearchStrategy) -> st.SearchStrategy:
|
36
|
+
return st.lists(strategy, max_size=3) | st.dictionaries(st.text(), strategy, max_size=3)
|
37
|
+
|
38
|
+
|
39
|
+
BUFFER_SIZE = 8 * 1024
|
40
|
+
FLOAT_STRATEGY: st.SearchStrategy = st.floats(allow_nan=False, allow_infinity=False).map(_replace_zero_with_nonzero)
|
41
|
+
NUMERIC_STRATEGY: st.SearchStrategy = st.integers() | FLOAT_STRATEGY
|
42
|
+
JSON_STRATEGY: st.SearchStrategy = st.recursive(
|
43
|
+
st.none() | st.booleans() | NUMERIC_STRATEGY | st.text(), json_recursive_strategy
|
44
|
+
)
|
45
|
+
ARRAY_STRATEGY: st.SearchStrategy = st.lists(JSON_STRATEGY, min_size=2)
|
46
|
+
OBJECT_STRATEGY: st.SearchStrategy = st.dictionaries(st.text(), JSON_STRATEGY)
|
47
|
+
|
48
|
+
|
49
|
+
STRATEGIES_FOR_TYPE = {
|
50
|
+
"integer": st.integers(),
|
51
|
+
"number": NUMERIC_STRATEGY,
|
52
|
+
"boolean": st.booleans(),
|
53
|
+
"null": st.none(),
|
54
|
+
"string": st.text(),
|
55
|
+
"array": ARRAY_STRATEGY,
|
56
|
+
"object": OBJECT_STRATEGY,
|
57
|
+
}
|
58
|
+
FORMAT_STRATEGIES = {**BUILT_IN_STRING_FORMATS, **get_default_format_strategies(), **STRING_FORMATS}
|
59
|
+
|
60
|
+
UNKNOWN_PROPERTY_KEY = "x-schemathesis-unknown-property"
|
61
|
+
UNKNOWN_PROPERTY_VALUE = 42
|
62
|
+
|
63
|
+
|
64
|
+
@dataclass
|
65
|
+
class GeneratedValue:
|
66
|
+
value: Any
|
67
|
+
generation_mode: GenerationMode
|
68
|
+
description: str
|
69
|
+
parameter: str | None
|
70
|
+
location: str | None
|
71
|
+
|
72
|
+
__slots__ = ("value", "generation_mode", "description", "parameter", "location")
|
73
|
+
|
74
|
+
@classmethod
|
75
|
+
def with_positive(cls, value: Any, *, description: str) -> GeneratedValue:
|
76
|
+
return cls(
|
77
|
+
value=value,
|
78
|
+
generation_mode=GenerationMode.POSITIVE,
|
79
|
+
description=description,
|
80
|
+
location=None,
|
81
|
+
parameter=None,
|
82
|
+
)
|
83
|
+
|
84
|
+
@classmethod
|
85
|
+
def with_negative(
|
86
|
+
cls, value: Any, *, description: str, location: str, parameter: str | None = None
|
87
|
+
) -> GeneratedValue:
|
88
|
+
return cls(
|
89
|
+
value=value,
|
90
|
+
generation_mode=GenerationMode.NEGATIVE,
|
91
|
+
description=description,
|
92
|
+
location=location,
|
93
|
+
parameter=parameter,
|
94
|
+
)
|
95
|
+
|
96
|
+
|
97
|
+
PositiveValue = GeneratedValue.with_positive
|
98
|
+
NegativeValue = GeneratedValue.with_negative
|
99
|
+
|
100
|
+
|
101
|
+
@lru_cache(maxsize=128)
|
102
|
+
def cached_draw(strategy: st.SearchStrategy) -> Any:
|
103
|
+
return examples.generate_one(strategy)
|
104
|
+
|
105
|
+
|
106
|
+
@dataclass
|
107
|
+
class CoverageContext:
|
108
|
+
generation_modes: list[GenerationMode]
|
109
|
+
location: str
|
110
|
+
path: list[str | int]
|
111
|
+
|
112
|
+
__slots__ = ("location", "generation_modes", "path")
|
113
|
+
|
114
|
+
def __init__(
|
115
|
+
self,
|
116
|
+
*,
|
117
|
+
location: str,
|
118
|
+
generation_modes: list[GenerationMode] | None = None,
|
119
|
+
path: list[str | int] | None = None,
|
120
|
+
) -> None:
|
121
|
+
self.location = location
|
122
|
+
self.generation_modes = generation_modes if generation_modes is not None else GenerationMode.all()
|
123
|
+
self.path = path or []
|
124
|
+
|
125
|
+
@contextmanager
|
126
|
+
def at(self, key: str | int) -> Generator[None, None, None]:
|
127
|
+
self.path.append(key)
|
128
|
+
try:
|
129
|
+
yield
|
130
|
+
finally:
|
131
|
+
self.path.pop()
|
132
|
+
|
133
|
+
@property
|
134
|
+
def current_path(self) -> str:
|
135
|
+
return "/" + "/".join(str(key) for key in self.path)
|
136
|
+
|
137
|
+
def with_positive(self) -> CoverageContext:
|
138
|
+
return CoverageContext(
|
139
|
+
location=self.location,
|
140
|
+
generation_modes=[GenerationMode.POSITIVE],
|
141
|
+
path=self.path,
|
142
|
+
)
|
143
|
+
|
144
|
+
def with_negative(self) -> CoverageContext:
|
145
|
+
return CoverageContext(
|
146
|
+
location=self.location,
|
147
|
+
generation_modes=[GenerationMode.NEGATIVE],
|
148
|
+
path=self.path,
|
149
|
+
)
|
150
|
+
|
151
|
+
def is_valid_for_location(self, value: Any) -> bool:
|
152
|
+
if self.location in ("header", "cookie") and isinstance(value, str):
|
153
|
+
return is_latin_1_encodable(value) and not has_invalid_characters("", value)
|
154
|
+
return True
|
155
|
+
|
156
|
+
def generate_from(self, strategy: st.SearchStrategy) -> Any:
|
157
|
+
return cached_draw(strategy)
|
158
|
+
|
159
|
+
def generate_from_schema(self, schema: dict | bool) -> Any:
|
160
|
+
if isinstance(schema, bool):
|
161
|
+
return 0
|
162
|
+
keys = sorted([k for k in schema if not k.startswith("x-") and k not in ["description", "example"]])
|
163
|
+
if keys == ["type"] and isinstance(schema["type"], str) and schema["type"] in STRATEGIES_FOR_TYPE:
|
164
|
+
return cached_draw(STRATEGIES_FOR_TYPE[schema["type"]])
|
165
|
+
if keys == ["format", "type"]:
|
166
|
+
if schema["type"] != "string":
|
167
|
+
return cached_draw(STRATEGIES_FOR_TYPE[schema["type"]])
|
168
|
+
elif schema["format"] in FORMAT_STRATEGIES:
|
169
|
+
return cached_draw(FORMAT_STRATEGIES[schema["format"]])
|
170
|
+
if (keys == ["maxLength", "minLength", "type"] or keys == ["maxLength", "type"]) and schema["type"] == "string":
|
171
|
+
return cached_draw(st.text(min_size=schema.get("minLength", 0), max_size=schema["maxLength"]))
|
172
|
+
if (
|
173
|
+
keys == ["properties", "required", "type"]
|
174
|
+
or keys == ["properties", "required"]
|
175
|
+
or keys == ["properties", "type"]
|
176
|
+
or keys == ["properties"]
|
177
|
+
):
|
178
|
+
obj = {}
|
179
|
+
for key, sub_schema in schema["properties"].items():
|
180
|
+
if isinstance(sub_schema, dict) and "const" in sub_schema:
|
181
|
+
obj[key] = sub_schema["const"]
|
182
|
+
else:
|
183
|
+
obj[key] = self.generate_from_schema(sub_schema)
|
184
|
+
return obj
|
185
|
+
if (
|
186
|
+
keys == ["maximum", "minimum", "type"] or keys == ["maximum", "type"] or keys == ["minimum", "type"]
|
187
|
+
) and schema["type"] == "integer":
|
188
|
+
return cached_draw(st.integers(min_value=schema.get("minimum"), max_value=schema.get("maximum")))
|
189
|
+
if "enum" in schema:
|
190
|
+
return cached_draw(st.sampled_from(schema["enum"]))
|
191
|
+
if "pattern" in schema:
|
192
|
+
pattern = schema["pattern"]
|
193
|
+
try:
|
194
|
+
re.compile(pattern)
|
195
|
+
except re.error:
|
196
|
+
raise Unsatisfiable from None
|
197
|
+
return cached_draw(st.from_regex(pattern))
|
198
|
+
if (keys == ["items", "type"] or keys == ["items", "minItems", "type"]) and isinstance(schema["items"], dict):
|
199
|
+
items = schema["items"]
|
200
|
+
min_items = schema.get("minItems", 0)
|
201
|
+
if "enum" in items:
|
202
|
+
return cached_draw(st.lists(st.sampled_from(items["enum"]), min_size=min_items))
|
203
|
+
sub_keys = sorted([k for k in items if not k.startswith("x-") and k not in ["description", "example"]])
|
204
|
+
if sub_keys == ["type"] and items["type"] == "string":
|
205
|
+
return cached_draw(st.lists(st.text(), min_size=min_items))
|
206
|
+
if (
|
207
|
+
sub_keys == ["properties", "required", "type"]
|
208
|
+
or sub_keys == ["properties", "type"]
|
209
|
+
or sub_keys == ["properties"]
|
210
|
+
):
|
211
|
+
return cached_draw(
|
212
|
+
st.lists(
|
213
|
+
st.fixed_dictionaries(
|
214
|
+
{key: from_schema(sub_schema) for key, sub_schema in items["properties"].items()}
|
215
|
+
),
|
216
|
+
min_size=min_items,
|
217
|
+
)
|
218
|
+
)
|
219
|
+
|
220
|
+
if keys == ["allOf"]:
|
221
|
+
schema = canonicalish(schema)
|
222
|
+
if isinstance(schema, dict) and "allOf" not in schema:
|
223
|
+
return self.generate_from_schema(schema)
|
224
|
+
|
225
|
+
return self.generate_from(from_schema(schema))
|
226
|
+
|
227
|
+
|
228
|
+
T = TypeVar("T")
|
229
|
+
|
230
|
+
|
231
|
+
if c_make_encoder is not None:
|
232
|
+
_iterencode = c_make_encoder(None, None, encode_basestring_ascii, None, ":", ",", True, False, False)
|
233
|
+
else:
|
234
|
+
_iterencode = _make_iterencode(
|
235
|
+
None, None, encode_basestring_ascii, None, float.__repr__, ":", ",", True, False, True
|
236
|
+
)
|
237
|
+
|
238
|
+
|
239
|
+
def _encode(o: Any) -> str:
|
240
|
+
return "".join(_iterencode(o, 0))
|
241
|
+
|
242
|
+
|
243
|
+
def _to_hashable_key(value: T, _encode: Callable = _encode) -> tuple[type, str | T]:
|
244
|
+
if isinstance(value, (dict, list)):
|
245
|
+
serialized = _encode(value)
|
246
|
+
return (type(value), serialized)
|
247
|
+
return (type(value), value)
|
248
|
+
|
249
|
+
|
250
|
+
def _cover_positive_for_type(
|
251
|
+
ctx: CoverageContext, schema: dict, ty: str | None
|
252
|
+
) -> Generator[GeneratedValue, None, None]:
|
253
|
+
if ty == "object" or ty == "array":
|
254
|
+
template_schema = _get_template_schema(schema, ty)
|
255
|
+
template = ctx.generate_from_schema(template_schema)
|
256
|
+
else:
|
257
|
+
template = None
|
258
|
+
if GenerationMode.POSITIVE in ctx.generation_modes:
|
259
|
+
ctx = ctx.with_positive()
|
260
|
+
enum = schema.get("enum", NOT_SET)
|
261
|
+
const = schema.get("const", NOT_SET)
|
262
|
+
for key in ("anyOf", "oneOf"):
|
263
|
+
sub_schemas = schema.get(key)
|
264
|
+
if sub_schemas is not None:
|
265
|
+
for sub_schema in sub_schemas:
|
266
|
+
yield from cover_schema_iter(ctx, sub_schema)
|
267
|
+
all_of = schema.get("allOf")
|
268
|
+
if all_of is not None:
|
269
|
+
if len(all_of) == 1:
|
270
|
+
yield from cover_schema_iter(ctx, all_of[0])
|
271
|
+
else:
|
272
|
+
with suppress(jsonschema.SchemaError):
|
273
|
+
canonical = canonicalish(schema)
|
274
|
+
yield from cover_schema_iter(ctx, canonical)
|
275
|
+
if enum is not NOT_SET:
|
276
|
+
for value in enum:
|
277
|
+
yield PositiveValue(value, description="Enum value")
|
278
|
+
elif const is not NOT_SET:
|
279
|
+
yield PositiveValue(const, description="Const value")
|
280
|
+
elif ty is not None:
|
281
|
+
if ty == "null":
|
282
|
+
yield PositiveValue(None, description="Value null value")
|
283
|
+
elif ty == "boolean":
|
284
|
+
yield PositiveValue(True, description="Valid boolean value")
|
285
|
+
yield PositiveValue(False, description="Valid boolean value")
|
286
|
+
elif ty == "string":
|
287
|
+
yield from _positive_string(ctx, schema)
|
288
|
+
elif ty == "integer" or ty == "number":
|
289
|
+
yield from _positive_number(ctx, schema)
|
290
|
+
elif ty == "array":
|
291
|
+
yield from _positive_array(ctx, schema, cast(list, template))
|
292
|
+
elif ty == "object":
|
293
|
+
yield from _positive_object(ctx, schema, cast(dict, template))
|
294
|
+
|
295
|
+
|
296
|
+
@contextmanager
|
297
|
+
def _ignore_unfixable(
|
298
|
+
*,
|
299
|
+
# Cache exception types here as `jsonschema` uses a custom `__getattr__` on the module level
|
300
|
+
# and it may cause errors during the interpreter shutdown
|
301
|
+
ref_error: type[Exception] = RefResolutionError,
|
302
|
+
schema_error: type[Exception] = jsonschema.SchemaError,
|
303
|
+
) -> Generator:
|
304
|
+
try:
|
305
|
+
yield
|
306
|
+
except (Unsatisfiable, ref_error, schema_error):
|
307
|
+
pass
|
308
|
+
except InvalidArgument as exc:
|
309
|
+
message = str(exc)
|
310
|
+
if "Cannot create non-empty" not in message and "is not in the specified alphabet" not in message:
|
311
|
+
raise
|
312
|
+
except TypeError as exc:
|
313
|
+
if "first argument must be string or compiled pattern" not in str(exc):
|
314
|
+
raise
|
315
|
+
|
316
|
+
|
317
|
+
def cover_schema_iter(
|
318
|
+
ctx: CoverageContext, schema: dict | bool, seen: set[Any | tuple[type, str]] | None = None
|
319
|
+
) -> Generator[GeneratedValue, None, None]:
|
320
|
+
if seen is None:
|
321
|
+
seen = set()
|
322
|
+
if isinstance(schema, bool):
|
323
|
+
types = ["null", "boolean", "string", "number", "array", "object"]
|
324
|
+
schema = {}
|
325
|
+
else:
|
326
|
+
types = schema.get("type", [])
|
327
|
+
push_examples_to_properties(schema)
|
328
|
+
if not isinstance(types, list):
|
329
|
+
types = [types] # type: ignore[unreachable]
|
330
|
+
if not types:
|
331
|
+
with _ignore_unfixable():
|
332
|
+
yield from _cover_positive_for_type(ctx, schema, None)
|
333
|
+
for ty in types:
|
334
|
+
with _ignore_unfixable():
|
335
|
+
yield from _cover_positive_for_type(ctx, schema, ty)
|
336
|
+
if GenerationMode.NEGATIVE in ctx.generation_modes:
|
337
|
+
template = None
|
338
|
+
for key, value in schema.items():
|
339
|
+
with _ignore_unfixable(), ctx.at(key):
|
340
|
+
if key == "enum":
|
341
|
+
yield from _negative_enum(ctx, value, seen)
|
342
|
+
elif key == "const":
|
343
|
+
for value_ in _negative_enum(ctx, [value], seen):
|
344
|
+
k = _to_hashable_key(value_.value)
|
345
|
+
if k not in seen:
|
346
|
+
yield value_
|
347
|
+
seen.add(k)
|
348
|
+
elif key == "type":
|
349
|
+
yield from _negative_type(ctx, seen, value)
|
350
|
+
elif key == "properties":
|
351
|
+
template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
|
352
|
+
yield from _negative_properties(ctx, template, value)
|
353
|
+
elif key == "patternProperties":
|
354
|
+
template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
|
355
|
+
yield from _negative_pattern_properties(ctx, template, value)
|
356
|
+
elif key == "items" and isinstance(value, dict):
|
357
|
+
yield from _negative_items(ctx, value)
|
358
|
+
elif key == "pattern":
|
359
|
+
min_length = schema.get("minLength")
|
360
|
+
max_length = schema.get("maxLength")
|
361
|
+
yield from _negative_pattern(ctx, value, min_length=min_length, max_length=max_length)
|
362
|
+
elif key == "format" and ("string" in types or not types):
|
363
|
+
yield from _negative_format(ctx, schema, value)
|
364
|
+
elif key == "maximum":
|
365
|
+
next = value + 1
|
366
|
+
if next not in seen:
|
367
|
+
yield NegativeValue(next, description="Value greater than maximum", location=ctx.current_path)
|
368
|
+
seen.add(next)
|
369
|
+
elif key == "minimum":
|
370
|
+
next = value - 1
|
371
|
+
if next not in seen:
|
372
|
+
yield NegativeValue(next, description="Value smaller than minimum", location=ctx.current_path)
|
373
|
+
seen.add(next)
|
374
|
+
elif key == "exclusiveMaximum" or key == "exclusiveMinimum" and value not in seen:
|
375
|
+
verb = "greater" if key == "exclusiveMaximum" else "smaller"
|
376
|
+
limit = "maximum" if key == "exclusiveMaximum" else "minimum"
|
377
|
+
yield NegativeValue(value, description=f"Value {verb} than {limit}", location=ctx.current_path)
|
378
|
+
seen.add(value)
|
379
|
+
elif key == "multipleOf":
|
380
|
+
for value_ in _negative_multiple_of(ctx, schema, value):
|
381
|
+
k = _to_hashable_key(value_.value)
|
382
|
+
if k not in seen:
|
383
|
+
yield value_
|
384
|
+
seen.add(k)
|
385
|
+
elif key == "minLength" and 0 < value < BUFFER_SIZE:
|
386
|
+
with suppress(InvalidArgument):
|
387
|
+
min_length = max_length = value - 1
|
388
|
+
new_schema = {**schema, "minLength": min_length, "maxLength": max_length}
|
389
|
+
new_schema.setdefault("type", "string")
|
390
|
+
if "pattern" in new_schema:
|
391
|
+
new_schema["pattern"] = update_quantifier(schema["pattern"], min_length, max_length)
|
392
|
+
if new_schema["pattern"] == schema["pattern"]:
|
393
|
+
# Pattern wasn't updated, try to generate a valid value then shrink the string to the required length
|
394
|
+
del new_schema["minLength"]
|
395
|
+
del new_schema["maxLength"]
|
396
|
+
value = ctx.generate_from_schema(new_schema)[:max_length]
|
397
|
+
else:
|
398
|
+
value = ctx.generate_from_schema(new_schema)
|
399
|
+
else:
|
400
|
+
value = ctx.generate_from_schema(new_schema)
|
401
|
+
k = _to_hashable_key(value)
|
402
|
+
if k not in seen:
|
403
|
+
yield NegativeValue(
|
404
|
+
value, description="String smaller than minLength", location=ctx.current_path
|
405
|
+
)
|
406
|
+
seen.add(k)
|
407
|
+
elif key == "maxLength" and value < BUFFER_SIZE:
|
408
|
+
try:
|
409
|
+
min_length = max_length = value + 1
|
410
|
+
new_schema = {**schema, "minLength": min_length, "maxLength": max_length}
|
411
|
+
new_schema.setdefault("type", "string")
|
412
|
+
if "pattern" in new_schema:
|
413
|
+
new_schema["pattern"] = update_quantifier(schema["pattern"], min_length, max_length)
|
414
|
+
if new_schema["pattern"] == schema["pattern"]:
|
415
|
+
# Pattern wasn't updated, try to generate a valid value then extend the string to the required length
|
416
|
+
del new_schema["minLength"]
|
417
|
+
del new_schema["maxLength"]
|
418
|
+
value = ctx.generate_from_schema(new_schema).ljust(max_length, "0")
|
419
|
+
else:
|
420
|
+
value = ctx.generate_from_schema(new_schema)
|
421
|
+
else:
|
422
|
+
value = ctx.generate_from_schema(new_schema)
|
423
|
+
k = _to_hashable_key(value)
|
424
|
+
if k not in seen:
|
425
|
+
yield NegativeValue(
|
426
|
+
value, description="String larger than maxLength", location=ctx.current_path
|
427
|
+
)
|
428
|
+
seen.add(k)
|
429
|
+
except (InvalidArgument, Unsatisfiable):
|
430
|
+
pass
|
431
|
+
elif key == "uniqueItems" and value:
|
432
|
+
yield from _negative_unique_items(ctx, schema)
|
433
|
+
elif key == "required":
|
434
|
+
template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
|
435
|
+
yield from _negative_required(ctx, template, value)
|
436
|
+
elif key == "additionalProperties" and not value and "pattern" not in schema:
|
437
|
+
template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
|
438
|
+
yield NegativeValue(
|
439
|
+
{**template, UNKNOWN_PROPERTY_KEY: UNKNOWN_PROPERTY_VALUE},
|
440
|
+
description="Object with unexpected properties",
|
441
|
+
location=ctx.current_path,
|
442
|
+
)
|
443
|
+
elif key == "allOf":
|
444
|
+
nctx = ctx.with_negative()
|
445
|
+
if len(value) == 1:
|
446
|
+
with nctx.at(0):
|
447
|
+
yield from cover_schema_iter(nctx, value[0], seen)
|
448
|
+
else:
|
449
|
+
with _ignore_unfixable():
|
450
|
+
canonical = canonicalish(schema)
|
451
|
+
yield from cover_schema_iter(nctx, canonical, seen)
|
452
|
+
elif key == "anyOf" or key == "oneOf":
|
453
|
+
nctx = ctx.with_negative()
|
454
|
+
# NOTE: Other sub-schemas are not filtered out
|
455
|
+
for idx, sub_schema in enumerate(value):
|
456
|
+
with nctx.at(idx):
|
457
|
+
yield from cover_schema_iter(nctx, sub_schema, seen)
|
458
|
+
|
459
|
+
|
460
|
+
def _get_properties(schema: dict | bool) -> dict | bool:
|
461
|
+
if isinstance(schema, dict):
|
462
|
+
if "example" in schema:
|
463
|
+
return {"const": schema["example"]}
|
464
|
+
if "default" in schema:
|
465
|
+
return {"const": schema["default"]}
|
466
|
+
if schema.get("examples"):
|
467
|
+
return {"enum": schema["examples"]}
|
468
|
+
if schema.get("type") == "object":
|
469
|
+
return _get_template_schema(schema, "object")
|
470
|
+
_schema = deepclone(schema)
|
471
|
+
update_pattern_in_schema(_schema)
|
472
|
+
return _schema
|
473
|
+
return schema
|
474
|
+
|
475
|
+
|
476
|
+
def _get_template_schema(schema: dict, ty: str) -> dict:
|
477
|
+
if ty == "object":
|
478
|
+
properties = schema.get("properties")
|
479
|
+
if properties is not None:
|
480
|
+
return {
|
481
|
+
**schema,
|
482
|
+
"required": list(properties),
|
483
|
+
"type": ty,
|
484
|
+
"properties": {k: _get_properties(v) for k, v in properties.items()},
|
485
|
+
}
|
486
|
+
return {**schema, "type": ty}
|
487
|
+
|
488
|
+
|
489
|
+
def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
|
490
|
+
"""Generate positive string values."""
|
491
|
+
# Boundary and near boundary values
|
492
|
+
min_length = schema.get("minLength")
|
493
|
+
if min_length == 0:
|
494
|
+
min_length = None
|
495
|
+
max_length = schema.get("maxLength")
|
496
|
+
example = schema.get("example")
|
497
|
+
examples = schema.get("examples")
|
498
|
+
default = schema.get("default")
|
499
|
+
if example or examples or default:
|
500
|
+
if example and ctx.is_valid_for_location(example):
|
501
|
+
yield PositiveValue(example, description="Example value")
|
502
|
+
if examples:
|
503
|
+
for example in examples:
|
504
|
+
if ctx.is_valid_for_location(example):
|
505
|
+
yield PositiveValue(example, description="Example value")
|
506
|
+
if (
|
507
|
+
default
|
508
|
+
and not (example is not None and default == example)
|
509
|
+
and not (examples is not None and any(default == ex for ex in examples))
|
510
|
+
and ctx.is_valid_for_location(default)
|
511
|
+
):
|
512
|
+
yield PositiveValue(default, description="Default value")
|
513
|
+
elif not min_length and not max_length:
|
514
|
+
# Default positive value
|
515
|
+
yield PositiveValue(ctx.generate_from_schema(schema), description="Valid string")
|
516
|
+
elif "pattern" in schema:
|
517
|
+
# Without merging `maxLength` & `minLength` into a regex it is problematic
|
518
|
+
# to generate a valid value as the unredlying machinery will resort to filtering
|
519
|
+
# and it is unlikely that it will generate a string of that length
|
520
|
+
yield PositiveValue(ctx.generate_from_schema(schema), description="Valid string")
|
521
|
+
return
|
522
|
+
|
523
|
+
seen = set()
|
524
|
+
|
525
|
+
if min_length is not None and min_length < BUFFER_SIZE:
|
526
|
+
# Exactly the minimum length
|
527
|
+
yield PositiveValue(
|
528
|
+
ctx.generate_from_schema({**schema, "maxLength": min_length}), description="Minimum length string"
|
529
|
+
)
|
530
|
+
seen.add(min_length)
|
531
|
+
|
532
|
+
# One character more than minimum if possible
|
533
|
+
larger = min_length + 1
|
534
|
+
if larger < BUFFER_SIZE and larger not in seen and (not max_length or larger <= max_length):
|
535
|
+
yield PositiveValue(
|
536
|
+
ctx.generate_from_schema({**schema, "minLength": larger, "maxLength": larger}),
|
537
|
+
description="Near-boundary length string",
|
538
|
+
)
|
539
|
+
seen.add(larger)
|
540
|
+
|
541
|
+
if max_length is not None:
|
542
|
+
# Exactly the maximum length
|
543
|
+
if max_length < BUFFER_SIZE and max_length not in seen:
|
544
|
+
yield PositiveValue(
|
545
|
+
ctx.generate_from_schema({**schema, "minLength": max_length}), description="Maximum length string"
|
546
|
+
)
|
547
|
+
seen.add(max_length)
|
548
|
+
|
549
|
+
# One character less than maximum if possible
|
550
|
+
smaller = max_length - 1
|
551
|
+
if (
|
552
|
+
smaller < BUFFER_SIZE
|
553
|
+
and smaller not in seen
|
554
|
+
and (smaller > 0 and (min_length is None or smaller >= min_length))
|
555
|
+
):
|
556
|
+
yield PositiveValue(
|
557
|
+
ctx.generate_from_schema({**schema, "minLength": smaller, "maxLength": smaller}),
|
558
|
+
description="Near-boundary length string",
|
559
|
+
)
|
560
|
+
seen.add(smaller)
|
561
|
+
|
562
|
+
|
563
|
+
def closest_multiple_greater_than(y: int, x: int) -> int:
|
564
|
+
"""Find the closest multiple of X that is greater than Y."""
|
565
|
+
quotient, remainder = divmod(y, x)
|
566
|
+
if remainder == 0:
|
567
|
+
return y
|
568
|
+
return x * (quotient + 1)
|
569
|
+
|
570
|
+
|
571
|
+
def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
|
572
|
+
"""Generate positive integer values."""
|
573
|
+
# Boundary and near boundary values
|
574
|
+
minimum = schema.get("minimum")
|
575
|
+
maximum = schema.get("maximum")
|
576
|
+
exclusive_minimum = schema.get("exclusiveMinimum")
|
577
|
+
exclusive_maximum = schema.get("exclusiveMaximum")
|
578
|
+
if exclusive_minimum is not None:
|
579
|
+
minimum = exclusive_minimum + 1
|
580
|
+
if exclusive_maximum is not None:
|
581
|
+
maximum = exclusive_maximum - 1
|
582
|
+
multiple_of = schema.get("multipleOf")
|
583
|
+
example = schema.get("example")
|
584
|
+
examples = schema.get("examples")
|
585
|
+
default = schema.get("default")
|
586
|
+
|
587
|
+
if example or examples or default:
|
588
|
+
if example:
|
589
|
+
yield PositiveValue(example, description="Example value")
|
590
|
+
if examples:
|
591
|
+
for example in examples:
|
592
|
+
yield PositiveValue(example, description="Example value")
|
593
|
+
if (
|
594
|
+
default
|
595
|
+
and not (example is not None and default == example)
|
596
|
+
and not (examples is not None and any(default == ex for ex in examples))
|
597
|
+
):
|
598
|
+
yield PositiveValue(default, description="Default value")
|
599
|
+
elif not minimum and not maximum:
|
600
|
+
# Default positive value
|
601
|
+
yield PositiveValue(ctx.generate_from_schema(schema), description="Valid number")
|
602
|
+
|
603
|
+
seen = set()
|
604
|
+
|
605
|
+
if minimum is not None:
|
606
|
+
# Exactly the minimum
|
607
|
+
if multiple_of is not None:
|
608
|
+
smallest = closest_multiple_greater_than(minimum, multiple_of)
|
609
|
+
else:
|
610
|
+
smallest = minimum
|
611
|
+
seen.add(smallest)
|
612
|
+
yield PositiveValue(smallest, description="Minimum value")
|
613
|
+
|
614
|
+
# One more than minimum if possible
|
615
|
+
if multiple_of is not None:
|
616
|
+
larger = smallest + multiple_of
|
617
|
+
else:
|
618
|
+
larger = minimum + 1
|
619
|
+
if larger not in seen and (not maximum or larger <= maximum):
|
620
|
+
seen.add(larger)
|
621
|
+
yield PositiveValue(larger, description="Near-boundary number")
|
622
|
+
|
623
|
+
if maximum is not None:
|
624
|
+
# Exactly the maximum
|
625
|
+
if multiple_of is not None:
|
626
|
+
largest = maximum - (maximum % multiple_of)
|
627
|
+
else:
|
628
|
+
largest = maximum
|
629
|
+
if largest not in seen:
|
630
|
+
seen.add(largest)
|
631
|
+
yield PositiveValue(largest, description="Maximum value")
|
632
|
+
|
633
|
+
# One less than maximum if possible
|
634
|
+
if multiple_of is not None:
|
635
|
+
smaller = largest - multiple_of
|
636
|
+
else:
|
637
|
+
smaller = maximum - 1
|
638
|
+
if smaller not in seen and (smaller > 0 and (minimum is None or smaller >= minimum)):
|
639
|
+
seen.add(smaller)
|
640
|
+
yield PositiveValue(smaller, description="Near-boundary number")
|
641
|
+
|
642
|
+
|
643
|
+
def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Generator[GeneratedValue, None, None]:
|
644
|
+
seen = set()
|
645
|
+
example = schema.get("example")
|
646
|
+
examples = schema.get("examples")
|
647
|
+
default = schema.get("default")
|
648
|
+
|
649
|
+
if example or examples or default:
|
650
|
+
if example:
|
651
|
+
yield PositiveValue(example, description="Example value")
|
652
|
+
if examples:
|
653
|
+
for example in examples:
|
654
|
+
yield PositiveValue(example, description="Example value")
|
655
|
+
if (
|
656
|
+
default
|
657
|
+
and not (example is not None and default == example)
|
658
|
+
and not (examples is not None and any(default == ex for ex in examples))
|
659
|
+
):
|
660
|
+
yield PositiveValue(default, description="Default value")
|
661
|
+
else:
|
662
|
+
yield PositiveValue(template, description="Valid array")
|
663
|
+
seen.add(len(template))
|
664
|
+
|
665
|
+
# Boundary and near-boundary sizes
|
666
|
+
min_items = schema.get("minItems")
|
667
|
+
max_items = schema.get("maxItems")
|
668
|
+
if min_items is not None:
|
669
|
+
# Do not generate an array with `minItems` length, because it is already covered by `template`
|
670
|
+
|
671
|
+
# One item more than minimum if possible
|
672
|
+
larger = min_items + 1
|
673
|
+
if larger not in seen and (max_items is None or larger <= max_items):
|
674
|
+
yield PositiveValue(
|
675
|
+
ctx.generate_from_schema({**schema, "minItems": larger, "maxItems": larger}),
|
676
|
+
description="Near-boundary items array",
|
677
|
+
)
|
678
|
+
seen.add(larger)
|
679
|
+
|
680
|
+
if max_items is not None:
|
681
|
+
if max_items < BUFFER_SIZE and max_items not in seen:
|
682
|
+
yield PositiveValue(
|
683
|
+
ctx.generate_from_schema({**schema, "minItems": max_items}),
|
684
|
+
description="Maximum items array",
|
685
|
+
)
|
686
|
+
seen.add(max_items)
|
687
|
+
|
688
|
+
# One item smaller than maximum if possible
|
689
|
+
smaller = max_items - 1
|
690
|
+
if (
|
691
|
+
smaller < BUFFER_SIZE
|
692
|
+
and smaller > 0
|
693
|
+
and smaller not in seen
|
694
|
+
and (min_items is None or smaller >= min_items)
|
695
|
+
):
|
696
|
+
yield PositiveValue(
|
697
|
+
ctx.generate_from_schema({**schema, "minItems": smaller, "maxItems": smaller}),
|
698
|
+
description="Near-boundary items array",
|
699
|
+
)
|
700
|
+
seen.add(smaller)
|
701
|
+
|
702
|
+
|
703
|
+
def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Generator[GeneratedValue, None, None]:
|
704
|
+
example = schema.get("example")
|
705
|
+
examples = schema.get("examples")
|
706
|
+
default = schema.get("default")
|
707
|
+
|
708
|
+
if example or examples or default:
|
709
|
+
if example:
|
710
|
+
yield PositiveValue(example, description="Example value")
|
711
|
+
if examples:
|
712
|
+
for example in examples:
|
713
|
+
yield PositiveValue(example, description="Example value")
|
714
|
+
if (
|
715
|
+
default
|
716
|
+
and not (example is not None and default == example)
|
717
|
+
and not (examples is not None and any(default == ex for ex in examples))
|
718
|
+
):
|
719
|
+
yield PositiveValue(default, description="Default value")
|
720
|
+
else:
|
721
|
+
yield PositiveValue(template, description="Valid object")
|
722
|
+
|
723
|
+
properties = schema.get("properties", {})
|
724
|
+
required = set(schema.get("required", []))
|
725
|
+
optional = list(set(properties) - required)
|
726
|
+
optional.sort()
|
727
|
+
|
728
|
+
# Generate combinations with required properties and one optional property
|
729
|
+
for name in optional:
|
730
|
+
combo = {k: v for k, v in template.items() if k in required or k == name}
|
731
|
+
if combo != template:
|
732
|
+
yield PositiveValue(combo, description=f"Object with all required properties and '{name}'")
|
733
|
+
# Generate one combination for each size from 2 to N-1
|
734
|
+
for selection in select_combinations(optional):
|
735
|
+
combo = {k: v for k, v in template.items() if k in required or k in selection}
|
736
|
+
yield PositiveValue(combo, description="Object with all required and a subset of optional properties")
|
737
|
+
# Generate only required properties
|
738
|
+
if set(properties) != required:
|
739
|
+
only_required = {k: v for k, v in template.items() if k in required}
|
740
|
+
yield PositiveValue(only_required, description="Object with only required properties")
|
741
|
+
seen = set()
|
742
|
+
for name, sub_schema in properties.items():
|
743
|
+
seen.add(_to_hashable_key(template.get(name)))
|
744
|
+
for new in cover_schema_iter(ctx, sub_schema):
|
745
|
+
key = _to_hashable_key(new.value)
|
746
|
+
if key not in seen:
|
747
|
+
yield PositiveValue(
|
748
|
+
{**template, name: new.value}, description=f"Object with valid '{name}' value: {new.description}"
|
749
|
+
)
|
750
|
+
seen.add(key)
|
751
|
+
seen.clear()
|
752
|
+
|
753
|
+
|
754
|
+
def select_combinations(optional: list[str]) -> Iterator[tuple[str, ...]]:
|
755
|
+
for size in range(2, len(optional)):
|
756
|
+
yield next(combinations(optional, size))
|
757
|
+
|
758
|
+
|
759
|
+
def _negative_enum(
|
760
|
+
ctx: CoverageContext, value: list, seen: set[Any | tuple[type, str]]
|
761
|
+
) -> Generator[GeneratedValue, None, None]:
|
762
|
+
def is_not_in_value(x: Any) -> bool:
|
763
|
+
if x in value or not ctx.is_valid_for_location(x):
|
764
|
+
return False
|
765
|
+
_hashed = _to_hashable_key(x)
|
766
|
+
return _hashed not in seen
|
767
|
+
|
768
|
+
strategy = (st.none() | st.booleans() | NUMERIC_STRATEGY | st.text()).filter(is_not_in_value)
|
769
|
+
value = ctx.generate_from(strategy)
|
770
|
+
yield NegativeValue(value, description="Invalid enum value", location=ctx.current_path)
|
771
|
+
hashed = _to_hashable_key(value)
|
772
|
+
seen.add(hashed)
|
773
|
+
|
774
|
+
|
775
|
+
def _negative_properties(
|
776
|
+
ctx: CoverageContext, template: dict, properties: dict
|
777
|
+
) -> Generator[GeneratedValue, None, None]:
|
778
|
+
nctx = ctx.with_negative()
|
779
|
+
for key, sub_schema in properties.items():
|
780
|
+
with nctx.at(key):
|
781
|
+
for value in cover_schema_iter(nctx, sub_schema):
|
782
|
+
yield NegativeValue(
|
783
|
+
{**template, key: value.value},
|
784
|
+
description=f"Object with invalid '{key}' value: {value.description}",
|
785
|
+
location=nctx.current_path,
|
786
|
+
parameter=key,
|
787
|
+
)
|
788
|
+
|
789
|
+
|
790
|
+
def _negative_pattern_properties(
|
791
|
+
ctx: CoverageContext, template: dict, pattern_properties: dict
|
792
|
+
) -> Generator[GeneratedValue, None, None]:
|
793
|
+
nctx = ctx.with_negative()
|
794
|
+
for pattern, sub_schema in pattern_properties.items():
|
795
|
+
try:
|
796
|
+
key = ctx.generate_from(st.from_regex(pattern))
|
797
|
+
except re.error:
|
798
|
+
continue
|
799
|
+
with nctx.at(pattern):
|
800
|
+
for value in cover_schema_iter(nctx, sub_schema):
|
801
|
+
yield NegativeValue(
|
802
|
+
{**template, key: value.value},
|
803
|
+
description=f"Object with invalid pattern key '{key}' ('{pattern}') value: {value.description}",
|
804
|
+
location=nctx.current_path,
|
805
|
+
)
|
806
|
+
|
807
|
+
|
808
|
+
def _negative_items(ctx: CoverageContext, schema: dict[str, Any] | bool) -> Generator[GeneratedValue, None, None]:
|
809
|
+
"""Arrays not matching the schema."""
|
810
|
+
nctx = ctx.with_negative()
|
811
|
+
for value in cover_schema_iter(nctx, schema):
|
812
|
+
yield NegativeValue(
|
813
|
+
[value.value],
|
814
|
+
description=f"Array with invalid items: {value.description}",
|
815
|
+
location=nctx.current_path,
|
816
|
+
)
|
817
|
+
|
818
|
+
|
819
|
+
def _not_matching_pattern(value: str, pattern: re.Pattern) -> bool:
|
820
|
+
return pattern.search(value) is None
|
821
|
+
|
822
|
+
|
823
|
+
def _negative_pattern(
|
824
|
+
ctx: CoverageContext, pattern: str, min_length: int | None = None, max_length: int | None = None
|
825
|
+
) -> Generator[GeneratedValue, None, None]:
|
826
|
+
try:
|
827
|
+
compiled = re.compile(pattern)
|
828
|
+
except re.error:
|
829
|
+
return
|
830
|
+
yield NegativeValue(
|
831
|
+
ctx.generate_from(
|
832
|
+
st.text(min_size=min_length or 0, max_size=max_length)
|
833
|
+
.filter(partial(_not_matching_pattern, pattern=compiled))
|
834
|
+
.filter(ctx.is_valid_for_location)
|
835
|
+
),
|
836
|
+
description=f"Value not matching the '{pattern}' pattern",
|
837
|
+
location=ctx.current_path,
|
838
|
+
)
|
839
|
+
|
840
|
+
|
841
|
+
def _with_negated_key(schema: dict, key: str, value: Any) -> dict:
|
842
|
+
return {"allOf": [{k: v for k, v in schema.items() if k != key}, {"not": {key: value}}]}
|
843
|
+
|
844
|
+
|
845
|
+
def _negative_multiple_of(
|
846
|
+
ctx: CoverageContext, schema: dict, multiple_of: int | float
|
847
|
+
) -> Generator[GeneratedValue, None, None]:
|
848
|
+
yield NegativeValue(
|
849
|
+
ctx.generate_from_schema(_with_negated_key(schema, "multipleOf", multiple_of)),
|
850
|
+
description=f"Non-multiple of {multiple_of}",
|
851
|
+
location=ctx.current_path,
|
852
|
+
)
|
853
|
+
|
854
|
+
|
855
|
+
def _negative_unique_items(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
|
856
|
+
unique = ctx.generate_from_schema({**schema, "type": "array", "minItems": 1, "maxItems": 1})
|
857
|
+
yield NegativeValue(unique + unique, description="Non-unique items", location=ctx.current_path)
|
858
|
+
|
859
|
+
|
860
|
+
def _negative_required(
|
861
|
+
ctx: CoverageContext, template: dict, required: list[str]
|
862
|
+
) -> Generator[GeneratedValue, None, None]:
|
863
|
+
for key in required:
|
864
|
+
yield NegativeValue(
|
865
|
+
{k: v for k, v in template.items() if k != key},
|
866
|
+
description=f"Missing required property: {key}",
|
867
|
+
location=ctx.current_path,
|
868
|
+
parameter=key,
|
869
|
+
)
|
870
|
+
|
871
|
+
|
872
|
+
def _is_invalid_hostname(v: Any) -> bool:
|
873
|
+
return v == "" or not jsonschema.Draft202012Validator.FORMAT_CHECKER.conforms(v, "hostname")
|
874
|
+
|
875
|
+
|
876
|
+
def _is_invalid_format(v: Any, format: str) -> bool:
|
877
|
+
return not jsonschema.Draft202012Validator.FORMAT_CHECKER.conforms(v, format)
|
878
|
+
|
879
|
+
|
880
|
+
def _negative_format(ctx: CoverageContext, schema: dict, format: str) -> Generator[GeneratedValue, None, None]:
|
881
|
+
# Hypothesis-jsonschema does not canonicalise it properly right now, which leads to unsatisfiable schema
|
882
|
+
without_format = {k: v for k, v in schema.items() if k != "format"}
|
883
|
+
without_format.setdefault("type", "string")
|
884
|
+
strategy = from_schema(without_format)
|
885
|
+
if format in jsonschema.Draft202012Validator.FORMAT_CHECKER.checkers:
|
886
|
+
if format == "hostname":
|
887
|
+
strategy = strategy.filter(_is_invalid_hostname)
|
888
|
+
else:
|
889
|
+
strategy = strategy.filter(functools.partial(_is_invalid_format, format=format))
|
890
|
+
yield NegativeValue(
|
891
|
+
ctx.generate_from(strategy),
|
892
|
+
description=f"Value not matching the '{format}' format",
|
893
|
+
location=ctx.current_path,
|
894
|
+
)
|
895
|
+
|
896
|
+
|
897
|
+
def _is_non_integer_float(x: float) -> bool:
|
898
|
+
return x != int(x)
|
899
|
+
|
900
|
+
|
901
|
+
def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Generator[GeneratedValue, None, None]:
|
902
|
+
if isinstance(ty, str):
|
903
|
+
types = [ty]
|
904
|
+
else:
|
905
|
+
types = ty
|
906
|
+
strategies = {ty: strategy for ty, strategy in STRATEGIES_FOR_TYPE.items() if ty not in types}
|
907
|
+
if "number" in types:
|
908
|
+
del strategies["integer"]
|
909
|
+
if "integer" in types:
|
910
|
+
strategies["number"] = FLOAT_STRATEGY.filter(_is_non_integer_float)
|
911
|
+
for strategy in strategies.values():
|
912
|
+
value = ctx.generate_from(strategy)
|
913
|
+
hashed = _to_hashable_key(value)
|
914
|
+
if hashed in seen:
|
915
|
+
continue
|
916
|
+
yield NegativeValue(value, description="Incorrect type", location=ctx.current_path)
|
917
|
+
seen.add(hashed)
|
918
|
+
|
919
|
+
|
920
|
+
def push_examples_to_properties(schema: dict[str, Any]) -> None:
|
921
|
+
"""Push examples from the top-level 'examples' field to the corresponding properties."""
|
922
|
+
if "examples" in schema and "properties" in schema:
|
923
|
+
properties = schema["properties"]
|
924
|
+
for example in schema["examples"]:
|
925
|
+
if isinstance(example, dict):
|
926
|
+
for prop, value in example.items():
|
927
|
+
if prop in properties:
|
928
|
+
if "examples" not in properties[prop]:
|
929
|
+
properties[prop]["examples"] = []
|
930
|
+
if value not in schema["properties"][prop]["examples"]:
|
931
|
+
properties[prop]["examples"].append(value)
|