schemathesis 3.13.0__py3-none-any.whl → 4.4.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +53 -25
- schemathesis/auths.py +507 -0
- schemathesis/checks.py +190 -25
- schemathesis/cli/__init__.py +27 -1016
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +133 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +602 -0
- schemathesis/cli/commands/run/context.py +228 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +45 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
- schemathesis/cli/commands/run/handlers/output.py +1750 -0
- schemathesis/cli/commands/run/loaders.py +118 -0
- schemathesis/cli/commands/run/validation.py +256 -0
- schemathesis/cli/constants.py +5 -0
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +203 -0
- schemathesis/cli/ext/options.py +81 -0
- schemathesis/config/__init__.py +202 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +101 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +163 -0
- schemathesis/config/_generation.py +157 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +335 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +253 -0
- schemathesis/config/_projects.py +543 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +89 -0
- schemathesis/config/schema.json +975 -0
- schemathesis/core/__init__.py +72 -0
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +100 -0
- schemathesis/core/deserialization.py +210 -0
- schemathesis/core/errors.py +588 -0
- schemathesis/core/failures.py +316 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +183 -0
- schemathesis/core/jsonschema/keywords.py +40 -0
- schemathesis/core/jsonschema/references.py +222 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/lazy_import.py +15 -0
- schemathesis/core/loaders.py +107 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/core/media_types.py +79 -0
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/core/parameters.py +45 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +34 -0
- schemathesis/core/result.py +27 -0
- schemathesis/core/schema_analysis.py +17 -0
- schemathesis/core/shell.py +203 -0
- schemathesis/core/transforms.py +144 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +73 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +152 -0
- schemathesis/engine/control.py +44 -0
- schemathesis/engine/core.py +201 -0
- schemathesis/engine/errors.py +446 -0
- schemathesis/engine/events.py +284 -0
- schemathesis/engine/observations.py +42 -0
- schemathesis/engine/phases/__init__.py +108 -0
- schemathesis/engine/phases/analysis.py +28 -0
- schemathesis/engine/phases/probes.py +172 -0
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +364 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +220 -0
- schemathesis/engine/phases/unit/_executor.py +459 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +254 -0
- schemathesis/errors.py +47 -0
- schemathesis/filters.py +395 -0
- schemathesis/generation/__init__.py +25 -0
- schemathesis/generation/case.py +478 -0
- schemathesis/generation/coverage.py +1528 -0
- schemathesis/generation/hypothesis/__init__.py +121 -0
- schemathesis/generation/hypothesis/builder.py +992 -0
- schemathesis/generation/hypothesis/examples.py +56 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +285 -0
- schemathesis/generation/meta.py +227 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +127 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +294 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +285 -0
- schemathesis/hooks.py +270 -91
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +467 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +315 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +341 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/pytest/plugin.py +357 -0
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +683 -247
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/nodes.py +27 -0
- schemathesis/specs/graphql/scalars.py +86 -0
- schemathesis/specs/graphql/schemas.py +395 -123
- schemathesis/specs/graphql/validation.py +33 -0
- schemathesis/specs/openapi/__init__.py +9 -1
- schemathesis/specs/openapi/_hypothesis.py +578 -317
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +729 -0
- schemathesis/specs/openapi/adapter/protocol.py +59 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +368 -0
- schemathesis/specs/openapi/adapter/security.py +144 -0
- schemathesis/specs/openapi/adapter/v2.py +30 -0
- schemathesis/specs/openapi/adapter/v3_0.py +30 -0
- schemathesis/specs/openapi/adapter/v3_1.py +30 -0
- schemathesis/specs/openapi/analysis.py +96 -0
- schemathesis/specs/openapi/checks.py +753 -74
- schemathesis/specs/openapi/converter.py +176 -37
- schemathesis/specs/openapi/definitions.py +599 -4
- schemathesis/specs/openapi/examples.py +581 -165
- schemathesis/specs/openapi/expressions/__init__.py +52 -5
- schemathesis/specs/openapi/expressions/extractors.py +25 -0
- schemathesis/specs/openapi/expressions/lexer.py +34 -31
- schemathesis/specs/openapi/expressions/nodes.py +97 -46
- schemathesis/specs/openapi/expressions/parser.py +35 -13
- schemathesis/specs/openapi/formats.py +122 -0
- schemathesis/specs/openapi/media_types.py +75 -0
- schemathesis/specs/openapi/negative/__init__.py +117 -68
- schemathesis/specs/openapi/negative/mutations.py +294 -104
- schemathesis/specs/openapi/negative/utils.py +3 -6
- schemathesis/specs/openapi/patterns.py +458 -0
- schemathesis/specs/openapi/references.py +60 -81
- schemathesis/specs/openapi/schemas.py +648 -650
- schemathesis/specs/openapi/serialization.py +53 -30
- schemathesis/specs/openapi/stateful/__init__.py +404 -69
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
- schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
- schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
- schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
- schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
- schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
- schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
- schemathesis/specs/openapi/stateful/inference.py +254 -0
- schemathesis/specs/openapi/stateful/links.py +219 -78
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/common.py +23 -0
- schemathesis/specs/openapi/types/v2.py +129 -0
- schemathesis/specs/openapi/types/v3.py +134 -0
- schemathesis/specs/openapi/utils.py +7 -6
- schemathesis/specs/openapi/warnings.py +75 -0
- schemathesis/transport/__init__.py +224 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +126 -0
- schemathesis/transport/requests.py +278 -0
- schemathesis/transport/serialization.py +329 -0
- schemathesis/transport/wsgi.py +175 -0
- schemathesis-4.4.2.dist-info/METADATA +213 -0
- schemathesis-4.4.2.dist-info/RECORD +192 -0
- {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
- schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
- {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
- schemathesis/_compat.py +0 -41
- schemathesis/_hypothesis.py +0 -115
- schemathesis/cli/callbacks.py +0 -188
- schemathesis/cli/cassettes.py +0 -253
- schemathesis/cli/context.py +0 -36
- schemathesis/cli/debug.py +0 -21
- schemathesis/cli/handlers.py +0 -11
- schemathesis/cli/junitxml.py +0 -41
- schemathesis/cli/options.py +0 -51
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -508
- schemathesis/cli/output/short.py +0 -40
- schemathesis/constants.py +0 -79
- schemathesis/exceptions.py +0 -207
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -16
- schemathesis/extra/pytest_plugin.py +0 -216
- schemathesis/failures.py +0 -131
- schemathesis/fixups/__init__.py +0 -29
- schemathesis/fixups/fast_api.py +0 -30
- schemathesis/lazy.py +0 -227
- schemathesis/models.py +0 -1041
- schemathesis/parameters.py +0 -88
- schemathesis/runner/__init__.py +0 -460
- schemathesis/runner/events.py +0 -240
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -755
- schemathesis/runner/impl/solo.py +0 -85
- schemathesis/runner/impl/threadpool.py +0 -367
- schemathesis/runner/serialization.py +0 -189
- schemathesis/serializers.py +0 -233
- schemathesis/service/__init__.py +0 -3
- schemathesis/service/client.py +0 -46
- schemathesis/service/constants.py +0 -12
- schemathesis/service/events.py +0 -39
- schemathesis/service/handler.py +0 -39
- schemathesis/service/models.py +0 -7
- schemathesis/service/serialization.py +0 -153
- schemathesis/service/worker.py +0 -40
- schemathesis/specs/graphql/loaders.py +0 -215
- schemathesis/specs/openapi/constants.py +0 -7
- schemathesis/specs/openapi/expressions/context.py +0 -12
- schemathesis/specs/openapi/expressions/pointers.py +0 -29
- schemathesis/specs/openapi/filters.py +0 -44
- schemathesis/specs/openapi/links.py +0 -302
- schemathesis/specs/openapi/loaders.py +0 -453
- schemathesis/specs/openapi/parameters.py +0 -413
- schemathesis/specs/openapi/security.py +0 -129
- schemathesis/specs/openapi/validation.py +0 -24
- schemathesis/stateful.py +0 -349
- schemathesis/targets.py +0 -32
- schemathesis/types.py +0 -38
- schemathesis/utils.py +0 -436
- schemathesis-3.13.0.dist-info/METADATA +0 -202
- schemathesis-3.13.0.dist-info/RECORD +0 -91
- schemathesis-3.13.0.dist-info/entry_points.txt +0 -6
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
|
@@ -1,23 +1,41 @@
|
|
|
1
1
|
"""Schema mutations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
2
5
|
import enum
|
|
3
|
-
from
|
|
6
|
+
from dataclasses import dataclass
|
|
4
7
|
from functools import wraps
|
|
5
|
-
from typing import Any, Callable,
|
|
8
|
+
from typing import Any, Callable, Optional, Sequence, TypeVar
|
|
6
9
|
|
|
7
|
-
import attr
|
|
8
10
|
from hypothesis import reject
|
|
9
11
|
from hypothesis import strategies as st
|
|
10
12
|
from hypothesis.strategies._internal.featureflags import FeatureStrategy
|
|
11
13
|
from hypothesis_jsonschema._canonicalise import canonicalish
|
|
14
|
+
from typing_extensions import TypeAlias
|
|
15
|
+
|
|
16
|
+
from schemathesis.core.jsonschema import BUNDLE_STORAGE_KEY, get_type
|
|
17
|
+
from schemathesis.core.jsonschema.types import JsonSchemaObject
|
|
18
|
+
from schemathesis.core.parameters import ParameterLocation
|
|
19
|
+
from schemathesis.core.transforms import deepclone
|
|
12
20
|
|
|
13
|
-
from ..utils import is_header_location
|
|
14
21
|
from .types import Draw, Schema
|
|
15
|
-
from .utils import
|
|
22
|
+
from .utils import can_negate
|
|
16
23
|
|
|
17
24
|
T = TypeVar("T")
|
|
18
25
|
|
|
19
26
|
|
|
20
|
-
|
|
27
|
+
@dataclass
|
|
28
|
+
class MutationMetadata:
|
|
29
|
+
"""Metadata about a mutation that was applied."""
|
|
30
|
+
|
|
31
|
+
parameter: str | None
|
|
32
|
+
description: str
|
|
33
|
+
location: str | None
|
|
34
|
+
|
|
35
|
+
__slots__ = ("parameter", "description", "location")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MutationResult(int, enum.Enum):
|
|
21
39
|
"""The result of applying some mutation to some schema.
|
|
22
40
|
|
|
23
41
|
Failing to mutate something means that by applying some mutation, it is not possible to change
|
|
@@ -29,17 +47,17 @@ class MutationResult(enum.Enum):
|
|
|
29
47
|
SUCCESS = 1
|
|
30
48
|
FAILURE = 2
|
|
31
49
|
|
|
32
|
-
def __ior__(self, other: Any) ->
|
|
50
|
+
def __ior__(self, other: Any) -> MutationResult:
|
|
33
51
|
return self | other
|
|
34
52
|
|
|
35
|
-
def __or__(self, other: Any) ->
|
|
53
|
+
def __or__(self, other: Any) -> MutationResult:
|
|
36
54
|
# Syntactic sugar to simplify handling of multiple results
|
|
37
55
|
if self == MutationResult.SUCCESS:
|
|
38
56
|
return self
|
|
39
57
|
return other
|
|
40
58
|
|
|
41
59
|
|
|
42
|
-
Mutation = Callable[["MutationContext", Draw, Schema], MutationResult]
|
|
60
|
+
Mutation: TypeAlias = Callable[["MutationContext", Draw, Schema], tuple[MutationResult, Optional[MutationMetadata]]]
|
|
43
61
|
ANY_TYPE_KEYS = {"$ref", "allOf", "anyOf", "const", "else", "enum", "if", "not", "oneOf", "then", "type"}
|
|
44
62
|
TYPE_SPECIFIC_KEYS = {
|
|
45
63
|
"number": ("multipleOf", "maximum", "exclusiveMaximum", "minimum", "exclusiveMinimum"),
|
|
@@ -59,31 +77,59 @@ TYPE_SPECIFIC_KEYS = {
|
|
|
59
77
|
}
|
|
60
78
|
|
|
61
79
|
|
|
62
|
-
@
|
|
80
|
+
@dataclass
|
|
63
81
|
class MutationContext:
|
|
64
82
|
"""Meta information about the current mutation state."""
|
|
65
83
|
|
|
66
84
|
# The original schema
|
|
67
|
-
keywords: Schema
|
|
68
|
-
non_keywords: Schema
|
|
85
|
+
keywords: Schema # only keywords
|
|
86
|
+
non_keywords: Schema # everything else
|
|
69
87
|
# Schema location within API operation (header, query, etc)
|
|
70
|
-
location:
|
|
88
|
+
location: ParameterLocation
|
|
71
89
|
# Payload media type, if available
|
|
72
|
-
media_type:
|
|
90
|
+
media_type: str | None
|
|
91
|
+
# Whether generating unexpected parameters is permitted
|
|
92
|
+
allow_extra_parameters: bool
|
|
93
|
+
|
|
94
|
+
__slots__ = ("keywords", "non_keywords", "location", "media_type", "allow_extra_parameters")
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
*,
|
|
99
|
+
keywords: Schema,
|
|
100
|
+
non_keywords: Schema,
|
|
101
|
+
location: ParameterLocation,
|
|
102
|
+
media_type: str | None,
|
|
103
|
+
allow_extra_parameters: bool,
|
|
104
|
+
) -> None:
|
|
105
|
+
self.keywords = keywords
|
|
106
|
+
self.non_keywords = non_keywords
|
|
107
|
+
self.location = location
|
|
108
|
+
self.media_type = media_type
|
|
109
|
+
self.allow_extra_parameters = allow_extra_parameters
|
|
73
110
|
|
|
74
111
|
@property
|
|
75
|
-
def
|
|
76
|
-
return
|
|
112
|
+
def is_path_location(self) -> bool:
|
|
113
|
+
return self.location == ParameterLocation.PATH
|
|
77
114
|
|
|
78
115
|
@property
|
|
79
|
-
def
|
|
80
|
-
return self.location ==
|
|
116
|
+
def is_query_location(self) -> bool:
|
|
117
|
+
return self.location == ParameterLocation.QUERY
|
|
118
|
+
|
|
119
|
+
def ensure_bundle(self, schema: Schema) -> None:
|
|
120
|
+
"""Ensure schema has the bundle from context if needed.
|
|
81
121
|
|
|
82
|
-
|
|
122
|
+
This is necessary when working with nested schemas (e.g., property schemas)
|
|
123
|
+
that may contain bundled references but don't have the x-bundled key themselves.
|
|
124
|
+
"""
|
|
125
|
+
if BUNDLE_STORAGE_KEY in self.non_keywords and BUNDLE_STORAGE_KEY not in schema:
|
|
126
|
+
schema[BUNDLE_STORAGE_KEY] = self.non_keywords[BUNDLE_STORAGE_KEY]
|
|
127
|
+
|
|
128
|
+
def mutate(self, draw: Draw) -> tuple[Schema, MutationMetadata | None]:
|
|
83
129
|
# On the top level, Schemathesis creates "object" schemas for all parameter "in" values except "body", which is
|
|
84
|
-
# taken as-is. Therefore we can only apply mutations that won't change the Open API semantics of the schema.
|
|
85
|
-
mutations:
|
|
86
|
-
if self.location in (
|
|
130
|
+
# taken as-is. Therefore, we can only apply mutations that won't change the Open API semantics of the schema.
|
|
131
|
+
mutations: list[Mutation]
|
|
132
|
+
if self.location in (ParameterLocation.HEADER, ParameterLocation.COOKIE, ParameterLocation.QUERY):
|
|
87
133
|
# These objects follow this pattern:
|
|
88
134
|
# {
|
|
89
135
|
# "properties": properties,
|
|
@@ -105,24 +151,34 @@ class MutationContext:
|
|
|
105
151
|
# Body can be of any type and does not have any specific type semantic.
|
|
106
152
|
mutations = draw(ordered(get_mutations(draw, self.keywords)))
|
|
107
153
|
# Deep copy all keywords to avoid modifying the original schema
|
|
108
|
-
new_schema =
|
|
109
|
-
|
|
110
|
-
|
|
154
|
+
new_schema = deepclone(self.keywords)
|
|
155
|
+
# Add x-bundled before mutations so they can resolve bundled references
|
|
156
|
+
if BUNDLE_STORAGE_KEY in self.non_keywords:
|
|
157
|
+
new_schema[BUNDLE_STORAGE_KEY] = self.non_keywords[BUNDLE_STORAGE_KEY]
|
|
158
|
+
enabled_mutations = draw(st.shared(FeatureStrategy(), key="mutations"))
|
|
159
|
+
# Always apply at least one mutation, otherwise everything is rejected, and we'd like to avoid it
|
|
160
|
+
# for performance reasons
|
|
161
|
+
always_applied_mutation = draw(st.sampled_from(mutations))
|
|
162
|
+
result, metadata = always_applied_mutation(self, draw, new_schema)
|
|
111
163
|
for mutation in mutations:
|
|
112
|
-
if enabled_mutations.is_enabled(mutation.__name__):
|
|
113
|
-
|
|
164
|
+
if mutation is not always_applied_mutation and enabled_mutations.is_enabled(mutation.__name__):
|
|
165
|
+
mut_result, mut_metadata = mutation(self, draw, new_schema)
|
|
166
|
+
result |= mut_result
|
|
167
|
+
# Keep first successful mutation's metadata
|
|
168
|
+
if metadata is None and mut_metadata is not None:
|
|
169
|
+
metadata = mut_metadata
|
|
114
170
|
if result == MutationResult.FAILURE:
|
|
115
171
|
# If we failed to apply anything, then reject the whole case
|
|
116
|
-
reject()
|
|
172
|
+
reject()
|
|
117
173
|
new_schema.update(self.non_keywords)
|
|
118
|
-
if self.
|
|
174
|
+
if self.location.is_in_header:
|
|
119
175
|
# All headers should have names that can be sent over network
|
|
120
176
|
new_schema["propertyNames"] = {"type": "string", "format": "_header_name"}
|
|
121
177
|
for sub_schema in new_schema.get("properties", {}).values():
|
|
122
178
|
sub_schema["type"] = "string"
|
|
123
179
|
if len(sub_schema) == 1:
|
|
124
180
|
sub_schema["format"] = "_header_value"
|
|
125
|
-
if draw(st.booleans()):
|
|
181
|
+
if self.allow_extra_parameters and draw(st.booleans()):
|
|
126
182
|
# In headers, `additionalProperties` are False by default, which means that Schemathesis won't generate
|
|
127
183
|
# any headers that are not defined. This change adds the possibility of generating valid extra headers
|
|
128
184
|
new_schema["additionalProperties"] = {"type": "string", "format": "_header_value"}
|
|
@@ -135,21 +191,20 @@ class MutationContext:
|
|
|
135
191
|
and "minProperties" not in new_schema.get("not", {})
|
|
136
192
|
):
|
|
137
193
|
new_schema.setdefault("minProperties", 1)
|
|
138
|
-
return new_schema
|
|
194
|
+
return new_schema, metadata
|
|
139
195
|
|
|
140
196
|
|
|
141
197
|
def for_types(*allowed_types: str) -> Callable[[Mutation], Mutation]:
|
|
142
198
|
"""Immediately return FAILURE for schemas with types not from ``allowed_types``."""
|
|
143
|
-
|
|
144
199
|
_allowed_types = set(allowed_types)
|
|
145
200
|
|
|
146
201
|
def wrapper(mutation: Mutation) -> Mutation:
|
|
147
202
|
@wraps(mutation)
|
|
148
|
-
def inner(
|
|
203
|
+
def inner(ctx: MutationContext, draw: Draw, schema: Schema) -> tuple[MutationResult, MutationMetadata | None]:
|
|
149
204
|
types = get_type(schema)
|
|
150
205
|
if _allowed_types & set(types):
|
|
151
|
-
return mutation(
|
|
152
|
-
return MutationResult.FAILURE
|
|
206
|
+
return mutation(ctx, draw, schema)
|
|
207
|
+
return MutationResult.FAILURE, None
|
|
153
208
|
|
|
154
209
|
return inner
|
|
155
210
|
|
|
@@ -157,7 +212,9 @@ def for_types(*allowed_types: str) -> Callable[[Mutation], Mutation]:
|
|
|
157
212
|
|
|
158
213
|
|
|
159
214
|
@for_types("object")
|
|
160
|
-
def remove_required_property(
|
|
215
|
+
def remove_required_property(
|
|
216
|
+
ctx: MutationContext, draw: Draw, schema: Schema
|
|
217
|
+
) -> tuple[MutationResult, MutationMetadata | None]:
|
|
161
218
|
"""Remove a required property.
|
|
162
219
|
|
|
163
220
|
Effect: Some property won't be generated.
|
|
@@ -165,13 +222,13 @@ def remove_required_property(context: MutationContext, draw: Draw, schema: Schem
|
|
|
165
222
|
required = schema.get("required")
|
|
166
223
|
if not required:
|
|
167
224
|
# No required properties - can't mutate
|
|
168
|
-
return MutationResult.FAILURE
|
|
225
|
+
return MutationResult.FAILURE, None
|
|
169
226
|
if len(required) == 1:
|
|
170
227
|
property_name = draw(st.sampled_from(sorted(required)))
|
|
171
228
|
else:
|
|
172
229
|
candidate = draw(st.sampled_from(sorted(required)))
|
|
173
|
-
enabled_properties = draw(st.shared(FeatureStrategy(), key="properties"))
|
|
174
|
-
candidates = [candidate
|
|
230
|
+
enabled_properties = draw(st.shared(FeatureStrategy(), key="properties"))
|
|
231
|
+
candidates = [candidate, *sorted([prop for prop in required if enabled_properties.is_enabled(prop)])]
|
|
175
232
|
property_name = draw(st.sampled_from(candidates))
|
|
176
233
|
required.remove(property_name)
|
|
177
234
|
if not required:
|
|
@@ -187,45 +244,73 @@ def remove_required_property(context: MutationContext, draw: Draw, schema: Schem
|
|
|
187
244
|
# This property still can be generated via `patternProperties`, but this implementation doesn't cover this case
|
|
188
245
|
# Its probability is relatively low, and the complete solution compatible with Draft 4 will require extra complexity
|
|
189
246
|
# The output filter covers cases like this
|
|
190
|
-
|
|
247
|
+
metadata = MutationMetadata(
|
|
248
|
+
parameter=property_name,
|
|
249
|
+
description="Required property removed",
|
|
250
|
+
location=f"/properties/{property_name}",
|
|
251
|
+
)
|
|
252
|
+
return MutationResult.SUCCESS, metadata
|
|
191
253
|
|
|
192
254
|
|
|
193
|
-
def change_type(
|
|
255
|
+
def change_type(
|
|
256
|
+
ctx: MutationContext, draw: Draw, schema: JsonSchemaObject
|
|
257
|
+
) -> tuple[MutationResult, MutationMetadata | None]:
|
|
194
258
|
"""Change type of values accepted by a schema."""
|
|
195
259
|
if "type" not in schema:
|
|
196
260
|
# The absence of this keyword means that the schema values can be of any type;
|
|
197
261
|
# Therefore, we can't choose a different type
|
|
198
|
-
return MutationResult.FAILURE
|
|
199
|
-
if
|
|
262
|
+
return MutationResult.FAILURE, None
|
|
263
|
+
if ctx.media_type == "application/x-www-form-urlencoded":
|
|
200
264
|
# Form data should be an object, do not change it
|
|
201
|
-
return MutationResult.FAILURE
|
|
202
|
-
if
|
|
203
|
-
|
|
204
|
-
|
|
265
|
+
return MutationResult.FAILURE, None
|
|
266
|
+
# For headers, query and path parameters, if the current type is string, then it already
|
|
267
|
+
# includes all possible values as those parameters will be stringified before sending,
|
|
268
|
+
# therefore it can't be negated.
|
|
269
|
+
old_types = get_type(schema)
|
|
270
|
+
if "string" in old_types and (ctx.location.is_in_header or ctx.is_path_location or ctx.is_query_location):
|
|
271
|
+
return MutationResult.FAILURE, None
|
|
272
|
+
candidates = _get_type_candidates(ctx, schema)
|
|
205
273
|
if not candidates:
|
|
206
274
|
# Schema covers all possible types, not possible to choose something else
|
|
207
|
-
return MutationResult.FAILURE
|
|
275
|
+
return MutationResult.FAILURE, None
|
|
208
276
|
if len(candidates) == 1:
|
|
209
277
|
new_type = candidates.pop()
|
|
210
278
|
schema["type"] = new_type
|
|
279
|
+
_ensure_query_serializes_to_non_empty(ctx, schema)
|
|
280
|
+
prevent_unsatisfiable_schema(schema, new_type)
|
|
281
|
+
else:
|
|
282
|
+
# Choose one type that will be present in the final candidates list
|
|
283
|
+
candidate = draw(st.sampled_from(sorted(candidates)))
|
|
284
|
+
candidates.remove(candidate)
|
|
285
|
+
enabled_types = draw(st.shared(FeatureStrategy(), key="types"))
|
|
286
|
+
remaining_candidates = [
|
|
287
|
+
candidate,
|
|
288
|
+
*sorted([candidate for candidate in candidates if enabled_types.is_enabled(candidate)]),
|
|
289
|
+
]
|
|
290
|
+
new_type = draw(st.sampled_from(remaining_candidates))
|
|
291
|
+
schema["type"] = new_type
|
|
292
|
+
_ensure_query_serializes_to_non_empty(ctx, schema)
|
|
211
293
|
prevent_unsatisfiable_schema(schema, new_type)
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
[candidate for candidate in candidates if enabled_types.is_enabled(candidate)]
|
|
294
|
+
|
|
295
|
+
old_type_str = " | ".join(sorted(old_types)) if len(old_types) > 1 else old_types[0]
|
|
296
|
+
metadata = MutationMetadata(
|
|
297
|
+
parameter=None,
|
|
298
|
+
description=f"Invalid type {new_type} (expected {old_type_str})",
|
|
299
|
+
location=None,
|
|
219
300
|
)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
301
|
+
return MutationResult.SUCCESS, metadata
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _ensure_query_serializes_to_non_empty(ctx: MutationContext, schema: Schema) -> None:
|
|
305
|
+
if ctx.is_query_location and schema.get("type") == "array":
|
|
306
|
+
# Query parameters with empty arrays or arrays of `None` or empty arrays / objects will not appear in the final URL
|
|
307
|
+
schema["minItems"] = schema.get("minItems") or 1
|
|
308
|
+
schema.setdefault("items", {}).update({"not": {"enum": [None, [], {}]}})
|
|
224
309
|
|
|
225
310
|
|
|
226
|
-
def _get_type_candidates(
|
|
311
|
+
def _get_type_candidates(ctx: MutationContext, schema: Schema) -> set[str]:
|
|
227
312
|
types = set(get_type(schema))
|
|
228
|
-
if
|
|
313
|
+
if ctx.is_path_location:
|
|
229
314
|
candidates = {"string", "integer", "number", "boolean", "null"} - types
|
|
230
315
|
else:
|
|
231
316
|
candidates = {"string", "integer", "number", "object", "array", "boolean", "null"} - types
|
|
@@ -254,7 +339,9 @@ def drop_not_type_specific_keywords(schema: Schema, new_type: str) -> None:
|
|
|
254
339
|
|
|
255
340
|
|
|
256
341
|
@for_types("object")
|
|
257
|
-
def change_properties(
|
|
342
|
+
def change_properties(
|
|
343
|
+
ctx: MutationContext, draw: Draw, schema: Schema
|
|
344
|
+
) -> tuple[MutationResult, MutationMetadata | None]:
|
|
258
345
|
"""Mutate individual object schema properties.
|
|
259
346
|
|
|
260
347
|
Effect: Some properties will not validate the original schema
|
|
@@ -262,12 +349,18 @@ def change_properties(context: MutationContext, draw: Draw, schema: Schema) -> M
|
|
|
262
349
|
properties = sorted(schema.get("properties", {}).items())
|
|
263
350
|
if not properties:
|
|
264
351
|
# No properties to mutate
|
|
265
|
-
return MutationResult.FAILURE
|
|
352
|
+
return MutationResult.FAILURE, None
|
|
266
353
|
# Order properties randomly and iterate over them until at least one mutation is successfully applied to at least
|
|
267
354
|
# one property
|
|
268
|
-
ordered_properties =
|
|
355
|
+
ordered_properties = [
|
|
356
|
+
(name, canonicalish(subschema) if isinstance(subschema, bool) else subschema)
|
|
357
|
+
for name, subschema in draw(ordered(properties, unique_by=lambda x: x[0]))
|
|
358
|
+
]
|
|
359
|
+
nested_metadata = None
|
|
269
360
|
for property_name, property_schema in ordered_properties:
|
|
270
|
-
|
|
361
|
+
ctx.ensure_bundle(property_schema)
|
|
362
|
+
result, nested_metadata = apply_until_success(ctx, draw, property_schema)
|
|
363
|
+
if result == MutationResult.SUCCESS:
|
|
271
364
|
# It is still possible to generate "positive" cases, for example, when this property is optional.
|
|
272
365
|
# They are filtered out on the upper level anyway, but to avoid performance penalty we adjust the schema
|
|
273
366
|
# so the generated samples are less likely to be "positive"
|
|
@@ -281,31 +374,47 @@ def change_properties(context: MutationContext, draw: Draw, schema: Schema) -> M
|
|
|
281
374
|
break
|
|
282
375
|
else:
|
|
283
376
|
# No successful mutations
|
|
284
|
-
return MutationResult.FAILURE
|
|
285
|
-
enabled_properties = draw(st.shared(FeatureStrategy(), key="properties"))
|
|
286
|
-
enabled_mutations = draw(st.shared(FeatureStrategy(), key="mutations"))
|
|
377
|
+
return MutationResult.FAILURE, None
|
|
378
|
+
enabled_properties = draw(st.shared(FeatureStrategy(), key="properties"))
|
|
379
|
+
enabled_mutations = draw(st.shared(FeatureStrategy(), key="mutations"))
|
|
287
380
|
for name, property_schema in properties:
|
|
288
381
|
# Skip already mutated property
|
|
289
|
-
if name == property_name:
|
|
382
|
+
if name == property_name:
|
|
290
383
|
# Pylint: `properties` variable has at least one element as it is checked at the beginning of the function
|
|
291
384
|
# Then those properties are ordered and iterated over, therefore `property_name` is always defined
|
|
292
385
|
continue
|
|
293
386
|
if enabled_properties.is_enabled(name):
|
|
387
|
+
ctx.ensure_bundle(property_schema)
|
|
294
388
|
for mutation in get_mutations(draw, property_schema):
|
|
295
389
|
if enabled_mutations.is_enabled(mutation.__name__):
|
|
296
|
-
mutation(
|
|
297
|
-
|
|
390
|
+
mutation(ctx, draw, property_schema)
|
|
391
|
+
|
|
392
|
+
# Use nested metadata description if available, otherwise use generic description
|
|
393
|
+
if nested_metadata and nested_metadata.description:
|
|
394
|
+
description = nested_metadata.description
|
|
395
|
+
else:
|
|
396
|
+
description = "Property constraint violated"
|
|
397
|
+
|
|
398
|
+
metadata = MutationMetadata(
|
|
399
|
+
parameter=property_name,
|
|
400
|
+
description=description,
|
|
401
|
+
location=f"/properties/{property_name}",
|
|
402
|
+
)
|
|
403
|
+
return MutationResult.SUCCESS, metadata
|
|
298
404
|
|
|
299
405
|
|
|
300
|
-
def apply_until_success(
|
|
406
|
+
def apply_until_success(
|
|
407
|
+
ctx: MutationContext, draw: Draw, schema: Schema
|
|
408
|
+
) -> tuple[MutationResult, MutationMetadata | None]:
|
|
301
409
|
for mutation in get_mutations(draw, schema):
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
410
|
+
result, metadata = mutation(ctx, draw, schema)
|
|
411
|
+
if result == MutationResult.SUCCESS:
|
|
412
|
+
return MutationResult.SUCCESS, metadata
|
|
413
|
+
return MutationResult.FAILURE, None
|
|
305
414
|
|
|
306
415
|
|
|
307
416
|
@for_types("array")
|
|
308
|
-
def change_items(
|
|
417
|
+
def change_items(ctx: MutationContext, draw: Draw, schema: Schema) -> tuple[MutationResult, MutationMetadata | None]:
|
|
309
418
|
"""Mutate individual array items.
|
|
310
419
|
|
|
311
420
|
Effect: Some items will not validate the original schema
|
|
@@ -313,92 +422,173 @@ def change_items(context: MutationContext, draw: Draw, schema: Schema) -> Mutati
|
|
|
313
422
|
items = schema.get("items", {})
|
|
314
423
|
if not items:
|
|
315
424
|
# No items to mutate
|
|
316
|
-
return MutationResult.FAILURE
|
|
425
|
+
return MutationResult.FAILURE, None
|
|
426
|
+
# For query/path/header/cookie, string items cannot be meaningfully mutated
|
|
427
|
+
# because all types serialize to strings anyway
|
|
428
|
+
if ctx.location.is_in_header or ctx.is_path_location or ctx.is_query_location:
|
|
429
|
+
items = schema.get("items", {})
|
|
430
|
+
if isinstance(items, dict):
|
|
431
|
+
items_types = get_type(items)
|
|
432
|
+
if "string" in items_types:
|
|
433
|
+
return MutationResult.FAILURE, None
|
|
317
434
|
if isinstance(items, dict):
|
|
318
|
-
return _change_items_object(
|
|
435
|
+
return _change_items_object(ctx, draw, schema, items)
|
|
319
436
|
if isinstance(items, list):
|
|
320
|
-
return _change_items_array(
|
|
321
|
-
return MutationResult.FAILURE
|
|
437
|
+
return _change_items_array(ctx, draw, schema, items)
|
|
438
|
+
return MutationResult.FAILURE, None
|
|
322
439
|
|
|
323
440
|
|
|
324
|
-
def _change_items_object(
|
|
441
|
+
def _change_items_object(
|
|
442
|
+
ctx: MutationContext, draw: Draw, schema: Schema, items: Schema
|
|
443
|
+
) -> tuple[MutationResult, MutationMetadata | None]:
|
|
444
|
+
ctx.ensure_bundle(items)
|
|
325
445
|
result = MutationResult.FAILURE
|
|
446
|
+
metadata = None
|
|
326
447
|
for mutation in get_mutations(draw, items):
|
|
327
|
-
|
|
448
|
+
mut_result, mut_metadata = mutation(ctx, draw, items)
|
|
449
|
+
result |= mut_result
|
|
450
|
+
if metadata is None and mut_metadata is not None:
|
|
451
|
+
metadata = mut_metadata
|
|
328
452
|
if result == MutationResult.FAILURE:
|
|
329
|
-
return MutationResult.FAILURE
|
|
453
|
+
return MutationResult.FAILURE, None
|
|
330
454
|
min_items = schema.get("minItems", 0)
|
|
331
455
|
schema["minItems"] = max(min_items, 1)
|
|
332
|
-
|
|
456
|
+
# Use nested metadata description if available, update location to show it's in array items
|
|
457
|
+
if metadata:
|
|
458
|
+
metadata = MutationMetadata(
|
|
459
|
+
parameter=None,
|
|
460
|
+
description=f"Array item: {metadata.description}",
|
|
461
|
+
location="/items",
|
|
462
|
+
)
|
|
463
|
+
return MutationResult.SUCCESS, metadata
|
|
333
464
|
|
|
334
465
|
|
|
335
|
-
def _change_items_array(
|
|
466
|
+
def _change_items_array(
|
|
467
|
+
ctx: MutationContext, draw: Draw, schema: Schema, items: list
|
|
468
|
+
) -> tuple[MutationResult, MutationMetadata | None]:
|
|
336
469
|
latest_success_index = None
|
|
470
|
+
metadata = None
|
|
337
471
|
for idx, item in enumerate(items):
|
|
472
|
+
ctx.ensure_bundle(item)
|
|
338
473
|
result = MutationResult.FAILURE
|
|
339
474
|
for mutation in get_mutations(draw, item):
|
|
340
|
-
|
|
475
|
+
mut_result, mut_metadata = mutation(ctx, draw, item)
|
|
476
|
+
result |= mut_result
|
|
477
|
+
if metadata is None and mut_metadata is not None:
|
|
478
|
+
metadata = mut_metadata
|
|
341
479
|
if result == MutationResult.SUCCESS:
|
|
342
480
|
latest_success_index = idx
|
|
343
481
|
if latest_success_index is None:
|
|
344
|
-
return MutationResult.FAILURE
|
|
482
|
+
return MutationResult.FAILURE, None
|
|
345
483
|
min_items = schema.get("minItems", 0)
|
|
346
484
|
schema["minItems"] = max(min_items, latest_success_index + 1)
|
|
347
|
-
|
|
485
|
+
# Use nested metadata description if available, update location to show specific array index
|
|
486
|
+
if metadata:
|
|
487
|
+
metadata = MutationMetadata(
|
|
488
|
+
parameter=None,
|
|
489
|
+
description=f"Array item at index {latest_success_index}: {metadata.description}",
|
|
490
|
+
location=f"/items/{latest_success_index}",
|
|
491
|
+
)
|
|
492
|
+
return MutationResult.SUCCESS, metadata
|
|
348
493
|
|
|
349
494
|
|
|
350
|
-
def negate_constraints(
|
|
495
|
+
def negate_constraints(
|
|
496
|
+
ctx: MutationContext, draw: Draw, schema: Schema
|
|
497
|
+
) -> tuple[MutationResult, MutationMetadata | None]:
|
|
351
498
|
"""Negate schema constrains while keeping the original type."""
|
|
352
|
-
|
|
353
|
-
|
|
499
|
+
ctx.ensure_bundle(schema)
|
|
500
|
+
if not can_negate(schema):
|
|
501
|
+
return MutationResult.FAILURE, None
|
|
354
502
|
copied = schema.copy()
|
|
503
|
+
# Preserve x-bundled before clearing
|
|
504
|
+
bundled = schema.get(BUNDLE_STORAGE_KEY)
|
|
355
505
|
schema.clear()
|
|
506
|
+
if bundled is not None:
|
|
507
|
+
schema[BUNDLE_STORAGE_KEY] = bundled
|
|
356
508
|
is_negated = False
|
|
509
|
+
negated_keys = []
|
|
357
510
|
|
|
358
|
-
def is_mutation_candidate(k: str) -> bool:
|
|
511
|
+
def is_mutation_candidate(k: str, v: Any) -> bool:
|
|
359
512
|
# Should we negate this key?
|
|
513
|
+
if k == "required":
|
|
514
|
+
return v != []
|
|
515
|
+
if k in ("example", "examples"):
|
|
516
|
+
return False
|
|
517
|
+
if ctx.is_path_location and k == "minLength" and v == 1:
|
|
518
|
+
# Empty path parameter will be filtered out
|
|
519
|
+
return False
|
|
520
|
+
if (
|
|
521
|
+
not ctx.allow_extra_parameters
|
|
522
|
+
and k == "additionalProperties"
|
|
523
|
+
and ctx.location in (ParameterLocation.QUERY, ParameterLocation.HEADER, ParameterLocation.COOKIE)
|
|
524
|
+
):
|
|
525
|
+
return False
|
|
360
526
|
return not (
|
|
361
527
|
k in ("type", "properties", "items", "minItems")
|
|
362
|
-
or (k == "additionalProperties" and
|
|
528
|
+
or (k == "additionalProperties" and ctx.location.is_in_header)
|
|
363
529
|
)
|
|
364
530
|
|
|
365
|
-
enabled_keywords = draw(st.shared(FeatureStrategy(), key="keywords"))
|
|
531
|
+
enabled_keywords = draw(st.shared(FeatureStrategy(), key="keywords"))
|
|
366
532
|
candidates = []
|
|
367
|
-
mutation_candidates = [key for key in copied if is_mutation_candidate(key)]
|
|
533
|
+
mutation_candidates = [key for key, value in copied.items() if is_mutation_candidate(key, value)]
|
|
368
534
|
if mutation_candidates:
|
|
369
535
|
# There should be at least one mutated keyword
|
|
370
|
-
candidate = draw(st.sampled_from([key for key in copied if is_mutation_candidate(key)]))
|
|
536
|
+
candidate = draw(st.sampled_from([key for key, value in copied.items() if is_mutation_candidate(key, value)]))
|
|
371
537
|
candidates.append(candidate)
|
|
372
538
|
# If the chosen candidate has dependency, then the dependency should also be present in the final schema
|
|
373
539
|
if candidate in DEPENDENCIES:
|
|
374
540
|
candidates.append(DEPENDENCIES[candidate])
|
|
375
541
|
for key, value in copied.items():
|
|
376
|
-
if is_mutation_candidate(key):
|
|
542
|
+
if is_mutation_candidate(key, value):
|
|
377
543
|
if key in candidates or enabled_keywords.is_enabled(key):
|
|
378
544
|
is_negated = True
|
|
545
|
+
negated_keys.append(key)
|
|
379
546
|
negated = schema.setdefault("not", {})
|
|
380
547
|
negated[key] = value
|
|
381
548
|
if key in DEPENDENCIES:
|
|
382
549
|
# If this keyword has a dependency, then it should be also negated
|
|
383
550
|
dependency = DEPENDENCIES[key]
|
|
384
|
-
if dependency not in negated:
|
|
385
|
-
negated[dependency] = copied[dependency]
|
|
551
|
+
if dependency not in negated and dependency in copied:
|
|
552
|
+
negated[dependency] = copied[dependency]
|
|
386
553
|
else:
|
|
387
554
|
schema[key] = value
|
|
388
555
|
if is_negated:
|
|
389
|
-
|
|
390
|
-
|
|
556
|
+
# Build concise description from negated constraints
|
|
557
|
+
descriptions = []
|
|
558
|
+
for key in negated_keys:
|
|
559
|
+
value = copied[key]
|
|
560
|
+
# Special case: format required properties list nicely with quoted names
|
|
561
|
+
if key == "required" and isinstance(value, list) and len(value) <= 3:
|
|
562
|
+
props = ", ".join(f"`{prop}`" for prop in value)
|
|
563
|
+
descriptions.append(f"`{key}` ({props})")
|
|
564
|
+
else:
|
|
565
|
+
# Default: show `key` (value) for all constraints
|
|
566
|
+
descriptions.append(f"`{key}` ({value})")
|
|
567
|
+
|
|
568
|
+
constraint_desc = ", ".join(descriptions)
|
|
569
|
+
metadata = MutationMetadata(
|
|
570
|
+
parameter=None,
|
|
571
|
+
description=f"Violates {constraint_desc}",
|
|
572
|
+
location=None,
|
|
573
|
+
)
|
|
574
|
+
return MutationResult.SUCCESS, metadata
|
|
575
|
+
return MutationResult.FAILURE, None
|
|
391
576
|
|
|
392
577
|
|
|
393
578
|
DEPENDENCIES = {"exclusiveMaximum": "maximum", "exclusiveMinimum": "minimum"}
|
|
394
579
|
|
|
395
580
|
|
|
396
|
-
def get_mutations(draw: Draw, schema:
|
|
581
|
+
def get_mutations(draw: Draw, schema: JsonSchemaObject) -> tuple[Mutation, ...]:
|
|
397
582
|
"""Get mutations possible for a schema."""
|
|
398
583
|
types = get_type(schema)
|
|
399
584
|
# On the top-level of Open API schemas, types are always strings, but inside "schema" objects, they are the same as
|
|
400
585
|
# in JSON Schema, where it could be either a string or an array of strings.
|
|
401
|
-
options:
|
|
586
|
+
options: list[Mutation]
|
|
587
|
+
if list(schema) == ["type"]:
|
|
588
|
+
# When there is only `type` in schema then `negate_constraints` is not applicable
|
|
589
|
+
options = [change_type]
|
|
590
|
+
else:
|
|
591
|
+
options = [negate_constraints, change_type]
|
|
402
592
|
if "object" in types:
|
|
403
593
|
options.extend([change_properties, remove_required_property])
|
|
404
594
|
elif "array" in types:
|
|
@@ -410,7 +600,7 @@ def ident(x: T) -> T:
|
|
|
410
600
|
return x
|
|
411
601
|
|
|
412
602
|
|
|
413
|
-
def ordered(items: Sequence[T], unique_by: Callable[[T], Any] = ident) -> st.SearchStrategy[
|
|
603
|
+
def ordered(items: Sequence[T], unique_by: Callable[[T], Any] = ident) -> st.SearchStrategy[list[T]]:
|
|
414
604
|
"""Returns a strategy that generates randomly ordered lists of T.
|
|
415
605
|
|
|
416
606
|
NOTE. Items should be unique.
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
from
|
|
1
|
+
from hypothesis_jsonschema._canonicalise import canonicalish
|
|
2
2
|
|
|
3
3
|
from .types import Schema
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
def
|
|
7
|
-
|
|
8
|
-
if isinstance(type_, str):
|
|
9
|
-
return [type_]
|
|
10
|
-
return type_
|
|
6
|
+
def can_negate(schema: Schema) -> bool:
|
|
7
|
+
return canonicalish(schema) != {}
|