schemathesis 3.39.15__py3-none-any.whl → 4.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +41 -79
- schemathesis/auths.py +111 -122
- schemathesis/checks.py +169 -60
- schemathesis/cli/__init__.py +15 -2117
- schemathesis/cli/commands/__init__.py +85 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +590 -0
- schemathesis/cli/commands/run/context.py +204 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
- schemathesis/cli/commands/run/handlers/output.py +1628 -0
- schemathesis/cli/commands/run/loaders.py +114 -0
- schemathesis/cli/commands/run/validation.py +246 -0
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +84 -0
- schemathesis/cli/{options.py → ext/options.py} +36 -34
- schemathesis/config/__init__.py +189 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +99 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +156 -0
- schemathesis/config/_generation.py +149 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +327 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +187 -0
- schemathesis/config/_projects.py +527 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +885 -0
- schemathesis/core/__init__.py +67 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +459 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/{throttling.py → core/rate_limit.py} +16 -17
- schemathesis/core/registries.py +31 -0
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +54 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +118 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +169 -0
- schemathesis/engine/errors.py +464 -0
- schemathesis/engine/events.py +258 -0
- schemathesis/engine/phases/__init__.py +88 -0
- schemathesis/{runner → engine/phases}/probes.py +52 -68
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +356 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +212 -0
- schemathesis/engine/phases/unit/_executor.py +416 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +247 -0
- schemathesis/errors.py +43 -0
- schemathesis/filters.py +17 -98
- schemathesis/generation/__init__.py +5 -33
- schemathesis/generation/case.py +317 -0
- schemathesis/generation/coverage.py +282 -175
- schemathesis/generation/hypothesis/__init__.py +36 -0
- schemathesis/generation/hypothesis/builder.py +800 -0
- schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +116 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +278 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +284 -0
- schemathesis/hooks.py +80 -101
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +455 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +313 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +281 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -273
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +42 -6
- schemathesis/specs/graphql/schemas.py +141 -137
- schemathesis/specs/graphql/validation.py +11 -17
- schemathesis/specs/openapi/__init__.py +6 -1
- schemathesis/specs/openapi/_cache.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +142 -156
- schemathesis/specs/openapi/checks.py +368 -257
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +23 -21
- schemathesis/specs/openapi/expressions/__init__.py +31 -19
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/lexer.py +1 -1
- schemathesis/specs/openapi/expressions/nodes.py +36 -41
- schemathesis/specs/openapi/expressions/parser.py +1 -1
- schemathesis/specs/openapi/formats.py +35 -7
- schemathesis/specs/openapi/media_types.py +53 -12
- schemathesis/specs/openapi/negative/__init__.py +7 -4
- schemathesis/specs/openapi/negative/mutations.py +6 -5
- schemathesis/specs/openapi/parameters.py +7 -10
- schemathesis/specs/openapi/patterns.py +94 -31
- schemathesis/specs/openapi/references.py +12 -53
- schemathesis/specs/openapi/schemas.py +238 -308
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +12 -6
- schemathesis/specs/openapi/stateful/__init__.py +268 -133
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/links.py +209 -0
- schemathesis/transport/__init__.py +142 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +124 -0
- schemathesis/transport/requests.py +244 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -11
- schemathesis/transport/wsgi.py +171 -0
- schemathesis-4.0.0.dist-info/METADATA +204 -0
- schemathesis-4.0.0.dist-info/RECORD +164 -0
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -712
- schemathesis/_override.py +0 -50
- schemathesis/_patches.py +0 -21
- schemathesis/_rate_limiter.py +0 -7
- schemathesis/cli/callbacks.py +0 -466
- schemathesis/cli/cassettes.py +0 -561
- schemathesis/cli/context.py +0 -75
- schemathesis/cli/debug.py +0 -27
- schemathesis/cli/handlers.py +0 -19
- schemathesis/cli/junitxml.py +0 -124
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -920
- schemathesis/cli/output/short.py +0 -59
- schemathesis/cli/reporting.py +0 -79
- schemathesis/cli/sanitization.py +0 -26
- schemathesis/code_samples.py +0 -151
- schemathesis/constants.py +0 -54
- schemathesis/contrib/__init__.py +0 -11
- schemathesis/contrib/openapi/__init__.py +0 -11
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -16
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -571
- schemathesis/experimental/__init__.py +0 -109
- schemathesis/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -284
- schemathesis/fixups/__init__.py +0 -37
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -28
- schemathesis/generation/_methods.py +0 -44
- schemathesis/graphql.py +0 -3
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/checks.py +0 -86
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -37
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- schemathesis/internal/output.py +0 -68
- schemathesis/internal/transformation.py +0 -26
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -474
- schemathesis/loaders.py +0 -122
- schemathesis/models.py +0 -1341
- schemathesis/parameters.py +0 -90
- schemathesis/runner/__init__.py +0 -605
- schemathesis/runner/events.py +0 -389
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/context.py +0 -88
- schemathesis/runner/impl/core.py +0 -1280
- schemathesis/runner/impl/solo.py +0 -80
- schemathesis/runner/impl/threadpool.py +0 -391
- schemathesis/runner/serialization.py +0 -544
- schemathesis/sanitization.py +0 -252
- schemathesis/serializers.py +0 -328
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -202
- schemathesis/service/client.py +0 -133
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -61
- schemathesis/service/extensions.py +0 -224
- schemathesis/service/hosts.py +0 -111
- schemathesis/service/metadata.py +0 -71
- schemathesis/service/models.py +0 -258
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -173
- schemathesis/service/usage.py +0 -66
- schemathesis/specs/graphql/loaders.py +0 -364
- schemathesis/specs/openapi/expressions/context.py +0 -16
- schemathesis/specs/openapi/links.py +0 -389
- schemathesis/specs/openapi/loaders.py +0 -707
- schemathesis/specs/openapi/stateful/statistic.py +0 -198
- schemathesis/specs/openapi/stateful/types.py +0 -14
- schemathesis/specs/openapi/validation.py +0 -26
- schemathesis/stateful/__init__.py +0 -147
- schemathesis/stateful/config.py +0 -97
- schemathesis/stateful/context.py +0 -135
- schemathesis/stateful/events.py +0 -274
- schemathesis/stateful/runner.py +0 -309
- schemathesis/stateful/sink.py +0 -68
- schemathesis/stateful/state_machine.py +0 -328
- schemathesis/stateful/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -369
- schemathesis/transports/asgi.py +0 -7
- schemathesis/transports/auth.py +0 -38
- schemathesis/transports/headers.py +0 -36
- schemathesis/transports/responses.py +0 -57
- schemathesis/types.py +0 -44
- schemathesis/utils.py +0 -164
- schemathesis-3.39.15.dist-info/METADATA +0 -293
- schemathesis-3.39.15.dist-info/RECORD +0 -160
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
- /schemathesis/{internal → core}/result.py +0 -0
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -16,14 +16,17 @@ from hypothesis_jsonschema import from_schema
|
|
16
16
|
from hypothesis_jsonschema._canonicalise import canonicalish
|
17
17
|
from hypothesis_jsonschema._from_schema import STRING_FORMATS as BUILT_IN_STRING_FORMATS
|
18
18
|
|
19
|
-
from
|
20
|
-
from
|
19
|
+
from schemathesis.core import INTERNAL_BUFFER_SIZE, 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
|
+
from schemathesis.openapi.generation.filters import is_invalid_path_parameter
|
26
|
+
|
21
27
|
from ..specs.openapi.converter import update_pattern_in_schema
|
22
28
|
from ..specs.openapi.formats import STRING_FORMATS, get_default_format_strategies
|
23
29
|
from ..specs.openapi.patterns import update_quantifier
|
24
|
-
from ..transports.headers import has_invalid_characters, is_latin_1_encodable
|
25
|
-
from ._hypothesis import get_single_example
|
26
|
-
from ._methods import DataGenerationMethod
|
27
30
|
|
28
31
|
|
29
32
|
def _replace_zero_with_nonzero(x: float) -> float:
|
@@ -34,7 +37,8 @@ def json_recursive_strategy(strategy: st.SearchStrategy) -> st.SearchStrategy:
|
|
34
37
|
return st.lists(strategy, max_size=3) | st.dictionaries(st.text(), strategy, max_size=3)
|
35
38
|
|
36
39
|
|
37
|
-
|
40
|
+
NEGATIVE_MODE_MAX_LENGTH_WITH_PATTERN = 100
|
41
|
+
NEGATIVE_MODE_MAX_ITEMS = 15
|
38
42
|
FLOAT_STRATEGY: st.SearchStrategy = st.floats(allow_nan=False, allow_infinity=False).map(_replace_zero_with_nonzero)
|
39
43
|
NUMERIC_STRATEGY: st.SearchStrategy = st.integers() | FLOAT_STRATEGY
|
40
44
|
JSON_STRATEGY: st.SearchStrategy = st.recursive(
|
@@ -62,18 +66,18 @@ UNKNOWN_PROPERTY_VALUE = 42
|
|
62
66
|
@dataclass
|
63
67
|
class GeneratedValue:
|
64
68
|
value: Any
|
65
|
-
|
69
|
+
generation_mode: GenerationMode
|
66
70
|
description: str
|
67
71
|
parameter: str | None
|
68
72
|
location: str | None
|
69
73
|
|
70
|
-
__slots__ = ("value", "
|
74
|
+
__slots__ = ("value", "generation_mode", "description", "parameter", "location")
|
71
75
|
|
72
76
|
@classmethod
|
73
77
|
def with_positive(cls, value: Any, *, description: str) -> GeneratedValue:
|
74
78
|
return cls(
|
75
79
|
value=value,
|
76
|
-
|
80
|
+
generation_mode=GenerationMode.POSITIVE,
|
77
81
|
description=description,
|
78
82
|
location=None,
|
79
83
|
parameter=None,
|
@@ -85,7 +89,7 @@ class GeneratedValue:
|
|
85
89
|
) -> GeneratedValue:
|
86
90
|
return cls(
|
87
91
|
value=value,
|
88
|
-
|
92
|
+
generation_mode=GenerationMode.NEGATIVE,
|
89
93
|
description=description,
|
90
94
|
location=location,
|
91
95
|
parameter=parameter,
|
@@ -98,28 +102,26 @@ NegativeValue = GeneratedValue.with_negative
|
|
98
102
|
|
99
103
|
@lru_cache(maxsize=128)
|
100
104
|
def cached_draw(strategy: st.SearchStrategy) -> Any:
|
101
|
-
return
|
105
|
+
return examples.generate_one(strategy)
|
102
106
|
|
103
107
|
|
104
108
|
@dataclass
|
105
109
|
class CoverageContext:
|
106
|
-
|
110
|
+
generation_modes: list[GenerationMode]
|
107
111
|
location: str
|
108
112
|
path: list[str | int]
|
109
113
|
|
110
|
-
__slots__ = ("location", "
|
114
|
+
__slots__ = ("location", "generation_modes", "path")
|
111
115
|
|
112
116
|
def __init__(
|
113
117
|
self,
|
114
118
|
*,
|
115
119
|
location: str,
|
116
|
-
|
120
|
+
generation_modes: list[GenerationMode] | None = None,
|
117
121
|
path: list[str | int] | None = None,
|
118
122
|
) -> None:
|
119
123
|
self.location = location
|
120
|
-
self.
|
121
|
-
data_generation_methods if data_generation_methods is not None else DataGenerationMethod.all()
|
122
|
-
)
|
124
|
+
self.generation_modes = generation_modes if generation_modes is not None else list(GenerationMode)
|
123
125
|
self.path = path or []
|
124
126
|
|
125
127
|
@contextmanager
|
@@ -137,20 +139,22 @@ class CoverageContext:
|
|
137
139
|
def with_positive(self) -> CoverageContext:
|
138
140
|
return CoverageContext(
|
139
141
|
location=self.location,
|
140
|
-
|
142
|
+
generation_modes=[GenerationMode.POSITIVE],
|
141
143
|
path=self.path,
|
142
144
|
)
|
143
145
|
|
144
146
|
def with_negative(self) -> CoverageContext:
|
145
147
|
return CoverageContext(
|
146
148
|
location=self.location,
|
147
|
-
|
149
|
+
generation_modes=[GenerationMode.NEGATIVE],
|
148
150
|
path=self.path,
|
149
151
|
)
|
150
152
|
|
151
153
|
def is_valid_for_location(self, value: Any) -> bool:
|
152
154
|
if self.location in ("header", "cookie") and isinstance(value, str):
|
153
155
|
return not value or (is_latin_1_encodable(value) and not has_invalid_characters("", value))
|
156
|
+
elif self.location == "path":
|
157
|
+
return not is_invalid_path_parameter(value)
|
154
158
|
return True
|
155
159
|
|
156
160
|
def generate_from(self, strategy: st.SearchStrategy) -> Any:
|
@@ -159,7 +163,7 @@ class CoverageContext:
|
|
159
163
|
def generate_from_schema(self, schema: dict | bool) -> Any:
|
160
164
|
if isinstance(schema, bool):
|
161
165
|
return 0
|
162
|
-
keys = sorted([k for k in schema if not k.startswith("x-") and k not in ["description", "example"]])
|
166
|
+
keys = sorted([k for k in schema if not k.startswith("x-") and k not in ["description", "example", "examples"]])
|
163
167
|
if keys == ["type"] and isinstance(schema["type"], str) and schema["type"] in STRATEGIES_FOR_TYPE:
|
164
168
|
return cached_draw(STRATEGIES_FOR_TYPE[schema["type"]])
|
165
169
|
if keys == ["format", "type"]:
|
@@ -251,15 +255,36 @@ def _to_hashable_key(value: T, _encode: Callable = _encode) -> tuple[type, str |
|
|
251
255
|
return (type(value), value)
|
252
256
|
|
253
257
|
|
258
|
+
class HashSet:
|
259
|
+
"""Helper to track already generated values."""
|
260
|
+
|
261
|
+
__slots__ = ("_data",)
|
262
|
+
|
263
|
+
def __init__(self) -> None:
|
264
|
+
self._data: set[tuple] = set()
|
265
|
+
|
266
|
+
def insert(self, value: Any) -> bool:
|
267
|
+
key = _to_hashable_key(value)
|
268
|
+
before = len(self._data)
|
269
|
+
self._data.add(key)
|
270
|
+
return len(self._data) > before
|
271
|
+
|
272
|
+
def clear(self) -> None:
|
273
|
+
self._data.clear()
|
274
|
+
|
275
|
+
|
254
276
|
def _cover_positive_for_type(
|
255
277
|
ctx: CoverageContext, schema: dict, ty: str | None
|
256
278
|
) -> Generator[GeneratedValue, None, None]:
|
257
279
|
if ty == "object" or ty == "array":
|
258
280
|
template_schema = _get_template_schema(schema, ty)
|
259
281
|
template = ctx.generate_from_schema(template_schema)
|
282
|
+
elif "properties" in schema or "required" in schema:
|
283
|
+
template_schema = _get_template_schema(schema, "object")
|
284
|
+
template = ctx.generate_from_schema(template_schema)
|
260
285
|
else:
|
261
286
|
template = None
|
262
|
-
if
|
287
|
+
if GenerationMode.POSITIVE in ctx.generation_modes:
|
263
288
|
ctx = ctx.with_positive()
|
264
289
|
enum = schema.get("enum", NOT_SET)
|
265
290
|
const = schema.get("const", NOT_SET)
|
@@ -295,6 +320,8 @@ def _cover_positive_for_type(
|
|
295
320
|
yield from _positive_array(ctx, schema, cast(list, template))
|
296
321
|
elif ty == "object":
|
297
322
|
yield from _positive_object(ctx, schema, cast(dict, template))
|
323
|
+
elif "properties" in schema or "required" in schema:
|
324
|
+
yield from _positive_object(ctx, schema, cast(dict, template))
|
298
325
|
|
299
326
|
|
300
327
|
@contextmanager
|
@@ -302,7 +329,7 @@ def _ignore_unfixable(
|
|
302
329
|
*,
|
303
330
|
# Cache exception types here as `jsonschema` uses a custom `__getattr__` on the module level
|
304
331
|
# and it may cause errors during the interpreter shutdown
|
305
|
-
ref_error: type[Exception] =
|
332
|
+
ref_error: type[Exception] = RefResolutionError,
|
306
333
|
schema_error: type[Exception] = jsonschema.SchemaError,
|
307
334
|
) -> Generator:
|
308
335
|
try:
|
@@ -319,10 +346,10 @@ def _ignore_unfixable(
|
|
319
346
|
|
320
347
|
|
321
348
|
def cover_schema_iter(
|
322
|
-
ctx: CoverageContext, schema: dict | bool, seen:
|
349
|
+
ctx: CoverageContext, schema: dict | bool, seen: HashSet | None = None
|
323
350
|
) -> Generator[GeneratedValue, None, None]:
|
324
351
|
if seen is None:
|
325
|
-
seen =
|
352
|
+
seen = HashSet()
|
326
353
|
if isinstance(schema, bool):
|
327
354
|
types = ["null", "boolean", "string", "number", "array", "object"]
|
328
355
|
schema = {}
|
@@ -337,7 +364,7 @@ def cover_schema_iter(
|
|
337
364
|
for ty in types:
|
338
365
|
with _ignore_unfixable():
|
339
366
|
yield from _cover_positive_for_type(ctx, schema, ty)
|
340
|
-
if
|
367
|
+
if GenerationMode.NEGATIVE in ctx.generation_modes:
|
341
368
|
template = None
|
342
369
|
for key, value in schema.items():
|
343
370
|
with _ignore_unfixable(), ctx.at(key):
|
@@ -345,12 +372,9 @@ def cover_schema_iter(
|
|
345
372
|
yield from _negative_enum(ctx, value, seen)
|
346
373
|
elif key == "const":
|
347
374
|
for value_ in _negative_enum(ctx, [value], seen):
|
348
|
-
|
349
|
-
if k not in seen:
|
350
|
-
yield value_
|
351
|
-
seen.add(k)
|
375
|
+
yield value_
|
352
376
|
elif key == "type":
|
353
|
-
yield from _negative_type(ctx,
|
377
|
+
yield from _negative_type(ctx, value, seen)
|
354
378
|
elif key == "properties":
|
355
379
|
template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
|
356
380
|
yield from _negative_properties(ctx, template, value)
|
@@ -367,69 +391,75 @@ def cover_schema_iter(
|
|
367
391
|
yield from _negative_format(ctx, schema, value)
|
368
392
|
elif key == "maximum":
|
369
393
|
next = value + 1
|
370
|
-
if next
|
394
|
+
if seen.insert(next):
|
371
395
|
yield NegativeValue(next, description="Value greater than maximum", location=ctx.current_path)
|
372
|
-
seen.add(next)
|
373
396
|
elif key == "minimum":
|
374
397
|
next = value - 1
|
375
|
-
if next
|
398
|
+
if seen.insert(next):
|
376
399
|
yield NegativeValue(next, description="Value smaller than minimum", location=ctx.current_path)
|
377
|
-
|
378
|
-
elif key == "exclusiveMaximum" or key == "exclusiveMinimum" and value not in seen:
|
400
|
+
elif key == "exclusiveMaximum" or key == "exclusiveMinimum" and seen.insert(value):
|
379
401
|
verb = "greater" if key == "exclusiveMaximum" else "smaller"
|
380
402
|
limit = "maximum" if key == "exclusiveMaximum" else "minimum"
|
381
403
|
yield NegativeValue(value, description=f"Value {verb} than {limit}", location=ctx.current_path)
|
382
|
-
seen.add(value)
|
383
404
|
elif key == "multipleOf":
|
384
405
|
for value_ in _negative_multiple_of(ctx, schema, value):
|
385
|
-
|
386
|
-
if k not in seen:
|
406
|
+
if seen.insert(value_.value):
|
387
407
|
yield value_
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
if
|
395
|
-
new_schema["pattern"] = update_quantifier(schema["pattern"], min_length, max_length)
|
396
|
-
if new_schema["pattern"] == schema["pattern"]:
|
397
|
-
# Pattern wasn't updated, try to generate a valid value then shrink the string to the required length
|
398
|
-
del new_schema["minLength"]
|
399
|
-
del new_schema["maxLength"]
|
400
|
-
value = ctx.generate_from_schema(new_schema)[:max_length]
|
401
|
-
else:
|
402
|
-
value = ctx.generate_from_schema(new_schema)
|
403
|
-
else:
|
404
|
-
value = ctx.generate_from_schema(new_schema)
|
405
|
-
k = _to_hashable_key(value)
|
406
|
-
if k not in seen:
|
408
|
+
elif key == "minLength" and 0 < value < INTERNAL_BUFFER_SIZE:
|
409
|
+
if value == 1:
|
410
|
+
# In this case, the only possible negative string is an empty one
|
411
|
+
# The `pattern` value may require an non-empty one and the generation will fail
|
412
|
+
# However, it is fine to violate `pattern` here as it is negative string generation anyway
|
413
|
+
value = ""
|
414
|
+
if seen.insert(value):
|
407
415
|
yield NegativeValue(
|
408
416
|
value, description="String smaller than minLength", location=ctx.current_path
|
409
417
|
)
|
410
|
-
|
411
|
-
|
418
|
+
else:
|
419
|
+
with suppress(InvalidArgument):
|
420
|
+
min_length = max_length = value - 1
|
421
|
+
new_schema = {**schema, "minLength": min_length, "maxLength": max_length}
|
422
|
+
new_schema.setdefault("type", "string")
|
423
|
+
if "pattern" in new_schema:
|
424
|
+
new_schema["pattern"] = update_quantifier(schema["pattern"], min_length, max_length)
|
425
|
+
if new_schema["pattern"] == schema["pattern"]:
|
426
|
+
# Pattern wasn't updated, try to generate a valid value then shrink the string to the required length
|
427
|
+
del new_schema["minLength"]
|
428
|
+
del new_schema["maxLength"]
|
429
|
+
value = ctx.generate_from_schema(new_schema)[:max_length]
|
430
|
+
else:
|
431
|
+
value = ctx.generate_from_schema(new_schema)
|
432
|
+
else:
|
433
|
+
value = ctx.generate_from_schema(new_schema)
|
434
|
+
if seen.insert(value):
|
435
|
+
yield NegativeValue(
|
436
|
+
value, description="String smaller than minLength", location=ctx.current_path
|
437
|
+
)
|
438
|
+
elif key == "maxLength" and value < INTERNAL_BUFFER_SIZE:
|
412
439
|
try:
|
413
440
|
min_length = max_length = value + 1
|
414
441
|
new_schema = {**schema, "minLength": min_length, "maxLength": max_length}
|
415
442
|
new_schema.setdefault("type", "string")
|
416
443
|
if "pattern" in new_schema:
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
del new_schema["minLength"]
|
421
|
-
del new_schema["maxLength"]
|
422
|
-
value = ctx.generate_from_schema(new_schema).ljust(max_length, "0")
|
423
|
-
else:
|
444
|
+
if value > NEGATIVE_MODE_MAX_LENGTH_WITH_PATTERN:
|
445
|
+
# Large `maxLength` value can be extremely slow to generate when combined with `pattern`
|
446
|
+
del new_schema["pattern"]
|
424
447
|
value = ctx.generate_from_schema(new_schema)
|
448
|
+
else:
|
449
|
+
new_schema["pattern"] = update_quantifier(schema["pattern"], min_length, max_length)
|
450
|
+
if new_schema["pattern"] == schema["pattern"]:
|
451
|
+
# Pattern wasn't updated, try to generate a valid value then extend the string to the required length
|
452
|
+
del new_schema["minLength"]
|
453
|
+
del new_schema["maxLength"]
|
454
|
+
value = ctx.generate_from_schema(new_schema).ljust(max_length, "0")
|
455
|
+
else:
|
456
|
+
value = ctx.generate_from_schema(new_schema)
|
425
457
|
else:
|
426
458
|
value = ctx.generate_from_schema(new_schema)
|
427
|
-
|
428
|
-
if k not in seen:
|
459
|
+
if seen.insert(value):
|
429
460
|
yield NegativeValue(
|
430
461
|
value, description="String larger than maxLength", location=ctx.current_path
|
431
462
|
)
|
432
|
-
seen.add(k)
|
433
463
|
except (InvalidArgument, Unsatisfiable):
|
434
464
|
pass
|
435
465
|
elif key == "uniqueItems" and value:
|
@@ -437,34 +467,64 @@ def cover_schema_iter(
|
|
437
467
|
elif key == "required":
|
438
468
|
template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
|
439
469
|
yield from _negative_required(ctx, template, value)
|
440
|
-
elif key == "maxItems" and isinstance(value, int) and value <
|
441
|
-
|
442
|
-
#
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
470
|
+
elif key == "maxItems" and isinstance(value, int) and value < INTERNAL_BUFFER_SIZE:
|
471
|
+
if value > NEGATIVE_MODE_MAX_ITEMS:
|
472
|
+
# It could be extremely slow to generate large arrays
|
473
|
+
# Generate values up to the limit and reuse them to construct the final array
|
474
|
+
new_schema = {
|
475
|
+
**schema,
|
476
|
+
"minItems": NEGATIVE_MODE_MAX_ITEMS,
|
477
|
+
"maxItems": NEGATIVE_MODE_MAX_ITEMS,
|
478
|
+
"type": "array",
|
479
|
+
}
|
480
|
+
if "items" in schema and isinstance(schema["items"], dict):
|
481
|
+
# The schema may have another large array nested, therefore generate covering cases
|
482
|
+
# and use them to build an array for the current schema
|
483
|
+
negative = [case.value for case in cover_schema_iter(ctx, schema["items"])]
|
484
|
+
positive = [case.value for case in cover_schema_iter(ctx.with_positive(), schema["items"])]
|
485
|
+
# Interleave positive & negative values
|
486
|
+
array_value = [value for pair in zip(positive, negative) for value in pair][
|
487
|
+
:NEGATIVE_MODE_MAX_ITEMS
|
488
|
+
]
|
489
|
+
else:
|
490
|
+
array_value = ctx.generate_from_schema(new_schema)
|
491
|
+
|
492
|
+
# Extend the array to be of length value + 1 by repeating its own elements
|
493
|
+
diff = value + 1 - len(array_value)
|
494
|
+
if diff > 0 and array_value:
|
495
|
+
array_value += (
|
496
|
+
array_value * (diff // len(array_value)) + array_value[: diff % len(array_value)]
|
497
|
+
)
|
498
|
+
if seen.insert(array_value):
|
447
499
|
yield NegativeValue(
|
448
500
|
array_value,
|
449
501
|
description="Array with more items than allowed by maxItems",
|
450
502
|
location=ctx.current_path,
|
451
503
|
)
|
452
|
-
|
453
|
-
|
454
|
-
|
504
|
+
else:
|
505
|
+
try:
|
506
|
+
# Force the array to have one more item than allowed
|
507
|
+
new_schema = {**schema, "minItems": value + 1, "maxItems": value + 1, "type": "array"}
|
508
|
+
array_value = ctx.generate_from_schema(new_schema)
|
509
|
+
if seen.insert(array_value):
|
510
|
+
yield NegativeValue(
|
511
|
+
array_value,
|
512
|
+
description="Array with more items than allowed by maxItems",
|
513
|
+
location=ctx.current_path,
|
514
|
+
)
|
515
|
+
except (InvalidArgument, Unsatisfiable):
|
516
|
+
pass
|
455
517
|
elif key == "minItems" and isinstance(value, int) and value > 0:
|
456
518
|
try:
|
457
519
|
# Force the array to have one less item than the minimum
|
458
520
|
new_schema = {**schema, "minItems": value - 1, "maxItems": value - 1, "type": "array"}
|
459
521
|
array_value = ctx.generate_from_schema(new_schema)
|
460
|
-
|
461
|
-
if k not in seen:
|
522
|
+
if seen.insert(array_value):
|
462
523
|
yield NegativeValue(
|
463
524
|
array_value,
|
464
525
|
description="Array with fewer items than allowed by minItems",
|
465
526
|
location=ctx.current_path,
|
466
527
|
)
|
467
|
-
seen.add(k)
|
468
528
|
except (InvalidArgument, Unsatisfiable):
|
469
529
|
pass
|
470
530
|
elif (
|
@@ -506,7 +566,7 @@ def _get_properties(schema: dict | bool) -> dict | bool:
|
|
506
566
|
return {"enum": schema["examples"]}
|
507
567
|
if schema.get("type") == "object":
|
508
568
|
return _get_template_schema(schema, "object")
|
509
|
-
_schema =
|
569
|
+
_schema = deepclone(schema)
|
510
570
|
update_pattern_in_schema(_schema)
|
511
571
|
return _schema
|
512
572
|
return schema
|
@@ -525,6 +585,22 @@ def _get_template_schema(schema: dict, ty: str) -> dict:
|
|
525
585
|
return {**schema, "type": ty}
|
526
586
|
|
527
587
|
|
588
|
+
def _ensure_valid_path_parameter_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
589
|
+
# Path parameters should have at least 1 character length and don't contain any characters with special treatment
|
590
|
+
# on the transport level.
|
591
|
+
# The implementation below sneaks into `not` to avoid clashing with existing `pattern` keyword
|
592
|
+
not_ = schema.get("not", {}).copy()
|
593
|
+
not_["pattern"] = r"[/{}]"
|
594
|
+
return {**schema, "minLength": 1, "not": not_}
|
595
|
+
|
596
|
+
|
597
|
+
def _ensure_valid_headers_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
598
|
+
# Reject any character that is not A-Z, a-z, or 0-9 for simplicity
|
599
|
+
not_ = schema.get("not", {}).copy()
|
600
|
+
not_["pattern"] = r"[^A-Za-z0-9]"
|
601
|
+
return {**schema, "not": not_}
|
602
|
+
|
603
|
+
|
528
604
|
def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
|
529
605
|
"""Generate positive string values."""
|
530
606
|
# Boundary and near boundary values
|
@@ -532,67 +608,90 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
532
608
|
if min_length == 0:
|
533
609
|
min_length = None
|
534
610
|
max_length = schema.get("maxLength")
|
611
|
+
if ctx.location == "path":
|
612
|
+
schema = _ensure_valid_path_parameter_schema(schema)
|
613
|
+
elif ctx.location in ("header", "cookie") and not ("format" in schema and schema["format"] in FORMAT_STRATEGIES):
|
614
|
+
# Don't apply it for known formats - they will insure the correct format during generation
|
615
|
+
schema = _ensure_valid_headers_schema(schema)
|
616
|
+
|
535
617
|
example = schema.get("example")
|
536
618
|
examples = schema.get("examples")
|
537
619
|
default = schema.get("default")
|
620
|
+
|
621
|
+
# Two-layer check to avoid potentially expensive data generation using schema constraints as a key
|
622
|
+
seen_values = HashSet()
|
623
|
+
seen_constraints: set[tuple] = set()
|
624
|
+
|
538
625
|
if example or examples or default:
|
539
|
-
|
626
|
+
has_valid_example = False
|
627
|
+
if example and ctx.is_valid_for_location(example) and seen_values.insert(example):
|
628
|
+
has_valid_example = True
|
540
629
|
yield PositiveValue(example, description="Example value")
|
541
630
|
if examples:
|
542
631
|
for example in examples:
|
543
|
-
if ctx.is_valid_for_location(example):
|
632
|
+
if ctx.is_valid_for_location(example) and seen_values.insert(example):
|
633
|
+
has_valid_example = True
|
544
634
|
yield PositiveValue(example, description="Example value")
|
545
635
|
if (
|
546
636
|
default
|
547
637
|
and not (example is not None and default == example)
|
548
638
|
and not (examples is not None and any(default == ex for ex in examples))
|
549
639
|
and ctx.is_valid_for_location(default)
|
640
|
+
and seen_values.insert(default)
|
550
641
|
):
|
642
|
+
has_valid_example = True
|
551
643
|
yield PositiveValue(default, description="Default value")
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
644
|
+
if not has_valid_example:
|
645
|
+
if not min_length and not max_length or "pattern" in schema:
|
646
|
+
value = ctx.generate_from_schema(schema)
|
647
|
+
seen_values.insert(value)
|
648
|
+
seen_constraints.add((min_length, max_length))
|
649
|
+
yield PositiveValue(value, description="Valid string")
|
650
|
+
elif not min_length and not max_length or "pattern" in schema:
|
651
|
+
value = ctx.generate_from_schema(schema)
|
652
|
+
seen_values.insert(value)
|
653
|
+
seen_constraints.add((min_length, max_length))
|
654
|
+
yield PositiveValue(value, description="Valid string")
|
655
|
+
|
656
|
+
if min_length is not None and min_length < INTERNAL_BUFFER_SIZE:
|
561
657
|
# Exactly the minimum length
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
658
|
+
key = (min_length, min_length)
|
659
|
+
if key not in seen_constraints:
|
660
|
+
seen_constraints.add(key)
|
661
|
+
value = ctx.generate_from_schema({**schema, "maxLength": min_length})
|
662
|
+
if seen_values.insert(value):
|
663
|
+
yield PositiveValue(value, description="Minimum length string")
|
566
664
|
|
567
665
|
# One character more than minimum if possible
|
568
666
|
larger = min_length + 1
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
)
|
574
|
-
|
667
|
+
key = (larger, larger)
|
668
|
+
if larger < INTERNAL_BUFFER_SIZE and key not in seen_constraints and (not max_length or larger <= max_length):
|
669
|
+
seen_constraints.add(key)
|
670
|
+
value = ctx.generate_from_schema({**schema, "minLength": larger, "maxLength": larger})
|
671
|
+
if seen_values.insert(value):
|
672
|
+
yield PositiveValue(value, description="Near-boundary length string")
|
575
673
|
|
576
674
|
if max_length is not None:
|
577
675
|
# Exactly the maximum length
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
)
|
582
|
-
|
676
|
+
key = (max_length, max_length)
|
677
|
+
if max_length < INTERNAL_BUFFER_SIZE and key not in seen_constraints:
|
678
|
+
seen_constraints.add(key)
|
679
|
+
value = ctx.generate_from_schema({**schema, "minLength": max_length, "maxLength": max_length})
|
680
|
+
if seen_values.insert(value):
|
681
|
+
yield PositiveValue(value, description="Maximum length string")
|
583
682
|
|
584
683
|
# One character less than maximum if possible
|
585
684
|
smaller = max_length - 1
|
685
|
+
key = (smaller, smaller)
|
586
686
|
if (
|
587
|
-
smaller <
|
588
|
-
and
|
687
|
+
smaller < INTERNAL_BUFFER_SIZE
|
688
|
+
and key not in seen_constraints
|
589
689
|
and (smaller > 0 and (min_length is None or smaller >= min_length))
|
590
690
|
):
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
seen.add(smaller)
|
691
|
+
seen_constraints.add(key)
|
692
|
+
value = ctx.generate_from_schema({**schema, "minLength": smaller, "maxLength": smaller})
|
693
|
+
if seen_values.insert(value):
|
694
|
+
yield PositiveValue(value, description="Near-boundary length string")
|
596
695
|
|
597
696
|
|
598
697
|
def closest_multiple_greater_than(y: int, x: int) -> int:
|
@@ -619,23 +718,26 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
619
718
|
examples = schema.get("examples")
|
620
719
|
default = schema.get("default")
|
621
720
|
|
721
|
+
seen = HashSet()
|
722
|
+
|
622
723
|
if example or examples or default:
|
623
|
-
if example:
|
724
|
+
if example and seen.insert(example):
|
624
725
|
yield PositiveValue(example, description="Example value")
|
625
726
|
if examples:
|
626
727
|
for example in examples:
|
627
|
-
|
728
|
+
if seen.insert(example):
|
729
|
+
yield PositiveValue(example, description="Example value")
|
628
730
|
if (
|
629
731
|
default
|
630
732
|
and not (example is not None and default == example)
|
631
733
|
and not (examples is not None and any(default == ex for ex in examples))
|
734
|
+
and seen.insert(default)
|
632
735
|
):
|
633
736
|
yield PositiveValue(default, description="Default value")
|
634
737
|
elif not minimum and not maximum:
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
seen = set()
|
738
|
+
value = ctx.generate_from_schema(schema)
|
739
|
+
seen.insert(value)
|
740
|
+
yield PositiveValue(value, description="Valid number")
|
639
741
|
|
640
742
|
if minimum is not None:
|
641
743
|
# Exactly the minimum
|
@@ -643,16 +745,15 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
643
745
|
smallest = closest_multiple_greater_than(minimum, multiple_of)
|
644
746
|
else:
|
645
747
|
smallest = minimum
|
646
|
-
seen.
|
647
|
-
|
748
|
+
if seen.insert(smallest):
|
749
|
+
yield PositiveValue(smallest, description="Minimum value")
|
648
750
|
|
649
751
|
# One more than minimum if possible
|
650
752
|
if multiple_of is not None:
|
651
753
|
larger = smallest + multiple_of
|
652
754
|
else:
|
653
755
|
larger = minimum + 1
|
654
|
-
if
|
655
|
-
seen.add(larger)
|
756
|
+
if (not maximum or larger <= maximum) and seen.insert(larger):
|
656
757
|
yield PositiveValue(larger, description="Near-boundary number")
|
657
758
|
|
658
759
|
if maximum is not None:
|
@@ -661,8 +762,7 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
661
762
|
largest = maximum - (maximum % multiple_of)
|
662
763
|
else:
|
663
764
|
largest = maximum
|
664
|
-
if largest
|
665
|
-
seen.add(largest)
|
765
|
+
if seen.insert(largest):
|
666
766
|
yield PositiveValue(largest, description="Maximum value")
|
667
767
|
|
668
768
|
# One less than maximum if possible
|
@@ -670,69 +770,79 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
670
770
|
smaller = largest - multiple_of
|
671
771
|
else:
|
672
772
|
smaller = maximum - 1
|
673
|
-
if
|
674
|
-
seen.add(smaller)
|
773
|
+
if (smaller > 0 and (minimum is None or smaller >= minimum)) and seen.insert(smaller):
|
675
774
|
yield PositiveValue(smaller, description="Near-boundary number")
|
676
775
|
|
677
776
|
|
678
777
|
def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Generator[GeneratedValue, None, None]:
|
679
|
-
seen = set()
|
680
778
|
example = schema.get("example")
|
681
779
|
examples = schema.get("examples")
|
682
780
|
default = schema.get("default")
|
683
781
|
|
782
|
+
seen = HashSet()
|
783
|
+
seen_constraints: set[tuple] = set()
|
784
|
+
|
684
785
|
if example or examples or default:
|
685
|
-
if example:
|
786
|
+
if example and seen.insert(example):
|
686
787
|
yield PositiveValue(example, description="Example value")
|
687
788
|
if examples:
|
688
789
|
for example in examples:
|
689
|
-
|
790
|
+
if seen.insert(example):
|
791
|
+
yield PositiveValue(example, description="Example value")
|
690
792
|
if (
|
691
793
|
default
|
692
794
|
and not (example is not None and default == example)
|
693
795
|
and not (examples is not None and any(default == ex for ex in examples))
|
796
|
+
and seen.insert(default)
|
694
797
|
):
|
695
798
|
yield PositiveValue(default, description="Default value")
|
696
|
-
|
799
|
+
elif seen.insert(template):
|
697
800
|
yield PositiveValue(template, description="Valid array")
|
698
|
-
seen.add(len(template))
|
699
801
|
|
700
802
|
# Boundary and near-boundary sizes
|
701
803
|
min_items = schema.get("minItems")
|
702
804
|
max_items = schema.get("maxItems")
|
703
805
|
if min_items is not None:
|
704
806
|
# Do not generate an array with `minItems` length, because it is already covered by `template`
|
705
|
-
|
706
807
|
# One item more than minimum if possible
|
707
808
|
larger = min_items + 1
|
708
|
-
if
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
seen.add(larger)
|
809
|
+
if (max_items is None or larger <= max_items) and larger not in seen_constraints:
|
810
|
+
seen_constraints.add(larger)
|
811
|
+
value = ctx.generate_from_schema({**schema, "minItems": larger, "maxItems": larger})
|
812
|
+
if seen.insert(value):
|
813
|
+
yield PositiveValue(value, description="Near-boundary items array")
|
714
814
|
|
715
815
|
if max_items is not None:
|
716
|
-
if max_items <
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
seen.add(max_items)
|
816
|
+
if max_items < INTERNAL_BUFFER_SIZE and max_items not in seen_constraints:
|
817
|
+
seen_constraints.add(max_items)
|
818
|
+
value = ctx.generate_from_schema({**schema, "minItems": max_items})
|
819
|
+
if seen.insert(value):
|
820
|
+
yield PositiveValue(value, description="Maximum items array")
|
722
821
|
|
723
822
|
# One item smaller than maximum if possible
|
724
823
|
smaller = max_items - 1
|
725
824
|
if (
|
726
|
-
smaller <
|
825
|
+
smaller < INTERNAL_BUFFER_SIZE
|
727
826
|
and smaller > 0
|
728
|
-
and smaller not in seen
|
729
827
|
and (min_items is None or smaller >= min_items)
|
828
|
+
and smaller not in seen_constraints
|
730
829
|
):
|
731
|
-
|
732
|
-
|
733
|
-
description="Near-boundary items array"
|
734
|
-
|
735
|
-
|
830
|
+
value = ctx.generate_from_schema({**schema, "minItems": smaller, "maxItems": smaller})
|
831
|
+
if seen.insert(value):
|
832
|
+
yield PositiveValue(value, description="Near-boundary items array")
|
833
|
+
|
834
|
+
if "items" in schema and "enum" in schema["items"] and isinstance(schema["items"]["enum"], list) and max_items != 0:
|
835
|
+
# Ensure there is enough items to pass `minItems` if it is specified
|
836
|
+
length = min_items or 1
|
837
|
+
for variant in schema["items"]["enum"]:
|
838
|
+
value = [variant] * length
|
839
|
+
if seen.insert(value):
|
840
|
+
yield PositiveValue(value, description="Enum value from available for items array")
|
841
|
+
elif min_items is None and max_items is None and "items" in schema and isinstance(schema["items"], dict):
|
842
|
+
# Otherwise only an empty array is generated
|
843
|
+
sub_schema = schema["items"]
|
844
|
+
for item in cover_schema_iter(ctx, sub_schema):
|
845
|
+
yield PositiveValue([item.value], description=f"Single-item array: {item.description}")
|
736
846
|
|
737
847
|
|
738
848
|
def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Generator[GeneratedValue, None, None]:
|
@@ -773,16 +883,14 @@ def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Gene
|
|
773
883
|
if set(properties) != required:
|
774
884
|
only_required = {k: v for k, v in template.items() if k in required}
|
775
885
|
yield PositiveValue(only_required, description="Object with only required properties")
|
776
|
-
seen =
|
886
|
+
seen = HashSet()
|
777
887
|
for name, sub_schema in properties.items():
|
778
|
-
seen.
|
888
|
+
seen.insert(template.get(name))
|
779
889
|
for new in cover_schema_iter(ctx, sub_schema):
|
780
|
-
|
781
|
-
if key not in seen:
|
890
|
+
if seen.insert(new.value):
|
782
891
|
yield PositiveValue(
|
783
892
|
{**template, name: new.value}, description=f"Object with valid '{name}' value: {new.description}"
|
784
893
|
)
|
785
|
-
seen.add(key)
|
786
894
|
seen.clear()
|
787
895
|
|
788
896
|
|
@@ -791,14 +899,11 @@ def select_combinations(optional: list[str]) -> Iterator[tuple[str, ...]]:
|
|
791
899
|
yield next(combinations(optional, size))
|
792
900
|
|
793
901
|
|
794
|
-
def _negative_enum(
|
795
|
-
ctx: CoverageContext, value: list, seen: set[Any | tuple[type, str]]
|
796
|
-
) -> Generator[GeneratedValue, None, None]:
|
902
|
+
def _negative_enum(ctx: CoverageContext, value: list, seen: HashSet) -> Generator[GeneratedValue, None, None]:
|
797
903
|
def is_not_in_value(x: Any) -> bool:
|
798
904
|
if x in value or not ctx.is_valid_for_location(x):
|
799
905
|
return False
|
800
|
-
|
801
|
-
return _hashed not in seen
|
906
|
+
return seen.insert(x)
|
802
907
|
|
803
908
|
strategy = (
|
804
909
|
st.text(alphabet=st.characters(min_codepoint=65, max_codepoint=122, categories=["L"]), min_size=3)
|
@@ -806,10 +911,11 @@ def _negative_enum(
|
|
806
911
|
| st.booleans()
|
807
912
|
| NUMERIC_STRATEGY
|
808
913
|
).filter(is_not_in_value)
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
914
|
+
yield NegativeValue(
|
915
|
+
ctx.generate_from(strategy),
|
916
|
+
description="Invalid enum value",
|
917
|
+
location=ctx.current_path,
|
918
|
+
)
|
813
919
|
|
814
920
|
|
815
921
|
def _negative_properties(
|
@@ -938,7 +1044,11 @@ def _is_non_integer_float(x: float) -> bool:
|
|
938
1044
|
return x != int(x)
|
939
1045
|
|
940
1046
|
|
941
|
-
def _negative_type(
|
1047
|
+
def _negative_type(
|
1048
|
+
ctx: CoverageContext,
|
1049
|
+
ty: str | list[str],
|
1050
|
+
seen: HashSet,
|
1051
|
+
) -> Generator[GeneratedValue, None, None]:
|
942
1052
|
if isinstance(ty, str):
|
943
1053
|
types = [ty]
|
944
1054
|
else:
|
@@ -950,11 +1060,8 @@ def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Gene
|
|
950
1060
|
strategies["number"] = FLOAT_STRATEGY.filter(_is_non_integer_float)
|
951
1061
|
for strategy in strategies.values():
|
952
1062
|
value = ctx.generate_from(strategy)
|
953
|
-
|
954
|
-
|
955
|
-
continue
|
956
|
-
yield NegativeValue(value, description="Incorrect type", location=ctx.current_path)
|
957
|
-
seen.add(hashed)
|
1063
|
+
if seen.insert(value):
|
1064
|
+
yield NegativeValue(value, description="Incorrect type", location=ctx.current_path)
|
958
1065
|
|
959
1066
|
|
960
1067
|
def push_examples_to_properties(schema: dict[str, Any]) -> None:
|