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
schemathesis/runner/impl/core.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import functools
|
|
3
4
|
import logging
|
|
5
|
+
import operator
|
|
4
6
|
import re
|
|
5
7
|
import threading
|
|
6
8
|
import time
|
|
@@ -9,7 +11,6 @@ import uuid
|
|
|
9
11
|
import warnings
|
|
10
12
|
from contextlib import contextmanager
|
|
11
13
|
from dataclasses import dataclass, field
|
|
12
|
-
from types import TracebackType
|
|
13
14
|
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, List, Literal, cast
|
|
14
15
|
from warnings import WarningMessage, catch_warnings
|
|
15
16
|
|
|
@@ -20,7 +21,7 @@ from hypothesis.errors import HypothesisException, InvalidArgument
|
|
|
20
21
|
from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
|
|
21
22
|
from jsonschema.exceptions import SchemaError as JsonSchemaError
|
|
22
23
|
from jsonschema.exceptions import ValidationError
|
|
23
|
-
from requests.
|
|
24
|
+
from requests.structures import CaseInsensitiveDict
|
|
24
25
|
from urllib3.exceptions import InsecureRequestWarning
|
|
25
26
|
|
|
26
27
|
from ... import experimental, failures, hooks
|
|
@@ -31,7 +32,6 @@ from ..._hypothesis import (
|
|
|
31
32
|
get_non_serializable_mark,
|
|
32
33
|
has_unsatisfied_example_mark,
|
|
33
34
|
)
|
|
34
|
-
from ..._override import CaseOverride
|
|
35
35
|
from ...auths import unregister as unregister_auth
|
|
36
36
|
from ...checks import _make_max_response_time_failure_message
|
|
37
37
|
from ...constants import (
|
|
@@ -57,11 +57,11 @@ from ...exceptions import (
|
|
|
57
57
|
)
|
|
58
58
|
from ...generation import DataGenerationMethod, GenerationConfig
|
|
59
59
|
from ...hooks import HookContext, get_all_by_name
|
|
60
|
+
from ...internal.checks import CheckContext
|
|
60
61
|
from ...internal.datetime import current_datetime
|
|
61
62
|
from ...internal.result import Err, Ok, Result
|
|
62
|
-
from ...models import APIOperation, Case, Check,
|
|
63
|
+
from ...models import APIOperation, Case, Check, Status, TestResult
|
|
63
64
|
from ...runner import events
|
|
64
|
-
from ...schemas import BaseSchema
|
|
65
65
|
from ...service import extensions
|
|
66
66
|
from ...service.models import AnalysisResult, AnalysisSuccess
|
|
67
67
|
from ...specs.openapi import formats
|
|
@@ -71,14 +71,22 @@ from ...stateful import runner as stateful_runner
|
|
|
71
71
|
from ...targets import Target, TargetContext
|
|
72
72
|
from ...transports import RequestConfig, RequestsTransport
|
|
73
73
|
from ...transports.auth import get_requests_auth, prepare_wsgi_headers
|
|
74
|
-
from ...types import RawAuth
|
|
75
74
|
from ...utils import capture_hypothesis_output
|
|
76
75
|
from .. import probes
|
|
77
76
|
from ..serialization import SerializedTestResult
|
|
77
|
+
from .context import RunnerContext
|
|
78
78
|
|
|
79
79
|
if TYPE_CHECKING:
|
|
80
|
+
from types import TracebackType
|
|
81
|
+
|
|
82
|
+
from requests.auth import HTTPDigestAuth
|
|
83
|
+
|
|
84
|
+
from ..._override import CaseOverride
|
|
85
|
+
from ...internal.checks import CheckFunction
|
|
86
|
+
from ...schemas import BaseSchema
|
|
80
87
|
from ...service.client import ServiceClient
|
|
81
88
|
from ...transports.responses import GenericResponse, WSGIResponse
|
|
89
|
+
from ...types import RawAuth
|
|
82
90
|
|
|
83
91
|
|
|
84
92
|
def _should_count_towards_stop(event: events.ExecutionEvent) -> bool:
|
|
@@ -92,7 +100,7 @@ class BaseRunner:
|
|
|
92
100
|
max_response_time: int | None
|
|
93
101
|
targets: Iterable[Target]
|
|
94
102
|
hypothesis_settings: hypothesis.settings
|
|
95
|
-
generation_config: GenerationConfig
|
|
103
|
+
generation_config: GenerationConfig | None
|
|
96
104
|
probe_config: probes.ProbeConfig
|
|
97
105
|
request_config: RequestConfig = field(default_factory=RequestConfig)
|
|
98
106
|
override: CaseOverride | None = None
|
|
@@ -104,6 +112,7 @@ class BaseRunner:
|
|
|
104
112
|
exit_first: bool = False
|
|
105
113
|
max_failures: int | None = None
|
|
106
114
|
started_at: str = field(default_factory=current_datetime)
|
|
115
|
+
unique_data: bool = False
|
|
107
116
|
dry_run: bool = False
|
|
108
117
|
stateful: Stateful | None = None
|
|
109
118
|
stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT
|
|
@@ -122,7 +131,7 @@ class BaseRunner:
|
|
|
122
131
|
# If auth is explicitly provided, then the global provider is ignored
|
|
123
132
|
if self.auth is not None:
|
|
124
133
|
unregister_auth()
|
|
125
|
-
|
|
134
|
+
ctx = RunnerContext(seed=self.seed, stop_event=stop_event, unique_data=self.unique_data)
|
|
126
135
|
start_time = time.monotonic()
|
|
127
136
|
initialized = None
|
|
128
137
|
__probes = None
|
|
@@ -134,15 +143,15 @@ class BaseRunner:
|
|
|
134
143
|
schema=self.schema,
|
|
135
144
|
count_operations=self.count_operations,
|
|
136
145
|
count_links=self.count_links,
|
|
137
|
-
seed=
|
|
146
|
+
seed=ctx.seed,
|
|
138
147
|
start_time=start_time,
|
|
139
148
|
)
|
|
140
149
|
return initialized
|
|
141
150
|
|
|
142
151
|
def _finish() -> events.Finished:
|
|
143
|
-
if has_all_not_found
|
|
144
|
-
|
|
145
|
-
return events.Finished.from_results(results=
|
|
152
|
+
if ctx.has_all_not_found:
|
|
153
|
+
ctx.add_warning(ALL_NOT_FOUND_WARNING_MESSAGE)
|
|
154
|
+
return events.Finished.from_results(results=ctx.data, running_time=time.monotonic() - start_time)
|
|
146
155
|
|
|
147
156
|
def _before_probes() -> events.BeforeProbing:
|
|
148
157
|
return events.BeforeProbing()
|
|
@@ -176,7 +185,7 @@ class BaseRunner:
|
|
|
176
185
|
def _after_analysis() -> events.AfterAnalysis:
|
|
177
186
|
return events.AfterAnalysis(analysis=__analysis)
|
|
178
187
|
|
|
179
|
-
if
|
|
188
|
+
if ctx.is_stopped:
|
|
180
189
|
yield _finish()
|
|
181
190
|
return
|
|
182
191
|
|
|
@@ -192,16 +201,16 @@ class BaseRunner:
|
|
|
192
201
|
event = event_factory()
|
|
193
202
|
if event is not None:
|
|
194
203
|
yield event
|
|
195
|
-
if
|
|
196
|
-
yield _finish()
|
|
204
|
+
if ctx.is_stopped:
|
|
205
|
+
yield _finish() # type: ignore[unreachable]
|
|
197
206
|
return
|
|
198
207
|
|
|
199
208
|
try:
|
|
200
209
|
warnings.simplefilter("ignore", InsecureRequestWarning)
|
|
201
210
|
if not experimental.STATEFUL_ONLY.is_enabled:
|
|
202
|
-
yield from self._execute(
|
|
211
|
+
yield from self._execute(ctx)
|
|
203
212
|
if not self._is_stopping_due_to_failure_limit:
|
|
204
|
-
yield from self._run_stateful_tests(
|
|
213
|
+
yield from self._run_stateful_tests(ctx)
|
|
205
214
|
except KeyboardInterrupt:
|
|
206
215
|
yield events.Interrupted()
|
|
207
216
|
|
|
@@ -222,19 +231,17 @@ class BaseRunner:
|
|
|
222
231
|
return self._failures_counter >= self.max_failures
|
|
223
232
|
return False
|
|
224
233
|
|
|
225
|
-
def _execute(
|
|
226
|
-
self, results: TestResultSet, stop_event: threading.Event
|
|
227
|
-
) -> Generator[events.ExecutionEvent, None, None]:
|
|
234
|
+
def _execute(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
|
|
228
235
|
raise NotImplementedError
|
|
229
236
|
|
|
230
|
-
def _run_stateful_tests(self,
|
|
237
|
+
def _run_stateful_tests(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
|
|
231
238
|
# Run new-style stateful tests
|
|
232
239
|
if self.stateful is not None and experimental.STATEFUL_TEST_RUNNER.is_enabled and self.schema.links_count > 0:
|
|
233
240
|
result = TestResult(
|
|
234
241
|
method="",
|
|
235
242
|
path="",
|
|
236
243
|
verbose_name="Stateful tests",
|
|
237
|
-
seed=
|
|
244
|
+
seed=ctx.seed,
|
|
238
245
|
data_generation_method=self.schema.data_generation_methods,
|
|
239
246
|
)
|
|
240
247
|
headers = self.headers or {}
|
|
@@ -251,7 +258,7 @@ class BaseRunner:
|
|
|
251
258
|
max_failures=None if self.max_failures is None else self.max_failures - self._failures_counter,
|
|
252
259
|
request=self.request_config,
|
|
253
260
|
auth=auth,
|
|
254
|
-
seed=
|
|
261
|
+
seed=ctx.seed,
|
|
255
262
|
override=self.override,
|
|
256
263
|
)
|
|
257
264
|
state_machine = self.schema.as_state_machine()
|
|
@@ -277,6 +284,8 @@ class BaseRunner:
|
|
|
277
284
|
case=event.case,
|
|
278
285
|
response=response,
|
|
279
286
|
checks=event.checks,
|
|
287
|
+
headers=headers,
|
|
288
|
+
session=None,
|
|
280
289
|
)
|
|
281
290
|
|
|
282
291
|
else:
|
|
@@ -317,7 +326,7 @@ class BaseRunner:
|
|
|
317
326
|
status = Status.error
|
|
318
327
|
result.add_error(stateful_event.exception)
|
|
319
328
|
yield events.StatefulEvent(data=stateful_event)
|
|
320
|
-
|
|
329
|
+
ctx.add_result(result)
|
|
321
330
|
yield events.AfterStatefulExecution(
|
|
322
331
|
status=status,
|
|
323
332
|
result=SerializedTestResult.from_test_result(result),
|
|
@@ -328,11 +337,10 @@ class BaseRunner:
|
|
|
328
337
|
def _run_tests(
|
|
329
338
|
self,
|
|
330
339
|
maker: Callable,
|
|
331
|
-
|
|
340
|
+
test_func: Callable,
|
|
332
341
|
settings: hypothesis.settings,
|
|
333
|
-
generation_config: GenerationConfig,
|
|
334
|
-
|
|
335
|
-
results: TestResultSet,
|
|
342
|
+
generation_config: GenerationConfig | None,
|
|
343
|
+
ctx: RunnerContext,
|
|
336
344
|
recursion_level: int = 0,
|
|
337
345
|
headers: dict[str, Any] | None = None,
|
|
338
346
|
**kwargs: Any,
|
|
@@ -352,10 +360,10 @@ class BaseRunner:
|
|
|
352
360
|
return kw
|
|
353
361
|
|
|
354
362
|
for result in maker(
|
|
355
|
-
|
|
363
|
+
test_func,
|
|
356
364
|
settings=settings,
|
|
357
365
|
generation_config=generation_config,
|
|
358
|
-
seed=seed,
|
|
366
|
+
seed=ctx.seed,
|
|
359
367
|
as_strategy_kwargs=as_strategy_kwargs,
|
|
360
368
|
):
|
|
361
369
|
if isinstance(result, Ok):
|
|
@@ -372,7 +380,7 @@ class BaseRunner:
|
|
|
372
380
|
for event in run_test(
|
|
373
381
|
operation,
|
|
374
382
|
test,
|
|
375
|
-
|
|
383
|
+
ctx=ctx,
|
|
376
384
|
feedback=feedback,
|
|
377
385
|
recursion_level=recursion_level,
|
|
378
386
|
data_generation_methods=self.schema.data_generation_methods,
|
|
@@ -388,28 +396,25 @@ class BaseRunner:
|
|
|
388
396
|
if feedback is not None:
|
|
389
397
|
yield from self._run_tests(
|
|
390
398
|
feedback.get_stateful_tests,
|
|
391
|
-
|
|
399
|
+
test_func,
|
|
392
400
|
settings=settings,
|
|
393
401
|
generation_config=generation_config,
|
|
394
|
-
seed=seed,
|
|
395
402
|
recursion_level=recursion_level + 1,
|
|
396
|
-
|
|
403
|
+
ctx=ctx,
|
|
397
404
|
headers=headers,
|
|
398
405
|
**kwargs,
|
|
399
406
|
)
|
|
400
407
|
except OperationSchemaError as exc:
|
|
401
408
|
yield from handle_schema_error(
|
|
402
409
|
exc,
|
|
403
|
-
|
|
410
|
+
ctx,
|
|
404
411
|
self.schema.data_generation_methods,
|
|
405
412
|
recursion_level,
|
|
406
413
|
before_execution_correlation_id=before_execution_correlation_id,
|
|
407
414
|
)
|
|
408
415
|
else:
|
|
409
416
|
# Schema errors
|
|
410
|
-
yield from handle_schema_error(
|
|
411
|
-
result.err(), results, self.schema.data_generation_methods, recursion_level
|
|
412
|
-
)
|
|
417
|
+
yield from handle_schema_error(result.err(), ctx, self.schema.data_generation_methods, recursion_level)
|
|
413
418
|
|
|
414
419
|
|
|
415
420
|
def run_probes(schema: BaseSchema, config: probes.ProbeConfig) -> list[probes.ProbeRun]:
|
|
@@ -454,7 +459,7 @@ class EventStream:
|
|
|
454
459
|
|
|
455
460
|
def handle_schema_error(
|
|
456
461
|
error: OperationSchemaError,
|
|
457
|
-
|
|
462
|
+
ctx: RunnerContext,
|
|
458
463
|
data_generation_methods: Iterable[DataGenerationMethod],
|
|
459
464
|
recursion_level: int,
|
|
460
465
|
*,
|
|
@@ -499,11 +504,11 @@ def handle_schema_error(
|
|
|
499
504
|
hypothesis_output=[],
|
|
500
505
|
correlation_id=correlation_id,
|
|
501
506
|
)
|
|
502
|
-
|
|
507
|
+
ctx.add_result(result)
|
|
503
508
|
else:
|
|
504
509
|
# When there is no `method`, then the schema error may cover multiple operations, and we can't display it in
|
|
505
510
|
# the progress bar
|
|
506
|
-
|
|
511
|
+
ctx.add_generic_error(error)
|
|
507
512
|
|
|
508
513
|
|
|
509
514
|
def run_test(
|
|
@@ -512,7 +517,7 @@ def run_test(
|
|
|
512
517
|
checks: Iterable[CheckFunction],
|
|
513
518
|
data_generation_methods: Iterable[DataGenerationMethod],
|
|
514
519
|
targets: Iterable[Target],
|
|
515
|
-
|
|
520
|
+
ctx: RunnerContext,
|
|
516
521
|
headers: dict[str, Any] | None,
|
|
517
522
|
recursion_level: int,
|
|
518
523
|
**kwargs: Any,
|
|
@@ -562,9 +567,10 @@ def run_test(
|
|
|
562
567
|
try:
|
|
563
568
|
with catch_warnings(record=True) as warnings, capture_hypothesis_output() as hypothesis_output:
|
|
564
569
|
test(
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
570
|
+
ctx=ctx,
|
|
571
|
+
checks=checks,
|
|
572
|
+
targets=targets,
|
|
573
|
+
result=result,
|
|
568
574
|
errors=errors,
|
|
569
575
|
headers=headers,
|
|
570
576
|
data_generation_methods=data_generation_methods,
|
|
@@ -689,10 +695,10 @@ def run_test(
|
|
|
689
695
|
result.seed = getattr(test, "_hypothesis_internal_use_seed", None) or getattr(
|
|
690
696
|
test, "_hypothesis_internal_use_generated_seed", None
|
|
691
697
|
)
|
|
692
|
-
|
|
698
|
+
ctx.add_result(result)
|
|
693
699
|
for status_code in (401, 403):
|
|
694
700
|
if has_too_many_responses_with_status(result, status_code):
|
|
695
|
-
|
|
701
|
+
ctx.add_warning(TOO_MANY_RESPONSES_WARNING_TEMPLATE.format(f"`{operation.verbose_name}`", status_code))
|
|
696
702
|
yield events.AfterExecution.from_result(
|
|
697
703
|
result=result,
|
|
698
704
|
status=status,
|
|
@@ -727,22 +733,6 @@ def has_too_many_responses_with_status(result: TestResult, status_code: int) ->
|
|
|
727
733
|
ALL_NOT_FOUND_WARNING_MESSAGE = "All API responses have a 404 status code. Did you specify the proper API location?"
|
|
728
734
|
|
|
729
735
|
|
|
730
|
-
def has_all_not_found(results: TestResultSet) -> bool:
|
|
731
|
-
"""Check if all responses are 404."""
|
|
732
|
-
has_not_found = False
|
|
733
|
-
for result in results.results:
|
|
734
|
-
for check in result.checks:
|
|
735
|
-
if check.response is not None:
|
|
736
|
-
if check.response.status_code == 404:
|
|
737
|
-
has_not_found = True
|
|
738
|
-
else:
|
|
739
|
-
# There are non-404 responses, no reason to check any other response
|
|
740
|
-
return False
|
|
741
|
-
# Only happens if all responses are 404, or there are no responses at all.
|
|
742
|
-
# In the first case, it returns True, for the latter - False
|
|
743
|
-
return has_not_found
|
|
744
|
-
|
|
745
|
-
|
|
746
736
|
def setup_hypothesis_database_key(test: Callable, operation: APIOperation) -> None:
|
|
747
737
|
"""Make Hypothesis use separate database entries for every API operation.
|
|
748
738
|
|
|
@@ -779,7 +769,9 @@ def group_errors(errors: list[Exception]) -> None:
|
|
|
779
769
|
serialization_errors = [error for error in errors if isinstance(error, SerializationNotPossible)]
|
|
780
770
|
if len(serialization_errors) > 1:
|
|
781
771
|
errors[:] = [error for error in errors if not isinstance(error, SerializationNotPossible)]
|
|
782
|
-
media_types =
|
|
772
|
+
media_types: list[str] = functools.reduce(
|
|
773
|
+
operator.iadd, (entry.media_types for entry in serialization_errors), []
|
|
774
|
+
)
|
|
783
775
|
errors.append(SerializationNotPossible.from_media_types(*media_types))
|
|
784
776
|
|
|
785
777
|
|
|
@@ -804,6 +796,7 @@ def deduplicate_errors(errors: list[Exception]) -> Generator[Exception, None, No
|
|
|
804
796
|
def run_checks(
|
|
805
797
|
*,
|
|
806
798
|
case: Case,
|
|
799
|
+
ctx: CheckContext,
|
|
807
800
|
checks: Iterable[CheckFunction],
|
|
808
801
|
check_results: list[Check],
|
|
809
802
|
result: TestResult,
|
|
@@ -826,7 +819,7 @@ def run_checks(
|
|
|
826
819
|
check_name = check.__name__
|
|
827
820
|
copied_case = case.partial_deepcopy()
|
|
828
821
|
try:
|
|
829
|
-
skip_check = check(response, copied_case)
|
|
822
|
+
skip_check = check(ctx, response, copied_case)
|
|
830
823
|
if not skip_check:
|
|
831
824
|
check_result = result.add_success(check_name, copied_case, response, elapsed_time)
|
|
832
825
|
check_results.append(check_result)
|
|
@@ -912,7 +905,33 @@ def _force_data_generation_method(values: list[DataGenerationMethod], case: Case
|
|
|
912
905
|
values[:] = [data_generation_method]
|
|
913
906
|
|
|
914
907
|
|
|
908
|
+
def cached_test_func(f: Callable) -> Callable:
|
|
909
|
+
def wrapped(*, ctx: RunnerContext, case: Case, **kwargs: Any) -> None:
|
|
910
|
+
if ctx.unique_data:
|
|
911
|
+
cached = ctx.get_cached_outcome(case)
|
|
912
|
+
if isinstance(cached, BaseException):
|
|
913
|
+
raise cached
|
|
914
|
+
elif cached is None:
|
|
915
|
+
return None
|
|
916
|
+
try:
|
|
917
|
+
f(ctx=ctx, case=case, **kwargs)
|
|
918
|
+
except BaseException as exc:
|
|
919
|
+
ctx.cache_outcome(case, exc)
|
|
920
|
+
raise
|
|
921
|
+
else:
|
|
922
|
+
ctx.cache_outcome(case, None)
|
|
923
|
+
else:
|
|
924
|
+
f(ctx=ctx, case=case, **kwargs)
|
|
925
|
+
|
|
926
|
+
wrapped.__name__ = f.__name__
|
|
927
|
+
|
|
928
|
+
return wrapped
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
@cached_test_func
|
|
915
932
|
def network_test(
|
|
933
|
+
*,
|
|
934
|
+
ctx: RunnerContext,
|
|
916
935
|
case: Case,
|
|
917
936
|
checks: Iterable[CheckFunction],
|
|
918
937
|
targets: Iterable[Target],
|
|
@@ -948,6 +967,8 @@ def network_test(
|
|
|
948
967
|
)
|
|
949
968
|
response = _network_test(case, *args)
|
|
950
969
|
add_cases(case, response, _network_test, *args)
|
|
970
|
+
elif store_interactions:
|
|
971
|
+
result.store_requests_response(case, None, Status.skip, [], headers=headers, session=session)
|
|
951
972
|
|
|
952
973
|
|
|
953
974
|
def _network_test(
|
|
@@ -987,13 +1008,18 @@ def _network_test(
|
|
|
987
1008
|
check_name, case, None, elapsed, f"Response timed out after {1000 * elapsed:.2f}ms", exc.context, request
|
|
988
1009
|
)
|
|
989
1010
|
check_results.append(check_result)
|
|
1011
|
+
if store_interactions:
|
|
1012
|
+
result.store_requests_response(case, None, Status.failure, [check_result], headers=headers, session=session)
|
|
990
1013
|
raise exc
|
|
991
1014
|
context = TargetContext(case=case, response=response, response_time=response.elapsed.total_seconds())
|
|
992
1015
|
run_targets(targets, context)
|
|
993
1016
|
status = Status.success
|
|
1017
|
+
|
|
1018
|
+
ctx = CheckContext(headers=CaseInsensitiveDict(headers) if headers else None)
|
|
994
1019
|
try:
|
|
995
1020
|
run_checks(
|
|
996
1021
|
case=case,
|
|
1022
|
+
ctx=ctx,
|
|
997
1023
|
checks=checks,
|
|
998
1024
|
check_results=check_results,
|
|
999
1025
|
result=result,
|
|
@@ -1008,7 +1034,7 @@ def _network_test(
|
|
|
1008
1034
|
if feedback is not None:
|
|
1009
1035
|
feedback.add_test_case(case, response)
|
|
1010
1036
|
if store_interactions:
|
|
1011
|
-
result.store_requests_response(case, response, status, check_results)
|
|
1037
|
+
result.store_requests_response(case, response, status, check_results, headers=headers, session=session)
|
|
1012
1038
|
return response
|
|
1013
1039
|
|
|
1014
1040
|
|
|
@@ -1020,7 +1046,9 @@ def get_session(auth: HTTPDigestAuth | RawAuth | None = None) -> Generator[reque
|
|
|
1020
1046
|
yield session
|
|
1021
1047
|
|
|
1022
1048
|
|
|
1049
|
+
@cached_test_func
|
|
1023
1050
|
def wsgi_test(
|
|
1051
|
+
ctx: RunnerContext,
|
|
1024
1052
|
case: Case,
|
|
1025
1053
|
checks: Iterable[CheckFunction],
|
|
1026
1054
|
targets: Iterable[Target],
|
|
@@ -1051,6 +1079,8 @@ def wsgi_test(
|
|
|
1051
1079
|
)
|
|
1052
1080
|
response = _wsgi_test(case, *args)
|
|
1053
1081
|
add_cases(case, response, _wsgi_test, *args)
|
|
1082
|
+
elif store_interactions:
|
|
1083
|
+
result.store_wsgi_response(case, None, headers, None, Status.skip, [])
|
|
1054
1084
|
|
|
1055
1085
|
|
|
1056
1086
|
def _wsgi_test(
|
|
@@ -1075,9 +1105,11 @@ def _wsgi_test(
|
|
|
1075
1105
|
result.logs.extend(recorded.records)
|
|
1076
1106
|
status = Status.success
|
|
1077
1107
|
check_results: list[Check] = []
|
|
1108
|
+
ctx = CheckContext(headers=CaseInsensitiveDict(headers) if headers else None)
|
|
1078
1109
|
try:
|
|
1079
1110
|
run_checks(
|
|
1080
1111
|
case=case,
|
|
1112
|
+
ctx=ctx,
|
|
1081
1113
|
checks=checks,
|
|
1082
1114
|
check_results=check_results,
|
|
1083
1115
|
result=result,
|
|
@@ -1096,7 +1128,9 @@ def _wsgi_test(
|
|
|
1096
1128
|
return response
|
|
1097
1129
|
|
|
1098
1130
|
|
|
1131
|
+
@cached_test_func
|
|
1099
1132
|
def asgi_test(
|
|
1133
|
+
ctx: RunnerContext,
|
|
1100
1134
|
case: Case,
|
|
1101
1135
|
checks: Iterable[CheckFunction],
|
|
1102
1136
|
targets: Iterable[Target],
|
|
@@ -1127,6 +1161,8 @@ def asgi_test(
|
|
|
1127
1161
|
)
|
|
1128
1162
|
response = _asgi_test(case, *args)
|
|
1129
1163
|
add_cases(case, response, _asgi_test, *args)
|
|
1164
|
+
elif store_interactions:
|
|
1165
|
+
result.store_requests_response(case, None, Status.skip, [], headers=headers, session=None)
|
|
1130
1166
|
|
|
1131
1167
|
|
|
1132
1168
|
def _asgi_test(
|
|
@@ -1147,9 +1183,11 @@ def _asgi_test(
|
|
|
1147
1183
|
run_targets(targets, context)
|
|
1148
1184
|
status = Status.success
|
|
1149
1185
|
check_results: list[Check] = []
|
|
1186
|
+
ctx = CheckContext(headers=CaseInsensitiveDict(headers) if headers else None)
|
|
1150
1187
|
try:
|
|
1151
1188
|
run_checks(
|
|
1152
1189
|
case=case,
|
|
1190
|
+
ctx=ctx,
|
|
1153
1191
|
checks=checks,
|
|
1154
1192
|
check_results=check_results,
|
|
1155
1193
|
result=result,
|
|
@@ -1164,5 +1202,5 @@ def _asgi_test(
|
|
|
1164
1202
|
if feedback is not None:
|
|
1165
1203
|
feedback.add_test_case(case, response)
|
|
1166
1204
|
if store_interactions:
|
|
1167
|
-
result.store_requests_response(case, response, status, check_results)
|
|
1205
|
+
result.store_requests_response(case, response, status, check_results, headers, session=None)
|
|
1168
1206
|
return response
|
schemathesis/runner/impl/solo.py
CHANGED
|
@@ -1,40 +1,39 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import threading
|
|
4
3
|
from dataclasses import dataclass
|
|
5
|
-
from typing import Generator
|
|
4
|
+
from typing import TYPE_CHECKING, Generator
|
|
6
5
|
|
|
7
|
-
from ...models import TestResultSet
|
|
8
6
|
from ...transports.auth import get_requests_auth
|
|
9
7
|
from .. import events
|
|
10
8
|
from .core import BaseRunner, asgi_test, get_session, network_test, wsgi_test
|
|
11
9
|
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .. import events
|
|
12
|
+
from .context import RunnerContext
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
@dataclass
|
|
14
16
|
class SingleThreadRunner(BaseRunner):
|
|
15
17
|
"""Fast runner that runs tests sequentially in the main thread."""
|
|
16
18
|
|
|
17
|
-
def _execute(
|
|
18
|
-
|
|
19
|
-
) -> Generator[events.ExecutionEvent, None, None]:
|
|
20
|
-
for event in self._execute_impl(results):
|
|
19
|
+
def _execute(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
|
|
20
|
+
for event in self._execute_impl(ctx):
|
|
21
21
|
yield event
|
|
22
|
-
if
|
|
22
|
+
if ctx.is_stopped or self._should_stop(event):
|
|
23
23
|
break
|
|
24
24
|
|
|
25
|
-
def _execute_impl(self,
|
|
25
|
+
def _execute_impl(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
|
|
26
26
|
auth = get_requests_auth(self.auth, self.auth_type)
|
|
27
27
|
with get_session(auth) as session:
|
|
28
28
|
yield from self._run_tests(
|
|
29
29
|
maker=self.schema.get_all_tests,
|
|
30
|
-
|
|
30
|
+
test_func=network_test,
|
|
31
31
|
settings=self.hypothesis_settings,
|
|
32
32
|
generation_config=self.generation_config,
|
|
33
|
-
seed=self.seed,
|
|
34
33
|
checks=self.checks,
|
|
35
34
|
max_response_time=self.max_response_time,
|
|
36
35
|
targets=self.targets,
|
|
37
|
-
|
|
36
|
+
ctx=ctx,
|
|
38
37
|
session=session,
|
|
39
38
|
headers=self.headers,
|
|
40
39
|
request_config=self.request_config,
|
|
@@ -45,17 +44,16 @@ class SingleThreadRunner(BaseRunner):
|
|
|
45
44
|
|
|
46
45
|
@dataclass
|
|
47
46
|
class SingleThreadWSGIRunner(SingleThreadRunner):
|
|
48
|
-
def _execute_impl(self,
|
|
47
|
+
def _execute_impl(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
|
|
49
48
|
yield from self._run_tests(
|
|
50
49
|
maker=self.schema.get_all_tests,
|
|
51
|
-
|
|
50
|
+
test_func=wsgi_test,
|
|
52
51
|
settings=self.hypothesis_settings,
|
|
53
52
|
generation_config=self.generation_config,
|
|
54
|
-
seed=self.seed,
|
|
55
53
|
checks=self.checks,
|
|
56
54
|
max_response_time=self.max_response_time,
|
|
57
55
|
targets=self.targets,
|
|
58
|
-
|
|
56
|
+
ctx=ctx,
|
|
59
57
|
auth=self.auth,
|
|
60
58
|
auth_type=self.auth_type,
|
|
61
59
|
headers=self.headers,
|
|
@@ -66,17 +64,16 @@ class SingleThreadWSGIRunner(SingleThreadRunner):
|
|
|
66
64
|
|
|
67
65
|
@dataclass
|
|
68
66
|
class SingleThreadASGIRunner(SingleThreadRunner):
|
|
69
|
-
def _execute_impl(self,
|
|
67
|
+
def _execute_impl(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
|
|
70
68
|
yield from self._run_tests(
|
|
71
69
|
maker=self.schema.get_all_tests,
|
|
72
|
-
|
|
70
|
+
test_func=asgi_test,
|
|
73
71
|
settings=self.hypothesis_settings,
|
|
74
72
|
generation_config=self.generation_config,
|
|
75
|
-
seed=self.seed,
|
|
76
73
|
checks=self.checks,
|
|
77
74
|
max_response_time=self.max_response_time,
|
|
78
75
|
targets=self.targets,
|
|
79
|
-
|
|
76
|
+
ctx=ctx,
|
|
80
77
|
headers=self.headers,
|
|
81
78
|
store_interactions=self.store_interactions,
|
|
82
79
|
dry_run=self.dry_run,
|