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,544 +0,0 @@
1
- """Transformation from Schemathesis-specific data structures to ones that can be serialized and sent over network.
2
-
3
- They all consist of primitive types and don't have references to schemas, app, etc.
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- import logging
9
- import re
10
- import textwrap
11
- from dataclasses import asdict, dataclass, field
12
- from typing import TYPE_CHECKING, Any, cast
13
-
14
- from ..code_samples import get_excluded_headers
15
- from ..exceptions import (
16
- BodyInGetRequestError,
17
- DeadlineExceeded,
18
- InternalError,
19
- InvalidRegularExpression,
20
- OperationSchemaError,
21
- RecursiveReferenceError,
22
- RuntimeErrorType,
23
- SerializationError,
24
- UnboundPrefixError,
25
- extract_requests_exception_details,
26
- format_exception,
27
- make_unique_by_key,
28
- )
29
- from ..models import Case, Check, Interaction, Request, Response, Status, TestPhase, TestResult, TransitionId
30
- from ..transports import deserialize_payload, serialize_payload
31
-
32
- if TYPE_CHECKING:
33
- import hypothesis.errors
34
- from requests.structures import CaseInsensitiveDict
35
-
36
- from ..failures import FailureContext
37
- from ..generation import DataGenerationMethod
38
-
39
-
40
- @dataclass
41
- class SerializedCase:
42
- # Case data
43
- id: str
44
- generation_time: float
45
- path_parameters: dict[str, Any] | None
46
- headers: dict[str, Any] | None
47
- cookies: dict[str, Any] | None
48
- query: dict[str, Any] | None
49
- body: str | None
50
- media_type: str | None
51
- data_generation_method: str | None
52
- # Operation data
53
- method: str
54
- url: str
55
- path_template: str
56
- full_path: str
57
- verbose_name: str
58
- transition_id: TransitionId | None
59
- # Transport info
60
- verify: bool
61
- # Headers coming from sources outside data generation
62
- extra_headers: dict[str, Any]
63
-
64
- @classmethod
65
- def from_case(cls, case: Case, headers: dict[str, Any] | None, verify: bool) -> SerializedCase:
66
- # `headers` include not only explicitly provided headers but also ones added by hooks, custom auth, etc.
67
- request_data = case.prepare_code_sample_data(headers)
68
- serialized_body = _serialize_body(request_data.body)
69
- return cls(
70
- id=case.id,
71
- generation_time=case.generation_time,
72
- path_parameters=case.path_parameters,
73
- headers=dict(case.headers) if case.headers is not None else None,
74
- cookies=case.cookies,
75
- query=case.query,
76
- body=serialized_body,
77
- media_type=case.media_type,
78
- data_generation_method=case.data_generation_method.as_short_name()
79
- if case.data_generation_method is not None
80
- else None,
81
- method=case.method,
82
- url=request_data.url,
83
- path_template=case.path,
84
- full_path=case.full_path,
85
- verbose_name=case.operation.verbose_name,
86
- transition_id=case.source.transition_id if case.source is not None else None,
87
- verify=verify,
88
- extra_headers=request_data.headers,
89
- )
90
-
91
- def deserialize_body(self) -> bytes | None:
92
- """Deserialize the test case body.
93
-
94
- `SerializedCase` should be serializable to JSON, therefore body is encoded as base64 string
95
- to support arbitrary binary data.
96
- """
97
- return deserialize_payload(self.body)
98
-
99
-
100
- def _serialize_body(body: str | bytes | None) -> str | None:
101
- if body is None:
102
- return None
103
- if isinstance(body, str):
104
- body = body.encode("utf-8")
105
- return serialize_payload(body)
106
-
107
-
108
- @dataclass
109
- class SerializedCheck:
110
- # Check name
111
- name: str
112
- # Check result
113
- value: Status
114
- request: Request
115
- response: Response | None
116
- # Generated example
117
- example: SerializedCase
118
- # Message could be absent for plain `assert` statements
119
- message: str | None = None
120
- # Failure-specific context
121
- context: FailureContext | None = None
122
- # Cases & responses that were made before this one
123
- history: list[SerializedHistoryEntry] = field(default_factory=list)
124
-
125
- @classmethod
126
- def from_check(cls, check: Check) -> SerializedCheck:
127
- import requests
128
-
129
- from ..transports.responses import WSGIResponse
130
-
131
- if check.response is not None:
132
- request = Request.from_prepared_request(check.response.request)
133
- elif check.request is not None:
134
- # Response is not available, but it is not an error (only time-out behaves this way at the moment)
135
- request = Request.from_prepared_request(check.request)
136
- else:
137
- raise InternalError("Can not find request data")
138
-
139
- response: Response | None
140
- if isinstance(check.response, requests.Response):
141
- response = Response.from_requests(check.response)
142
- elif isinstance(check.response, WSGIResponse):
143
- response = Response.from_wsgi(check.response, check.elapsed)
144
- else:
145
- response = None
146
- headers = _get_headers(request.headers)
147
- history = get_serialized_history(check.example)
148
- return cls(
149
- name=check.name,
150
- value=check.value,
151
- example=SerializedCase.from_case(
152
- check.example, headers, verify=response.verify if response is not None else True
153
- ),
154
- message=check.message,
155
- request=request,
156
- response=response,
157
- context=check.context,
158
- history=history,
159
- )
160
-
161
- @property
162
- def title(self) -> str:
163
- if self.context is not None:
164
- return self.context.title
165
- return f"Custom check failed: `{self.name}`"
166
-
167
- @property
168
- def formatted_message(self) -> str | None:
169
- if self.context is not None:
170
- if self.context.message:
171
- message = self.context.message
172
- else:
173
- message = None
174
- else:
175
- message = self.message
176
- if message is not None:
177
- message = textwrap.indent(message, prefix=" ")
178
- return message
179
-
180
-
181
- def _get_headers(headers: dict[str, Any] | CaseInsensitiveDict) -> dict[str, str]:
182
- return {
183
- key: value[0] if isinstance(value, list) else value
184
- for key, value in headers.items()
185
- if key not in get_excluded_headers()
186
- }
187
-
188
-
189
- @dataclass
190
- class SerializedHistoryEntry:
191
- case: SerializedCase
192
- response: Response
193
-
194
-
195
- def get_serialized_history(case: Case) -> list[SerializedHistoryEntry]:
196
- import requests
197
-
198
- history = []
199
- while case.source is not None:
200
- history_request = case.source.response.request
201
- headers = _get_headers(history_request.headers)
202
- if isinstance(case.source.response, requests.Response):
203
- history_response = Response.from_requests(case.source.response)
204
- verify = history_response.verify
205
- else:
206
- history_response = Response.from_wsgi(case.source.response, case.source.elapsed)
207
- verify = True
208
- entry = SerializedHistoryEntry(
209
- case=SerializedCase.from_case(case.source.case, headers, verify=verify), response=history_response
210
- )
211
- history.append(entry)
212
- case = case.source.case
213
- return history
214
-
215
-
216
- @dataclass
217
- class SerializedError:
218
- type: RuntimeErrorType
219
- title: str | None
220
- message: str | None
221
- extras: list[str]
222
-
223
- # Exception info
224
- exception: str
225
- exception_with_traceback: str
226
-
227
- @classmethod
228
- def with_exception(
229
- cls,
230
- type_: RuntimeErrorType,
231
- title: str | None,
232
- message: str | None,
233
- extras: list[str],
234
- exception: Exception,
235
- ) -> SerializedError:
236
- return cls(
237
- type=type_,
238
- title=title,
239
- message=message,
240
- extras=extras,
241
- exception=format_exception(exception),
242
- exception_with_traceback=format_exception(exception, True),
243
- )
244
-
245
- @classmethod
246
- def from_exception(cls, exception: Exception) -> SerializedError:
247
- import hypothesis.errors
248
- import requests
249
- from hypothesis import HealthCheck
250
-
251
- title = "Runtime Error"
252
- message: str | None
253
- if isinstance(exception, requests.RequestException):
254
- if isinstance(exception, requests.exceptions.SSLError):
255
- type_ = RuntimeErrorType.CONNECTION_SSL
256
- elif isinstance(exception, requests.exceptions.ConnectionError):
257
- type_ = RuntimeErrorType.CONNECTION_OTHER
258
- else:
259
- type_ = RuntimeErrorType.NETWORK_OTHER
260
- message, extras = extract_requests_exception_details(exception)
261
- title = "Network Error"
262
- elif isinstance(exception, DeadlineExceeded):
263
- type_ = RuntimeErrorType.HYPOTHESIS_DEADLINE_EXCEEDED
264
- message = str(exception).strip()
265
- extras = []
266
- elif isinstance(exception, RecursiveReferenceError):
267
- type_ = RuntimeErrorType.SCHEMA_UNSUPPORTED
268
- message = str(exception).strip()
269
- extras = []
270
- title = "Unsupported Schema"
271
- elif isinstance(exception, hypothesis.errors.InvalidArgument) and str(exception).startswith("Scalar "):
272
- # Comes from `hypothesis-graphql`
273
- scalar_name = _scalar_name_from_error(exception)
274
- type_ = RuntimeErrorType.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR
275
- message = f"Scalar type '{scalar_name}' is not recognized"
276
- extras = []
277
- title = "Unknown GraphQL Scalar"
278
- elif (
279
- isinstance(exception, hypothesis.errors.InvalidArgument)
280
- and str(exception).endswith("larger than Hypothesis is designed to handle")
281
- or "can never generate an example, because min_size is larger than Hypothesis supports" in str(exception)
282
- ):
283
- type_ = RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE
284
- message = HEALTH_CHECK_MESSAGE_LARGE_BASE_EXAMPLE
285
- extras = []
286
- title = "Failed Health Check"
287
- elif isinstance(exception, hypothesis.errors.Unsatisfiable):
288
- type_ = RuntimeErrorType.HYPOTHESIS_UNSATISFIABLE
289
- message = f"{exception}. Possible reasons:"
290
- extras = [
291
- "- Contradictory schema constraints, such as a minimum value exceeding the maximum.",
292
- "- Invalid schema definitions for headers or cookies, for example allowing for non-ASCII characters.",
293
- "- Excessive schema complexity, which hinders parameter generation.",
294
- ]
295
- title = "Schema Error"
296
- elif isinstance(exception, hypothesis.errors.FailedHealthCheck):
297
- health_check = _health_check_from_error(exception)
298
- if health_check is not None:
299
- message, type_ = {
300
- HealthCheck.data_too_large: (
301
- HEALTH_CHECK_MESSAGE_DATA_TOO_LARGE,
302
- RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE,
303
- ),
304
- HealthCheck.filter_too_much: (
305
- HEALTH_CHECK_MESSAGE_FILTER_TOO_MUCH,
306
- RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH,
307
- ),
308
- HealthCheck.too_slow: (
309
- HEALTH_CHECK_MESSAGE_TOO_SLOW,
310
- RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW,
311
- ),
312
- HealthCheck.large_base_example: (
313
- HEALTH_CHECK_MESSAGE_LARGE_BASE_EXAMPLE,
314
- RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE,
315
- ),
316
- }[health_check]
317
- else:
318
- type_ = RuntimeErrorType.UNCLASSIFIED
319
- message = str(exception)
320
- extras = []
321
- title = "Failed Health Check"
322
- elif isinstance(exception, OperationSchemaError):
323
- if isinstance(exception, BodyInGetRequestError):
324
- type_ = RuntimeErrorType.SCHEMA_BODY_IN_GET_REQUEST
325
- elif isinstance(exception, InvalidRegularExpression) and exception.is_valid_type:
326
- type_ = RuntimeErrorType.SCHEMA_INVALID_REGULAR_EXPRESSION
327
- else:
328
- type_ = RuntimeErrorType.SCHEMA_GENERIC
329
- message = exception.message
330
- extras = []
331
- title = "Schema Error"
332
- elif isinstance(exception, SerializationError):
333
- if isinstance(exception, UnboundPrefixError):
334
- type_ = RuntimeErrorType.SERIALIZATION_UNBOUNDED_PREFIX
335
- title = "XML serialization error"
336
- else:
337
- title = "Serialization not possible"
338
- type_ = RuntimeErrorType.SERIALIZATION_NOT_POSSIBLE
339
- message = str(exception)
340
- extras = []
341
- else:
342
- type_ = RuntimeErrorType.UNCLASSIFIED
343
- message = str(exception)
344
- extras = []
345
- return cls.with_exception(type_=type_, exception=exception, title=title, message=message, extras=extras)
346
-
347
-
348
- HEALTH_CHECK_MESSAGE_DATA_TOO_LARGE = """There's a notable occurrence of examples surpassing the maximum size limit.
349
- Typically, generating excessively large examples can compromise the quality of test outcomes.
350
-
351
- Consider revising the schema to more accurately represent typical use cases
352
- or applying constraints to reduce the data size."""
353
- HEALTH_CHECK_MESSAGE_FILTER_TOO_MUCH = """A significant number of generated examples are being filtered out, indicating
354
- that the schema's constraints may be too complex.
355
-
356
- This level of filtration can slow down testing and affect the distribution
357
- of generated data. Review and simplify the schema constraints where
358
- possible to mitigate this issue."""
359
- HEALTH_CHECK_MESSAGE_TOO_SLOW = "Data generation is extremely slow. Consider reducing the complexity of the schema."
360
- HEALTH_CHECK_MESSAGE_LARGE_BASE_EXAMPLE = """A health check has identified that the smallest example derived from the schema
361
- is excessively large, potentially leading to inefficient test execution.
362
-
363
- This is commonly due to schemas that specify large-scale data structures by
364
- default, such as an array with an extensive number of elements.
365
-
366
- Consider revising the schema to more accurately represent typical use cases
367
- or applying constraints to reduce the data size."""
368
-
369
-
370
- def _health_check_from_error(exception: hypothesis.errors.FailedHealthCheck) -> hypothesis.HealthCheck | None:
371
- from hypothesis import HealthCheck
372
-
373
- match = re.search(r"add HealthCheck\.(\w+) to the suppress_health_check ", str(exception))
374
- if match:
375
- return {
376
- "data_too_large": HealthCheck.data_too_large,
377
- "filter_too_much": HealthCheck.filter_too_much,
378
- "too_slow": HealthCheck.too_slow,
379
- "large_base_example": HealthCheck.large_base_example,
380
- }.get(match.group(1))
381
- return None
382
-
383
-
384
- def _scalar_name_from_error(exception: hypothesis.errors.InvalidArgument) -> str:
385
- # This one is always available as the format is checked upfront
386
- match = re.search(r"Scalar '(\w+)' is not supported", str(exception))
387
- match = cast(re.Match, match)
388
- return match.group(1)
389
-
390
-
391
- @dataclass
392
- class SerializedInteraction:
393
- request: Request
394
- response: Response | None
395
- checks: list[SerializedCheck]
396
- status: Status
397
- data_generation_method: DataGenerationMethod
398
- phase: TestPhase | None
399
- description: str | None
400
- location: str | None
401
- parameter: str | None
402
- parameter_location: str | None
403
- recorded_at: str
404
-
405
- @classmethod
406
- def from_interaction(cls, interaction: Interaction) -> SerializedInteraction:
407
- return cls(
408
- request=interaction.request,
409
- response=interaction.response,
410
- checks=[SerializedCheck.from_check(check) for check in interaction.checks],
411
- status=interaction.status,
412
- data_generation_method=interaction.data_generation_method,
413
- phase=interaction.phase,
414
- description=interaction.description,
415
- location=interaction.location,
416
- parameter=interaction.parameter,
417
- parameter_location=interaction.parameter_location,
418
- recorded_at=interaction.recorded_at,
419
- )
420
-
421
-
422
- @dataclass
423
- class SerializedTestResult:
424
- method: str
425
- path: str
426
- verbose_name: str
427
- has_failures: bool
428
- has_errors: bool
429
- has_logs: bool
430
- is_errored: bool
431
- is_flaky: bool
432
- is_skipped: bool
433
- skip_reason: str | None
434
- seed: int | None
435
- data_generation_method: list[str]
436
- checks: list[SerializedCheck]
437
- logs: list[str]
438
- errors: list[SerializedError]
439
- interactions: list[SerializedInteraction]
440
-
441
- @classmethod
442
- def from_test_result(cls, result: TestResult) -> SerializedTestResult:
443
- formatter = logging.Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s")
444
- return cls(
445
- method=result.method,
446
- path=result.path,
447
- verbose_name=result.verbose_name,
448
- has_failures=result.has_failures,
449
- has_errors=result.has_errors,
450
- has_logs=result.has_logs,
451
- is_errored=result.is_errored,
452
- is_flaky=result.is_flaky,
453
- is_skipped=result.is_skipped,
454
- skip_reason=result.skip_reason,
455
- seed=result.seed,
456
- data_generation_method=[m.as_short_name() for m in result.data_generation_method],
457
- checks=[SerializedCheck.from_check(check) for check in result.checks],
458
- logs=[formatter.format(record) for record in result.logs],
459
- errors=[SerializedError.from_exception(error) for error in result.errors],
460
- interactions=[SerializedInteraction.from_interaction(interaction) for interaction in result.interactions],
461
- )
462
-
463
-
464
- def deduplicate_failures(checks: list[SerializedCheck]) -> list[SerializedCheck]:
465
- """Return only unique checks that should be displayed in the output."""
466
- seen: set[tuple[str | None, ...]] = set()
467
- unique_checks = []
468
- for check in reversed(checks):
469
- # There are also could be checks that didn't fail
470
- if check.value == Status.failure:
471
- key = make_unique_by_key(check.name, check.message, check.context)
472
- if key not in seen:
473
- unique_checks.append(check)
474
- seen.add(key)
475
- return unique_checks
476
-
477
-
478
- def _serialize_case(case: SerializedCase) -> dict[str, Any]:
479
- return {
480
- "id": case.id,
481
- "generation_time": case.generation_time,
482
- "verbose_name": case.verbose_name,
483
- "path_template": case.path_template,
484
- "path_parameters": stringify_path_parameters(case.path_parameters),
485
- "query": prepare_query(case.query),
486
- "cookies": case.cookies,
487
- "media_type": case.media_type,
488
- }
489
-
490
-
491
- def _serialize_response(response: Response) -> dict[str, Any]:
492
- return {
493
- "status_code": response.status_code,
494
- "headers": response.headers,
495
- "body": response.body,
496
- "encoding": response.encoding,
497
- "elapsed": response.elapsed,
498
- }
499
-
500
-
501
- def _serialize_check(check: SerializedCheck) -> dict[str, Any]:
502
- return {
503
- "name": check.name,
504
- "value": check.value,
505
- "request": {
506
- "method": check.request.method,
507
- "uri": check.request.uri,
508
- "body": check.request.body,
509
- "headers": check.request.headers,
510
- },
511
- "response": _serialize_response(check.response) if check.response is not None else None,
512
- "example": _serialize_case(check.example),
513
- "message": check.message,
514
- "context": asdict(check.context) if check.context is not None else None, # type: ignore
515
- "history": [
516
- {"case": _serialize_case(entry.case), "response": _serialize_response(entry.response)}
517
- for entry in check.history
518
- ],
519
- }
520
-
521
-
522
- def stringify_path_parameters(path_parameters: dict[str, Any] | None) -> dict[str, str]:
523
- """Cast all path parameter values to strings.
524
-
525
- Path parameter values may be of arbitrary type, but to display them properly they should be casted to strings.
526
- """
527
- return {key: str(value) for key, value in (path_parameters or {}).items()}
528
-
529
-
530
- def prepare_query(query: dict[str, Any] | None) -> dict[str, list[str]]:
531
- """Convert all query values to list of strings.
532
-
533
- Query parameters may be generated in different shapes, including integers, strings, list of strings, etc.
534
- It can also be an object, if the schema contains an object, but `style` and `explode` combo is not applicable.
535
- """
536
-
537
- def to_list_of_strings(value: Any) -> list[str]:
538
- if isinstance(value, list):
539
- return list(map(str, value))
540
- if isinstance(value, str):
541
- return [value]
542
- return [str(value)]
543
-
544
- return {key: to_list_of_strings(value) for key, value in (query or {}).items()}