schemathesis 3.25.5__py3-none-any.whl → 4.0.0a1__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 +27 -65
- schemathesis/auths.py +102 -82
- schemathesis/checks.py +126 -46
- schemathesis/cli/__init__.py +11 -1766
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +37 -0
- schemathesis/cli/commands/run/__init__.py +662 -0
- schemathesis/cli/commands/run/checks.py +80 -0
- schemathesis/cli/commands/run/context.py +117 -0
- schemathesis/cli/commands/run/events.py +35 -0
- schemathesis/cli/commands/run/executor.py +138 -0
- schemathesis/cli/commands/run/filters.py +194 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +746 -0
- schemathesis/cli/commands/run/hypothesis.py +105 -0
- schemathesis/cli/commands/run/loaders.py +129 -0
- schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
- schemathesis/cli/constants.py +5 -52
- schemathesis/cli/core.py +17 -0
- schemathesis/cli/ext/fs.py +14 -0
- schemathesis/cli/ext/groups.py +55 -0
- schemathesis/cli/{options.py → ext/options.py} +39 -10
- schemathesis/cli/hooks.py +36 -0
- schemathesis/contrib/__init__.py +1 -3
- schemathesis/contrib/openapi/__init__.py +1 -3
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
- schemathesis/core/__init__.py +58 -0
- schemathesis/core/compat.py +25 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +370 -0
- schemathesis/core/failures.py +285 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
- schemathesis/core/output/__init__.py +69 -0
- schemathesis/core/output/sanitization.py +197 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +31 -0
- schemathesis/{internal → core}/result.py +1 -1
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +108 -0
- schemathesis/core/validation.py +38 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +30 -0
- schemathesis/engine/config.py +59 -0
- schemathesis/engine/context.py +119 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +157 -0
- schemathesis/engine/errors.py +394 -0
- schemathesis/engine/events.py +337 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{cli → engine/phases}/probes.py +63 -70
- schemathesis/engine/phases/stateful/__init__.py +65 -0
- schemathesis/engine/phases/stateful/_executor.py +326 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +174 -0
- schemathesis/engine/phases/unit/_executor.py +321 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +241 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +18 -14
- schemathesis/filters.py +103 -14
- schemathesis/generation/__init__.py +21 -37
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/generation/hypothesis/__init__.py +30 -0
- schemathesis/generation/hypothesis/builder.py +585 -0
- schemathesis/generation/hypothesis/examples.py +50 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/modes.py +28 -0
- schemathesis/generation/overrides.py +96 -0
- schemathesis/generation/stateful/__init__.py +20 -0
- schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +115 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +99 -67
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +412 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +63 -0
- schemathesis/openapi/loaders.py +178 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +273 -0
- schemathesis/pytest/loaders.py +12 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -261
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +25 -0
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +7 -5
- schemathesis/specs/graphql/schemas.py +215 -187
- schemathesis/specs/graphql/validation.py +11 -18
- schemathesis/specs/openapi/__init__.py +7 -1
- schemathesis/specs/openapi/_cache.py +122 -0
- schemathesis/specs/openapi/_hypothesis.py +146 -165
- schemathesis/specs/openapi/checks.py +565 -67
- schemathesis/specs/openapi/converter.py +33 -6
- schemathesis/specs/openapi/definitions.py +11 -18
- schemathesis/specs/openapi/examples.py +153 -39
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +4 -6
- schemathesis/specs/openapi/expressions/extractors.py +23 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +38 -14
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +45 -0
- schemathesis/specs/openapi/links.py +65 -165
- schemathesis/specs/openapi/media_types.py +32 -0
- schemathesis/specs/openapi/negative/__init__.py +7 -3
- schemathesis/specs/openapi/negative/mutations.py +24 -8
- schemathesis/specs/openapi/parameters.py +46 -30
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +47 -57
- schemathesis/specs/openapi/schemas.py +483 -367
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +11 -6
- schemathesis/specs/openapi/stateful/__init__.py +185 -73
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/transport/__init__.py +104 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +99 -0
- schemathesis/transport/requests.py +221 -0
- schemathesis/{_xml.py → transport/serialization.py} +143 -28
- schemathesis/transport/wsgi.py +165 -0
- schemathesis-4.0.0a1.dist-info/METADATA +297 -0
- schemathesis-4.0.0a1.dist-info/RECORD +152 -0
- {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
- {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -17
- schemathesis/_hypothesis.py +0 -246
- schemathesis/_override.py +0 -49
- schemathesis/cli/cassettes.py +0 -375
- schemathesis/cli/context.py +0 -55
- schemathesis/cli/debug.py +0 -26
- schemathesis/cli/handlers.py +0 -16
- schemathesis/cli/junitxml.py +0 -43
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -765
- schemathesis/cli/output/short.py +0 -40
- schemathesis/cli/sanitization.py +0 -20
- schemathesis/code_samples.py +0 -149
- schemathesis/constants.py +0 -55
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -15
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -560
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -17
- schemathesis/failures.py +0 -209
- schemathesis/fixups/__init__.py +0 -36
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -29
- schemathesis/graphql.py +0 -4
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/copy.py +0 -13
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -34
- schemathesis/internal/jsonschema.py +0 -35
- schemathesis/internal/transformation.py +0 -15
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -361
- schemathesis/loaders.py +0 -120
- schemathesis/models.py +0 -1231
- schemathesis/parameters.py +0 -86
- schemathesis/runner/__init__.py +0 -555
- schemathesis/runner/events.py +0 -309
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -986
- schemathesis/runner/impl/solo.py +0 -90
- schemathesis/runner/impl/threadpool.py +0 -400
- schemathesis/runner/serialization.py +0 -411
- schemathesis/sanitization.py +0 -248
- schemathesis/serializers.py +0 -315
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -201
- schemathesis/service/client.py +0 -100
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -57
- schemathesis/service/hosts.py +0 -107
- schemathesis/service/metadata.py +0 -46
- schemathesis/service/models.py +0 -49
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -184
- schemathesis/service/usage.py +0 -65
- schemathesis/specs/graphql/loaders.py +0 -344
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/loaders.py +0 -667
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis/specs/openapi/validation.py +0 -25
- schemathesis/stateful/__init__.py +0 -133
- schemathesis/targets.py +0 -45
- schemathesis/throttling.py +0 -41
- schemathesis/transports/__init__.py +0 -5
- schemathesis/transports/auth.py +0 -15
- schemathesis/transports/headers.py +0 -35
- schemathesis/transports/responses.py +0 -52
- schemathesis/types.py +0 -35
- schemathesis/utils.py +0 -169
- schemathesis-3.25.5.dist-info/METADATA +0 -356
- schemathesis-3.25.5.dist-info/RECORD +0 -134
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
schemathesis/runner/impl/solo.py
DELETED
@@ -1,90 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
import threading
|
3
|
-
from dataclasses import dataclass
|
4
|
-
from typing import Generator
|
5
|
-
|
6
|
-
from ...models import TestResultSet
|
7
|
-
from ...types import RequestCert
|
8
|
-
from ...transports.auth import get_requests_auth
|
9
|
-
from .. import events
|
10
|
-
from .core import BaseRunner, asgi_test, get_session, network_test, wsgi_test
|
11
|
-
|
12
|
-
|
13
|
-
@dataclass
|
14
|
-
class SingleThreadRunner(BaseRunner):
|
15
|
-
"""Fast runner that runs tests sequentially in the main thread."""
|
16
|
-
|
17
|
-
request_tls_verify: bool | str = True
|
18
|
-
request_proxy: str | None = None
|
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):
|
25
|
-
yield event
|
26
|
-
if stop_event.is_set() or self._should_stop(event):
|
27
|
-
break
|
28
|
-
|
29
|
-
def _execute_impl(self, results: TestResultSet) -> Generator[events.ExecutionEvent, None, None]:
|
30
|
-
auth = get_requests_auth(self.auth, self.auth_type)
|
31
|
-
with get_session(auth) as session:
|
32
|
-
yield from self._run_tests(
|
33
|
-
maker=self.schema.get_all_tests,
|
34
|
-
template=network_test,
|
35
|
-
settings=self.hypothesis_settings,
|
36
|
-
generation_config=self.generation_config,
|
37
|
-
seed=self.seed,
|
38
|
-
checks=self.checks,
|
39
|
-
max_response_time=self.max_response_time,
|
40
|
-
targets=self.targets,
|
41
|
-
results=results,
|
42
|
-
session=session,
|
43
|
-
headers=self.headers,
|
44
|
-
request_timeout=self.request_timeout,
|
45
|
-
request_tls_verify=self.request_tls_verify,
|
46
|
-
request_proxy=self.request_proxy,
|
47
|
-
request_cert=self.request_cert,
|
48
|
-
store_interactions=self.store_interactions,
|
49
|
-
dry_run=self.dry_run,
|
50
|
-
)
|
51
|
-
|
52
|
-
|
53
|
-
@dataclass
|
54
|
-
class SingleThreadWSGIRunner(SingleThreadRunner):
|
55
|
-
def _execute_impl(self, results: TestResultSet) -> Generator[events.ExecutionEvent, None, None]:
|
56
|
-
yield from self._run_tests(
|
57
|
-
maker=self.schema.get_all_tests,
|
58
|
-
template=wsgi_test,
|
59
|
-
settings=self.hypothesis_settings,
|
60
|
-
generation_config=self.generation_config,
|
61
|
-
seed=self.seed,
|
62
|
-
checks=self.checks,
|
63
|
-
max_response_time=self.max_response_time,
|
64
|
-
targets=self.targets,
|
65
|
-
results=results,
|
66
|
-
auth=self.auth,
|
67
|
-
auth_type=self.auth_type,
|
68
|
-
headers=self.headers,
|
69
|
-
store_interactions=self.store_interactions,
|
70
|
-
dry_run=self.dry_run,
|
71
|
-
)
|
72
|
-
|
73
|
-
|
74
|
-
@dataclass
|
75
|
-
class SingleThreadASGIRunner(SingleThreadRunner):
|
76
|
-
def _execute_impl(self, results: TestResultSet) -> Generator[events.ExecutionEvent, None, None]:
|
77
|
-
yield from self._run_tests(
|
78
|
-
maker=self.schema.get_all_tests,
|
79
|
-
template=asgi_test,
|
80
|
-
settings=self.hypothesis_settings,
|
81
|
-
generation_config=self.generation_config,
|
82
|
-
seed=self.seed,
|
83
|
-
checks=self.checks,
|
84
|
-
max_response_time=self.max_response_time,
|
85
|
-
targets=self.targets,
|
86
|
-
results=results,
|
87
|
-
headers=self.headers,
|
88
|
-
store_interactions=self.store_interactions,
|
89
|
-
dry_run=self.dry_run,
|
90
|
-
)
|
@@ -1,400 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
import ctypes
|
3
|
-
import queue
|
4
|
-
import threading
|
5
|
-
import time
|
6
|
-
from dataclasses import dataclass
|
7
|
-
from queue import Queue
|
8
|
-
from typing import Any, Callable, Generator, Iterable, cast
|
9
|
-
|
10
|
-
import hypothesis
|
11
|
-
|
12
|
-
from ..._hypothesis import create_test
|
13
|
-
from ...generation import DataGenerationMethod, GenerationConfig
|
14
|
-
from ...internal.result import Ok
|
15
|
-
from ...models import CheckFunction, TestResultSet
|
16
|
-
from ...stateful import Feedback, Stateful
|
17
|
-
from ...targets import Target
|
18
|
-
from ...transports.auth import get_requests_auth
|
19
|
-
from ...types import RawAuth, RequestCert
|
20
|
-
from ...utils import capture_hypothesis_output
|
21
|
-
from .. import events
|
22
|
-
from .core import BaseRunner, asgi_test, get_session, handle_schema_error, network_test, run_test, wsgi_test
|
23
|
-
|
24
|
-
|
25
|
-
def _run_task(
|
26
|
-
test_template: Callable,
|
27
|
-
tasks_queue: Queue,
|
28
|
-
events_queue: Queue,
|
29
|
-
generator_done: threading.Event,
|
30
|
-
checks: Iterable[CheckFunction],
|
31
|
-
targets: Iterable[Target],
|
32
|
-
data_generation_methods: Iterable[DataGenerationMethod],
|
33
|
-
settings: hypothesis.settings,
|
34
|
-
generation_config: GenerationConfig,
|
35
|
-
seed: int | None,
|
36
|
-
results: TestResultSet,
|
37
|
-
stateful: Stateful | None,
|
38
|
-
stateful_recursion_limit: int,
|
39
|
-
headers: dict[str, Any] | None = None,
|
40
|
-
**kwargs: Any,
|
41
|
-
) -> None:
|
42
|
-
as_strategy_kwargs = {}
|
43
|
-
if headers is not None:
|
44
|
-
as_strategy_kwargs["headers"] = {key: value for key, value in headers.items() if key.lower() != "user-agent"}
|
45
|
-
|
46
|
-
def _run_tests(maker: Callable, recursion_level: int = 0) -> None:
|
47
|
-
if recursion_level > stateful_recursion_limit:
|
48
|
-
return
|
49
|
-
for _result in maker(
|
50
|
-
test_template,
|
51
|
-
settings=settings,
|
52
|
-
generation_config=generation_config,
|
53
|
-
seed=seed,
|
54
|
-
as_strategy_kwargs=as_strategy_kwargs,
|
55
|
-
):
|
56
|
-
# `result` is always `Ok` here
|
57
|
-
_operation, test = _result.ok()
|
58
|
-
feedback = Feedback(stateful, _operation)
|
59
|
-
for _event in run_test(
|
60
|
-
_operation,
|
61
|
-
test,
|
62
|
-
checks,
|
63
|
-
data_generation_methods,
|
64
|
-
targets,
|
65
|
-
results,
|
66
|
-
recursion_level=recursion_level,
|
67
|
-
feedback=feedback,
|
68
|
-
headers=headers,
|
69
|
-
**kwargs,
|
70
|
-
):
|
71
|
-
events_queue.put(_event)
|
72
|
-
_run_tests(feedback.get_stateful_tests, recursion_level + 1)
|
73
|
-
|
74
|
-
with capture_hypothesis_output():
|
75
|
-
while True:
|
76
|
-
try:
|
77
|
-
result = tasks_queue.get(timeout=0.001)
|
78
|
-
except queue.Empty:
|
79
|
-
# The queue is empty & there will be no more tasks
|
80
|
-
if generator_done.is_set():
|
81
|
-
break
|
82
|
-
# If there is a possibility for new tasks - try again
|
83
|
-
continue
|
84
|
-
if isinstance(result, Ok):
|
85
|
-
operation = result.ok()
|
86
|
-
test_function = create_test(
|
87
|
-
operation=operation,
|
88
|
-
test=test_template,
|
89
|
-
settings=settings,
|
90
|
-
seed=seed,
|
91
|
-
data_generation_methods=list(data_generation_methods),
|
92
|
-
generation_config=generation_config,
|
93
|
-
as_strategy_kwargs=as_strategy_kwargs,
|
94
|
-
)
|
95
|
-
items = Ok((operation, test_function))
|
96
|
-
# This lambda ignores the input arguments to support the same interface for
|
97
|
-
# `feedback.get_stateful_tests`
|
98
|
-
_run_tests(lambda *_, **__: (items,)) # noqa: B023
|
99
|
-
else:
|
100
|
-
for event in handle_schema_error(result.err(), results, data_generation_methods, 0):
|
101
|
-
events_queue.put(event)
|
102
|
-
|
103
|
-
|
104
|
-
def thread_task(
|
105
|
-
tasks_queue: Queue,
|
106
|
-
events_queue: Queue,
|
107
|
-
generator_done: threading.Event,
|
108
|
-
checks: Iterable[CheckFunction],
|
109
|
-
targets: Iterable[Target],
|
110
|
-
data_generation_methods: Iterable[DataGenerationMethod],
|
111
|
-
settings: hypothesis.settings,
|
112
|
-
generation_config: GenerationConfig,
|
113
|
-
auth: RawAuth | None,
|
114
|
-
auth_type: str | None,
|
115
|
-
headers: dict[str, Any] | None,
|
116
|
-
seed: int | None,
|
117
|
-
results: TestResultSet,
|
118
|
-
stateful: Stateful | None,
|
119
|
-
stateful_recursion_limit: int,
|
120
|
-
kwargs: Any,
|
121
|
-
) -> None:
|
122
|
-
"""A single task, that threads do.
|
123
|
-
|
124
|
-
Pretty similar to the default one-thread flow, but includes communication with the main thread via the events queue.
|
125
|
-
"""
|
126
|
-
prepared_auth = get_requests_auth(auth, auth_type)
|
127
|
-
with get_session(prepared_auth) as session:
|
128
|
-
_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
|
-
seed,
|
139
|
-
results,
|
140
|
-
stateful=stateful,
|
141
|
-
stateful_recursion_limit=stateful_recursion_limit,
|
142
|
-
session=session,
|
143
|
-
headers=headers,
|
144
|
-
**kwargs,
|
145
|
-
)
|
146
|
-
|
147
|
-
|
148
|
-
def wsgi_thread_task(
|
149
|
-
tasks_queue: Queue,
|
150
|
-
events_queue: Queue,
|
151
|
-
generator_done: threading.Event,
|
152
|
-
checks: Iterable[CheckFunction],
|
153
|
-
targets: Iterable[Target],
|
154
|
-
data_generation_methods: Iterable[DataGenerationMethod],
|
155
|
-
settings: hypothesis.settings,
|
156
|
-
generation_config: GenerationConfig,
|
157
|
-
seed: int | None,
|
158
|
-
results: TestResultSet,
|
159
|
-
stateful: Stateful | None,
|
160
|
-
stateful_recursion_limit: int,
|
161
|
-
kwargs: Any,
|
162
|
-
) -> None:
|
163
|
-
_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
|
-
seed,
|
174
|
-
results,
|
175
|
-
stateful=stateful,
|
176
|
-
stateful_recursion_limit=stateful_recursion_limit,
|
177
|
-
**kwargs,
|
178
|
-
)
|
179
|
-
|
180
|
-
|
181
|
-
def asgi_thread_task(
|
182
|
-
tasks_queue: Queue,
|
183
|
-
events_queue: Queue,
|
184
|
-
generator_done: threading.Event,
|
185
|
-
checks: Iterable[CheckFunction],
|
186
|
-
targets: Iterable[Target],
|
187
|
-
data_generation_methods: Iterable[DataGenerationMethod],
|
188
|
-
settings: hypothesis.settings,
|
189
|
-
generation_config: GenerationConfig,
|
190
|
-
headers: dict[str, Any] | None,
|
191
|
-
seed: int | None,
|
192
|
-
results: TestResultSet,
|
193
|
-
stateful: Stateful | None,
|
194
|
-
stateful_recursion_limit: int,
|
195
|
-
kwargs: Any,
|
196
|
-
) -> None:
|
197
|
-
_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
|
-
seed,
|
208
|
-
results,
|
209
|
-
stateful=stateful,
|
210
|
-
stateful_recursion_limit=stateful_recursion_limit,
|
211
|
-
headers=headers,
|
212
|
-
**kwargs,
|
213
|
-
)
|
214
|
-
|
215
|
-
|
216
|
-
def stop_worker(thread_id: int) -> None:
|
217
|
-
"""Raise an error in a thread, so it is possible to asynchronously stop thread execution."""
|
218
|
-
ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread_id), ctypes.py_object(SystemExit))
|
219
|
-
|
220
|
-
|
221
|
-
@dataclass
|
222
|
-
class ThreadPoolRunner(BaseRunner):
|
223
|
-
"""Spread different tests among multiple worker threads."""
|
224
|
-
|
225
|
-
workers_num: int = 2
|
226
|
-
request_tls_verify: bool | str = True
|
227
|
-
request_proxy: str | None = None
|
228
|
-
request_cert: RequestCert | None = None
|
229
|
-
|
230
|
-
def _execute(
|
231
|
-
self, results: TestResultSet, stop_event: threading.Event
|
232
|
-
) -> Generator[events.ExecutionEvent, None, None]:
|
233
|
-
"""All events come from a queue where different workers push their events."""
|
234
|
-
# Instead of generating all tests at once, we do it when there is a free worker to pick it up
|
235
|
-
# This is extremely important for memory consumption when testing large schemas
|
236
|
-
# IMPLEMENTATION NOTE:
|
237
|
-
# It would be better to have a separate producer thread and communicate via threading events.
|
238
|
-
# Though it is a bit more complex, so the current solution is suboptimal in terms of resources utilization,
|
239
|
-
# but good enough and easy enough to implement.
|
240
|
-
tasks_generator = iter(self.schema.get_all_operations())
|
241
|
-
generator_done = threading.Event()
|
242
|
-
tasks_queue: Queue = Queue()
|
243
|
-
# Add at least `workers_num` tasks first, so all workers are busy
|
244
|
-
for _ in range(self.workers_num):
|
245
|
-
try:
|
246
|
-
# SAFETY: Workers didn't start yet, direct modification is OK
|
247
|
-
tasks_queue.queue.append(next(tasks_generator))
|
248
|
-
except StopIteration:
|
249
|
-
generator_done.set()
|
250
|
-
break
|
251
|
-
# Events are pushed by workers via a separate queue
|
252
|
-
events_queue: Queue = Queue()
|
253
|
-
workers = self._init_workers(tasks_queue, events_queue, results, generator_done)
|
254
|
-
|
255
|
-
def stop_workers() -> None:
|
256
|
-
for worker in workers:
|
257
|
-
# workers are initialized at this point and `worker.ident` is set with an integer value
|
258
|
-
ident = cast(int, worker.ident)
|
259
|
-
stop_worker(ident)
|
260
|
-
worker.join()
|
261
|
-
|
262
|
-
is_finished = False
|
263
|
-
try:
|
264
|
-
while not is_finished:
|
265
|
-
# Sleep is needed for performance reasons
|
266
|
-
# each call to `is_alive` of an alive worker waits for a lock
|
267
|
-
# iterations without waiting are too frequent, and a lot of time will be spent on waiting for this locks
|
268
|
-
time.sleep(0.001)
|
269
|
-
is_finished = all(not worker.is_alive() for worker in workers)
|
270
|
-
while not events_queue.empty():
|
271
|
-
event = events_queue.get()
|
272
|
-
if stop_event.is_set() or isinstance(event, events.Interrupted) or self._should_stop(event):
|
273
|
-
# We could still have events in the queue, but ignore them to keep the logic simple
|
274
|
-
# for now, could be improved in the future to show more info in such corner cases
|
275
|
-
stop_workers()
|
276
|
-
is_finished = True
|
277
|
-
if stop_event.is_set():
|
278
|
-
# Discard the event. The invariant is: the next event after `stream.stop()` is `Finished`
|
279
|
-
break
|
280
|
-
yield event
|
281
|
-
# When we know that there are more tasks, put another task to the queue.
|
282
|
-
# The worker might not actually finish the current one yet, but we put the new one now, so
|
283
|
-
# the worker can immediately pick it up when the current one is done
|
284
|
-
if isinstance(event, events.BeforeExecution) and not generator_done.is_set():
|
285
|
-
try:
|
286
|
-
tasks_queue.put(next(tasks_generator))
|
287
|
-
except StopIteration:
|
288
|
-
generator_done.set()
|
289
|
-
except KeyboardInterrupt:
|
290
|
-
stop_workers()
|
291
|
-
yield events.Interrupted()
|
292
|
-
|
293
|
-
def _init_workers(
|
294
|
-
self, tasks_queue: Queue, events_queue: Queue, results: TestResultSet, generator_done: threading.Event
|
295
|
-
) -> list[threading.Thread]:
|
296
|
-
"""Initialize & start workers that will execute tests."""
|
297
|
-
workers = [
|
298
|
-
threading.Thread(
|
299
|
-
target=self._get_task(),
|
300
|
-
kwargs=self._get_worker_kwargs(tasks_queue, events_queue, results, generator_done),
|
301
|
-
name=f"schemathesis_{num}",
|
302
|
-
)
|
303
|
-
for num in range(self.workers_num)
|
304
|
-
]
|
305
|
-
for worker in workers:
|
306
|
-
worker.start()
|
307
|
-
return workers
|
308
|
-
|
309
|
-
def _get_task(self) -> Callable:
|
310
|
-
return thread_task
|
311
|
-
|
312
|
-
def _get_worker_kwargs(
|
313
|
-
self, tasks_queue: Queue, events_queue: Queue, results: TestResultSet, generator_done: threading.Event
|
314
|
-
) -> dict[str, Any]:
|
315
|
-
return {
|
316
|
-
"tasks_queue": tasks_queue,
|
317
|
-
"events_queue": events_queue,
|
318
|
-
"generator_done": generator_done,
|
319
|
-
"checks": self.checks,
|
320
|
-
"targets": self.targets,
|
321
|
-
"settings": self.hypothesis_settings,
|
322
|
-
"generation_config": self.generation_config,
|
323
|
-
"auth": self.auth,
|
324
|
-
"auth_type": self.auth_type,
|
325
|
-
"headers": self.headers,
|
326
|
-
"seed": self.seed,
|
327
|
-
"results": results,
|
328
|
-
"stateful": self.stateful,
|
329
|
-
"stateful_recursion_limit": self.stateful_recursion_limit,
|
330
|
-
"data_generation_methods": self.schema.data_generation_methods,
|
331
|
-
"kwargs": {
|
332
|
-
"request_timeout": self.request_timeout,
|
333
|
-
"request_tls_verify": self.request_tls_verify,
|
334
|
-
"request_proxy": self.request_proxy,
|
335
|
-
"request_cert": self.request_cert,
|
336
|
-
"store_interactions": self.store_interactions,
|
337
|
-
"max_response_time": self.max_response_time,
|
338
|
-
"dry_run": self.dry_run,
|
339
|
-
},
|
340
|
-
}
|
341
|
-
|
342
|
-
|
343
|
-
class ThreadPoolWSGIRunner(ThreadPoolRunner):
|
344
|
-
def _get_task(self) -> Callable:
|
345
|
-
return wsgi_thread_task
|
346
|
-
|
347
|
-
def _get_worker_kwargs(
|
348
|
-
self, tasks_queue: Queue, events_queue: Queue, results: TestResultSet, generator_done: threading.Event
|
349
|
-
) -> dict[str, Any]:
|
350
|
-
return {
|
351
|
-
"tasks_queue": tasks_queue,
|
352
|
-
"events_queue": events_queue,
|
353
|
-
"generator_done": generator_done,
|
354
|
-
"checks": self.checks,
|
355
|
-
"targets": self.targets,
|
356
|
-
"settings": self.hypothesis_settings,
|
357
|
-
"generation_config": self.generation_config,
|
358
|
-
"seed": self.seed,
|
359
|
-
"results": results,
|
360
|
-
"stateful": self.stateful,
|
361
|
-
"stateful_recursion_limit": self.stateful_recursion_limit,
|
362
|
-
"data_generation_methods": self.schema.data_generation_methods,
|
363
|
-
"kwargs": {
|
364
|
-
"auth": self.auth,
|
365
|
-
"auth_type": self.auth_type,
|
366
|
-
"headers": self.headers,
|
367
|
-
"store_interactions": self.store_interactions,
|
368
|
-
"max_response_time": self.max_response_time,
|
369
|
-
"dry_run": self.dry_run,
|
370
|
-
},
|
371
|
-
}
|
372
|
-
|
373
|
-
|
374
|
-
class ThreadPoolASGIRunner(ThreadPoolRunner):
|
375
|
-
def _get_task(self) -> Callable:
|
376
|
-
return asgi_thread_task
|
377
|
-
|
378
|
-
def _get_worker_kwargs(
|
379
|
-
self, tasks_queue: Queue, events_queue: Queue, results: TestResultSet, generator_done: threading.Event
|
380
|
-
) -> dict[str, Any]:
|
381
|
-
return {
|
382
|
-
"tasks_queue": tasks_queue,
|
383
|
-
"events_queue": events_queue,
|
384
|
-
"generator_done": generator_done,
|
385
|
-
"checks": self.checks,
|
386
|
-
"targets": self.targets,
|
387
|
-
"settings": self.hypothesis_settings,
|
388
|
-
"generation_config": self.generation_config,
|
389
|
-
"headers": self.headers,
|
390
|
-
"seed": self.seed,
|
391
|
-
"results": results,
|
392
|
-
"stateful": self.stateful,
|
393
|
-
"stateful_recursion_limit": self.stateful_recursion_limit,
|
394
|
-
"data_generation_methods": self.schema.data_generation_methods,
|
395
|
-
"kwargs": {
|
396
|
-
"store_interactions": self.store_interactions,
|
397
|
-
"max_response_time": self.max_response_time,
|
398
|
-
"dry_run": self.dry_run,
|
399
|
-
},
|
400
|
-
}
|