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
@@ -1,198 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass, field
4
- from typing import TYPE_CHECKING, Iterator, Union
5
-
6
- from ....internal.copy import fast_deepcopy
7
- from ....stateful.statistic import TransitionStats
8
-
9
- if TYPE_CHECKING:
10
- from ....stateful import events
11
- from .types import AggregatedResponseCounter, LinkName, ResponseCounter, SourceName, StatusCode, TargetName
12
-
13
-
14
- @dataclass
15
- class LinkSource:
16
- name: str
17
- responses: dict[StatusCode, dict[TargetName, dict[LinkName, ResponseCounter]]]
18
- is_first: bool
19
-
20
- __slots__ = ("name", "responses", "is_first")
21
-
22
-
23
- @dataclass
24
- class OperationResponse:
25
- status_code: str
26
- targets: dict[TargetName, dict[LinkName, ResponseCounter]]
27
- is_last: bool
28
-
29
- __slots__ = ("status_code", "targets", "is_last")
30
-
31
-
32
- @dataclass
33
- class Link:
34
- name: str
35
- target: str
36
- responses: ResponseCounter
37
- is_last: bool
38
- is_single: bool
39
-
40
- __slots__ = ("name", "target", "responses", "is_last", "is_single")
41
-
42
-
43
- StatisticEntry = Union[LinkSource, OperationResponse, Link]
44
-
45
-
46
- @dataclass
47
- class FormattedStatisticEntry:
48
- line: str
49
- entry: StatisticEntry
50
- __slots__ = ("line", "entry")
51
-
52
-
53
- @dataclass
54
- class OpenAPILinkStats(TransitionStats):
55
- """Statistics about link transitions for a state machine run."""
56
-
57
- transitions: dict[SourceName, dict[StatusCode, dict[TargetName, dict[LinkName, ResponseCounter]]]]
58
-
59
- roots: dict[TargetName, ResponseCounter] = field(default_factory=dict)
60
-
61
- __slots__ = ("transitions",)
62
-
63
- def consume(self, event: events.StatefulEvent) -> None:
64
- from ....stateful import events
65
-
66
- if isinstance(event, events.StepFinished):
67
- if event.transition_id is not None:
68
- transition_id = event.transition_id
69
- source = self.transitions[transition_id.source]
70
- transition = source[transition_id.status_code][event.target][transition_id.name]
71
- if event.response is not None:
72
- key = event.response.status_code
73
- else:
74
- key = None
75
- counter = transition.setdefault(key, 0)
76
- transition[key] = counter + 1
77
- else:
78
- # A start of a sequence has an empty source and does not belong to any transition
79
- target = self.roots.setdefault(event.target, {})
80
- if event.response is not None:
81
- key = event.response.status_code
82
- else:
83
- key = None
84
- counter = target.setdefault(key, 0)
85
- target[key] = counter + 1
86
-
87
- def copy(self) -> OpenAPILinkStats:
88
- return self.__class__(transitions=fast_deepcopy(self.transitions))
89
-
90
- def iter(self) -> Iterator[StatisticEntry]:
91
- for source_idx, (source, responses) in enumerate(self.transitions.items()):
92
- yield LinkSource(name=source, responses=responses, is_first=source_idx == 0)
93
- for response_idx, (status_code, targets) in enumerate(responses.items()):
94
- yield OperationResponse(
95
- status_code=status_code, targets=targets, is_last=response_idx == len(responses) - 1
96
- )
97
- for target_idx, (target, links) in enumerate(targets.items()):
98
- for link_idx, (link_name, link_responses) in enumerate(links.items()):
99
- yield Link(
100
- name=link_name,
101
- target=target,
102
- responses=link_responses,
103
- is_last=target_idx == len(targets) - 1 and link_idx == len(links) - 1,
104
- is_single=len(links) == 1,
105
- )
106
-
107
- def iter_with_format(self) -> Iterator[FormattedStatisticEntry]:
108
- current_response = None
109
- for entry in self.iter():
110
- if isinstance(entry, LinkSource):
111
- if not entry.is_first:
112
- yield FormattedStatisticEntry(line=f"\n{entry.name}", entry=entry)
113
- else:
114
- yield FormattedStatisticEntry(line=f"{entry.name}", entry=entry)
115
- elif isinstance(entry, OperationResponse):
116
- current_response = entry
117
- if entry.is_last:
118
- yield FormattedStatisticEntry(line=f"└── {entry.status_code}", entry=entry)
119
- else:
120
- yield FormattedStatisticEntry(line=f"├── {entry.status_code}", entry=entry)
121
- else:
122
- if current_response is not None and current_response.is_last:
123
- line = " "
124
- else:
125
- line = "│ "
126
- if entry.is_last:
127
- line += "└"
128
- else:
129
- line += "├"
130
- if entry.is_single or entry.name == entry.target:
131
- line += f"── {entry.target}"
132
- else:
133
- line += f"── {entry.name} -> {entry.target}"
134
- yield FormattedStatisticEntry(line=line, entry=entry)
135
-
136
- def to_formatted_table(self, width: int) -> str:
137
- """Format the statistic as a table."""
138
- entries = list(self.iter_with_format())
139
- lines: list[str | list[str]] = [HEADER, ""]
140
- column_widths = [len(column) for column in HEADER]
141
- for entry in entries:
142
- if isinstance(entry.entry, Link):
143
- aggregated = _aggregate_responses(entry.entry.responses)
144
- values = [
145
- entry.line,
146
- str(aggregated["2xx"]),
147
- str(aggregated["4xx"]),
148
- str(aggregated["5xx"]),
149
- str(aggregated["Total"]),
150
- ]
151
- column_widths = [max(column_widths[idx], len(column)) for idx, column in enumerate(values)]
152
- lines.append(values)
153
- else:
154
- lines.append(entry.line)
155
- used_width = sum(column_widths) + 4 * PADDING
156
- max_space = width - used_width if used_width < width else 0
157
- formatted_lines = []
158
-
159
- for line in lines:
160
- if isinstance(line, list):
161
- formatted_line, *counters = line
162
- formatted_line = formatted_line.ljust(column_widths[0] + max_space)
163
-
164
- for column, max_width in zip(counters, column_widths[1:]):
165
- formatted_line += f"{column:>{max_width + PADDING}}"
166
-
167
- formatted_lines.append(formatted_line)
168
- else:
169
- formatted_lines.append(line)
170
-
171
- return "\n".join(formatted_lines)
172
-
173
-
174
- PADDING = 4
175
- HEADER = ["Links", "2xx", "4xx", "5xx", "Total"]
176
-
177
-
178
- def _aggregate_responses(responses: ResponseCounter) -> AggregatedResponseCounter:
179
- """Aggregate responses by status code ranges."""
180
- output: AggregatedResponseCounter = {
181
- "2xx": 0,
182
- # NOTE: 3xx responses are not counted
183
- "4xx": 0,
184
- "5xx": 0,
185
- "Total": 0,
186
- }
187
- for status_code, count in responses.items():
188
- if status_code is not None:
189
- if 200 <= status_code < 300:
190
- output["2xx"] += count
191
- output["Total"] += count
192
- elif 400 <= status_code < 500:
193
- output["4xx"] += count
194
- output["Total"] += count
195
- elif 500 <= status_code < 600:
196
- output["5xx"] += count
197
- output["Total"] += count
198
- return output
@@ -1,14 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING, Callable, Dict, TypedDict, Union
4
-
5
- if TYPE_CHECKING:
6
- from ....stateful.state_machine import StepResult
7
-
8
- StatusCode = str
9
- LinkName = str
10
- TargetName = str
11
- SourceName = str
12
- ResponseCounter = Dict[Union[int, None], int]
13
- FilterFunction = Callable[["StepResult"], bool]
14
- AggregatedResponseCounter = TypedDict("AggregatedResponseCounter", {"2xx": int, "4xx": int, "5xx": int, "Total": int})
@@ -1,26 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Any
4
-
5
- from ...constants import HTTP_METHODS
6
-
7
-
8
- def is_pattern_error(exception: TypeError) -> bool:
9
- """Detect whether the input exception was caused by invalid type passed to `re.search`."""
10
- # This is intentionally simplistic and do not involve any traceback analysis
11
- return "expected string or bytes-like object" in str(exception)
12
-
13
-
14
- def find_numeric_http_status_codes(schema: Any) -> list[tuple[int, list[str | int]]]:
15
- if not isinstance(schema, dict):
16
- return []
17
- found = []
18
- for path, methods in schema.get("paths", {}).items():
19
- if isinstance(methods, dict):
20
- for method, definition in methods.items():
21
- if method not in HTTP_METHODS or not isinstance(definition, dict):
22
- continue
23
- for key in definition.get("responses", {}):
24
- if isinstance(key, int):
25
- found.append((key, [path, method]))
26
- return found
@@ -1,147 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import enum
4
- import json
5
- from dataclasses import dataclass, field
6
- from typing import TYPE_CHECKING, Any, Callable, Generator
7
-
8
- from ..constants import NOT_SET
9
- from ..internal.result import Ok, Result
10
-
11
- if TYPE_CHECKING:
12
- import hypothesis
13
-
14
- from .. import GenerationConfig
15
- from ..exceptions import OperationSchemaError
16
- from ..models import APIOperation, Case
17
- from ..transports.responses import GenericResponse
18
- from .state_machine import APIStateMachine
19
-
20
-
21
- class UnresolvableLink(Exception):
22
- """Raised when a link cannot be resolved."""
23
-
24
-
25
- @enum.unique
26
- class Stateful(enum.Enum):
27
- none = 1
28
- links = 2
29
-
30
-
31
- @dataclass
32
- class ParsedData:
33
- """A structure that holds information parsed from a test outcome.
34
-
35
- It is used later to create a new version of an API operation that will reuse this data.
36
- """
37
-
38
- parameters: dict[str, Any]
39
- body: Any = NOT_SET
40
-
41
- def __hash__(self) -> int:
42
- """Custom hash simplifies deduplication of parsed data."""
43
- value = hash(tuple(self.parameters.items())) # parameters never contain nested dicts / lists
44
- if self.body is not NOT_SET:
45
- if isinstance(self.body, (dict, list)):
46
- # The simplest way to get a hash of a potentially nested structure
47
- value ^= hash(json.dumps(self.body, sort_keys=True))
48
- else:
49
- # These types should be hashable
50
- value ^= hash(self.body)
51
- return value
52
-
53
-
54
- @dataclass
55
- class StatefulTest:
56
- """A template for a test that will be executed after another one by reusing the outcomes from it."""
57
-
58
- name: str
59
-
60
- def parse(self, case: Case, response: GenericResponse) -> ParsedData:
61
- raise NotImplementedError
62
-
63
- def is_match(self) -> bool:
64
- raise NotImplementedError
65
-
66
- def make_operation(self, collected: list[ParsedData]) -> APIOperation:
67
- raise NotImplementedError
68
-
69
-
70
- @dataclass
71
- class StatefulData:
72
- """Storage for data that will be used in later tests."""
73
-
74
- stateful_test: StatefulTest
75
- container: list[ParsedData] = field(default_factory=list)
76
-
77
- def make_operation(self) -> APIOperation:
78
- return self.stateful_test.make_operation(self.container)
79
-
80
- def store(self, case: Case, response: GenericResponse) -> None:
81
- """Parse and store data for a stateful test."""
82
- try:
83
- parsed = self.stateful_test.parse(case, response)
84
- self.container.append(parsed)
85
- except UnresolvableLink:
86
- # For now, ignore if a link cannot be resolved
87
- pass
88
-
89
-
90
- @dataclass
91
- class Feedback:
92
- """Handler for feedback from tests.
93
-
94
- Provides a way to control runner's behavior from tests.
95
- """
96
-
97
- stateful: Stateful | None
98
- operation: APIOperation = field(repr=False)
99
- stateful_tests: dict[str, StatefulData] = field(default_factory=dict, repr=False)
100
-
101
- def add_test_case(self, case: Case, response: GenericResponse) -> None:
102
- """Store test data to reuse it in the future additional tests."""
103
- for stateful_test in case.operation.get_stateful_tests(response, self.stateful):
104
- data = self.stateful_tests.setdefault(stateful_test.name, StatefulData(stateful_test))
105
- data.store(case, response)
106
-
107
- def get_stateful_tests(
108
- self,
109
- test: Callable,
110
- settings: hypothesis.settings | None,
111
- generation_config: GenerationConfig | None,
112
- seed: int | None,
113
- as_strategy_kwargs: dict[str, Any] | Callable[[APIOperation], dict[str, Any]] | None,
114
- ) -> Generator[Result[tuple[APIOperation, Callable], OperationSchemaError], None, None]:
115
- """Generate additional tests that use data from the previous ones."""
116
- from .._hypothesis import create_test
117
-
118
- for data in self.stateful_tests.values():
119
- if data.stateful_test.is_match():
120
- operation = data.make_operation()
121
- _as_strategy_kwargs: dict[str, Any] | None
122
- if callable(as_strategy_kwargs):
123
- _as_strategy_kwargs = as_strategy_kwargs(operation)
124
- else:
125
- _as_strategy_kwargs = as_strategy_kwargs
126
- test_function = create_test(
127
- operation=operation,
128
- test=test,
129
- settings=settings,
130
- seed=seed,
131
- data_generation_methods=operation.schema.data_generation_methods,
132
- generation_config=generation_config,
133
- as_strategy_kwargs=_as_strategy_kwargs,
134
- )
135
- yield Ok((operation, test_function))
136
-
137
-
138
- def run_state_machine_as_test(
139
- state_machine_factory: type[APIStateMachine], *, settings: hypothesis.settings | None = None
140
- ) -> None:
141
- """Run a state machine as a test.
142
-
143
- It automatically adds the `_min_steps` argument if ``Hypothesis`` is recent enough.
144
- """
145
- from hypothesis.stateful import run_state_machine_as_test as _run_state_machine_as_test
146
-
147
- return _run_state_machine_as_test(state_machine_factory, settings=settings, _min_steps=2)
@@ -1,97 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass, field
4
- from datetime import timedelta
5
- from typing import TYPE_CHECKING, Any
6
-
7
- from ..constants import DEFAULT_DEADLINE
8
-
9
- if TYPE_CHECKING:
10
- import hypothesis
11
- from requests.auth import HTTPDigestAuth
12
-
13
- from .._override import CaseOverride
14
- from ..models import CheckFunction
15
- from ..targets import Target
16
- from ..transports import RequestConfig
17
- from ..types import RawAuth
18
-
19
-
20
- def _default_checks_factory() -> tuple[CheckFunction, ...]:
21
- from ..checks import ALL_CHECKS
22
- from ..specs.openapi.checks import ensure_resource_availability, use_after_free
23
-
24
- return (*ALL_CHECKS, use_after_free, ensure_resource_availability)
25
-
26
-
27
- def _get_default_hypothesis_settings_kwargs() -> dict[str, Any]:
28
- import hypothesis
29
-
30
- return {
31
- "phases": (hypothesis.Phase.generate,),
32
- "deadline": None,
33
- "stateful_step_count": 6,
34
- "suppress_health_check": list(hypothesis.HealthCheck),
35
- }
36
-
37
-
38
- def _default_hypothesis_settings_factory() -> hypothesis.settings:
39
- # To avoid importing hypothesis at the module level
40
- import hypothesis
41
-
42
- return hypothesis.settings(**_get_default_hypothesis_settings_kwargs())
43
-
44
-
45
- def _default_request_config_factory() -> RequestConfig:
46
- from ..transports import RequestConfig
47
-
48
- return RequestConfig()
49
-
50
-
51
- @dataclass
52
- class StatefulTestRunnerConfig:
53
- """Configuration for the stateful test runner."""
54
-
55
- # Checks to run against each response
56
- checks: tuple[CheckFunction, ...] = field(default_factory=_default_checks_factory)
57
- # Hypothesis settings for state machine execution
58
- hypothesis_settings: hypothesis.settings = field(default_factory=_default_hypothesis_settings_factory)
59
- # Request-level configuration
60
- request: RequestConfig = field(default_factory=_default_request_config_factory)
61
- # Whether to stop the execution after the first failure
62
- exit_first: bool = False
63
- max_failures: int | None = None
64
- # Custom headers sent with each request
65
- headers: dict[str, str] = field(default_factory=dict)
66
- auth: HTTPDigestAuth | RawAuth | None = None
67
- seed: int | None = None
68
- override: CaseOverride | None = None
69
- max_response_time: int | None = None
70
- dry_run: bool = False
71
- targets: list[Target] = field(default_factory=list)
72
- unique_data: bool = False
73
-
74
- def __post_init__(self) -> None:
75
- import hypothesis
76
-
77
- kwargs = _get_hypothesis_settings_kwargs_override(self.hypothesis_settings)
78
- if kwargs:
79
- self.hypothesis_settings = hypothesis.settings(self.hypothesis_settings, **kwargs)
80
-
81
-
82
- def _get_hypothesis_settings_kwargs_override(settings: hypothesis.settings) -> dict[str, Any]:
83
- """Get the settings that should be overridden to match the defaults for API state machines."""
84
- import hypothesis
85
-
86
- kwargs = {}
87
- hypothesis_default = hypothesis.settings()
88
- state_machine_default = _default_hypothesis_settings_factory()
89
- if settings.phases == hypothesis_default.phases:
90
- kwargs["phases"] = state_machine_default.phases
91
- if settings.stateful_step_count == hypothesis_default.stateful_step_count:
92
- kwargs["stateful_step_count"] = state_machine_default.stateful_step_count
93
- if settings.deadline in (hypothesis_default.deadline, timedelta(milliseconds=DEFAULT_DEADLINE)):
94
- kwargs["deadline"] = state_machine_default.deadline
95
- if settings.suppress_health_check == hypothesis_default.suppress_health_check:
96
- kwargs["suppress_health_check"] = state_machine_default.suppress_health_check
97
- return kwargs
@@ -1,135 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import traceback
4
- from dataclasses import dataclass, field
5
- from typing import TYPE_CHECKING, Tuple, Type, Union
6
-
7
- from ..constants import NOT_SET
8
- from ..exceptions import CheckFailed
9
- from ..targets import TargetMetricCollector
10
- from . import events
11
-
12
- if TYPE_CHECKING:
13
- from ..models import Case, Check
14
- from ..transports.responses import GenericResponse
15
- from ..types import NotSet
16
-
17
- FailureKey = Union[Type[CheckFailed], Tuple[str, int]]
18
-
19
-
20
- def _failure_cache_key(exc: CheckFailed | AssertionError) -> FailureKey:
21
- """Create a key to identify unique failures."""
22
- from hypothesis.internal.escalation import get_trimmed_traceback
23
-
24
- # For CheckFailed, we already have all distinctive information about the failure, which is contained
25
- # in the exception type itself.
26
- if isinstance(exc, CheckFailed):
27
- return exc.__class__
28
-
29
- # Assertion come from the user's code and we may try to group them by location
30
- tb = get_trimmed_traceback(exc)
31
- filename, lineno, *_ = traceback.extract_tb(tb)[-1]
32
- return (filename, lineno)
33
-
34
-
35
- @dataclass
36
- class RunnerContext:
37
- """Mutable context for state machine execution."""
38
-
39
- # All seen failure keys, both grouped and individual ones
40
- seen_in_run: set[FailureKey] = field(default_factory=set)
41
- # Failures keys seen in the current suite
42
- seen_in_suite: set[FailureKey] = field(default_factory=set)
43
- # Unique failures collected in the current suite
44
- failures_for_suite: list[Check] = field(default_factory=list)
45
- # All checks executed in the current run
46
- checks_for_step: list[Check] = field(default_factory=list)
47
- # Status of the current step
48
- current_step_status: events.StepStatus | None = None
49
- # The currently processed response
50
- current_response: GenericResponse | None = None
51
- # Total number of failures
52
- failures_count: int = 0
53
- # The total number of completed test scenario
54
- completed_scenarios: int = 0
55
- # Metrics collector for targeted testing
56
- metric_collector: TargetMetricCollector = field(default_factory=lambda: TargetMetricCollector(targets=[]))
57
- step_outcomes: dict[int, BaseException | None] = field(default_factory=dict)
58
-
59
- @property
60
- def current_scenario_status(self) -> events.ScenarioStatus:
61
- if self.current_step_status == events.StepStatus.SUCCESS:
62
- return events.ScenarioStatus.SUCCESS
63
- if self.current_step_status == events.StepStatus.FAILURE:
64
- return events.ScenarioStatus.FAILURE
65
- if self.current_step_status == events.StepStatus.ERROR:
66
- return events.ScenarioStatus.ERROR
67
- if self.current_step_status == events.StepStatus.INTERRUPTED:
68
- return events.ScenarioStatus.INTERRUPTED
69
- return events.ScenarioStatus.REJECTED
70
-
71
- def reset_scenario(self) -> None:
72
- self.completed_scenarios += 1
73
- self.current_step_status = None
74
- self.current_response = None
75
- self.step_outcomes.clear()
76
-
77
- def reset_step(self) -> None:
78
- self.checks_for_step = []
79
-
80
- def step_succeeded(self) -> None:
81
- self.current_step_status = events.StepStatus.SUCCESS
82
-
83
- def step_failed(self) -> None:
84
- self.current_step_status = events.StepStatus.FAILURE
85
-
86
- def step_errored(self) -> None:
87
- self.current_step_status = events.StepStatus.ERROR
88
-
89
- def step_interrupted(self) -> None:
90
- self.current_step_status = events.StepStatus.INTERRUPTED
91
-
92
- def mark_as_seen_in_run(self, exc: CheckFailed) -> None:
93
- key = _failure_cache_key(exc)
94
- self.seen_in_run.add(key)
95
- causes = exc.causes or ()
96
- for cause in causes:
97
- key = _failure_cache_key(cause)
98
- self.seen_in_run.add(key)
99
-
100
- def mark_as_seen_in_suite(self, exc: CheckFailed | AssertionError) -> None:
101
- key = _failure_cache_key(exc)
102
- self.seen_in_suite.add(key)
103
-
104
- def mark_current_suite_as_seen_in_run(self) -> None:
105
- self.seen_in_run.update(self.seen_in_suite)
106
-
107
- def is_seen_in_run(self, exc: CheckFailed | AssertionError) -> bool:
108
- key = _failure_cache_key(exc)
109
- return key in self.seen_in_run
110
-
111
- def is_seen_in_suite(self, exc: CheckFailed | AssertionError) -> bool:
112
- key = _failure_cache_key(exc)
113
- return key in self.seen_in_suite
114
-
115
- def add_failed_check(self, check: Check) -> None:
116
- self.failures_for_suite.append(check)
117
- self.failures_count += 1
118
-
119
- def collect_metric(self, case: Case, response: GenericResponse) -> None:
120
- self.metric_collector.store(case, response)
121
-
122
- def maximize_metrics(self) -> None:
123
- self.metric_collector.maximize()
124
-
125
- def reset(self) -> None:
126
- self.failures_for_suite = []
127
- self.seen_in_suite.clear()
128
- self.reset_scenario()
129
- self.metric_collector.reset()
130
-
131
- def store_step_outcome(self, case: Case, outcome: BaseException | None) -> None:
132
- self.step_outcomes[hash(case)] = outcome
133
-
134
- def get_step_outcome(self, case: Case) -> BaseException | None | NotSet:
135
- return self.step_outcomes.get(hash(case), NOT_SET)