schemathesis 4.0.0a9__py3-none-any.whl → 4.0.0a11__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 +3 -7
- schemathesis/checks.py +17 -7
- schemathesis/cli/commands/__init__.py +51 -3
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +147 -260
- schemathesis/cli/commands/run/context.py +2 -3
- schemathesis/cli/commands/run/events.py +4 -0
- schemathesis/cli/commands/run/executor.py +60 -73
- schemathesis/cli/commands/run/filters.py +15 -165
- schemathesis/cli/commands/run/handlers/cassettes.py +105 -104
- schemathesis/cli/commands/run/handlers/junitxml.py +6 -5
- schemathesis/cli/commands/run/handlers/output.py +26 -47
- schemathesis/cli/commands/run/loaders.py +35 -50
- schemathesis/cli/commands/run/validation.py +36 -161
- schemathesis/cli/core.py +5 -3
- schemathesis/cli/ext/fs.py +7 -5
- schemathesis/cli/ext/options.py +0 -21
- schemathesis/config/__init__.py +188 -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 +150 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +313 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +151 -0
- schemathesis/config/_projects.py +495 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +116 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/schema.json +837 -0
- schemathesis/core/__init__.py +3 -0
- schemathesis/core/compat.py +16 -9
- schemathesis/core/errors.py +19 -2
- 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/validation.py +16 -0
- schemathesis/engine/__init__.py +2 -4
- schemathesis/engine/context.py +41 -43
- schemathesis/engine/core.py +7 -5
- schemathesis/engine/phases/__init__.py +10 -0
- schemathesis/engine/phases/probes.py +8 -8
- schemathesis/engine/phases/stateful/_executor.py +68 -43
- schemathesis/engine/phases/unit/__init__.py +23 -15
- schemathesis/engine/phases/unit/_executor.py +77 -17
- schemathesis/engine/phases/unit/_pool.py +1 -1
- schemathesis/errors.py +2 -0
- schemathesis/filters.py +2 -3
- schemathesis/generation/__init__.py +6 -31
- schemathesis/generation/case.py +5 -3
- schemathesis/generation/coverage.py +174 -134
- schemathesis/generation/hypothesis/__init__.py +7 -1
- schemathesis/generation/hypothesis/builder.py +40 -14
- schemathesis/generation/meta.py +3 -3
- schemathesis/generation/overrides.py +37 -1
- schemathesis/generation/stateful/state_machine.py +8 -1
- schemathesis/graphql/loaders.py +21 -12
- schemathesis/openapi/checks.py +12 -8
- schemathesis/openapi/generation/filters.py +10 -8
- schemathesis/openapi/loaders.py +22 -13
- schemathesis/pytest/lazy.py +2 -5
- schemathesis/pytest/plugin.py +11 -2
- schemathesis/schemas.py +13 -61
- schemathesis/specs/graphql/schemas.py +11 -15
- schemathesis/specs/openapi/_hypothesis.py +12 -8
- schemathesis/specs/openapi/checks.py +16 -18
- schemathesis/specs/openapi/examples.py +4 -3
- schemathesis/specs/openapi/formats.py +2 -2
- 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 +11 -20
- schemathesis/specs/openapi/stateful/__init__.py +10 -5
- schemathesis/transport/prepare.py +7 -6
- schemathesis/transport/requests.py +3 -1
- schemathesis/transport/wsgi.py +3 -4
- {schemathesis-4.0.0a9.dist-info → schemathesis-4.0.0a11.dist-info}/METADATA +7 -8
- schemathesis-4.0.0a11.dist-info/RECORD +166 -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/engine/config.py +0 -59
- schemathesis/experimental/__init__.py +0 -72
- schemathesis-4.0.0a9.dist-info/RECORD +0 -153
- {schemathesis-4.0.0a9.dist-info → schemathesis-4.0.0a11.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a9.dist-info → schemathesis-4.0.0a11.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.0a9.dist-info → schemathesis-4.0.0a11.dist-info}/licenses/LICENSE +0 -0
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,7 +165,7 @@ 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
|
)
|
168
170
|
raise FailureGroup(_failures, message) from None
|
169
171
|
|
@@ -16,12 +16,13 @@ from hypothesis_jsonschema import from_schema
|
|
16
16
|
from hypothesis_jsonschema._canonicalise import canonicalish
|
17
17
|
from hypothesis_jsonschema._from_schema import STRING_FORMATS as BUILT_IN_STRING_FORMATS
|
18
18
|
|
19
|
-
from schemathesis.core import NOT_SET
|
19
|
+
from schemathesis.core import INTERNAL_BUFFER_SIZE, NOT_SET
|
20
20
|
from schemathesis.core.compat import RefResolutionError
|
21
21
|
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
|
@@ -36,7 +37,6 @@ def json_recursive_strategy(strategy: st.SearchStrategy) -> st.SearchStrategy:
|
|
36
37
|
return st.lists(strategy, max_size=3) | st.dictionaries(st.text(), strategy, max_size=3)
|
37
38
|
|
38
39
|
|
39
|
-
BUFFER_SIZE = 8 * 1024
|
40
40
|
NEGATIVE_MODE_MAX_LENGTH_WITH_PATTERN = 100
|
41
41
|
NEGATIVE_MODE_MAX_ITEMS = 15
|
42
42
|
FLOAT_STRATEGY: st.SearchStrategy = st.floats(allow_nan=False, allow_infinity=False).map(_replace_zero_with_nonzero)
|
@@ -153,6 +153,8 @@ class CoverageContext:
|
|
153
153
|
def is_valid_for_location(self, value: Any) -> bool:
|
154
154
|
if self.location in ("header", "cookie") and isinstance(value, str):
|
155
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)
|
156
158
|
return True
|
157
159
|
|
158
160
|
def generate_from(self, strategy: st.SearchStrategy) -> Any:
|
@@ -161,7 +163,7 @@ class CoverageContext:
|
|
161
163
|
def generate_from_schema(self, schema: dict | bool) -> Any:
|
162
164
|
if isinstance(schema, bool):
|
163
165
|
return 0
|
164
|
-
keys = sorted([k for k in schema if not k.startswith("x-") and k not in ["description", "example"]])
|
166
|
+
keys = sorted([k for k in schema if not k.startswith("x-") and k not in ["description", "example", "examples"]])
|
165
167
|
if keys == ["type"] and isinstance(schema["type"], str) and schema["type"] in STRATEGIES_FOR_TYPE:
|
166
168
|
return cached_draw(STRATEGIES_FOR_TYPE[schema["type"]])
|
167
169
|
if keys == ["format", "type"]:
|
@@ -253,6 +255,24 @@ def _to_hashable_key(value: T, _encode: Callable = _encode) -> tuple[type, str |
|
|
253
255
|
return (type(value), value)
|
254
256
|
|
255
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
|
+
|
256
276
|
def _cover_positive_for_type(
|
257
277
|
ctx: CoverageContext, schema: dict, ty: str | None
|
258
278
|
) -> Generator[GeneratedValue, None, None]:
|
@@ -326,10 +346,10 @@ def _ignore_unfixable(
|
|
326
346
|
|
327
347
|
|
328
348
|
def cover_schema_iter(
|
329
|
-
ctx: CoverageContext, schema: dict | bool, seen:
|
349
|
+
ctx: CoverageContext, schema: dict | bool, seen: HashSet | None = None
|
330
350
|
) -> Generator[GeneratedValue, None, None]:
|
331
351
|
if seen is None:
|
332
|
-
seen =
|
352
|
+
seen = HashSet()
|
333
353
|
if isinstance(schema, bool):
|
334
354
|
types = ["null", "boolean", "string", "number", "array", "object"]
|
335
355
|
schema = {}
|
@@ -352,12 +372,9 @@ def cover_schema_iter(
|
|
352
372
|
yield from _negative_enum(ctx, value, seen)
|
353
373
|
elif key == "const":
|
354
374
|
for value_ in _negative_enum(ctx, [value], seen):
|
355
|
-
|
356
|
-
if k not in seen:
|
357
|
-
yield value_
|
358
|
-
seen.add(k)
|
375
|
+
yield value_
|
359
376
|
elif key == "type":
|
360
|
-
yield from _negative_type(ctx,
|
377
|
+
yield from _negative_type(ctx, value, seen)
|
361
378
|
elif key == "properties":
|
362
379
|
template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
|
363
380
|
yield from _negative_properties(ctx, template, value)
|
@@ -374,37 +391,30 @@ def cover_schema_iter(
|
|
374
391
|
yield from _negative_format(ctx, schema, value)
|
375
392
|
elif key == "maximum":
|
376
393
|
next = value + 1
|
377
|
-
if next
|
394
|
+
if seen.insert(next):
|
378
395
|
yield NegativeValue(next, description="Value greater than maximum", location=ctx.current_path)
|
379
|
-
seen.add(next)
|
380
396
|
elif key == "minimum":
|
381
397
|
next = value - 1
|
382
|
-
if next
|
398
|
+
if seen.insert(next):
|
383
399
|
yield NegativeValue(next, description="Value smaller than minimum", location=ctx.current_path)
|
384
|
-
|
385
|
-
elif key == "exclusiveMaximum" or key == "exclusiveMinimum" and value not in seen:
|
400
|
+
elif key == "exclusiveMaximum" or key == "exclusiveMinimum" and seen.insert(value):
|
386
401
|
verb = "greater" if key == "exclusiveMaximum" else "smaller"
|
387
402
|
limit = "maximum" if key == "exclusiveMaximum" else "minimum"
|
388
403
|
yield NegativeValue(value, description=f"Value {verb} than {limit}", location=ctx.current_path)
|
389
|
-
seen.add(value)
|
390
404
|
elif key == "multipleOf":
|
391
405
|
for value_ in _negative_multiple_of(ctx, schema, value):
|
392
|
-
|
393
|
-
if k not in seen:
|
406
|
+
if seen.insert(value_.value):
|
394
407
|
yield value_
|
395
|
-
|
396
|
-
elif key == "minLength" and 0 < value < BUFFER_SIZE:
|
408
|
+
elif key == "minLength" and 0 < value < INTERNAL_BUFFER_SIZE:
|
397
409
|
if value == 1:
|
398
410
|
# In this case, the only possible negative string is an empty one
|
399
411
|
# The `pattern` value may require an non-empty one and the generation will fail
|
400
412
|
# However, it is fine to violate `pattern` here as it is negative string generation anyway
|
401
413
|
value = ""
|
402
|
-
|
403
|
-
if k not in seen:
|
414
|
+
if seen.insert(value):
|
404
415
|
yield NegativeValue(
|
405
416
|
value, description="String smaller than minLength", location=ctx.current_path
|
406
417
|
)
|
407
|
-
seen.add(k)
|
408
418
|
else:
|
409
419
|
with suppress(InvalidArgument):
|
410
420
|
min_length = max_length = value - 1
|
@@ -421,13 +431,11 @@ def cover_schema_iter(
|
|
421
431
|
value = ctx.generate_from_schema(new_schema)
|
422
432
|
else:
|
423
433
|
value = ctx.generate_from_schema(new_schema)
|
424
|
-
|
425
|
-
if k not in seen:
|
434
|
+
if seen.insert(value):
|
426
435
|
yield NegativeValue(
|
427
436
|
value, description="String smaller than minLength", location=ctx.current_path
|
428
437
|
)
|
429
|
-
|
430
|
-
elif key == "maxLength" and value < BUFFER_SIZE:
|
438
|
+
elif key == "maxLength" and value < INTERNAL_BUFFER_SIZE:
|
431
439
|
try:
|
432
440
|
min_length = max_length = value + 1
|
433
441
|
new_schema = {**schema, "minLength": min_length, "maxLength": max_length}
|
@@ -448,12 +456,10 @@ def cover_schema_iter(
|
|
448
456
|
value = ctx.generate_from_schema(new_schema)
|
449
457
|
else:
|
450
458
|
value = ctx.generate_from_schema(new_schema)
|
451
|
-
|
452
|
-
if k not in seen:
|
459
|
+
if seen.insert(value):
|
453
460
|
yield NegativeValue(
|
454
461
|
value, description="String larger than maxLength", location=ctx.current_path
|
455
462
|
)
|
456
|
-
seen.add(k)
|
457
463
|
except (InvalidArgument, Unsatisfiable):
|
458
464
|
pass
|
459
465
|
elif key == "uniqueItems" and value:
|
@@ -461,7 +467,7 @@ def cover_schema_iter(
|
|
461
467
|
elif key == "required":
|
462
468
|
template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
|
463
469
|
yield from _negative_required(ctx, template, value)
|
464
|
-
elif key == "maxItems" and isinstance(value, int) and value <
|
470
|
+
elif key == "maxItems" and isinstance(value, int) and value < INTERNAL_BUFFER_SIZE:
|
465
471
|
if value > NEGATIVE_MODE_MAX_ITEMS:
|
466
472
|
# It could be extremely slow to generate large arrays
|
467
473
|
# Generate values up to the limit and reuse them to construct the final array
|
@@ -471,34 +477,41 @@ def cover_schema_iter(
|
|
471
477
|
"maxItems": NEGATIVE_MODE_MAX_ITEMS,
|
472
478
|
"type": "array",
|
473
479
|
}
|
474
|
-
|
480
|
+
if "items" in schema and isinstance(schema["items"], dict):
|
481
|
+
# The schema may have another large array nested, therefore generate covering cases
|
482
|
+
# and use them to build an array for the current schema
|
483
|
+
negative = [case.value for case in cover_schema_iter(ctx, schema["items"])]
|
484
|
+
positive = [case.value for case in cover_schema_iter(ctx.with_positive(), schema["items"])]
|
485
|
+
# Interleave positive & negative values
|
486
|
+
array_value = [value for pair in zip(positive, negative) for value in pair][
|
487
|
+
:NEGATIVE_MODE_MAX_ITEMS
|
488
|
+
]
|
489
|
+
else:
|
490
|
+
array_value = ctx.generate_from_schema(new_schema)
|
491
|
+
|
475
492
|
# Extend the array to be of length value + 1 by repeating its own elements
|
476
493
|
diff = value + 1 - len(array_value)
|
477
|
-
if diff > 0:
|
494
|
+
if diff > 0 and array_value:
|
478
495
|
array_value += (
|
479
496
|
array_value * (diff // len(array_value)) + array_value[: diff % len(array_value)]
|
480
497
|
)
|
481
|
-
|
482
|
-
if k not in seen:
|
498
|
+
if seen.insert(array_value):
|
483
499
|
yield NegativeValue(
|
484
500
|
array_value,
|
485
501
|
description="Array with more items than allowed by maxItems",
|
486
502
|
location=ctx.current_path,
|
487
503
|
)
|
488
|
-
seen.add(k)
|
489
504
|
else:
|
490
505
|
try:
|
491
506
|
# Force the array to have one more item than allowed
|
492
507
|
new_schema = {**schema, "minItems": value + 1, "maxItems": value + 1, "type": "array"}
|
493
508
|
array_value = ctx.generate_from_schema(new_schema)
|
494
|
-
|
495
|
-
if k not in seen:
|
509
|
+
if seen.insert(array_value):
|
496
510
|
yield NegativeValue(
|
497
511
|
array_value,
|
498
512
|
description="Array with more items than allowed by maxItems",
|
499
513
|
location=ctx.current_path,
|
500
514
|
)
|
501
|
-
seen.add(k)
|
502
515
|
except (InvalidArgument, Unsatisfiable):
|
503
516
|
pass
|
504
517
|
elif key == "minItems" and isinstance(value, int) and value > 0:
|
@@ -506,14 +519,12 @@ def cover_schema_iter(
|
|
506
519
|
# Force the array to have one less item than the minimum
|
507
520
|
new_schema = {**schema, "minItems": value - 1, "maxItems": value - 1, "type": "array"}
|
508
521
|
array_value = ctx.generate_from_schema(new_schema)
|
509
|
-
|
510
|
-
if k not in seen:
|
522
|
+
if seen.insert(array_value):
|
511
523
|
yield NegativeValue(
|
512
524
|
array_value,
|
513
525
|
description="Array with fewer items than allowed by minItems",
|
514
526
|
location=ctx.current_path,
|
515
527
|
)
|
516
|
-
seen.add(k)
|
517
528
|
except (InvalidArgument, Unsatisfiable):
|
518
529
|
pass
|
519
530
|
elif (
|
@@ -574,6 +585,22 @@ def _get_template_schema(schema: dict, ty: str) -> dict:
|
|
574
585
|
return {**schema, "type": ty}
|
575
586
|
|
576
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
|
+
|
577
604
|
def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
|
578
605
|
"""Generate positive string values."""
|
579
606
|
# Boundary and near boundary values
|
@@ -581,67 +608,90 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
581
608
|
if min_length == 0:
|
582
609
|
min_length = None
|
583
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
|
+
|
584
617
|
example = schema.get("example")
|
585
618
|
examples = schema.get("examples")
|
586
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
|
+
|
587
625
|
if example or examples or default:
|
588
|
-
|
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
|
589
629
|
yield PositiveValue(example, description="Example value")
|
590
630
|
if examples:
|
591
631
|
for example in examples:
|
592
|
-
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
|
593
634
|
yield PositiveValue(example, description="Example value")
|
594
635
|
if (
|
595
636
|
default
|
596
637
|
and not (example is not None and default == example)
|
597
638
|
and not (examples is not None and any(default == ex for ex in examples))
|
598
639
|
and ctx.is_valid_for_location(default)
|
640
|
+
and seen_values.insert(default)
|
599
641
|
):
|
642
|
+
has_valid_example = True
|
600
643
|
yield PositiveValue(default, description="Default value")
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
644
|
+
if not has_valid_example:
|
645
|
+
if not min_length and not max_length or "pattern" in schema:
|
646
|
+
value = ctx.generate_from_schema(schema)
|
647
|
+
seen_values.insert(value)
|
648
|
+
seen_constraints.add((min_length, max_length))
|
649
|
+
yield PositiveValue(value, description="Valid string")
|
650
|
+
elif not min_length and not max_length or "pattern" in schema:
|
651
|
+
value = ctx.generate_from_schema(schema)
|
652
|
+
seen_values.insert(value)
|
653
|
+
seen_constraints.add((min_length, max_length))
|
654
|
+
yield PositiveValue(value, description="Valid string")
|
655
|
+
|
656
|
+
if min_length is not None and min_length < INTERNAL_BUFFER_SIZE:
|
610
657
|
# Exactly the minimum length
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
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")
|
615
664
|
|
616
665
|
# One character more than minimum if possible
|
617
666
|
larger = min_length + 1
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
)
|
623
|
-
|
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")
|
624
673
|
|
625
674
|
if max_length is not None:
|
626
675
|
# Exactly the maximum length
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
)
|
631
|
-
|
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")
|
632
682
|
|
633
683
|
# One character less than maximum if possible
|
634
684
|
smaller = max_length - 1
|
685
|
+
key = (smaller, smaller)
|
635
686
|
if (
|
636
|
-
smaller <
|
637
|
-
and
|
687
|
+
smaller < INTERNAL_BUFFER_SIZE
|
688
|
+
and key not in seen_constraints
|
638
689
|
and (smaller > 0 and (min_length is None or smaller >= min_length))
|
639
690
|
):
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
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")
|
645
695
|
|
646
696
|
|
647
697
|
def closest_multiple_greater_than(y: int, x: int) -> int:
|
@@ -668,23 +718,26 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
668
718
|
examples = schema.get("examples")
|
669
719
|
default = schema.get("default")
|
670
720
|
|
721
|
+
seen = HashSet()
|
722
|
+
|
671
723
|
if example or examples or default:
|
672
|
-
if example:
|
724
|
+
if example and seen.insert(example):
|
673
725
|
yield PositiveValue(example, description="Example value")
|
674
726
|
if examples:
|
675
727
|
for example in examples:
|
676
|
-
|
728
|
+
if seen.insert(example):
|
729
|
+
yield PositiveValue(example, description="Example value")
|
677
730
|
if (
|
678
731
|
default
|
679
732
|
and not (example is not None and default == example)
|
680
733
|
and not (examples is not None and any(default == ex for ex in examples))
|
734
|
+
and seen.insert(default)
|
681
735
|
):
|
682
736
|
yield PositiveValue(default, description="Default value")
|
683
737
|
elif not minimum and not maximum:
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
seen = set()
|
738
|
+
value = ctx.generate_from_schema(schema)
|
739
|
+
seen.insert(value)
|
740
|
+
yield PositiveValue(value, description="Valid number")
|
688
741
|
|
689
742
|
if minimum is not None:
|
690
743
|
# Exactly the minimum
|
@@ -692,16 +745,15 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
692
745
|
smallest = closest_multiple_greater_than(minimum, multiple_of)
|
693
746
|
else:
|
694
747
|
smallest = minimum
|
695
|
-
seen.
|
696
|
-
|
748
|
+
if seen.insert(smallest):
|
749
|
+
yield PositiveValue(smallest, description="Minimum value")
|
697
750
|
|
698
751
|
# One more than minimum if possible
|
699
752
|
if multiple_of is not None:
|
700
753
|
larger = smallest + multiple_of
|
701
754
|
else:
|
702
755
|
larger = minimum + 1
|
703
|
-
if
|
704
|
-
seen.add(larger)
|
756
|
+
if (not maximum or larger <= maximum) and seen.insert(larger):
|
705
757
|
yield PositiveValue(larger, description="Near-boundary number")
|
706
758
|
|
707
759
|
if maximum is not None:
|
@@ -710,8 +762,7 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
710
762
|
largest = maximum - (maximum % multiple_of)
|
711
763
|
else:
|
712
764
|
largest = maximum
|
713
|
-
if largest
|
714
|
-
seen.add(largest)
|
765
|
+
if seen.insert(largest):
|
715
766
|
yield PositiveValue(largest, description="Maximum value")
|
716
767
|
|
717
768
|
# One less than maximum if possible
|
@@ -719,71 +770,65 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
719
770
|
smaller = largest - multiple_of
|
720
771
|
else:
|
721
772
|
smaller = maximum - 1
|
722
|
-
if
|
723
|
-
seen.add(smaller)
|
773
|
+
if (smaller > 0 and (minimum is None or smaller >= minimum)) and seen.insert(smaller):
|
724
774
|
yield PositiveValue(smaller, description="Near-boundary number")
|
725
775
|
|
726
776
|
|
727
777
|
def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Generator[GeneratedValue, None, None]:
|
728
|
-
seen = set()
|
729
778
|
example = schema.get("example")
|
730
779
|
examples = schema.get("examples")
|
731
780
|
default = schema.get("default")
|
732
781
|
|
782
|
+
seen = HashSet()
|
783
|
+
seen_constraints: set[tuple] = set()
|
784
|
+
|
733
785
|
if example or examples or default:
|
734
|
-
if example:
|
735
|
-
seen.add(_to_hashable_key(example))
|
786
|
+
if example and seen.insert(example):
|
736
787
|
yield PositiveValue(example, description="Example value")
|
737
788
|
if examples:
|
738
789
|
for example in examples:
|
739
|
-
seen.
|
740
|
-
|
790
|
+
if seen.insert(example):
|
791
|
+
yield PositiveValue(example, description="Example value")
|
741
792
|
if (
|
742
793
|
default
|
743
794
|
and not (example is not None and default == example)
|
744
795
|
and not (examples is not None and any(default == ex for ex in examples))
|
796
|
+
and seen.insert(default)
|
745
797
|
):
|
746
|
-
seen.add(_to_hashable_key(default))
|
747
798
|
yield PositiveValue(default, description="Default value")
|
748
|
-
|
799
|
+
elif seen.insert(template):
|
749
800
|
yield PositiveValue(template, description="Valid array")
|
750
|
-
seen.add(_to_hashable_key(template))
|
751
801
|
|
752
802
|
# Boundary and near-boundary sizes
|
753
803
|
min_items = schema.get("minItems")
|
754
804
|
max_items = schema.get("maxItems")
|
755
805
|
if min_items is not None:
|
756
806
|
# Do not generate an array with `minItems` length, because it is already covered by `template`
|
757
|
-
|
758
807
|
# One item more than minimum if possible
|
759
808
|
larger = min_items + 1
|
760
|
-
if
|
809
|
+
if (max_items is None or larger <= max_items) and larger not in seen_constraints:
|
810
|
+
seen_constraints.add(larger)
|
761
811
|
value = ctx.generate_from_schema({**schema, "minItems": larger, "maxItems": larger})
|
762
|
-
|
763
|
-
if key not in seen:
|
764
|
-
seen.add(key)
|
812
|
+
if seen.insert(value):
|
765
813
|
yield PositiveValue(value, description="Near-boundary items array")
|
766
814
|
|
767
815
|
if max_items is not None:
|
768
|
-
if max_items <
|
816
|
+
if max_items < INTERNAL_BUFFER_SIZE and max_items not in seen_constraints:
|
817
|
+
seen_constraints.add(max_items)
|
769
818
|
value = ctx.generate_from_schema({**schema, "minItems": max_items})
|
770
|
-
|
771
|
-
if key not in seen:
|
772
|
-
seen.add(key)
|
819
|
+
if seen.insert(value):
|
773
820
|
yield PositiveValue(value, description="Maximum items array")
|
774
821
|
|
775
822
|
# One item smaller than maximum if possible
|
776
823
|
smaller = max_items - 1
|
777
824
|
if (
|
778
|
-
smaller <
|
825
|
+
smaller < INTERNAL_BUFFER_SIZE
|
779
826
|
and smaller > 0
|
780
|
-
and smaller not in seen
|
781
827
|
and (min_items is None or smaller >= min_items)
|
828
|
+
and smaller not in seen_constraints
|
782
829
|
):
|
783
830
|
value = ctx.generate_from_schema({**schema, "minItems": smaller, "maxItems": smaller})
|
784
|
-
|
785
|
-
if key not in seen:
|
786
|
-
seen.add(key)
|
831
|
+
if seen.insert(value):
|
787
832
|
yield PositiveValue(value, description="Near-boundary items array")
|
788
833
|
|
789
834
|
if "items" in schema and "enum" in schema["items"] and isinstance(schema["items"]["enum"], list) and max_items != 0:
|
@@ -791,9 +836,7 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
|
|
791
836
|
length = min_items or 1
|
792
837
|
for variant in schema["items"]["enum"]:
|
793
838
|
value = [variant] * length
|
794
|
-
|
795
|
-
if key not in seen:
|
796
|
-
seen.add(key)
|
839
|
+
if seen.insert(value):
|
797
840
|
yield PositiveValue(value, description="Enum value from available for items array")
|
798
841
|
elif min_items is None and max_items is None and "items" in schema and isinstance(schema["items"], dict):
|
799
842
|
# Otherwise only an empty array is generated
|
@@ -840,16 +883,14 @@ def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Gene
|
|
840
883
|
if set(properties) != required:
|
841
884
|
only_required = {k: v for k, v in template.items() if k in required}
|
842
885
|
yield PositiveValue(only_required, description="Object with only required properties")
|
843
|
-
seen =
|
886
|
+
seen = HashSet()
|
844
887
|
for name, sub_schema in properties.items():
|
845
|
-
seen.
|
888
|
+
seen.insert(template.get(name))
|
846
889
|
for new in cover_schema_iter(ctx, sub_schema):
|
847
|
-
|
848
|
-
if key not in seen:
|
890
|
+
if seen.insert(new.value):
|
849
891
|
yield PositiveValue(
|
850
892
|
{**template, name: new.value}, description=f"Object with valid '{name}' value: {new.description}"
|
851
893
|
)
|
852
|
-
seen.add(key)
|
853
894
|
seen.clear()
|
854
895
|
|
855
896
|
|
@@ -858,14 +899,11 @@ def select_combinations(optional: list[str]) -> Iterator[tuple[str, ...]]:
|
|
858
899
|
yield next(combinations(optional, size))
|
859
900
|
|
860
901
|
|
861
|
-
def _negative_enum(
|
862
|
-
ctx: CoverageContext, value: list, seen: set[Any | tuple[type, str]]
|
863
|
-
) -> Generator[GeneratedValue, None, None]:
|
902
|
+
def _negative_enum(ctx: CoverageContext, value: list, seen: HashSet) -> Generator[GeneratedValue, None, None]:
|
864
903
|
def is_not_in_value(x: Any) -> bool:
|
865
904
|
if x in value or not ctx.is_valid_for_location(x):
|
866
905
|
return False
|
867
|
-
|
868
|
-
return _hashed not in seen
|
906
|
+
return seen.insert(x)
|
869
907
|
|
870
908
|
strategy = (
|
871
909
|
st.text(alphabet=st.characters(min_codepoint=65, max_codepoint=122, categories=["L"]), min_size=3)
|
@@ -873,10 +911,11 @@ def _negative_enum(
|
|
873
911
|
| st.booleans()
|
874
912
|
| NUMERIC_STRATEGY
|
875
913
|
).filter(is_not_in_value)
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
914
|
+
yield NegativeValue(
|
915
|
+
ctx.generate_from(strategy),
|
916
|
+
description="Invalid enum value",
|
917
|
+
location=ctx.current_path,
|
918
|
+
)
|
880
919
|
|
881
920
|
|
882
921
|
def _negative_properties(
|
@@ -1005,7 +1044,11 @@ def _is_non_integer_float(x: float) -> bool:
|
|
1005
1044
|
return x != int(x)
|
1006
1045
|
|
1007
1046
|
|
1008
|
-
def _negative_type(
|
1047
|
+
def _negative_type(
|
1048
|
+
ctx: CoverageContext,
|
1049
|
+
ty: str | list[str],
|
1050
|
+
seen: HashSet,
|
1051
|
+
) -> Generator[GeneratedValue, None, None]:
|
1009
1052
|
if isinstance(ty, str):
|
1010
1053
|
types = [ty]
|
1011
1054
|
else:
|
@@ -1017,11 +1060,8 @@ def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Gene
|
|
1017
1060
|
strategies["number"] = FLOAT_STRATEGY.filter(_is_non_integer_float)
|
1018
1061
|
for strategy in strategies.values():
|
1019
1062
|
value = ctx.generate_from(strategy)
|
1020
|
-
|
1021
|
-
|
1022
|
-
continue
|
1023
|
-
yield NegativeValue(value, description="Incorrect type", location=ctx.current_path)
|
1024
|
-
seen.add(hashed)
|
1063
|
+
if seen.insert(value):
|
1064
|
+
yield NegativeValue(value, description="Incorrect type", location=ctx.current_path)
|
1025
1065
|
|
1026
1066
|
|
1027
1067
|
def push_examples_to_properties(schema: dict[str, Any]) -> None:
|