schemathesis 4.0.3__py3-none-any.whl → 4.0.4__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/engine/errors.py +8 -3
- schemathesis/engine/phases/stateful/_executor.py +3 -3
- schemathesis/engine/phases/unit/_executor.py +3 -3
- schemathesis/generation/coverage.py +27 -7
- schemathesis/generation/hypothesis/builder.py +14 -6
- {schemathesis-4.0.3.dist-info → schemathesis-4.0.4.dist-info}/METADATA +1 -1
- {schemathesis-4.0.3.dist-info → schemathesis-4.0.4.dist-info}/RECORD +10 -10
- {schemathesis-4.0.3.dist-info → schemathesis-4.0.4.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.3.dist-info → schemathesis-4.0.4.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.3.dist-info → schemathesis-4.0.4.dist-info}/licenses/LICENSE +0 -0
schemathesis/engine/errors.py
CHANGED
@@ -421,6 +421,7 @@ def clear_hypothesis_notes(exc: Exception) -> None:
|
|
421
421
|
def is_unrecoverable_network_error(exc: Exception) -> bool:
|
422
422
|
from http.client import RemoteDisconnected
|
423
423
|
|
424
|
+
import requests
|
424
425
|
from urllib3.exceptions import ProtocolError
|
425
426
|
|
426
427
|
def has_connection_reset(inner: BaseException) -> bool:
|
@@ -433,6 +434,8 @@ def is_unrecoverable_network_error(exc: Exception) -> bool:
|
|
433
434
|
|
434
435
|
return False
|
435
436
|
|
437
|
+
if isinstance(exc, requests.Timeout):
|
438
|
+
return True
|
436
439
|
if isinstance(exc.__context__, ProtocolError):
|
437
440
|
if len(exc.__context__.args) == 2 and isinstance(exc.__context__.args[1], RemoteDisconnected):
|
438
441
|
return True
|
@@ -442,14 +445,16 @@ def is_unrecoverable_network_error(exc: Exception) -> bool:
|
|
442
445
|
return has_connection_reset(exc)
|
443
446
|
|
444
447
|
|
445
|
-
@dataclass
|
448
|
+
@dataclass
|
446
449
|
class UnrecoverableNetworkError:
|
447
|
-
error: requests.ConnectionError | ChunkedEncodingError
|
450
|
+
error: requests.ConnectionError | ChunkedEncodingError | requests.Timeout
|
448
451
|
code_sample: str
|
449
452
|
|
450
453
|
__slots__ = ("error", "code_sample")
|
451
454
|
|
452
|
-
def __init__(
|
455
|
+
def __init__(
|
456
|
+
self, error: requests.ConnectionError | ChunkedEncodingError | requests.Timeout, code_sample: str
|
457
|
+
) -> None:
|
453
458
|
self.error = error
|
454
459
|
self.code_sample = code_sample
|
455
460
|
|
@@ -133,9 +133,9 @@ def execute_state_machine_loop(
|
|
133
133
|
ctx.step_failed()
|
134
134
|
raise
|
135
135
|
except Exception as exc:
|
136
|
-
if isinstance(
|
137
|
-
exc
|
138
|
-
):
|
136
|
+
if isinstance(
|
137
|
+
exc, (requests.ConnectionError, ChunkedEncodingError, requests.Timeout)
|
138
|
+
) and is_unrecoverable_network_error(exc):
|
139
139
|
transport_kwargs = engine.get_transport_kwargs(operation=input.case.operation)
|
140
140
|
if exc.request is not None:
|
141
141
|
headers = {key: value[0] for key, value in exc.request.headers.items()}
|
@@ -327,9 +327,9 @@ def cached_test_func(f: Callable) -> Callable:
|
|
327
327
|
except (KeyboardInterrupt, Failure):
|
328
328
|
raise
|
329
329
|
except Exception as exc:
|
330
|
-
if isinstance(
|
331
|
-
exc
|
332
|
-
):
|
330
|
+
if isinstance(
|
331
|
+
exc, (requests.ConnectionError, ChunkedEncodingError, requests.Timeout)
|
332
|
+
) and is_unrecoverable_network_error(exc):
|
333
333
|
# Server likely has crashed and does not accept any connections at all
|
334
334
|
# Don't report these error - only the original crash should be reported
|
335
335
|
if exc.request is not None:
|
@@ -109,19 +109,22 @@ def cached_draw(strategy: st.SearchStrategy) -> Any:
|
|
109
109
|
class CoverageContext:
|
110
110
|
generation_modes: list[GenerationMode]
|
111
111
|
location: str
|
112
|
+
is_required: bool
|
112
113
|
path: list[str | int]
|
113
114
|
|
114
|
-
__slots__ = ("location", "generation_modes", "path")
|
115
|
+
__slots__ = ("location", "generation_modes", "is_required", "path")
|
115
116
|
|
116
117
|
def __init__(
|
117
118
|
self,
|
118
119
|
*,
|
119
120
|
location: str,
|
120
121
|
generation_modes: list[GenerationMode] | None = None,
|
122
|
+
is_required: bool,
|
121
123
|
path: list[str | int] | None = None,
|
122
124
|
) -> None:
|
123
125
|
self.location = location
|
124
126
|
self.generation_modes = generation_modes if generation_modes is not None else list(GenerationMode)
|
127
|
+
self.is_required = is_required
|
125
128
|
self.path = path or []
|
126
129
|
|
127
130
|
@contextmanager
|
@@ -140,6 +143,7 @@ class CoverageContext:
|
|
140
143
|
return CoverageContext(
|
141
144
|
location=self.location,
|
142
145
|
generation_modes=[GenerationMode.POSITIVE],
|
146
|
+
is_required=self.is_required,
|
143
147
|
path=self.path,
|
144
148
|
)
|
145
149
|
|
@@ -147,6 +151,7 @@ class CoverageContext:
|
|
147
151
|
return CoverageContext(
|
148
152
|
location=self.location,
|
149
153
|
generation_modes=[GenerationMode.NEGATIVE],
|
154
|
+
is_required=self.is_required,
|
150
155
|
path=self.path,
|
151
156
|
)
|
152
157
|
|
@@ -157,6 +162,16 @@ class CoverageContext:
|
|
157
162
|
return not is_invalid_path_parameter(value)
|
158
163
|
return True
|
159
164
|
|
165
|
+
def leads_to_negative_test_case(self, value: Any) -> bool:
|
166
|
+
if self.location == "query":
|
167
|
+
# Some values will not be serialized into the query string
|
168
|
+
if isinstance(value, list) and not self.is_required:
|
169
|
+
# Optional parameters should be present
|
170
|
+
return any(item not in [{}, []] for item in value)
|
171
|
+
if isinstance(value, dict) and not self.is_required:
|
172
|
+
return bool(value)
|
173
|
+
return True
|
174
|
+
|
160
175
|
def generate_from(self, strategy: st.SearchStrategy) -> Any:
|
161
176
|
return cached_draw(strategy)
|
162
177
|
|
@@ -955,11 +970,13 @@ def _negative_items(ctx: CoverageContext, schema: dict[str, Any] | bool) -> Gene
|
|
955
970
|
"""Arrays not matching the schema."""
|
956
971
|
nctx = ctx.with_negative()
|
957
972
|
for value in cover_schema_iter(nctx, schema):
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
973
|
+
items = [value.value]
|
974
|
+
if ctx.leads_to_negative_test_case(items):
|
975
|
+
yield NegativeValue(
|
976
|
+
items,
|
977
|
+
description=f"Array with invalid items: {value.description}",
|
978
|
+
location=nctx.current_path,
|
979
|
+
)
|
963
980
|
|
964
981
|
|
965
982
|
def _not_matching_pattern(value: str, pattern: re.Pattern) -> bool:
|
@@ -1027,6 +1044,9 @@ def _negative_format(ctx: CoverageContext, schema: dict, format: str) -> Generat
|
|
1027
1044
|
# Hypothesis-jsonschema does not canonicalise it properly right now, which leads to unsatisfiable schema
|
1028
1045
|
without_format = {k: v for k, v in schema.items() if k != "format"}
|
1029
1046
|
without_format.setdefault("type", "string")
|
1047
|
+
if ctx.location == "path":
|
1048
|
+
# Empty path parameters are invalid
|
1049
|
+
without_format["minLength"] = 1
|
1030
1050
|
strategy = from_schema(without_format)
|
1031
1051
|
if format in jsonschema.Draft202012Validator.FORMAT_CHECKER.checkers:
|
1032
1052
|
if format == "hostname":
|
@@ -1060,7 +1080,7 @@ def _negative_type(
|
|
1060
1080
|
strategies["number"] = FLOAT_STRATEGY.filter(_is_non_integer_float)
|
1061
1081
|
for strategy in strategies.values():
|
1062
1082
|
value = ctx.generate_from(strategy)
|
1063
|
-
if seen.insert(value):
|
1083
|
+
if seen.insert(value) and ctx.is_valid_for_location(value):
|
1064
1084
|
yield NegativeValue(value, description="Incorrect type", location=ctx.current_path)
|
1065
1085
|
|
1066
1086
|
|
@@ -474,7 +474,10 @@ def _iter_coverage_cases(
|
|
474
474
|
for value in find_matching_in_responses(responses, parameter.name):
|
475
475
|
schema.setdefault("examples", []).append(value)
|
476
476
|
gen = coverage.cover_schema_iter(
|
477
|
-
coverage.CoverageContext(
|
477
|
+
coverage.CoverageContext(
|
478
|
+
location=location, generation_modes=generation_modes, is_required=parameter.is_required
|
479
|
+
),
|
480
|
+
schema,
|
478
481
|
)
|
479
482
|
value = next(gen, NOT_SET)
|
480
483
|
if isinstance(value, NotSet):
|
@@ -492,7 +495,10 @@ def _iter_coverage_cases(
|
|
492
495
|
if examples:
|
493
496
|
schema.setdefault("examples", []).extend(examples)
|
494
497
|
gen = coverage.cover_schema_iter(
|
495
|
-
coverage.CoverageContext(
|
498
|
+
coverage.CoverageContext(
|
499
|
+
location="body", generation_modes=generation_modes, is_required=body.is_required
|
500
|
+
),
|
501
|
+
schema,
|
496
502
|
)
|
497
503
|
value = next(gen, NOT_SET)
|
498
504
|
if isinstance(value, NotSet):
|
@@ -712,11 +718,13 @@ def _iter_coverage_cases(
|
|
712
718
|
}
|
713
719
|
|
714
720
|
def _yield_negative(
|
715
|
-
subschema: dict[str, Any], _location: str, _container_name: str
|
721
|
+
subschema: dict[str, Any], _location: str, _container_name: str, is_required: bool
|
716
722
|
) -> Generator[Case, None, None]:
|
717
723
|
iterator = iter(
|
718
724
|
coverage.cover_schema_iter(
|
719
|
-
coverage.CoverageContext(
|
725
|
+
coverage.CoverageContext(
|
726
|
+
location=_location, generation_modes=[GenerationMode.NEGATIVE], is_required=is_required
|
727
|
+
),
|
720
728
|
subschema,
|
721
729
|
)
|
722
730
|
)
|
@@ -751,7 +759,7 @@ def _iter_coverage_cases(
|
|
751
759
|
)
|
752
760
|
if GenerationMode.NEGATIVE in generation_modes:
|
753
761
|
subschema = _combination_schema(only_required, required, parameter_set)
|
754
|
-
for case in _yield_negative(subschema, location, container_name):
|
762
|
+
for case in _yield_negative(subschema, location, container_name, is_required=bool(required)):
|
755
763
|
kwargs = _case_to_kwargs(case)
|
756
764
|
if not seen_negative.insert(kwargs):
|
757
765
|
continue
|
@@ -778,7 +786,7 @@ def _iter_coverage_cases(
|
|
778
786
|
)
|
779
787
|
if GenerationMode.NEGATIVE in generation_modes:
|
780
788
|
subschema = _combination_schema(combo, required, parameter_set)
|
781
|
-
for case in _yield_negative(subschema, location, container_name):
|
789
|
+
for case in _yield_negative(subschema, location, container_name, is_required=bool(required)):
|
782
790
|
assert case.meta is not None
|
783
791
|
assert isinstance(case.meta.phase.data, CoveragePhaseData)
|
784
792
|
# Already generated in one of the blocks above
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: schemathesis
|
3
|
-
Version: 4.0.
|
3
|
+
Version: 4.0.4
|
4
4
|
Summary: Property-based testing framework for Open API and GraphQL based apps
|
5
5
|
Project-URL: Documentation, https://schemathesis.readthedocs.io/en/stable/
|
6
6
|
Project-URL: Changelog, https://github.com/schemathesis/schemathesis/blob/master/CHANGELOG.md
|
@@ -72,26 +72,26 @@ schemathesis/engine/__init__.py,sha256=QaFE-FinaTAaarteADo2RRMJ-Sz6hZB9TzD5KjMin
|
|
72
72
|
schemathesis/engine/context.py,sha256=x-I9KX6rO6hdCvvN8FEdzIZBqIcNaxdNYHgQjcXbZhM,3931
|
73
73
|
schemathesis/engine/control.py,sha256=FXzP8dxL47j1Giqpy2-Bsr_MdMw9YiATSK_UfpFwDtk,1348
|
74
74
|
schemathesis/engine/core.py,sha256=5jfAqFH0XSD7NVgoSXuUPW-dooItscneAzUNq1RBh1E,5712
|
75
|
-
schemathesis/engine/errors.py,sha256=
|
75
|
+
schemathesis/engine/errors.py,sha256=HRtFFg-TQ68VmGAM3p6VLOimTU7VaFnv6iKD9-ucjaw,18932
|
76
76
|
schemathesis/engine/events.py,sha256=VV6epicFIJnX4c87fVNSd0ibDccX3gryDv52OUGa3FI,6370
|
77
77
|
schemathesis/engine/recorder.py,sha256=K3HfMARrT5mPWXPnYebjjcq5CcsBRhMrtZwEL9_Lvtg,8432
|
78
78
|
schemathesis/engine/phases/__init__.py,sha256=jUIfb_9QoUo4zmJEVU0z70PgXPYjt8CIqp4qP_HlYHg,3146
|
79
79
|
schemathesis/engine/phases/probes.py,sha256=SEtWKPdkLfRTKV0_tbiNHTK3sJsUUPZ0jZQ9Nv4qUi8,5678
|
80
80
|
schemathesis/engine/phases/stateful/__init__.py,sha256=Lz1rgNqCfUSIz173XqCGsiMuUI5bh4L-RIFexU1-c_Q,2461
|
81
|
-
schemathesis/engine/phases/stateful/_executor.py,sha256=
|
81
|
+
schemathesis/engine/phases/stateful/_executor.py,sha256=_303Yqflx1iFNTQI2EfjSp_2T21YvzJJgMSazhpv5JQ,15200
|
82
82
|
schemathesis/engine/phases/stateful/context.py,sha256=A7X1SLDOWFpCvFN9IiIeNVZM0emjqatmJL_k9UsO7vM,2946
|
83
83
|
schemathesis/engine/phases/unit/__init__.py,sha256=BvZh39LZmXg90Cy_Tn0cQY5y7eWzYvAEmJ43fGKFAt8,8715
|
84
|
-
schemathesis/engine/phases/unit/_executor.py,sha256=
|
84
|
+
schemathesis/engine/phases/unit/_executor.py,sha256=9MmZoKSBVSPk0LWwN3PZ3iaO9nzpT1Z70yzdEE48YYw,16489
|
85
85
|
schemathesis/engine/phases/unit/_pool.py,sha256=iU0hdHDmohPnEv7_S1emcabuzbTf-Cznqwn0pGQ5wNQ,2480
|
86
86
|
schemathesis/generation/__init__.py,sha256=tvNO2FLiY8z3fZ_kL_QJhSgzXfnT4UqwSXMHCwfLI0g,645
|
87
87
|
schemathesis/generation/case.py,sha256=MuqnKsJBpGm2gaqDFdJi1yGSWgBhqJUwtYaX97kfXgo,11820
|
88
|
-
schemathesis/generation/coverage.py,sha256=
|
88
|
+
schemathesis/generation/coverage.py,sha256=SlPD8WfrRXca4A9p6P894JXBAqjdCVAto0V4qQrceOE,48825
|
89
89
|
schemathesis/generation/meta.py,sha256=adkoMuCfzSjHJ9ZDocQn0GnVldSCkLL3eVR5A_jafwM,2552
|
90
90
|
schemathesis/generation/metrics.py,sha256=cZU5HdeAMcLFEDnTbNE56NuNq4P0N4ew-g1NEz5-kt4,2836
|
91
91
|
schemathesis/generation/modes.py,sha256=Q1fhjWr3zxabU5qdtLvKfpMFZJAwlW9pnxgenjeXTyU,481
|
92
92
|
schemathesis/generation/overrides.py,sha256=OBWqDQPreiliaf2M-oyXppVKHoJkCRzxtwSJx1b6AFw,3759
|
93
93
|
schemathesis/generation/hypothesis/__init__.py,sha256=SVwM-rx07jPZzms0idWYACgUtWAxh49HRuTnaQ__zf0,1549
|
94
|
-
schemathesis/generation/hypothesis/builder.py,sha256=
|
94
|
+
schemathesis/generation/hypothesis/builder.py,sha256=JtKh9hzob2byQrAtf0IXOgKX1c17mLiMQ8f030Hae2Y,33414
|
95
95
|
schemathesis/generation/hypothesis/examples.py,sha256=6eGaKUEC3elmKsaqfKj1sLvM8EHc-PWT4NRBq4NI0Rs,1409
|
96
96
|
schemathesis/generation/hypothesis/given.py,sha256=sTZR1of6XaHAPWtHx2_WLlZ50M8D5Rjux0GmWkWjDq4,2337
|
97
97
|
schemathesis/generation/hypothesis/reporting.py,sha256=uDVow6Ya8YFkqQuOqRsjbzsbyP4KKfr3jA7ZaY4FuKY,279
|
@@ -157,8 +157,8 @@ schemathesis/transport/prepare.py,sha256=iiB8KTAqnnuqjWzblIPiGVdkGIF7Yr1SAEz-KZz
|
|
157
157
|
schemathesis/transport/requests.py,sha256=rziZTrZCVMAqgy6ldB8iTwhkpAsnjKSgK8hj5Sq3ThE,10656
|
158
158
|
schemathesis/transport/serialization.py,sha256=igUXKZ_VJ9gV7P0TUc5PDQBJXl_s0kK9T3ljGWWvo6E,10339
|
159
159
|
schemathesis/transport/wsgi.py,sha256=KoAfvu6RJtzyj24VGB8e-Iaa9smpgXJ3VsM8EgAz2tc,6152
|
160
|
-
schemathesis-4.0.
|
161
|
-
schemathesis-4.0.
|
162
|
-
schemathesis-4.0.
|
163
|
-
schemathesis-4.0.
|
164
|
-
schemathesis-4.0.
|
160
|
+
schemathesis-4.0.4.dist-info/METADATA,sha256=rRUbFXvyFsf72j7N6smvPu4mMvQYJLnDCvjPwf3XPOY,8471
|
161
|
+
schemathesis-4.0.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
162
|
+
schemathesis-4.0.4.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
|
163
|
+
schemathesis-4.0.4.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
|
164
|
+
schemathesis-4.0.4.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|