schemathesis 4.1.4__py3-none-any.whl → 4.2.1__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/cli/commands/run/executor.py +1 -1
- schemathesis/cli/commands/run/handlers/base.py +28 -1
- schemathesis/cli/commands/run/handlers/cassettes.py +109 -137
- schemathesis/cli/commands/run/handlers/junitxml.py +5 -6
- schemathesis/cli/commands/run/handlers/output.py +7 -1
- schemathesis/cli/ext/fs.py +1 -1
- schemathesis/config/_diff_base.py +3 -1
- schemathesis/config/_operations.py +2 -0
- schemathesis/config/_phases.py +21 -4
- schemathesis/config/_projects.py +10 -2
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/errors.py +29 -5
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +163 -0
- schemathesis/{specs/openapi/constants.py → core/jsonschema/keywords.py} +0 -8
- schemathesis/core/jsonschema/references.py +122 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/media_types.py +6 -4
- schemathesis/core/parameters.py +37 -0
- schemathesis/core/transforms.py +25 -2
- schemathesis/core/validation.py +19 -0
- schemathesis/engine/context.py +1 -1
- schemathesis/engine/errors.py +11 -18
- schemathesis/engine/phases/stateful/_executor.py +1 -1
- schemathesis/engine/phases/unit/_executor.py +30 -13
- schemathesis/errors.py +4 -0
- schemathesis/filters.py +2 -2
- schemathesis/generation/coverage.py +87 -11
- schemathesis/generation/hypothesis/__init__.py +79 -2
- schemathesis/generation/hypothesis/builder.py +108 -70
- schemathesis/generation/meta.py +5 -14
- schemathesis/generation/overrides.py +17 -17
- schemathesis/pytest/lazy.py +1 -1
- schemathesis/pytest/plugin.py +1 -6
- schemathesis/schemas.py +22 -72
- schemathesis/specs/graphql/schemas.py +27 -16
- schemathesis/specs/openapi/_hypothesis.py +83 -68
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +504 -0
- schemathesis/specs/openapi/adapter/protocol.py +57 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +329 -0
- schemathesis/specs/openapi/adapter/security.py +141 -0
- schemathesis/specs/openapi/adapter/v2.py +28 -0
- schemathesis/specs/openapi/adapter/v3_0.py +28 -0
- schemathesis/specs/openapi/adapter/v3_1.py +28 -0
- schemathesis/specs/openapi/checks.py +99 -90
- schemathesis/specs/openapi/converter.py +114 -27
- schemathesis/specs/openapi/examples.py +210 -168
- schemathesis/specs/openapi/negative/__init__.py +12 -7
- schemathesis/specs/openapi/negative/mutations.py +68 -40
- schemathesis/specs/openapi/references.py +2 -175
- schemathesis/specs/openapi/schemas.py +142 -490
- schemathesis/specs/openapi/serialization.py +15 -7
- schemathesis/specs/openapi/stateful/__init__.py +17 -12
- schemathesis/specs/openapi/stateful/inference.py +13 -11
- schemathesis/specs/openapi/stateful/links.py +5 -20
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/v3.py +68 -0
- schemathesis/specs/openapi/utils.py +1 -13
- schemathesis/transport/requests.py +3 -11
- schemathesis/transport/serialization.py +63 -27
- schemathesis/transport/wsgi.py +1 -8
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.1.dist-info}/METADATA +2 -2
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.1.dist-info}/RECORD +68 -53
- schemathesis/specs/openapi/parameters.py +0 -405
- schemathesis/specs/openapi/security.py +0 -162
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.1.dist-info}/WHEEL +0 -0
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.1.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.1.dist-info}/licenses/LICENSE +0 -0
schemathesis/engine/errors.py
CHANGED
@@ -14,9 +14,10 @@ from typing import TYPE_CHECKING, Callable, Iterator, Sequence, cast
|
|
14
14
|
|
15
15
|
from schemathesis import errors
|
16
16
|
from schemathesis.core.errors import (
|
17
|
-
|
17
|
+
InfiniteRecursiveReference,
|
18
18
|
InvalidTransition,
|
19
19
|
SerializationNotPossible,
|
20
|
+
UnresolvableReference,
|
20
21
|
format_exception,
|
21
22
|
get_request_error_extras,
|
22
23
|
get_request_error_message,
|
@@ -28,7 +29,7 @@ if TYPE_CHECKING:
|
|
28
29
|
import requests
|
29
30
|
from requests.exceptions import ChunkedEncodingError
|
30
31
|
|
31
|
-
__all__ = ["EngineErrorInfo", "DeadlineExceeded", "
|
32
|
+
__all__ = ["EngineErrorInfo", "DeadlineExceeded", "UnexpectedError"]
|
32
33
|
|
33
34
|
|
34
35
|
class DeadlineExceeded(errors.SchemathesisError):
|
@@ -43,13 +44,6 @@ class DeadlineExceeded(errors.SchemathesisError):
|
|
43
44
|
)
|
44
45
|
|
45
46
|
|
46
|
-
class UnsupportedRecursiveReference(errors.SchemathesisError):
|
47
|
-
"""Recursive reference is impossible to resolve due to current limitations."""
|
48
|
-
|
49
|
-
def __init__(self) -> None:
|
50
|
-
super().__init__(RECURSIVE_REFERENCE_ERROR_MESSAGE)
|
51
|
-
|
52
|
-
|
53
47
|
class UnexpectedError(errors.SchemathesisError):
|
54
48
|
"""An unexpected error during the engine execution.
|
55
49
|
|
@@ -103,7 +97,6 @@ class EngineErrorInfo:
|
|
103
97
|
return "Schema Error"
|
104
98
|
|
105
99
|
return {
|
106
|
-
RuntimeErrorKind.SCHEMA_UNSUPPORTED: "Unsupported Schema",
|
107
100
|
RuntimeErrorKind.SCHEMA_NO_LINKS_FOUND: "Missing Open API links",
|
108
101
|
RuntimeErrorKind.SCHEMA_INVALID_STATE_MACHINE: "Invalid OpenAPI Links Definition",
|
109
102
|
RuntimeErrorKind.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR: "Unknown GraphQL Scalar",
|
@@ -120,9 +113,6 @@ class EngineErrorInfo:
|
|
120
113
|
if isinstance(self._error, requests.RequestException):
|
121
114
|
return get_request_error_message(self._error)
|
122
115
|
|
123
|
-
if self._kind == RuntimeErrorKind.SCHEMA_UNSUPPORTED:
|
124
|
-
return str(self._error).strip()
|
125
|
-
|
126
116
|
if self._kind == RuntimeErrorKind.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR and isinstance(
|
127
117
|
self._error, hypothesis.errors.InvalidArgument
|
128
118
|
):
|
@@ -175,7 +165,8 @@ class EngineErrorInfo:
|
|
175
165
|
return self._kind not in (
|
176
166
|
RuntimeErrorKind.SCHEMA_INVALID_REGULAR_EXPRESSION,
|
177
167
|
RuntimeErrorKind.SCHEMA_INVALID_STATE_MACHINE,
|
178
|
-
RuntimeErrorKind.
|
168
|
+
RuntimeErrorKind.SCHEMA_INVALID_UNRESOLVABLE_REFERENCE,
|
169
|
+
RuntimeErrorKind.SCHEMA_INVALID_INFINITE_RECURSION,
|
179
170
|
RuntimeErrorKind.SCHEMA_GENERIC,
|
180
171
|
RuntimeErrorKind.SCHEMA_NO_LINKS_FOUND,
|
181
172
|
RuntimeErrorKind.SERIALIZATION_NOT_POSSIBLE,
|
@@ -312,8 +303,9 @@ class RuntimeErrorKind(str, enum.Enum):
|
|
312
303
|
|
313
304
|
SCHEMA_INVALID_REGULAR_EXPRESSION = "schema_invalid_regular_expression"
|
314
305
|
SCHEMA_INVALID_STATE_MACHINE = "schema_invalid_state_machine"
|
306
|
+
SCHEMA_INVALID_INFINITE_RECURSION = "schema_invalid_infinite_recursion"
|
307
|
+
SCHEMA_INVALID_UNRESOLVABLE_REFERENCE = "schema_invalid_unresolvable_reference"
|
315
308
|
SCHEMA_NO_LINKS_FOUND = "schema_no_links_found"
|
316
|
-
SCHEMA_UNSUPPORTED = "schema_unsupported"
|
317
309
|
SCHEMA_GENERIC = "schema_generic"
|
318
310
|
|
319
311
|
SERIALIZATION_NOT_POSSIBLE = "serialization_not_possible"
|
@@ -368,9 +360,10 @@ def _classify(*, error: Exception) -> RuntimeErrorKind:
|
|
368
360
|
return RuntimeErrorKind.SCHEMA_INVALID_STATE_MACHINE
|
369
361
|
if isinstance(error, errors.NoLinksFound):
|
370
362
|
return RuntimeErrorKind.SCHEMA_NO_LINKS_FOUND
|
371
|
-
if isinstance(error,
|
372
|
-
|
373
|
-
|
363
|
+
if isinstance(error, InfiniteRecursiveReference):
|
364
|
+
return RuntimeErrorKind.SCHEMA_INVALID_INFINITE_RECURSION
|
365
|
+
if isinstance(error, UnresolvableReference):
|
366
|
+
return RuntimeErrorKind.SCHEMA_INVALID_UNRESOLVABLE_REFERENCE
|
374
367
|
if isinstance(error, errors.SerializationError):
|
375
368
|
if isinstance(error, errors.UnboundPrefix):
|
376
369
|
return RuntimeErrorKind.SERIALIZATION_UNBOUNDED_PREFIX
|
@@ -46,7 +46,7 @@ from schemathesis.generation.metrics import MetricCollector
|
|
46
46
|
def _get_hypothesis_settings_kwargs_override(settings: hypothesis.settings) -> dict[str, Any]:
|
47
47
|
"""Get the settings that should be overridden to match the defaults for API state machines."""
|
48
48
|
kwargs = {}
|
49
|
-
hypothesis_default = hypothesis.settings()
|
49
|
+
hypothesis_default = hypothesis.settings.get_profile("default")
|
50
50
|
if settings.phases == hypothesis_default.phases:
|
51
51
|
kwargs["phases"] = DEFAULT_STATE_MACHINE_SETTINGS.phases
|
52
52
|
if settings.stateful_step_count == hypothesis_default.stateful_step_count:
|
@@ -8,7 +8,6 @@ from warnings import WarningMessage, catch_warnings
|
|
8
8
|
|
9
9
|
import requests
|
10
10
|
from hypothesis.errors import InvalidArgument
|
11
|
-
from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
|
12
11
|
from jsonschema.exceptions import SchemaError as JsonSchemaError
|
13
12
|
from jsonschema.exceptions import ValidationError
|
14
13
|
from requests.exceptions import ChunkedEncodingError
|
@@ -37,7 +36,6 @@ from schemathesis.engine.errors import (
|
|
37
36
|
TestingState,
|
38
37
|
UnexpectedError,
|
39
38
|
UnrecoverableNetworkError,
|
40
|
-
UnsupportedRecursiveReference,
|
41
39
|
clear_hypothesis_notes,
|
42
40
|
deduplicate_errors,
|
43
41
|
is_unrecoverable_network_error,
|
@@ -47,10 +45,12 @@ from schemathesis.engine.recorder import ScenarioRecorder
|
|
47
45
|
from schemathesis.generation import metrics, overrides
|
48
46
|
from schemathesis.generation.case import Case
|
49
47
|
from schemathesis.generation.hypothesis.builder import (
|
48
|
+
InfiniteRecursiveReferenceMark,
|
50
49
|
InvalidHeadersExampleMark,
|
51
50
|
InvalidRegexMark,
|
52
51
|
MissingPathParameters,
|
53
52
|
NonSerializableMark,
|
53
|
+
UnresolvableReferenceMark,
|
54
54
|
UnsatisfiableExampleMark,
|
55
55
|
)
|
56
56
|
from schemathesis.generation.hypothesis.reporting import ignore_hypothesis_output
|
@@ -173,14 +173,24 @@ def run_test(
|
|
173
173
|
status = Status.ERROR
|
174
174
|
try:
|
175
175
|
operation.schema.validate()
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
176
|
+
# JSON Schema validation can miss it if there is `$ref` adjacent to `type` on older specifications
|
177
|
+
if str(exc).startswith("Unknown type"):
|
178
|
+
yield non_fatal_error(
|
179
|
+
InvalidSchema(
|
180
|
+
message=str(exc),
|
181
|
+
path=operation.path,
|
182
|
+
method=operation.method,
|
183
|
+
)
|
184
|
+
)
|
185
|
+
else:
|
186
|
+
msg = "Unexpected error during testing of this API operation"
|
187
|
+
exc_msg = str(exc)
|
188
|
+
if exc_msg:
|
189
|
+
msg += f": {exc_msg}"
|
190
|
+
try:
|
191
|
+
raise InternalError(msg) from exc
|
192
|
+
except InternalError as exc:
|
193
|
+
yield non_fatal_error(exc)
|
184
194
|
except ValidationError as exc:
|
185
195
|
yield non_fatal_error(
|
186
196
|
InvalidSchema.from_jsonschema_error(
|
@@ -190,9 +200,6 @@ def run_test(
|
|
190
200
|
config=ctx.config.output,
|
191
201
|
)
|
192
202
|
)
|
193
|
-
except HypothesisRefResolutionError:
|
194
|
-
status = Status.ERROR
|
195
|
-
yield non_fatal_error(UnsupportedRecursiveReference())
|
196
203
|
except InvalidArgument as exc:
|
197
204
|
status = Status.ERROR
|
198
205
|
message = get_invalid_regular_expression_message(warnings)
|
@@ -265,6 +272,16 @@ def run_test(
|
|
265
272
|
status = Status.ERROR
|
266
273
|
yield non_fatal_error(missing_path_parameters)
|
267
274
|
|
275
|
+
infinite_recursive_reference = InfiniteRecursiveReferenceMark.get(test_function)
|
276
|
+
if infinite_recursive_reference:
|
277
|
+
status = Status.ERROR
|
278
|
+
yield non_fatal_error(infinite_recursive_reference)
|
279
|
+
|
280
|
+
unresolvable_reference = UnresolvableReferenceMark.get(test_function)
|
281
|
+
if unresolvable_reference:
|
282
|
+
status = Status.ERROR
|
283
|
+
yield non_fatal_error(unresolvable_reference)
|
284
|
+
|
268
285
|
for error in deduplicate_errors(errors):
|
269
286
|
yield non_fatal_error(error)
|
270
287
|
|
schemathesis/errors.py
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
from schemathesis.core.errors import (
|
4
4
|
HookError,
|
5
5
|
IncorrectUsage,
|
6
|
+
InfiniteRecursiveReference,
|
6
7
|
InternalError,
|
7
8
|
InvalidHeadersExample,
|
8
9
|
InvalidRateLimit,
|
@@ -19,11 +20,13 @@ from schemathesis.core.errors import (
|
|
19
20
|
SerializationNotPossible,
|
20
21
|
TransitionValidationError,
|
21
22
|
UnboundPrefix,
|
23
|
+
UnresolvableReference,
|
22
24
|
)
|
23
25
|
|
24
26
|
__all__ = [
|
25
27
|
"HookError",
|
26
28
|
"IncorrectUsage",
|
29
|
+
"InfiniteRecursiveReference",
|
27
30
|
"InternalError",
|
28
31
|
"InvalidHeadersExample",
|
29
32
|
"InvalidRateLimit",
|
@@ -40,4 +43,5 @@ __all__ = [
|
|
40
43
|
"SerializationNotPossible",
|
41
44
|
"TransitionValidationError",
|
42
45
|
"UnboundPrefix",
|
46
|
+
"UnresolvableReference",
|
43
47
|
]
|
schemathesis/filters.py
CHANGED
@@ -382,13 +382,13 @@ def expression_to_filter_function(expression: str) -> Callable[[HasAPIOperation]
|
|
382
382
|
if op == "==":
|
383
383
|
|
384
384
|
def filter_function(ctx: HasAPIOperation) -> bool:
|
385
|
-
definition = ctx.operation.definition.
|
385
|
+
definition = ctx.operation.definition.raw
|
386
386
|
resolved = resolve_pointer(definition, pointer)
|
387
387
|
return resolved == value
|
388
388
|
else:
|
389
389
|
|
390
390
|
def filter_function(ctx: HasAPIOperation) -> bool:
|
391
|
-
definition = ctx.operation.definition.
|
391
|
+
definition = ctx.operation.definition.raw
|
392
392
|
resolved = resolve_pointer(definition, pointer)
|
393
393
|
return resolved != value
|
394
394
|
|
@@ -29,7 +29,8 @@ from hypothesis_jsonschema._canonicalise import canonicalish
|
|
29
29
|
from hypothesis_jsonschema._from_schema import STRING_FORMATS as BUILT_IN_STRING_FORMATS
|
30
30
|
|
31
31
|
from schemathesis.core import INTERNAL_BUFFER_SIZE, NOT_SET
|
32
|
-
from schemathesis.core.compat import RefResolutionError
|
32
|
+
from schemathesis.core.compat import RefResolutionError, RefResolver
|
33
|
+
from schemathesis.core.parameters import ParameterLocation
|
33
34
|
from schemathesis.core.transforms import deepclone
|
34
35
|
from schemathesis.core.validation import contains_unicode_surrogate_pair, has_invalid_characters, is_latin_1_encodable
|
35
36
|
from schemathesis.generation import GenerationMode
|
@@ -121,31 +122,62 @@ def cached_draw(strategy: st.SearchStrategy) -> Any:
|
|
121
122
|
|
122
123
|
@dataclass
|
123
124
|
class CoverageContext:
|
125
|
+
root_schema: dict[str, Any]
|
124
126
|
generation_modes: list[GenerationMode]
|
125
|
-
location:
|
127
|
+
location: ParameterLocation
|
128
|
+
media_type: tuple[str, str] | None
|
126
129
|
is_required: bool
|
127
130
|
path: list[str | int]
|
128
131
|
custom_formats: dict[str, st.SearchStrategy]
|
129
132
|
validator_cls: type[jsonschema.protocols.Validator]
|
130
|
-
|
131
|
-
|
133
|
+
_resolver: RefResolver | None
|
134
|
+
|
135
|
+
__slots__ = (
|
136
|
+
"root_schema",
|
137
|
+
"location",
|
138
|
+
"media_type",
|
139
|
+
"generation_modes",
|
140
|
+
"is_required",
|
141
|
+
"path",
|
142
|
+
"custom_formats",
|
143
|
+
"validator_cls",
|
144
|
+
"_resolver",
|
145
|
+
)
|
132
146
|
|
133
147
|
def __init__(
|
134
148
|
self,
|
135
149
|
*,
|
136
|
-
|
150
|
+
root_schema: dict[str, Any],
|
151
|
+
location: ParameterLocation,
|
152
|
+
media_type: tuple[str, str] | None,
|
137
153
|
generation_modes: list[GenerationMode] | None = None,
|
138
154
|
is_required: bool,
|
139
155
|
path: list[str | int] | None = None,
|
140
156
|
custom_formats: dict[str, st.SearchStrategy],
|
141
157
|
validator_cls: type[jsonschema.protocols.Validator],
|
158
|
+
_resolver: RefResolver | None = None,
|
142
159
|
) -> None:
|
160
|
+
self.root_schema = root_schema
|
143
161
|
self.location = location
|
162
|
+
self.media_type = media_type
|
144
163
|
self.generation_modes = generation_modes if generation_modes is not None else list(GenerationMode)
|
145
164
|
self.is_required = is_required
|
146
165
|
self.path = path or []
|
147
166
|
self.custom_formats = custom_formats
|
148
167
|
self.validator_cls = validator_cls
|
168
|
+
self._resolver = _resolver
|
169
|
+
|
170
|
+
@property
|
171
|
+
def resolver(self) -> RefResolver:
|
172
|
+
"""Lazy-initialized cached resolver."""
|
173
|
+
if self._resolver is None:
|
174
|
+
self._resolver = RefResolver.from_schema(self.root_schema)
|
175
|
+
return cast(RefResolver, self._resolver)
|
176
|
+
|
177
|
+
def resolve_ref(self, ref: str) -> dict | bool:
|
178
|
+
"""Resolve a $ref to its schema definition."""
|
179
|
+
_, resolved = self.resolver.resolve(ref)
|
180
|
+
return resolved
|
149
181
|
|
150
182
|
@contextmanager
|
151
183
|
def at(self, key: str | int) -> Generator[None, None, None]:
|
@@ -161,22 +193,28 @@ class CoverageContext:
|
|
161
193
|
|
162
194
|
def with_positive(self) -> CoverageContext:
|
163
195
|
return CoverageContext(
|
196
|
+
root_schema=self.root_schema,
|
164
197
|
location=self.location,
|
198
|
+
media_type=self.media_type,
|
165
199
|
generation_modes=[GenerationMode.POSITIVE],
|
166
200
|
is_required=self.is_required,
|
167
201
|
path=self.path,
|
168
202
|
custom_formats=self.custom_formats,
|
169
203
|
validator_cls=self.validator_cls,
|
204
|
+
_resolver=self._resolver,
|
170
205
|
)
|
171
206
|
|
172
207
|
def with_negative(self) -> CoverageContext:
|
173
208
|
return CoverageContext(
|
209
|
+
root_schema=self.root_schema,
|
174
210
|
location=self.location,
|
211
|
+
media_type=self.media_type,
|
175
212
|
generation_modes=[GenerationMode.NEGATIVE],
|
176
213
|
is_required=self.is_required,
|
177
214
|
path=self.path,
|
178
215
|
custom_formats=self.custom_formats,
|
179
216
|
validator_cls=self.validator_cls,
|
217
|
+
_resolver=self._resolver,
|
180
218
|
)
|
181
219
|
|
182
220
|
def is_valid_for_location(self, value: Any) -> bool:
|
@@ -195,7 +233,16 @@ class CoverageContext:
|
|
195
233
|
return True
|
196
234
|
|
197
235
|
def will_be_serialized_to_string(self) -> bool:
|
198
|
-
return self.location in ("query", "path", "header", "cookie")
|
236
|
+
return self.location in ("query", "path", "header", "cookie") or (
|
237
|
+
self.location == "body"
|
238
|
+
and self.media_type
|
239
|
+
in frozenset(
|
240
|
+
[
|
241
|
+
("multipart", "form-data"),
|
242
|
+
("application", "x-www-form-urlencoded"),
|
243
|
+
]
|
244
|
+
)
|
245
|
+
)
|
199
246
|
|
200
247
|
def can_be_negated(self, schema: dict[str, Any]) -> bool:
|
201
248
|
# Path, query, header, and cookie parameters will be stringified anyway
|
@@ -213,6 +260,10 @@ class CoverageContext:
|
|
213
260
|
return cached_draw(strategy)
|
214
261
|
|
215
262
|
def generate_from_schema(self, schema: dict | bool) -> Any:
|
263
|
+
if isinstance(schema, dict) and "$ref" in schema:
|
264
|
+
reference = schema["$ref"]
|
265
|
+
# Deep clone to avoid circular references in Python objects
|
266
|
+
schema = deepclone(self.resolve_ref(reference))
|
216
267
|
if isinstance(schema, bool):
|
217
268
|
return 0
|
218
269
|
keys = sorted([k for k in schema if not k.startswith("x-") and k not in ["description", "example", "examples"]])
|
@@ -284,10 +335,17 @@ class CoverageContext:
|
|
284
335
|
)
|
285
336
|
|
286
337
|
if keys == ["allOf"]:
|
338
|
+
for idx, sub_schema in enumerate(schema["allOf"]):
|
339
|
+
if "$ref" in sub_schema:
|
340
|
+
schema["allOf"][idx] = self.resolve_ref(sub_schema["$ref"])
|
341
|
+
|
287
342
|
schema = canonicalish(schema)
|
288
343
|
if isinstance(schema, dict) and "allOf" not in schema:
|
289
344
|
return self.generate_from_schema(schema)
|
290
345
|
|
346
|
+
if isinstance(schema, dict) and "examples" in schema:
|
347
|
+
# Examples may contain binary data which will fail the canonicalisation process in `hypothesis-jsonschema`
|
348
|
+
schema = {key: value for key, value in schema.items() if key != "examples"}
|
291
349
|
return self.generate_from(from_schema(schema, custom_formats=self.custom_formats))
|
292
350
|
|
293
351
|
|
@@ -306,7 +364,7 @@ else:
|
|
306
364
|
|
307
365
|
|
308
366
|
def _encode(o: Any) -> str:
|
309
|
-
return "".join(_iterencode(o,
|
367
|
+
return "".join(_iterencode(o, False))
|
310
368
|
|
311
369
|
|
312
370
|
def _to_hashable_key(value: T, _encode: Callable = _encode) -> tuple[type, str | T]:
|
@@ -360,6 +418,9 @@ def _cover_positive_for_type(
|
|
360
418
|
yield from cover_schema_iter(ctx, all_of[0])
|
361
419
|
else:
|
362
420
|
with suppress(jsonschema.SchemaError):
|
421
|
+
for idx, sub_schema in enumerate(all_of):
|
422
|
+
if "$ref" in sub_schema:
|
423
|
+
all_of[idx] = ctx.resolve_ref(sub_schema["$ref"])
|
363
424
|
canonical = canonicalish(schema)
|
364
425
|
yield from cover_schema_iter(ctx, canonical)
|
365
426
|
if enum is not NOT_SET:
|
@@ -414,6 +475,21 @@ def cover_schema_iter(
|
|
414
475
|
) -> Generator[GeneratedValue, None, None]:
|
415
476
|
if seen is None:
|
416
477
|
seen = HashSet()
|
478
|
+
|
479
|
+
if isinstance(schema, dict) and "$ref" in schema:
|
480
|
+
reference = schema["$ref"]
|
481
|
+
try:
|
482
|
+
resolved = ctx.resolve_ref(reference)
|
483
|
+
if isinstance(resolved, dict):
|
484
|
+
schema = {**resolved, **{k: v for k, v in schema.items() if k != "$ref"}}
|
485
|
+
yield from cover_schema_iter(ctx, schema, seen)
|
486
|
+
else:
|
487
|
+
yield from cover_schema_iter(ctx, resolved, seen)
|
488
|
+
return
|
489
|
+
except RefResolutionError:
|
490
|
+
# Can't resolve a reference - at this point, we can't generate anything useful as `$ref` is in the current schema root
|
491
|
+
return
|
492
|
+
|
417
493
|
if schema == {} or schema is True:
|
418
494
|
types = ["null", "boolean", "string", "number", "array", "object"]
|
419
495
|
schema = {}
|
@@ -881,7 +957,7 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
881
957
|
smaller = largest - multiple_of
|
882
958
|
else:
|
883
959
|
smaller = maximum - 1
|
884
|
-
if (
|
960
|
+
if (minimum is None or smaller >= minimum) and seen.insert(smaller):
|
885
961
|
yield PositiveValue(smaller, description="Near-boundary number")
|
886
962
|
|
887
963
|
|
@@ -1232,7 +1308,7 @@ def _negative_type(
|
|
1232
1308
|
del strategies["integer"]
|
1233
1309
|
if "integer" in types:
|
1234
1310
|
strategies["number"] = FLOAT_STRATEGY.filter(_is_non_integer_float)
|
1235
|
-
if ctx.location ==
|
1311
|
+
if ctx.location == ParameterLocation.QUERY:
|
1236
1312
|
strategies.pop("object", None)
|
1237
1313
|
if filter_func is not None:
|
1238
1314
|
for ty, strategy in strategies.items():
|
@@ -1264,10 +1340,10 @@ def _negative_type(
|
|
1264
1340
|
def _does_not_match_the_original_schema(value: Any) -> bool:
|
1265
1341
|
return not is_valid(str(value))
|
1266
1342
|
|
1267
|
-
if ctx.location ==
|
1343
|
+
if ctx.location == ParameterLocation.PATH:
|
1268
1344
|
for ty, strategy in strategies.items():
|
1269
1345
|
strategies[ty] = strategy.map(jsonify).map(quote_path_parameter)
|
1270
|
-
elif ctx.location ==
|
1346
|
+
elif ctx.location == ParameterLocation.QUERY:
|
1271
1347
|
for ty, strategy in strategies.items():
|
1272
1348
|
strategies[ty] = strategy.map(jsonify)
|
1273
1349
|
|
@@ -1,4 +1,8 @@
|
|
1
|
-
from
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import json
|
4
|
+
from functools import lru_cache
|
5
|
+
from typing import Any, Literal
|
2
6
|
|
3
7
|
|
4
8
|
def setup() -> None:
|
@@ -8,9 +12,12 @@ def setup() -> None:
|
|
8
12
|
from hypothesis.internal.reflection import is_first_param_referenced_in_function
|
9
13
|
from hypothesis.strategies._internal import collections, core
|
10
14
|
from hypothesis.vendor import pretty
|
11
|
-
from hypothesis_jsonschema import _from_schema, _resolve
|
15
|
+
from hypothesis_jsonschema import _canonicalise, _from_schema, _resolve
|
16
|
+
from hypothesis_jsonschema._canonicalise import SCHEMA_KEYS, SCHEMA_OBJECT_KEYS, merged
|
17
|
+
from hypothesis_jsonschema._resolve import LocalResolver
|
12
18
|
|
13
19
|
from schemathesis.core import INTERNAL_BUFFER_SIZE
|
20
|
+
from schemathesis.core.jsonschema.types import _get_type
|
14
21
|
from schemathesis.core.transforms import deepclone
|
15
22
|
|
16
23
|
# Forcefully initializes Hypothesis' global PRNG to avoid races that initialize it
|
@@ -34,9 +41,79 @@ def setup() -> None:
|
|
34
41
|
# depending on the schema size (~300 seconds -> 4.5 seconds in one of the benchmarks)
|
35
42
|
return None
|
36
43
|
|
44
|
+
class CacheableSchema:
|
45
|
+
"""Cache schema by its JSON representation.
|
46
|
+
|
47
|
+
Canonicalisation is not required as schemas with the same JSON representation
|
48
|
+
will have the same validator.
|
49
|
+
"""
|
50
|
+
|
51
|
+
__slots__ = ("schema", "encoded")
|
52
|
+
|
53
|
+
def __init__(self, schema: dict[str, Any]) -> None:
|
54
|
+
self.schema = schema
|
55
|
+
self.encoded = hash(json.dumps(schema, sort_keys=True))
|
56
|
+
|
57
|
+
def __eq__(self, other: "CacheableSchema") -> bool: # type: ignore
|
58
|
+
return self.encoded == other.encoded
|
59
|
+
|
60
|
+
def __hash__(self) -> int:
|
61
|
+
return self.encoded
|
62
|
+
|
63
|
+
SCHEMA_KEYS = frozenset(SCHEMA_KEYS)
|
64
|
+
SCHEMA_OBJECT_KEYS = frozenset(SCHEMA_OBJECT_KEYS)
|
65
|
+
|
66
|
+
@lru_cache()
|
67
|
+
def get_resolver(cache_key: CacheableSchema) -> LocalResolver:
|
68
|
+
"""LRU resolver cache."""
|
69
|
+
return LocalResolver.from_schema(cache_key.schema)
|
70
|
+
|
71
|
+
def resolve_all_refs(
|
72
|
+
schema: Literal[True, False] | dict[str, Any],
|
73
|
+
*,
|
74
|
+
resolver: LocalResolver | None = None,
|
75
|
+
) -> dict[str, Any]:
|
76
|
+
if schema is True:
|
77
|
+
return {}
|
78
|
+
if schema is False:
|
79
|
+
return {"not": {}}
|
80
|
+
if not schema:
|
81
|
+
return schema
|
82
|
+
if resolver is None:
|
83
|
+
resolver = get_resolver(CacheableSchema(schema))
|
84
|
+
|
85
|
+
_resolve_all_refs = resolve_all_refs
|
86
|
+
|
87
|
+
if "$ref" in schema:
|
88
|
+
s = dict(schema)
|
89
|
+
ref = s.pop("$ref")
|
90
|
+
url, resolved = resolver.resolve(ref)
|
91
|
+
resolver.push_scope(url)
|
92
|
+
try:
|
93
|
+
return merged([s, _resolve_all_refs(deepclone(resolved), resolver=resolver)]) # type: ignore
|
94
|
+
finally:
|
95
|
+
resolver.pop_scope()
|
96
|
+
|
97
|
+
for key, value in schema.items():
|
98
|
+
if key in SCHEMA_KEYS:
|
99
|
+
if isinstance(value, list):
|
100
|
+
schema[key] = [_resolve_all_refs(v, resolver=resolver) if isinstance(v, dict) else v for v in value]
|
101
|
+
elif isinstance(value, dict):
|
102
|
+
schema[key] = _resolve_all_refs(value, resolver=resolver)
|
103
|
+
if key in SCHEMA_OBJECT_KEYS:
|
104
|
+
schema[key] = {
|
105
|
+
k: _resolve_all_refs(v, resolver=resolver) if isinstance(v, dict) else v for k, v in value.items()
|
106
|
+
}
|
107
|
+
return schema
|
108
|
+
|
37
109
|
root_core.RepresentationPrinter = RepresentationPrinter # type: ignore
|
38
110
|
_resolve.deepcopy = deepclone # type: ignore
|
111
|
+
_resolve.resolve_all_refs = resolve_all_refs # type: ignore
|
39
112
|
_from_schema.deepcopy = deepclone # type: ignore
|
113
|
+
_from_schema.get_type = _get_type # type: ignore
|
114
|
+
_from_schema.resolve_all_refs = resolve_all_refs # type: ignore
|
115
|
+
_canonicalise.get_type = _get_type # type: ignore
|
116
|
+
_canonicalise.CacheableSchema = CacheableSchema # type: ignore
|
40
117
|
root_core.BUFFER_SIZE = INTERNAL_BUFFER_SIZE # type: ignore
|
41
118
|
engine.BUFFER_SIZE = INTERNAL_BUFFER_SIZE
|
42
119
|
collections.BUFFER_SIZE = INTERNAL_BUFFER_SIZE # type: ignore
|