schemathesis 3.39.15__py3-none-any.whl → 4.0.0__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 (255) hide show
  1. schemathesis/__init__.py +41 -79
  2. schemathesis/auths.py +111 -122
  3. schemathesis/checks.py +169 -60
  4. schemathesis/cli/__init__.py +15 -2117
  5. schemathesis/cli/commands/__init__.py +85 -0
  6. schemathesis/cli/commands/data.py +10 -0
  7. schemathesis/cli/commands/run/__init__.py +590 -0
  8. schemathesis/cli/commands/run/context.py +204 -0
  9. schemathesis/cli/commands/run/events.py +60 -0
  10. schemathesis/cli/commands/run/executor.py +157 -0
  11. schemathesis/cli/commands/run/filters.py +53 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
  15. schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1628 -0
  17. schemathesis/cli/commands/run/loaders.py +114 -0
  18. schemathesis/cli/commands/run/validation.py +246 -0
  19. schemathesis/cli/constants.py +5 -58
  20. schemathesis/cli/core.py +19 -0
  21. schemathesis/cli/ext/fs.py +16 -0
  22. schemathesis/cli/ext/groups.py +84 -0
  23. schemathesis/cli/{options.py → ext/options.py} +36 -34
  24. schemathesis/config/__init__.py +189 -0
  25. schemathesis/config/_auth.py +51 -0
  26. schemathesis/config/_checks.py +268 -0
  27. schemathesis/config/_diff_base.py +99 -0
  28. schemathesis/config/_env.py +21 -0
  29. schemathesis/config/_error.py +156 -0
  30. schemathesis/config/_generation.py +149 -0
  31. schemathesis/config/_health_check.py +24 -0
  32. schemathesis/config/_operations.py +327 -0
  33. schemathesis/config/_output.py +171 -0
  34. schemathesis/config/_parameters.py +19 -0
  35. schemathesis/config/_phases.py +187 -0
  36. schemathesis/config/_projects.py +527 -0
  37. schemathesis/config/_rate_limit.py +17 -0
  38. schemathesis/config/_report.py +120 -0
  39. schemathesis/config/_validator.py +9 -0
  40. schemathesis/config/_warnings.py +25 -0
  41. schemathesis/config/schema.json +885 -0
  42. schemathesis/core/__init__.py +67 -0
  43. schemathesis/core/compat.py +32 -0
  44. schemathesis/core/control.py +2 -0
  45. schemathesis/core/curl.py +58 -0
  46. schemathesis/core/deserialization.py +65 -0
  47. schemathesis/core/errors.py +459 -0
  48. schemathesis/core/failures.py +315 -0
  49. schemathesis/core/fs.py +19 -0
  50. schemathesis/core/hooks.py +20 -0
  51. schemathesis/core/loaders.py +104 -0
  52. schemathesis/core/marks.py +66 -0
  53. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  54. schemathesis/core/output/__init__.py +46 -0
  55. schemathesis/core/output/sanitization.py +54 -0
  56. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  57. schemathesis/core/registries.py +31 -0
  58. schemathesis/core/transforms.py +113 -0
  59. schemathesis/core/transport.py +223 -0
  60. schemathesis/core/validation.py +54 -0
  61. schemathesis/core/version.py +7 -0
  62. schemathesis/engine/__init__.py +28 -0
  63. schemathesis/engine/context.py +118 -0
  64. schemathesis/engine/control.py +36 -0
  65. schemathesis/engine/core.py +169 -0
  66. schemathesis/engine/errors.py +464 -0
  67. schemathesis/engine/events.py +258 -0
  68. schemathesis/engine/phases/__init__.py +88 -0
  69. schemathesis/{runner → engine/phases}/probes.py +52 -68
  70. schemathesis/engine/phases/stateful/__init__.py +68 -0
  71. schemathesis/engine/phases/stateful/_executor.py +356 -0
  72. schemathesis/engine/phases/stateful/context.py +85 -0
  73. schemathesis/engine/phases/unit/__init__.py +212 -0
  74. schemathesis/engine/phases/unit/_executor.py +416 -0
  75. schemathesis/engine/phases/unit/_pool.py +82 -0
  76. schemathesis/engine/recorder.py +247 -0
  77. schemathesis/errors.py +43 -0
  78. schemathesis/filters.py +17 -98
  79. schemathesis/generation/__init__.py +5 -33
  80. schemathesis/generation/case.py +317 -0
  81. schemathesis/generation/coverage.py +282 -175
  82. schemathesis/generation/hypothesis/__init__.py +36 -0
  83. schemathesis/generation/hypothesis/builder.py +800 -0
  84. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  85. schemathesis/generation/hypothesis/given.py +66 -0
  86. schemathesis/generation/hypothesis/reporting.py +14 -0
  87. schemathesis/generation/hypothesis/strategies.py +16 -0
  88. schemathesis/generation/meta.py +115 -0
  89. schemathesis/generation/metrics.py +93 -0
  90. schemathesis/generation/modes.py +20 -0
  91. schemathesis/generation/overrides.py +116 -0
  92. schemathesis/generation/stateful/__init__.py +37 -0
  93. schemathesis/generation/stateful/state_machine.py +278 -0
  94. schemathesis/graphql/__init__.py +15 -0
  95. schemathesis/graphql/checks.py +109 -0
  96. schemathesis/graphql/loaders.py +284 -0
  97. schemathesis/hooks.py +80 -101
  98. schemathesis/openapi/__init__.py +13 -0
  99. schemathesis/openapi/checks.py +455 -0
  100. schemathesis/openapi/generation/__init__.py +0 -0
  101. schemathesis/openapi/generation/filters.py +72 -0
  102. schemathesis/openapi/loaders.py +313 -0
  103. schemathesis/pytest/__init__.py +5 -0
  104. schemathesis/pytest/control_flow.py +7 -0
  105. schemathesis/pytest/lazy.py +281 -0
  106. schemathesis/pytest/loaders.py +36 -0
  107. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
  108. schemathesis/python/__init__.py +0 -0
  109. schemathesis/python/asgi.py +12 -0
  110. schemathesis/python/wsgi.py +12 -0
  111. schemathesis/schemas.py +537 -273
  112. schemathesis/specs/graphql/__init__.py +0 -1
  113. schemathesis/specs/graphql/_cache.py +1 -2
  114. schemathesis/specs/graphql/scalars.py +42 -6
  115. schemathesis/specs/graphql/schemas.py +141 -137
  116. schemathesis/specs/graphql/validation.py +11 -17
  117. schemathesis/specs/openapi/__init__.py +6 -1
  118. schemathesis/specs/openapi/_cache.py +1 -2
  119. schemathesis/specs/openapi/_hypothesis.py +142 -156
  120. schemathesis/specs/openapi/checks.py +368 -257
  121. schemathesis/specs/openapi/converter.py +4 -4
  122. schemathesis/specs/openapi/definitions.py +1 -1
  123. schemathesis/specs/openapi/examples.py +23 -21
  124. schemathesis/specs/openapi/expressions/__init__.py +31 -19
  125. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  126. schemathesis/specs/openapi/expressions/lexer.py +1 -1
  127. schemathesis/specs/openapi/expressions/nodes.py +36 -41
  128. schemathesis/specs/openapi/expressions/parser.py +1 -1
  129. schemathesis/specs/openapi/formats.py +35 -7
  130. schemathesis/specs/openapi/media_types.py +53 -12
  131. schemathesis/specs/openapi/negative/__init__.py +7 -4
  132. schemathesis/specs/openapi/negative/mutations.py +6 -5
  133. schemathesis/specs/openapi/parameters.py +7 -10
  134. schemathesis/specs/openapi/patterns.py +94 -31
  135. schemathesis/specs/openapi/references.py +12 -53
  136. schemathesis/specs/openapi/schemas.py +238 -308
  137. schemathesis/specs/openapi/security.py +1 -1
  138. schemathesis/specs/openapi/serialization.py +12 -6
  139. schemathesis/specs/openapi/stateful/__init__.py +268 -133
  140. schemathesis/specs/openapi/stateful/control.py +87 -0
  141. schemathesis/specs/openapi/stateful/links.py +209 -0
  142. schemathesis/transport/__init__.py +142 -0
  143. schemathesis/transport/asgi.py +26 -0
  144. schemathesis/transport/prepare.py +124 -0
  145. schemathesis/transport/requests.py +244 -0
  146. schemathesis/{_xml.py → transport/serialization.py} +69 -11
  147. schemathesis/transport/wsgi.py +171 -0
  148. schemathesis-4.0.0.dist-info/METADATA +204 -0
  149. schemathesis-4.0.0.dist-info/RECORD +164 -0
  150. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
  151. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
  152. schemathesis/_compat.py +0 -74
  153. schemathesis/_dependency_versions.py +0 -19
  154. schemathesis/_hypothesis.py +0 -712
  155. schemathesis/_override.py +0 -50
  156. schemathesis/_patches.py +0 -21
  157. schemathesis/_rate_limiter.py +0 -7
  158. schemathesis/cli/callbacks.py +0 -466
  159. schemathesis/cli/cassettes.py +0 -561
  160. schemathesis/cli/context.py +0 -75
  161. schemathesis/cli/debug.py +0 -27
  162. schemathesis/cli/handlers.py +0 -19
  163. schemathesis/cli/junitxml.py +0 -124
  164. schemathesis/cli/output/__init__.py +0 -1
  165. schemathesis/cli/output/default.py +0 -920
  166. schemathesis/cli/output/short.py +0 -59
  167. schemathesis/cli/reporting.py +0 -79
  168. schemathesis/cli/sanitization.py +0 -26
  169. schemathesis/code_samples.py +0 -151
  170. schemathesis/constants.py +0 -54
  171. schemathesis/contrib/__init__.py +0 -11
  172. schemathesis/contrib/openapi/__init__.py +0 -11
  173. schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
  174. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  175. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  176. schemathesis/contrib/unique_data.py +0 -41
  177. schemathesis/exceptions.py +0 -571
  178. schemathesis/experimental/__init__.py +0 -109
  179. schemathesis/extra/_aiohttp.py +0 -28
  180. schemathesis/extra/_flask.py +0 -13
  181. schemathesis/extra/_server.py +0 -18
  182. schemathesis/failures.py +0 -284
  183. schemathesis/fixups/__init__.py +0 -37
  184. schemathesis/fixups/fast_api.py +0 -41
  185. schemathesis/fixups/utf8_bom.py +0 -28
  186. schemathesis/generation/_methods.py +0 -44
  187. schemathesis/graphql.py +0 -3
  188. schemathesis/internal/__init__.py +0 -7
  189. schemathesis/internal/checks.py +0 -86
  190. schemathesis/internal/copy.py +0 -32
  191. schemathesis/internal/datetime.py +0 -5
  192. schemathesis/internal/deprecation.py +0 -37
  193. schemathesis/internal/diff.py +0 -15
  194. schemathesis/internal/extensions.py +0 -27
  195. schemathesis/internal/jsonschema.py +0 -36
  196. schemathesis/internal/output.py +0 -68
  197. schemathesis/internal/transformation.py +0 -26
  198. schemathesis/internal/validation.py +0 -34
  199. schemathesis/lazy.py +0 -474
  200. schemathesis/loaders.py +0 -122
  201. schemathesis/models.py +0 -1341
  202. schemathesis/parameters.py +0 -90
  203. schemathesis/runner/__init__.py +0 -605
  204. schemathesis/runner/events.py +0 -389
  205. schemathesis/runner/impl/__init__.py +0 -3
  206. schemathesis/runner/impl/context.py +0 -88
  207. schemathesis/runner/impl/core.py +0 -1280
  208. schemathesis/runner/impl/solo.py +0 -80
  209. schemathesis/runner/impl/threadpool.py +0 -391
  210. schemathesis/runner/serialization.py +0 -544
  211. schemathesis/sanitization.py +0 -252
  212. schemathesis/serializers.py +0 -328
  213. schemathesis/service/__init__.py +0 -18
  214. schemathesis/service/auth.py +0 -11
  215. schemathesis/service/ci.py +0 -202
  216. schemathesis/service/client.py +0 -133
  217. schemathesis/service/constants.py +0 -38
  218. schemathesis/service/events.py +0 -61
  219. schemathesis/service/extensions.py +0 -224
  220. schemathesis/service/hosts.py +0 -111
  221. schemathesis/service/metadata.py +0 -71
  222. schemathesis/service/models.py +0 -258
  223. schemathesis/service/report.py +0 -255
  224. schemathesis/service/serialization.py +0 -173
  225. schemathesis/service/usage.py +0 -66
  226. schemathesis/specs/graphql/loaders.py +0 -364
  227. schemathesis/specs/openapi/expressions/context.py +0 -16
  228. schemathesis/specs/openapi/links.py +0 -389
  229. schemathesis/specs/openapi/loaders.py +0 -707
  230. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  231. schemathesis/specs/openapi/stateful/types.py +0 -14
  232. schemathesis/specs/openapi/validation.py +0 -26
  233. schemathesis/stateful/__init__.py +0 -147
  234. schemathesis/stateful/config.py +0 -97
  235. schemathesis/stateful/context.py +0 -135
  236. schemathesis/stateful/events.py +0 -274
  237. schemathesis/stateful/runner.py +0 -309
  238. schemathesis/stateful/sink.py +0 -68
  239. schemathesis/stateful/state_machine.py +0 -328
  240. schemathesis/stateful/statistic.py +0 -22
  241. schemathesis/stateful/validation.py +0 -100
  242. schemathesis/targets.py +0 -77
  243. schemathesis/transports/__init__.py +0 -369
  244. schemathesis/transports/asgi.py +0 -7
  245. schemathesis/transports/auth.py +0 -38
  246. schemathesis/transports/headers.py +0 -36
  247. schemathesis/transports/responses.py +0 -57
  248. schemathesis/types.py +0 -44
  249. schemathesis/utils.py +0 -164
  250. schemathesis-3.39.15.dist-info/METADATA +0 -293
  251. schemathesis-3.39.15.dist-info/RECORD +0 -160
  252. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  253. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  254. /schemathesis/{internal → core}/result.py +0 -0
  255. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -1,920 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- import platform
5
- import shutil
6
- import textwrap
7
- import time
8
- from importlib import metadata
9
- from types import GeneratorType
10
- from typing import TYPE_CHECKING, Any, Generator, Literal, cast
11
-
12
- import click
13
-
14
- from ... import experimental, service
15
- from ...constants import (
16
- DISCORD_LINK,
17
- FLAKY_FAILURE_MESSAGE,
18
- ISSUE_TRACKER_URL,
19
- SCHEMATHESIS_TEST_CASE_HEADER,
20
- SCHEMATHESIS_VERSION,
21
- )
22
- from ...exceptions import (
23
- RuntimeErrorType,
24
- extract_requests_exception_details,
25
- format_exception,
26
- )
27
- from ...experimental import GLOBAL_EXPERIMENTS
28
- from ...internal.output import prepare_response_payload
29
- from ...internal.result import Ok
30
- from ...models import Status
31
- from ...runner import events
32
- from ...runner.events import InternalErrorType, SchemaErrorType
33
- from ...runner.probes import ProbeOutcome
34
- from ...runner.serialization import SerializedError, SerializedTestResult
35
- from ...service.models import AnalysisSuccess, ErrorState, UnknownExtension
36
- from ...stateful import events as stateful_events
37
- from ...stateful.sink import StateMachineSink
38
- from ..context import ExecutionContext, FileReportContext, ServiceReportContext
39
- from ..handlers import EventHandler
40
- from ..reporting import TEST_CASE_ID_TITLE, get_runtime_error_suggestion, group_by_case, split_traceback
41
-
42
- if TYPE_CHECKING:
43
- from queue import Queue
44
-
45
- import requests
46
-
47
- SPINNER_REPETITION_NUMBER = 10
48
- IO_ENCODING = os.getenv("PYTHONIOENCODING", "utf-8")
49
-
50
-
51
- def get_terminal_width() -> int:
52
- # Some CI/CD providers (e.g. CircleCI) return a (0, 0) terminal size so provide a default
53
- return shutil.get_terminal_size((80, 24)).columns
54
-
55
-
56
- def display_section_name(title: str, separator: str = "=", **kwargs: Any) -> None:
57
- """Print section name with separators in terminal with the given title nicely centered."""
58
- message = f" {title} ".center(get_terminal_width(), separator)
59
- kwargs.setdefault("bold", True)
60
- click.secho(message, **kwargs)
61
-
62
-
63
- def display_subsection(result: SerializedTestResult, color: str | None = "red") -> None:
64
- display_section_name(result.verbose_name, "_", fg=color)
65
-
66
-
67
- def get_percentage(position: int, length: int) -> str:
68
- """Format completion percentage in square brackets."""
69
- percentage_message = f"{position * 100 // length}%".rjust(4)
70
- return f"[{percentage_message}]"
71
-
72
-
73
- def display_execution_result(context: ExecutionContext, status: Literal["success", "failure", "error", "skip"]) -> None:
74
- """Display an appropriate symbol for the given event's execution result."""
75
- symbol, color = {
76
- "success": (".", "green"),
77
- "failure": ("F", "red"),
78
- "error": ("E", "red"),
79
- "skip": ("S", "yellow"),
80
- }[status]
81
- context.current_line_length += len(symbol)
82
- click.secho(symbol, nl=False, fg=color)
83
-
84
-
85
- def display_percentage(context: ExecutionContext, event: events.AfterExecution) -> None:
86
- """Add the current progress in % to the right side of the current line."""
87
- operations_count = cast(int, context.operations_count) # is already initialized via `Initialized` event
88
- current_percentage = get_percentage(context.operations_processed, operations_count)
89
- styled = click.style(current_percentage, fg="cyan")
90
- # Total length of the message, so it will fill to the right border of the terminal.
91
- # Padding is already taken into account in `context.current_line_length`
92
- length = max(get_terminal_width() - context.current_line_length + len(styled) - len(current_percentage), 1)
93
- template = f"{{:>{length}}}"
94
- click.echo(template.format(styled))
95
-
96
-
97
- def display_summary(event: events.Finished) -> None:
98
- message, color = get_summary_output(event)
99
- display_section_name(message, fg=color)
100
-
101
-
102
- def get_summary_message_parts(event: events.Finished) -> list[str]:
103
- parts = []
104
- passed = event.passed_count
105
- if passed:
106
- parts.append(f"{passed} passed")
107
- failed = event.failed_count
108
- if failed:
109
- parts.append(f"{failed} failed")
110
- errored = event.errored_count
111
- if errored:
112
- parts.append(f"{errored} errored")
113
- skipped = event.skipped_count
114
- if skipped:
115
- parts.append(f"{skipped} skipped")
116
- return parts
117
-
118
-
119
- def get_summary_output(event: events.Finished) -> tuple[str, str]:
120
- parts = get_summary_message_parts(event)
121
- if not parts:
122
- message = "Empty test suite"
123
- color = "yellow"
124
- else:
125
- message = f"{', '.join(parts)} in {event.running_time:.2f}s"
126
- if event.has_failures or event.has_errors:
127
- color = "red"
128
- elif event.skipped_count > 0:
129
- color = "yellow"
130
- else:
131
- color = "green"
132
- return message, color
133
-
134
-
135
- def display_hypothesis_output(hypothesis_output: list[str]) -> None:
136
- """Show falsifying examples from Hypothesis output if there are any."""
137
- if hypothesis_output:
138
- display_section_name("HYPOTHESIS OUTPUT")
139
- output = "\n".join(hypothesis_output)
140
- click.secho(output, fg="red")
141
-
142
-
143
- def display_errors(context: ExecutionContext, event: events.Finished) -> None:
144
- """Display all errors in the test run."""
145
- probes = context.probes or []
146
- has_probe_errors = any(probe.outcome == ProbeOutcome.ERROR for probe in probes)
147
- if not event.has_errors and not has_probe_errors:
148
- return
149
-
150
- display_section_name("ERRORS")
151
- should_display_full_traceback_message = False
152
- if context.workers_num > 1:
153
- # Events may come out of order when multiple workers are involved
154
- # Sort them to get a stable output
155
- results = sorted(context.results, key=lambda r: r.verbose_name)
156
- else:
157
- results = context.results
158
- for result in results:
159
- if not result.has_errors:
160
- continue
161
- should_display_full_traceback_message |= display_single_error(context, result)
162
- if event.generic_errors:
163
- display_generic_errors(context, event.generic_errors)
164
- if has_probe_errors:
165
- display_section_name("API Probe errors", "_", fg="red")
166
- for probe in probes:
167
- if probe.error is not None:
168
- error = SerializedError.from_exception(probe.error)
169
- _display_error(context, error)
170
- if should_display_full_traceback_message and not context.show_trace:
171
- click.secho(
172
- "\nAdd this option to your command line parameters to see full tracebacks: --show-trace",
173
- fg="red",
174
- )
175
- click.secho(
176
- f"\nNeed more help?\n Join our Discord server: {DISCORD_LINK}",
177
- fg="red",
178
- )
179
-
180
-
181
- def display_single_error(context: ExecutionContext, result: SerializedTestResult) -> bool:
182
- display_subsection(result)
183
- should_display_full_traceback_message = False
184
- first = True
185
- for error in result.errors:
186
- if first:
187
- first = False
188
- else:
189
- click.echo()
190
- should_display_full_traceback_message |= _display_error(context, error)
191
- return should_display_full_traceback_message
192
-
193
-
194
- def display_generic_errors(context: ExecutionContext, errors: list[SerializedError]) -> None:
195
- for error in errors:
196
- display_section_name(error.title or "Generic error", "_", fg="red")
197
- _display_error(context, error)
198
-
199
-
200
- def display_full_traceback_message(error: SerializedError) -> bool:
201
- # Some errors should not trigger the message that suggests to show full tracebacks to the user
202
- return (
203
- not error.exception.startswith(
204
- (
205
- "DeadlineExceeded",
206
- "OperationSchemaError",
207
- "requests.exceptions",
208
- "SerializationNotPossible",
209
- "hypothesis.errors.FailedHealthCheck",
210
- "hypothesis.errors.InvalidArgument: Scalar ",
211
- "hypothesis.errors.InvalidArgument: min_size=",
212
- "hypothesis.errors.Unsatisfiable",
213
- )
214
- )
215
- and "can never generate an example, because min_size is larger than Hypothesis supports." not in error.exception
216
- )
217
-
218
-
219
- def bold(option: str) -> str:
220
- return click.style(option, bold=True)
221
-
222
-
223
- DISABLE_SSL_SUGGESTION = f"Bypass SSL verification with {bold('`--request-tls-verify=false`')}."
224
- DISABLE_SCHEMA_VALIDATION_SUGGESTION = (
225
- f"Bypass validation using {bold('`--validate-schema=false`')}. Caution: May cause unexpected errors."
226
- )
227
-
228
-
229
- def _format_health_check_suggestion(label: str) -> str:
230
- return f"Bypass this health check using {bold(f'`--hypothesis-suppress-health-check={label}`')}."
231
-
232
-
233
- RUNTIME_ERROR_SUGGESTIONS = {
234
- RuntimeErrorType.CONNECTION_SSL: DISABLE_SSL_SUGGESTION,
235
- RuntimeErrorType.HYPOTHESIS_DEADLINE_EXCEEDED: (
236
- f"Adjust the deadline using {bold('`--hypothesis-deadline=MILLIS`')} or "
237
- f"disable with {bold('`--hypothesis-deadline=None`')}."
238
- ),
239
- RuntimeErrorType.HYPOTHESIS_UNSATISFIABLE: "Examine the schema for inconsistencies and consider simplifying it.",
240
- RuntimeErrorType.SCHEMA_BODY_IN_GET_REQUEST: DISABLE_SCHEMA_VALIDATION_SUGGESTION,
241
- RuntimeErrorType.SCHEMA_INVALID_REGULAR_EXPRESSION: "Ensure your regex is compatible with Python's syntax.\n"
242
- "For guidance, visit: https://docs.python.org/3/library/re.html",
243
- RuntimeErrorType.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR: "Define a custom strategy for it.\n"
244
- "For guidance, visit: https://schemathesis.readthedocs.io/en/stable/graphql.html#custom-scalars",
245
- RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE: _format_health_check_suggestion("data_too_large"),
246
- RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH: _format_health_check_suggestion("filter_too_much"),
247
- RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW: _format_health_check_suggestion("too_slow"),
248
- RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE: _format_health_check_suggestion("large_base_example"),
249
- }
250
-
251
-
252
- def _display_error(context: ExecutionContext, error: SerializedError) -> bool:
253
- if error.title:
254
- if error.type == RuntimeErrorType.SCHEMA_GENERIC:
255
- click.secho("Schema Error", fg="red", bold=True)
256
- else:
257
- click.secho(error.title, fg="red", bold=True)
258
- click.echo()
259
- if error.message:
260
- click.echo(error.message)
261
- elif error.message:
262
- click.echo(error.message)
263
- else:
264
- click.echo(error.exception)
265
- if error.extras:
266
- extras = error.extras
267
- elif context.show_trace and error.type.has_useful_traceback:
268
- extras = split_traceback(error.exception_with_traceback)
269
- else:
270
- extras = []
271
- _display_extras(extras)
272
- suggestion = get_runtime_error_suggestion(error.type)
273
- _maybe_display_tip(suggestion)
274
- return display_full_traceback_message(error)
275
-
276
-
277
- def display_failures(context: ExecutionContext, event: events.Finished) -> None:
278
- """Display all failures in the test run."""
279
- if not event.has_failures:
280
- return
281
- relevant_results = [result for result in context.results if not result.is_errored]
282
- if not relevant_results:
283
- return
284
- display_section_name("FAILURES")
285
- for result in relevant_results:
286
- if not result.has_failures:
287
- continue
288
- display_failures_for_single_test(context, result)
289
-
290
-
291
- if IO_ENCODING != "utf-8":
292
-
293
- def _secho(text: str, **kwargs: Any) -> None:
294
- text = text.encode(IO_ENCODING, errors="replace").decode("utf-8")
295
- click.secho(text, **kwargs)
296
-
297
- else:
298
-
299
- def _secho(text: str, **kwargs: Any) -> None:
300
- click.secho(text, **kwargs)
301
-
302
-
303
- def display_failures_for_single_test(context: ExecutionContext, result: SerializedTestResult) -> None:
304
- """Display a failure for a single method / path."""
305
- from ...transports.responses import get_reason
306
-
307
- display_subsection(result)
308
- if result.is_flaky:
309
- click.secho(FLAKY_FAILURE_MESSAGE, fg="red")
310
- click.echo()
311
- for idx, (code_sample, group) in enumerate(group_by_case(result.checks, context.code_sample_style), 1):
312
- # Make server errors appear first in the list of checks
313
- checks = sorted(group, key=lambda c: c.name != "not_a_server_error")
314
-
315
- for check_idx, check in enumerate(checks):
316
- if check_idx == 0:
317
- click.secho(f"{idx}. {TEST_CASE_ID_TITLE}: {check.example.id}", bold=True)
318
- click.secho(f"\n- {check.title}", fg="red", bold=True)
319
- message = check.formatted_message
320
- if message:
321
- _secho(f"\n{message}", fg="red")
322
- if check_idx + 1 == len(checks):
323
- if check.response is not None:
324
- status_code = check.response.status_code
325
- reason = get_reason(status_code)
326
- response = bold(f"[{check.response.status_code}] {reason}")
327
- click.echo(f"\n{response}:")
328
- if check.response.body is not None:
329
- if not check.response.body:
330
- click.echo("\n <EMPTY>")
331
- else:
332
- encoding = check.response.encoding or "utf8"
333
- try:
334
- # Checked that is not None
335
- body = cast(bytes, check.response.deserialize_body())
336
- payload = body.decode(encoding)
337
- payload = prepare_response_payload(payload, config=context.output_config)
338
- payload = textwrap.indent(f"\n`{payload}`", prefix=" ")
339
- click.echo(payload)
340
- except UnicodeDecodeError:
341
- click.echo("\n <BINARY>")
342
- _secho(f"\n{bold('Reproduce with')}: \n\n {code_sample}\n")
343
-
344
-
345
- def display_application_logs(context: ExecutionContext, event: events.Finished) -> None:
346
- """Print logs captured during the application run."""
347
- if not event.has_logs:
348
- return
349
- display_section_name("APPLICATION LOGS")
350
- for result in context.results:
351
- if not result.has_logs:
352
- continue
353
- display_single_log(result)
354
-
355
-
356
- def display_single_log(result: SerializedTestResult) -> None:
357
- display_subsection(result, None)
358
- click.echo("\n\n".join(result.logs))
359
-
360
-
361
- def display_analysis(context: ExecutionContext) -> None:
362
- """Display schema analysis details."""
363
- import requests.exceptions
364
-
365
- if context.analysis is None:
366
- return
367
- display_section_name("SCHEMA ANALYSIS")
368
- if isinstance(context.analysis, Ok):
369
- analysis = context.analysis.ok()
370
- click.echo()
371
- if isinstance(analysis, AnalysisSuccess):
372
- click.secho(analysis.message, bold=True)
373
- click.echo(f"\nAnalysis took: {analysis.elapsed:.2f}ms")
374
- if analysis.extensions:
375
- known = []
376
- failed = []
377
- unknown = []
378
- for extension in analysis.extensions:
379
- if isinstance(extension, UnknownExtension):
380
- unknown.append(extension)
381
- elif isinstance(extension.state, ErrorState):
382
- failed.append(extension)
383
- else:
384
- known.append(extension)
385
- if known:
386
- click.echo("\nThe following extensions have been applied:\n")
387
- for extension in known:
388
- click.echo(f" - {extension.summary}")
389
- if failed:
390
- click.echo("\nThe following extensions errored:\n")
391
- for extension in failed:
392
- click.echo(f" - {extension.summary}")
393
- suggestion = f"Please, consider reporting this to our issue tracker:\n\n {ISSUE_TRACKER_URL}"
394
- click.secho(f"\n{click.style('Tip:', bold=True, fg='green')} {suggestion}")
395
- if unknown:
396
- noun = "extension" if len(unknown) == 1 else "extensions"
397
- specific_noun = "this extension" if len(unknown) == 1 else "these extensions"
398
- title = click.style("Compatibility Notice", bold=True)
399
- click.secho(f"\n{title}: {len(unknown)} {noun} not recognized:\n")
400
- for extension in unknown:
401
- click.echo(f" - {extension.summary}")
402
- suggestion = f"Consider updating the CLI to add support for {specific_noun}."
403
- click.secho(f"\n{click.style('Tip:', bold=True, fg='green')} {suggestion}")
404
- else:
405
- click.echo("\nNo extensions have been applied.")
406
- else:
407
- click.echo("An error happened during schema analysis:\n")
408
- click.secho(f" {analysis.message}", bold=True)
409
- click.echo()
410
- else:
411
- exception = context.analysis.err()
412
- suggestion = None
413
- if isinstance(exception, requests.exceptions.HTTPError):
414
- response = exception.response
415
- click.secho("Error\n", fg="red", bold=True)
416
- _display_service_network_error(response)
417
- click.echo()
418
- return
419
- if isinstance(exception, requests.RequestException):
420
- message, extras = extract_requests_exception_details(exception)
421
- suggestion = "Please check your network connection and try again."
422
- title = "Network Error"
423
- else:
424
- traceback = format_exception(exception, True)
425
- extras = split_traceback(traceback)
426
- title = "Internal Error"
427
- message = f"We apologize for the inconvenience. This appears to be an internal issue.\nPlease, consider reporting the following details to our issue tracker:\n\n {ISSUE_TRACKER_URL}"
428
- suggestion = "Please update your CLI to the latest version and try again."
429
- click.secho(f"{title}\n", fg="red", bold=True)
430
- click.echo(message)
431
- _display_extras(extras)
432
- _maybe_display_tip(suggestion)
433
- click.echo()
434
-
435
-
436
- def display_statistic(context: ExecutionContext, event: events.Finished) -> None:
437
- """Format and print statistic collected by :obj:`models.TestResult`."""
438
- display_section_name("SUMMARY")
439
- click.echo()
440
- total = event.total
441
- if context.state_machine_sink is not None:
442
- click.echo(context.state_machine_sink.transitions.to_formatted_table(get_terminal_width()))
443
- click.echo()
444
- if event.is_empty or not total:
445
- click.secho("No checks were performed.", bold=True)
446
-
447
- if total:
448
- display_checks_statistics(total)
449
-
450
- if context.cassette_path:
451
- click.echo()
452
- category = click.style("Network log", bold=True)
453
- click.secho(f"{category}: {context.cassette_path}")
454
-
455
- if context.junit_xml_file:
456
- click.echo()
457
- category = click.style("JUnit XML file", bold=True)
458
- click.secho(f"{category}: {context.junit_xml_file}")
459
-
460
- if event.warnings:
461
- click.echo()
462
- if len(event.warnings) == 1:
463
- title = click.style("WARNING:", bold=True, fg="yellow")
464
- warning = click.style(event.warnings[0], fg="yellow")
465
- click.secho(f"{title} {warning}")
466
- else:
467
- click.secho("WARNINGS:", bold=True, fg="yellow")
468
- for warning in event.warnings:
469
- click.secho(f" - {warning}", fg="yellow")
470
-
471
- if len(GLOBAL_EXPERIMENTS.enabled) > 0:
472
- click.secho("\nExperimental Features:", bold=True)
473
- for experiment in sorted(GLOBAL_EXPERIMENTS.enabled, key=lambda e: e.name):
474
- click.secho(f" - {experiment.verbose_name}: {experiment.description}")
475
- click.secho(f" Feedback: {experiment.discussion_url}")
476
- click.echo()
477
- click.echo(
478
- "Your feedback is crucial for experimental features. "
479
- "Please visit the provided URL(s) to share your thoughts."
480
- )
481
-
482
- if event.failed_count > 0:
483
- click.echo(
484
- f"\n{bold('Note')}: Use the '{SCHEMATHESIS_TEST_CASE_HEADER}' header to correlate test case ids "
485
- "from failure messages with server logs for debugging."
486
- )
487
- if context.seed is not None:
488
- seed_option = f"`--hypothesis-seed={context.seed}`"
489
- click.secho(f"\n{bold('Note')}: To replicate these test failures, rerun with {bold(seed_option)}")
490
-
491
- if context.report is not None and not context.is_interrupted:
492
- if isinstance(context.report, FileReportContext):
493
- click.echo()
494
- display_report_metadata(context.report.queue.get())
495
- click.secho(f"Report is saved to {context.report.filename}", bold=True)
496
- elif isinstance(context.report, ServiceReportContext):
497
- click.echo()
498
- handle_service_integration(context.report)
499
-
500
-
501
- def handle_service_integration(context: ServiceReportContext) -> None:
502
- """If Schemathesis.io integration is enabled, wait for the handler & print the resulting status."""
503
- event = context.queue.get()
504
- title = click.style("Upload", bold=True)
505
- if isinstance(event, service.Metadata):
506
- display_report_metadata(event)
507
- click.secho(f"Uploading reports to {context.service_base_url} ...", bold=True)
508
- event = wait_for_report_handler(context.queue, title)
509
- color = {
510
- service.Completed: "green",
511
- service.Error: "red",
512
- service.Failed: "red",
513
- service.Timeout: "red",
514
- }[event.__class__]
515
- status = click.style(event.status, fg=color, bold=True)
516
- click.echo(f"{title}: {status}\r", nl=False)
517
- click.echo()
518
- if isinstance(event, service.Error):
519
- click.echo()
520
- display_service_error(event)
521
- if isinstance(event, service.Failed):
522
- click.echo()
523
- click.echo(event.detail)
524
- if isinstance(event, service.Completed):
525
- click.echo()
526
- click.echo(event.message)
527
- click.echo()
528
- click.echo(event.next_url)
529
-
530
-
531
- def display_report_metadata(meta: service.Metadata) -> None:
532
- if meta.ci_environment is not None:
533
- click.secho(f"{meta.ci_environment.verbose_name} detected:", bold=True)
534
- for key, value in meta.ci_environment.as_env().items():
535
- if value is not None:
536
- click.secho(f" -> {key}: {value}")
537
- click.echo()
538
- click.secho(f"Compressed report size: {meta.size / 1024.0:,.0f} KB", bold=True)
539
-
540
-
541
- def display_service_unauthorized(hostname: str) -> None:
542
- click.secho("\nTo authenticate:")
543
- click.secho(f"1. Retrieve your token from {bold(hostname)}")
544
- click.secho(f"2. Execute {bold('`st auth login <TOKEN>`')}")
545
- env_var = bold(f"`{service.TOKEN_ENV_VAR}`")
546
- click.secho(
547
- f"\nAs an alternative, supply the token directly "
548
- f"using the {bold('`--schemathesis-io-token`')} option "
549
- f"or the {env_var} environment variable."
550
- )
551
- click.echo("\nFor more information, please visit: https://schemathesis.readthedocs.io/en/stable/service.html")
552
-
553
-
554
- def display_service_error(event: service.Error, message_prefix: str = "") -> None:
555
- """Show information about an error during communication with Schemathesis.io."""
556
- from requests import HTTPError, RequestException, Response
557
-
558
- if isinstance(event.exception, HTTPError):
559
- response = cast(Response, event.exception.response)
560
- _display_service_network_error(response, message_prefix)
561
- elif isinstance(event.exception, RequestException):
562
- ask_to_report(event, report_to_issues=False)
563
- else:
564
- ask_to_report(event)
565
-
566
-
567
- def _display_service_network_error(response: requests.Response, message_prefix: str = "") -> None:
568
- status_code = response.status_code
569
- if 500 <= status_code <= 599:
570
- click.secho(f"Schemathesis.io responded with HTTP {status_code}", fg="red")
571
- # Server error, should be resolved soon
572
- click.secho(
573
- "\nIt is likely that we are already notified about the issue and working on a fix\n"
574
- "Please, try again in 30 minutes",
575
- fg="red",
576
- )
577
- elif status_code == 401:
578
- # Likely an invalid token
579
- click.echo("Your CLI is not authenticated.")
580
- display_service_unauthorized("schemathesis.io")
581
- else:
582
- try:
583
- data = response.json()
584
- detail = data["detail"]
585
- click.secho(f"{message_prefix}{detail}", fg="red")
586
- except Exception:
587
- # Other client-side errors are likely caused by a bug on the CLI side
588
- click.secho(
589
- "We apologize for the inconvenience. This appears to be an internal issue.\n"
590
- "Please, consider reporting the following details to our issue "
591
- f"tracker:\n\n {ISSUE_TRACKER_URL}\n\nResponse: {response.text!r}\n"
592
- f"Status: {response.status_code}\n"
593
- f"Headers: {response.headers!r}",
594
- fg="red",
595
- )
596
- _maybe_display_tip("Please update your CLI to the latest version and try again.")
597
-
598
-
599
- SERVICE_ERROR_MESSAGE = "An error happened during uploading reports to Schemathesis.io"
600
-
601
-
602
- def ask_to_report(event: service.Error, report_to_issues: bool = True, extra: str = "") -> None:
603
- from requests import RequestException
604
-
605
- # Likely an internal Schemathesis error
606
- traceback = event.get_message(True)
607
- if isinstance(event.exception, RequestException) and event.exception.response is not None:
608
- response = f"Response: {event.exception.response.text}\n"
609
- else:
610
- response = ""
611
- if report_to_issues:
612
- ask = f"Please, consider reporting the following details to our issue tracker:\n\n {ISSUE_TRACKER_URL}\n\n"
613
- else:
614
- ask = ""
615
- click.secho(
616
- f"{SERVICE_ERROR_MESSAGE}:\n{extra}{ask}{response}\n{traceback.strip()}",
617
- fg="red",
618
- )
619
-
620
-
621
- def wait_for_report_handler(queue: Queue, title: str, timeout: float = service.WORKER_FINISH_TIMEOUT) -> service.Event:
622
- """Wait for the Schemathesis.io handler to finish its job."""
623
- start = time.monotonic()
624
- spinner = create_spinner(SPINNER_REPETITION_NUMBER)
625
- # The testing process is done, and we need to wait for the Schemathesis.io handler to finish
626
- # It might still have some data to send
627
- while queue.empty():
628
- if time.monotonic() - start >= timeout:
629
- return service.Timeout()
630
- click.echo(f"{title}: {next(spinner)}\r", nl=False)
631
- time.sleep(service.WORKER_CHECK_PERIOD)
632
- return queue.get()
633
-
634
-
635
- def create_spinner(repetitions: int) -> Generator[str, None, None]:
636
- """A simple spinner that yields its individual characters."""
637
- while True:
638
- for ch in "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏":
639
- # Skip branch coverage, as it is not possible because of the assertion above
640
- for _ in range(repetitions): # pragma: no branch
641
- yield ch
642
-
643
-
644
- def display_checks_statistics(total: dict[str, dict[str | Status, int]]) -> None:
645
- padding = 20
646
- col1_len = max(map(len, total.keys())) + padding
647
- col2_len = len(str(max(total.values(), key=lambda v: v["total"])["total"])) * 2 + padding
648
- col3_len = padding
649
- click.secho("Performed checks:", bold=True)
650
- template = f" {{:{col1_len}}}{{:{col2_len}}}{{:{col3_len}}}"
651
- for check_name, results in total.items():
652
- display_check_result(check_name, results, template)
653
-
654
-
655
- def display_check_result(check_name: str, results: dict[str | Status, int], template: str) -> None:
656
- """Show results of single check execution."""
657
- if Status.failure in results:
658
- verdict = "FAILED"
659
- color = "red"
660
- else:
661
- verdict = "PASSED"
662
- color = "green"
663
- success = results.get(Status.success, 0)
664
- total = results.get("total", 0)
665
- click.echo(template.format(check_name, f"{success} / {total} passed", click.style(verdict, fg=color, bold=True)))
666
-
667
-
668
- VERIFY_URL_SUGGESTION = "Verify that the URL points directly to the Open API schema"
669
-
670
-
671
- SCHEMA_ERROR_SUGGESTIONS = {
672
- # SSL-specific connection issue
673
- SchemaErrorType.CONNECTION_SSL: DISABLE_SSL_SUGGESTION,
674
- # Other connection problems
675
- SchemaErrorType.CONNECTION_OTHER: f"Use {bold('`--wait-for-schema=NUM`')} to wait up to NUM seconds for schema availability.",
676
- # Response issues
677
- SchemaErrorType.UNEXPECTED_CONTENT_TYPE: VERIFY_URL_SUGGESTION,
678
- SchemaErrorType.HTTP_FORBIDDEN: "Verify your API keys or authentication headers.",
679
- SchemaErrorType.HTTP_NOT_FOUND: VERIFY_URL_SUGGESTION,
680
- # OpenAPI specification issues
681
- SchemaErrorType.OPEN_API_UNSPECIFIED_VERSION: f"Include the version in the schema or manually set it with {bold('`--force-schema-version`')}.",
682
- SchemaErrorType.OPEN_API_UNSUPPORTED_VERSION: f"Proceed with {bold('`--force-schema-version`')}. Caution: May not be fully supported.",
683
- SchemaErrorType.OPEN_API_EXPERIMENTAL_VERSION: f"Proceed with {bold('`--experimental=openapi-3.1`. Caution: May not be fully supported.')}",
684
- SchemaErrorType.OPEN_API_INVALID_SCHEMA: DISABLE_SCHEMA_VALIDATION_SUGGESTION,
685
- # YAML specific issues
686
- SchemaErrorType.YAML_NUMERIC_STATUS_CODES: "Convert numeric status codes to strings.",
687
- SchemaErrorType.YAML_NON_STRING_KEYS: "Convert non-string keys to strings.",
688
- # Unclassified
689
- SchemaErrorType.UNCLASSIFIED: f"If you suspect this is a Schemathesis issue and the schema is valid, please report it and include the schema if you can:\n\n {ISSUE_TRACKER_URL}",
690
- }
691
-
692
-
693
- def should_skip_suggestion(context: ExecutionContext, event: events.InternalError) -> bool:
694
- return event.subtype == SchemaErrorType.CONNECTION_OTHER and context.wait_for_schema is not None
695
-
696
-
697
- def _display_extras(extras: list[str]) -> None:
698
- if extras:
699
- click.echo()
700
- for extra in extras:
701
- click.secho(f" {extra}")
702
-
703
-
704
- def _maybe_display_tip(suggestion: str | None) -> None:
705
- # Display suggestion if any
706
- if suggestion is not None:
707
- click.secho(f"\n{click.style('Tip:', bold=True, fg='green')} {suggestion}")
708
-
709
-
710
- def display_internal_error(context: ExecutionContext, event: events.InternalError) -> None:
711
- click.secho(event.title, fg="red", bold=True)
712
- click.echo()
713
- click.secho(event.message)
714
- if event.type == InternalErrorType.SCHEMA:
715
- extras = event.extras
716
- elif context.show_trace:
717
- extras = split_traceback(event.exception_with_traceback)
718
- else:
719
- extras = [event.exception]
720
- _display_extras(extras)
721
- if not should_skip_suggestion(context, event):
722
- if event.type == InternalErrorType.SCHEMA and isinstance(event.subtype, SchemaErrorType):
723
- suggestion = SCHEMA_ERROR_SUGGESTIONS.get(event.subtype)
724
- elif context.show_trace:
725
- suggestion = (
726
- f"Please consider reporting the traceback above to our issue tracker:\n\n {ISSUE_TRACKER_URL}."
727
- )
728
- else:
729
- suggestion = f"To see full tracebacks, add {bold('`--show-trace`')} to your CLI options"
730
- _maybe_display_tip(suggestion)
731
-
732
-
733
- def handle_initialized(context: ExecutionContext, event: events.Initialized) -> None:
734
- """Display information about the test session."""
735
- context.operations_count = cast(int, event.operations_count) # INVARIANT: should not be `None`
736
- context.seed = event.seed
737
- display_section_name("Schemathesis test session starts")
738
- if context.verbosity > 0:
739
- versions = (
740
- f"platform {platform.system()} -- "
741
- f"Python {platform.python_version()}, "
742
- f"schemathesis-{SCHEMATHESIS_VERSION}, "
743
- f"hypothesis-{metadata.version('hypothesis')}, "
744
- f"hypothesis_jsonschema-{metadata.version('hypothesis_jsonschema')}, "
745
- f"jsonschema-{metadata.version('jsonschema')}"
746
- )
747
- click.echo(versions)
748
- click.echo(f"rootdir: {os.getcwd()}")
749
- click.echo(f"Hypothesis: {context.hypothesis_settings.show_changed()}")
750
- if event.location is not None:
751
- click.secho(f"Schema location: {event.location}", bold=True)
752
- click.secho(f"Base URL: {event.base_url}", bold=True)
753
- click.secho(f"Specification version: {event.specification_name}", bold=True)
754
- if context.seed is not None:
755
- click.secho(f"Random seed: {context.seed}", bold=True)
756
- click.secho(f"Workers: {context.workers_num}", bold=True)
757
- if context.rate_limit is not None:
758
- click.secho(f"Rate limit: {context.rate_limit}", bold=True)
759
- click.secho(f"Collected API operations: {context.operations_count}", bold=True)
760
- links_count = cast(int, event.links_count)
761
- click.secho(f"Collected API links: {links_count}", bold=True)
762
- if isinstance(context.report, ServiceReportContext):
763
- click.secho("Report to Schemathesis.io: ENABLED", bold=True)
764
- if context.initialization_lines:
765
- _print_lines(context.initialization_lines)
766
-
767
-
768
- def handle_before_probing(context: ExecutionContext, event: events.BeforeProbing) -> None:
769
- click.secho("API probing: ...\r", bold=True, nl=False)
770
-
771
-
772
- def handle_after_probing(context: ExecutionContext, event: events.AfterProbing) -> None:
773
- context.probes = event.probes
774
- status = "SKIP"
775
- if event.probes is not None:
776
- for probe in event.probes:
777
- if probe.outcome in (ProbeOutcome.SUCCESS, ProbeOutcome.FAILURE):
778
- # The probe itself has been executed
779
- status = "SUCCESS"
780
- elif probe.outcome == ProbeOutcome.ERROR:
781
- status = "ERROR"
782
- click.secho(f"API probing: {status}", bold=True, nl=False)
783
- click.echo()
784
-
785
-
786
- def handle_before_analysis(context: ExecutionContext, event: events.BeforeAnalysis) -> None:
787
- click.secho("Schema analysis: ...\r", bold=True, nl=False)
788
-
789
-
790
- def handle_after_analysis(context: ExecutionContext, event: events.AfterAnalysis) -> None:
791
- context.analysis = event.analysis
792
- status = "SKIP"
793
- if event.analysis is not None:
794
- if isinstance(event.analysis, Ok) and isinstance(event.analysis.ok(), AnalysisSuccess):
795
- status = "SUCCESS"
796
- else:
797
- status = "ERROR"
798
- click.secho(f"Schema analysis: {status}", bold=True, nl=False)
799
- click.echo()
800
- operations_count = cast(int, context.operations_count) # INVARIANT: should not be `None`
801
- if operations_count >= 1:
802
- click.echo()
803
-
804
-
805
- TRUNCATION_PLACEHOLDER = "[...]"
806
-
807
-
808
- def handle_before_execution(context: ExecutionContext, event: events.BeforeExecution) -> None:
809
- """Display what method / path will be tested next."""
810
- # We should display execution result + percentage in the end. For example:
811
- max_length = get_terminal_width() - len(" . [XXX%]") - len(TRUNCATION_PLACEHOLDER)
812
- message = event.verbose_name
813
- if event.recursion_level > 0:
814
- message = f"{' ' * event.recursion_level}-> {message}"
815
- # This value is not `None` - the value is set in runtime before this line
816
- context.operations_count += 1 # type: ignore
817
-
818
- message = message[:max_length] + (message[max_length:] and "[...]") + " "
819
- context.current_line_length = len(message)
820
- click.echo(message, nl=False)
821
-
822
-
823
- def handle_after_execution(context: ExecutionContext, event: events.AfterExecution) -> None:
824
- """Display the execution result + current progress at the same line with the method / path names."""
825
- context.operations_processed += 1
826
- context.results.append(event.result)
827
- display_execution_result(context, event.status.value)
828
- display_percentage(context, event)
829
-
830
-
831
- def handle_finished(context: ExecutionContext, event: events.Finished) -> None:
832
- """Show the outcome of the whole testing session."""
833
- click.echo()
834
- display_hypothesis_output(context.hypothesis_output)
835
- display_errors(context, event)
836
- display_failures(context, event)
837
- display_application_logs(context, event)
838
- display_analysis(context)
839
- display_statistic(context, event)
840
- if context.summary_lines:
841
- click.echo()
842
- _print_lines(context.summary_lines)
843
- click.echo()
844
- display_summary(event)
845
-
846
-
847
- def _print_lines(lines: list[str | Generator[str, None, None]]) -> None:
848
- for entry in lines:
849
- if isinstance(entry, str):
850
- click.echo(entry)
851
- elif isinstance(entry, GeneratorType):
852
- for line in entry:
853
- click.echo(line)
854
-
855
-
856
- def handle_interrupted(context: ExecutionContext, event: events.Interrupted) -> None:
857
- click.echo()
858
- _handle_interrupted(context)
859
-
860
-
861
- def _handle_interrupted(context: ExecutionContext) -> None:
862
- context.is_interrupted = True
863
- display_section_name("KeyboardInterrupt", "!", bold=False)
864
-
865
-
866
- def handle_internal_error(context: ExecutionContext, event: events.InternalError) -> None:
867
- display_internal_error(context, event)
868
- raise click.Abort
869
-
870
-
871
- def handle_stateful_event(context: ExecutionContext, event: events.StatefulEvent) -> None:
872
- if isinstance(event.data, stateful_events.RunStarted):
873
- context.state_machine_sink = event.data.state_machine.sink()
874
- if not experimental.STATEFUL_ONLY.is_enabled:
875
- click.echo()
876
- click.secho("Stateful tests\n", bold=True)
877
- elif isinstance(event.data, stateful_events.ScenarioFinished) and not event.data.is_final:
878
- if event.data.status == stateful_events.ScenarioStatus.INTERRUPTED:
879
- _handle_interrupted(context)
880
- elif event.data.status != stateful_events.ScenarioStatus.REJECTED:
881
- display_execution_result(context, event.data.status.value)
882
- elif isinstance(event.data, stateful_events.RunFinished):
883
- click.echo()
884
- # It is initialized in `RunStarted`
885
- sink = cast(StateMachineSink, context.state_machine_sink)
886
- sink.consume(event.data)
887
-
888
-
889
- def handle_after_stateful_execution(context: ExecutionContext, event: events.AfterStatefulExecution) -> None:
890
- context.results.append(event.result)
891
-
892
-
893
- class DefaultOutputStyleHandler(EventHandler):
894
- def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
895
- """Choose and execute a proper handler for the given event."""
896
- if isinstance(event, events.Initialized):
897
- handle_initialized(context, event)
898
- elif isinstance(event, events.BeforeProbing):
899
- handle_before_probing(context, event)
900
- elif isinstance(event, events.AfterProbing):
901
- handle_after_probing(context, event)
902
- elif isinstance(event, events.BeforeAnalysis):
903
- handle_before_analysis(context, event)
904
- elif isinstance(event, events.AfterAnalysis):
905
- handle_after_analysis(context, event)
906
- elif isinstance(event, events.BeforeExecution):
907
- handle_before_execution(context, event)
908
- elif isinstance(event, events.AfterExecution):
909
- context.hypothesis_output.extend(event.hypothesis_output)
910
- handle_after_execution(context, event)
911
- elif isinstance(event, events.Finished):
912
- handle_finished(context, event)
913
- elif isinstance(event, events.Interrupted):
914
- handle_interrupted(context, event)
915
- elif isinstance(event, events.InternalError):
916
- handle_internal_error(context, event)
917
- elif isinstance(event, events.StatefulEvent):
918
- handle_stateful_event(context, event)
919
- elif isinstance(event, events.AfterStatefulExecution):
920
- handle_after_stateful_execution(context, event)