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,175 @@
1
+ """Unit testing by Schemathesis Engine.
2
+
3
+ This module provides high-level flow for single-, and multi-threaded modes.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import queue
9
+ import uuid
10
+ import warnings
11
+ from queue import Queue
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ from schemathesis.core.result import Ok
15
+ from schemathesis.engine import Status, events
16
+ from schemathesis.engine.phases import PhaseName, PhaseSkipReason
17
+ from schemathesis.engine.recorder import ScenarioRecorder
18
+ from schemathesis.generation.hypothesis.builder import HypothesisTestConfig
19
+ from schemathesis.generation.hypothesis.reporting import ignore_hypothesis_output
20
+
21
+ from ._pool import TaskProducer, WorkerPool
22
+
23
+ if TYPE_CHECKING:
24
+ from schemathesis.engine.context import EngineContext
25
+ from schemathesis.engine.phases import Phase
26
+ from schemathesis.schemas import APIOperation
27
+
28
+ WORKER_TIMEOUT = 0.1
29
+
30
+
31
+ def execute(engine: EngineContext, phase: Phase) -> events.EventGenerator:
32
+ """Run a set of unit tests.
33
+
34
+ Implemented as a producer-consumer pattern via a task queue.
35
+ The main thread provides an iterator over API operations and worker threads create test functions and run them.
36
+ """
37
+ producer = TaskProducer(engine)
38
+ workers_num = engine.config.execution.workers_num
39
+
40
+ suite_started = events.SuiteStarted(phase=phase.name)
41
+
42
+ yield suite_started
43
+
44
+ status = None
45
+ is_executed = False
46
+
47
+ with WorkerPool(
48
+ workers_num=workers_num, producer=producer, worker_factory=worker_task, ctx=engine, suite_id=suite_started.id
49
+ ) as pool:
50
+ try:
51
+ while True:
52
+ try:
53
+ event = pool.events_queue.get(timeout=WORKER_TIMEOUT)
54
+ is_executed = True
55
+ if engine.is_interrupted:
56
+ raise KeyboardInterrupt
57
+ yield event
58
+ if isinstance(event, events.NonFatalError):
59
+ status = Status.ERROR
60
+ if isinstance(event, events.ScenarioFinished):
61
+ if event.status != Status.SKIP and (status is None or status < event.status):
62
+ status = event.status
63
+ if event.status in (Status.ERROR, Status.FAILURE):
64
+ engine.control.count_failure()
65
+ if isinstance(event, events.Interrupted) or engine.is_interrupted:
66
+ status = Status.INTERRUPTED
67
+ engine.stop()
68
+ if engine.has_to_stop:
69
+ break # type: ignore[unreachable]
70
+ except queue.Empty:
71
+ if all(not worker.is_alive() for worker in pool.workers):
72
+ break
73
+ continue
74
+ except KeyboardInterrupt:
75
+ engine.stop()
76
+ status = Status.INTERRUPTED
77
+ yield events.Interrupted(phase=PhaseName.UNIT_TESTING)
78
+
79
+ if not is_executed:
80
+ phase.skip_reason = PhaseSkipReason.NOTHING_TO_TEST
81
+ status = Status.SKIP
82
+ elif status is None:
83
+ status = Status.SKIP
84
+ # NOTE: Right now there is just one suite, hence two events go one after another
85
+ yield events.SuiteFinished(id=suite_started.id, phase=phase.name, status=status)
86
+ yield events.PhaseFinished(phase=phase, status=status, payload=None)
87
+
88
+
89
+ def worker_task(*, events_queue: Queue, producer: TaskProducer, ctx: EngineContext, suite_id: uuid.UUID) -> None:
90
+ from hypothesis.errors import HypothesisWarning
91
+
92
+ from schemathesis.generation.hypothesis.builder import create_test
93
+
94
+ from ._executor import run_test, test_func
95
+
96
+ warnings.filterwarnings("ignore", message="The recursion limit will not be reset", category=HypothesisWarning)
97
+ with ignore_hypothesis_output():
98
+ try:
99
+ while not ctx.has_to_stop:
100
+ result = producer.next_operation()
101
+ if result is None:
102
+ break
103
+
104
+ if isinstance(result, Ok):
105
+ operation = result.ok()
106
+ as_strategy_kwargs = get_strategy_kwargs(ctx, operation)
107
+ test_function = create_test(
108
+ operation=operation,
109
+ test_func=test_func,
110
+ config=HypothesisTestConfig(
111
+ settings=ctx.config.execution.hypothesis_settings,
112
+ seed=ctx.config.execution.seed,
113
+ generation=ctx.config.execution.generation,
114
+ as_strategy_kwargs=as_strategy_kwargs,
115
+ ),
116
+ )
117
+
118
+ # The test is blocking, meaning that even if CTRL-C comes to the main thread, this tasks will continue
119
+ # executing. However, as we set a stop event, it will be checked before the next network request.
120
+ # However, this is still suboptimal, as there could be slow requests and they will block for longer
121
+ for event in run_test(operation=operation, test_function=test_function, ctx=ctx, suite_id=suite_id):
122
+ events_queue.put(event)
123
+ else:
124
+ error = result.err()
125
+ if error.method:
126
+ label = f"{error.method.upper()} {error.full_path}"
127
+ scenario_started = events.ScenarioStarted(
128
+ label=label, phase=PhaseName.UNIT_TESTING, suite_id=suite_id
129
+ )
130
+ events_queue.put(scenario_started)
131
+
132
+ events_queue.put(
133
+ events.NonFatalError(
134
+ error=error, phase=PhaseName.UNIT_TESTING, label=label, related_to_operation=True
135
+ )
136
+ )
137
+
138
+ events_queue.put(
139
+ events.ScenarioFinished(
140
+ id=scenario_started.id,
141
+ suite_id=suite_id,
142
+ phase=PhaseName.UNIT_TESTING,
143
+ label=label,
144
+ status=Status.ERROR,
145
+ recorder=ScenarioRecorder(label="Error"),
146
+ elapsed_time=0.0,
147
+ skip_reason=None,
148
+ is_final=True,
149
+ )
150
+ )
151
+ else:
152
+ assert error.full_path is not None
153
+ events_queue.put(
154
+ events.NonFatalError(
155
+ error=error,
156
+ phase=PhaseName.UNIT_TESTING,
157
+ label=error.full_path,
158
+ related_to_operation=False,
159
+ )
160
+ )
161
+ except KeyboardInterrupt:
162
+ events_queue.put(events.Interrupted(phase=PhaseName.UNIT_TESTING))
163
+
164
+
165
+ def get_strategy_kwargs(ctx: EngineContext, operation: APIOperation) -> dict[str, Any]:
166
+ kwargs = {}
167
+ if ctx.config.override is not None:
168
+ for location, entry in ctx.config.override.for_operation(operation).items():
169
+ if entry:
170
+ kwargs[location] = entry
171
+ if ctx.config.network.headers:
172
+ kwargs["headers"] = {
173
+ key: value for key, value in ctx.config.network.headers.items() if key.lower() != "user-agent"
174
+ }
175
+ return kwargs
@@ -0,0 +1,322 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ import unittest
5
+ import uuid
6
+ from typing import TYPE_CHECKING, Callable, Iterable
7
+ from warnings import WarningMessage, catch_warnings
8
+
9
+ import requests
10
+ from hypothesis.errors import InvalidArgument
11
+ from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
12
+ from jsonschema.exceptions import SchemaError as JsonSchemaError
13
+ from jsonschema.exceptions import ValidationError
14
+
15
+ from schemathesis.checks import CheckContext, CheckFunction, run_checks
16
+ from schemathesis.core.compat import BaseExceptionGroup
17
+ from schemathesis.core.control import SkipTest
18
+ from schemathesis.core.errors import (
19
+ SERIALIZERS_SUGGESTION_MESSAGE,
20
+ InternalError,
21
+ InvalidHeadersExample,
22
+ InvalidRegexPattern,
23
+ InvalidRegexType,
24
+ InvalidSchema,
25
+ MalformedMediaType,
26
+ SerializationNotPossible,
27
+ )
28
+ from schemathesis.core.failures import Failure, FailureGroup
29
+ from schemathesis.core.transport import Response
30
+ from schemathesis.engine import Status, events
31
+ from schemathesis.engine.context import EngineContext
32
+ from schemathesis.engine.errors import (
33
+ DeadlineExceeded,
34
+ UnexpectedError,
35
+ UnsupportedRecursiveReference,
36
+ deduplicate_errors,
37
+ )
38
+ from schemathesis.engine.phases import PhaseName
39
+ from schemathesis.engine.recorder import ScenarioRecorder
40
+ from schemathesis.generation import targets
41
+ from schemathesis.generation.case import Case
42
+ from schemathesis.generation.hypothesis.builder import (
43
+ InvalidHeadersExampleMark,
44
+ InvalidRegexMark,
45
+ NonSerializableMark,
46
+ UnsatisfiableExampleMark,
47
+ )
48
+ from schemathesis.generation.hypothesis.reporting import ignore_hypothesis_output
49
+
50
+ if TYPE_CHECKING:
51
+ from schemathesis.schemas import APIOperation
52
+
53
+
54
+ def run_test(
55
+ *, operation: APIOperation, test_function: Callable, ctx: EngineContext, suite_id: uuid.UUID
56
+ ) -> events.EventGenerator:
57
+ """A single test run with all error handling needed."""
58
+ import hypothesis.errors
59
+
60
+ # To simplify connecting `before` and `after` events in external systems
61
+ scenario_started = events.ScenarioStarted(label=operation.label, phase=PhaseName.UNIT_TESTING, suite_id=suite_id)
62
+ yield scenario_started
63
+ errors: list[Exception] = []
64
+ skip_reason = None
65
+ test_start_time = time.monotonic()
66
+ recorder = ScenarioRecorder(label=operation.label)
67
+
68
+ def non_fatal_error(error: Exception) -> events.NonFatalError:
69
+ return events.NonFatalError(
70
+ error=error, phase=PhaseName.UNIT_TESTING, label=operation.label, related_to_operation=True
71
+ )
72
+
73
+ try:
74
+ setup_hypothesis_database_key(test_function, operation)
75
+ with catch_warnings(record=True) as warnings, ignore_hypothesis_output():
76
+ test_function(ctx=ctx, errors=errors, recorder=recorder)
77
+ # Test body was not executed at all - Hypothesis did not generate any tests, but there is no error
78
+ status = Status.SUCCESS
79
+ except (SkipTest, unittest.case.SkipTest) as exc:
80
+ status = Status.SKIP
81
+ skip_reason = {"Hypothesis has been told to run no examples for this test.": "No examples in schema"}.get(
82
+ str(exc), str(exc)
83
+ )
84
+ except (FailureGroup, Failure):
85
+ status = Status.FAILURE
86
+ except UnexpectedError:
87
+ # It could be an error in user-defined extensions, network errors or internal Schemathesis errors
88
+ status = Status.ERROR
89
+ for idx, err in enumerate(errors):
90
+ if isinstance(err, MalformedMediaType):
91
+ errors[idx] = InvalidSchema(str(err))
92
+ except hypothesis.errors.Flaky as exc:
93
+ if isinstance(exc.__cause__, hypothesis.errors.DeadlineExceeded):
94
+ status = Status.ERROR
95
+ yield non_fatal_error(DeadlineExceeded.from_exc(exc.__cause__))
96
+ elif isinstance(exc, hypothesis.errors.FlakyFailure) and any(
97
+ isinstance(subexc, hypothesis.errors.DeadlineExceeded) for subexc in exc.exceptions
98
+ ):
99
+ for sub_exc in exc.exceptions:
100
+ if isinstance(sub_exc, hypothesis.errors.DeadlineExceeded):
101
+ yield non_fatal_error(DeadlineExceeded.from_exc(sub_exc))
102
+ status = Status.ERROR
103
+ elif errors:
104
+ status = Status.ERROR
105
+ else:
106
+ status = Status.FAILURE
107
+ except BaseExceptionGroup:
108
+ status = Status.ERROR
109
+ except hypothesis.errors.Unsatisfiable:
110
+ # We need more clear error message here
111
+ status = Status.ERROR
112
+ yield non_fatal_error(hypothesis.errors.Unsatisfiable("Failed to generate test cases for this API operation"))
113
+ except KeyboardInterrupt:
114
+ yield events.Interrupted(phase=PhaseName.UNIT_TESTING)
115
+ return
116
+ except AssertionError as exc: # May come from `hypothesis-jsonschema` or `hypothesis`
117
+ status = Status.ERROR
118
+ try:
119
+ operation.schema.validate()
120
+ msg = "Unexpected error during testing of this API operation"
121
+ exc_msg = str(exc)
122
+ if exc_msg:
123
+ msg += f": {exc_msg}"
124
+ try:
125
+ raise InternalError(msg) from exc
126
+ except InternalError as exc:
127
+ yield non_fatal_error(exc)
128
+ except ValidationError as exc:
129
+ yield non_fatal_error(
130
+ InvalidSchema.from_jsonschema_error(
131
+ exc,
132
+ path=operation.path,
133
+ method=operation.method,
134
+ full_path=operation.schema.get_full_path(operation.path),
135
+ )
136
+ )
137
+ except HypothesisRefResolutionError:
138
+ status = Status.ERROR
139
+ yield non_fatal_error(UnsupportedRecursiveReference())
140
+ except InvalidArgument as exc:
141
+ status = Status.ERROR
142
+ message = get_invalid_regular_expression_message(warnings)
143
+ if message:
144
+ # `hypothesis-jsonschema` emits a warning on invalid regular expression syntax
145
+ yield non_fatal_error(InvalidRegexPattern.from_hypothesis_jsonschema_message(message))
146
+ else:
147
+ yield non_fatal_error(exc)
148
+ except hypothesis.errors.DeadlineExceeded as exc:
149
+ status = Status.ERROR
150
+ yield non_fatal_error(DeadlineExceeded.from_exc(exc))
151
+ except JsonSchemaError as exc:
152
+ status = Status.ERROR
153
+ yield non_fatal_error(InvalidRegexPattern.from_schema_error(exc, from_examples=False))
154
+ except Exception as exc:
155
+ status = Status.ERROR
156
+ # Likely a YAML parsing issue. E.g. `00:00:00.00` (without quotes) is parsed as float `0.0`
157
+ if str(exc) == "first argument must be string or compiled pattern":
158
+ yield non_fatal_error(
159
+ InvalidRegexType(
160
+ "Invalid `pattern` value: expected a string. "
161
+ "If your schema is in YAML, ensure `pattern` values are quoted",
162
+ )
163
+ )
164
+ else:
165
+ yield non_fatal_error(exc)
166
+ if (
167
+ status == Status.SUCCESS
168
+ and ctx.config.execution.no_failfast
169
+ and any(check.status == Status.FAILURE for checks in recorder.checks.values() for check in checks)
170
+ ):
171
+ status = Status.FAILURE
172
+ if UnsatisfiableExampleMark.is_set(test_function):
173
+ status = Status.ERROR
174
+ yield non_fatal_error(
175
+ hypothesis.errors.Unsatisfiable("Failed to generate test cases from examples for this API operation")
176
+ )
177
+ non_serializable = NonSerializableMark.get(test_function)
178
+ if non_serializable is not None and status != Status.ERROR:
179
+ status = Status.ERROR
180
+ media_types = ", ".join(non_serializable.media_types)
181
+ yield non_fatal_error(
182
+ SerializationNotPossible(
183
+ "Failed to generate test cases from examples for this API operation because of"
184
+ f" unsupported payload media types: {media_types}\n{SERIALIZERS_SUGGESTION_MESSAGE}",
185
+ media_types=non_serializable.media_types,
186
+ )
187
+ )
188
+
189
+ invalid_regex = InvalidRegexMark.get(test_function)
190
+ if invalid_regex is not None and status != Status.ERROR:
191
+ status = Status.ERROR
192
+ yield non_fatal_error(InvalidRegexPattern.from_schema_error(invalid_regex, from_examples=True))
193
+ invalid_headers = InvalidHeadersExampleMark.get(test_function)
194
+ if invalid_headers:
195
+ status = Status.ERROR
196
+ yield non_fatal_error(InvalidHeadersExample.from_headers(invalid_headers))
197
+ test_elapsed_time = time.monotonic() - test_start_time
198
+ for error in deduplicate_errors(errors):
199
+ yield non_fatal_error(error)
200
+ yield events.ScenarioFinished(
201
+ id=scenario_started.id,
202
+ suite_id=suite_id,
203
+ phase=PhaseName.UNIT_TESTING,
204
+ label=operation.label,
205
+ recorder=recorder,
206
+ status=status,
207
+ elapsed_time=test_elapsed_time,
208
+ skip_reason=skip_reason,
209
+ is_final=False,
210
+ )
211
+
212
+
213
+ def setup_hypothesis_database_key(test: Callable, operation: APIOperation) -> None:
214
+ """Make Hypothesis use separate database entries for every API operation.
215
+
216
+ It increases the effectiveness of the Hypothesis database in the CLI.
217
+ """
218
+ # Hypothesis's function digest depends on the test function signature. To reflect it for the web API case,
219
+ # we use all API operation parameters in the digest.
220
+ extra = operation.label.encode("utf8")
221
+ for parameter in operation.iter_parameters():
222
+ extra += parameter.serialize(operation).encode("utf8")
223
+ test.hypothesis.inner_test._hypothesis_internal_add_digest = extra # type: ignore
224
+
225
+
226
+ def get_invalid_regular_expression_message(warnings: list[WarningMessage]) -> str | None:
227
+ for warning in warnings:
228
+ message = str(warning.message)
229
+ if "is not valid syntax for a Python regular expression" in message:
230
+ return message
231
+ return None
232
+
233
+
234
+ def cached_test_func(f: Callable) -> Callable:
235
+ def wrapped(*, ctx: EngineContext, case: Case, errors: list[Exception], recorder: ScenarioRecorder) -> None:
236
+ try:
237
+ if ctx.has_to_stop:
238
+ raise KeyboardInterrupt
239
+ if ctx.config.execution.unique_inputs:
240
+ cached = ctx.get_cached_outcome(case)
241
+ if isinstance(cached, BaseException):
242
+ raise cached
243
+ elif cached is None:
244
+ return None
245
+ try:
246
+ f(ctx=ctx, case=case, recorder=recorder)
247
+ except BaseException as exc:
248
+ ctx.cache_outcome(case, exc)
249
+ raise
250
+ else:
251
+ ctx.cache_outcome(case, None)
252
+ else:
253
+ f(ctx=ctx, case=case, recorder=recorder)
254
+ except (KeyboardInterrupt, Failure):
255
+ raise
256
+ except Exception as exc:
257
+ errors.append(exc)
258
+ raise UnexpectedError from None
259
+
260
+ wrapped.__name__ = f.__name__
261
+
262
+ return wrapped
263
+
264
+
265
+ @cached_test_func
266
+ def test_func(*, ctx: EngineContext, case: Case, recorder: ScenarioRecorder) -> None:
267
+ recorder.record_case(parent_id=None, transition=None, case=case)
268
+ try:
269
+ response = case.call(**ctx.transport_kwargs)
270
+ except (requests.Timeout, requests.ConnectionError) as error:
271
+ if isinstance(error.request, requests.Request):
272
+ recorder.record_request(case_id=case.id, request=error.request.prepare())
273
+ elif isinstance(error.request, requests.PreparedRequest):
274
+ recorder.record_request(case_id=case.id, request=error.request)
275
+ raise
276
+ recorder.record_response(case_id=case.id, response=response)
277
+ targets.run(ctx.config.execution.targets, case=case, response=response)
278
+ validate_response(
279
+ case=case,
280
+ ctx=ctx.get_check_context(recorder),
281
+ checks=ctx.config.execution.checks,
282
+ response=response,
283
+ no_failfast=ctx.config.execution.no_failfast,
284
+ recorder=recorder,
285
+ )
286
+
287
+
288
+ def validate_response(
289
+ *,
290
+ case: Case,
291
+ ctx: CheckContext,
292
+ checks: Iterable[CheckFunction],
293
+ response: Response,
294
+ no_failfast: bool,
295
+ recorder: ScenarioRecorder,
296
+ ) -> None:
297
+ failures = set()
298
+
299
+ def on_failure(name: str, collected: set[Failure], failure: Failure) -> None:
300
+ collected.add(failure)
301
+ failure_data = recorder.find_failure_data(parent_id=case.id, failure=failure)
302
+ recorder.record_check_failure(
303
+ name=name,
304
+ case_id=failure_data.case.id,
305
+ code_sample=failure_data.case.as_curl_command(headers=failure_data.headers, verify=failure_data.verify),
306
+ failure=failure,
307
+ )
308
+
309
+ def on_success(name: str, _case: Case) -> None:
310
+ recorder.record_check_success(name=name, case_id=_case.id)
311
+
312
+ failures = run_checks(
313
+ case=case,
314
+ response=response,
315
+ ctx=ctx,
316
+ checks=checks,
317
+ on_failure=on_failure,
318
+ on_success=on_success,
319
+ )
320
+
321
+ if failures and not no_failfast:
322
+ raise FailureGroup(list(failures)) from None
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ import uuid
5
+ from queue import Queue
6
+ from types import TracebackType
7
+ from typing import TYPE_CHECKING, Callable
8
+
9
+ from schemathesis.core.result import Result
10
+
11
+ if TYPE_CHECKING:
12
+ from schemathesis.engine.context import EngineContext
13
+
14
+
15
+ class TaskProducer:
16
+ """Produces test tasks for workers to execute."""
17
+
18
+ def __init__(self, ctx: EngineContext) -> None:
19
+ self.operations = ctx.schema.get_all_operations(generation_config=ctx.config.execution.generation)
20
+ self.lock = threading.Lock()
21
+
22
+ def next_operation(self) -> Result | None:
23
+ """Get next API operation in a thread-safe manner."""
24
+ with self.lock:
25
+ return next(self.operations, None)
26
+
27
+
28
+ class WorkerPool:
29
+ """Manages a pool of worker threads."""
30
+
31
+ def __init__(
32
+ self,
33
+ workers_num: int,
34
+ producer: TaskProducer,
35
+ worker_factory: Callable,
36
+ ctx: EngineContext,
37
+ suite_id: uuid.UUID,
38
+ ) -> None:
39
+ self.workers_num = workers_num
40
+ self.producer = producer
41
+ self.worker_factory = worker_factory
42
+ self.ctx = ctx
43
+ self.suite_id = suite_id
44
+ self.workers: list[threading.Thread] = []
45
+ self.events_queue: Queue = Queue()
46
+
47
+ def start(self) -> None:
48
+ """Start all worker threads."""
49
+ for i in range(self.workers_num):
50
+ worker = threading.Thread(
51
+ target=self.worker_factory,
52
+ kwargs={
53
+ "ctx": self.ctx,
54
+ "events_queue": self.events_queue,
55
+ "producer": self.producer,
56
+ "suite_id": self.suite_id,
57
+ },
58
+ name=f"schemathesis_unit_tests_{i}",
59
+ daemon=True,
60
+ )
61
+ self.workers.append(worker)
62
+ worker.start()
63
+
64
+ def stop(self) -> None:
65
+ """Stop all workers gracefully."""
66
+ for worker in self.workers:
67
+ worker.join()
68
+
69
+ def __enter__(self) -> WorkerPool:
70
+ self.start()
71
+ return self
72
+
73
+ def __exit__(self, ty: type[BaseException] | None, value: BaseException | None, tb: TracebackType | None) -> None:
74
+ self.stop()