schemathesis 3.36.2__py3-none-any.whl → 3.36.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/_hypothesis.py +15 -5
- schemathesis/cli/cassettes.py +16 -9
- schemathesis/generation/_hypothesis.py +7 -1
- schemathesis/generation/coverage.py +140 -75
- schemathesis/internal/checks.py +2 -1
- schemathesis/models.py +5 -1
- schemathesis/runner/serialization.py +2 -0
- schemathesis/specs/openapi/_hypothesis.py +1 -0
- schemathesis/specs/openapi/definitions.py +7 -11
- schemathesis/stateful/runner.py +5 -1
- schemathesis/stateful/validation.py +11 -14
- schemathesis/transports/__init__.py +1 -1
- {schemathesis-3.36.2.dist-info → schemathesis-3.36.4.dist-info}/METADATA +1 -1
- {schemathesis-3.36.2.dist-info → schemathesis-3.36.4.dist-info}/RECORD +17 -17
- {schemathesis-3.36.2.dist-info → schemathesis-3.36.4.dist-info}/WHEEL +0 -0
- {schemathesis-3.36.2.dist-info → schemathesis-3.36.4.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.36.2.dist-info → schemathesis-3.36.4.dist-info}/licenses/LICENSE +0 -0
schemathesis/_hypothesis.py
CHANGED
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import json
|
|
7
|
+
from copy import copy
|
|
7
8
|
import warnings
|
|
8
9
|
from typing import TYPE_CHECKING, Any, Callable, Generator, Mapping
|
|
9
10
|
|
|
@@ -221,7 +222,13 @@ def _iter_coverage_cases(
|
|
|
221
222
|
|
|
222
223
|
ctx = coverage.CoverageContext(data_generation_methods=data_generation_methods)
|
|
223
224
|
meta = GenerationMetadata(
|
|
224
|
-
query=None,
|
|
225
|
+
query=None,
|
|
226
|
+
path_parameters=None,
|
|
227
|
+
headers=None,
|
|
228
|
+
cookies=None,
|
|
229
|
+
body=None,
|
|
230
|
+
phase=TestPhase.COVERAGE,
|
|
231
|
+
description=None,
|
|
225
232
|
)
|
|
226
233
|
generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
|
|
227
234
|
template: dict[str, Any] = {}
|
|
@@ -259,17 +266,19 @@ def _iter_coverage_cases(
|
|
|
259
266
|
template["media_type"] = body.media_type
|
|
260
267
|
case = operation.make_case(**{**template, "body": value.value, "media_type": body.media_type})
|
|
261
268
|
case.data_generation_method = value.data_generation_method
|
|
262
|
-
case.meta = meta
|
|
269
|
+
case.meta = copy(meta)
|
|
270
|
+
case.meta.description = value.description
|
|
263
271
|
yield case
|
|
264
272
|
for next_value in gen:
|
|
265
273
|
case = operation.make_case(**{**template, "body": next_value.value, "media_type": body.media_type})
|
|
266
274
|
case.data_generation_method = next_value.data_generation_method
|
|
267
|
-
case.meta = meta
|
|
275
|
+
case.meta = copy(meta)
|
|
276
|
+
case.meta.description = next_value.description
|
|
268
277
|
yield case
|
|
269
278
|
elif DataGenerationMethod.positive in data_generation_methods:
|
|
270
279
|
case = operation.make_case(**template)
|
|
271
280
|
case.data_generation_method = DataGenerationMethod.positive
|
|
272
|
-
case.meta = meta
|
|
281
|
+
case.meta = copy(meta)
|
|
273
282
|
yield case
|
|
274
283
|
for (location, name), gen in generators.items():
|
|
275
284
|
container_name = LOCATION_TO_CONTAINER[location]
|
|
@@ -281,7 +290,8 @@ def _iter_coverage_cases(
|
|
|
281
290
|
generated = value.value
|
|
282
291
|
case = operation.make_case(**{**template, container_name: {**container, name: generated}})
|
|
283
292
|
case.data_generation_method = value.data_generation_method
|
|
284
|
-
case.meta = meta
|
|
293
|
+
case.meta = copy(meta)
|
|
294
|
+
case.meta.description = value.description
|
|
285
295
|
yield case
|
|
286
296
|
|
|
287
297
|
|
schemathesis/cli/cassettes.py
CHANGED
|
@@ -79,24 +79,18 @@ class CassetteWriter(EventHandler):
|
|
|
79
79
|
def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
|
|
80
80
|
if isinstance(event, events.Initialized):
|
|
81
81
|
# In the beginning we write metadata and start `http_interactions` list
|
|
82
|
-
self.queue.put(Initialize())
|
|
82
|
+
self.queue.put(Initialize(seed=event.seed))
|
|
83
83
|
elif isinstance(event, events.AfterExecution):
|
|
84
|
-
# Seed is always present at this point, the original Optional[int] type is there because `TestResult`
|
|
85
|
-
# instance is created before `seed` is generated on the hypothesis side
|
|
86
|
-
seed = cast(int, event.result.seed)
|
|
87
84
|
self.queue.put(
|
|
88
85
|
Process(
|
|
89
|
-
seed=seed,
|
|
90
86
|
correlation_id=event.correlation_id,
|
|
91
87
|
thread_id=event.thread_id,
|
|
92
88
|
interactions=event.result.interactions,
|
|
93
89
|
)
|
|
94
90
|
)
|
|
95
91
|
elif isinstance(event, events.AfterStatefulExecution):
|
|
96
|
-
seed = cast(int, event.result.seed)
|
|
97
92
|
self.queue.put(
|
|
98
93
|
Process(
|
|
99
|
-
seed=seed,
|
|
100
94
|
# Correlation ID is not used in stateful testing
|
|
101
95
|
correlation_id="",
|
|
102
96
|
thread_id=event.thread_id,
|
|
@@ -118,12 +112,13 @@ class CassetteWriter(EventHandler):
|
|
|
118
112
|
class Initialize:
|
|
119
113
|
"""Start up, the first message to make preparations before proceeding the input data."""
|
|
120
114
|
|
|
115
|
+
seed: int | None
|
|
116
|
+
|
|
121
117
|
|
|
122
118
|
@dataclass
|
|
123
119
|
class Process:
|
|
124
120
|
"""A new chunk of data should be processed."""
|
|
125
121
|
|
|
126
|
-
seed: int
|
|
127
122
|
correlation_id: str
|
|
128
123
|
thread_id: int
|
|
129
124
|
interactions: list[SerializedInteraction]
|
|
@@ -219,9 +214,11 @@ def vcr_writer(file_handle: click.utils.LazyFile, preserve_exact_body_bytes: boo
|
|
|
219
214
|
)
|
|
220
215
|
write_double_quoted(output, string)
|
|
221
216
|
|
|
217
|
+
seed = "null"
|
|
222
218
|
while True:
|
|
223
219
|
item = queue.get()
|
|
224
220
|
if isinstance(item, Initialize):
|
|
221
|
+
seed = f"'{item.seed}'"
|
|
225
222
|
stream.write(
|
|
226
223
|
f"""command: '{get_command_representation()}'
|
|
227
224
|
recorded_with: 'Schemathesis {SCHEMATHESIS_VERSION}'
|
|
@@ -235,10 +232,20 @@ http_interactions:"""
|
|
|
235
232
|
stream.write(
|
|
236
233
|
f"""\n- id: '{current_id}'
|
|
237
234
|
status: '{status}'
|
|
238
|
-
seed:
|
|
235
|
+
seed: {seed}
|
|
239
236
|
thread_id: {item.thread_id}
|
|
240
237
|
correlation_id: '{item.correlation_id}'
|
|
241
238
|
data_generation_method: '{interaction.data_generation_method.value}'
|
|
239
|
+
meta:
|
|
240
|
+
description: """
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if interaction.description is not None:
|
|
244
|
+
write_double_quoted(stream, interaction.description)
|
|
245
|
+
else:
|
|
246
|
+
stream.write("null")
|
|
247
|
+
stream.write(
|
|
248
|
+
f"""
|
|
242
249
|
phase: {phase}
|
|
243
250
|
elapsed: '{interaction.response.elapsed if interaction.response else 0}'
|
|
244
251
|
recorded_at: '{interaction.recorded_at}'
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
from functools import lru_cache, reduce
|
|
4
5
|
from operator import or_
|
|
5
6
|
from typing import TYPE_CHECKING, TypeVar
|
|
@@ -8,6 +9,8 @@ if TYPE_CHECKING:
|
|
|
8
9
|
from hypothesis import settings
|
|
9
10
|
from hypothesis import strategies as st
|
|
10
11
|
|
|
12
|
+
SCHEMATHESIS_BENCHMARK_SEED = os.environ.get("SCHEMATHESIS_BENCHMARK_SEED")
|
|
13
|
+
|
|
11
14
|
|
|
12
15
|
@lru_cache
|
|
13
16
|
def default_settings() -> settings:
|
|
@@ -33,13 +36,16 @@ def get_single_example(strategy: st.SearchStrategy[T]) -> T: # type: ignore[typ
|
|
|
33
36
|
|
|
34
37
|
|
|
35
38
|
def add_single_example(strategy: st.SearchStrategy[T], examples: list[T]) -> None:
|
|
36
|
-
from hypothesis import given
|
|
39
|
+
from hypothesis import given, seed
|
|
37
40
|
|
|
38
41
|
@given(strategy) # type: ignore
|
|
39
42
|
@default_settings() # type: ignore
|
|
40
43
|
def example_generating_inner_function(ex: T) -> None:
|
|
41
44
|
examples.append(ex)
|
|
42
45
|
|
|
46
|
+
if SCHEMATHESIS_BENCHMARK_SEED is not None:
|
|
47
|
+
example_generating_inner_function = seed(SCHEMATHESIS_BENCHMARK_SEED)(example_generating_inner_function)
|
|
48
|
+
|
|
43
49
|
example_generating_inner_function()
|
|
44
50
|
|
|
45
51
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import functools
|
|
4
5
|
from contextlib import contextmanager, suppress
|
|
5
|
-
from dataclasses import dataclass
|
|
6
|
+
from dataclasses import dataclass
|
|
6
7
|
from functools import lru_cache
|
|
7
8
|
from itertools import combinations
|
|
8
9
|
from typing import Any, Generator, Iterator, TypeVar, cast
|
|
@@ -18,12 +19,20 @@ from schemathesis.constants import NOT_SET
|
|
|
18
19
|
from ._hypothesis import get_single_example
|
|
19
20
|
from ._methods import DataGenerationMethod
|
|
20
21
|
|
|
22
|
+
|
|
23
|
+
def _replace_zero_with_nonzero(x: float) -> float:
|
|
24
|
+
return x or 0.0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def json_recursive_strategy(strategy: st.SearchStrategy) -> st.SearchStrategy:
|
|
28
|
+
return st.lists(strategy, max_size=3) | st.dictionaries(st.text(), strategy, max_size=3)
|
|
29
|
+
|
|
30
|
+
|
|
21
31
|
BUFFER_SIZE = 8 * 1024
|
|
22
|
-
FLOAT_STRATEGY: st.SearchStrategy = st.floats(allow_nan=False, allow_infinity=False).map(
|
|
32
|
+
FLOAT_STRATEGY: st.SearchStrategy = st.floats(allow_nan=False, allow_infinity=False).map(_replace_zero_with_nonzero)
|
|
23
33
|
NUMERIC_STRATEGY: st.SearchStrategy = st.integers() | FLOAT_STRATEGY
|
|
24
34
|
JSON_STRATEGY: st.SearchStrategy = st.recursive(
|
|
25
|
-
st.none() | st.booleans() | NUMERIC_STRATEGY | st.text(),
|
|
26
|
-
lambda strategy: st.lists(strategy, max_size=3) | st.dictionaries(st.text(), strategy, max_size=3),
|
|
35
|
+
st.none() | st.booleans() | NUMERIC_STRATEGY | st.text(), json_recursive_strategy
|
|
27
36
|
)
|
|
28
37
|
ARRAY_STRATEGY: st.SearchStrategy = st.lists(JSON_STRATEGY)
|
|
29
38
|
OBJECT_STRATEGY: st.SearchStrategy = st.dictionaries(st.text(), JSON_STRATEGY)
|
|
@@ -36,16 +45,17 @@ UNKNOWN_PROPERTY_VALUE = 42
|
|
|
36
45
|
class GeneratedValue:
|
|
37
46
|
value: Any
|
|
38
47
|
data_generation_method: DataGenerationMethod
|
|
48
|
+
description: str
|
|
39
49
|
|
|
40
|
-
__slots__ = ("value", "data_generation_method")
|
|
50
|
+
__slots__ = ("value", "data_generation_method", "description")
|
|
41
51
|
|
|
42
52
|
@classmethod
|
|
43
|
-
def with_positive(cls, value: Any) -> GeneratedValue:
|
|
44
|
-
return cls(value, DataGenerationMethod.positive)
|
|
53
|
+
def with_positive(cls, value: Any, *, description: str) -> GeneratedValue:
|
|
54
|
+
return cls(value=value, data_generation_method=DataGenerationMethod.positive, description=description)
|
|
45
55
|
|
|
46
56
|
@classmethod
|
|
47
|
-
def with_negative(cls, value: Any) -> GeneratedValue:
|
|
48
|
-
return cls(value, DataGenerationMethod.negative)
|
|
57
|
+
def with_negative(cls, value: Any, *, description: str) -> GeneratedValue:
|
|
58
|
+
return cls(value=value, data_generation_method=DataGenerationMethod.negative, description=description)
|
|
49
59
|
|
|
50
60
|
|
|
51
61
|
PositiveValue = GeneratedValue.with_positive
|
|
@@ -59,7 +69,14 @@ def cached_draw(strategy: st.SearchStrategy) -> Any:
|
|
|
59
69
|
|
|
60
70
|
@dataclass
|
|
61
71
|
class CoverageContext:
|
|
62
|
-
data_generation_methods: list[DataGenerationMethod]
|
|
72
|
+
data_generation_methods: list[DataGenerationMethod]
|
|
73
|
+
|
|
74
|
+
__slots__ = ("data_generation_methods",)
|
|
75
|
+
|
|
76
|
+
def __init__(self, data_generation_methods: list[DataGenerationMethod] | None = None) -> None:
|
|
77
|
+
self.data_generation_methods = (
|
|
78
|
+
data_generation_methods if data_generation_methods is not None else DataGenerationMethod.all()
|
|
79
|
+
)
|
|
63
80
|
|
|
64
81
|
@classmethod
|
|
65
82
|
def with_positive(cls) -> CoverageContext:
|
|
@@ -69,12 +86,8 @@ class CoverageContext:
|
|
|
69
86
|
def with_negative(cls) -> CoverageContext:
|
|
70
87
|
return CoverageContext(data_generation_methods=[DataGenerationMethod.negative])
|
|
71
88
|
|
|
72
|
-
def generate_from(self, strategy: st.SearchStrategy
|
|
73
|
-
|
|
74
|
-
value = cached_draw(strategy)
|
|
75
|
-
else:
|
|
76
|
-
value = get_single_example(strategy)
|
|
77
|
-
return value
|
|
89
|
+
def generate_from(self, strategy: st.SearchStrategy) -> Any:
|
|
90
|
+
return cached_draw(strategy)
|
|
78
91
|
|
|
79
92
|
def generate_from_schema(self, schema: dict) -> Any:
|
|
80
93
|
return self.generate_from(from_schema(schema))
|
|
@@ -117,15 +130,15 @@ def _cover_positive_for_type(
|
|
|
117
130
|
yield from cover_schema_iter(ctx, canonical)
|
|
118
131
|
if enum is not NOT_SET:
|
|
119
132
|
for value in enum:
|
|
120
|
-
yield PositiveValue(value)
|
|
133
|
+
yield PositiveValue(value, description="Enum value")
|
|
121
134
|
elif const is not NOT_SET:
|
|
122
|
-
yield PositiveValue(const)
|
|
135
|
+
yield PositiveValue(const, description="Const value")
|
|
123
136
|
elif ty is not None:
|
|
124
137
|
if ty == "null":
|
|
125
|
-
yield PositiveValue(None)
|
|
138
|
+
yield PositiveValue(None, description="Value null value")
|
|
126
139
|
elif ty == "boolean":
|
|
127
|
-
yield PositiveValue(True)
|
|
128
|
-
yield PositiveValue(False)
|
|
140
|
+
yield PositiveValue(True, description="Valid boolean value")
|
|
141
|
+
yield PositiveValue(False, description="Valid boolean value")
|
|
129
142
|
elif ty == "string":
|
|
130
143
|
yield from _positive_string(ctx, schema)
|
|
131
144
|
elif ty == "integer" or ty == "number":
|
|
@@ -200,15 +213,17 @@ def cover_schema_iter(
|
|
|
200
213
|
elif key == "maximum":
|
|
201
214
|
next = value + 1
|
|
202
215
|
if next not in seen:
|
|
203
|
-
yield NegativeValue(next)
|
|
216
|
+
yield NegativeValue(next, description="Value greater than maximum")
|
|
204
217
|
seen.add(next)
|
|
205
218
|
elif key == "minimum":
|
|
206
219
|
next = value - 1
|
|
207
220
|
if next not in seen:
|
|
208
|
-
yield NegativeValue(next)
|
|
221
|
+
yield NegativeValue(next, description="Value smaller than minimum")
|
|
209
222
|
seen.add(next)
|
|
210
223
|
elif key == "exclusiveMaximum" or key == "exclusiveMinimum" and value not in seen:
|
|
211
|
-
|
|
224
|
+
verb = "greater" if key == "exclusiveMaximum" else "smaller"
|
|
225
|
+
limit = "maximum" if key == "exclusiveMaximum" else "minimum"
|
|
226
|
+
yield NegativeValue(value, description=f"Value {verb} than {limit}")
|
|
212
227
|
seen.add(value)
|
|
213
228
|
elif key == "multipleOf":
|
|
214
229
|
for value_ in _negative_multiple_of(ctx, schema, value):
|
|
@@ -221,14 +236,14 @@ def cover_schema_iter(
|
|
|
221
236
|
value = ctx.generate_from_schema({**schema, "minLength": value - 1, "maxLength": value - 1})
|
|
222
237
|
k = _to_hashable_key(value)
|
|
223
238
|
if k not in seen:
|
|
224
|
-
yield NegativeValue(value)
|
|
239
|
+
yield NegativeValue(value, description="String smaller than minLength")
|
|
225
240
|
seen.add(k)
|
|
226
241
|
elif key == "maxLength" and value < BUFFER_SIZE:
|
|
227
242
|
with suppress(InvalidArgument):
|
|
228
243
|
value = ctx.generate_from_schema({**schema, "minLength": value + 1, "maxLength": value + 1})
|
|
229
244
|
k = _to_hashable_key(value)
|
|
230
245
|
if k not in seen:
|
|
231
|
-
yield NegativeValue(value)
|
|
246
|
+
yield NegativeValue(value, description="String larger than maxLength")
|
|
232
247
|
seen.add(k)
|
|
233
248
|
elif key == "uniqueItems" and value:
|
|
234
249
|
yield from _negative_unique_items(ctx, schema)
|
|
@@ -237,7 +252,10 @@ def cover_schema_iter(
|
|
|
237
252
|
yield from _negative_required(ctx, template, value)
|
|
238
253
|
elif key == "additionalProperties" and not value:
|
|
239
254
|
template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
|
|
240
|
-
yield NegativeValue(
|
|
255
|
+
yield NegativeValue(
|
|
256
|
+
{**template, UNKNOWN_PROPERTY_KEY: UNKNOWN_PROPERTY_VALUE},
|
|
257
|
+
description="Object with unexpected properties",
|
|
258
|
+
)
|
|
241
259
|
elif key == "allOf":
|
|
242
260
|
nctx = ctx.with_negative()
|
|
243
261
|
if len(value) == 1:
|
|
@@ -291,43 +309,50 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
|
291
309
|
default = schema.get("default")
|
|
292
310
|
if example or examples or default:
|
|
293
311
|
if example:
|
|
294
|
-
yield PositiveValue(example)
|
|
312
|
+
yield PositiveValue(example, description="Example value")
|
|
295
313
|
if examples:
|
|
296
314
|
for example in examples:
|
|
297
|
-
yield PositiveValue(example)
|
|
315
|
+
yield PositiveValue(example, description="Example value")
|
|
298
316
|
if (
|
|
299
317
|
default
|
|
300
318
|
and not (example is not None and default == example)
|
|
301
319
|
and not (examples is not None and any(default == ex for ex in examples))
|
|
302
320
|
):
|
|
303
|
-
yield PositiveValue(default)
|
|
321
|
+
yield PositiveValue(default, description="Default value")
|
|
304
322
|
elif not min_length and not max_length:
|
|
305
323
|
# Default positive value
|
|
306
|
-
yield PositiveValue(ctx.generate_from_schema(schema))
|
|
324
|
+
yield PositiveValue(ctx.generate_from_schema(schema), description="Valid string")
|
|
307
325
|
elif "pattern" in schema:
|
|
308
326
|
# Without merging `maxLength` & `minLength` into a regex it is problematic
|
|
309
327
|
# to generate a valid value as the unredlying machinery will resort to filtering
|
|
310
328
|
# and it is unlikely that it will generate a string of that length
|
|
311
|
-
yield PositiveValue(ctx.generate_from_schema(schema))
|
|
329
|
+
yield PositiveValue(ctx.generate_from_schema(schema), description="Valid string")
|
|
312
330
|
return
|
|
313
331
|
|
|
314
332
|
seen = set()
|
|
315
333
|
|
|
316
334
|
if min_length is not None and min_length < BUFFER_SIZE:
|
|
317
335
|
# Exactly the minimum length
|
|
318
|
-
yield PositiveValue(
|
|
336
|
+
yield PositiveValue(
|
|
337
|
+
ctx.generate_from_schema({**schema, "maxLength": min_length}), description="Minimum length string"
|
|
338
|
+
)
|
|
319
339
|
seen.add(min_length)
|
|
320
340
|
|
|
321
341
|
# One character more than minimum if possible
|
|
322
342
|
larger = min_length + 1
|
|
323
343
|
if larger < BUFFER_SIZE and larger not in seen and (not max_length or larger <= max_length):
|
|
324
|
-
yield PositiveValue(
|
|
344
|
+
yield PositiveValue(
|
|
345
|
+
ctx.generate_from_schema({**schema, "minLength": larger, "maxLength": larger}),
|
|
346
|
+
description="Near-boundary length string",
|
|
347
|
+
)
|
|
325
348
|
seen.add(larger)
|
|
326
349
|
|
|
327
350
|
if max_length is not None:
|
|
328
351
|
# Exactly the maximum length
|
|
329
352
|
if max_length < BUFFER_SIZE and max_length not in seen:
|
|
330
|
-
yield PositiveValue(
|
|
353
|
+
yield PositiveValue(
|
|
354
|
+
ctx.generate_from_schema({**schema, "minLength": max_length}), description="Maximum length string"
|
|
355
|
+
)
|
|
331
356
|
seen.add(max_length)
|
|
332
357
|
|
|
333
358
|
# One character less than maximum if possible
|
|
@@ -337,7 +362,10 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
|
337
362
|
and smaller not in seen
|
|
338
363
|
and (smaller > 0 and (min_length is None or smaller >= min_length))
|
|
339
364
|
):
|
|
340
|
-
yield PositiveValue(
|
|
365
|
+
yield PositiveValue(
|
|
366
|
+
ctx.generate_from_schema({**schema, "minLength": smaller, "maxLength": smaller}),
|
|
367
|
+
description="Near-boundary length string",
|
|
368
|
+
)
|
|
341
369
|
seen.add(smaller)
|
|
342
370
|
|
|
343
371
|
|
|
@@ -367,19 +395,19 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
|
367
395
|
|
|
368
396
|
if example or examples or default:
|
|
369
397
|
if example:
|
|
370
|
-
yield PositiveValue(example)
|
|
398
|
+
yield PositiveValue(example, description="Example value")
|
|
371
399
|
if examples:
|
|
372
400
|
for example in examples:
|
|
373
|
-
yield PositiveValue(example)
|
|
401
|
+
yield PositiveValue(example, description="Example value")
|
|
374
402
|
if (
|
|
375
403
|
default
|
|
376
404
|
and not (example is not None and default == example)
|
|
377
405
|
and not (examples is not None and any(default == ex for ex in examples))
|
|
378
406
|
):
|
|
379
|
-
yield PositiveValue(default)
|
|
407
|
+
yield PositiveValue(default, description="Default value")
|
|
380
408
|
elif not minimum and not maximum:
|
|
381
409
|
# Default positive value
|
|
382
|
-
yield PositiveValue(ctx.generate_from_schema(schema))
|
|
410
|
+
yield PositiveValue(ctx.generate_from_schema(schema), description="Valid number")
|
|
383
411
|
|
|
384
412
|
seen = set()
|
|
385
413
|
|
|
@@ -390,7 +418,7 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
|
390
418
|
else:
|
|
391
419
|
smallest = minimum
|
|
392
420
|
seen.add(smallest)
|
|
393
|
-
yield PositiveValue(smallest)
|
|
421
|
+
yield PositiveValue(smallest, description="Minimum value")
|
|
394
422
|
|
|
395
423
|
# One more than minimum if possible
|
|
396
424
|
if multiple_of is not None:
|
|
@@ -399,7 +427,7 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
|
399
427
|
larger = minimum + 1
|
|
400
428
|
if larger not in seen and (not maximum or larger <= maximum):
|
|
401
429
|
seen.add(larger)
|
|
402
|
-
yield PositiveValue(larger)
|
|
430
|
+
yield PositiveValue(larger, description="Near-boundary number")
|
|
403
431
|
|
|
404
432
|
if maximum is not None:
|
|
405
433
|
# Exactly the maximum
|
|
@@ -409,7 +437,7 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
|
409
437
|
largest = maximum
|
|
410
438
|
if largest not in seen:
|
|
411
439
|
seen.add(largest)
|
|
412
|
-
yield PositiveValue(largest)
|
|
440
|
+
yield PositiveValue(largest, description="Maximum value")
|
|
413
441
|
|
|
414
442
|
# One less than maximum if possible
|
|
415
443
|
if multiple_of is not None:
|
|
@@ -418,7 +446,7 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
|
418
446
|
smaller = maximum - 1
|
|
419
447
|
if smaller not in seen and (smaller > 0 and (minimum is None or smaller >= minimum)):
|
|
420
448
|
seen.add(smaller)
|
|
421
|
-
yield PositiveValue(smaller)
|
|
449
|
+
yield PositiveValue(smaller, description="Near-boundary number")
|
|
422
450
|
|
|
423
451
|
|
|
424
452
|
def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Generator[GeneratedValue, None, None]:
|
|
@@ -429,18 +457,18 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
|
|
|
429
457
|
|
|
430
458
|
if example or examples or default:
|
|
431
459
|
if example:
|
|
432
|
-
yield PositiveValue(example)
|
|
460
|
+
yield PositiveValue(example, description="Example value")
|
|
433
461
|
if examples:
|
|
434
462
|
for example in examples:
|
|
435
|
-
yield PositiveValue(example)
|
|
463
|
+
yield PositiveValue(example, description="Example value")
|
|
436
464
|
if (
|
|
437
465
|
default
|
|
438
466
|
and not (example is not None and default == example)
|
|
439
467
|
and not (examples is not None and any(default == ex for ex in examples))
|
|
440
468
|
):
|
|
441
|
-
yield PositiveValue(default)
|
|
469
|
+
yield PositiveValue(default, description="Default value")
|
|
442
470
|
else:
|
|
443
|
-
yield PositiveValue(template)
|
|
471
|
+
yield PositiveValue(template, description="Valid array")
|
|
444
472
|
seen.add(len(template))
|
|
445
473
|
|
|
446
474
|
# Boundary and near-boundary sizes
|
|
@@ -452,12 +480,18 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
|
|
|
452
480
|
# One item more than minimum if possible
|
|
453
481
|
larger = min_items + 1
|
|
454
482
|
if larger not in seen and (max_items is None or larger <= max_items):
|
|
455
|
-
yield PositiveValue(
|
|
483
|
+
yield PositiveValue(
|
|
484
|
+
ctx.generate_from_schema({**schema, "minItems": larger, "maxItems": larger}),
|
|
485
|
+
description="Near-boundary items array",
|
|
486
|
+
)
|
|
456
487
|
seen.add(larger)
|
|
457
488
|
|
|
458
489
|
if max_items is not None:
|
|
459
490
|
if max_items < BUFFER_SIZE and max_items not in seen:
|
|
460
|
-
yield PositiveValue(
|
|
491
|
+
yield PositiveValue(
|
|
492
|
+
ctx.generate_from_schema({**schema, "minItems": max_items}),
|
|
493
|
+
description="Maximum items array",
|
|
494
|
+
)
|
|
461
495
|
seen.add(max_items)
|
|
462
496
|
|
|
463
497
|
# One item smaller than maximum if possible
|
|
@@ -468,7 +502,10 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
|
|
|
468
502
|
and smaller not in seen
|
|
469
503
|
and (min_items is None or smaller >= min_items)
|
|
470
504
|
):
|
|
471
|
-
yield PositiveValue(
|
|
505
|
+
yield PositiveValue(
|
|
506
|
+
ctx.generate_from_schema({**schema, "minItems": smaller, "maxItems": smaller}),
|
|
507
|
+
description="Near-boundary items array",
|
|
508
|
+
)
|
|
472
509
|
seen.add(smaller)
|
|
473
510
|
|
|
474
511
|
|
|
@@ -479,19 +516,18 @@ def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Gene
|
|
|
479
516
|
|
|
480
517
|
if example or examples or default:
|
|
481
518
|
if example:
|
|
482
|
-
yield PositiveValue(example)
|
|
519
|
+
yield PositiveValue(example, description="Example value")
|
|
483
520
|
if examples:
|
|
484
521
|
for example in examples:
|
|
485
|
-
yield PositiveValue(example)
|
|
522
|
+
yield PositiveValue(example, description="Example value")
|
|
486
523
|
if (
|
|
487
524
|
default
|
|
488
525
|
and not (example is not None and default == example)
|
|
489
526
|
and not (examples is not None and any(default == ex for ex in examples))
|
|
490
527
|
):
|
|
491
|
-
yield PositiveValue(default)
|
|
492
|
-
|
|
528
|
+
yield PositiveValue(default, description="Default value")
|
|
493
529
|
else:
|
|
494
|
-
yield PositiveValue(template)
|
|
530
|
+
yield PositiveValue(template, description="Valid object")
|
|
495
531
|
|
|
496
532
|
properties = schema.get("properties", {})
|
|
497
533
|
required = set(schema.get("required", []))
|
|
@@ -502,22 +538,24 @@ def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Gene
|
|
|
502
538
|
for name in optional:
|
|
503
539
|
combo = {k: v for k, v in template.items() if k in required or k == name}
|
|
504
540
|
if combo != template:
|
|
505
|
-
yield PositiveValue(combo)
|
|
541
|
+
yield PositiveValue(combo, description=f"Object with all required properties and '{name}'")
|
|
506
542
|
# Generate one combination for each size from 2 to N-1
|
|
507
543
|
for selection in select_combinations(optional):
|
|
508
544
|
combo = {k: v for k, v in template.items() if k in required or k in selection}
|
|
509
|
-
yield PositiveValue(combo)
|
|
545
|
+
yield PositiveValue(combo, description="Object with all required and a subset of optional properties")
|
|
510
546
|
# Generate only required properties
|
|
511
547
|
if set(properties) != required:
|
|
512
548
|
only_required = {k: v for k, v in template.items() if k in required}
|
|
513
|
-
yield PositiveValue(only_required)
|
|
549
|
+
yield PositiveValue(only_required, description="Object with only required properties")
|
|
514
550
|
seen = set()
|
|
515
551
|
for name, sub_schema in properties.items():
|
|
516
552
|
seen.add(_to_hashable_key(template.get(name)))
|
|
517
553
|
for new in cover_schema_iter(ctx, sub_schema):
|
|
518
554
|
key = _to_hashable_key(new.value)
|
|
519
555
|
if key not in seen:
|
|
520
|
-
yield PositiveValue(
|
|
556
|
+
yield PositiveValue(
|
|
557
|
+
{**template, name: new.value}, description=f"Object with valid '{name}' value: {new.description}"
|
|
558
|
+
)
|
|
521
559
|
seen.add(key)
|
|
522
560
|
seen.clear()
|
|
523
561
|
|
|
@@ -528,9 +566,12 @@ def select_combinations(optional: list[str]) -> Iterator[tuple[str, ...]]:
|
|
|
528
566
|
|
|
529
567
|
|
|
530
568
|
def _negative_enum(ctx: CoverageContext, value: list) -> Generator[GeneratedValue, None, None]:
|
|
531
|
-
|
|
569
|
+
def is_not_in_value(x: Any) -> bool:
|
|
570
|
+
return x not in value
|
|
571
|
+
|
|
572
|
+
strategy = JSON_STRATEGY.filter(is_not_in_value)
|
|
532
573
|
# The exact negative value is not important here
|
|
533
|
-
yield NegativeValue(ctx.generate_from(strategy,
|
|
574
|
+
yield NegativeValue(ctx.generate_from(strategy), description="Invalid enum value")
|
|
534
575
|
|
|
535
576
|
|
|
536
577
|
def _negative_properties(
|
|
@@ -539,11 +580,17 @@ def _negative_properties(
|
|
|
539
580
|
nctx = ctx.with_negative()
|
|
540
581
|
for key, sub_schema in properties.items():
|
|
541
582
|
for value in cover_schema_iter(nctx, sub_schema):
|
|
542
|
-
yield NegativeValue(
|
|
583
|
+
yield NegativeValue(
|
|
584
|
+
{**template, key: value.value},
|
|
585
|
+
description=f"Object with invalid '{key}' value: {value.description}",
|
|
586
|
+
)
|
|
543
587
|
|
|
544
588
|
|
|
545
589
|
def _negative_pattern(ctx: CoverageContext, pattern: str) -> Generator[GeneratedValue, None, None]:
|
|
546
|
-
yield NegativeValue(
|
|
590
|
+
yield NegativeValue(
|
|
591
|
+
ctx.generate_from(st.text().filter(pattern.__ne__)),
|
|
592
|
+
description=f"Value not matching the '{pattern}' pattern",
|
|
593
|
+
)
|
|
547
594
|
|
|
548
595
|
|
|
549
596
|
def _with_negated_key(schema: dict, key: str, value: Any) -> dict:
|
|
@@ -553,19 +600,33 @@ def _with_negated_key(schema: dict, key: str, value: Any) -> dict:
|
|
|
553
600
|
def _negative_multiple_of(
|
|
554
601
|
ctx: CoverageContext, schema: dict, multiple_of: int | float
|
|
555
602
|
) -> Generator[GeneratedValue, None, None]:
|
|
556
|
-
yield NegativeValue(
|
|
603
|
+
yield NegativeValue(
|
|
604
|
+
ctx.generate_from_schema(_with_negated_key(schema, "multipleOf", multiple_of)),
|
|
605
|
+
description=f"Non-multiple of {multiple_of}",
|
|
606
|
+
)
|
|
557
607
|
|
|
558
608
|
|
|
559
609
|
def _negative_unique_items(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
|
|
560
610
|
unique = ctx.generate_from_schema({**schema, "type": "array", "minItems": 1, "maxItems": 1})
|
|
561
|
-
yield NegativeValue(unique + unique)
|
|
611
|
+
yield NegativeValue(unique + unique, description="Non-unique items")
|
|
562
612
|
|
|
563
613
|
|
|
564
614
|
def _negative_required(
|
|
565
615
|
ctx: CoverageContext, template: dict, required: list[str]
|
|
566
616
|
) -> Generator[GeneratedValue, None, None]:
|
|
567
617
|
for key in required:
|
|
568
|
-
yield NegativeValue(
|
|
618
|
+
yield NegativeValue(
|
|
619
|
+
{k: v for k, v in template.items() if k != key},
|
|
620
|
+
description=f"Missing required property: {key}",
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _is_invalid_hostname(v: Any) -> bool:
|
|
625
|
+
return v == "" or not jsonschema.Draft202012Validator.FORMAT_CHECKER.conforms(v, "hostname")
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _is_invalid_format(v: Any, format: str) -> bool:
|
|
629
|
+
return not jsonschema.Draft202012Validator.FORMAT_CHECKER.conforms(v, format)
|
|
569
630
|
|
|
570
631
|
|
|
571
632
|
def _negative_format(ctx: CoverageContext, schema: dict, format: str) -> Generator[GeneratedValue, None, None]:
|
|
@@ -574,11 +635,15 @@ def _negative_format(ctx: CoverageContext, schema: dict, format: str) -> Generat
|
|
|
574
635
|
without_format.setdefault("type", "string")
|
|
575
636
|
strategy = from_schema(without_format)
|
|
576
637
|
if format in jsonschema.Draft202012Validator.FORMAT_CHECKER.checkers:
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
yield NegativeValue(ctx.generate_from(strategy))
|
|
638
|
+
if format == "hostname":
|
|
639
|
+
strategy = strategy.filter(_is_invalid_hostname)
|
|
640
|
+
else:
|
|
641
|
+
strategy = strategy.filter(functools.partial(_is_invalid_format, format=format))
|
|
642
|
+
yield NegativeValue(ctx.generate_from(strategy), description=f"Value not matching the '{format}' format")
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _is_non_integer_float(x: float) -> bool:
|
|
646
|
+
return x != int(x)
|
|
582
647
|
|
|
583
648
|
|
|
584
649
|
def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Generator[GeneratedValue, None, None]:
|
|
@@ -600,13 +665,13 @@ def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Gene
|
|
|
600
665
|
if "number" in types:
|
|
601
666
|
del strategies["integer"]
|
|
602
667
|
if "integer" in types:
|
|
603
|
-
strategies["number"] = FLOAT_STRATEGY.filter(
|
|
668
|
+
strategies["number"] = FLOAT_STRATEGY.filter(_is_non_integer_float)
|
|
604
669
|
for strat in strategies.values():
|
|
605
|
-
value = ctx.generate_from(strat
|
|
670
|
+
value = ctx.generate_from(strat)
|
|
606
671
|
hashed = _to_hashable_key(value)
|
|
607
672
|
if hashed in seen:
|
|
608
673
|
continue
|
|
609
|
-
yield NegativeValue(value)
|
|
674
|
+
yield NegativeValue(value, description="Incorrect type")
|
|
610
675
|
seen.add(hashed)
|
|
611
676
|
|
|
612
677
|
|
schemathesis/internal/checks.py
CHANGED
|
@@ -6,6 +6,7 @@ from dataclasses import dataclass
|
|
|
6
6
|
from typing import TYPE_CHECKING, Callable, Optional
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
|
+
from requests.auth import HTTPDigestAuth
|
|
9
10
|
from requests.structures import CaseInsensitiveDict
|
|
10
11
|
|
|
11
12
|
from ..models import Case
|
|
@@ -23,7 +24,7 @@ class CheckContext:
|
|
|
23
24
|
Provides access to broader test execution data beyond individual test cases.
|
|
24
25
|
"""
|
|
25
26
|
|
|
26
|
-
auth: RawAuth | None = None
|
|
27
|
+
auth: HTTPDigestAuth | RawAuth | None = None
|
|
27
28
|
headers: CaseInsensitiveDict | None = None
|
|
28
29
|
|
|
29
30
|
|
schemathesis/models.py
CHANGED
|
@@ -154,8 +154,9 @@ class GenerationMetadata:
|
|
|
154
154
|
cookies: DataGenerationMethod | None
|
|
155
155
|
body: DataGenerationMethod | None
|
|
156
156
|
phase: TestPhase
|
|
157
|
+
description: str | None
|
|
157
158
|
|
|
158
|
-
__slots__ = ("query", "path_parameters", "headers", "cookies", "body", "phase")
|
|
159
|
+
__slots__ = ("query", "path_parameters", "headers", "cookies", "body", "phase", "description")
|
|
159
160
|
|
|
160
161
|
|
|
161
162
|
@dataclass(repr=False)
|
|
@@ -1017,6 +1018,7 @@ class Interaction:
|
|
|
1017
1018
|
status: Status
|
|
1018
1019
|
data_generation_method: DataGenerationMethod
|
|
1019
1020
|
phase: TestPhase | None
|
|
1021
|
+
description: str | None
|
|
1020
1022
|
recorded_at: str = field(default_factory=lambda: datetime.datetime.now(TIMEZONE).isoformat())
|
|
1021
1023
|
|
|
1022
1024
|
@classmethod
|
|
@@ -1046,6 +1048,7 @@ class Interaction:
|
|
|
1046
1048
|
checks=checks,
|
|
1047
1049
|
data_generation_method=cast(DataGenerationMethod, case.data_generation_method),
|
|
1048
1050
|
phase=case.meta.phase if case.meta is not None else None,
|
|
1051
|
+
description=case.meta.description if case.meta is not None else None,
|
|
1049
1052
|
)
|
|
1050
1053
|
|
|
1051
1054
|
@classmethod
|
|
@@ -1069,6 +1072,7 @@ class Interaction:
|
|
|
1069
1072
|
checks=checks,
|
|
1070
1073
|
data_generation_method=cast(DataGenerationMethod, case.data_generation_method),
|
|
1071
1074
|
phase=case.meta.phase if case.meta is not None else None,
|
|
1075
|
+
description=case.meta.description if case.meta is not None else None,
|
|
1072
1076
|
)
|
|
1073
1077
|
|
|
1074
1078
|
|
|
@@ -395,6 +395,7 @@ class SerializedInteraction:
|
|
|
395
395
|
status: Status
|
|
396
396
|
data_generation_method: DataGenerationMethod
|
|
397
397
|
phase: TestPhase | None
|
|
398
|
+
description: str | None
|
|
398
399
|
recorded_at: str
|
|
399
400
|
|
|
400
401
|
@classmethod
|
|
@@ -406,6 +407,7 @@ class SerializedInteraction:
|
|
|
406
407
|
status=interaction.status,
|
|
407
408
|
data_generation_method=interaction.data_generation_method,
|
|
408
409
|
phase=interaction.phase,
|
|
410
|
+
description=interaction.description,
|
|
409
411
|
recorded_at=interaction.recorded_at,
|
|
410
412
|
)
|
|
411
413
|
|
|
@@ -1330,6 +1330,8 @@ OPENAPI_30 = {
|
|
|
1330
1330
|
},
|
|
1331
1331
|
},
|
|
1332
1332
|
}
|
|
1333
|
+
# Generated from the updated schema.yaml from 0035208, which includes unpublished bugfixes
|
|
1334
|
+
# https://github.com/OAI/OpenAPI-Specification/blob/0035208611701b4f7f2c959eb99a8725cca41e6e/schemas/v3.1/schema.yaml
|
|
1333
1335
|
OPENAPI_31 = {
|
|
1334
1336
|
"$id": "https://spec.openapis.org/oas/3.1/schema/2022-10-07",
|
|
1335
1337
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
@@ -1345,7 +1347,7 @@ OPENAPI_31 = {
|
|
|
1345
1347
|
},
|
|
1346
1348
|
"servers": {"type": "array", "items": {"$ref": "#/$defs/server"}, "default": [{"url": "/"}]},
|
|
1347
1349
|
"paths": {"$ref": "#/$defs/paths"},
|
|
1348
|
-
"webhooks": {"type": "object", "additionalProperties": {"$ref": "#/$defs/path-item
|
|
1350
|
+
"webhooks": {"type": "object", "additionalProperties": {"$ref": "#/$defs/path-item"}},
|
|
1349
1351
|
"components": {"$ref": "#/$defs/components"},
|
|
1350
1352
|
"security": {"type": "array", "items": {"$ref": "#/$defs/security-requirement"}},
|
|
1351
1353
|
"tags": {"type": "array", "items": {"$ref": "#/$defs/tag"}},
|
|
@@ -1400,7 +1402,7 @@ OPENAPI_31 = {
|
|
|
1400
1402
|
"$comment": "https://spec.openapis.org/oas/v3.1.0#server-object",
|
|
1401
1403
|
"type": "object",
|
|
1402
1404
|
"properties": {
|
|
1403
|
-
"url": {"type": "string"
|
|
1405
|
+
"url": {"type": "string"},
|
|
1404
1406
|
"description": {"type": "string"},
|
|
1405
1407
|
"variables": {"type": "object", "additionalProperties": {"$ref": "#/$defs/server-variable"}},
|
|
1406
1408
|
},
|
|
@@ -1439,7 +1441,7 @@ OPENAPI_31 = {
|
|
|
1439
1441
|
},
|
|
1440
1442
|
"links": {"type": "object", "additionalProperties": {"$ref": "#/$defs/link-or-reference"}},
|
|
1441
1443
|
"callbacks": {"type": "object", "additionalProperties": {"$ref": "#/$defs/callbacks-or-reference"}},
|
|
1442
|
-
"pathItems": {"type": "object", "additionalProperties": {"$ref": "#/$defs/path-item
|
|
1444
|
+
"pathItems": {"type": "object", "additionalProperties": {"$ref": "#/$defs/path-item"}},
|
|
1443
1445
|
},
|
|
1444
1446
|
"patternProperties": {
|
|
1445
1447
|
"^(schemas|responses|parameters|examples|requestBodies|headers|securitySchemes|links|callbacks|pathItems)$": {
|
|
@@ -1461,6 +1463,7 @@ OPENAPI_31 = {
|
|
|
1461
1463
|
"$comment": "https://spec.openapis.org/oas/v3.1.0#path-item-object",
|
|
1462
1464
|
"type": "object",
|
|
1463
1465
|
"properties": {
|
|
1466
|
+
"$ref": {"type": "string", "format": "uri-reference"},
|
|
1464
1467
|
"summary": {"type": "string"},
|
|
1465
1468
|
"description": {"type": "string"},
|
|
1466
1469
|
"servers": {"type": "array", "items": {"$ref": "#/$defs/server"}},
|
|
@@ -1477,11 +1480,6 @@ OPENAPI_31 = {
|
|
|
1477
1480
|
"$ref": "#/$defs/specification-extensions",
|
|
1478
1481
|
"unevaluatedProperties": False,
|
|
1479
1482
|
},
|
|
1480
|
-
"path-item-or-reference": {
|
|
1481
|
-
"if": {"type": "object", "required": ["$ref"]},
|
|
1482
|
-
"then": {"$ref": "#/$defs/reference"},
|
|
1483
|
-
"else": {"$ref": "#/$defs/path-item"},
|
|
1484
|
-
},
|
|
1485
1483
|
"operation": {
|
|
1486
1484
|
"$comment": "https://spec.openapis.org/oas/v3.1.0#operation-object",
|
|
1487
1485
|
"type": "object",
|
|
@@ -1542,7 +1540,6 @@ OPENAPI_31 = {
|
|
|
1542
1540
|
"if": {"properties": {"in": {"const": "path"}}, "required": ["in"]},
|
|
1543
1541
|
"then": {
|
|
1544
1542
|
"properties": {
|
|
1545
|
-
"name": {"pattern": "[^/#?]+$"},
|
|
1546
1543
|
"style": {"default": "simple", "enum": ["matrix", "label", "simple"]},
|
|
1547
1544
|
"required": {"const": True},
|
|
1548
1545
|
},
|
|
@@ -1662,7 +1659,7 @@ OPENAPI_31 = {
|
|
|
1662
1659
|
"$comment": "https://spec.openapis.org/oas/v3.1.0#callback-object",
|
|
1663
1660
|
"type": "object",
|
|
1664
1661
|
"$ref": "#/$defs/specification-extensions",
|
|
1665
|
-
"additionalProperties": {"$ref": "#/$defs/path-item
|
|
1662
|
+
"additionalProperties": {"$ref": "#/$defs/path-item"},
|
|
1666
1663
|
},
|
|
1667
1664
|
"callbacks-or-reference": {
|
|
1668
1665
|
"if": {"type": "object", "required": ["$ref"]},
|
|
@@ -1755,7 +1752,6 @@ OPENAPI_31 = {
|
|
|
1755
1752
|
"summary": {"type": "string"},
|
|
1756
1753
|
"description": {"type": "string"},
|
|
1757
1754
|
},
|
|
1758
|
-
"unevaluatedProperties": False,
|
|
1759
1755
|
},
|
|
1760
1756
|
"schema": {
|
|
1761
1757
|
"$comment": "https://spec.openapis.org/oas/v3.1.0#schema-object",
|
schemathesis/stateful/runner.py
CHANGED
|
@@ -12,6 +12,7 @@ from hypothesis.control import current_build_context
|
|
|
12
12
|
from hypothesis.errors import Flaky, Unsatisfiable
|
|
13
13
|
|
|
14
14
|
from ..exceptions import CheckFailed
|
|
15
|
+
from ..internal.checks import CheckContext
|
|
15
16
|
from ..targets import TargetMetricCollector
|
|
16
17
|
from . import events
|
|
17
18
|
from .config import StatefulTestRunnerConfig
|
|
@@ -113,6 +114,7 @@ def _execute_state_machine_loop(
|
|
|
113
114
|
) -> None:
|
|
114
115
|
"""Execute the state machine testing loop."""
|
|
115
116
|
from hypothesis import reporting
|
|
117
|
+
from requests.structures import CaseInsensitiveDict
|
|
116
118
|
|
|
117
119
|
from ..transports import RequestsTransport
|
|
118
120
|
|
|
@@ -129,6 +131,7 @@ def _execute_state_machine_loop(
|
|
|
129
131
|
if config.auth is not None:
|
|
130
132
|
session.auth = config.auth
|
|
131
133
|
call_kwargs["session"] = session
|
|
134
|
+
check_ctx = CheckContext(auth=config.auth, headers=CaseInsensitiveDict(config.headers) if config.headers else None)
|
|
132
135
|
|
|
133
136
|
class _InstrumentedStateMachine(state_machine): # type: ignore[valid-type,misc]
|
|
134
137
|
"""State machine with additional hooks for emitting events."""
|
|
@@ -223,7 +226,8 @@ def _execute_state_machine_loop(
|
|
|
223
226
|
validate_response(
|
|
224
227
|
response=response,
|
|
225
228
|
case=case,
|
|
226
|
-
|
|
229
|
+
runner_ctx=ctx,
|
|
230
|
+
check_ctx=check_ctx,
|
|
227
231
|
checks=config.checks,
|
|
228
232
|
additional_checks=additional_checks,
|
|
229
233
|
max_response_time=config.max_response_time,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import TYPE_CHECKING
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from ..exceptions import CheckFailed, get_grouped_exception
|
|
6
6
|
from ..internal.checks import CheckContext
|
|
@@ -17,27 +17,24 @@ def validate_response(
|
|
|
17
17
|
*,
|
|
18
18
|
response: GenericResponse,
|
|
19
19
|
case: Case,
|
|
20
|
-
|
|
20
|
+
runner_ctx: RunnerContext,
|
|
21
|
+
check_ctx: CheckContext,
|
|
21
22
|
checks: tuple[CheckFunction, ...],
|
|
22
23
|
additional_checks: tuple[CheckFunction, ...] = (),
|
|
23
24
|
max_response_time: int | None = None,
|
|
24
|
-
headers: dict[str, Any] | None = None,
|
|
25
25
|
) -> None:
|
|
26
26
|
"""Validate the response against the provided checks."""
|
|
27
|
-
from requests.structures import CaseInsensitiveDict
|
|
28
|
-
|
|
29
27
|
from .._compat import MultipleFailures
|
|
30
28
|
from ..checks import _make_max_response_time_failure_message
|
|
31
29
|
from ..failures import ResponseTimeExceeded
|
|
32
30
|
from ..models import Check, Status
|
|
33
31
|
|
|
34
32
|
exceptions: list[CheckFailed | AssertionError] = []
|
|
35
|
-
check_results =
|
|
36
|
-
check_ctx = CheckContext(headers=CaseInsensitiveDict(headers) if headers else None)
|
|
33
|
+
check_results = runner_ctx.checks_for_step
|
|
37
34
|
|
|
38
35
|
def _on_failure(exc: CheckFailed | AssertionError, message: str, context: FailureContext | None) -> None:
|
|
39
36
|
exceptions.append(exc)
|
|
40
|
-
if
|
|
37
|
+
if runner_ctx.is_seen_in_suite(exc):
|
|
41
38
|
return
|
|
42
39
|
failed_check = Check(
|
|
43
40
|
name=name,
|
|
@@ -49,9 +46,9 @@ def validate_response(
|
|
|
49
46
|
context=context,
|
|
50
47
|
request=None,
|
|
51
48
|
)
|
|
52
|
-
|
|
49
|
+
runner_ctx.add_failed_check(failed_check)
|
|
53
50
|
check_results.append(failed_check)
|
|
54
|
-
|
|
51
|
+
runner_ctx.mark_as_seen_in_suite(exc)
|
|
55
52
|
|
|
56
53
|
def _on_passed(_name: str, _case: Case) -> None:
|
|
57
54
|
passed_check = Check(
|
|
@@ -72,16 +69,16 @@ def validate_response(
|
|
|
72
69
|
if not skip_check:
|
|
73
70
|
_on_passed(name, copied_case)
|
|
74
71
|
except CheckFailed as exc:
|
|
75
|
-
if
|
|
72
|
+
if runner_ctx.is_seen_in_run(exc):
|
|
76
73
|
continue
|
|
77
74
|
_on_failure(exc, str(exc), exc.context)
|
|
78
75
|
except AssertionError as exc:
|
|
79
|
-
if
|
|
76
|
+
if runner_ctx.is_seen_in_run(exc):
|
|
80
77
|
continue
|
|
81
78
|
_on_failure(exc, str(exc) or f"Custom check failed: `{name}`", None)
|
|
82
79
|
except MultipleFailures as exc:
|
|
83
80
|
for subexc in exc.exceptions:
|
|
84
|
-
if
|
|
81
|
+
if runner_ctx.is_seen_in_run(subexc):
|
|
85
82
|
continue
|
|
86
83
|
_on_failure(subexc, str(subexc), subexc.context)
|
|
87
84
|
|
|
@@ -93,7 +90,7 @@ def validate_response(
|
|
|
93
90
|
try:
|
|
94
91
|
raise AssertionError(message)
|
|
95
92
|
except AssertionError as _exc:
|
|
96
|
-
if not
|
|
93
|
+
if not runner_ctx.is_seen_in_run(_exc):
|
|
97
94
|
_on_failure(_exc, message, context)
|
|
98
95
|
else:
|
|
99
96
|
_on_passed("max_response_time", case)
|
|
@@ -7,7 +7,7 @@ from contextlib import contextmanager
|
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from datetime import timedelta
|
|
9
9
|
from inspect import iscoroutinefunction
|
|
10
|
-
from typing import TYPE_CHECKING, Any, Generator,
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Generator, Protocol, TypeVar, cast
|
|
11
11
|
from urllib.parse import urlparse
|
|
12
12
|
|
|
13
13
|
from .. import failures
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: schemathesis
|
|
3
|
-
Version: 3.36.
|
|
3
|
+
Version: 3.36.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://schemathesis.readthedocs.io/en/stable/changelog.html
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
schemathesis/__init__.py,sha256=UW2Bq8hDDkcBeAAA7PzpBFXkOOxkmHox-mfQwzHDjL0,1914
|
|
2
2
|
schemathesis/_compat.py,sha256=y4RZd59i2NCnZ91VQhnKeMn_8t3SgvLOk2Xm8nymUHY,1837
|
|
3
3
|
schemathesis/_dependency_versions.py,sha256=pjEkkGAfOQJYNb-9UOo84V8nj_lKHr_TGDVdFwY2UU0,816
|
|
4
|
-
schemathesis/_hypothesis.py,sha256=
|
|
4
|
+
schemathesis/_hypothesis.py,sha256=TUGODyKJzTSnUQPWt_uj6-MBXCPthquJXGiCobVdFh0,14930
|
|
5
5
|
schemathesis/_lazy_import.py,sha256=aMhWYgbU2JOltyWBb32vnWBb6kykOghucEzI_F70yVE,470
|
|
6
6
|
schemathesis/_override.py,sha256=TAjYB3eJQmlw9K_xiR9ptt9Wj7if4U7UFlUhGjpBAoM,1625
|
|
7
7
|
schemathesis/_rate_limiter.py,sha256=q_XWst5hzuAyXQRiZc4s_bx7-JlPYZM_yKDmeavt3oo,242
|
|
@@ -17,7 +17,7 @@ schemathesis/graphql.py,sha256=XiuKcfoOB92iLFC8zpz2msLkM0_V0TLdxPNBqrrGZ8w,216
|
|
|
17
17
|
schemathesis/hooks.py,sha256=qXyVRfJdhsLk1GuJX47VAqkX0VPm6X6fK-cXhEnFLT4,14765
|
|
18
18
|
schemathesis/lazy.py,sha256=uE8ef_7U_9ovs0-7UA7ssIiiDipJurJFHuxaUFOUETo,18956
|
|
19
19
|
schemathesis/loaders.py,sha256=MoEhcdOEBJxNRn5X-ZNhWB9jZDHQQNpkNfEdQjf_NDw,4590
|
|
20
|
-
schemathesis/models.py,sha256=
|
|
20
|
+
schemathesis/models.py,sha256=mgufFwK6pVWn6hjfJIJwbk0qZM-XUeUaVVaOOEPaR50,46646
|
|
21
21
|
schemathesis/parameters.py,sha256=PndmqQRlEYsCt1kWjSShPsFf6vj7X_7FRdz_-A95eNg,2258
|
|
22
22
|
schemathesis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
23
|
schemathesis/sanitization.py,sha256=Lycn1VVfula9B6XpzkxTHja7CZ7RHqbUh9kBic0Yi4M,9056
|
|
@@ -30,7 +30,7 @@ schemathesis/utils.py,sha256=8RkTZ9Ft5IUaGkxABhh34oU7WO2ouMsfgtvFPTx9alI,4875
|
|
|
30
30
|
schemathesis/cli/__init__.py,sha256=OC6QO38QDf55DTIVwrWiQKz8BfTD5QcK574m67NCE2w,72862
|
|
31
31
|
schemathesis/cli/__main__.py,sha256=MWaenjaUTZIfNPFzKmnkTiawUri7DVldtg3mirLwzU8,92
|
|
32
32
|
schemathesis/cli/callbacks.py,sha256=grMKlx_iGiJA4oJsYt_q8l354Y8Nb11IBvOKwbD0jOA,15192
|
|
33
|
-
schemathesis/cli/cassettes.py,sha256=
|
|
33
|
+
schemathesis/cli/cassettes.py,sha256=jD1JTkkEALUUEyzyuJ-KuxgntfGodILUuOu3C9HKjIw,19412
|
|
34
34
|
schemathesis/cli/constants.py,sha256=wk-0GsoJIel8wFFerQ6Kf_6eAYUtIWkwMFwyAqv3yj4,1635
|
|
35
35
|
schemathesis/cli/context.py,sha256=j_lvYQiPa6Q7P4P_IGCM9V2y2gJSpDbpxIIzR5oFB2I,2567
|
|
36
36
|
schemathesis/cli/debug.py,sha256=_YA-bX1ujHl4bqQDEum7M-I2XHBTEGbvgkhvcvKhmgU,658
|
|
@@ -58,11 +58,11 @@ schemathesis/fixups/__init__.py,sha256=RP5QYJVJhp8LXjhH89fCRaIVU26dHCy74jD9seoYM
|
|
|
58
58
|
schemathesis/fixups/fast_api.py,sha256=mn-KzBqnR8jl4W5fY-_ZySabMDMUnpzCIESMHnlvE1c,1304
|
|
59
59
|
schemathesis/fixups/utf8_bom.py,sha256=lWT9RNmJG8i-l5AXIpaCT3qCPUwRgzXPW3eoOjmZETA,745
|
|
60
60
|
schemathesis/generation/__init__.py,sha256=29Zys_tD6kfngaC4zHeC6TOBZQcmo7CWm7KDSYsHStQ,1581
|
|
61
|
-
schemathesis/generation/_hypothesis.py,sha256=
|
|
61
|
+
schemathesis/generation/_hypothesis.py,sha256=Aaol5w3TEOVZn8znrnnJCYfrll8eALW0dCRtz3k0Eis,1661
|
|
62
62
|
schemathesis/generation/_methods.py,sha256=jCK09f4sedDfePrS-6BIiE-CcEE8fJ4ZHxq1BHoTltQ,1101
|
|
63
|
-
schemathesis/generation/coverage.py,sha256=
|
|
63
|
+
schemathesis/generation/coverage.py,sha256=rq7em0ifTQMZxprBE4NIJWFiV6PquKFcnMMSuR2-ohI,28268
|
|
64
64
|
schemathesis/internal/__init__.py,sha256=93HcdG3LF0BbQKbCteOsFMa1w6nXl8yTmx87QLNJOik,161
|
|
65
|
-
schemathesis/internal/checks.py,sha256=
|
|
65
|
+
schemathesis/internal/checks.py,sha256=qxJ5Ndeiveh_BT0QZq0Vbv72-ZTpbevOY48XKGQC9Ec,1730
|
|
66
66
|
schemathesis/internal/copy.py,sha256=DcL56z-d69kKR_5u8mlHvjSL1UTyUKNMAwexrwHFY1s,1031
|
|
67
67
|
schemathesis/internal/datetime.py,sha256=zPLBL0XXLNfP-KYel3H2m8pnsxjsA_4d-zTOhJg2EPQ,136
|
|
68
68
|
schemathesis/internal/deprecation.py,sha256=Ty5VBFBlufkITpP0WWTPIPbnB7biDi0kQgXVYWZp820,1273
|
|
@@ -75,7 +75,7 @@ schemathesis/internal/validation.py,sha256=G7i8jIMUpAeOnDsDF_eWYvRZe_yMprRswx0QA
|
|
|
75
75
|
schemathesis/runner/__init__.py,sha256=dLvb4FvH1zvYyVj5i7naR0ehfKL7K8QBKKbBNp_ClY8,21536
|
|
76
76
|
schemathesis/runner/events.py,sha256=cRKKSDvHvKLBIyFBz-J0JtAKshbGGKco9eaMyLCgzsY,11734
|
|
77
77
|
schemathesis/runner/probes.py,sha256=no5AfO3kse25qvHevjeUfB0Q3C860V2AYzschUW3QMQ,5688
|
|
78
|
-
schemathesis/runner/serialization.py,sha256=
|
|
78
|
+
schemathesis/runner/serialization.py,sha256=jHpfm1PgPAmorNkF8_rkzIYoeA43jpbSKeh5Hm5nqF0,20495
|
|
79
79
|
schemathesis/runner/impl/__init__.py,sha256=1E2iME8uthYPBh9MjwVBCTFV-P3fi7AdphCCoBBspjs,199
|
|
80
80
|
schemathesis/runner/impl/context.py,sha256=KT3Dl1HIUM29Jpp_DwfoSx_NbWFH_7s6gw-p2Sr-N24,2505
|
|
81
81
|
schemathesis/runner/impl/core.py,sha256=bAPwfhLJrXLlPN6BDNGyQoO9v35YlmeuxAmyFTNQg5Y,47197
|
|
@@ -104,11 +104,11 @@ schemathesis/specs/graphql/schemas.py,sha256=b7QwglKbcYQCMjuYmqDsVoFu2o4xaA_kduU
|
|
|
104
104
|
schemathesis/specs/graphql/validation.py,sha256=uINIOt-2E7ZuQV2CxKzwez-7L9tDtqzMSpnVoRWvxy0,1635
|
|
105
105
|
schemathesis/specs/openapi/__init__.py,sha256=HDcx3bqpa6qWPpyMrxAbM3uTo0Lqpg-BUNZhDJSJKnw,279
|
|
106
106
|
schemathesis/specs/openapi/_cache.py,sha256=PAiAu4X_a2PQgD2lG5H3iisXdyg4SaHpU46bRZvfNkM,4320
|
|
107
|
-
schemathesis/specs/openapi/_hypothesis.py,sha256=
|
|
107
|
+
schemathesis/specs/openapi/_hypothesis.py,sha256=Ym1d3GXlabOSbDk_AEkmkZGl9EMIDpumciLZtfYNdUI,24323
|
|
108
108
|
schemathesis/specs/openapi/checks.py,sha256=-4qOzkova0e4QSqdgsoUiOv2bg57HZmzbpAiAeotc3Q,22288
|
|
109
109
|
schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnKDwCwPLhID4,783
|
|
110
110
|
schemathesis/specs/openapi/converter.py,sha256=NkrzBNjtmVwQTeE73NOtwB_puvQTjxxqqrc7gD_yscc,3241
|
|
111
|
-
schemathesis/specs/openapi/definitions.py,sha256=
|
|
111
|
+
schemathesis/specs/openapi/definitions.py,sha256=WTkWwCgTc3OMxfKsqh6YDoGfZMTThSYrHGp8h0vLAK0,93935
|
|
112
112
|
schemathesis/specs/openapi/examples.py,sha256=FwhPWca7bpdHpUp_LRoK09DVgusojO3aXXhXYrK373I,20354
|
|
113
113
|
schemathesis/specs/openapi/formats.py,sha256=JmmkQWNAj5XreXb7Edgj4LADAf4m86YulR_Ec8evpJ4,1220
|
|
114
114
|
schemathesis/specs/openapi/links.py,sha256=a8JmWM9aZhrR5CfyIh6t2SkfonMLfYKOScXY2XlZYN0,17749
|
|
@@ -140,19 +140,19 @@ schemathesis/stateful/__init__.py,sha256=HBg-h131EI8IipHQgufSaXe-CrFTKmffPVsoEFj
|
|
|
140
140
|
schemathesis/stateful/config.py,sha256=huYzqDoD6x20p_VNAR79NgxPwUFO8UXoc3_z4BEuHqU,3586
|
|
141
141
|
schemathesis/stateful/context.py,sha256=vJ9nxTTjI5wo7A6PBGCvVVO_7y-ELs3XERi9PxLzykA,5085
|
|
142
142
|
schemathesis/stateful/events.py,sha256=CyYvyQebOaeTn6UevaB7HXOrUhxCWbqXMfQ7pZK7fV8,6727
|
|
143
|
-
schemathesis/stateful/runner.py,sha256=
|
|
143
|
+
schemathesis/stateful/runner.py,sha256=e3vvRrx0NwN20RdC0cC4mZjcsYezwkhoDwE1cCVOAlw,12615
|
|
144
144
|
schemathesis/stateful/sink.py,sha256=bHYlgh-fMwg1Srxk_XGs0-WV34YccotwH9PGrxCK57A,2474
|
|
145
145
|
schemathesis/stateful/state_machine.py,sha256=PFztY82W5enuXjO6k4Mz8fbHmDJ7Z8OLYZRWtuBeyjg,12956
|
|
146
146
|
schemathesis/stateful/statistic.py,sha256=2-uU5xpT9CbMulKgJWLZN6MUpC0Fskf5yXTt4ef4NFA,542
|
|
147
|
-
schemathesis/stateful/validation.py,sha256=
|
|
148
|
-
schemathesis/transports/__init__.py,sha256=
|
|
147
|
+
schemathesis/stateful/validation.py,sha256=23qSZjC1_xRmtCX4OqsyG6pGxdlo6IZYid695ZpDQyU,3747
|
|
148
|
+
schemathesis/transports/__init__.py,sha256=ybH90TrGwODO5s94UMEX2P2HX-6Jb66X5UUOgKTbZz8,12882
|
|
149
149
|
schemathesis/transports/asgi.py,sha256=bwW9vMd1h89Jh7I4jHJVwSNUQzHvc7-JOD5u4hSHZd8,212
|
|
150
150
|
schemathesis/transports/auth.py,sha256=urSTO9zgFO1qU69xvnKHPFQV0SlJL3d7_Ojl0tLnZwo,1143
|
|
151
151
|
schemathesis/transports/content_types.py,sha256=MiKOm-Hy5i75hrROPdpiBZPOTDzOwlCdnthJD12AJzI,2187
|
|
152
152
|
schemathesis/transports/headers.py,sha256=hr_AIDOfUxsJxpHfemIZ_uNG3_vzS_ZeMEKmZjbYiBE,990
|
|
153
153
|
schemathesis/transports/responses.py,sha256=OFD4ZLqwEFpo7F9vaP_SVgjhxAqatxIj38FS4XVq8Qs,1680
|
|
154
|
-
schemathesis-3.36.
|
|
155
|
-
schemathesis-3.36.
|
|
156
|
-
schemathesis-3.36.
|
|
157
|
-
schemathesis-3.36.
|
|
158
|
-
schemathesis-3.36.
|
|
154
|
+
schemathesis-3.36.4.dist-info/METADATA,sha256=_FwLfobUt1E2LW6L-xhL-bMbe3VGSOgoykQT8DhOan0,12904
|
|
155
|
+
schemathesis-3.36.4.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
156
|
+
schemathesis-3.36.4.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
|
|
157
|
+
schemathesis-3.36.4.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
|
|
158
|
+
schemathesis-3.36.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|