schemathesis 3.25.5__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 +793 -448
- 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 +24 -4
- 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 +286 -115
- schemathesis/cli/output/short.py +25 -6
- 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 +323 -213
- schemathesis/parameters.py +4 -0
- schemathesis/runner/__init__.py +72 -22
- schemathesis/runner/events.py +86 -6
- schemathesis/runner/impl/context.py +104 -0
- schemathesis/runner/impl/core.py +447 -187
- schemathesis/runner/impl/solo.py +19 -29
- schemathesis/runner/impl/threadpool.py +70 -79
- schemathesis/{cli → runner}/probes.py +37 -25
- schemathesis/runner/serialization.py +150 -17
- schemathesis/sanitization.py +5 -1
- schemathesis/schemas.py +170 -102
- schemathesis/serializers.py +17 -4
- 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 +60 -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 +79 -61
- 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 +143 -31
- 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 +368 -242
- 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.5.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.5.dist-info/METADATA +0 -356
- schemathesis-3.25.5.dist-info/RECORD +0 -134
- {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
schemathesis/runner/impl/solo.py
CHANGED
|
@@ -1,50 +1,42 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Generator
|
|
4
|
+
from typing import TYPE_CHECKING, Generator
|
|
5
5
|
|
|
6
|
-
from ...models import TestResultSet
|
|
7
|
-
from ...types import RequestCert
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
request_cert: RequestCert | None = None
|
|
20
|
-
|
|
21
|
-
def _execute(
|
|
22
|
-
self, results: TestResultSet, stop_event: threading.Event
|
|
23
|
-
) -> Generator[events.ExecutionEvent, None, None]:
|
|
24
|
-
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):
|
|
25
21
|
yield event
|
|
26
|
-
if
|
|
22
|
+
if ctx.is_stopped or self._should_stop(event):
|
|
27
23
|
break
|
|
28
24
|
|
|
29
|
-
def _execute_impl(self,
|
|
25
|
+
def _execute_impl(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
|
|
30
26
|
auth = get_requests_auth(self.auth, self.auth_type)
|
|
31
27
|
with get_session(auth) as session:
|
|
32
28
|
yield from self._run_tests(
|
|
33
29
|
maker=self.schema.get_all_tests,
|
|
34
|
-
|
|
30
|
+
test_func=network_test,
|
|
35
31
|
settings=self.hypothesis_settings,
|
|
36
32
|
generation_config=self.generation_config,
|
|
37
|
-
seed=self.seed,
|
|
38
33
|
checks=self.checks,
|
|
39
34
|
max_response_time=self.max_response_time,
|
|
40
35
|
targets=self.targets,
|
|
41
|
-
|
|
36
|
+
ctx=ctx,
|
|
42
37
|
session=session,
|
|
43
38
|
headers=self.headers,
|
|
44
|
-
|
|
45
|
-
request_tls_verify=self.request_tls_verify,
|
|
46
|
-
request_proxy=self.request_proxy,
|
|
47
|
-
request_cert=self.request_cert,
|
|
39
|
+
request_config=self.request_config,
|
|
48
40
|
store_interactions=self.store_interactions,
|
|
49
41
|
dry_run=self.dry_run,
|
|
50
42
|
)
|
|
@@ -52,17 +44,16 @@ class SingleThreadRunner(BaseRunner):
|
|
|
52
44
|
|
|
53
45
|
@dataclass
|
|
54
46
|
class SingleThreadWSGIRunner(SingleThreadRunner):
|
|
55
|
-
def _execute_impl(self,
|
|
47
|
+
def _execute_impl(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
|
|
56
48
|
yield from self._run_tests(
|
|
57
49
|
maker=self.schema.get_all_tests,
|
|
58
|
-
|
|
50
|
+
test_func=wsgi_test,
|
|
59
51
|
settings=self.hypothesis_settings,
|
|
60
52
|
generation_config=self.generation_config,
|
|
61
|
-
seed=self.seed,
|
|
62
53
|
checks=self.checks,
|
|
63
54
|
max_response_time=self.max_response_time,
|
|
64
55
|
targets=self.targets,
|
|
65
|
-
|
|
56
|
+
ctx=ctx,
|
|
66
57
|
auth=self.auth,
|
|
67
58
|
auth_type=self.auth_type,
|
|
68
59
|
headers=self.headers,
|
|
@@ -73,17 +64,16 @@ class SingleThreadWSGIRunner(SingleThreadRunner):
|
|
|
73
64
|
|
|
74
65
|
@dataclass
|
|
75
66
|
class SingleThreadASGIRunner(SingleThreadRunner):
|
|
76
|
-
def _execute_impl(self,
|
|
67
|
+
def _execute_impl(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
|
|
77
68
|
yield from self._run_tests(
|
|
78
69
|
maker=self.schema.get_all_tests,
|
|
79
|
-
|
|
70
|
+
test_func=asgi_test,
|
|
80
71
|
settings=self.hypothesis_settings,
|
|
81
72
|
generation_config=self.generation_config,
|
|
82
|
-
seed=self.seed,
|
|
83
73
|
checks=self.checks,
|
|
84
74
|
max_response_time=self.max_response_time,
|
|
85
75
|
targets=self.targets,
|
|
86
|
-
|
|
76
|
+
ctx=ctx,
|
|
87
77
|
headers=self.headers,
|
|
88
78
|
store_interactions=self.store_interactions,
|
|
89
79
|
dry_run=self.dry_run,
|
|
@@ -1,29 +1,37 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import ctypes
|
|
3
4
|
import queue
|
|
4
5
|
import threading
|
|
5
6
|
import time
|
|
7
|
+
import warnings
|
|
6
8
|
from dataclasses import dataclass
|
|
7
9
|
from queue import Queue
|
|
8
|
-
from typing import Any, Callable, Generator, Iterable, cast
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, cast
|
|
9
11
|
|
|
10
|
-
import
|
|
12
|
+
from hypothesis.errors import HypothesisWarning
|
|
11
13
|
|
|
12
14
|
from ..._hypothesis import create_test
|
|
13
|
-
from ...generation import DataGenerationMethod, GenerationConfig
|
|
14
15
|
from ...internal.result import Ok
|
|
15
|
-
from ...models import CheckFunction, TestResultSet
|
|
16
16
|
from ...stateful import Feedback, Stateful
|
|
17
|
-
from ...targets import Target
|
|
18
17
|
from ...transports.auth import get_requests_auth
|
|
19
|
-
from ...types import RawAuth, RequestCert
|
|
20
18
|
from ...utils import capture_hypothesis_output
|
|
21
19
|
from .. import events
|
|
22
20
|
from .core import BaseRunner, asgi_test, get_session, handle_schema_error, network_test, run_test, wsgi_test
|
|
23
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
|
+
|
|
24
31
|
|
|
25
32
|
def _run_task(
|
|
26
|
-
|
|
33
|
+
*,
|
|
34
|
+
test_func: Callable,
|
|
27
35
|
tasks_queue: Queue,
|
|
28
36
|
events_queue: Queue,
|
|
29
37
|
generator_done: threading.Event,
|
|
@@ -32,13 +40,13 @@ def _run_task(
|
|
|
32
40
|
data_generation_methods: Iterable[DataGenerationMethod],
|
|
33
41
|
settings: hypothesis.settings,
|
|
34
42
|
generation_config: GenerationConfig,
|
|
35
|
-
|
|
36
|
-
results: TestResultSet,
|
|
43
|
+
ctx: RunnerContext,
|
|
37
44
|
stateful: Stateful | None,
|
|
38
45
|
stateful_recursion_limit: int,
|
|
39
46
|
headers: dict[str, Any] | None = None,
|
|
40
47
|
**kwargs: Any,
|
|
41
48
|
) -> None:
|
|
49
|
+
warnings.filterwarnings("ignore", message="The recursion limit will not be reset", category=HypothesisWarning)
|
|
42
50
|
as_strategy_kwargs = {}
|
|
43
51
|
if headers is not None:
|
|
44
52
|
as_strategy_kwargs["headers"] = {key: value for key, value in headers.items() if key.lower() != "user-agent"}
|
|
@@ -47,10 +55,10 @@ def _run_task(
|
|
|
47
55
|
if recursion_level > stateful_recursion_limit:
|
|
48
56
|
return
|
|
49
57
|
for _result in maker(
|
|
50
|
-
|
|
58
|
+
test_func,
|
|
51
59
|
settings=settings,
|
|
52
60
|
generation_config=generation_config,
|
|
53
|
-
seed=seed,
|
|
61
|
+
seed=ctx.seed,
|
|
54
62
|
as_strategy_kwargs=as_strategy_kwargs,
|
|
55
63
|
):
|
|
56
64
|
# `result` is always `Ok` here
|
|
@@ -62,7 +70,7 @@ def _run_task(
|
|
|
62
70
|
checks,
|
|
63
71
|
data_generation_methods,
|
|
64
72
|
targets,
|
|
65
|
-
|
|
73
|
+
ctx=ctx,
|
|
66
74
|
recursion_level=recursion_level,
|
|
67
75
|
feedback=feedback,
|
|
68
76
|
headers=headers,
|
|
@@ -85,9 +93,9 @@ def _run_task(
|
|
|
85
93
|
operation = result.ok()
|
|
86
94
|
test_function = create_test(
|
|
87
95
|
operation=operation,
|
|
88
|
-
test=
|
|
96
|
+
test=test_func,
|
|
89
97
|
settings=settings,
|
|
90
|
-
seed=seed,
|
|
98
|
+
seed=ctx.seed,
|
|
91
99
|
data_generation_methods=list(data_generation_methods),
|
|
92
100
|
generation_config=generation_config,
|
|
93
101
|
as_strategy_kwargs=as_strategy_kwargs,
|
|
@@ -97,7 +105,7 @@ def _run_task(
|
|
|
97
105
|
# `feedback.get_stateful_tests`
|
|
98
106
|
_run_tests(lambda *_, **__: (items,)) # noqa: B023
|
|
99
107
|
else:
|
|
100
|
-
for event in handle_schema_error(result.err(),
|
|
108
|
+
for event in handle_schema_error(result.err(), ctx, data_generation_methods, 0):
|
|
101
109
|
events_queue.put(event)
|
|
102
110
|
|
|
103
111
|
|
|
@@ -113,8 +121,7 @@ def thread_task(
|
|
|
113
121
|
auth: RawAuth | None,
|
|
114
122
|
auth_type: str | None,
|
|
115
123
|
headers: dict[str, Any] | None,
|
|
116
|
-
|
|
117
|
-
results: TestResultSet,
|
|
124
|
+
ctx: RunnerContext,
|
|
118
125
|
stateful: Stateful | None,
|
|
119
126
|
stateful_recursion_limit: int,
|
|
120
127
|
kwargs: Any,
|
|
@@ -126,17 +133,16 @@ def thread_task(
|
|
|
126
133
|
prepared_auth = get_requests_auth(auth, auth_type)
|
|
127
134
|
with get_session(prepared_auth) as session:
|
|
128
135
|
_run_task(
|
|
129
|
-
network_test,
|
|
130
|
-
tasks_queue,
|
|
131
|
-
events_queue,
|
|
132
|
-
generator_done,
|
|
133
|
-
checks,
|
|
134
|
-
targets,
|
|
135
|
-
data_generation_methods,
|
|
136
|
-
settings,
|
|
137
|
-
generation_config,
|
|
138
|
-
|
|
139
|
-
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,
|
|
140
146
|
stateful=stateful,
|
|
141
147
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
142
148
|
session=session,
|
|
@@ -154,24 +160,22 @@ def wsgi_thread_task(
|
|
|
154
160
|
data_generation_methods: Iterable[DataGenerationMethod],
|
|
155
161
|
settings: hypothesis.settings,
|
|
156
162
|
generation_config: GenerationConfig,
|
|
157
|
-
|
|
158
|
-
results: TestResultSet,
|
|
163
|
+
ctx: RunnerContext,
|
|
159
164
|
stateful: Stateful | None,
|
|
160
165
|
stateful_recursion_limit: int,
|
|
161
166
|
kwargs: Any,
|
|
162
167
|
) -> None:
|
|
163
168
|
_run_task(
|
|
164
|
-
wsgi_test,
|
|
165
|
-
tasks_queue,
|
|
166
|
-
events_queue,
|
|
167
|
-
generator_done,
|
|
168
|
-
checks,
|
|
169
|
-
targets,
|
|
170
|
-
data_generation_methods,
|
|
171
|
-
settings,
|
|
172
|
-
generation_config,
|
|
173
|
-
|
|
174
|
-
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,
|
|
175
179
|
stateful=stateful,
|
|
176
180
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
177
181
|
**kwargs,
|
|
@@ -188,24 +192,22 @@ def asgi_thread_task(
|
|
|
188
192
|
settings: hypothesis.settings,
|
|
189
193
|
generation_config: GenerationConfig,
|
|
190
194
|
headers: dict[str, Any] | None,
|
|
191
|
-
|
|
192
|
-
results: TestResultSet,
|
|
195
|
+
ctx: RunnerContext,
|
|
193
196
|
stateful: Stateful | None,
|
|
194
197
|
stateful_recursion_limit: int,
|
|
195
198
|
kwargs: Any,
|
|
196
199
|
) -> None:
|
|
197
200
|
_run_task(
|
|
198
|
-
asgi_test,
|
|
199
|
-
tasks_queue,
|
|
200
|
-
events_queue,
|
|
201
|
-
generator_done,
|
|
202
|
-
checks,
|
|
203
|
-
targets,
|
|
204
|
-
data_generation_methods,
|
|
205
|
-
settings,
|
|
206
|
-
generation_config,
|
|
207
|
-
|
|
208
|
-
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,
|
|
209
211
|
stateful=stateful,
|
|
210
212
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
211
213
|
headers=headers,
|
|
@@ -223,13 +225,8 @@ class ThreadPoolRunner(BaseRunner):
|
|
|
223
225
|
"""Spread different tests among multiple worker threads."""
|
|
224
226
|
|
|
225
227
|
workers_num: int = 2
|
|
226
|
-
request_tls_verify: bool | str = True
|
|
227
|
-
request_proxy: str | None = None
|
|
228
|
-
request_cert: RequestCert | None = None
|
|
229
228
|
|
|
230
|
-
def _execute(
|
|
231
|
-
self, results: TestResultSet, stop_event: threading.Event
|
|
232
|
-
) -> Generator[events.ExecutionEvent, None, None]:
|
|
229
|
+
def _execute(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
|
|
233
230
|
"""All events come from a queue where different workers push their events."""
|
|
234
231
|
# Instead of generating all tests at once, we do it when there is a free worker to pick it up
|
|
235
232
|
# This is extremely important for memory consumption when testing large schemas
|
|
@@ -237,7 +234,7 @@ class ThreadPoolRunner(BaseRunner):
|
|
|
237
234
|
# It would be better to have a separate producer thread and communicate via threading events.
|
|
238
235
|
# Though it is a bit more complex, so the current solution is suboptimal in terms of resources utilization,
|
|
239
236
|
# but good enough and easy enough to implement.
|
|
240
|
-
tasks_generator = iter(self.schema.get_all_operations())
|
|
237
|
+
tasks_generator = iter(self.schema.get_all_operations(generation_config=self.generation_config))
|
|
241
238
|
generator_done = threading.Event()
|
|
242
239
|
tasks_queue: Queue = Queue()
|
|
243
240
|
# Add at least `workers_num` tasks first, so all workers are busy
|
|
@@ -250,7 +247,7 @@ class ThreadPoolRunner(BaseRunner):
|
|
|
250
247
|
break
|
|
251
248
|
# Events are pushed by workers via a separate queue
|
|
252
249
|
events_queue: Queue = Queue()
|
|
253
|
-
workers = self._init_workers(tasks_queue, events_queue,
|
|
250
|
+
workers = self._init_workers(tasks_queue, events_queue, ctx, generator_done)
|
|
254
251
|
|
|
255
252
|
def stop_workers() -> None:
|
|
256
253
|
for worker in workers:
|
|
@@ -269,12 +266,12 @@ class ThreadPoolRunner(BaseRunner):
|
|
|
269
266
|
is_finished = all(not worker.is_alive() for worker in workers)
|
|
270
267
|
while not events_queue.empty():
|
|
271
268
|
event = events_queue.get()
|
|
272
|
-
if
|
|
269
|
+
if ctx.is_stopped or isinstance(event, events.Interrupted) or self._should_stop(event):
|
|
273
270
|
# We could still have events in the queue, but ignore them to keep the logic simple
|
|
274
271
|
# for now, could be improved in the future to show more info in such corner cases
|
|
275
272
|
stop_workers()
|
|
276
273
|
is_finished = True
|
|
277
|
-
if
|
|
274
|
+
if ctx.is_stopped:
|
|
278
275
|
# Discard the event. The invariant is: the next event after `stream.stop()` is `Finished`
|
|
279
276
|
break
|
|
280
277
|
yield event
|
|
@@ -291,13 +288,13 @@ class ThreadPoolRunner(BaseRunner):
|
|
|
291
288
|
yield events.Interrupted()
|
|
292
289
|
|
|
293
290
|
def _init_workers(
|
|
294
|
-
self, tasks_queue: Queue, events_queue: Queue,
|
|
291
|
+
self, tasks_queue: Queue, events_queue: Queue, ctx: RunnerContext, generator_done: threading.Event
|
|
295
292
|
) -> list[threading.Thread]:
|
|
296
293
|
"""Initialize & start workers that will execute tests."""
|
|
297
294
|
workers = [
|
|
298
295
|
threading.Thread(
|
|
299
296
|
target=self._get_task(),
|
|
300
|
-
kwargs=self._get_worker_kwargs(tasks_queue, events_queue,
|
|
297
|
+
kwargs=self._get_worker_kwargs(tasks_queue, events_queue, ctx, generator_done),
|
|
301
298
|
name=f"schemathesis_{num}",
|
|
302
299
|
)
|
|
303
300
|
for num in range(self.workers_num)
|
|
@@ -310,7 +307,7 @@ class ThreadPoolRunner(BaseRunner):
|
|
|
310
307
|
return thread_task
|
|
311
308
|
|
|
312
309
|
def _get_worker_kwargs(
|
|
313
|
-
self, tasks_queue: Queue, events_queue: Queue,
|
|
310
|
+
self, tasks_queue: Queue, events_queue: Queue, ctx: RunnerContext, generator_done: threading.Event
|
|
314
311
|
) -> dict[str, Any]:
|
|
315
312
|
return {
|
|
316
313
|
"tasks_queue": tasks_queue,
|
|
@@ -323,16 +320,12 @@ class ThreadPoolRunner(BaseRunner):
|
|
|
323
320
|
"auth": self.auth,
|
|
324
321
|
"auth_type": self.auth_type,
|
|
325
322
|
"headers": self.headers,
|
|
326
|
-
"
|
|
327
|
-
"results": results,
|
|
323
|
+
"ctx": ctx,
|
|
328
324
|
"stateful": self.stateful,
|
|
329
325
|
"stateful_recursion_limit": self.stateful_recursion_limit,
|
|
330
326
|
"data_generation_methods": self.schema.data_generation_methods,
|
|
331
327
|
"kwargs": {
|
|
332
|
-
"
|
|
333
|
-
"request_tls_verify": self.request_tls_verify,
|
|
334
|
-
"request_proxy": self.request_proxy,
|
|
335
|
-
"request_cert": self.request_cert,
|
|
328
|
+
"request_config": self.request_config,
|
|
336
329
|
"store_interactions": self.store_interactions,
|
|
337
330
|
"max_response_time": self.max_response_time,
|
|
338
331
|
"dry_run": self.dry_run,
|
|
@@ -345,7 +338,7 @@ class ThreadPoolWSGIRunner(ThreadPoolRunner):
|
|
|
345
338
|
return wsgi_thread_task
|
|
346
339
|
|
|
347
340
|
def _get_worker_kwargs(
|
|
348
|
-
self, tasks_queue: Queue, events_queue: Queue,
|
|
341
|
+
self, tasks_queue: Queue, events_queue: Queue, ctx: RunnerContext, generator_done: threading.Event
|
|
349
342
|
) -> dict[str, Any]:
|
|
350
343
|
return {
|
|
351
344
|
"tasks_queue": tasks_queue,
|
|
@@ -355,8 +348,7 @@ class ThreadPoolWSGIRunner(ThreadPoolRunner):
|
|
|
355
348
|
"targets": self.targets,
|
|
356
349
|
"settings": self.hypothesis_settings,
|
|
357
350
|
"generation_config": self.generation_config,
|
|
358
|
-
"
|
|
359
|
-
"results": results,
|
|
351
|
+
"ctx": ctx,
|
|
360
352
|
"stateful": self.stateful,
|
|
361
353
|
"stateful_recursion_limit": self.stateful_recursion_limit,
|
|
362
354
|
"data_generation_methods": self.schema.data_generation_methods,
|
|
@@ -376,7 +368,7 @@ class ThreadPoolASGIRunner(ThreadPoolRunner):
|
|
|
376
368
|
return asgi_thread_task
|
|
377
369
|
|
|
378
370
|
def _get_worker_kwargs(
|
|
379
|
-
self, tasks_queue: Queue, events_queue: Queue,
|
|
371
|
+
self, tasks_queue: Queue, events_queue: Queue, ctx: RunnerContext, generator_done: threading.Event
|
|
380
372
|
) -> dict[str, Any]:
|
|
381
373
|
return {
|
|
382
374
|
"tasks_queue": tasks_queue,
|
|
@@ -387,8 +379,7 @@ class ThreadPoolASGIRunner(ThreadPoolRunner):
|
|
|
387
379
|
"settings": self.hypothesis_settings,
|
|
388
380
|
"generation_config": self.generation_config,
|
|
389
381
|
"headers": self.headers,
|
|
390
|
-
"
|
|
391
|
-
"results": results,
|
|
382
|
+
"ctx": ctx,
|
|
392
383
|
"stateful": self.stateful,
|
|
393
384
|
"stateful_recursion_limit": self.stateful_recursion_limit,
|
|
394
385
|
"data_generation_methods": self.schema.data_generation_methods,
|
|
@@ -5,29 +5,39 @@ the application supports certain inputs. This is done to avoid false positives i
|
|
|
5
5
|
For example, certail web servers do not support NULL bytes in headers, in such cases, the generated test case
|
|
6
6
|
will not reach the tested application at all.
|
|
7
7
|
"""
|
|
8
|
+
|
|
8
9
|
from __future__ import annotations
|
|
9
10
|
|
|
10
11
|
import enum
|
|
11
12
|
import warnings
|
|
12
|
-
from dataclasses import asdict, dataclass
|
|
13
|
+
from dataclasses import asdict, dataclass, field
|
|
13
14
|
from typing import TYPE_CHECKING, Any
|
|
14
15
|
|
|
15
16
|
from ..constants import USER_AGENT
|
|
16
17
|
from ..exceptions import format_exception
|
|
17
18
|
from ..models import Request, Response
|
|
18
19
|
from ..sanitization import sanitize_request, sanitize_response
|
|
20
|
+
from ..transports import RequestConfig
|
|
19
21
|
from ..transports.auth import get_requests_auth
|
|
20
22
|
|
|
21
23
|
if TYPE_CHECKING:
|
|
22
24
|
import requests
|
|
23
25
|
|
|
24
26
|
from ..schemas import BaseSchema
|
|
25
|
-
from . import LoaderConfig
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
HEADER_NAME = "X-Schemathesis-Probe"
|
|
29
30
|
|
|
30
31
|
|
|
32
|
+
@dataclass
|
|
33
|
+
class ProbeConfig:
|
|
34
|
+
base_url: str | None = None
|
|
35
|
+
request: RequestConfig = field(default_factory=RequestConfig)
|
|
36
|
+
auth: tuple[str, str] | None = None
|
|
37
|
+
auth_type: str | None = None
|
|
38
|
+
headers: dict[str, str] | None = None
|
|
39
|
+
|
|
40
|
+
|
|
31
41
|
@dataclass
|
|
32
42
|
class Probe:
|
|
33
43
|
"""A request to determine the capabilities of the application under test."""
|
|
@@ -35,15 +45,15 @@ class Probe:
|
|
|
35
45
|
name: str
|
|
36
46
|
|
|
37
47
|
def prepare_request(
|
|
38
|
-
self, session: requests.Session, request: requests.Request, schema: BaseSchema, config:
|
|
48
|
+
self, session: requests.Session, request: requests.Request, schema: BaseSchema, config: ProbeConfig
|
|
39
49
|
) -> requests.PreparedRequest:
|
|
40
50
|
raise NotImplementedError
|
|
41
51
|
|
|
42
|
-
def analyze_response(self, response: requests.Response) ->
|
|
52
|
+
def analyze_response(self, response: requests.Response) -> ProbeOutcome:
|
|
43
53
|
raise NotImplementedError
|
|
44
54
|
|
|
45
55
|
|
|
46
|
-
class
|
|
56
|
+
class ProbeOutcome(str, enum.Enum):
|
|
47
57
|
# Capability is supported
|
|
48
58
|
SUCCESS = "success"
|
|
49
59
|
# Capability is not supported
|
|
@@ -55,18 +65,16 @@ class ProbeResultType(str, enum.Enum):
|
|
|
55
65
|
|
|
56
66
|
|
|
57
67
|
@dataclass
|
|
58
|
-
class
|
|
59
|
-
"""Result of a probe."""
|
|
60
|
-
|
|
68
|
+
class ProbeRun:
|
|
61
69
|
probe: Probe
|
|
62
|
-
|
|
70
|
+
outcome: ProbeOutcome
|
|
63
71
|
request: requests.PreparedRequest | None = None
|
|
64
72
|
response: requests.Response | None = None
|
|
65
73
|
error: requests.RequestException | None = None
|
|
66
74
|
|
|
67
75
|
@property
|
|
68
76
|
def is_failure(self) -> bool:
|
|
69
|
-
return self.
|
|
77
|
+
return self.outcome == ProbeOutcome.FAILURE
|
|
70
78
|
|
|
71
79
|
def serialize(self) -> dict[str, Any]:
|
|
72
80
|
"""Serialize probe results so it can be sent over the network."""
|
|
@@ -87,7 +95,7 @@ class ProbeResult:
|
|
|
87
95
|
error = None
|
|
88
96
|
return {
|
|
89
97
|
"name": self.probe.name,
|
|
90
|
-
"
|
|
98
|
+
"outcome": self.outcome.value,
|
|
91
99
|
"request": request,
|
|
92
100
|
"response": response,
|
|
93
101
|
"error": error,
|
|
@@ -101,25 +109,25 @@ class NullByteInHeader(Probe):
|
|
|
101
109
|
name: str = "NULL_BYTE_IN_HEADER"
|
|
102
110
|
|
|
103
111
|
def prepare_request(
|
|
104
|
-
self, session: requests.Session, request: requests.Request, schema: BaseSchema, config:
|
|
112
|
+
self, session: requests.Session, request: requests.Request, schema: BaseSchema, config: ProbeConfig
|
|
105
113
|
) -> requests.PreparedRequest:
|
|
106
114
|
request.method = "GET"
|
|
107
115
|
request.url = config.base_url or schema.get_base_url()
|
|
108
116
|
request.headers = {"X-Schemathesis-Probe-Null": "\x00"}
|
|
109
117
|
return session.prepare_request(request)
|
|
110
118
|
|
|
111
|
-
def analyze_response(self, response: requests.Response) ->
|
|
119
|
+
def analyze_response(self, response: requests.Response) -> ProbeOutcome:
|
|
112
120
|
if response.status_code == 400:
|
|
113
|
-
return
|
|
114
|
-
return
|
|
121
|
+
return ProbeOutcome.FAILURE
|
|
122
|
+
return ProbeOutcome.SUCCESS
|
|
115
123
|
|
|
116
124
|
|
|
117
125
|
PROBES = (NullByteInHeader,)
|
|
118
126
|
|
|
119
127
|
|
|
120
|
-
def send(probe: Probe, session: requests.Session, schema: BaseSchema, config:
|
|
128
|
+
def send(probe: Probe, session: requests.Session, schema: BaseSchema, config: ProbeConfig) -> ProbeRun:
|
|
121
129
|
"""Send the probe to the application."""
|
|
122
|
-
from requests import Request, RequestException
|
|
130
|
+
from requests import PreparedRequest, Request, RequestException
|
|
123
131
|
from requests.exceptions import MissingSchema
|
|
124
132
|
from urllib3.exceptions import InsecureRequestWarning
|
|
125
133
|
|
|
@@ -127,28 +135,32 @@ def send(probe: Probe, session: requests.Session, schema: BaseSchema, config: Lo
|
|
|
127
135
|
request = probe.prepare_request(session, Request(), schema, config)
|
|
128
136
|
request.headers[HEADER_NAME] = probe.name
|
|
129
137
|
request.headers["User-Agent"] = USER_AGENT
|
|
138
|
+
kwargs: dict[str, Any] = {"timeout": config.request.prepared_timeout or 2}
|
|
139
|
+
if config.request.proxy is not None:
|
|
140
|
+
kwargs["proxies"] = {"all": config.request.proxy}
|
|
130
141
|
with warnings.catch_warnings():
|
|
131
142
|
warnings.simplefilter("ignore", InsecureRequestWarning)
|
|
132
|
-
response = session.send(request)
|
|
143
|
+
response = session.send(request, **kwargs)
|
|
133
144
|
except MissingSchema:
|
|
134
145
|
# In-process ASGI/WSGI testing will have local URLs and requires extra handling
|
|
135
146
|
# which is not currently implemented
|
|
136
|
-
return
|
|
147
|
+
return ProbeRun(probe, ProbeOutcome.SKIP, None, None, None)
|
|
137
148
|
except RequestException as exc:
|
|
138
149
|
req = exc.request if isinstance(exc.request, PreparedRequest) else None
|
|
139
|
-
return
|
|
150
|
+
return ProbeRun(probe, ProbeOutcome.ERROR, req, None, exc)
|
|
140
151
|
result_type = probe.analyze_response(response)
|
|
141
|
-
return
|
|
152
|
+
return ProbeRun(probe, result_type, request, response)
|
|
142
153
|
|
|
143
154
|
|
|
144
|
-
def run(schema: BaseSchema, config:
|
|
155
|
+
def run(schema: BaseSchema, config: ProbeConfig) -> list[ProbeRun]:
|
|
145
156
|
"""Run all probes against the given schema."""
|
|
146
157
|
from requests import Session
|
|
147
158
|
|
|
148
159
|
session = Session()
|
|
149
|
-
session.
|
|
150
|
-
|
|
151
|
-
|
|
160
|
+
session.headers.update(config.headers or {})
|
|
161
|
+
session.verify = config.request.tls_verify
|
|
162
|
+
if config.request.cert is not None:
|
|
163
|
+
session.cert = config.request.cert
|
|
152
164
|
if config.auth is not None:
|
|
153
165
|
session.auth = get_requests_auth(config.auth, config.auth_type)
|
|
154
166
|
|