schemathesis 3.39.7__py3-none-any.whl → 4.0.0a2__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.
Files changed (229) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +26 -68
  3. schemathesis/checks.py +130 -60
  4. schemathesis/cli/__init__.py +5 -2105
  5. schemathesis/cli/commands/__init__.py +37 -0
  6. schemathesis/cli/commands/run/__init__.py +662 -0
  7. schemathesis/cli/commands/run/checks.py +80 -0
  8. schemathesis/cli/commands/run/context.py +117 -0
  9. schemathesis/cli/commands/run/events.py +30 -0
  10. schemathesis/cli/commands/run/executor.py +141 -0
  11. schemathesis/cli/commands/run/filters.py +202 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/{cassettes.py → commands/run/handlers/cassettes.py} +178 -247
  15. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1368 -0
  17. schemathesis/cli/commands/run/hypothesis.py +105 -0
  18. schemathesis/cli/commands/run/loaders.py +129 -0
  19. schemathesis/cli/{callbacks.py → commands/run/validation.py} +59 -175
  20. schemathesis/cli/constants.py +5 -58
  21. schemathesis/cli/core.py +17 -0
  22. schemathesis/cli/ext/fs.py +14 -0
  23. schemathesis/cli/ext/groups.py +55 -0
  24. schemathesis/cli/{options.py → ext/options.py} +37 -16
  25. schemathesis/cli/hooks.py +36 -0
  26. schemathesis/contrib/__init__.py +1 -3
  27. schemathesis/contrib/openapi/__init__.py +1 -3
  28. schemathesis/contrib/openapi/fill_missing_examples.py +3 -7
  29. schemathesis/core/__init__.py +58 -0
  30. schemathesis/core/compat.py +25 -0
  31. schemathesis/core/control.py +2 -0
  32. schemathesis/core/curl.py +58 -0
  33. schemathesis/core/deserialization.py +65 -0
  34. schemathesis/core/errors.py +370 -0
  35. schemathesis/core/failures.py +315 -0
  36. schemathesis/core/fs.py +19 -0
  37. schemathesis/core/loaders.py +104 -0
  38. schemathesis/core/marks.py +66 -0
  39. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  40. schemathesis/{internal/output.py → core/output/__init__.py} +1 -0
  41. schemathesis/core/output/sanitization.py +197 -0
  42. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  43. schemathesis/core/registries.py +31 -0
  44. schemathesis/core/transforms.py +113 -0
  45. schemathesis/core/transport.py +108 -0
  46. schemathesis/core/validation.py +38 -0
  47. schemathesis/core/version.py +7 -0
  48. schemathesis/engine/__init__.py +30 -0
  49. schemathesis/engine/config.py +59 -0
  50. schemathesis/engine/context.py +119 -0
  51. schemathesis/engine/control.py +36 -0
  52. schemathesis/engine/core.py +157 -0
  53. schemathesis/engine/errors.py +394 -0
  54. schemathesis/engine/events.py +243 -0
  55. schemathesis/engine/phases/__init__.py +66 -0
  56. schemathesis/{runner → engine/phases}/probes.py +49 -68
  57. schemathesis/engine/phases/stateful/__init__.py +66 -0
  58. schemathesis/engine/phases/stateful/_executor.py +301 -0
  59. schemathesis/engine/phases/stateful/context.py +85 -0
  60. schemathesis/engine/phases/unit/__init__.py +175 -0
  61. schemathesis/engine/phases/unit/_executor.py +322 -0
  62. schemathesis/engine/phases/unit/_pool.py +74 -0
  63. schemathesis/engine/recorder.py +246 -0
  64. schemathesis/errors.py +31 -0
  65. schemathesis/experimental/__init__.py +9 -40
  66. schemathesis/filters.py +7 -95
  67. schemathesis/generation/__init__.py +3 -3
  68. schemathesis/generation/case.py +190 -0
  69. schemathesis/generation/coverage.py +22 -22
  70. schemathesis/{_patches.py → generation/hypothesis/__init__.py} +15 -6
  71. schemathesis/generation/hypothesis/builder.py +585 -0
  72. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  73. schemathesis/generation/hypothesis/given.py +66 -0
  74. schemathesis/generation/hypothesis/reporting.py +14 -0
  75. schemathesis/generation/hypothesis/strategies.py +16 -0
  76. schemathesis/generation/meta.py +115 -0
  77. schemathesis/generation/modes.py +28 -0
  78. schemathesis/generation/overrides.py +96 -0
  79. schemathesis/generation/stateful/__init__.py +20 -0
  80. schemathesis/{stateful → generation/stateful}/state_machine.py +84 -109
  81. schemathesis/generation/targets.py +69 -0
  82. schemathesis/graphql/__init__.py +15 -0
  83. schemathesis/graphql/checks.py +109 -0
  84. schemathesis/graphql/loaders.py +131 -0
  85. schemathesis/hooks.py +17 -62
  86. schemathesis/openapi/__init__.py +13 -0
  87. schemathesis/openapi/checks.py +387 -0
  88. schemathesis/openapi/generation/__init__.py +0 -0
  89. schemathesis/openapi/generation/filters.py +63 -0
  90. schemathesis/openapi/loaders.py +178 -0
  91. schemathesis/pytest/__init__.py +5 -0
  92. schemathesis/pytest/control_flow.py +7 -0
  93. schemathesis/pytest/lazy.py +273 -0
  94. schemathesis/pytest/loaders.py +12 -0
  95. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +94 -107
  96. schemathesis/python/__init__.py +0 -0
  97. schemathesis/python/asgi.py +12 -0
  98. schemathesis/python/wsgi.py +12 -0
  99. schemathesis/schemas.py +456 -228
  100. schemathesis/specs/graphql/__init__.py +0 -1
  101. schemathesis/specs/graphql/_cache.py +1 -2
  102. schemathesis/specs/graphql/scalars.py +5 -3
  103. schemathesis/specs/graphql/schemas.py +122 -123
  104. schemathesis/specs/graphql/validation.py +11 -17
  105. schemathesis/specs/openapi/__init__.py +6 -1
  106. schemathesis/specs/openapi/_cache.py +1 -2
  107. schemathesis/specs/openapi/_hypothesis.py +97 -134
  108. schemathesis/specs/openapi/checks.py +238 -219
  109. schemathesis/specs/openapi/converter.py +4 -4
  110. schemathesis/specs/openapi/definitions.py +1 -1
  111. schemathesis/specs/openapi/examples.py +22 -20
  112. schemathesis/specs/openapi/expressions/__init__.py +11 -15
  113. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  114. schemathesis/specs/openapi/expressions/nodes.py +33 -32
  115. schemathesis/specs/openapi/formats.py +3 -2
  116. schemathesis/specs/openapi/links.py +123 -299
  117. schemathesis/specs/openapi/media_types.py +10 -12
  118. schemathesis/specs/openapi/negative/__init__.py +2 -1
  119. schemathesis/specs/openapi/negative/mutations.py +3 -2
  120. schemathesis/specs/openapi/parameters.py +8 -6
  121. schemathesis/specs/openapi/patterns.py +1 -1
  122. schemathesis/specs/openapi/references.py +11 -51
  123. schemathesis/specs/openapi/schemas.py +177 -191
  124. schemathesis/specs/openapi/security.py +1 -1
  125. schemathesis/specs/openapi/serialization.py +10 -6
  126. schemathesis/specs/openapi/stateful/__init__.py +97 -91
  127. schemathesis/transport/__init__.py +104 -0
  128. schemathesis/transport/asgi.py +26 -0
  129. schemathesis/transport/prepare.py +99 -0
  130. schemathesis/transport/requests.py +221 -0
  131. schemathesis/{_xml.py → transport/serialization.py} +69 -7
  132. schemathesis/transport/wsgi.py +165 -0
  133. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +18 -14
  134. schemathesis-4.0.0a2.dist-info/RECORD +151 -0
  135. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +1 -1
  136. schemathesis/_compat.py +0 -74
  137. schemathesis/_dependency_versions.py +0 -19
  138. schemathesis/_hypothesis.py +0 -559
  139. schemathesis/_override.py +0 -50
  140. schemathesis/_rate_limiter.py +0 -7
  141. schemathesis/cli/context.py +0 -75
  142. schemathesis/cli/debug.py +0 -27
  143. schemathesis/cli/handlers.py +0 -19
  144. schemathesis/cli/junitxml.py +0 -124
  145. schemathesis/cli/output/__init__.py +0 -1
  146. schemathesis/cli/output/default.py +0 -936
  147. schemathesis/cli/output/short.py +0 -59
  148. schemathesis/cli/reporting.py +0 -79
  149. schemathesis/cli/sanitization.py +0 -26
  150. schemathesis/code_samples.py +0 -151
  151. schemathesis/constants.py +0 -56
  152. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  153. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  154. schemathesis/contrib/unique_data.py +0 -41
  155. schemathesis/exceptions.py +0 -571
  156. schemathesis/extra/_aiohttp.py +0 -28
  157. schemathesis/extra/_flask.py +0 -13
  158. schemathesis/extra/_server.py +0 -18
  159. schemathesis/failures.py +0 -277
  160. schemathesis/fixups/__init__.py +0 -37
  161. schemathesis/fixups/fast_api.py +0 -41
  162. schemathesis/fixups/utf8_bom.py +0 -28
  163. schemathesis/generation/_methods.py +0 -44
  164. schemathesis/graphql.py +0 -3
  165. schemathesis/internal/__init__.py +0 -7
  166. schemathesis/internal/checks.py +0 -84
  167. schemathesis/internal/copy.py +0 -32
  168. schemathesis/internal/datetime.py +0 -5
  169. schemathesis/internal/deprecation.py +0 -38
  170. schemathesis/internal/diff.py +0 -15
  171. schemathesis/internal/extensions.py +0 -27
  172. schemathesis/internal/jsonschema.py +0 -36
  173. schemathesis/internal/transformation.py +0 -26
  174. schemathesis/internal/validation.py +0 -34
  175. schemathesis/lazy.py +0 -474
  176. schemathesis/loaders.py +0 -122
  177. schemathesis/models.py +0 -1341
  178. schemathesis/parameters.py +0 -90
  179. schemathesis/runner/__init__.py +0 -605
  180. schemathesis/runner/events.py +0 -389
  181. schemathesis/runner/impl/__init__.py +0 -3
  182. schemathesis/runner/impl/context.py +0 -104
  183. schemathesis/runner/impl/core.py +0 -1246
  184. schemathesis/runner/impl/solo.py +0 -80
  185. schemathesis/runner/impl/threadpool.py +0 -391
  186. schemathesis/runner/serialization.py +0 -544
  187. schemathesis/sanitization.py +0 -252
  188. schemathesis/serializers.py +0 -328
  189. schemathesis/service/__init__.py +0 -18
  190. schemathesis/service/auth.py +0 -11
  191. schemathesis/service/ci.py +0 -202
  192. schemathesis/service/client.py +0 -133
  193. schemathesis/service/constants.py +0 -38
  194. schemathesis/service/events.py +0 -61
  195. schemathesis/service/extensions.py +0 -224
  196. schemathesis/service/hosts.py +0 -111
  197. schemathesis/service/metadata.py +0 -71
  198. schemathesis/service/models.py +0 -258
  199. schemathesis/service/report.py +0 -255
  200. schemathesis/service/serialization.py +0 -173
  201. schemathesis/service/usage.py +0 -66
  202. schemathesis/specs/graphql/loaders.py +0 -364
  203. schemathesis/specs/openapi/expressions/context.py +0 -16
  204. schemathesis/specs/openapi/loaders.py +0 -708
  205. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  206. schemathesis/specs/openapi/stateful/types.py +0 -14
  207. schemathesis/specs/openapi/validation.py +0 -26
  208. schemathesis/stateful/__init__.py +0 -147
  209. schemathesis/stateful/config.py +0 -97
  210. schemathesis/stateful/context.py +0 -135
  211. schemathesis/stateful/events.py +0 -274
  212. schemathesis/stateful/runner.py +0 -309
  213. schemathesis/stateful/sink.py +0 -68
  214. schemathesis/stateful/statistic.py +0 -22
  215. schemathesis/stateful/validation.py +0 -100
  216. schemathesis/targets.py +0 -77
  217. schemathesis/transports/__init__.py +0 -359
  218. schemathesis/transports/asgi.py +0 -7
  219. schemathesis/transports/auth.py +0 -38
  220. schemathesis/transports/headers.py +0 -36
  221. schemathesis/transports/responses.py +0 -57
  222. schemathesis/types.py +0 -44
  223. schemathesis/utils.py +0 -164
  224. schemathesis-3.39.7.dist-info/RECORD +0 -160
  225. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  226. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  227. /schemathesis/{internal → core}/result.py +0 -0
  228. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
  229. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,108 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import json
5
+ from typing import TYPE_CHECKING, Any, Mapping
6
+
7
+ from schemathesis.core.version import SCHEMATHESIS_VERSION
8
+
9
+ if TYPE_CHECKING:
10
+ import requests
11
+
12
+ USER_AGENT = f"schemathesis/{SCHEMATHESIS_VERSION}"
13
+ DEFAULT_RESPONSE_TIMEOUT = 10
14
+
15
+
16
+ def prepare_urlencoded(data: Any) -> Any:
17
+ if isinstance(data, list):
18
+ output = []
19
+ for item in data:
20
+ if isinstance(item, dict):
21
+ for key, value in item.items():
22
+ output.append((key, value))
23
+ else:
24
+ output.append((item, "arbitrary-value"))
25
+ return output
26
+ return data
27
+
28
+
29
+ class Response:
30
+ """Unified response for both testing and reporting purposes."""
31
+
32
+ __slots__ = (
33
+ "status_code",
34
+ "headers",
35
+ "content",
36
+ "request",
37
+ "elapsed",
38
+ "verify",
39
+ "_json",
40
+ "message",
41
+ "http_version",
42
+ "encoding",
43
+ "_encoded_body",
44
+ )
45
+
46
+ def __init__(
47
+ self,
48
+ status_code: int,
49
+ headers: Mapping[str, list[str]],
50
+ content: bytes,
51
+ request: requests.PreparedRequest,
52
+ elapsed: float,
53
+ verify: bool,
54
+ message: str = "",
55
+ http_version: str = "1.1",
56
+ encoding: str | None = None,
57
+ ):
58
+ self.status_code = status_code
59
+ self.headers = {key.lower(): value for key, value in headers.items()}
60
+ assert all(isinstance(v, list) for v in headers.values())
61
+ self.content = content
62
+ self.request = request
63
+ self.elapsed = elapsed
64
+ self.verify = verify
65
+ self._json = None
66
+ self._encoded_body: str | None = None
67
+ self.message = message
68
+ self.http_version = http_version
69
+ self.encoding = encoding
70
+
71
+ @classmethod
72
+ def from_requests(cls, response: requests.Response, verify: bool) -> Response:
73
+ raw = response.raw
74
+ raw_headers = raw.headers if raw is not None else {}
75
+ headers = {name: response.raw.headers.getlist(name) for name in raw_headers.keys()}
76
+ # Similar to http.client:319 (HTTP version detection in stdlib's `http` package)
77
+ version = raw.version if raw is not None else 10
78
+ http_version = "1.0" if version == 10 else "1.1"
79
+ return Response(
80
+ status_code=response.status_code,
81
+ headers=headers,
82
+ content=response.content,
83
+ request=response.request,
84
+ elapsed=response.elapsed.total_seconds(),
85
+ message=response.reason,
86
+ encoding=response.encoding,
87
+ http_version=http_version,
88
+ verify=verify,
89
+ )
90
+
91
+ @property
92
+ def text(self) -> str:
93
+ return self.content.decode(self.encoding if self.encoding else "utf-8")
94
+
95
+ def json(self) -> Any:
96
+ if self._json is None:
97
+ self._json = json.loads(self.text)
98
+ return self._json
99
+
100
+ @property
101
+ def body_size(self) -> int | None:
102
+ return len(self.content) if self.content else None
103
+
104
+ @property
105
+ def encoded_body(self) -> str | None:
106
+ if self._encoded_body is None and self.content:
107
+ self._encoded_body = base64.b64encode(self.content).decode()
108
+ return self._encoded_body
@@ -0,0 +1,38 @@
1
+ import re
2
+
3
+ # Adapted from http.client._is_illegal_header_value
4
+ INVALID_HEADER_RE = re.compile(r"\n(?![ \t])|\r(?![ \t\n])")
5
+
6
+
7
+ def has_invalid_characters(name: str, value: object) -> bool:
8
+ from requests.exceptions import InvalidHeader
9
+ from requests.utils import check_header_validity
10
+
11
+ if not isinstance(value, str):
12
+ return False
13
+ try:
14
+ check_header_validity((name, value))
15
+ return bool(INVALID_HEADER_RE.search(value))
16
+ except InvalidHeader:
17
+ return True
18
+
19
+
20
+ def is_latin_1_encodable(value: object) -> bool:
21
+ """Check if a value is a Latin-1 encodable string."""
22
+ if not isinstance(value, str):
23
+ return False
24
+ try:
25
+ value.encode("latin-1")
26
+ return True
27
+ except UnicodeEncodeError:
28
+ return False
29
+
30
+
31
+ SURROGATE_PAIR_RE = re.compile(r"[\ud800-\udfff]")
32
+ _contains_surrogate_pair = SURROGATE_PAIR_RE.search
33
+
34
+
35
+ def contains_unicode_surrogate_pair(item: object) -> bool:
36
+ if isinstance(item, list):
37
+ return any(isinstance(item_, str) and bool(_contains_surrogate_pair(item_)) for item_ in item)
38
+ return isinstance(item, str) and bool(_contains_surrogate_pair(item))
@@ -0,0 +1,7 @@
1
+ from importlib import metadata
2
+
3
+ try:
4
+ SCHEMATHESIS_VERSION = metadata.version(__package__)
5
+ except metadata.PackageNotFoundError:
6
+ # Local run without installation
7
+ SCHEMATHESIS_VERSION = "dev"
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+ from typing import TYPE_CHECKING
5
+
6
+ from schemathesis.engine.config import EngineConfig
7
+
8
+ if TYPE_CHECKING:
9
+ from schemathesis.engine.core import Engine
10
+ from schemathesis.schemas import BaseSchema
11
+
12
+
13
+ class Status(str, Enum):
14
+ SUCCESS = "success"
15
+ FAILURE = "failure"
16
+ ERROR = "error"
17
+ INTERRUPTED = "interrupted"
18
+ SKIP = "skip"
19
+
20
+ def __lt__(self, other: Status) -> bool: # type: ignore[override]
21
+ return _STATUS_ORDER[self] < _STATUS_ORDER[other]
22
+
23
+
24
+ _STATUS_ORDER = {Status.SUCCESS: 0, Status.FAILURE: 1, Status.ERROR: 2, Status.INTERRUPTED: 3, Status.SKIP: 4}
25
+
26
+
27
+ def from_schema(schema: BaseSchema, *, config: EngineConfig | None = None) -> Engine:
28
+ from .core import Engine
29
+
30
+ return Engine(schema=schema, config=config or EngineConfig())
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from schemathesis.checks import CheckFunction, not_a_server_error
7
+ from schemathesis.engine.phases import PhaseName
8
+ from schemathesis.generation import GenerationConfig
9
+ from schemathesis.generation.overrides import Override
10
+
11
+ if TYPE_CHECKING:
12
+ import hypothesis
13
+
14
+ from schemathesis.checks import ChecksConfig
15
+ from schemathesis.generation.targets import TargetFunction
16
+
17
+
18
+ def _default_hypothesis_settings() -> hypothesis.settings:
19
+ import hypothesis
20
+
21
+ return hypothesis.settings(deadline=None)
22
+
23
+
24
+ @dataclass
25
+ class ExecutionConfig:
26
+ """Configuration for test execution."""
27
+
28
+ phases: list[PhaseName] = field(default_factory=lambda: [PhaseName.UNIT_TESTING, PhaseName.STATEFUL_TESTING])
29
+ checks: list[CheckFunction] = field(default_factory=lambda: [not_a_server_error])
30
+ targets: list[TargetFunction] = field(default_factory=list)
31
+ hypothesis_settings: hypothesis.settings = field(default_factory=_default_hypothesis_settings)
32
+ generation: GenerationConfig = field(default_factory=GenerationConfig)
33
+ max_failures: int | None = None
34
+ unique_inputs: bool = False
35
+ no_failfast: bool = False
36
+ seed: int | None = None
37
+ workers_num: int = 1
38
+
39
+
40
+ @dataclass
41
+ class NetworkConfig:
42
+ """Network-related configuration."""
43
+
44
+ auth: tuple[str, str] | None = None
45
+ headers: dict[str, Any] = field(default_factory=dict)
46
+ timeout: int | None = None
47
+ tls_verify: bool | str = True
48
+ proxy: str | None = None
49
+ cert: str | tuple[str, str] | None = None
50
+
51
+
52
+ @dataclass
53
+ class EngineConfig:
54
+ """Complete engine configuration."""
55
+
56
+ execution: ExecutionConfig = field(default_factory=ExecutionConfig)
57
+ network: NetworkConfig = field(default_factory=NetworkConfig)
58
+ checks_config: ChecksConfig = field(default_factory=dict)
59
+ override: Override | None = None
@@ -0,0 +1,119 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from dataclasses import dataclass
5
+ from functools import cached_property
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from schemathesis.checks import CheckContext
9
+ from schemathesis.core import NOT_SET, NotSet
10
+ from schemathesis.engine.recorder import ScenarioRecorder
11
+ from schemathesis.generation.case import Case
12
+ from schemathesis.schemas import BaseSchema
13
+
14
+ from .control import ExecutionControl
15
+
16
+ if TYPE_CHECKING:
17
+ import threading
18
+
19
+ import requests
20
+
21
+ from schemathesis.engine.config import EngineConfig
22
+
23
+
24
+ @dataclass
25
+ class EngineContext:
26
+ """Holds context shared for a test run."""
27
+
28
+ schema: BaseSchema
29
+ control: ExecutionControl
30
+ outcome_cache: dict[int, BaseException | None]
31
+ config: EngineConfig
32
+ start_time: float
33
+
34
+ def __init__(
35
+ self,
36
+ *,
37
+ schema: BaseSchema,
38
+ stop_event: threading.Event,
39
+ config: EngineConfig,
40
+ session: requests.Session | None = None,
41
+ ) -> None:
42
+ self.schema = schema
43
+ self.control = ExecutionControl(stop_event=stop_event, max_failures=config.execution.max_failures)
44
+ self.outcome_cache = {}
45
+ self.config = config
46
+ self.start_time = time.monotonic()
47
+ self._session = session
48
+
49
+ def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
50
+
51
+ @property
52
+ def running_time(self) -> float:
53
+ return time.monotonic() - self.start_time
54
+
55
+ @property
56
+ def has_to_stop(self) -> bool:
57
+ """Check if execution should stop."""
58
+ return self.control.is_stopped
59
+
60
+ @property
61
+ def is_interrupted(self) -> bool:
62
+ return self.control.is_interrupted
63
+
64
+ @property
65
+ def has_reached_the_failure_limit(self) -> bool:
66
+ return self.control.has_reached_the_failure_limit
67
+
68
+ def stop(self) -> None:
69
+ self.control.stop()
70
+
71
+ def cache_outcome(self, case: Case, outcome: BaseException | None) -> None:
72
+ self.outcome_cache[hash(case)] = outcome
73
+
74
+ def get_cached_outcome(self, case: Case) -> BaseException | None | NotSet:
75
+ return self.outcome_cache.get(hash(case), NOT_SET)
76
+
77
+ @cached_property
78
+ def session(self) -> requests.Session:
79
+ if self._session is not None:
80
+ return self._session
81
+ import requests
82
+
83
+ session = requests.Session()
84
+ config = self.config.network
85
+ session.verify = config.tls_verify
86
+ if config.auth is not None:
87
+ session.auth = config.auth
88
+ if config.headers:
89
+ session.headers.update(config.headers)
90
+ if config.cert is not None:
91
+ session.cert = config.cert
92
+ if config.proxy is not None:
93
+ session.proxies["all"] = config.proxy
94
+ return session
95
+
96
+ @property
97
+ def transport_kwargs(self) -> dict[str, Any]:
98
+ kwargs: dict[str, Any] = {
99
+ "session": self.session,
100
+ "headers": self.config.network.headers,
101
+ "timeout": self.config.network.timeout,
102
+ "verify": self.config.network.tls_verify,
103
+ "cert": self.config.network.cert,
104
+ }
105
+ if self.config.network.proxy is not None:
106
+ kwargs["proxies"] = {"all": self.config.network.proxy}
107
+ return kwargs
108
+
109
+ def get_check_context(self, recorder: ScenarioRecorder) -> CheckContext:
110
+ from requests.models import CaseInsensitiveDict
111
+
112
+ return CheckContext(
113
+ override=self.config.override,
114
+ auth=self.config.network.auth,
115
+ headers=CaseInsensitiveDict(self.config.network.headers) if self.config.network.headers else None,
116
+ config=self.config.checks_config,
117
+ transport_kwargs=self.transport_kwargs,
118
+ recorder=recorder,
119
+ )
@@ -0,0 +1,36 @@
1
+ """Control for the Schemathesis Engine execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ from dataclasses import dataclass
7
+
8
+
9
+ @dataclass
10
+ class ExecutionControl:
11
+ """Controls engine execution flow and tracks failures."""
12
+
13
+ stop_event: threading.Event
14
+ max_failures: int | None
15
+ _failures_counter: int = 0
16
+ has_reached_the_failure_limit: bool = False
17
+
18
+ @property
19
+ def is_stopped(self) -> bool:
20
+ """Check if execution should stop."""
21
+ return self.is_interrupted or self.has_reached_the_failure_limit
22
+
23
+ @property
24
+ def is_interrupted(self) -> bool:
25
+ return self.stop_event.is_set()
26
+
27
+ def stop(self) -> None:
28
+ """Signal to stop execution."""
29
+ self.stop_event.set()
30
+
31
+ def count_failure(self) -> None:
32
+ # N failures limit
33
+ if self.max_failures is not None:
34
+ self._failures_counter += 1
35
+ if self._failures_counter >= self.max_failures:
36
+ self.has_reached_the_failure_limit = True
@@ -0,0 +1,157 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ from dataclasses import dataclass
5
+ from typing import Sequence
6
+
7
+ from schemathesis.auths import unregister as unregister_auth
8
+ from schemathesis.core import SpecificationFeature
9
+ from schemathesis.engine import Status, events, phases
10
+ from schemathesis.schemas import BaseSchema
11
+
12
+ from .config import EngineConfig
13
+ from .context import EngineContext
14
+ from .events import EventGenerator
15
+ from .phases import Phase, PhaseName, PhaseSkipReason
16
+
17
+
18
+ @dataclass
19
+ class Engine:
20
+ schema: BaseSchema
21
+ config: EngineConfig
22
+
23
+ def execute(self) -> EventStream:
24
+ """Execute all test phases."""
25
+ # Unregister auth if explicitly provided
26
+ if self.config.network.auth is not None:
27
+ unregister_auth()
28
+
29
+ ctx = EngineContext(schema=self.schema, stop_event=threading.Event(), config=self.config)
30
+ plan = self._create_execution_plan()
31
+ return EventStream(plan.execute(ctx), ctx.control.stop_event)
32
+
33
+ def _create_execution_plan(self) -> ExecutionPlan:
34
+ """Create execution plan based on configuration."""
35
+ phases = [
36
+ self.get_phase_config(PhaseName.PROBING, is_supported=True, requires_links=False),
37
+ self.get_phase_config(PhaseName.UNIT_TESTING, is_supported=True, requires_links=False),
38
+ self.get_phase_config(
39
+ PhaseName.STATEFUL_TESTING,
40
+ is_supported=self.schema.specification.supports_feature(SpecificationFeature.STATEFUL_TESTING),
41
+ requires_links=True,
42
+ ),
43
+ ]
44
+ return ExecutionPlan(phases)
45
+
46
+ def get_phase_config(
47
+ self,
48
+ phase_name: PhaseName,
49
+ *,
50
+ is_supported: bool = True,
51
+ requires_links: bool = False,
52
+ ) -> Phase:
53
+ """Helper to determine phase configuration with proper skip reasons."""
54
+ # Check if feature is supported by the schema
55
+ if not is_supported:
56
+ return Phase(
57
+ name=phase_name,
58
+ is_supported=False,
59
+ is_enabled=False,
60
+ skip_reason=PhaseSkipReason.NOT_SUPPORTED,
61
+ )
62
+
63
+ if phase_name not in self.config.execution.phases:
64
+ return Phase(
65
+ name=phase_name,
66
+ is_supported=True,
67
+ is_enabled=False,
68
+ skip_reason=PhaseSkipReason.DISABLED,
69
+ )
70
+
71
+ if requires_links and self.schema.statistic.links.selected == 0:
72
+ return Phase(
73
+ name=phase_name,
74
+ is_supported=True,
75
+ is_enabled=False,
76
+ skip_reason=PhaseSkipReason.NOT_APPLICABLE,
77
+ )
78
+
79
+ # Phase can be executed
80
+ return Phase(
81
+ name=phase_name,
82
+ is_supported=True,
83
+ is_enabled=True,
84
+ skip_reason=None,
85
+ )
86
+
87
+
88
+ @dataclass
89
+ class ExecutionPlan:
90
+ """Manages test execution phases."""
91
+
92
+ phases: Sequence[Phase]
93
+
94
+ def execute(self, engine: EngineContext) -> EventGenerator:
95
+ """Execute all phases in sequence."""
96
+ yield events.EngineStarted()
97
+ try:
98
+ if engine.is_interrupted:
99
+ yield from self._finish(engine)
100
+ return
101
+ if engine.is_interrupted:
102
+ yield from self._finish(engine) # type: ignore[unreachable]
103
+ return
104
+
105
+ # Run main phases
106
+ for phase in self.phases:
107
+ if engine.has_reached_the_failure_limit:
108
+ phase.skip_reason = PhaseSkipReason.FAILURE_LIMIT_REACHED
109
+ yield events.PhaseStarted(phase=phase)
110
+ if phase.should_execute(engine):
111
+ yield from phases.execute(engine, phase)
112
+ else:
113
+ if engine.has_reached_the_failure_limit:
114
+ phase.skip_reason = PhaseSkipReason.FAILURE_LIMIT_REACHED
115
+ yield events.PhaseFinished(phase=phase, status=Status.SKIP, payload=None)
116
+ if engine.is_interrupted:
117
+ break # type: ignore[unreachable]
118
+
119
+ except KeyboardInterrupt:
120
+ engine.stop()
121
+ yield events.Interrupted(phase=None)
122
+
123
+ # Always finish
124
+ yield from self._finish(engine)
125
+
126
+ def _finish(self, ctx: EngineContext) -> EventGenerator:
127
+ """Finish the test run."""
128
+ yield events.EngineFinished(running_time=ctx.running_time)
129
+
130
+
131
+ @dataclass
132
+ class EventStream:
133
+ """Schemathesis event stream.
134
+
135
+ Provides an API to control the execution flow.
136
+ """
137
+
138
+ generator: EventGenerator
139
+ stop_event: threading.Event
140
+
141
+ def __next__(self) -> events.EngineEvent:
142
+ return next(self.generator)
143
+
144
+ def __iter__(self) -> EventGenerator:
145
+ return self.generator
146
+
147
+ def stop(self) -> None:
148
+ """Stop the event stream.
149
+
150
+ Its next value will be the last one (Finished).
151
+ """
152
+ self.stop_event.set()
153
+
154
+ def finish(self) -> events.EngineEvent:
155
+ """Stop the event stream & return the last event."""
156
+ self.stop()
157
+ return next(self)