schemathesis 4.3.15__py3-none-any.whl → 4.3.17__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.

Potentially problematic release.


This version of schemathesis might be problematic. Click here for more details.

@@ -38,6 +38,7 @@ from schemathesis.core.transforms import deepclone
38
38
  from schemathesis.core.validation import contains_unicode_surrogate_pair, has_invalid_characters, is_latin_1_encodable
39
39
  from schemathesis.generation import GenerationMode
40
40
  from schemathesis.generation.hypothesis import examples
41
+ from schemathesis.generation.meta import CoverageScenario
41
42
  from schemathesis.openapi.generation.filters import is_invalid_path_parameter
42
43
 
43
44
  from ..specs.openapi.converter import update_pattern_in_schema
@@ -93,17 +94,19 @@ UNKNOWN_PROPERTY_VALUE = 42
93
94
  class GeneratedValue:
94
95
  value: Any
95
96
  generation_mode: GenerationMode
97
+ scenario: CoverageScenario
96
98
  description: str
97
99
  parameter: str | None
98
100
  location: str | None
99
101
 
100
- __slots__ = ("value", "generation_mode", "description", "parameter", "location")
102
+ __slots__ = ("value", "generation_mode", "scenario", "description", "parameter", "location")
101
103
 
102
104
  @classmethod
103
- def with_positive(cls, value: Any, *, description: str) -> GeneratedValue:
105
+ def with_positive(cls, value: Any, *, scenario: CoverageScenario, description: str) -> GeneratedValue:
104
106
  return cls(
105
107
  value=value,
106
108
  generation_mode=GenerationMode.POSITIVE,
109
+ scenario=scenario,
107
110
  description=description,
108
111
  location=None,
109
112
  parameter=None,
@@ -111,11 +114,12 @@ class GeneratedValue:
111
114
 
112
115
  @classmethod
113
116
  def with_negative(
114
- cls, value: Any, *, description: str, location: str, parameter: str | None = None
117
+ cls, value: Any, *, scenario: CoverageScenario, description: str, location: str, parameter: str | None = None
115
118
  ) -> GeneratedValue:
116
119
  return cls(
117
120
  value=value,
118
121
  generation_mode=GenerationMode.NEGATIVE,
122
+ scenario=scenario,
119
123
  description=description,
120
124
  location=location,
121
125
  parameter=parameter,
@@ -449,15 +453,15 @@ def _cover_positive_for_type(
449
453
  yield from cover_schema_iter(ctx, canonical)
450
454
  if enum is not NOT_SET:
451
455
  for value in enum:
452
- yield PositiveValue(value, description="Enum value")
456
+ yield PositiveValue(value, scenario=CoverageScenario.ENUM_VALUE, description="Enum value")
453
457
  elif const is not NOT_SET:
454
- yield PositiveValue(const, description="Const value")
458
+ yield PositiveValue(const, scenario=CoverageScenario.CONST_VALUE, description="Const value")
455
459
  elif ty is not None:
456
460
  if ty == "null":
457
- yield PositiveValue(None, description="Value null value")
461
+ yield PositiveValue(None, scenario=CoverageScenario.NULL_VALUE, description="Value null value")
458
462
  elif ty == "boolean":
459
- yield PositiveValue(True, description="Valid boolean value")
460
- yield PositiveValue(False, description="Valid boolean value")
463
+ yield PositiveValue(True, scenario=CoverageScenario.VALID_BOOLEAN, description="Valid boolean value")
464
+ yield PositiveValue(False, scenario=CoverageScenario.VALID_BOOLEAN, description="Valid boolean value")
461
465
  elif ty == "string":
462
466
  yield from _positive_string(ctx, schema)
463
467
  elif ty == "integer" or ty == "number":
@@ -563,15 +567,32 @@ def cover_schema_iter(
563
567
  elif key == "maximum":
564
568
  next = value + 1
565
569
  if seen.insert(next):
566
- yield NegativeValue(next, description="Value greater than maximum", location=ctx.current_path)
570
+ yield NegativeValue(
571
+ next,
572
+ scenario=CoverageScenario.VALUE_ABOVE_MAXIMUM,
573
+ description="Value greater than maximum",
574
+ location=ctx.current_path,
575
+ )
567
576
  elif key == "minimum":
568
577
  next = value - 1
569
578
  if seen.insert(next):
570
- yield NegativeValue(next, description="Value smaller than minimum", location=ctx.current_path)
579
+ yield NegativeValue(
580
+ next,
581
+ scenario=CoverageScenario.VALUE_BELOW_MINIMUM,
582
+ description="Value smaller than minimum",
583
+ location=ctx.current_path,
584
+ )
571
585
  elif key == "exclusiveMaximum" or key == "exclusiveMinimum" and seen.insert(value):
572
586
  verb = "greater" if key == "exclusiveMaximum" else "smaller"
573
587
  limit = "maximum" if key == "exclusiveMaximum" else "minimum"
574
- yield NegativeValue(value, description=f"Value {verb} than {limit}", location=ctx.current_path)
588
+ scenario = (
589
+ CoverageScenario.VALUE_ABOVE_MAXIMUM
590
+ if key == "exclusiveMaximum"
591
+ else CoverageScenario.VALUE_BELOW_MINIMUM
592
+ )
593
+ yield NegativeValue(
594
+ value, scenario=scenario, description=f"Value {verb} than {limit}", location=ctx.current_path
595
+ )
575
596
  elif key == "multipleOf":
576
597
  for value_ in _negative_multiple_of(ctx, schema, value):
577
598
  if seen.insert(value_.value):
@@ -584,7 +605,10 @@ def cover_schema_iter(
584
605
  value = ""
585
606
  if ctx.is_valid_for_location(value) and seen.insert(value):
586
607
  yield NegativeValue(
587
- value, description="String smaller than minLength", location=ctx.current_path
608
+ value,
609
+ scenario=CoverageScenario.STRING_BELOW_MIN_LENGTH,
610
+ description="String smaller than minLength",
611
+ location=ctx.current_path,
588
612
  )
589
613
  else:
590
614
  with suppress(InvalidArgument):
@@ -604,7 +628,10 @@ def cover_schema_iter(
604
628
  value = ctx.generate_from_schema(new_schema)
605
629
  if ctx.is_valid_for_location(value) and seen.insert(value):
606
630
  yield NegativeValue(
607
- value, description="String smaller than minLength", location=ctx.current_path
631
+ value,
632
+ scenario=CoverageScenario.STRING_BELOW_MIN_LENGTH,
633
+ description="String smaller than minLength",
634
+ location=ctx.current_path,
608
635
  )
609
636
  elif key == "maxLength" and value < INTERNAL_BUFFER_SIZE:
610
637
  try:
@@ -629,7 +656,10 @@ def cover_schema_iter(
629
656
  value = ctx.generate_from_schema(new_schema)
630
657
  if seen.insert(value):
631
658
  yield NegativeValue(
632
- value, description="String larger than maxLength", location=ctx.current_path
659
+ value,
660
+ scenario=CoverageScenario.STRING_ABOVE_MAX_LENGTH,
661
+ description="String larger than maxLength",
662
+ location=ctx.current_path,
633
663
  )
634
664
  except (InvalidArgument, Unsatisfiable):
635
665
  pass
@@ -669,6 +699,7 @@ def cover_schema_iter(
669
699
  if seen.insert(array_value):
670
700
  yield NegativeValue(
671
701
  array_value,
702
+ scenario=CoverageScenario.ARRAY_ABOVE_MAX_ITEMS,
672
703
  description="Array with more items than allowed by maxItems",
673
704
  location=ctx.current_path,
674
705
  )
@@ -680,6 +711,7 @@ def cover_schema_iter(
680
711
  if seen.insert(array_value):
681
712
  yield NegativeValue(
682
713
  array_value,
714
+ scenario=CoverageScenario.ARRAY_ABOVE_MAX_ITEMS,
683
715
  description="Array with more items than allowed by maxItems",
684
716
  location=ctx.current_path,
685
717
  )
@@ -693,6 +725,7 @@ def cover_schema_iter(
693
725
  if seen.insert(array_value):
694
726
  yield NegativeValue(
695
727
  array_value,
728
+ scenario=CoverageScenario.ARRAY_BELOW_MIN_ITEMS,
696
729
  description="Array with fewer items than allowed by minItems",
697
730
  location=ctx.current_path,
698
731
  )
@@ -707,6 +740,7 @@ def cover_schema_iter(
707
740
  template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
708
741
  yield NegativeValue(
709
742
  {**template, UNKNOWN_PROPERTY_KEY: UNKNOWN_PROPERTY_VALUE},
743
+ scenario=CoverageScenario.OBJECT_UNEXPECTED_PROPERTIES,
710
744
  description="Object with unexpected properties",
711
745
  location=ctx.current_path,
712
746
  )
@@ -838,12 +872,12 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
838
872
  has_valid_example = False
839
873
  if example and ctx.is_valid_for_location(example) and seen_values.insert(example):
840
874
  has_valid_example = True
841
- yield PositiveValue(example, description="Example value")
875
+ yield PositiveValue(example, scenario=CoverageScenario.EXAMPLE_VALUE, description="Example value")
842
876
  if examples:
843
877
  for example in examples:
844
878
  if ctx.is_valid_for_location(example) and seen_values.insert(example):
845
879
  has_valid_example = True
846
- yield PositiveValue(example, description="Example value")
880
+ yield PositiveValue(example, scenario=CoverageScenario.EXAMPLE_VALUE, description="Example value")
847
881
  if (
848
882
  default
849
883
  and not (example is not None and default == example)
@@ -852,18 +886,18 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
852
886
  and seen_values.insert(default)
853
887
  ):
854
888
  has_valid_example = True
855
- yield PositiveValue(default, description="Default value")
889
+ yield PositiveValue(default, scenario=CoverageScenario.DEFAULT_VALUE, description="Default value")
856
890
  if not has_valid_example:
857
891
  if not min_length and not max_length or "pattern" in schema:
858
892
  value = ctx.generate_from_schema(schema)
859
893
  seen_values.insert(value)
860
894
  seen_constraints.add((min_length, max_length))
861
- yield PositiveValue(value, description="Valid string")
895
+ yield PositiveValue(value, scenario=CoverageScenario.VALID_STRING, description="Valid string")
862
896
  elif not min_length and not max_length or "pattern" in schema:
863
897
  value = ctx.generate_from_schema(schema)
864
898
  seen_values.insert(value)
865
899
  seen_constraints.add((min_length, max_length))
866
- yield PositiveValue(value, description="Valid string")
900
+ yield PositiveValue(value, scenario=CoverageScenario.VALID_STRING, description="Valid string")
867
901
 
868
902
  if min_length is not None and min_length < INTERNAL_BUFFER_SIZE:
869
903
  # Exactly the minimum length
@@ -872,7 +906,9 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
872
906
  seen_constraints.add(key)
873
907
  value = ctx.generate_from_schema({**schema, "maxLength": min_length})
874
908
  if seen_values.insert(value):
875
- yield PositiveValue(value, description="Minimum length string")
909
+ yield PositiveValue(
910
+ value, scenario=CoverageScenario.MINIMUM_LENGTH_STRING, description="Minimum length string"
911
+ )
876
912
 
877
913
  # One character more than minimum if possible
878
914
  larger = min_length + 1
@@ -881,7 +917,11 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
881
917
  seen_constraints.add(key)
882
918
  value = ctx.generate_from_schema({**schema, "minLength": larger, "maxLength": larger})
883
919
  if seen_values.insert(value):
884
- yield PositiveValue(value, description="Near-boundary length string")
920
+ yield PositiveValue(
921
+ value,
922
+ scenario=CoverageScenario.NEAR_BOUNDARY_LENGTH_STRING,
923
+ description="Near-boundary length string",
924
+ )
885
925
 
886
926
  if max_length is not None:
887
927
  # Exactly the maximum length
@@ -890,7 +930,9 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
890
930
  seen_constraints.add(key)
891
931
  value = ctx.generate_from_schema({**schema, "minLength": max_length, "maxLength": max_length})
892
932
  if seen_values.insert(value):
893
- yield PositiveValue(value, description="Maximum length string")
933
+ yield PositiveValue(
934
+ value, scenario=CoverageScenario.MAXIMUM_LENGTH_STRING, description="Maximum length string"
935
+ )
894
936
 
895
937
  # One character less than maximum if possible
896
938
  smaller = max_length - 1
@@ -903,7 +945,11 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
903
945
  seen_constraints.add(key)
904
946
  value = ctx.generate_from_schema({**schema, "minLength": smaller, "maxLength": smaller})
905
947
  if seen_values.insert(value):
906
- yield PositiveValue(value, description="Near-boundary length string")
948
+ yield PositiveValue(
949
+ value,
950
+ scenario=CoverageScenario.NEAR_BOUNDARY_LENGTH_STRING,
951
+ description="Near-boundary length string",
952
+ )
907
953
 
908
954
 
909
955
  def closest_multiple_greater_than(y: int, x: int) -> int:
@@ -935,22 +981,22 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
935
981
 
936
982
  if example or examples or default:
937
983
  if example and seen.insert(example):
938
- yield PositiveValue(example, description="Example value")
984
+ yield PositiveValue(example, scenario=CoverageScenario.EXAMPLE_VALUE, description="Example value")
939
985
  if examples:
940
986
  for example in examples:
941
987
  if seen.insert(example):
942
- yield PositiveValue(example, description="Example value")
988
+ yield PositiveValue(example, scenario=CoverageScenario.EXAMPLE_VALUE, description="Example value")
943
989
  if (
944
990
  default
945
991
  and not (example is not None and default == example)
946
992
  and not (examples is not None and any(default == ex for ex in examples))
947
993
  and seen.insert(default)
948
994
  ):
949
- yield PositiveValue(default, description="Default value")
995
+ yield PositiveValue(default, scenario=CoverageScenario.DEFAULT_VALUE, description="Default value")
950
996
  elif not minimum and not maximum:
951
997
  value = ctx.generate_from_schema(schema)
952
998
  seen.insert(value)
953
- yield PositiveValue(value, description="Valid number")
999
+ yield PositiveValue(value, scenario=CoverageScenario.VALID_NUMBER, description="Valid number")
954
1000
 
955
1001
  if minimum is not None:
956
1002
  # Exactly the minimum
@@ -959,7 +1005,7 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
959
1005
  else:
960
1006
  smallest = minimum
961
1007
  if seen.insert(smallest):
962
- yield PositiveValue(smallest, description="Minimum value")
1008
+ yield PositiveValue(smallest, scenario=CoverageScenario.MINIMUM_VALUE, description="Minimum value")
963
1009
 
964
1010
  # One more than minimum if possible
965
1011
  if multiple_of is not None:
@@ -967,7 +1013,9 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
967
1013
  else:
968
1014
  larger = minimum + 1
969
1015
  if (not maximum or larger <= maximum) and seen.insert(larger):
970
- yield PositiveValue(larger, description="Near-boundary number")
1016
+ yield PositiveValue(
1017
+ larger, scenario=CoverageScenario.NEAR_BOUNDARY_NUMBER, description="Near-boundary number"
1018
+ )
971
1019
 
972
1020
  if maximum is not None:
973
1021
  # Exactly the maximum
@@ -976,7 +1024,7 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
976
1024
  else:
977
1025
  largest = maximum
978
1026
  if seen.insert(largest):
979
- yield PositiveValue(largest, description="Maximum value")
1027
+ yield PositiveValue(largest, scenario=CoverageScenario.MAXIMUM_VALUE, description="Maximum value")
980
1028
 
981
1029
  # One less than maximum if possible
982
1030
  if multiple_of is not None:
@@ -984,7 +1032,9 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
984
1032
  else:
985
1033
  smaller = maximum - 1
986
1034
  if (minimum is None or smaller >= minimum) and seen.insert(smaller):
987
- yield PositiveValue(smaller, description="Near-boundary number")
1035
+ yield PositiveValue(
1036
+ smaller, scenario=CoverageScenario.NEAR_BOUNDARY_NUMBER, description="Near-boundary number"
1037
+ )
988
1038
 
989
1039
 
990
1040
  def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Generator[GeneratedValue, None, None]:
@@ -997,20 +1047,20 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
997
1047
 
998
1048
  if example or examples or default:
999
1049
  if example and seen.insert(example):
1000
- yield PositiveValue(example, description="Example value")
1050
+ yield PositiveValue(example, scenario=CoverageScenario.EXAMPLE_VALUE, description="Example value")
1001
1051
  if examples:
1002
1052
  for example in examples:
1003
1053
  if seen.insert(example):
1004
- yield PositiveValue(example, description="Example value")
1054
+ yield PositiveValue(example, scenario=CoverageScenario.EXAMPLE_VALUE, description="Example value")
1005
1055
  if (
1006
1056
  default
1007
1057
  and not (example is not None and default == example)
1008
1058
  and not (examples is not None and any(default == ex for ex in examples))
1009
1059
  and seen.insert(default)
1010
1060
  ):
1011
- yield PositiveValue(default, description="Default value")
1061
+ yield PositiveValue(default, scenario=CoverageScenario.DEFAULT_VALUE, description="Default value")
1012
1062
  elif seen.insert(template):
1013
- yield PositiveValue(template, description="Valid array")
1063
+ yield PositiveValue(template, scenario=CoverageScenario.VALID_ARRAY, description="Valid array")
1014
1064
 
1015
1065
  # Boundary and near-boundary sizes
1016
1066
  min_items = schema.get("minItems")
@@ -1023,14 +1073,18 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
1023
1073
  seen_constraints.add(larger)
1024
1074
  value = ctx.generate_from_schema({**schema, "minItems": larger, "maxItems": larger})
1025
1075
  if seen.insert(value):
1026
- yield PositiveValue(value, description="Near-boundary items array")
1076
+ yield PositiveValue(
1077
+ value, scenario=CoverageScenario.NEAR_BOUNDARY_ITEMS_ARRAY, description="Near-boundary items array"
1078
+ )
1027
1079
 
1028
1080
  if max_items is not None:
1029
1081
  if max_items < INTERNAL_BUFFER_SIZE and max_items not in seen_constraints:
1030
1082
  seen_constraints.add(max_items)
1031
1083
  value = ctx.generate_from_schema({**schema, "minItems": max_items})
1032
1084
  if seen.insert(value):
1033
- yield PositiveValue(value, description="Maximum items array")
1085
+ yield PositiveValue(
1086
+ value, scenario=CoverageScenario.MAXIMUM_ITEMS_ARRAY, description="Maximum items array"
1087
+ )
1034
1088
 
1035
1089
  # One item smaller than maximum if possible
1036
1090
  smaller = max_items - 1
@@ -1042,7 +1096,9 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
1042
1096
  ):
1043
1097
  value = ctx.generate_from_schema({**schema, "minItems": smaller, "maxItems": smaller})
1044
1098
  if seen.insert(value):
1045
- yield PositiveValue(value, description="Near-boundary items array")
1099
+ yield PositiveValue(
1100
+ value, scenario=CoverageScenario.NEAR_BOUNDARY_ITEMS_ARRAY, description="Near-boundary items array"
1101
+ )
1046
1102
 
1047
1103
  if "items" in schema and "enum" in schema["items"] and isinstance(schema["items"]["enum"], list) and max_items != 0:
1048
1104
  # Ensure there is enough items to pass `minItems` if it is specified
@@ -1050,12 +1106,20 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
1050
1106
  for variant in schema["items"]["enum"]:
1051
1107
  value = [variant] * length
1052
1108
  if seen.insert(value):
1053
- yield PositiveValue(value, description="Enum value from available for items array")
1109
+ yield PositiveValue(
1110
+ value,
1111
+ scenario=CoverageScenario.ENUM_VALUE_ITEMS_ARRAY,
1112
+ description="Enum value from available for items array",
1113
+ )
1054
1114
  elif min_items is None and max_items is None and "items" in schema and isinstance(schema["items"], dict):
1055
1115
  # Otherwise only an empty array is generated
1056
1116
  sub_schema = schema["items"]
1057
1117
  for item in cover_schema_iter(ctx, sub_schema):
1058
- yield PositiveValue([item.value], description=f"Single-item array: {item.description}")
1118
+ yield PositiveValue(
1119
+ [item.value],
1120
+ scenario=CoverageScenario.VALID_ARRAY,
1121
+ description=f"Single-item array: {item.description}",
1122
+ )
1059
1123
 
1060
1124
 
1061
1125
  def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Generator[GeneratedValue, None, None]:
@@ -1065,18 +1129,18 @@ def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Gene
1065
1129
 
1066
1130
  if example or examples or default:
1067
1131
  if example:
1068
- yield PositiveValue(example, description="Example value")
1132
+ yield PositiveValue(example, scenario=CoverageScenario.EXAMPLE_VALUE, description="Example value")
1069
1133
  if examples:
1070
1134
  for example in examples:
1071
- yield PositiveValue(example, description="Example value")
1135
+ yield PositiveValue(example, scenario=CoverageScenario.EXAMPLE_VALUE, description="Example value")
1072
1136
  if (
1073
1137
  default
1074
1138
  and not (example is not None and default == example)
1075
1139
  and not (examples is not None and any(default == ex for ex in examples))
1076
1140
  ):
1077
- yield PositiveValue(default, description="Default value")
1141
+ yield PositiveValue(default, scenario=CoverageScenario.DEFAULT_VALUE, description="Default value")
1078
1142
  else:
1079
- yield PositiveValue(template, description="Valid object")
1143
+ yield PositiveValue(template, scenario=CoverageScenario.VALID_OBJECT, description="Valid object")
1080
1144
 
1081
1145
  properties = schema.get("properties", {})
1082
1146
  required = set(schema.get("required", []))
@@ -1087,22 +1151,36 @@ def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Gene
1087
1151
  for name in optional:
1088
1152
  combo = {k: v for k, v in template.items() if k in required or k == name}
1089
1153
  if combo != template:
1090
- yield PositiveValue(combo, description=f"Object with all required properties and '{name}'")
1154
+ yield PositiveValue(
1155
+ combo,
1156
+ scenario=CoverageScenario.OBJECT_REQUIRED_AND_OPTIONAL,
1157
+ description=f"Object with all required properties and '{name}'",
1158
+ )
1091
1159
  # Generate one combination for each size from 2 to N-1
1092
1160
  for selection in select_combinations(optional):
1093
1161
  combo = {k: v for k, v in template.items() if k in required or k in selection}
1094
- yield PositiveValue(combo, description="Object with all required and a subset of optional properties")
1162
+ yield PositiveValue(
1163
+ combo,
1164
+ scenario=CoverageScenario.OBJECT_REQUIRED_AND_OPTIONAL,
1165
+ description="Object with all required and a subset of optional properties",
1166
+ )
1095
1167
  # Generate only required properties
1096
1168
  if set(properties) != required:
1097
1169
  only_required = {k: v for k, v in template.items() if k in required}
1098
- yield PositiveValue(only_required, description="Object with only required properties")
1170
+ yield PositiveValue(
1171
+ only_required,
1172
+ scenario=CoverageScenario.OBJECT_ONLY_REQUIRED,
1173
+ description="Object with only required properties",
1174
+ )
1099
1175
  seen = HashSet()
1100
1176
  for name, sub_schema in properties.items():
1101
1177
  seen.insert(template.get(name))
1102
1178
  for new in cover_schema_iter(ctx, sub_schema):
1103
1179
  if seen.insert(new.value):
1104
1180
  yield PositiveValue(
1105
- {**template, name: new.value}, description=f"Object with valid '{name}' value: {new.description}"
1181
+ {**template, name: new.value},
1182
+ scenario=CoverageScenario.VALID_OBJECT,
1183
+ description=f"Object with valid '{name}' value: {new.description}",
1106
1184
  )
1107
1185
  seen.clear()
1108
1186
 
@@ -1126,6 +1204,7 @@ def _negative_enum(ctx: CoverageContext, value: list, seen: HashSet) -> Generato
1126
1204
  ).filter(is_not_in_value)
1127
1205
  yield NegativeValue(
1128
1206
  ctx.generate_from(strategy),
1207
+ scenario=CoverageScenario.INVALID_ENUM_VALUE,
1129
1208
  description="Invalid enum value",
1130
1209
  location=ctx.current_path,
1131
1210
  )
@@ -1140,6 +1219,7 @@ def _negative_properties(
1140
1219
  for value in cover_schema_iter(nctx, sub_schema):
1141
1220
  yield NegativeValue(
1142
1221
  {**template, key: value.value},
1222
+ scenario=value.scenario,
1143
1223
  description=f"Object with invalid '{key}' value: {value.description}",
1144
1224
  location=nctx.current_path,
1145
1225
  parameter=key,
@@ -1159,6 +1239,7 @@ def _negative_pattern_properties(
1159
1239
  for value in cover_schema_iter(nctx, sub_schema):
1160
1240
  yield NegativeValue(
1161
1241
  {**template, key: value.value},
1242
+ scenario=value.scenario,
1162
1243
  description=f"Object with invalid pattern key '{key}' ('{pattern}') value: {value.description}",
1163
1244
  location=nctx.current_path,
1164
1245
  )
@@ -1172,6 +1253,7 @@ def _negative_items(ctx: CoverageContext, schema: dict[str, Any] | bool) -> Gene
1172
1253
  if ctx.leads_to_negative_test_case(items):
1173
1254
  yield NegativeValue(
1174
1255
  items,
1256
+ scenario=value.scenario,
1175
1257
  description=f"Array with invalid items: {value.description}",
1176
1258
  location=nctx.current_path,
1177
1259
  )
@@ -1194,6 +1276,7 @@ def _negative_pattern(
1194
1276
  .filter(partial(_not_matching_pattern, pattern=compiled))
1195
1277
  .filter(ctx.is_valid_for_location)
1196
1278
  ),
1279
+ scenario=CoverageScenario.INVALID_PATTERN,
1197
1280
  description=f"Value not matching the '{pattern}' pattern",
1198
1281
  location=ctx.current_path,
1199
1282
  )
@@ -1208,6 +1291,7 @@ def _negative_multiple_of(
1208
1291
  ) -> Generator[GeneratedValue, None, None]:
1209
1292
  yield NegativeValue(
1210
1293
  ctx.generate_from_schema(_with_negated_key(schema, "multipleOf", multiple_of)),
1294
+ scenario=CoverageScenario.NOT_MULTIPLE_OF,
1211
1295
  description=f"Non-multiple of {multiple_of}",
1212
1296
  location=ctx.current_path,
1213
1297
  )
@@ -1215,7 +1299,12 @@ def _negative_multiple_of(
1215
1299
 
1216
1300
  def _negative_unique_items(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
1217
1301
  unique = jsonify(ctx.generate_from_schema({**schema, "type": "array", "minItems": 1, "maxItems": 1}))
1218
- yield NegativeValue(unique + unique, description="Non-unique items", location=ctx.current_path)
1302
+ yield NegativeValue(
1303
+ unique + unique,
1304
+ scenario=CoverageScenario.NON_UNIQUE_ITEMS,
1305
+ description="Non-unique items",
1306
+ location=ctx.current_path,
1307
+ )
1219
1308
 
1220
1309
 
1221
1310
  def _negative_required(
@@ -1224,6 +1313,7 @@ def _negative_required(
1224
1313
  for key in required:
1225
1314
  yield NegativeValue(
1226
1315
  {k: v for k, v in template.items() if k != key},
1316
+ scenario=CoverageScenario.OBJECT_MISSING_REQUIRED_PROPERTY,
1227
1317
  description=f"Missing required property: {key}",
1228
1318
  location=ctx.current_path,
1229
1319
  parameter=key,
@@ -1253,6 +1343,7 @@ def _negative_format(ctx: CoverageContext, schema: dict, format: str) -> Generat
1253
1343
  strategy = strategy.filter(functools.partial(_is_invalid_format, format=format))
1254
1344
  yield NegativeValue(
1255
1345
  ctx.generate_from(strategy),
1346
+ scenario=CoverageScenario.INVALID_FORMAT,
1256
1347
  description=f"Value not matching the '{format}' format",
1257
1348
  location=ctx.current_path,
1258
1349
  )
@@ -1379,7 +1470,9 @@ def _negative_type(
1379
1470
  for strategy in strategies.values():
1380
1471
  value = ctx.generate_from(strategy)
1381
1472
  if seen.insert(value) and ctx.is_valid_for_location(value):
1382
- yield NegativeValue(value, description="Incorrect type", location=ctx.current_path)
1473
+ yield NegativeValue(
1474
+ value, scenario=CoverageScenario.INCORRECT_TYPE, description="Incorrect type", location=ctx.current_path
1475
+ )
1383
1476
 
1384
1477
 
1385
1478
  def push_examples_to_properties(schema: dict[str, Any]) -> None: