schemathesis 3.25.6__py3-none-any.whl → 3.39.7__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 +6 -6
- schemathesis/_compat.py +2 -2
- schemathesis/_dependency_versions.py +4 -2
- schemathesis/_hypothesis.py +369 -56
- schemathesis/_lazy_import.py +1 -0
- schemathesis/_override.py +5 -4
- schemathesis/_patches.py +21 -0
- schemathesis/_rate_limiter.py +7 -0
- schemathesis/_xml.py +75 -22
- schemathesis/auths.py +78 -16
- schemathesis/checks.py +21 -9
- schemathesis/cli/__init__.py +783 -432
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/callbacks.py +58 -13
- schemathesis/cli/cassettes.py +233 -47
- schemathesis/cli/constants.py +8 -2
- schemathesis/cli/context.py +22 -5
- schemathesis/cli/debug.py +2 -1
- schemathesis/cli/handlers.py +4 -1
- schemathesis/cli/junitxml.py +103 -22
- schemathesis/cli/options.py +15 -4
- schemathesis/cli/output/default.py +258 -112
- schemathesis/cli/output/short.py +23 -8
- schemathesis/cli/reporting.py +79 -0
- schemathesis/cli/sanitization.py +6 -0
- schemathesis/code_samples.py +5 -3
- schemathesis/constants.py +1 -0
- schemathesis/contrib/openapi/__init__.py +1 -1
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
- schemathesis/contrib/openapi/formats/uuid.py +2 -1
- schemathesis/contrib/unique_data.py +3 -3
- schemathesis/exceptions.py +76 -65
- schemathesis/experimental/__init__.py +35 -0
- schemathesis/extra/_aiohttp.py +1 -0
- schemathesis/extra/_flask.py +4 -1
- schemathesis/extra/_server.py +1 -0
- schemathesis/extra/pytest_plugin.py +17 -25
- schemathesis/failures.py +77 -9
- schemathesis/filters.py +185 -8
- schemathesis/fixups/__init__.py +1 -0
- schemathesis/fixups/fast_api.py +2 -2
- schemathesis/fixups/utf8_bom.py +1 -2
- schemathesis/generation/__init__.py +20 -36
- schemathesis/generation/_hypothesis.py +59 -0
- schemathesis/generation/_methods.py +44 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/graphql.py +0 -1
- schemathesis/hooks.py +89 -12
- schemathesis/internal/checks.py +84 -0
- schemathesis/internal/copy.py +22 -3
- schemathesis/internal/deprecation.py +6 -2
- schemathesis/internal/diff.py +15 -0
- schemathesis/internal/extensions.py +27 -0
- schemathesis/internal/jsonschema.py +2 -1
- schemathesis/internal/output.py +68 -0
- schemathesis/internal/result.py +1 -1
- schemathesis/internal/transformation.py +11 -0
- schemathesis/lazy.py +138 -25
- schemathesis/loaders.py +7 -5
- schemathesis/models.py +318 -211
- schemathesis/parameters.py +4 -0
- schemathesis/runner/__init__.py +50 -15
- schemathesis/runner/events.py +65 -5
- schemathesis/runner/impl/context.py +104 -0
- schemathesis/runner/impl/core.py +388 -177
- schemathesis/runner/impl/solo.py +19 -29
- schemathesis/runner/impl/threadpool.py +70 -79
- schemathesis/runner/probes.py +11 -9
- schemathesis/runner/serialization.py +150 -17
- schemathesis/sanitization.py +5 -1
- schemathesis/schemas.py +170 -102
- schemathesis/serializers.py +7 -2
- schemathesis/service/ci.py +1 -0
- schemathesis/service/client.py +39 -6
- schemathesis/service/events.py +5 -1
- schemathesis/service/extensions.py +224 -0
- schemathesis/service/hosts.py +6 -2
- schemathesis/service/metadata.py +25 -0
- schemathesis/service/models.py +211 -2
- schemathesis/service/report.py +6 -6
- schemathesis/service/serialization.py +45 -71
- schemathesis/service/usage.py +1 -0
- schemathesis/specs/graphql/_cache.py +26 -0
- schemathesis/specs/graphql/loaders.py +25 -5
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +2 -2
- schemathesis/specs/graphql/schemas.py +130 -100
- schemathesis/specs/graphql/validation.py +1 -2
- schemathesis/specs/openapi/__init__.py +1 -0
- schemathesis/specs/openapi/_cache.py +123 -0
- schemathesis/specs/openapi/_hypothesis.py +78 -60
- schemathesis/specs/openapi/checks.py +504 -25
- schemathesis/specs/openapi/converter.py +31 -4
- schemathesis/specs/openapi/definitions.py +10 -17
- schemathesis/specs/openapi/examples.py +126 -12
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +1 -1
- schemathesis/specs/openapi/expressions/extractors.py +26 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +29 -6
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +44 -0
- schemathesis/specs/openapi/links.py +125 -42
- schemathesis/specs/openapi/loaders.py +77 -36
- schemathesis/specs/openapi/media_types.py +34 -0
- schemathesis/specs/openapi/negative/__init__.py +6 -3
- schemathesis/specs/openapi/negative/mutations.py +21 -6
- schemathesis/specs/openapi/parameters.py +39 -25
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +37 -7
- schemathesis/specs/openapi/schemas.py +360 -241
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +1 -0
- schemathesis/specs/openapi/stateful/__init__.py +198 -70
- schemathesis/specs/openapi/stateful/statistic.py +198 -0
- schemathesis/specs/openapi/stateful/types.py +14 -0
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/specs/openapi/validation.py +1 -0
- schemathesis/stateful/__init__.py +35 -21
- schemathesis/stateful/config.py +97 -0
- schemathesis/stateful/context.py +135 -0
- schemathesis/stateful/events.py +274 -0
- schemathesis/stateful/runner.py +309 -0
- schemathesis/stateful/sink.py +68 -0
- schemathesis/stateful/state_machine.py +67 -38
- schemathesis/stateful/statistic.py +22 -0
- schemathesis/stateful/validation.py +100 -0
- schemathesis/targets.py +33 -1
- schemathesis/throttling.py +25 -5
- schemathesis/transports/__init__.py +354 -0
- schemathesis/transports/asgi.py +7 -0
- schemathesis/transports/auth.py +25 -2
- schemathesis/transports/content_types.py +3 -1
- schemathesis/transports/headers.py +2 -1
- schemathesis/transports/responses.py +9 -4
- schemathesis/types.py +9 -0
- schemathesis/utils.py +11 -16
- schemathesis-3.39.7.dist-info/METADATA +293 -0
- schemathesis-3.39.7.dist-info/RECORD +160 -0
- {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis-3.25.6.dist-info/METADATA +0 -356
- schemathesis-3.25.6.dist-info/RECORD +0 -134
- {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
schemathesis/runner/impl/core.py
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
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
|
|
7
9
|
import unittest
|
|
8
10
|
import uuid
|
|
11
|
+
import warnings
|
|
9
12
|
from contextlib import contextmanager
|
|
10
13
|
from dataclasses import dataclass, field
|
|
11
|
-
from types import TracebackType
|
|
12
14
|
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, List, Literal, cast
|
|
13
15
|
from warnings import WarningMessage, catch_warnings
|
|
14
16
|
|
|
@@ -19,9 +21,10 @@ from hypothesis.errors import HypothesisException, InvalidArgument
|
|
|
19
21
|
from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
|
|
20
22
|
from jsonschema.exceptions import SchemaError as JsonSchemaError
|
|
21
23
|
from jsonschema.exceptions import ValidationError
|
|
22
|
-
from requests.
|
|
24
|
+
from requests.structures import CaseInsensitiveDict
|
|
25
|
+
from urllib3.exceptions import InsecureRequestWarning
|
|
23
26
|
|
|
24
|
-
from ... import failures, hooks
|
|
27
|
+
from ... import experimental, failures, hooks
|
|
25
28
|
from ..._compat import MultipleFailures
|
|
26
29
|
from ..._hypothesis import (
|
|
27
30
|
get_invalid_example_headers_mark,
|
|
@@ -29,8 +32,8 @@ from ..._hypothesis import (
|
|
|
29
32
|
get_non_serializable_mark,
|
|
30
33
|
has_unsatisfied_example_mark,
|
|
31
34
|
)
|
|
32
|
-
from ..._override import CaseOverride
|
|
33
35
|
from ...auths import unregister as unregister_auth
|
|
36
|
+
from ...checks import _make_max_response_time_failure_message
|
|
34
37
|
from ...constants import (
|
|
35
38
|
DEFAULT_STATEFUL_RECURSION_LIMIT,
|
|
36
39
|
RECURSIVE_REFERENCE_ERROR_MESSAGE,
|
|
@@ -40,10 +43,12 @@ from ...constants import (
|
|
|
40
43
|
from ...exceptions import (
|
|
41
44
|
CheckFailed,
|
|
42
45
|
DeadlineExceeded,
|
|
46
|
+
InternalError,
|
|
43
47
|
InvalidHeadersExample,
|
|
44
48
|
InvalidRegularExpression,
|
|
45
49
|
NonCheckError,
|
|
46
50
|
OperationSchemaError,
|
|
51
|
+
RecursiveReferenceError,
|
|
47
52
|
SerializationNotPossible,
|
|
48
53
|
SkipTest,
|
|
49
54
|
format_exception,
|
|
@@ -52,21 +57,36 @@ from ...exceptions import (
|
|
|
52
57
|
)
|
|
53
58
|
from ...generation import DataGenerationMethod, GenerationConfig
|
|
54
59
|
from ...hooks import HookContext, get_all_by_name
|
|
60
|
+
from ...internal.checks import CheckConfig, CheckContext
|
|
55
61
|
from ...internal.datetime import current_datetime
|
|
56
|
-
from ...internal.result import Ok
|
|
57
|
-
from ...models import APIOperation, Case, Check,
|
|
62
|
+
from ...internal.result import Err, Ok, Result
|
|
63
|
+
from ...models import APIOperation, Case, Check, Status, TestResult
|
|
58
64
|
from ...runner import events
|
|
59
|
-
from ...
|
|
65
|
+
from ...service import extensions
|
|
66
|
+
from ...service.models import AnalysisResult, AnalysisSuccess
|
|
60
67
|
from ...specs.openapi import formats
|
|
61
68
|
from ...stateful import Feedback, Stateful
|
|
69
|
+
from ...stateful import events as stateful_events
|
|
70
|
+
from ...stateful import runner as stateful_runner
|
|
62
71
|
from ...targets import Target, TargetContext
|
|
63
|
-
from ...
|
|
72
|
+
from ...transports import RequestConfig, RequestsTransport
|
|
73
|
+
from ...transports.auth import get_requests_auth, prepare_wsgi_headers
|
|
64
74
|
from ...utils import capture_hypothesis_output
|
|
65
75
|
from .. import probes
|
|
66
76
|
from ..serialization import SerializedTestResult
|
|
77
|
+
from .context import RunnerContext
|
|
67
78
|
|
|
68
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
|
|
87
|
+
from ...service.client import ServiceClient
|
|
69
88
|
from ...transports.responses import GenericResponse, WSGIResponse
|
|
89
|
+
from ...types import RawAuth
|
|
70
90
|
|
|
71
91
|
|
|
72
92
|
def _should_count_towards_stop(event: events.ExecutionEvent) -> bool:
|
|
@@ -80,24 +100,29 @@ class BaseRunner:
|
|
|
80
100
|
max_response_time: int | None
|
|
81
101
|
targets: Iterable[Target]
|
|
82
102
|
hypothesis_settings: hypothesis.settings
|
|
83
|
-
generation_config: GenerationConfig
|
|
103
|
+
generation_config: GenerationConfig | None
|
|
84
104
|
probe_config: probes.ProbeConfig
|
|
105
|
+
checks_config: CheckConfig
|
|
106
|
+
request_config: RequestConfig = field(default_factory=RequestConfig)
|
|
85
107
|
override: CaseOverride | None = None
|
|
86
108
|
auth: RawAuth | None = None
|
|
87
109
|
auth_type: str | None = None
|
|
88
110
|
headers: dict[str, Any] | None = None
|
|
89
|
-
request_timeout: int | None = None
|
|
90
111
|
store_interactions: bool = False
|
|
91
112
|
seed: int | None = None
|
|
92
113
|
exit_first: bool = False
|
|
114
|
+
no_failfast: bool = False
|
|
93
115
|
max_failures: int | None = None
|
|
94
116
|
started_at: str = field(default_factory=current_datetime)
|
|
117
|
+
unique_data: bool = False
|
|
95
118
|
dry_run: bool = False
|
|
96
119
|
stateful: Stateful | None = None
|
|
97
120
|
stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT
|
|
98
121
|
count_operations: bool = True
|
|
99
122
|
count_links: bool = True
|
|
123
|
+
service_client: ServiceClient | None = None
|
|
100
124
|
_failures_counter: int = 0
|
|
125
|
+
_is_stopping_due_to_failure_limit: bool = False
|
|
101
126
|
|
|
102
127
|
def execute(self) -> EventStream:
|
|
103
128
|
"""Common logic for all runners."""
|
|
@@ -108,10 +133,19 @@ class BaseRunner:
|
|
|
108
133
|
# If auth is explicitly provided, then the global provider is ignored
|
|
109
134
|
if self.auth is not None:
|
|
110
135
|
unregister_auth()
|
|
111
|
-
|
|
136
|
+
ctx = RunnerContext(
|
|
137
|
+
auth=self.auth,
|
|
138
|
+
seed=self.seed,
|
|
139
|
+
stop_event=stop_event,
|
|
140
|
+
unique_data=self.unique_data,
|
|
141
|
+
checks_config=self.checks_config,
|
|
142
|
+
override=self.override,
|
|
143
|
+
no_failfast=self.no_failfast,
|
|
144
|
+
)
|
|
145
|
+
start_time = time.monotonic()
|
|
112
146
|
initialized = None
|
|
113
147
|
__probes = None
|
|
114
|
-
|
|
148
|
+
__analysis: Result[AnalysisResult, Exception] | None = None
|
|
115
149
|
|
|
116
150
|
def _initialize() -> events.Initialized:
|
|
117
151
|
nonlocal initialized
|
|
@@ -119,15 +153,15 @@ class BaseRunner:
|
|
|
119
153
|
schema=self.schema,
|
|
120
154
|
count_operations=self.count_operations,
|
|
121
155
|
count_links=self.count_links,
|
|
122
|
-
seed=
|
|
156
|
+
seed=ctx.seed,
|
|
123
157
|
start_time=start_time,
|
|
124
158
|
)
|
|
125
159
|
return initialized
|
|
126
160
|
|
|
127
161
|
def _finish() -> events.Finished:
|
|
128
|
-
if has_all_not_found
|
|
129
|
-
|
|
130
|
-
return events.Finished.from_results(results=
|
|
162
|
+
if ctx.has_all_not_found:
|
|
163
|
+
ctx.add_warning(ALL_NOT_FOUND_WARNING_MESSAGE)
|
|
164
|
+
return events.Finished.from_results(results=ctx.data, running_time=time.monotonic() - start_time)
|
|
131
165
|
|
|
132
166
|
def _before_probes() -> events.BeforeProbing:
|
|
133
167
|
return events.BeforeProbing()
|
|
@@ -142,7 +176,26 @@ class BaseRunner:
|
|
|
142
176
|
_probes = cast(List[probes.ProbeRun], __probes)
|
|
143
177
|
return events.AfterProbing(probes=_probes)
|
|
144
178
|
|
|
145
|
-
|
|
179
|
+
def _before_analysis() -> events.BeforeAnalysis:
|
|
180
|
+
return events.BeforeAnalysis()
|
|
181
|
+
|
|
182
|
+
def _run_analysis() -> None:
|
|
183
|
+
nonlocal __analysis, __probes
|
|
184
|
+
|
|
185
|
+
if self.service_client is not None:
|
|
186
|
+
try:
|
|
187
|
+
_probes = cast(List[probes.ProbeRun], __probes)
|
|
188
|
+
result = self.service_client.analyze_schema(_probes, self.schema.raw_schema)
|
|
189
|
+
if isinstance(result, AnalysisSuccess):
|
|
190
|
+
extensions.apply(result.extensions, self.schema)
|
|
191
|
+
__analysis = Ok(result)
|
|
192
|
+
except Exception as exc:
|
|
193
|
+
__analysis = Err(exc)
|
|
194
|
+
|
|
195
|
+
def _after_analysis() -> events.AfterAnalysis:
|
|
196
|
+
return events.AfterAnalysis(analysis=__analysis)
|
|
197
|
+
|
|
198
|
+
if ctx.is_stopped:
|
|
146
199
|
yield _finish()
|
|
147
200
|
return
|
|
148
201
|
|
|
@@ -151,22 +204,35 @@ class BaseRunner:
|
|
|
151
204
|
_before_probes,
|
|
152
205
|
_run_probes,
|
|
153
206
|
_after_probes,
|
|
207
|
+
_before_analysis,
|
|
208
|
+
_run_analysis,
|
|
209
|
+
_after_analysis,
|
|
154
210
|
):
|
|
155
211
|
event = event_factory()
|
|
156
212
|
if event is not None:
|
|
157
213
|
yield event
|
|
158
|
-
if
|
|
159
|
-
yield _finish()
|
|
214
|
+
if ctx.is_stopped:
|
|
215
|
+
yield _finish() # type: ignore[unreachable]
|
|
160
216
|
return
|
|
161
217
|
|
|
162
218
|
try:
|
|
163
|
-
|
|
219
|
+
warnings.simplefilter("ignore", InsecureRequestWarning)
|
|
220
|
+
if not experimental.STATEFUL_ONLY.is_enabled:
|
|
221
|
+
yield from self._execute(ctx)
|
|
222
|
+
if not self._is_stopping_due_to_failure_limit:
|
|
223
|
+
yield from self._run_stateful_tests(ctx)
|
|
164
224
|
except KeyboardInterrupt:
|
|
165
225
|
yield events.Interrupted()
|
|
166
226
|
|
|
167
227
|
yield _finish()
|
|
168
228
|
|
|
169
229
|
def _should_stop(self, event: events.ExecutionEvent) -> bool:
|
|
230
|
+
result = self.__should_stop(event)
|
|
231
|
+
if result:
|
|
232
|
+
self._is_stopping_due_to_failure_limit = True
|
|
233
|
+
return result
|
|
234
|
+
|
|
235
|
+
def __should_stop(self, event: events.ExecutionEvent) -> bool:
|
|
170
236
|
if _should_count_towards_stop(event):
|
|
171
237
|
if self.exit_first:
|
|
172
238
|
return True
|
|
@@ -175,19 +241,116 @@ class BaseRunner:
|
|
|
175
241
|
return self._failures_counter >= self.max_failures
|
|
176
242
|
return False
|
|
177
243
|
|
|
178
|
-
def _execute(
|
|
179
|
-
self, results: TestResultSet, stop_event: threading.Event
|
|
180
|
-
) -> Generator[events.ExecutionEvent, None, None]:
|
|
244
|
+
def _execute(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
|
|
181
245
|
raise NotImplementedError
|
|
182
246
|
|
|
247
|
+
def _run_stateful_tests(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
|
|
248
|
+
# Run new-style stateful tests
|
|
249
|
+
if self.stateful is not None and experimental.STATEFUL_TEST_RUNNER.is_enabled and self.schema.links_count > 0:
|
|
250
|
+
result = TestResult(
|
|
251
|
+
method="",
|
|
252
|
+
path="",
|
|
253
|
+
verbose_name="Stateful tests",
|
|
254
|
+
seed=ctx.seed,
|
|
255
|
+
data_generation_method=self.schema.data_generation_methods,
|
|
256
|
+
)
|
|
257
|
+
headers = self.headers or {}
|
|
258
|
+
if isinstance(self.schema.transport, RequestsTransport):
|
|
259
|
+
auth = get_requests_auth(self.auth, self.auth_type)
|
|
260
|
+
else:
|
|
261
|
+
auth = None
|
|
262
|
+
headers = prepare_wsgi_headers(headers, self.auth, self.auth_type)
|
|
263
|
+
config = stateful_runner.StatefulTestRunnerConfig(
|
|
264
|
+
checks=tuple(self.checks),
|
|
265
|
+
headers=headers,
|
|
266
|
+
hypothesis_settings=self.hypothesis_settings,
|
|
267
|
+
exit_first=self.exit_first,
|
|
268
|
+
max_failures=None if self.max_failures is None else self.max_failures - self._failures_counter,
|
|
269
|
+
request=self.request_config,
|
|
270
|
+
auth=auth,
|
|
271
|
+
seed=ctx.seed,
|
|
272
|
+
override=self.override,
|
|
273
|
+
)
|
|
274
|
+
state_machine = self.schema.as_state_machine()
|
|
275
|
+
runner = state_machine.runner(config=config)
|
|
276
|
+
status = Status.success
|
|
277
|
+
|
|
278
|
+
def from_step_status(step_status: stateful_events.StepStatus) -> Status:
|
|
279
|
+
return {
|
|
280
|
+
stateful_events.StepStatus.SUCCESS: Status.success,
|
|
281
|
+
stateful_events.StepStatus.FAILURE: Status.failure,
|
|
282
|
+
stateful_events.StepStatus.ERROR: Status.error,
|
|
283
|
+
stateful_events.StepStatus.INTERRUPTED: Status.error,
|
|
284
|
+
}[step_status]
|
|
285
|
+
|
|
286
|
+
if self.store_interactions:
|
|
287
|
+
if isinstance(state_machine.schema.transport, RequestsTransport):
|
|
288
|
+
|
|
289
|
+
def on_step_finished(event: stateful_events.StepFinished) -> None:
|
|
290
|
+
if event.response is not None and event.status is not None:
|
|
291
|
+
response = cast(requests.Response, event.response)
|
|
292
|
+
result.store_requests_response(
|
|
293
|
+
status=from_step_status(event.status),
|
|
294
|
+
case=event.case,
|
|
295
|
+
response=response,
|
|
296
|
+
checks=event.checks,
|
|
297
|
+
headers=headers,
|
|
298
|
+
session=None,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
else:
|
|
302
|
+
|
|
303
|
+
def on_step_finished(event: stateful_events.StepFinished) -> None:
|
|
304
|
+
from ...transports.responses import WSGIResponse
|
|
305
|
+
|
|
306
|
+
if event.response is not None and event.status is not None:
|
|
307
|
+
response = cast(WSGIResponse, event.response)
|
|
308
|
+
result.store_wsgi_response(
|
|
309
|
+
status=from_step_status(event.status),
|
|
310
|
+
case=event.case,
|
|
311
|
+
response=response,
|
|
312
|
+
headers=headers,
|
|
313
|
+
elapsed=response.elapsed.total_seconds(),
|
|
314
|
+
checks=event.checks,
|
|
315
|
+
)
|
|
316
|
+
else:
|
|
317
|
+
|
|
318
|
+
def on_step_finished(event: stateful_events.StepFinished) -> None:
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
test_start_time: float | None = None
|
|
322
|
+
test_elapsed_time: float | None = None
|
|
323
|
+
|
|
324
|
+
for stateful_event in runner.execute():
|
|
325
|
+
if isinstance(stateful_event, stateful_events.SuiteFinished):
|
|
326
|
+
if stateful_event.failures and status != Status.error:
|
|
327
|
+
status = Status.failure
|
|
328
|
+
elif isinstance(stateful_event, stateful_events.RunStarted):
|
|
329
|
+
test_start_time = stateful_event.timestamp
|
|
330
|
+
elif isinstance(stateful_event, stateful_events.RunFinished):
|
|
331
|
+
test_elapsed_time = stateful_event.timestamp - cast(float, test_start_time)
|
|
332
|
+
elif isinstance(stateful_event, stateful_events.StepFinished):
|
|
333
|
+
result.checks.extend(stateful_event.checks)
|
|
334
|
+
on_step_finished(stateful_event)
|
|
335
|
+
elif isinstance(stateful_event, stateful_events.Errored):
|
|
336
|
+
status = Status.error
|
|
337
|
+
result.add_error(stateful_event.exception)
|
|
338
|
+
yield events.StatefulEvent(data=stateful_event)
|
|
339
|
+
ctx.add_result(result)
|
|
340
|
+
yield events.AfterStatefulExecution(
|
|
341
|
+
status=status,
|
|
342
|
+
result=SerializedTestResult.from_test_result(result),
|
|
343
|
+
elapsed_time=cast(float, test_elapsed_time),
|
|
344
|
+
data_generation_method=self.schema.data_generation_methods,
|
|
345
|
+
)
|
|
346
|
+
|
|
183
347
|
def _run_tests(
|
|
184
348
|
self,
|
|
185
349
|
maker: Callable,
|
|
186
|
-
|
|
350
|
+
test_func: Callable,
|
|
187
351
|
settings: hypothesis.settings,
|
|
188
|
-
generation_config: GenerationConfig,
|
|
189
|
-
|
|
190
|
-
results: TestResultSet,
|
|
352
|
+
generation_config: GenerationConfig | None,
|
|
353
|
+
ctx: RunnerContext,
|
|
191
354
|
recursion_level: int = 0,
|
|
192
355
|
headers: dict[str, Any] | None = None,
|
|
193
356
|
**kwargs: Any,
|
|
@@ -207,15 +370,18 @@ class BaseRunner:
|
|
|
207
370
|
return kw
|
|
208
371
|
|
|
209
372
|
for result in maker(
|
|
210
|
-
|
|
373
|
+
test_func,
|
|
211
374
|
settings=settings,
|
|
212
375
|
generation_config=generation_config,
|
|
213
|
-
seed=seed,
|
|
376
|
+
seed=ctx.seed,
|
|
214
377
|
as_strategy_kwargs=as_strategy_kwargs,
|
|
215
378
|
):
|
|
216
379
|
if isinstance(result, Ok):
|
|
217
380
|
operation, test = result.ok()
|
|
218
|
-
|
|
381
|
+
if self.stateful is not None and not experimental.STATEFUL_TEST_RUNNER.is_enabled:
|
|
382
|
+
feedback = Feedback(self.stateful, operation)
|
|
383
|
+
else:
|
|
384
|
+
feedback = None
|
|
219
385
|
# Track whether `BeforeExecution` was already emitted.
|
|
220
386
|
# Schema error may happen before / after `BeforeExecution`, but it should be emitted only once
|
|
221
387
|
# and the `AfterExecution` event should have the same correlation id as previous `BeforeExecution`
|
|
@@ -224,7 +390,7 @@ class BaseRunner:
|
|
|
224
390
|
for event in run_test(
|
|
225
391
|
operation,
|
|
226
392
|
test,
|
|
227
|
-
|
|
393
|
+
ctx=ctx,
|
|
228
394
|
feedback=feedback,
|
|
229
395
|
recursion_level=recursion_level,
|
|
230
396
|
data_generation_methods=self.schema.data_generation_methods,
|
|
@@ -237,30 +403,28 @@ class BaseRunner:
|
|
|
237
403
|
if isinstance(event, events.Interrupted):
|
|
238
404
|
return
|
|
239
405
|
# Additional tests, generated via the `feedback` instance
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
406
|
+
if feedback is not None:
|
|
407
|
+
yield from self._run_tests(
|
|
408
|
+
feedback.get_stateful_tests,
|
|
409
|
+
test_func,
|
|
410
|
+
settings=settings,
|
|
411
|
+
generation_config=generation_config,
|
|
412
|
+
recursion_level=recursion_level + 1,
|
|
413
|
+
ctx=ctx,
|
|
414
|
+
headers=headers,
|
|
415
|
+
**kwargs,
|
|
416
|
+
)
|
|
251
417
|
except OperationSchemaError as exc:
|
|
252
418
|
yield from handle_schema_error(
|
|
253
419
|
exc,
|
|
254
|
-
|
|
420
|
+
ctx,
|
|
255
421
|
self.schema.data_generation_methods,
|
|
256
422
|
recursion_level,
|
|
257
423
|
before_execution_correlation_id=before_execution_correlation_id,
|
|
258
424
|
)
|
|
259
425
|
else:
|
|
260
426
|
# Schema errors
|
|
261
|
-
yield from handle_schema_error(
|
|
262
|
-
result.err(), results, self.schema.data_generation_methods, recursion_level
|
|
263
|
-
)
|
|
427
|
+
yield from handle_schema_error(result.err(), ctx, self.schema.data_generation_methods, recursion_level)
|
|
264
428
|
|
|
265
429
|
|
|
266
430
|
def run_probes(schema: BaseSchema, config: probes.ProbeConfig) -> list[probes.ProbeRun]:
|
|
@@ -268,12 +432,9 @@ def run_probes(schema: BaseSchema, config: probes.ProbeConfig) -> list[probes.Pr
|
|
|
268
432
|
results = probes.run(schema, config)
|
|
269
433
|
for result in results:
|
|
270
434
|
if isinstance(result.probe, probes.NullByteInHeader) and result.is_failure:
|
|
271
|
-
from ...specs.openapi.
|
|
435
|
+
from ...specs.openapi.formats import HEADER_FORMAT, header_values
|
|
272
436
|
|
|
273
|
-
formats.register(
|
|
274
|
-
HEADER_FORMAT,
|
|
275
|
-
header_values(blacklist_characters="\n\r\x00").map(str.lstrip),
|
|
276
|
-
)
|
|
437
|
+
formats.register(HEADER_FORMAT, header_values(blacklist_characters="\n\r\x00"))
|
|
277
438
|
return results
|
|
278
439
|
|
|
279
440
|
|
|
@@ -308,7 +469,7 @@ class EventStream:
|
|
|
308
469
|
|
|
309
470
|
def handle_schema_error(
|
|
310
471
|
error: OperationSchemaError,
|
|
311
|
-
|
|
472
|
+
ctx: RunnerContext,
|
|
312
473
|
data_generation_methods: Iterable[DataGenerationMethod],
|
|
313
474
|
recursion_level: int,
|
|
314
475
|
*,
|
|
@@ -353,11 +514,11 @@ def handle_schema_error(
|
|
|
353
514
|
hypothesis_output=[],
|
|
354
515
|
correlation_id=correlation_id,
|
|
355
516
|
)
|
|
356
|
-
|
|
517
|
+
ctx.add_result(result)
|
|
357
518
|
else:
|
|
358
519
|
# When there is no `method`, then the schema error may cover multiple operations, and we can't display it in
|
|
359
520
|
# the progress bar
|
|
360
|
-
|
|
521
|
+
ctx.add_generic_error(error)
|
|
361
522
|
|
|
362
523
|
|
|
363
524
|
def run_test(
|
|
@@ -366,7 +527,7 @@ def run_test(
|
|
|
366
527
|
checks: Iterable[CheckFunction],
|
|
367
528
|
data_generation_methods: Iterable[DataGenerationMethod],
|
|
368
529
|
targets: Iterable[Target],
|
|
369
|
-
|
|
530
|
+
ctx: RunnerContext,
|
|
370
531
|
headers: dict[str, Any] | None,
|
|
371
532
|
recursion_level: int,
|
|
372
533
|
**kwargs: Any,
|
|
@@ -391,12 +552,35 @@ def run_test(
|
|
|
391
552
|
errors: list[Exception] = []
|
|
392
553
|
test_start_time = time.monotonic()
|
|
393
554
|
setup_hypothesis_database_key(test, operation)
|
|
555
|
+
|
|
556
|
+
def _on_flaky(exc: Exception) -> Status:
|
|
557
|
+
if isinstance(exc.__cause__, hypothesis.errors.DeadlineExceeded):
|
|
558
|
+
status = Status.error
|
|
559
|
+
result.add_error(DeadlineExceeded.from_exc(exc.__cause__))
|
|
560
|
+
elif (
|
|
561
|
+
hasattr(hypothesis.errors, "FlakyFailure")
|
|
562
|
+
and isinstance(exc, hypothesis.errors.FlakyFailure)
|
|
563
|
+
and any(isinstance(subexc, hypothesis.errors.DeadlineExceeded) for subexc in exc.exceptions)
|
|
564
|
+
):
|
|
565
|
+
for sub_exc in exc.exceptions:
|
|
566
|
+
if isinstance(sub_exc, hypothesis.errors.DeadlineExceeded):
|
|
567
|
+
result.add_error(DeadlineExceeded.from_exc(sub_exc))
|
|
568
|
+
status = Status.error
|
|
569
|
+
elif errors:
|
|
570
|
+
status = Status.error
|
|
571
|
+
add_errors(result, errors)
|
|
572
|
+
else:
|
|
573
|
+
status = Status.failure
|
|
574
|
+
result.mark_flaky()
|
|
575
|
+
return status
|
|
576
|
+
|
|
394
577
|
try:
|
|
395
578
|
with catch_warnings(record=True) as warnings, capture_hypothesis_output() as hypothesis_output:
|
|
396
579
|
test(
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
580
|
+
ctx=ctx,
|
|
581
|
+
checks=checks,
|
|
582
|
+
targets=targets,
|
|
583
|
+
result=result,
|
|
400
584
|
errors=errors,
|
|
401
585
|
headers=headers,
|
|
402
586
|
data_generation_methods=data_generation_methods,
|
|
@@ -420,6 +604,8 @@ def run_test(
|
|
|
420
604
|
result.mark_errored()
|
|
421
605
|
for error in deduplicate_errors(errors):
|
|
422
606
|
result.add_error(error)
|
|
607
|
+
except hypothesis.errors.Flaky as exc:
|
|
608
|
+
status = _on_flaky(exc)
|
|
423
609
|
except MultipleFailures:
|
|
424
610
|
# Schemathesis may detect multiple errors that come from different check results
|
|
425
611
|
# They raise different "grouped" exceptions
|
|
@@ -428,16 +614,6 @@ def run_test(
|
|
|
428
614
|
add_errors(result, errors)
|
|
429
615
|
else:
|
|
430
616
|
status = Status.failure
|
|
431
|
-
except hypothesis.errors.Flaky as exc:
|
|
432
|
-
if isinstance(exc.__cause__, hypothesis.errors.DeadlineExceeded):
|
|
433
|
-
status = Status.error
|
|
434
|
-
result.add_error(DeadlineExceeded.from_exc(exc.__cause__))
|
|
435
|
-
elif errors:
|
|
436
|
-
status = Status.error
|
|
437
|
-
add_errors(result, errors)
|
|
438
|
-
else:
|
|
439
|
-
status = Status.failure
|
|
440
|
-
result.mark_flaky()
|
|
441
617
|
except hypothesis.errors.Unsatisfiable:
|
|
442
618
|
# We need more clear error message here
|
|
443
619
|
status = Status.error
|
|
@@ -448,13 +624,29 @@ def run_test(
|
|
|
448
624
|
except SkipTest as exc:
|
|
449
625
|
status = Status.skip
|
|
450
626
|
result.mark_skipped(exc)
|
|
451
|
-
except AssertionError: #
|
|
452
|
-
error = reraise(operation)
|
|
627
|
+
except AssertionError as exc: # May come from `hypothesis-jsonschema` or `hypothesis`
|
|
453
628
|
status = Status.error
|
|
629
|
+
try:
|
|
630
|
+
operation.schema.validate()
|
|
631
|
+
msg = "Unexpected error during testing of this API operation"
|
|
632
|
+
exc_msg = str(exc)
|
|
633
|
+
if exc_msg:
|
|
634
|
+
msg += f": {exc_msg}"
|
|
635
|
+
try:
|
|
636
|
+
raise InternalError(msg) from exc
|
|
637
|
+
except InternalError as exc:
|
|
638
|
+
error = exc
|
|
639
|
+
except ValidationError as exc:
|
|
640
|
+
error = OperationSchemaError.from_jsonschema_error(
|
|
641
|
+
exc,
|
|
642
|
+
path=operation.path,
|
|
643
|
+
method=operation.method,
|
|
644
|
+
full_path=operation.schema.get_full_path(operation.path),
|
|
645
|
+
)
|
|
454
646
|
result.add_error(error)
|
|
455
647
|
except HypothesisRefResolutionError:
|
|
456
648
|
status = Status.error
|
|
457
|
-
result.add_error(
|
|
649
|
+
result.add_error(RecursiveReferenceError(RECURSIVE_REFERENCE_ERROR_MESSAGE))
|
|
458
650
|
except InvalidArgument as error:
|
|
459
651
|
status = Status.error
|
|
460
652
|
message = get_invalid_regular_expression_message(warnings)
|
|
@@ -482,6 +674,8 @@ def run_test(
|
|
|
482
674
|
)
|
|
483
675
|
else:
|
|
484
676
|
result.add_error(error)
|
|
677
|
+
if status == Status.success and ctx.no_failfast and any(check.value == Status.failure for check in result.checks):
|
|
678
|
+
status = Status.failure
|
|
485
679
|
if has_unsatisfied_example_mark(test):
|
|
486
680
|
status = Status.error
|
|
487
681
|
result.add_error(
|
|
@@ -513,10 +707,10 @@ def run_test(
|
|
|
513
707
|
result.seed = getattr(test, "_hypothesis_internal_use_seed", None) or getattr(
|
|
514
708
|
test, "_hypothesis_internal_use_generated_seed", None
|
|
515
709
|
)
|
|
516
|
-
|
|
710
|
+
ctx.add_result(result)
|
|
517
711
|
for status_code in (401, 403):
|
|
518
712
|
if has_too_many_responses_with_status(result, status_code):
|
|
519
|
-
|
|
713
|
+
ctx.add_warning(TOO_MANY_RESPONSES_WARNING_TEMPLATE.format(f"`{operation.verbose_name}`", status_code))
|
|
520
714
|
yield events.AfterExecution.from_result(
|
|
521
715
|
result=result,
|
|
522
716
|
status=status,
|
|
@@ -551,22 +745,6 @@ def has_too_many_responses_with_status(result: TestResult, status_code: int) ->
|
|
|
551
745
|
ALL_NOT_FOUND_WARNING_MESSAGE = "All API responses have a 404 status code. Did you specify the proper API location?"
|
|
552
746
|
|
|
553
747
|
|
|
554
|
-
def has_all_not_found(results: TestResultSet) -> bool:
|
|
555
|
-
"""Check if all responses are 404."""
|
|
556
|
-
has_not_found = False
|
|
557
|
-
for result in results.results:
|
|
558
|
-
for check in result.checks:
|
|
559
|
-
if check.response is not None:
|
|
560
|
-
if check.response.status_code == 404:
|
|
561
|
-
has_not_found = True
|
|
562
|
-
else:
|
|
563
|
-
# There are non-404 responses, no reason to check any other response
|
|
564
|
-
return False
|
|
565
|
-
# Only happens if all responses are 404, or there are no responses at all.
|
|
566
|
-
# In the first case, it returns True, for the latter - False
|
|
567
|
-
return has_not_found
|
|
568
|
-
|
|
569
|
-
|
|
570
748
|
def setup_hypothesis_database_key(test: Callable, operation: APIOperation) -> None:
|
|
571
749
|
"""Make Hypothesis use separate database entries for every API operation.
|
|
572
750
|
|
|
@@ -575,7 +753,7 @@ def setup_hypothesis_database_key(test: Callable, operation: APIOperation) -> No
|
|
|
575
753
|
# Hypothesis's function digest depends on the test function signature. To reflect it for the web API case,
|
|
576
754
|
# we use all API operation parameters in the digest.
|
|
577
755
|
extra = operation.verbose_name.encode("utf8")
|
|
578
|
-
for parameter in operation.
|
|
756
|
+
for parameter in operation.iter_parameters():
|
|
579
757
|
extra += parameter.serialize(operation).encode("utf8")
|
|
580
758
|
test.hypothesis.inner_test._hypothesis_internal_add_digest = extra # type: ignore
|
|
581
759
|
|
|
@@ -588,16 +766,6 @@ def get_invalid_regular_expression_message(warnings: list[WarningMessage]) -> st
|
|
|
588
766
|
return None
|
|
589
767
|
|
|
590
768
|
|
|
591
|
-
def reraise(operation: APIOperation) -> OperationSchemaError:
|
|
592
|
-
try:
|
|
593
|
-
operation.schema.validate()
|
|
594
|
-
except ValidationError as exc:
|
|
595
|
-
return OperationSchemaError.from_jsonschema_error(
|
|
596
|
-
exc, path=operation.path, method=operation.method, full_path=operation.schema.get_full_path(operation.path)
|
|
597
|
-
)
|
|
598
|
-
return OperationSchemaError("Unknown schema error")
|
|
599
|
-
|
|
600
|
-
|
|
601
769
|
MEMORY_ADDRESS_RE = re.compile("0x[0-9a-fA-F]+")
|
|
602
770
|
URL_IN_ERROR_MESSAGE_RE = re.compile(r"Max retries exceeded with url: .*? \(Caused by")
|
|
603
771
|
|
|
@@ -613,7 +781,9 @@ def group_errors(errors: list[Exception]) -> None:
|
|
|
613
781
|
serialization_errors = [error for error in errors if isinstance(error, SerializationNotPossible)]
|
|
614
782
|
if len(serialization_errors) > 1:
|
|
615
783
|
errors[:] = [error for error in errors if not isinstance(error, SerializationNotPossible)]
|
|
616
|
-
media_types =
|
|
784
|
+
media_types: list[str] = functools.reduce(
|
|
785
|
+
operator.iadd, (entry.media_types for entry in serialization_errors), []
|
|
786
|
+
)
|
|
617
787
|
errors.append(SerializationNotPossible.from_media_types(*media_types))
|
|
618
788
|
|
|
619
789
|
|
|
@@ -638,12 +808,14 @@ def deduplicate_errors(errors: list[Exception]) -> Generator[Exception, None, No
|
|
|
638
808
|
def run_checks(
|
|
639
809
|
*,
|
|
640
810
|
case: Case,
|
|
811
|
+
ctx: CheckContext,
|
|
641
812
|
checks: Iterable[CheckFunction],
|
|
642
813
|
check_results: list[Check],
|
|
643
814
|
result: TestResult,
|
|
644
815
|
response: GenericResponse,
|
|
645
816
|
elapsed_time: float,
|
|
646
817
|
max_response_time: int | None = None,
|
|
818
|
+
no_failfast: bool,
|
|
647
819
|
) -> None:
|
|
648
820
|
errors = []
|
|
649
821
|
|
|
@@ -660,7 +832,7 @@ def run_checks(
|
|
|
660
832
|
check_name = check.__name__
|
|
661
833
|
copied_case = case.partial_deepcopy()
|
|
662
834
|
try:
|
|
663
|
-
skip_check = check(response, copied_case)
|
|
835
|
+
skip_check = check(ctx, response, copied_case)
|
|
664
836
|
if not skip_check:
|
|
665
837
|
check_result = result.add_success(check_name, copied_case, response, elapsed_time)
|
|
666
838
|
check_results.append(check_result)
|
|
@@ -672,7 +844,7 @@ def run_checks(
|
|
|
672
844
|
|
|
673
845
|
if max_response_time:
|
|
674
846
|
if elapsed_time > max_response_time:
|
|
675
|
-
message =
|
|
847
|
+
message = _make_max_response_time_failure_message(elapsed_time, max_response_time)
|
|
676
848
|
errors.append(AssertionError(message))
|
|
677
849
|
result.add_failure(
|
|
678
850
|
"max_response_time",
|
|
@@ -685,7 +857,7 @@ def run_checks(
|
|
|
685
857
|
else:
|
|
686
858
|
result.add_success("max_response_time", case, response, elapsed_time)
|
|
687
859
|
|
|
688
|
-
if errors:
|
|
860
|
+
if errors and not no_failfast:
|
|
689
861
|
raise get_grouped_exception(case.operation.verbose_name, *errors)(causes=tuple(errors))
|
|
690
862
|
|
|
691
863
|
|
|
@@ -746,19 +918,42 @@ def _force_data_generation_method(values: list[DataGenerationMethod], case: Case
|
|
|
746
918
|
values[:] = [data_generation_method]
|
|
747
919
|
|
|
748
920
|
|
|
921
|
+
def cached_test_func(f: Callable) -> Callable:
|
|
922
|
+
def wrapped(*, ctx: RunnerContext, case: Case, **kwargs: Any) -> None:
|
|
923
|
+
if ctx.unique_data:
|
|
924
|
+
cached = ctx.get_cached_outcome(case)
|
|
925
|
+
if isinstance(cached, BaseException):
|
|
926
|
+
raise cached
|
|
927
|
+
elif cached is None:
|
|
928
|
+
return None
|
|
929
|
+
try:
|
|
930
|
+
f(ctx=ctx, case=case, **kwargs)
|
|
931
|
+
except BaseException as exc:
|
|
932
|
+
ctx.cache_outcome(case, exc)
|
|
933
|
+
raise
|
|
934
|
+
else:
|
|
935
|
+
ctx.cache_outcome(case, None)
|
|
936
|
+
else:
|
|
937
|
+
f(ctx=ctx, case=case, **kwargs)
|
|
938
|
+
|
|
939
|
+
wrapped.__name__ = f.__name__
|
|
940
|
+
|
|
941
|
+
return wrapped
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
@cached_test_func
|
|
749
945
|
def network_test(
|
|
946
|
+
*,
|
|
947
|
+
ctx: RunnerContext,
|
|
750
948
|
case: Case,
|
|
751
949
|
checks: Iterable[CheckFunction],
|
|
752
950
|
targets: Iterable[Target],
|
|
753
951
|
result: TestResult,
|
|
754
952
|
session: requests.Session,
|
|
755
|
-
|
|
756
|
-
request_tls_verify: bool,
|
|
757
|
-
request_proxy: str | None,
|
|
758
|
-
request_cert: RequestCert | None,
|
|
953
|
+
request_config: RequestConfig,
|
|
759
954
|
store_interactions: bool,
|
|
760
955
|
headers: dict[str, Any] | None,
|
|
761
|
-
feedback: Feedback,
|
|
956
|
+
feedback: Feedback | None,
|
|
762
957
|
max_response_time: int | None,
|
|
763
958
|
data_generation_methods: list[DataGenerationMethod],
|
|
764
959
|
dry_run: bool,
|
|
@@ -771,85 +966,97 @@ def network_test(
|
|
|
771
966
|
headers = headers or {}
|
|
772
967
|
if "user-agent" not in {header.lower() for header in headers}:
|
|
773
968
|
headers["User-Agent"] = USER_AGENT
|
|
774
|
-
timeout = prepare_timeout(request_timeout)
|
|
775
969
|
if not dry_run:
|
|
776
970
|
args = (
|
|
971
|
+
ctx,
|
|
777
972
|
checks,
|
|
778
973
|
targets,
|
|
779
974
|
result,
|
|
780
975
|
session,
|
|
781
|
-
|
|
976
|
+
request_config,
|
|
782
977
|
store_interactions,
|
|
783
978
|
headers,
|
|
784
979
|
feedback,
|
|
785
|
-
request_tls_verify,
|
|
786
|
-
request_proxy,
|
|
787
|
-
request_cert,
|
|
788
980
|
max_response_time,
|
|
789
981
|
)
|
|
790
982
|
response = _network_test(case, *args)
|
|
791
983
|
add_cases(case, response, _network_test, *args)
|
|
984
|
+
elif store_interactions:
|
|
985
|
+
result.store_requests_response(case, None, Status.skip, [], headers=headers, session=session)
|
|
792
986
|
|
|
793
987
|
|
|
794
988
|
def _network_test(
|
|
795
989
|
case: Case,
|
|
990
|
+
ctx: RunnerContext,
|
|
796
991
|
checks: Iterable[CheckFunction],
|
|
797
992
|
targets: Iterable[Target],
|
|
798
993
|
result: TestResult,
|
|
799
994
|
session: requests.Session,
|
|
800
|
-
|
|
995
|
+
request_config: RequestConfig,
|
|
801
996
|
store_interactions: bool,
|
|
802
997
|
headers: dict[str, Any] | None,
|
|
803
|
-
feedback: Feedback,
|
|
804
|
-
request_tls_verify: bool,
|
|
805
|
-
request_proxy: str | None,
|
|
806
|
-
request_cert: RequestCert | None,
|
|
998
|
+
feedback: Feedback | None,
|
|
807
999
|
max_response_time: int | None,
|
|
808
1000
|
) -> requests.Response:
|
|
809
1001
|
check_results: list[Check] = []
|
|
1002
|
+
hook_context = HookContext(operation=case.operation)
|
|
1003
|
+
kwargs: dict[str, Any] = {
|
|
1004
|
+
"session": session,
|
|
1005
|
+
"headers": headers,
|
|
1006
|
+
"timeout": request_config.prepared_timeout,
|
|
1007
|
+
"verify": request_config.tls_verify,
|
|
1008
|
+
"cert": request_config.cert,
|
|
1009
|
+
}
|
|
1010
|
+
if request_config.proxy is not None:
|
|
1011
|
+
kwargs["proxies"] = {"all": request_config.proxy}
|
|
1012
|
+
hooks.dispatch("process_call_kwargs", hook_context, case, kwargs)
|
|
810
1013
|
try:
|
|
811
|
-
hook_context = HookContext(operation=case.operation)
|
|
812
|
-
kwargs: dict[str, Any] = {
|
|
813
|
-
"session": session,
|
|
814
|
-
"headers": headers,
|
|
815
|
-
"timeout": timeout,
|
|
816
|
-
"verify": request_tls_verify,
|
|
817
|
-
"cert": request_cert,
|
|
818
|
-
}
|
|
819
|
-
if request_proxy is not None:
|
|
820
|
-
kwargs["proxies"] = {"all": request_proxy}
|
|
821
|
-
hooks.dispatch("process_call_kwargs", hook_context, case, kwargs)
|
|
822
1014
|
response = case.call(**kwargs)
|
|
823
1015
|
except CheckFailed as exc:
|
|
824
1016
|
check_name = "request_timeout"
|
|
825
|
-
requests_kwargs =
|
|
1017
|
+
requests_kwargs = RequestsTransport().serialize_case(case, base_url=case.get_full_base_url(), headers=headers)
|
|
826
1018
|
request = requests.Request(**requests_kwargs).prepare()
|
|
827
|
-
elapsed = cast(
|
|
1019
|
+
elapsed = cast(
|
|
1020
|
+
float, request_config.prepared_timeout
|
|
1021
|
+
) # It is defined and not empty, since the exception happened
|
|
828
1022
|
check_result = result.add_failure(
|
|
829
1023
|
check_name, case, None, elapsed, f"Response timed out after {1000 * elapsed:.2f}ms", exc.context, request
|
|
830
1024
|
)
|
|
831
1025
|
check_results.append(check_result)
|
|
1026
|
+
if store_interactions:
|
|
1027
|
+
result.store_requests_response(case, None, Status.failure, [check_result], headers=headers, session=session)
|
|
832
1028
|
raise exc
|
|
833
1029
|
context = TargetContext(case=case, response=response, response_time=response.elapsed.total_seconds())
|
|
834
1030
|
run_targets(targets, context)
|
|
835
1031
|
status = Status.success
|
|
1032
|
+
|
|
1033
|
+
check_ctx = CheckContext(
|
|
1034
|
+
override=ctx.override,
|
|
1035
|
+
auth=ctx.auth,
|
|
1036
|
+
headers=CaseInsensitiveDict(headers) if headers else None,
|
|
1037
|
+
config=ctx.checks_config,
|
|
1038
|
+
transport_kwargs=kwargs,
|
|
1039
|
+
)
|
|
836
1040
|
try:
|
|
837
1041
|
run_checks(
|
|
838
1042
|
case=case,
|
|
1043
|
+
ctx=check_ctx,
|
|
839
1044
|
checks=checks,
|
|
840
1045
|
check_results=check_results,
|
|
841
1046
|
result=result,
|
|
842
1047
|
response=response,
|
|
843
1048
|
elapsed_time=context.response_time * 1000,
|
|
844
1049
|
max_response_time=max_response_time,
|
|
1050
|
+
no_failfast=ctx.no_failfast,
|
|
845
1051
|
)
|
|
846
1052
|
except CheckFailed:
|
|
847
1053
|
status = Status.failure
|
|
848
1054
|
raise
|
|
849
1055
|
finally:
|
|
850
|
-
feedback
|
|
1056
|
+
if feedback is not None:
|
|
1057
|
+
feedback.add_test_case(case, response)
|
|
851
1058
|
if store_interactions:
|
|
852
|
-
result.store_requests_response(case, response, status, check_results)
|
|
1059
|
+
result.store_requests_response(case, response, status, check_results, headers=headers, session=session)
|
|
853
1060
|
return response
|
|
854
1061
|
|
|
855
1062
|
|
|
@@ -861,15 +1068,9 @@ def get_session(auth: HTTPDigestAuth | RawAuth | None = None) -> Generator[reque
|
|
|
861
1068
|
yield session
|
|
862
1069
|
|
|
863
1070
|
|
|
864
|
-
|
|
865
|
-
"""Request timeout is in milliseconds, but `requests` uses seconds."""
|
|
866
|
-
output: int | float | None = timeout
|
|
867
|
-
if timeout is not None:
|
|
868
|
-
output = timeout / 1000
|
|
869
|
-
return output
|
|
870
|
-
|
|
871
|
-
|
|
1071
|
+
@cached_test_func
|
|
872
1072
|
def wsgi_test(
|
|
1073
|
+
ctx: RunnerContext,
|
|
873
1074
|
case: Case,
|
|
874
1075
|
checks: Iterable[CheckFunction],
|
|
875
1076
|
targets: Iterable[Target],
|
|
@@ -878,7 +1079,7 @@ def wsgi_test(
|
|
|
878
1079
|
auth_type: str | None,
|
|
879
1080
|
headers: dict[str, Any] | None,
|
|
880
1081
|
store_interactions: bool,
|
|
881
|
-
feedback: Feedback,
|
|
1082
|
+
feedback: Feedback | None,
|
|
882
1083
|
max_response_time: int | None,
|
|
883
1084
|
data_generation_methods: list[DataGenerationMethod],
|
|
884
1085
|
dry_run: bool,
|
|
@@ -887,9 +1088,10 @@ def wsgi_test(
|
|
|
887
1088
|
with ErrorCollector(errors):
|
|
888
1089
|
_force_data_generation_method(data_generation_methods, case)
|
|
889
1090
|
result.mark_executed()
|
|
890
|
-
headers =
|
|
1091
|
+
headers = prepare_wsgi_headers(headers, auth, auth_type)
|
|
891
1092
|
if not dry_run:
|
|
892
1093
|
args = (
|
|
1094
|
+
ctx,
|
|
893
1095
|
checks,
|
|
894
1096
|
targets,
|
|
895
1097
|
result,
|
|
@@ -900,78 +1102,73 @@ def wsgi_test(
|
|
|
900
1102
|
)
|
|
901
1103
|
response = _wsgi_test(case, *args)
|
|
902
1104
|
add_cases(case, response, _wsgi_test, *args)
|
|
1105
|
+
elif store_interactions:
|
|
1106
|
+
result.store_wsgi_response(case, None, headers, None, Status.skip, [])
|
|
903
1107
|
|
|
904
1108
|
|
|
905
1109
|
def _wsgi_test(
|
|
906
1110
|
case: Case,
|
|
1111
|
+
ctx: RunnerContext,
|
|
907
1112
|
checks: Iterable[CheckFunction],
|
|
908
1113
|
targets: Iterable[Target],
|
|
909
1114
|
result: TestResult,
|
|
910
1115
|
headers: dict[str, Any],
|
|
911
1116
|
store_interactions: bool,
|
|
912
|
-
feedback: Feedback,
|
|
1117
|
+
feedback: Feedback | None,
|
|
913
1118
|
max_response_time: int | None,
|
|
914
1119
|
) -> WSGIResponse:
|
|
1120
|
+
from ...transports.responses import WSGIResponse
|
|
1121
|
+
|
|
915
1122
|
with catching_logs(LogCaptureHandler(), level=logging.DEBUG) as recorded:
|
|
916
|
-
start = time.monotonic()
|
|
917
1123
|
hook_context = HookContext(operation=case.operation)
|
|
918
|
-
kwargs = {"headers": headers}
|
|
1124
|
+
kwargs: dict[str, Any] = {"headers": headers}
|
|
919
1125
|
hooks.dispatch("process_call_kwargs", hook_context, case, kwargs)
|
|
920
|
-
response = case.
|
|
921
|
-
|
|
922
|
-
context = TargetContext(case=case, response=response, response_time=elapsed)
|
|
1126
|
+
response = cast(WSGIResponse, case.call(**kwargs))
|
|
1127
|
+
context = TargetContext(case=case, response=response, response_time=response.elapsed.total_seconds())
|
|
923
1128
|
run_targets(targets, context)
|
|
924
1129
|
result.logs.extend(recorded.records)
|
|
925
1130
|
status = Status.success
|
|
926
1131
|
check_results: list[Check] = []
|
|
1132
|
+
check_ctx = CheckContext(
|
|
1133
|
+
override=ctx.override,
|
|
1134
|
+
auth=ctx.auth,
|
|
1135
|
+
headers=CaseInsensitiveDict(headers) if headers else None,
|
|
1136
|
+
config=ctx.checks_config,
|
|
1137
|
+
transport_kwargs=kwargs,
|
|
1138
|
+
)
|
|
927
1139
|
try:
|
|
928
1140
|
run_checks(
|
|
929
1141
|
case=case,
|
|
1142
|
+
ctx=check_ctx,
|
|
930
1143
|
checks=checks,
|
|
931
1144
|
check_results=check_results,
|
|
932
1145
|
result=result,
|
|
933
1146
|
response=response,
|
|
934
1147
|
elapsed_time=context.response_time * 1000,
|
|
935
1148
|
max_response_time=max_response_time,
|
|
1149
|
+
no_failfast=ctx.no_failfast,
|
|
936
1150
|
)
|
|
937
1151
|
except CheckFailed:
|
|
938
1152
|
status = Status.failure
|
|
939
1153
|
raise
|
|
940
1154
|
finally:
|
|
941
|
-
feedback
|
|
1155
|
+
if feedback is not None:
|
|
1156
|
+
feedback.add_test_case(case, response)
|
|
942
1157
|
if store_interactions:
|
|
943
|
-
result.store_wsgi_response(case, response, headers, elapsed, status, check_results)
|
|
1158
|
+
result.store_wsgi_response(case, response, headers, response.elapsed.total_seconds(), status, check_results)
|
|
944
1159
|
return response
|
|
945
1160
|
|
|
946
1161
|
|
|
947
|
-
|
|
948
|
-
headers: dict[str, Any] | None, auth: RawAuth | None, auth_type: str | None
|
|
949
|
-
) -> dict[str, Any]:
|
|
950
|
-
headers = headers or {}
|
|
951
|
-
if "user-agent" not in {header.lower() for header in headers}:
|
|
952
|
-
headers["User-Agent"] = USER_AGENT
|
|
953
|
-
wsgi_auth = get_wsgi_auth(auth, auth_type)
|
|
954
|
-
if wsgi_auth:
|
|
955
|
-
headers["Authorization"] = wsgi_auth
|
|
956
|
-
return headers
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
def get_wsgi_auth(auth: RawAuth | None, auth_type: str | None) -> str | None:
|
|
960
|
-
if auth:
|
|
961
|
-
if auth_type == "digest":
|
|
962
|
-
raise ValueError("Digest auth is not supported for WSGI apps")
|
|
963
|
-
return _basic_auth_str(*auth)
|
|
964
|
-
return None
|
|
965
|
-
|
|
966
|
-
|
|
1162
|
+
@cached_test_func
|
|
967
1163
|
def asgi_test(
|
|
1164
|
+
ctx: RunnerContext,
|
|
968
1165
|
case: Case,
|
|
969
1166
|
checks: Iterable[CheckFunction],
|
|
970
1167
|
targets: Iterable[Target],
|
|
971
1168
|
result: TestResult,
|
|
972
1169
|
store_interactions: bool,
|
|
973
1170
|
headers: dict[str, Any] | None,
|
|
974
|
-
feedback: Feedback,
|
|
1171
|
+
feedback: Feedback | None,
|
|
975
1172
|
max_response_time: int | None,
|
|
976
1173
|
data_generation_methods: list[DataGenerationMethod],
|
|
977
1174
|
dry_run: bool,
|
|
@@ -985,6 +1182,7 @@ def asgi_test(
|
|
|
985
1182
|
|
|
986
1183
|
if not dry_run:
|
|
987
1184
|
args = (
|
|
1185
|
+
ctx,
|
|
988
1186
|
checks,
|
|
989
1187
|
targets,
|
|
990
1188
|
result,
|
|
@@ -995,41 +1193,54 @@ def asgi_test(
|
|
|
995
1193
|
)
|
|
996
1194
|
response = _asgi_test(case, *args)
|
|
997
1195
|
add_cases(case, response, _asgi_test, *args)
|
|
1196
|
+
elif store_interactions:
|
|
1197
|
+
result.store_requests_response(case, None, Status.skip, [], headers=headers, session=None)
|
|
998
1198
|
|
|
999
1199
|
|
|
1000
1200
|
def _asgi_test(
|
|
1001
1201
|
case: Case,
|
|
1202
|
+
ctx: RunnerContext,
|
|
1002
1203
|
checks: Iterable[CheckFunction],
|
|
1003
1204
|
targets: Iterable[Target],
|
|
1004
1205
|
result: TestResult,
|
|
1005
1206
|
store_interactions: bool,
|
|
1006
1207
|
headers: dict[str, Any] | None,
|
|
1007
|
-
feedback: Feedback,
|
|
1208
|
+
feedback: Feedback | None,
|
|
1008
1209
|
max_response_time: int | None,
|
|
1009
1210
|
) -> requests.Response:
|
|
1010
1211
|
hook_context = HookContext(operation=case.operation)
|
|
1011
1212
|
kwargs: dict[str, Any] = {"headers": headers}
|
|
1012
1213
|
hooks.dispatch("process_call_kwargs", hook_context, case, kwargs)
|
|
1013
|
-
response = case.
|
|
1214
|
+
response = case.call(**kwargs)
|
|
1014
1215
|
context = TargetContext(case=case, response=response, response_time=response.elapsed.total_seconds())
|
|
1015
1216
|
run_targets(targets, context)
|
|
1016
1217
|
status = Status.success
|
|
1017
1218
|
check_results: list[Check] = []
|
|
1219
|
+
check_ctx = CheckContext(
|
|
1220
|
+
override=ctx.override,
|
|
1221
|
+
auth=ctx.auth,
|
|
1222
|
+
headers=CaseInsensitiveDict(headers) if headers else None,
|
|
1223
|
+
config=ctx.checks_config,
|
|
1224
|
+
transport_kwargs=kwargs,
|
|
1225
|
+
)
|
|
1018
1226
|
try:
|
|
1019
1227
|
run_checks(
|
|
1020
1228
|
case=case,
|
|
1229
|
+
ctx=check_ctx,
|
|
1021
1230
|
checks=checks,
|
|
1022
1231
|
check_results=check_results,
|
|
1023
1232
|
result=result,
|
|
1024
1233
|
response=response,
|
|
1025
1234
|
elapsed_time=context.response_time * 1000,
|
|
1026
1235
|
max_response_time=max_response_time,
|
|
1236
|
+
no_failfast=ctx.no_failfast,
|
|
1027
1237
|
)
|
|
1028
1238
|
except CheckFailed:
|
|
1029
1239
|
status = Status.failure
|
|
1030
1240
|
raise
|
|
1031
1241
|
finally:
|
|
1032
|
-
feedback
|
|
1242
|
+
if feedback is not None:
|
|
1243
|
+
feedback.add_test_case(case, response)
|
|
1033
1244
|
if store_interactions:
|
|
1034
|
-
result.store_requests_response(case, response, status, check_results)
|
|
1245
|
+
result.store_requests_response(case, response, status, check_results, headers, session=None)
|
|
1035
1246
|
return response
|