schemathesis 4.0.0a10__py3-none-any.whl → 4.0.0a12__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 +29 -30
- schemathesis/auths.py +65 -24
- schemathesis/checks.py +73 -39
- schemathesis/cli/commands/__init__.py +51 -3
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +163 -274
- schemathesis/cli/commands/run/context.py +8 -4
- schemathesis/cli/commands/run/events.py +11 -1
- schemathesis/cli/commands/run/executor.py +70 -78
- schemathesis/cli/commands/run/filters.py +15 -165
- schemathesis/cli/commands/run/handlers/cassettes.py +105 -104
- schemathesis/cli/commands/run/handlers/junitxml.py +5 -4
- schemathesis/cli/commands/run/handlers/output.py +195 -121
- schemathesis/cli/commands/run/loaders.py +35 -50
- schemathesis/cli/commands/run/validation.py +52 -162
- schemathesis/cli/core.py +5 -3
- schemathesis/cli/ext/fs.py +7 -5
- schemathesis/cli/ext/options.py +0 -21
- 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 +523 -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 +2 -0
- schemathesis/core/compat.py +16 -9
- schemathesis/core/errors.py +24 -4
- schemathesis/core/failures.py +6 -7
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/output/__init__.py +14 -37
- schemathesis/core/output/sanitization.py +3 -146
- schemathesis/core/transport.py +36 -1
- schemathesis/core/validation.py +16 -0
- schemathesis/engine/__init__.py +2 -4
- schemathesis/engine/context.py +42 -43
- schemathesis/engine/core.py +7 -5
- schemathesis/engine/errors.py +60 -1
- schemathesis/engine/events.py +10 -2
- schemathesis/engine/phases/__init__.py +10 -0
- schemathesis/engine/phases/probes.py +11 -8
- schemathesis/engine/phases/stateful/__init__.py +2 -1
- schemathesis/engine/phases/stateful/_executor.py +104 -46
- schemathesis/engine/phases/stateful/context.py +2 -2
- schemathesis/engine/phases/unit/__init__.py +23 -15
- schemathesis/engine/phases/unit/_executor.py +110 -21
- schemathesis/engine/phases/unit/_pool.py +1 -1
- schemathesis/errors.py +2 -0
- schemathesis/filters.py +2 -3
- schemathesis/generation/__init__.py +5 -33
- schemathesis/generation/case.py +6 -3
- schemathesis/generation/coverage.py +154 -124
- schemathesis/generation/hypothesis/builder.py +70 -20
- schemathesis/generation/meta.py +3 -3
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +0 -8
- schemathesis/generation/overrides.py +37 -1
- schemathesis/generation/stateful/__init__.py +4 -0
- schemathesis/generation/stateful/state_machine.py +9 -1
- schemathesis/graphql/loaders.py +159 -16
- schemathesis/hooks.py +62 -35
- schemathesis/openapi/checks.py +12 -8
- schemathesis/openapi/generation/filters.py +10 -8
- schemathesis/openapi/loaders.py +142 -17
- schemathesis/pytest/lazy.py +2 -5
- schemathesis/pytest/loaders.py +24 -0
- schemathesis/pytest/plugin.py +33 -2
- schemathesis/schemas.py +21 -66
- schemathesis/specs/graphql/scalars.py +37 -3
- schemathesis/specs/graphql/schemas.py +23 -18
- schemathesis/specs/openapi/_hypothesis.py +26 -28
- schemathesis/specs/openapi/checks.py +37 -36
- schemathesis/specs/openapi/examples.py +4 -3
- schemathesis/specs/openapi/formats.py +32 -5
- schemathesis/specs/openapi/media_types.py +44 -1
- schemathesis/specs/openapi/negative/__init__.py +2 -2
- schemathesis/specs/openapi/patterns.py +46 -16
- schemathesis/specs/openapi/references.py +2 -3
- schemathesis/specs/openapi/schemas.py +19 -22
- schemathesis/specs/openapi/stateful/__init__.py +12 -6
- schemathesis/transport/__init__.py +54 -16
- schemathesis/transport/prepare.py +38 -13
- schemathesis/transport/requests.py +12 -9
- schemathesis/transport/wsgi.py +11 -12
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/METADATA +50 -97
- schemathesis-4.0.0a12.dist-info/RECORD +164 -0
- schemathesis/cli/commands/run/checks.py +0 -79
- schemathesis/cli/commands/run/hypothesis.py +0 -78
- schemathesis/cli/commands/run/reports.py +0 -72
- schemathesis/cli/hooks.py +0 -36
- schemathesis/contrib/__init__.py +0 -9
- schemathesis/contrib/openapi/__init__.py +0 -9
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -20
- schemathesis/engine/config.py +0 -59
- schemathesis/experimental/__init__.py +0 -72
- schemathesis/generation/targets.py +0 -69
- schemathesis-4.0.0a10.dist-info/RECORD +0 -153
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/licenses/LICENSE +0 -0
@@ -1,16 +1,13 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import random
|
4
|
-
from dataclasses import dataclass, field
|
5
|
-
from typing import TYPE_CHECKING
|
6
4
|
|
7
|
-
from schemathesis.generation.modes import GenerationMode
|
5
|
+
from schemathesis.generation.modes import GenerationMode
|
8
6
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
DEFAULT_GENERATOR_MODES = (GenerationMode.default(),)
|
7
|
+
__all__ = [
|
8
|
+
"GenerationMode",
|
9
|
+
"generate_random_case_id",
|
10
|
+
]
|
14
11
|
|
15
12
|
|
16
13
|
CASE_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
@@ -26,28 +23,3 @@ def generate_random_case_id(length: int = 6) -> str:
|
|
26
23
|
number, rem = divmod(number, BASE)
|
27
24
|
output += CASE_ID_ALPHABET[rem]
|
28
25
|
return output
|
29
|
-
|
30
|
-
|
31
|
-
@dataclass
|
32
|
-
class HeaderConfig:
|
33
|
-
"""Configuration for generating headers."""
|
34
|
-
|
35
|
-
strategy: SearchStrategy[str] | None = None
|
36
|
-
|
37
|
-
|
38
|
-
@dataclass
|
39
|
-
class GenerationConfig:
|
40
|
-
"""Holds various configuration options relevant for data generation."""
|
41
|
-
|
42
|
-
modes: list[GenerationMode] = field(default_factory=lambda: [GenerationMode.default()])
|
43
|
-
# Allow generating `\x00` bytes in strings
|
44
|
-
allow_x00: bool = True
|
45
|
-
# Allowing using `null` for optional arguments in GraphQL queries
|
46
|
-
graphql_allow_null: bool = True
|
47
|
-
# Generate strings using the given codec
|
48
|
-
codec: str | None = "utf-8"
|
49
|
-
# Whether to generate security parameters
|
50
|
-
with_security_parameters: bool = True
|
51
|
-
# Header generation configuration
|
52
|
-
headers: HeaderConfig = field(default_factory=HeaderConfig)
|
53
|
-
unexpected_methods: set[str] | None = None
|
schemathesis/generation/case.py
CHANGED
@@ -71,7 +71,7 @@ class Case:
|
|
71
71
|
|
72
72
|
def as_curl_command(self, headers: Mapping[str, Any] | None = None, verify: bool = True) -> str:
|
73
73
|
"""Construct a curl command for a given case."""
|
74
|
-
request_data = prepare_request(self, headers, self.operation.schema.
|
74
|
+
request_data = prepare_request(self, headers, config=self.operation.schema.config.output.sanitization)
|
75
75
|
return curl.generate(
|
76
76
|
method=str(request_data.method),
|
77
77
|
url=str(request_data.url),
|
@@ -142,7 +142,9 @@ class Case:
|
|
142
142
|
override=self._override,
|
143
143
|
auth=None,
|
144
144
|
headers=CaseInsensitiveDict(headers) if headers else None,
|
145
|
-
config=
|
145
|
+
config=self.operation.schema.config.checks_config_for(
|
146
|
+
operation=self.operation, phase=self.meta.phase.name.value if self.meta is not None else None
|
147
|
+
),
|
146
148
|
transport_kwargs=transport_kwargs,
|
147
149
|
recorder=None,
|
148
150
|
)
|
@@ -163,8 +165,9 @@ class Case:
|
|
163
165
|
response=response,
|
164
166
|
failures=_failures,
|
165
167
|
curl=curl,
|
166
|
-
config=self.operation.schema.
|
168
|
+
config=self.operation.schema.config.output,
|
167
169
|
)
|
170
|
+
message += "\n\n"
|
168
171
|
raise FailureGroup(_failures, message) from None
|
169
172
|
|
170
173
|
def call_and_validate(
|
@@ -22,6 +22,7 @@ from schemathesis.core.transforms import deepclone
|
|
22
22
|
from schemathesis.core.validation import has_invalid_characters, is_latin_1_encodable
|
23
23
|
from schemathesis.generation import GenerationMode
|
24
24
|
from schemathesis.generation.hypothesis import examples
|
25
|
+
from schemathesis.openapi.generation.filters import is_invalid_path_parameter
|
25
26
|
|
26
27
|
from ..specs.openapi.converter import update_pattern_in_schema
|
27
28
|
from ..specs.openapi.formats import STRING_FORMATS, get_default_format_strategies
|
@@ -120,7 +121,7 @@ class CoverageContext:
|
|
120
121
|
path: list[str | int] | None = None,
|
121
122
|
) -> None:
|
122
123
|
self.location = location
|
123
|
-
self.generation_modes = generation_modes if generation_modes is not None else GenerationMode
|
124
|
+
self.generation_modes = generation_modes if generation_modes is not None else list(GenerationMode)
|
124
125
|
self.path = path or []
|
125
126
|
|
126
127
|
@contextmanager
|
@@ -152,6 +153,8 @@ class CoverageContext:
|
|
152
153
|
def is_valid_for_location(self, value: Any) -> bool:
|
153
154
|
if self.location in ("header", "cookie") and isinstance(value, str):
|
154
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)
|
155
158
|
return True
|
156
159
|
|
157
160
|
def generate_from(self, strategy: st.SearchStrategy) -> Any:
|
@@ -252,6 +255,24 @@ def _to_hashable_key(value: T, _encode: Callable = _encode) -> tuple[type, str |
|
|
252
255
|
return (type(value), value)
|
253
256
|
|
254
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
|
+
|
255
276
|
def _cover_positive_for_type(
|
256
277
|
ctx: CoverageContext, schema: dict, ty: str | None
|
257
278
|
) -> Generator[GeneratedValue, None, None]:
|
@@ -325,10 +346,10 @@ def _ignore_unfixable(
|
|
325
346
|
|
326
347
|
|
327
348
|
def cover_schema_iter(
|
328
|
-
ctx: CoverageContext, schema: dict | bool, seen:
|
349
|
+
ctx: CoverageContext, schema: dict | bool, seen: HashSet | None = None
|
329
350
|
) -> Generator[GeneratedValue, None, None]:
|
330
351
|
if seen is None:
|
331
|
-
seen =
|
352
|
+
seen = HashSet()
|
332
353
|
if isinstance(schema, bool):
|
333
354
|
types = ["null", "boolean", "string", "number", "array", "object"]
|
334
355
|
schema = {}
|
@@ -351,12 +372,9 @@ def cover_schema_iter(
|
|
351
372
|
yield from _negative_enum(ctx, value, seen)
|
352
373
|
elif key == "const":
|
353
374
|
for value_ in _negative_enum(ctx, [value], seen):
|
354
|
-
|
355
|
-
if k not in seen:
|
356
|
-
yield value_
|
357
|
-
seen.add(k)
|
375
|
+
yield value_
|
358
376
|
elif key == "type":
|
359
|
-
yield from _negative_type(ctx,
|
377
|
+
yield from _negative_type(ctx, value, seen)
|
360
378
|
elif key == "properties":
|
361
379
|
template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
|
362
380
|
yield from _negative_properties(ctx, template, value)
|
@@ -373,37 +391,30 @@ def cover_schema_iter(
|
|
373
391
|
yield from _negative_format(ctx, schema, value)
|
374
392
|
elif key == "maximum":
|
375
393
|
next = value + 1
|
376
|
-
if next
|
394
|
+
if seen.insert(next):
|
377
395
|
yield NegativeValue(next, description="Value greater than maximum", location=ctx.current_path)
|
378
|
-
seen.add(next)
|
379
396
|
elif key == "minimum":
|
380
397
|
next = value - 1
|
381
|
-
if next
|
398
|
+
if seen.insert(next):
|
382
399
|
yield NegativeValue(next, description="Value smaller than minimum", location=ctx.current_path)
|
383
|
-
|
384
|
-
elif key == "exclusiveMaximum" or key == "exclusiveMinimum" and value not in seen:
|
400
|
+
elif key == "exclusiveMaximum" or key == "exclusiveMinimum" and seen.insert(value):
|
385
401
|
verb = "greater" if key == "exclusiveMaximum" else "smaller"
|
386
402
|
limit = "maximum" if key == "exclusiveMaximum" else "minimum"
|
387
403
|
yield NegativeValue(value, description=f"Value {verb} than {limit}", location=ctx.current_path)
|
388
|
-
seen.add(value)
|
389
404
|
elif key == "multipleOf":
|
390
405
|
for value_ in _negative_multiple_of(ctx, schema, value):
|
391
|
-
|
392
|
-
if k not in seen:
|
406
|
+
if seen.insert(value_.value):
|
393
407
|
yield value_
|
394
|
-
seen.add(k)
|
395
408
|
elif key == "minLength" and 0 < value < INTERNAL_BUFFER_SIZE:
|
396
409
|
if value == 1:
|
397
410
|
# In this case, the only possible negative string is an empty one
|
398
411
|
# The `pattern` value may require an non-empty one and the generation will fail
|
399
412
|
# However, it is fine to violate `pattern` here as it is negative string generation anyway
|
400
413
|
value = ""
|
401
|
-
|
402
|
-
if k not in seen:
|
414
|
+
if seen.insert(value):
|
403
415
|
yield NegativeValue(
|
404
416
|
value, description="String smaller than minLength", location=ctx.current_path
|
405
417
|
)
|
406
|
-
seen.add(k)
|
407
418
|
else:
|
408
419
|
with suppress(InvalidArgument):
|
409
420
|
min_length = max_length = value - 1
|
@@ -420,12 +431,10 @@ def cover_schema_iter(
|
|
420
431
|
value = ctx.generate_from_schema(new_schema)
|
421
432
|
else:
|
422
433
|
value = ctx.generate_from_schema(new_schema)
|
423
|
-
|
424
|
-
if k not in seen:
|
434
|
+
if seen.insert(value):
|
425
435
|
yield NegativeValue(
|
426
436
|
value, description="String smaller than minLength", location=ctx.current_path
|
427
437
|
)
|
428
|
-
seen.add(k)
|
429
438
|
elif key == "maxLength" and value < INTERNAL_BUFFER_SIZE:
|
430
439
|
try:
|
431
440
|
min_length = max_length = value + 1
|
@@ -447,12 +456,10 @@ def cover_schema_iter(
|
|
447
456
|
value = ctx.generate_from_schema(new_schema)
|
448
457
|
else:
|
449
458
|
value = ctx.generate_from_schema(new_schema)
|
450
|
-
|
451
|
-
if k not in seen:
|
459
|
+
if seen.insert(value):
|
452
460
|
yield NegativeValue(
|
453
461
|
value, description="String larger than maxLength", location=ctx.current_path
|
454
462
|
)
|
455
|
-
seen.add(k)
|
456
463
|
except (InvalidArgument, Unsatisfiable):
|
457
464
|
pass
|
458
465
|
elif key == "uniqueItems" and value:
|
@@ -484,31 +491,27 @@ def cover_schema_iter(
|
|
484
491
|
|
485
492
|
# Extend the array to be of length value + 1 by repeating its own elements
|
486
493
|
diff = value + 1 - len(array_value)
|
487
|
-
if diff > 0:
|
494
|
+
if diff > 0 and array_value:
|
488
495
|
array_value += (
|
489
496
|
array_value * (diff // len(array_value)) + array_value[: diff % len(array_value)]
|
490
497
|
)
|
491
|
-
|
492
|
-
if k not in seen:
|
498
|
+
if seen.insert(array_value):
|
493
499
|
yield NegativeValue(
|
494
500
|
array_value,
|
495
501
|
description="Array with more items than allowed by maxItems",
|
496
502
|
location=ctx.current_path,
|
497
503
|
)
|
498
|
-
seen.add(k)
|
499
504
|
else:
|
500
505
|
try:
|
501
506
|
# Force the array to have one more item than allowed
|
502
507
|
new_schema = {**schema, "minItems": value + 1, "maxItems": value + 1, "type": "array"}
|
503
508
|
array_value = ctx.generate_from_schema(new_schema)
|
504
|
-
|
505
|
-
if k not in seen:
|
509
|
+
if seen.insert(array_value):
|
506
510
|
yield NegativeValue(
|
507
511
|
array_value,
|
508
512
|
description="Array with more items than allowed by maxItems",
|
509
513
|
location=ctx.current_path,
|
510
514
|
)
|
511
|
-
seen.add(k)
|
512
515
|
except (InvalidArgument, Unsatisfiable):
|
513
516
|
pass
|
514
517
|
elif key == "minItems" and isinstance(value, int) and value > 0:
|
@@ -516,14 +519,12 @@ def cover_schema_iter(
|
|
516
519
|
# Force the array to have one less item than the minimum
|
517
520
|
new_schema = {**schema, "minItems": value - 1, "maxItems": value - 1, "type": "array"}
|
518
521
|
array_value = ctx.generate_from_schema(new_schema)
|
519
|
-
|
520
|
-
if k not in seen:
|
522
|
+
if seen.insert(array_value):
|
521
523
|
yield NegativeValue(
|
522
524
|
array_value,
|
523
525
|
description="Array with fewer items than allowed by minItems",
|
524
526
|
location=ctx.current_path,
|
525
527
|
)
|
526
|
-
seen.add(k)
|
527
528
|
except (InvalidArgument, Unsatisfiable):
|
528
529
|
pass
|
529
530
|
elif (
|
@@ -584,6 +585,22 @@ def _get_template_schema(schema: dict, ty: str) -> dict:
|
|
584
585
|
return {**schema, "type": ty}
|
585
586
|
|
586
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
|
+
|
587
604
|
def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
|
588
605
|
"""Generate positive string values."""
|
589
606
|
# Boundary and near boundary values
|
@@ -591,67 +608,90 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
591
608
|
if min_length == 0:
|
592
609
|
min_length = None
|
593
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
|
+
|
594
617
|
example = schema.get("example")
|
595
618
|
examples = schema.get("examples")
|
596
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
|
+
|
597
625
|
if example or examples or default:
|
598
|
-
|
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
|
599
629
|
yield PositiveValue(example, description="Example value")
|
600
630
|
if examples:
|
601
631
|
for example in examples:
|
602
|
-
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
|
603
634
|
yield PositiveValue(example, description="Example value")
|
604
635
|
if (
|
605
636
|
default
|
606
637
|
and not (example is not None and default == example)
|
607
638
|
and not (examples is not None and any(default == ex for ex in examples))
|
608
639
|
and ctx.is_valid_for_location(default)
|
640
|
+
and seen_values.insert(default)
|
609
641
|
):
|
642
|
+
has_valid_example = True
|
610
643
|
yield PositiveValue(default, description="Default value")
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
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")
|
618
655
|
|
619
656
|
if min_length is not None and min_length < INTERNAL_BUFFER_SIZE:
|
620
657
|
# Exactly the minimum length
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
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")
|
625
664
|
|
626
665
|
# One character more than minimum if possible
|
627
666
|
larger = min_length + 1
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
)
|
633
|
-
|
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")
|
634
673
|
|
635
674
|
if max_length is not None:
|
636
675
|
# Exactly the maximum length
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
)
|
641
|
-
|
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")
|
642
682
|
|
643
683
|
# One character less than maximum if possible
|
644
684
|
smaller = max_length - 1
|
685
|
+
key = (smaller, smaller)
|
645
686
|
if (
|
646
687
|
smaller < INTERNAL_BUFFER_SIZE
|
647
|
-
and
|
688
|
+
and key not in seen_constraints
|
648
689
|
and (smaller > 0 and (min_length is None or smaller >= min_length))
|
649
690
|
):
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
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")
|
655
695
|
|
656
696
|
|
657
697
|
def closest_multiple_greater_than(y: int, x: int) -> int:
|
@@ -678,23 +718,26 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
678
718
|
examples = schema.get("examples")
|
679
719
|
default = schema.get("default")
|
680
720
|
|
721
|
+
seen = HashSet()
|
722
|
+
|
681
723
|
if example or examples or default:
|
682
|
-
if example:
|
724
|
+
if example and seen.insert(example):
|
683
725
|
yield PositiveValue(example, description="Example value")
|
684
726
|
if examples:
|
685
727
|
for example in examples:
|
686
|
-
|
728
|
+
if seen.insert(example):
|
729
|
+
yield PositiveValue(example, description="Example value")
|
687
730
|
if (
|
688
731
|
default
|
689
732
|
and not (example is not None and default == example)
|
690
733
|
and not (examples is not None and any(default == ex for ex in examples))
|
734
|
+
and seen.insert(default)
|
691
735
|
):
|
692
736
|
yield PositiveValue(default, description="Default value")
|
693
737
|
elif not minimum and not maximum:
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
seen = set()
|
738
|
+
value = ctx.generate_from_schema(schema)
|
739
|
+
seen.insert(value)
|
740
|
+
yield PositiveValue(value, description="Valid number")
|
698
741
|
|
699
742
|
if minimum is not None:
|
700
743
|
# Exactly the minimum
|
@@ -702,16 +745,15 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
702
745
|
smallest = closest_multiple_greater_than(minimum, multiple_of)
|
703
746
|
else:
|
704
747
|
smallest = minimum
|
705
|
-
seen.
|
706
|
-
|
748
|
+
if seen.insert(smallest):
|
749
|
+
yield PositiveValue(smallest, description="Minimum value")
|
707
750
|
|
708
751
|
# One more than minimum if possible
|
709
752
|
if multiple_of is not None:
|
710
753
|
larger = smallest + multiple_of
|
711
754
|
else:
|
712
755
|
larger = minimum + 1
|
713
|
-
if
|
714
|
-
seen.add(larger)
|
756
|
+
if (not maximum or larger <= maximum) and seen.insert(larger):
|
715
757
|
yield PositiveValue(larger, description="Near-boundary number")
|
716
758
|
|
717
759
|
if maximum is not None:
|
@@ -720,8 +762,7 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
720
762
|
largest = maximum - (maximum % multiple_of)
|
721
763
|
else:
|
722
764
|
largest = maximum
|
723
|
-
if largest
|
724
|
-
seen.add(largest)
|
765
|
+
if seen.insert(largest):
|
725
766
|
yield PositiveValue(largest, description="Maximum value")
|
726
767
|
|
727
768
|
# One less than maximum if possible
|
@@ -729,57 +770,53 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
729
770
|
smaller = largest - multiple_of
|
730
771
|
else:
|
731
772
|
smaller = maximum - 1
|
732
|
-
if
|
733
|
-
seen.add(smaller)
|
773
|
+
if (smaller > 0 and (minimum is None or smaller >= minimum)) and seen.insert(smaller):
|
734
774
|
yield PositiveValue(smaller, description="Near-boundary number")
|
735
775
|
|
736
776
|
|
737
777
|
def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Generator[GeneratedValue, None, None]:
|
738
|
-
seen = set()
|
739
778
|
example = schema.get("example")
|
740
779
|
examples = schema.get("examples")
|
741
780
|
default = schema.get("default")
|
742
781
|
|
782
|
+
seen = HashSet()
|
783
|
+
seen_constraints: set[tuple] = set()
|
784
|
+
|
743
785
|
if example or examples or default:
|
744
|
-
if example:
|
745
|
-
seen.add(_to_hashable_key(example))
|
786
|
+
if example and seen.insert(example):
|
746
787
|
yield PositiveValue(example, description="Example value")
|
747
788
|
if examples:
|
748
789
|
for example in examples:
|
749
|
-
seen.
|
750
|
-
|
790
|
+
if seen.insert(example):
|
791
|
+
yield PositiveValue(example, description="Example value")
|
751
792
|
if (
|
752
793
|
default
|
753
794
|
and not (example is not None and default == example)
|
754
795
|
and not (examples is not None and any(default == ex for ex in examples))
|
796
|
+
and seen.insert(default)
|
755
797
|
):
|
756
|
-
seen.add(_to_hashable_key(default))
|
757
798
|
yield PositiveValue(default, description="Default value")
|
758
|
-
|
799
|
+
elif seen.insert(template):
|
759
800
|
yield PositiveValue(template, description="Valid array")
|
760
|
-
seen.add(_to_hashable_key(template))
|
761
801
|
|
762
802
|
# Boundary and near-boundary sizes
|
763
803
|
min_items = schema.get("minItems")
|
764
804
|
max_items = schema.get("maxItems")
|
765
805
|
if min_items is not None:
|
766
806
|
# Do not generate an array with `minItems` length, because it is already covered by `template`
|
767
|
-
|
768
807
|
# One item more than minimum if possible
|
769
808
|
larger = min_items + 1
|
770
|
-
if
|
809
|
+
if (max_items is None or larger <= max_items) and larger not in seen_constraints:
|
810
|
+
seen_constraints.add(larger)
|
771
811
|
value = ctx.generate_from_schema({**schema, "minItems": larger, "maxItems": larger})
|
772
|
-
|
773
|
-
if key not in seen:
|
774
|
-
seen.add(key)
|
812
|
+
if seen.insert(value):
|
775
813
|
yield PositiveValue(value, description="Near-boundary items array")
|
776
814
|
|
777
815
|
if max_items is not None:
|
778
|
-
if max_items < INTERNAL_BUFFER_SIZE and max_items not in
|
816
|
+
if max_items < INTERNAL_BUFFER_SIZE and max_items not in seen_constraints:
|
817
|
+
seen_constraints.add(max_items)
|
779
818
|
value = ctx.generate_from_schema({**schema, "minItems": max_items})
|
780
|
-
|
781
|
-
if key not in seen:
|
782
|
-
seen.add(key)
|
819
|
+
if seen.insert(value):
|
783
820
|
yield PositiveValue(value, description="Maximum items array")
|
784
821
|
|
785
822
|
# One item smaller than maximum if possible
|
@@ -787,13 +824,11 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
|
|
787
824
|
if (
|
788
825
|
smaller < INTERNAL_BUFFER_SIZE
|
789
826
|
and smaller > 0
|
790
|
-
and smaller not in seen
|
791
827
|
and (min_items is None or smaller >= min_items)
|
828
|
+
and smaller not in seen_constraints
|
792
829
|
):
|
793
830
|
value = ctx.generate_from_schema({**schema, "minItems": smaller, "maxItems": smaller})
|
794
|
-
|
795
|
-
if key not in seen:
|
796
|
-
seen.add(key)
|
831
|
+
if seen.insert(value):
|
797
832
|
yield PositiveValue(value, description="Near-boundary items array")
|
798
833
|
|
799
834
|
if "items" in schema and "enum" in schema["items"] and isinstance(schema["items"]["enum"], list) and max_items != 0:
|
@@ -801,9 +836,7 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
|
|
801
836
|
length = min_items or 1
|
802
837
|
for variant in schema["items"]["enum"]:
|
803
838
|
value = [variant] * length
|
804
|
-
|
805
|
-
if key not in seen:
|
806
|
-
seen.add(key)
|
839
|
+
if seen.insert(value):
|
807
840
|
yield PositiveValue(value, description="Enum value from available for items array")
|
808
841
|
elif min_items is None and max_items is None and "items" in schema and isinstance(schema["items"], dict):
|
809
842
|
# Otherwise only an empty array is generated
|
@@ -850,16 +883,14 @@ def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Gene
|
|
850
883
|
if set(properties) != required:
|
851
884
|
only_required = {k: v for k, v in template.items() if k in required}
|
852
885
|
yield PositiveValue(only_required, description="Object with only required properties")
|
853
|
-
seen =
|
886
|
+
seen = HashSet()
|
854
887
|
for name, sub_schema in properties.items():
|
855
|
-
seen.
|
888
|
+
seen.insert(template.get(name))
|
856
889
|
for new in cover_schema_iter(ctx, sub_schema):
|
857
|
-
|
858
|
-
if key not in seen:
|
890
|
+
if seen.insert(new.value):
|
859
891
|
yield PositiveValue(
|
860
892
|
{**template, name: new.value}, description=f"Object with valid '{name}' value: {new.description}"
|
861
893
|
)
|
862
|
-
seen.add(key)
|
863
894
|
seen.clear()
|
864
895
|
|
865
896
|
|
@@ -868,14 +899,11 @@ def select_combinations(optional: list[str]) -> Iterator[tuple[str, ...]]:
|
|
868
899
|
yield next(combinations(optional, size))
|
869
900
|
|
870
901
|
|
871
|
-
def _negative_enum(
|
872
|
-
ctx: CoverageContext, value: list, seen: set[Any | tuple[type, str]]
|
873
|
-
) -> Generator[GeneratedValue, None, None]:
|
902
|
+
def _negative_enum(ctx: CoverageContext, value: list, seen: HashSet) -> Generator[GeneratedValue, None, None]:
|
874
903
|
def is_not_in_value(x: Any) -> bool:
|
875
904
|
if x in value or not ctx.is_valid_for_location(x):
|
876
905
|
return False
|
877
|
-
|
878
|
-
return _hashed not in seen
|
906
|
+
return seen.insert(x)
|
879
907
|
|
880
908
|
strategy = (
|
881
909
|
st.text(alphabet=st.characters(min_codepoint=65, max_codepoint=122, categories=["L"]), min_size=3)
|
@@ -883,10 +911,11 @@ def _negative_enum(
|
|
883
911
|
| st.booleans()
|
884
912
|
| NUMERIC_STRATEGY
|
885
913
|
).filter(is_not_in_value)
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
914
|
+
yield NegativeValue(
|
915
|
+
ctx.generate_from(strategy),
|
916
|
+
description="Invalid enum value",
|
917
|
+
location=ctx.current_path,
|
918
|
+
)
|
890
919
|
|
891
920
|
|
892
921
|
def _negative_properties(
|
@@ -1015,7 +1044,11 @@ def _is_non_integer_float(x: float) -> bool:
|
|
1015
1044
|
return x != int(x)
|
1016
1045
|
|
1017
1046
|
|
1018
|
-
def _negative_type(
|
1047
|
+
def _negative_type(
|
1048
|
+
ctx: CoverageContext,
|
1049
|
+
ty: str | list[str],
|
1050
|
+
seen: HashSet,
|
1051
|
+
) -> Generator[GeneratedValue, None, None]:
|
1019
1052
|
if isinstance(ty, str):
|
1020
1053
|
types = [ty]
|
1021
1054
|
else:
|
@@ -1027,11 +1060,8 @@ def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Gene
|
|
1027
1060
|
strategies["number"] = FLOAT_STRATEGY.filter(_is_non_integer_float)
|
1028
1061
|
for strategy in strategies.values():
|
1029
1062
|
value = ctx.generate_from(strategy)
|
1030
|
-
|
1031
|
-
|
1032
|
-
continue
|
1033
|
-
yield NegativeValue(value, description="Incorrect type", location=ctx.current_path)
|
1034
|
-
seen.add(hashed)
|
1063
|
+
if seen.insert(value):
|
1064
|
+
yield NegativeValue(value, description="Incorrect type", location=ctx.current_path)
|
1035
1065
|
|
1036
1066
|
|
1037
1067
|
def push_examples_to_properties(schema: dict[str, Any]) -> None:
|