schemathesis 3.35.4__py3-none-any.whl → 3.36.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +5 -5
- schemathesis/_hypothesis.py +12 -6
- schemathesis/_override.py +4 -4
- schemathesis/auths.py +1 -1
- schemathesis/checks.py +8 -5
- schemathesis/cli/__init__.py +23 -26
- schemathesis/cli/callbacks.py +6 -4
- schemathesis/cli/cassettes.py +67 -41
- schemathesis/cli/context.py +7 -6
- schemathesis/cli/junitxml.py +1 -1
- schemathesis/cli/options.py +7 -4
- schemathesis/cli/output/default.py +5 -5
- schemathesis/cli/reporting.py +4 -2
- schemathesis/code_samples.py +4 -3
- schemathesis/contrib/unique_data.py +1 -2
- schemathesis/exceptions.py +4 -3
- schemathesis/extra/_flask.py +4 -1
- schemathesis/extra/pytest_plugin.py +6 -3
- schemathesis/failures.py +2 -1
- schemathesis/filters.py +2 -2
- schemathesis/generation/__init__.py +2 -2
- schemathesis/generation/_hypothesis.py +1 -1
- schemathesis/generation/coverage.py +53 -12
- schemathesis/graphql.py +0 -1
- schemathesis/hooks.py +3 -3
- schemathesis/internal/checks.py +53 -0
- schemathesis/lazy.py +10 -7
- schemathesis/loaders.py +3 -3
- schemathesis/models.py +59 -23
- schemathesis/runner/__init__.py +12 -6
- schemathesis/runner/events.py +1 -1
- schemathesis/runner/impl/context.py +72 -0
- schemathesis/runner/impl/core.py +105 -67
- schemathesis/runner/impl/solo.py +17 -20
- schemathesis/runner/impl/threadpool.py +65 -72
- schemathesis/runner/serialization.py +4 -3
- schemathesis/sanitization.py +2 -1
- schemathesis/schemas.py +20 -22
- schemathesis/serializers.py +2 -0
- schemathesis/service/client.py +1 -1
- schemathesis/service/events.py +4 -1
- schemathesis/service/extensions.py +2 -2
- schemathesis/service/hosts.py +4 -2
- schemathesis/service/models.py +3 -3
- schemathesis/service/report.py +3 -3
- schemathesis/service/serialization.py +4 -2
- schemathesis/specs/graphql/loaders.py +5 -4
- schemathesis/specs/graphql/schemas.py +13 -8
- schemathesis/specs/openapi/checks.py +76 -27
- schemathesis/specs/openapi/definitions.py +1 -5
- schemathesis/specs/openapi/examples.py +92 -2
- schemathesis/specs/openapi/expressions/__init__.py +7 -0
- schemathesis/specs/openapi/expressions/extractors.py +4 -1
- schemathesis/specs/openapi/expressions/nodes.py +5 -3
- schemathesis/specs/openapi/links.py +4 -4
- schemathesis/specs/openapi/loaders.py +6 -5
- schemathesis/specs/openapi/negative/__init__.py +5 -3
- schemathesis/specs/openapi/negative/mutations.py +5 -4
- schemathesis/specs/openapi/parameters.py +4 -2
- schemathesis/specs/openapi/schemas.py +28 -13
- schemathesis/specs/openapi/security.py +6 -4
- schemathesis/specs/openapi/stateful/__init__.py +2 -2
- schemathesis/specs/openapi/stateful/statistic.py +3 -3
- schemathesis/specs/openapi/stateful/types.py +3 -2
- schemathesis/stateful/__init__.py +3 -3
- schemathesis/stateful/config.py +2 -1
- schemathesis/stateful/context.py +13 -3
- schemathesis/stateful/events.py +3 -3
- schemathesis/stateful/runner.py +24 -6
- schemathesis/stateful/sink.py +1 -1
- schemathesis/stateful/state_machine.py +7 -6
- schemathesis/stateful/statistic.py +3 -1
- schemathesis/stateful/validation.py +10 -5
- schemathesis/transports/__init__.py +2 -2
- schemathesis/transports/asgi.py +7 -0
- schemathesis/transports/auth.py +2 -1
- schemathesis/transports/content_types.py +1 -1
- schemathesis/transports/responses.py +2 -1
- schemathesis/utils.py +4 -2
- {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/METADATA +1 -1
- schemathesis-3.36.0.dist-info/RECORD +157 -0
- schemathesis-3.35.4.dist-info/RECORD +0 -154
- {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/WHEEL +0 -0
- {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -7,26 +7,31 @@ import time
|
|
|
7
7
|
import warnings
|
|
8
8
|
from dataclasses import dataclass
|
|
9
9
|
from queue import Queue
|
|
10
|
-
from typing import Any, Callable, Generator, Iterable, cast
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, cast
|
|
11
11
|
|
|
12
|
-
import hypothesis
|
|
13
12
|
from hypothesis.errors import HypothesisWarning
|
|
14
13
|
|
|
15
14
|
from ..._hypothesis import create_test
|
|
16
|
-
from ...generation import DataGenerationMethod, GenerationConfig
|
|
17
15
|
from ...internal.result import Ok
|
|
18
|
-
from ...models import CheckFunction, TestResultSet
|
|
19
16
|
from ...stateful import Feedback, Stateful
|
|
20
|
-
from ...targets import Target
|
|
21
17
|
from ...transports.auth import get_requests_auth
|
|
22
|
-
from ...types import RawAuth
|
|
23
18
|
from ...utils import capture_hypothesis_output
|
|
24
19
|
from .. import events
|
|
25
20
|
from .core import BaseRunner, asgi_test, get_session, handle_schema_error, network_test, run_test, wsgi_test
|
|
26
21
|
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
import hypothesis
|
|
24
|
+
|
|
25
|
+
from ...generation import DataGenerationMethod, GenerationConfig
|
|
26
|
+
from ...internal.checks import CheckFunction
|
|
27
|
+
from ...targets import Target
|
|
28
|
+
from ...types import RawAuth
|
|
29
|
+
from .context import RunnerContext
|
|
30
|
+
|
|
27
31
|
|
|
28
32
|
def _run_task(
|
|
29
|
-
|
|
33
|
+
*,
|
|
34
|
+
test_func: Callable,
|
|
30
35
|
tasks_queue: Queue,
|
|
31
36
|
events_queue: Queue,
|
|
32
37
|
generator_done: threading.Event,
|
|
@@ -35,8 +40,7 @@ def _run_task(
|
|
|
35
40
|
data_generation_methods: Iterable[DataGenerationMethod],
|
|
36
41
|
settings: hypothesis.settings,
|
|
37
42
|
generation_config: GenerationConfig,
|
|
38
|
-
|
|
39
|
-
results: TestResultSet,
|
|
43
|
+
ctx: RunnerContext,
|
|
40
44
|
stateful: Stateful | None,
|
|
41
45
|
stateful_recursion_limit: int,
|
|
42
46
|
headers: dict[str, Any] | None = None,
|
|
@@ -51,10 +55,10 @@ def _run_task(
|
|
|
51
55
|
if recursion_level > stateful_recursion_limit:
|
|
52
56
|
return
|
|
53
57
|
for _result in maker(
|
|
54
|
-
|
|
58
|
+
test_func,
|
|
55
59
|
settings=settings,
|
|
56
60
|
generation_config=generation_config,
|
|
57
|
-
seed=seed,
|
|
61
|
+
seed=ctx.seed,
|
|
58
62
|
as_strategy_kwargs=as_strategy_kwargs,
|
|
59
63
|
):
|
|
60
64
|
# `result` is always `Ok` here
|
|
@@ -66,7 +70,7 @@ def _run_task(
|
|
|
66
70
|
checks,
|
|
67
71
|
data_generation_methods,
|
|
68
72
|
targets,
|
|
69
|
-
|
|
73
|
+
ctx=ctx,
|
|
70
74
|
recursion_level=recursion_level,
|
|
71
75
|
feedback=feedback,
|
|
72
76
|
headers=headers,
|
|
@@ -89,9 +93,9 @@ def _run_task(
|
|
|
89
93
|
operation = result.ok()
|
|
90
94
|
test_function = create_test(
|
|
91
95
|
operation=operation,
|
|
92
|
-
test=
|
|
96
|
+
test=test_func,
|
|
93
97
|
settings=settings,
|
|
94
|
-
seed=seed,
|
|
98
|
+
seed=ctx.seed,
|
|
95
99
|
data_generation_methods=list(data_generation_methods),
|
|
96
100
|
generation_config=generation_config,
|
|
97
101
|
as_strategy_kwargs=as_strategy_kwargs,
|
|
@@ -101,7 +105,7 @@ def _run_task(
|
|
|
101
105
|
# `feedback.get_stateful_tests`
|
|
102
106
|
_run_tests(lambda *_, **__: (items,)) # noqa: B023
|
|
103
107
|
else:
|
|
104
|
-
for event in handle_schema_error(result.err(),
|
|
108
|
+
for event in handle_schema_error(result.err(), ctx, data_generation_methods, 0):
|
|
105
109
|
events_queue.put(event)
|
|
106
110
|
|
|
107
111
|
|
|
@@ -117,8 +121,7 @@ def thread_task(
|
|
|
117
121
|
auth: RawAuth | None,
|
|
118
122
|
auth_type: str | None,
|
|
119
123
|
headers: dict[str, Any] | None,
|
|
120
|
-
|
|
121
|
-
results: TestResultSet,
|
|
124
|
+
ctx: RunnerContext,
|
|
122
125
|
stateful: Stateful | None,
|
|
123
126
|
stateful_recursion_limit: int,
|
|
124
127
|
kwargs: Any,
|
|
@@ -130,17 +133,16 @@ def thread_task(
|
|
|
130
133
|
prepared_auth = get_requests_auth(auth, auth_type)
|
|
131
134
|
with get_session(prepared_auth) as session:
|
|
132
135
|
_run_task(
|
|
133
|
-
network_test,
|
|
134
|
-
tasks_queue,
|
|
135
|
-
events_queue,
|
|
136
|
-
generator_done,
|
|
137
|
-
checks,
|
|
138
|
-
targets,
|
|
139
|
-
data_generation_methods,
|
|
140
|
-
settings,
|
|
141
|
-
generation_config,
|
|
142
|
-
|
|
143
|
-
results,
|
|
136
|
+
test_func=network_test,
|
|
137
|
+
tasks_queue=tasks_queue,
|
|
138
|
+
events_queue=events_queue,
|
|
139
|
+
generator_done=generator_done,
|
|
140
|
+
checks=checks,
|
|
141
|
+
targets=targets,
|
|
142
|
+
data_generation_methods=data_generation_methods,
|
|
143
|
+
settings=settings,
|
|
144
|
+
generation_config=generation_config,
|
|
145
|
+
ctx=ctx,
|
|
144
146
|
stateful=stateful,
|
|
145
147
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
146
148
|
session=session,
|
|
@@ -158,24 +160,22 @@ def wsgi_thread_task(
|
|
|
158
160
|
data_generation_methods: Iterable[DataGenerationMethod],
|
|
159
161
|
settings: hypothesis.settings,
|
|
160
162
|
generation_config: GenerationConfig,
|
|
161
|
-
|
|
162
|
-
results: TestResultSet,
|
|
163
|
+
ctx: RunnerContext,
|
|
163
164
|
stateful: Stateful | None,
|
|
164
165
|
stateful_recursion_limit: int,
|
|
165
166
|
kwargs: Any,
|
|
166
167
|
) -> None:
|
|
167
168
|
_run_task(
|
|
168
|
-
wsgi_test,
|
|
169
|
-
tasks_queue,
|
|
170
|
-
events_queue,
|
|
171
|
-
generator_done,
|
|
172
|
-
checks,
|
|
173
|
-
targets,
|
|
174
|
-
data_generation_methods,
|
|
175
|
-
settings,
|
|
176
|
-
generation_config,
|
|
177
|
-
|
|
178
|
-
results,
|
|
169
|
+
test_func=wsgi_test,
|
|
170
|
+
tasks_queue=tasks_queue,
|
|
171
|
+
events_queue=events_queue,
|
|
172
|
+
generator_done=generator_done,
|
|
173
|
+
checks=checks,
|
|
174
|
+
targets=targets,
|
|
175
|
+
data_generation_methods=data_generation_methods,
|
|
176
|
+
settings=settings,
|
|
177
|
+
generation_config=generation_config,
|
|
178
|
+
ctx=ctx,
|
|
179
179
|
stateful=stateful,
|
|
180
180
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
181
181
|
**kwargs,
|
|
@@ -192,24 +192,22 @@ def asgi_thread_task(
|
|
|
192
192
|
settings: hypothesis.settings,
|
|
193
193
|
generation_config: GenerationConfig,
|
|
194
194
|
headers: dict[str, Any] | None,
|
|
195
|
-
|
|
196
|
-
results: TestResultSet,
|
|
195
|
+
ctx: RunnerContext,
|
|
197
196
|
stateful: Stateful | None,
|
|
198
197
|
stateful_recursion_limit: int,
|
|
199
198
|
kwargs: Any,
|
|
200
199
|
) -> None:
|
|
201
200
|
_run_task(
|
|
202
|
-
asgi_test,
|
|
203
|
-
tasks_queue,
|
|
204
|
-
events_queue,
|
|
205
|
-
generator_done,
|
|
206
|
-
checks,
|
|
207
|
-
targets,
|
|
208
|
-
data_generation_methods,
|
|
209
|
-
settings,
|
|
210
|
-
generation_config,
|
|
211
|
-
|
|
212
|
-
results,
|
|
201
|
+
test_func=asgi_test,
|
|
202
|
+
tasks_queue=tasks_queue,
|
|
203
|
+
events_queue=events_queue,
|
|
204
|
+
generator_done=generator_done,
|
|
205
|
+
checks=checks,
|
|
206
|
+
targets=targets,
|
|
207
|
+
data_generation_methods=data_generation_methods,
|
|
208
|
+
settings=settings,
|
|
209
|
+
generation_config=generation_config,
|
|
210
|
+
ctx=ctx,
|
|
213
211
|
stateful=stateful,
|
|
214
212
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
215
213
|
headers=headers,
|
|
@@ -228,9 +226,7 @@ class ThreadPoolRunner(BaseRunner):
|
|
|
228
226
|
|
|
229
227
|
workers_num: int = 2
|
|
230
228
|
|
|
231
|
-
def _execute(
|
|
232
|
-
self, results: TestResultSet, stop_event: threading.Event
|
|
233
|
-
) -> Generator[events.ExecutionEvent, None, None]:
|
|
229
|
+
def _execute(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
|
|
234
230
|
"""All events come from a queue where different workers push their events."""
|
|
235
231
|
# Instead of generating all tests at once, we do it when there is a free worker to pick it up
|
|
236
232
|
# This is extremely important for memory consumption when testing large schemas
|
|
@@ -238,7 +234,7 @@ class ThreadPoolRunner(BaseRunner):
|
|
|
238
234
|
# It would be better to have a separate producer thread and communicate via threading events.
|
|
239
235
|
# Though it is a bit more complex, so the current solution is suboptimal in terms of resources utilization,
|
|
240
236
|
# but good enough and easy enough to implement.
|
|
241
|
-
tasks_generator = iter(self.schema.get_all_operations())
|
|
237
|
+
tasks_generator = iter(self.schema.get_all_operations(generation_config=self.generation_config))
|
|
242
238
|
generator_done = threading.Event()
|
|
243
239
|
tasks_queue: Queue = Queue()
|
|
244
240
|
# Add at least `workers_num` tasks first, so all workers are busy
|
|
@@ -251,7 +247,7 @@ class ThreadPoolRunner(BaseRunner):
|
|
|
251
247
|
break
|
|
252
248
|
# Events are pushed by workers via a separate queue
|
|
253
249
|
events_queue: Queue = Queue()
|
|
254
|
-
workers = self._init_workers(tasks_queue, events_queue,
|
|
250
|
+
workers = self._init_workers(tasks_queue, events_queue, ctx, generator_done)
|
|
255
251
|
|
|
256
252
|
def stop_workers() -> None:
|
|
257
253
|
for worker in workers:
|
|
@@ -270,12 +266,12 @@ class ThreadPoolRunner(BaseRunner):
|
|
|
270
266
|
is_finished = all(not worker.is_alive() for worker in workers)
|
|
271
267
|
while not events_queue.empty():
|
|
272
268
|
event = events_queue.get()
|
|
273
|
-
if
|
|
269
|
+
if ctx.is_stopped or isinstance(event, events.Interrupted) or self._should_stop(event):
|
|
274
270
|
# We could still have events in the queue, but ignore them to keep the logic simple
|
|
275
271
|
# for now, could be improved in the future to show more info in such corner cases
|
|
276
272
|
stop_workers()
|
|
277
273
|
is_finished = True
|
|
278
|
-
if
|
|
274
|
+
if ctx.is_stopped:
|
|
279
275
|
# Discard the event. The invariant is: the next event after `stream.stop()` is `Finished`
|
|
280
276
|
break
|
|
281
277
|
yield event
|
|
@@ -292,13 +288,13 @@ class ThreadPoolRunner(BaseRunner):
|
|
|
292
288
|
yield events.Interrupted()
|
|
293
289
|
|
|
294
290
|
def _init_workers(
|
|
295
|
-
self, tasks_queue: Queue, events_queue: Queue,
|
|
291
|
+
self, tasks_queue: Queue, events_queue: Queue, ctx: RunnerContext, generator_done: threading.Event
|
|
296
292
|
) -> list[threading.Thread]:
|
|
297
293
|
"""Initialize & start workers that will execute tests."""
|
|
298
294
|
workers = [
|
|
299
295
|
threading.Thread(
|
|
300
296
|
target=self._get_task(),
|
|
301
|
-
kwargs=self._get_worker_kwargs(tasks_queue, events_queue,
|
|
297
|
+
kwargs=self._get_worker_kwargs(tasks_queue, events_queue, ctx, generator_done),
|
|
302
298
|
name=f"schemathesis_{num}",
|
|
303
299
|
)
|
|
304
300
|
for num in range(self.workers_num)
|
|
@@ -311,7 +307,7 @@ class ThreadPoolRunner(BaseRunner):
|
|
|
311
307
|
return thread_task
|
|
312
308
|
|
|
313
309
|
def _get_worker_kwargs(
|
|
314
|
-
self, tasks_queue: Queue, events_queue: Queue,
|
|
310
|
+
self, tasks_queue: Queue, events_queue: Queue, ctx: RunnerContext, generator_done: threading.Event
|
|
315
311
|
) -> dict[str, Any]:
|
|
316
312
|
return {
|
|
317
313
|
"tasks_queue": tasks_queue,
|
|
@@ -324,8 +320,7 @@ class ThreadPoolRunner(BaseRunner):
|
|
|
324
320
|
"auth": self.auth,
|
|
325
321
|
"auth_type": self.auth_type,
|
|
326
322
|
"headers": self.headers,
|
|
327
|
-
"
|
|
328
|
-
"results": results,
|
|
323
|
+
"ctx": ctx,
|
|
329
324
|
"stateful": self.stateful,
|
|
330
325
|
"stateful_recursion_limit": self.stateful_recursion_limit,
|
|
331
326
|
"data_generation_methods": self.schema.data_generation_methods,
|
|
@@ -343,7 +338,7 @@ class ThreadPoolWSGIRunner(ThreadPoolRunner):
|
|
|
343
338
|
return wsgi_thread_task
|
|
344
339
|
|
|
345
340
|
def _get_worker_kwargs(
|
|
346
|
-
self, tasks_queue: Queue, events_queue: Queue,
|
|
341
|
+
self, tasks_queue: Queue, events_queue: Queue, ctx: RunnerContext, generator_done: threading.Event
|
|
347
342
|
) -> dict[str, Any]:
|
|
348
343
|
return {
|
|
349
344
|
"tasks_queue": tasks_queue,
|
|
@@ -353,8 +348,7 @@ class ThreadPoolWSGIRunner(ThreadPoolRunner):
|
|
|
353
348
|
"targets": self.targets,
|
|
354
349
|
"settings": self.hypothesis_settings,
|
|
355
350
|
"generation_config": self.generation_config,
|
|
356
|
-
"
|
|
357
|
-
"results": results,
|
|
351
|
+
"ctx": ctx,
|
|
358
352
|
"stateful": self.stateful,
|
|
359
353
|
"stateful_recursion_limit": self.stateful_recursion_limit,
|
|
360
354
|
"data_generation_methods": self.schema.data_generation_methods,
|
|
@@ -374,7 +368,7 @@ class ThreadPoolASGIRunner(ThreadPoolRunner):
|
|
|
374
368
|
return asgi_thread_task
|
|
375
369
|
|
|
376
370
|
def _get_worker_kwargs(
|
|
377
|
-
self, tasks_queue: Queue, events_queue: Queue,
|
|
371
|
+
self, tasks_queue: Queue, events_queue: Queue, ctx: RunnerContext, generator_done: threading.Event
|
|
378
372
|
) -> dict[str, Any]:
|
|
379
373
|
return {
|
|
380
374
|
"tasks_queue": tasks_queue,
|
|
@@ -385,8 +379,7 @@ class ThreadPoolASGIRunner(ThreadPoolRunner):
|
|
|
385
379
|
"settings": self.hypothesis_settings,
|
|
386
380
|
"generation_config": self.generation_config,
|
|
387
381
|
"headers": self.headers,
|
|
388
|
-
"
|
|
389
|
-
"results": results,
|
|
382
|
+
"ctx": ctx,
|
|
390
383
|
"stateful": self.stateful,
|
|
391
384
|
"stateful_recursion_limit": self.stateful_recursion_limit,
|
|
392
385
|
"data_generation_methods": self.schema.data_generation_methods,
|
|
@@ -15,7 +15,6 @@ from ..code_samples import get_excluded_headers
|
|
|
15
15
|
from ..exceptions import (
|
|
16
16
|
BodyInGetRequestError,
|
|
17
17
|
DeadlineExceeded,
|
|
18
|
-
FailureContext,
|
|
19
18
|
InternalError,
|
|
20
19
|
InvalidRegularExpression,
|
|
21
20
|
OperationSchemaError,
|
|
@@ -27,7 +26,6 @@ from ..exceptions import (
|
|
|
27
26
|
format_exception,
|
|
28
27
|
make_unique_by_key,
|
|
29
28
|
)
|
|
30
|
-
from ..generation import DataGenerationMethod
|
|
31
29
|
from ..models import Case, Check, Interaction, Request, Response, Status, TestPhase, TestResult, TransitionId
|
|
32
30
|
from ..transports import deserialize_payload, serialize_payload
|
|
33
31
|
|
|
@@ -35,6 +33,9 @@ if TYPE_CHECKING:
|
|
|
35
33
|
import hypothesis.errors
|
|
36
34
|
from requests.structures import CaseInsensitiveDict
|
|
37
35
|
|
|
36
|
+
from ..failures import FailureContext
|
|
37
|
+
from ..generation import DataGenerationMethod
|
|
38
|
+
|
|
38
39
|
|
|
39
40
|
@dataclass
|
|
40
41
|
class SerializedCase:
|
|
@@ -389,7 +390,7 @@ def _scalar_name_from_error(exception: hypothesis.errors.InvalidArgument) -> str
|
|
|
389
390
|
@dataclass
|
|
390
391
|
class SerializedInteraction:
|
|
391
392
|
request: Request
|
|
392
|
-
response: Response
|
|
393
|
+
response: Response | None
|
|
393
394
|
checks: list[SerializedCheck]
|
|
394
395
|
status: Status
|
|
395
396
|
data_generation_method: DataGenerationMethod
|
schemathesis/sanitization.py
CHANGED
|
@@ -246,6 +246,7 @@ def sanitize_serialized_case(case: SerializedCase, *, config: Config | None = No
|
|
|
246
246
|
|
|
247
247
|
def sanitize_serialized_interaction(interaction: SerializedInteraction, *, config: Config | None = None) -> None:
|
|
248
248
|
sanitize_request(interaction.request, config=config)
|
|
249
|
-
|
|
249
|
+
if interaction.response is not None:
|
|
250
|
+
sanitize_value(interaction.response.headers, config=config)
|
|
250
251
|
for check in interaction.checks:
|
|
251
252
|
sanitize_serialized_check(check, config=config)
|
schemathesis/schemas.py
CHANGED
|
@@ -18,10 +18,6 @@ from typing import (
|
|
|
18
18
|
)
|
|
19
19
|
from urllib.parse import quote, unquote, urljoin, urlparse, urlsplit, urlunsplit
|
|
20
20
|
|
|
21
|
-
import hypothesis
|
|
22
|
-
from hypothesis.strategies import SearchStrategy
|
|
23
|
-
from pyrate_limiter import Limiter
|
|
24
|
-
|
|
25
21
|
from ._dependency_versions import IS_PYRATE_LIMITER_ABOVE_3
|
|
26
22
|
from ._hypothesis import create_test
|
|
27
23
|
from .auths import AuthStorage
|
|
@@ -48,25 +44,29 @@ from .internal.deprecation import warn_filtration_arguments
|
|
|
48
44
|
from .internal.output import OutputConfig
|
|
49
45
|
from .internal.result import Ok, Result
|
|
50
46
|
from .models import APIOperation, Case
|
|
51
|
-
from .stateful import Stateful, StatefulTest
|
|
52
|
-
from .stateful.state_machine import APIStateMachine
|
|
53
|
-
from .types import (
|
|
54
|
-
Body,
|
|
55
|
-
Cookies,
|
|
56
|
-
Filter,
|
|
57
|
-
FormData,
|
|
58
|
-
GenericTest,
|
|
59
|
-
Headers,
|
|
60
|
-
NotSet,
|
|
61
|
-
PathParameters,
|
|
62
|
-
Query,
|
|
63
|
-
Specification,
|
|
64
|
-
)
|
|
65
47
|
from .utils import PARAMETRIZE_MARKER, GivenInput, given_proxy
|
|
66
48
|
|
|
67
49
|
if TYPE_CHECKING:
|
|
50
|
+
import hypothesis
|
|
51
|
+
from hypothesis.strategies import SearchStrategy
|
|
52
|
+
from pyrate_limiter import Limiter
|
|
53
|
+
|
|
54
|
+
from .stateful import Stateful, StatefulTest
|
|
55
|
+
from .stateful.state_machine import APIStateMachine
|
|
68
56
|
from .transports import Transport
|
|
69
57
|
from .transports.responses import GenericResponse
|
|
58
|
+
from .types import (
|
|
59
|
+
Body,
|
|
60
|
+
Cookies,
|
|
61
|
+
Filter,
|
|
62
|
+
FormData,
|
|
63
|
+
GenericTest,
|
|
64
|
+
Headers,
|
|
65
|
+
NotSet,
|
|
66
|
+
PathParameters,
|
|
67
|
+
Query,
|
|
68
|
+
Specification,
|
|
69
|
+
)
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
C = TypeVar("C", bound=Case)
|
|
@@ -241,7 +241,7 @@ class BaseSchema(Mapping):
|
|
|
241
241
|
raise NotImplementedError
|
|
242
242
|
|
|
243
243
|
def get_all_operations(
|
|
244
|
-
self, hooks: HookDispatcher | None = None
|
|
244
|
+
self, hooks: HookDispatcher | None = None, generation_config: GenerationConfig | None = None
|
|
245
245
|
) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
|
|
246
246
|
raise NotImplementedError
|
|
247
247
|
|
|
@@ -276,7 +276,7 @@ class BaseSchema(Mapping):
|
|
|
276
276
|
_given_kwargs: dict[str, GivenInput] | None = None,
|
|
277
277
|
) -> Generator[Result[tuple[APIOperation, Callable], OperationSchemaError], None, None]:
|
|
278
278
|
"""Generate all operations and Hypothesis tests for them."""
|
|
279
|
-
for result in self.get_all_operations(hooks=hooks):
|
|
279
|
+
for result in self.get_all_operations(hooks=hooks, generation_config=generation_config):
|
|
280
280
|
if isinstance(result, Ok):
|
|
281
281
|
operation = result.ok()
|
|
282
282
|
_as_strategy_kwargs: dict[str, Any] | None
|
|
@@ -510,7 +510,6 @@ class BaseSchema(Mapping):
|
|
|
510
510
|
**kwargs: Any,
|
|
511
511
|
) -> SearchStrategy:
|
|
512
512
|
"""Build a strategy for generating test cases for all defined API operations."""
|
|
513
|
-
assert len(self) > 0, "No API operations found"
|
|
514
513
|
strategies = [
|
|
515
514
|
operation.ok().as_strategy(
|
|
516
515
|
hooks=hooks,
|
|
@@ -548,7 +547,6 @@ class APIOperationMap(Mapping):
|
|
|
548
547
|
**kwargs: Any,
|
|
549
548
|
) -> SearchStrategy:
|
|
550
549
|
"""Build a strategy for generating test cases for all API operations defined in this subset."""
|
|
551
|
-
assert len(self._data) > 0, "No API operations found"
|
|
552
550
|
strategies = [
|
|
553
551
|
operation.as_strategy(
|
|
554
552
|
hooks=hooks,
|
schemathesis/serializers.py
CHANGED
schemathesis/service/client.py
CHANGED
|
@@ -11,7 +11,6 @@ import requests
|
|
|
11
11
|
from requests.adapters import HTTPAdapter, Retry
|
|
12
12
|
|
|
13
13
|
from ..constants import USER_AGENT
|
|
14
|
-
from .ci import CIProvider
|
|
15
14
|
from .constants import CI_PROVIDER_HEADER, REPORT_CORRELATION_ID_HEADER, REQUEST_TIMEOUT, UPLOAD_SOURCE_HEADER
|
|
16
15
|
from .metadata import Metadata, collect_dependency_versions
|
|
17
16
|
from .models import (
|
|
@@ -29,6 +28,7 @@ from .models import (
|
|
|
29
28
|
|
|
30
29
|
if TYPE_CHECKING:
|
|
31
30
|
from ..runner import probes
|
|
31
|
+
from .ci import CIProvider
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
def response_hook(response: requests.Response, **_kwargs: Any) -> None:
|
schemathesis/service/events.py
CHANGED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import base64
|
|
4
4
|
import re
|
|
5
5
|
from ipaddress import IPv4Network, IPv6Network
|
|
6
|
-
from typing import TYPE_CHECKING, Any, Callable
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
7
7
|
|
|
8
8
|
from ..graphql import nodes
|
|
9
9
|
from ..internal.result import Err, Ok, Result
|
|
@@ -75,7 +75,7 @@ def _apply_media_types_extension(extension: MediaTypesExtension) -> None:
|
|
|
75
75
|
_apply_simple_extension(extension, extension.media_types, media_types.register_media_type)
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
def _find_built_in_strategy(name: str) ->
|
|
78
|
+
def _find_built_in_strategy(name: str) -> st.SearchStrategy | None:
|
|
79
79
|
"""Find a built-in Hypothesis strategy by its name."""
|
|
80
80
|
from hypothesis import provisional as pr
|
|
81
81
|
from hypothesis import strategies as st
|
schemathesis/service/hosts.py
CHANGED
|
@@ -6,14 +6,16 @@ import enum
|
|
|
6
6
|
import tempfile
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import Any
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
10
|
|
|
11
11
|
import tomli
|
|
12
12
|
import tomli_w
|
|
13
13
|
|
|
14
|
-
from ..types import PathLike
|
|
15
14
|
from .constants import DEFAULT_HOSTNAME, DEFAULT_HOSTS_PATH, HOSTS_FORMAT_VERSION
|
|
16
15
|
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from ..types import PathLike
|
|
18
|
+
|
|
17
19
|
|
|
18
20
|
@dataclass
|
|
19
21
|
class HostData:
|
schemathesis/service/models.py
CHANGED
|
@@ -224,11 +224,11 @@ Extension = Union[
|
|
|
224
224
|
def extension_from_dict(data: dict[str, Any]) -> Extension:
|
|
225
225
|
if data["type"] == "schema_patches":
|
|
226
226
|
return SchemaPatchesExtension(patches=data["patches"])
|
|
227
|
-
|
|
227
|
+
if data["type"] == "string_formats":
|
|
228
228
|
return OpenApiStringFormatsExtension.from_dict(formats=data["items"])
|
|
229
|
-
|
|
229
|
+
if data["type"] == "scalars":
|
|
230
230
|
return GraphQLScalarsExtension.from_dict(scalars=data["items"])
|
|
231
|
-
|
|
231
|
+
if data["type"] == "media_types":
|
|
232
232
|
return MediaTypesExtension.from_dict(media_types=data["items"])
|
|
233
233
|
return UnknownExtension(type=data["type"])
|
|
234
234
|
|
schemathesis/service/report.py
CHANGED
|
@@ -12,21 +12,21 @@ from io import BytesIO
|
|
|
12
12
|
from queue import Queue
|
|
13
13
|
from typing import TYPE_CHECKING, Any
|
|
14
14
|
|
|
15
|
-
import click
|
|
16
|
-
|
|
17
15
|
from ..cli.handlers import EventHandler
|
|
18
16
|
from ..runner.events import Initialized, InternalError, Interrupted
|
|
19
17
|
from . import ci, events, usage
|
|
20
18
|
from .constants import REPORT_FORMAT_VERSION, STOP_MARKER, WORKER_JOIN_TIMEOUT
|
|
21
|
-
from .hosts import HostData
|
|
22
19
|
from .metadata import Metadata
|
|
23
20
|
from .models import UploadResponse
|
|
24
21
|
from .serialization import serialize_event
|
|
25
22
|
|
|
26
23
|
if TYPE_CHECKING:
|
|
24
|
+
import click
|
|
25
|
+
|
|
27
26
|
from ..cli.context import ExecutionContext
|
|
28
27
|
from ..runner.events import ExecutionEvent
|
|
29
28
|
from .client import ServiceClient
|
|
29
|
+
from .hosts import HostData
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
@dataclass
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import asdict
|
|
4
|
-
from typing import Any, Callable, Dict, Optional, TypeVar, cast
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, TypeVar, cast
|
|
5
5
|
|
|
6
6
|
from ..internal.transformation import merge_recursively
|
|
7
7
|
from ..runner import events
|
|
8
8
|
from ..runner.serialization import _serialize_check
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ..stateful import events as stateful_events
|
|
10
12
|
|
|
11
13
|
S = TypeVar("S", bound=events.ExecutionEvent)
|
|
12
14
|
SerializeFunc = Callable[[S], Optional[Dict[str, Any]]]
|
|
@@ -7,7 +7,7 @@ from json import JSONDecodeError
|
|
|
7
7
|
from typing import IO, TYPE_CHECKING, Any, Callable, Dict, NoReturn, cast
|
|
8
8
|
|
|
9
9
|
from ...code_samples import CodeSampleStyle
|
|
10
|
-
from ...constants import WAIT_FOR_SCHEMA_INTERVAL
|
|
10
|
+
from ...constants import DEFAULT_RESPONSE_TIMEOUT, WAIT_FOR_SCHEMA_INTERVAL
|
|
11
11
|
from ...exceptions import SchemaError, SchemaErrorType
|
|
12
12
|
from ...generation import (
|
|
13
13
|
DEFAULT_DATA_GENERATION_METHODS,
|
|
@@ -139,11 +139,12 @@ def from_url(
|
|
|
139
139
|
interval=WAIT_FOR_SCHEMA_INTERVAL,
|
|
140
140
|
)
|
|
141
141
|
def _load_schema(_uri: str, **_kwargs: Any) -> requests.Response:
|
|
142
|
-
return requests.post(_uri, **
|
|
142
|
+
return requests.post(_uri, **_kwargs)
|
|
143
143
|
|
|
144
144
|
else:
|
|
145
145
|
_load_schema = requests.post
|
|
146
146
|
|
|
147
|
+
kwargs.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT / 1000)
|
|
147
148
|
response = load_schema_from_url(lambda: _load_schema(url, **kwargs))
|
|
148
149
|
raw_schema = extract_schema_from_response(response)
|
|
149
150
|
return from_dict(
|
|
@@ -356,8 +357,8 @@ def from_asgi(
|
|
|
356
357
|
|
|
357
358
|
|
|
358
359
|
def get_loader_for_app(app: Any) -> Callable:
|
|
359
|
-
from
|
|
360
|
+
from ...transports.asgi import is_asgi_app
|
|
360
361
|
|
|
361
|
-
if
|
|
362
|
+
if is_asgi_app(app):
|
|
362
363
|
return from_asgi
|
|
363
364
|
return from_wsgi
|