schemathesis 3.39.7__py3-none-any.whl → 4.0.0a2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +26 -68
  3. schemathesis/checks.py +130 -60
  4. schemathesis/cli/__init__.py +5 -2105
  5. schemathesis/cli/commands/__init__.py +37 -0
  6. schemathesis/cli/commands/run/__init__.py +662 -0
  7. schemathesis/cli/commands/run/checks.py +80 -0
  8. schemathesis/cli/commands/run/context.py +117 -0
  9. schemathesis/cli/commands/run/events.py +30 -0
  10. schemathesis/cli/commands/run/executor.py +141 -0
  11. schemathesis/cli/commands/run/filters.py +202 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/{cassettes.py → commands/run/handlers/cassettes.py} +178 -247
  15. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1368 -0
  17. schemathesis/cli/commands/run/hypothesis.py +105 -0
  18. schemathesis/cli/commands/run/loaders.py +129 -0
  19. schemathesis/cli/{callbacks.py → commands/run/validation.py} +59 -175
  20. schemathesis/cli/constants.py +5 -58
  21. schemathesis/cli/core.py +17 -0
  22. schemathesis/cli/ext/fs.py +14 -0
  23. schemathesis/cli/ext/groups.py +55 -0
  24. schemathesis/cli/{options.py → ext/options.py} +37 -16
  25. schemathesis/cli/hooks.py +36 -0
  26. schemathesis/contrib/__init__.py +1 -3
  27. schemathesis/contrib/openapi/__init__.py +1 -3
  28. schemathesis/contrib/openapi/fill_missing_examples.py +3 -7
  29. schemathesis/core/__init__.py +58 -0
  30. schemathesis/core/compat.py +25 -0
  31. schemathesis/core/control.py +2 -0
  32. schemathesis/core/curl.py +58 -0
  33. schemathesis/core/deserialization.py +65 -0
  34. schemathesis/core/errors.py +370 -0
  35. schemathesis/core/failures.py +315 -0
  36. schemathesis/core/fs.py +19 -0
  37. schemathesis/core/loaders.py +104 -0
  38. schemathesis/core/marks.py +66 -0
  39. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  40. schemathesis/{internal/output.py → core/output/__init__.py} +1 -0
  41. schemathesis/core/output/sanitization.py +197 -0
  42. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  43. schemathesis/core/registries.py +31 -0
  44. schemathesis/core/transforms.py +113 -0
  45. schemathesis/core/transport.py +108 -0
  46. schemathesis/core/validation.py +38 -0
  47. schemathesis/core/version.py +7 -0
  48. schemathesis/engine/__init__.py +30 -0
  49. schemathesis/engine/config.py +59 -0
  50. schemathesis/engine/context.py +119 -0
  51. schemathesis/engine/control.py +36 -0
  52. schemathesis/engine/core.py +157 -0
  53. schemathesis/engine/errors.py +394 -0
  54. schemathesis/engine/events.py +243 -0
  55. schemathesis/engine/phases/__init__.py +66 -0
  56. schemathesis/{runner → engine/phases}/probes.py +49 -68
  57. schemathesis/engine/phases/stateful/__init__.py +66 -0
  58. schemathesis/engine/phases/stateful/_executor.py +301 -0
  59. schemathesis/engine/phases/stateful/context.py +85 -0
  60. schemathesis/engine/phases/unit/__init__.py +175 -0
  61. schemathesis/engine/phases/unit/_executor.py +322 -0
  62. schemathesis/engine/phases/unit/_pool.py +74 -0
  63. schemathesis/engine/recorder.py +246 -0
  64. schemathesis/errors.py +31 -0
  65. schemathesis/experimental/__init__.py +9 -40
  66. schemathesis/filters.py +7 -95
  67. schemathesis/generation/__init__.py +3 -3
  68. schemathesis/generation/case.py +190 -0
  69. schemathesis/generation/coverage.py +22 -22
  70. schemathesis/{_patches.py → generation/hypothesis/__init__.py} +15 -6
  71. schemathesis/generation/hypothesis/builder.py +585 -0
  72. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  73. schemathesis/generation/hypothesis/given.py +66 -0
  74. schemathesis/generation/hypothesis/reporting.py +14 -0
  75. schemathesis/generation/hypothesis/strategies.py +16 -0
  76. schemathesis/generation/meta.py +115 -0
  77. schemathesis/generation/modes.py +28 -0
  78. schemathesis/generation/overrides.py +96 -0
  79. schemathesis/generation/stateful/__init__.py +20 -0
  80. schemathesis/{stateful → generation/stateful}/state_machine.py +84 -109
  81. schemathesis/generation/targets.py +69 -0
  82. schemathesis/graphql/__init__.py +15 -0
  83. schemathesis/graphql/checks.py +109 -0
  84. schemathesis/graphql/loaders.py +131 -0
  85. schemathesis/hooks.py +17 -62
  86. schemathesis/openapi/__init__.py +13 -0
  87. schemathesis/openapi/checks.py +387 -0
  88. schemathesis/openapi/generation/__init__.py +0 -0
  89. schemathesis/openapi/generation/filters.py +63 -0
  90. schemathesis/openapi/loaders.py +178 -0
  91. schemathesis/pytest/__init__.py +5 -0
  92. schemathesis/pytest/control_flow.py +7 -0
  93. schemathesis/pytest/lazy.py +273 -0
  94. schemathesis/pytest/loaders.py +12 -0
  95. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +94 -107
  96. schemathesis/python/__init__.py +0 -0
  97. schemathesis/python/asgi.py +12 -0
  98. schemathesis/python/wsgi.py +12 -0
  99. schemathesis/schemas.py +456 -228
  100. schemathesis/specs/graphql/__init__.py +0 -1
  101. schemathesis/specs/graphql/_cache.py +1 -2
  102. schemathesis/specs/graphql/scalars.py +5 -3
  103. schemathesis/specs/graphql/schemas.py +122 -123
  104. schemathesis/specs/graphql/validation.py +11 -17
  105. schemathesis/specs/openapi/__init__.py +6 -1
  106. schemathesis/specs/openapi/_cache.py +1 -2
  107. schemathesis/specs/openapi/_hypothesis.py +97 -134
  108. schemathesis/specs/openapi/checks.py +238 -219
  109. schemathesis/specs/openapi/converter.py +4 -4
  110. schemathesis/specs/openapi/definitions.py +1 -1
  111. schemathesis/specs/openapi/examples.py +22 -20
  112. schemathesis/specs/openapi/expressions/__init__.py +11 -15
  113. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  114. schemathesis/specs/openapi/expressions/nodes.py +33 -32
  115. schemathesis/specs/openapi/formats.py +3 -2
  116. schemathesis/specs/openapi/links.py +123 -299
  117. schemathesis/specs/openapi/media_types.py +10 -12
  118. schemathesis/specs/openapi/negative/__init__.py +2 -1
  119. schemathesis/specs/openapi/negative/mutations.py +3 -2
  120. schemathesis/specs/openapi/parameters.py +8 -6
  121. schemathesis/specs/openapi/patterns.py +1 -1
  122. schemathesis/specs/openapi/references.py +11 -51
  123. schemathesis/specs/openapi/schemas.py +177 -191
  124. schemathesis/specs/openapi/security.py +1 -1
  125. schemathesis/specs/openapi/serialization.py +10 -6
  126. schemathesis/specs/openapi/stateful/__init__.py +97 -91
  127. schemathesis/transport/__init__.py +104 -0
  128. schemathesis/transport/asgi.py +26 -0
  129. schemathesis/transport/prepare.py +99 -0
  130. schemathesis/transport/requests.py +221 -0
  131. schemathesis/{_xml.py → transport/serialization.py} +69 -7
  132. schemathesis/transport/wsgi.py +165 -0
  133. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +18 -14
  134. schemathesis-4.0.0a2.dist-info/RECORD +151 -0
  135. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +1 -1
  136. schemathesis/_compat.py +0 -74
  137. schemathesis/_dependency_versions.py +0 -19
  138. schemathesis/_hypothesis.py +0 -559
  139. schemathesis/_override.py +0 -50
  140. schemathesis/_rate_limiter.py +0 -7
  141. schemathesis/cli/context.py +0 -75
  142. schemathesis/cli/debug.py +0 -27
  143. schemathesis/cli/handlers.py +0 -19
  144. schemathesis/cli/junitxml.py +0 -124
  145. schemathesis/cli/output/__init__.py +0 -1
  146. schemathesis/cli/output/default.py +0 -936
  147. schemathesis/cli/output/short.py +0 -59
  148. schemathesis/cli/reporting.py +0 -79
  149. schemathesis/cli/sanitization.py +0 -26
  150. schemathesis/code_samples.py +0 -151
  151. schemathesis/constants.py +0 -56
  152. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  153. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  154. schemathesis/contrib/unique_data.py +0 -41
  155. schemathesis/exceptions.py +0 -571
  156. schemathesis/extra/_aiohttp.py +0 -28
  157. schemathesis/extra/_flask.py +0 -13
  158. schemathesis/extra/_server.py +0 -18
  159. schemathesis/failures.py +0 -277
  160. schemathesis/fixups/__init__.py +0 -37
  161. schemathesis/fixups/fast_api.py +0 -41
  162. schemathesis/fixups/utf8_bom.py +0 -28
  163. schemathesis/generation/_methods.py +0 -44
  164. schemathesis/graphql.py +0 -3
  165. schemathesis/internal/__init__.py +0 -7
  166. schemathesis/internal/checks.py +0 -84
  167. schemathesis/internal/copy.py +0 -32
  168. schemathesis/internal/datetime.py +0 -5
  169. schemathesis/internal/deprecation.py +0 -38
  170. schemathesis/internal/diff.py +0 -15
  171. schemathesis/internal/extensions.py +0 -27
  172. schemathesis/internal/jsonschema.py +0 -36
  173. schemathesis/internal/transformation.py +0 -26
  174. schemathesis/internal/validation.py +0 -34
  175. schemathesis/lazy.py +0 -474
  176. schemathesis/loaders.py +0 -122
  177. schemathesis/models.py +0 -1341
  178. schemathesis/parameters.py +0 -90
  179. schemathesis/runner/__init__.py +0 -605
  180. schemathesis/runner/events.py +0 -389
  181. schemathesis/runner/impl/__init__.py +0 -3
  182. schemathesis/runner/impl/context.py +0 -104
  183. schemathesis/runner/impl/core.py +0 -1246
  184. schemathesis/runner/impl/solo.py +0 -80
  185. schemathesis/runner/impl/threadpool.py +0 -391
  186. schemathesis/runner/serialization.py +0 -544
  187. schemathesis/sanitization.py +0 -252
  188. schemathesis/serializers.py +0 -328
  189. schemathesis/service/__init__.py +0 -18
  190. schemathesis/service/auth.py +0 -11
  191. schemathesis/service/ci.py +0 -202
  192. schemathesis/service/client.py +0 -133
  193. schemathesis/service/constants.py +0 -38
  194. schemathesis/service/events.py +0 -61
  195. schemathesis/service/extensions.py +0 -224
  196. schemathesis/service/hosts.py +0 -111
  197. schemathesis/service/metadata.py +0 -71
  198. schemathesis/service/models.py +0 -258
  199. schemathesis/service/report.py +0 -255
  200. schemathesis/service/serialization.py +0 -173
  201. schemathesis/service/usage.py +0 -66
  202. schemathesis/specs/graphql/loaders.py +0 -364
  203. schemathesis/specs/openapi/expressions/context.py +0 -16
  204. schemathesis/specs/openapi/loaders.py +0 -708
  205. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  206. schemathesis/specs/openapi/stateful/types.py +0 -14
  207. schemathesis/specs/openapi/validation.py +0 -26
  208. schemathesis/stateful/__init__.py +0 -147
  209. schemathesis/stateful/config.py +0 -97
  210. schemathesis/stateful/context.py +0 -135
  211. schemathesis/stateful/events.py +0 -274
  212. schemathesis/stateful/runner.py +0 -309
  213. schemathesis/stateful/sink.py +0 -68
  214. schemathesis/stateful/statistic.py +0 -22
  215. schemathesis/stateful/validation.py +0 -100
  216. schemathesis/targets.py +0 -77
  217. schemathesis/transports/__init__.py +0 -359
  218. schemathesis/transports/asgi.py +0 -7
  219. schemathesis/transports/auth.py +0 -38
  220. schemathesis/transports/headers.py +0 -36
  221. schemathesis/transports/responses.py +0 -57
  222. schemathesis/types.py +0 -44
  223. schemathesis/utils.py +0 -164
  224. schemathesis-3.39.7.dist-info/RECORD +0 -160
  225. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  226. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  227. /schemathesis/{internal → core}/result.py +0 -0
  228. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
  229. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import reduce
4
+ from operator import or_
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from hypothesis import strategies as st
9
+
10
+
11
+ def combine(strategies: list[st.SearchStrategy] | tuple[st.SearchStrategy]) -> st.SearchStrategy:
12
+ """Combine a list of strategies into a single one.
13
+
14
+ If the input is `[a, b, c]`, then the result is equivalent to `a | b | c`.
15
+ """
16
+ return reduce(or_, strategies[1:], strategies[0])
@@ -0,0 +1,115 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+
6
+ from schemathesis.generation import GenerationMode
7
+
8
+
9
+ class TestPhase(str, Enum):
10
+ __test__ = False
11
+
12
+ EXPLICIT = "explicit"
13
+ COVERAGE = "coverage"
14
+ GENERATE = "generate"
15
+
16
+
17
+ class ComponentKind(str, Enum):
18
+ """Components that can be generated."""
19
+
20
+ QUERY = "query"
21
+ PATH_PARAMETERS = "path_parameters"
22
+ HEADERS = "headers"
23
+ COOKIES = "cookies"
24
+ BODY = "body"
25
+
26
+
27
+ @dataclass
28
+ class ComponentInfo:
29
+ """Information about how a specific component was generated."""
30
+
31
+ mode: GenerationMode
32
+
33
+ __slots__ = ("mode",)
34
+
35
+
36
+ @dataclass
37
+ class GeneratePhaseData:
38
+ """Metadata specific to generate phase."""
39
+
40
+
41
+ @dataclass
42
+ class ExplicitPhaseData:
43
+ """Metadata specific to explicit phase."""
44
+
45
+
46
+ @dataclass
47
+ class CoveragePhaseData:
48
+ """Metadata specific to coverage phase."""
49
+
50
+ description: str
51
+ location: str | None
52
+ parameter: str | None
53
+ parameter_location: str | None
54
+
55
+ __slots__ = ("description", "location", "parameter", "parameter_location")
56
+
57
+
58
+ @dataclass
59
+ class PhaseInfo:
60
+ """Phase-specific information."""
61
+
62
+ name: TestPhase
63
+ data: CoveragePhaseData | ExplicitPhaseData | GeneratePhaseData
64
+
65
+ __slots__ = ("name", "data")
66
+
67
+ @classmethod
68
+ def coverage(
69
+ cls,
70
+ description: str,
71
+ location: str | None = None,
72
+ parameter: str | None = None,
73
+ parameter_location: str | None = None,
74
+ ) -> PhaseInfo:
75
+ return cls(
76
+ name=TestPhase.COVERAGE,
77
+ data=CoveragePhaseData(
78
+ description=description, location=location, parameter=parameter, parameter_location=parameter_location
79
+ ),
80
+ )
81
+
82
+ @classmethod
83
+ def generate(cls) -> PhaseInfo:
84
+ return cls(name=TestPhase.GENERATE, data=GeneratePhaseData())
85
+
86
+
87
+ @dataclass
88
+ class GenerationInfo:
89
+ """Information about test case generation."""
90
+
91
+ time: float
92
+ mode: GenerationMode
93
+
94
+ __slots__ = ("time", "mode")
95
+
96
+
97
+ @dataclass
98
+ class CaseMetadata:
99
+ """Complete metadata for generated cases."""
100
+
101
+ generation: GenerationInfo
102
+ components: dict[ComponentKind, ComponentInfo]
103
+ phase: PhaseInfo
104
+
105
+ __slots__ = ("generation", "components", "phase")
106
+
107
+ def __init__(
108
+ self,
109
+ generation: GenerationInfo,
110
+ components: dict[ComponentKind, ComponentInfo],
111
+ phase: PhaseInfo,
112
+ ) -> None:
113
+ self.generation = generation
114
+ self.components = components
115
+ self.phase = phase
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class GenerationMode(str, Enum):
7
+ """Defines what data Schemathesis generates for tests."""
8
+
9
+ # Generate data, that fits the API schema
10
+ POSITIVE = "positive"
11
+ # Doesn't fit the API schema
12
+ NEGATIVE = "negative"
13
+
14
+ @classmethod
15
+ def default(cls) -> GenerationMode:
16
+ return cls.POSITIVE
17
+
18
+ @classmethod
19
+ def all(cls) -> list[GenerationMode]:
20
+ return list(GenerationMode)
21
+
22
+ @property
23
+ def is_positive(self) -> bool:
24
+ return self == GenerationMode.POSITIVE
25
+
26
+ @property
27
+ def is_negative(self) -> bool:
28
+ return self == GenerationMode.NEGATIVE
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, Any, Callable
6
+
7
+ from schemathesis.core.errors import IncorrectUsage
8
+ from schemathesis.core.marks import Mark
9
+ from schemathesis.core.transforms import diff
10
+ from schemathesis.generation.meta import ComponentKind
11
+
12
+ if TYPE_CHECKING:
13
+ from schemathesis.generation.case import Case
14
+ from schemathesis.schemas import APIOperation, ParameterSet
15
+
16
+
17
+ @dataclass
18
+ class Override:
19
+ """Overrides for various parts of a test case."""
20
+
21
+ query: dict[str, str]
22
+ headers: dict[str, str]
23
+ cookies: dict[str, str]
24
+ path_parameters: dict[str, str]
25
+
26
+ def for_operation(self, operation: APIOperation) -> dict[str, dict[str, str]]:
27
+ return {
28
+ "query": (_for_parameters(self.query, operation.query)),
29
+ "headers": (_for_parameters(self.headers, operation.headers)),
30
+ "cookies": (_for_parameters(self.cookies, operation.cookies)),
31
+ "path_parameters": (_for_parameters(self.path_parameters, operation.path_parameters)),
32
+ }
33
+
34
+ @classmethod
35
+ def from_components(cls, components: dict[ComponentKind, StoredValue], case: Case) -> Override:
36
+ return Override(
37
+ **{
38
+ kind.value: get_component_diff(stored=stored, current=getattr(case, kind.value))
39
+ for kind, stored in components.items()
40
+ }
41
+ )
42
+
43
+
44
+ def _for_parameters(overridden: dict[str, str], defined: ParameterSet) -> dict[str, str]:
45
+ output = {}
46
+ for param in defined:
47
+ if param.name in overridden:
48
+ output[param.name] = overridden[param.name]
49
+ return output
50
+
51
+
52
+ @dataclass
53
+ class StoredValue:
54
+ value: dict[str, Any] | None
55
+ is_generated: bool
56
+
57
+ __slots__ = ("value", "is_generated")
58
+
59
+
60
+ def store_original_state(value: dict[str, Any] | None) -> dict[str, Any] | None:
61
+ if isinstance(value, Mapping):
62
+ return value.copy()
63
+ return value
64
+
65
+
66
+ def get_component_diff(stored: StoredValue, current: dict[str, Any] | None) -> dict[str, Any]:
67
+ """Calculate difference between stored and current components."""
68
+ if not (current and stored.value):
69
+ return {}
70
+ if stored.is_generated:
71
+ return diff(stored.value, current)
72
+ return current
73
+
74
+
75
+ def store_components(case: Case) -> dict[ComponentKind, StoredValue]:
76
+ """Store original component states for a test case."""
77
+ return {
78
+ kind: StoredValue(
79
+ value=store_original_state(getattr(case, kind.value)),
80
+ is_generated=bool(case.meta and kind in case.meta.components),
81
+ )
82
+ for kind in [
83
+ ComponentKind.QUERY,
84
+ ComponentKind.HEADERS,
85
+ ComponentKind.COOKIES,
86
+ ComponentKind.PATH_PARAMETERS,
87
+ ]
88
+ }
89
+
90
+
91
+ OverrideMark = Mark[Override](attr_name="override")
92
+
93
+
94
+ def check_no_override_mark(test: Callable) -> None:
95
+ if OverrideMark.is_set(test):
96
+ raise IncorrectUsage(f"`{test.__name__}` has already been decorated with `override`.")
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ import hypothesis
7
+
8
+ from schemathesis.generation.stateful.state_machine import APIStateMachine
9
+
10
+
11
+ def run_state_machine_as_test(
12
+ state_machine_factory: type[APIStateMachine], *, settings: hypothesis.settings | None = None
13
+ ) -> None:
14
+ """Run a state machine as a test.
15
+
16
+ It automatically adds the `_min_steps` argument if ``Hypothesis`` is recent enough.
17
+ """
18
+ from hypothesis.stateful import run_state_machine_as_test as _run_state_machine_as_test
19
+
20
+ return _run_state_machine_as_test(state_machine_factory, settings=settings, _min_steps=2)
@@ -1,39 +1,86 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import re
4
- import time
5
4
  from dataclasses import dataclass
6
5
  from functools import lru_cache
7
6
  from typing import TYPE_CHECKING, Any, ClassVar
8
7
 
8
+ import hypothesis
9
9
  from hypothesis.errors import InvalidDefinition
10
10
  from hypothesis.stateful import RuleBasedStateMachine
11
11
 
12
- from .._dependency_versions import HYPOTHESIS_HAS_STATEFUL_NAMING_IMPROVEMENTS
13
- from ..constants import NO_LINKS_ERROR_MESSAGE, NOT_SET
14
- from ..exceptions import UsageError
15
- from ..internal.checks import CheckFunction
16
- from ..models import APIOperation, Case
17
- from .config import _default_hypothesis_settings_factory
18
- from .runner import StatefulTestRunner, StatefulTestRunnerConfig
19
- from .sink import StateMachineSink
12
+ from schemathesis.checks import CheckFunction
13
+ from schemathesis.core.errors import IncorrectUsage
14
+ from schemathesis.core.result import Result
15
+ from schemathesis.core.transport import Response
16
+ from schemathesis.generation.case import Case
20
17
 
21
18
  if TYPE_CHECKING:
22
19
  import hypothesis
23
20
  from requests.structures import CaseInsensitiveDict
24
21
 
25
- from ..schemas import BaseSchema
26
- from ..transports.responses import GenericResponse
27
- from .statistic import TransitionStats
22
+ from schemathesis.schemas import BaseSchema
23
+
24
+
25
+ NO_LINKS_ERROR_MESSAGE = (
26
+ "Stateful testing requires at least one OpenAPI link in the schema, but no links detected. "
27
+ "Please add OpenAPI links to enable stateful testing or use stateless tests instead. \n"
28
+ "See https://schemathesis.readthedocs.io/en/stable/stateful.html#how-to-specify-connections for more information."
29
+ )
30
+
31
+ DEFAULT_STATE_MACHINE_SETTINGS = hypothesis.settings(
32
+ phases=[hypothesis.Phase.generate],
33
+ deadline=None,
34
+ stateful_step_count=6,
35
+ suppress_health_check=list(hypothesis.HealthCheck),
36
+ )
37
+
38
+
39
+ @dataclass
40
+ class StepInput:
41
+ """Input for a single state machine step."""
42
+
43
+ case: Case
44
+ transition: Transition | None # None for initial steps
45
+
46
+ __slots__ = ("case", "transition")
47
+
48
+ @classmethod
49
+ def initial(cls, case: Case) -> StepInput:
50
+ return cls(case=case, transition=None)
51
+
52
+
53
+ @dataclass
54
+ class Transition:
55
+ """Data about transition execution."""
56
+
57
+ # ID of the transition (e.g. link name)
58
+ id: str
59
+ parent_id: str
60
+ parameters: dict[str, dict[str, ExtractedParam]]
61
+ request_body: ExtractedParam | None
62
+
63
+ __slots__ = ("id", "parent_id", "parameters", "request_body")
64
+
65
+
66
+ @dataclass
67
+ class ExtractedParam:
68
+ """Result of parameter extraction."""
69
+
70
+ definition: Any
71
+ value: Result[Any, Exception]
72
+
73
+ __slots__ = ("definition", "value")
28
74
 
29
75
 
30
76
  @dataclass
31
- class StepResult:
77
+ class StepOutput:
32
78
  """Output from a single transition of a state machine."""
33
79
 
34
- response: GenericResponse
80
+ response: Response
35
81
  case: Case
36
- elapsed: float
82
+
83
+ __slots__ = ("response", "case")
37
84
 
38
85
 
39
86
  def _normalize_name(name: str) -> str:
@@ -51,25 +98,23 @@ class APIStateMachine(RuleBasedStateMachine):
51
98
  # attribute will be renamed in the future
52
99
  bundles: ClassVar[dict[str, CaseInsensitiveDict]] # type: ignore
53
100
  schema: BaseSchema
54
- # A template for transition statistics that can be filled with data from the state machine during its execution
55
- _transition_stats_template: ClassVar[TransitionStats]
56
101
 
57
102
  def __init__(self) -> None:
58
103
  try:
59
104
  super().__init__() # type: ignore
60
105
  except InvalidDefinition as exc:
61
106
  if "defines no rules" in str(exc):
62
- raise UsageError(NO_LINKS_ERROR_MESSAGE) from None
107
+ raise IncorrectUsage(NO_LINKS_ERROR_MESSAGE) from None
63
108
  raise
64
109
  self.setup()
65
110
 
66
111
  @classmethod
67
112
  @lru_cache
68
113
  def _to_test_case(cls) -> type:
69
- from . import run_state_machine_as_test
114
+ from schemathesis.generation.stateful import run_state_machine_as_test
70
115
 
71
116
  class StateMachineTestCase(RuleBasedStateMachine.TestCase):
72
- settings = _default_hypothesis_settings_factory()
117
+ settings = DEFAULT_STATE_MACHINE_SETTINGS
73
118
 
74
119
  def runTest(self) -> None:
75
120
  run_state_machine_as_test(cls, settings=self.settings)
@@ -80,33 +125,20 @@ class APIStateMachine(RuleBasedStateMachine):
80
125
  StateMachineTestCase.__qualname__ = cls.__qualname__ + ".TestCase"
81
126
  return StateMachineTestCase
82
127
 
83
- def _pretty_print(self, value: Any) -> str:
84
- if isinstance(value, Case):
85
- # State machines suppose to be reproducible, hence it is OK to get kwargs here
86
- kwargs = self.get_call_kwargs(value)
87
- return _print_case(value, kwargs)
88
- return super()._pretty_print(value) # type: ignore
89
-
90
- if HYPOTHESIS_HAS_STATEFUL_NAMING_IMPROVEMENTS:
128
+ def _new_name(self, target: str) -> str:
129
+ target = _normalize_name(target)
130
+ return super()._new_name(target) # type: ignore
91
131
 
92
- def _new_name(self, target: str) -> str:
93
- target = _normalize_name(target)
94
- return super()._new_name(target) # type: ignore
95
-
96
- def _get_target_for_result(self, result: StepResult) -> str | None:
132
+ def _get_target_for_result(self, result: StepOutput) -> str | None:
97
133
  raise NotImplementedError
98
134
 
99
- def _add_result_to_targets(self, targets: tuple[str, ...], result: StepResult | None) -> None:
135
+ def _add_result_to_targets(self, targets: tuple[str, ...], result: StepOutput | None) -> None:
100
136
  if result is None:
101
137
  return
102
138
  target = self._get_target_for_result(result)
103
139
  if target is not None:
104
140
  super()._add_result_to_targets((target,), result)
105
141
 
106
- @classmethod
107
- def format_rules(cls) -> str:
108
- raise NotImplementedError
109
-
110
142
  @classmethod
111
143
  def run(cls, *, settings: hypothesis.settings | None = None) -> None:
112
144
  """Run state machine as a test."""
@@ -114,23 +146,8 @@ class APIStateMachine(RuleBasedStateMachine):
114
146
 
115
147
  return run_state_machine_as_test(cls, settings=settings)
116
148
 
117
- @classmethod
118
- def runner(cls, *, config: StatefulTestRunnerConfig | None = None) -> StatefulTestRunner:
119
- """Create a runner for this state machine."""
120
- from .runner import StatefulTestRunnerConfig
121
-
122
- return StatefulTestRunner(cls, config=config or StatefulTestRunnerConfig())
123
-
124
- @classmethod
125
- def sink(cls) -> StateMachineSink:
126
- """Create a sink to collect events into."""
127
- return StateMachineSink(transitions=cls._transition_stats_template.copy())
128
-
129
149
  def setup(self) -> None:
130
- """Hook method that runs unconditionally in the beginning of each test scenario.
131
-
132
- Does nothing by default.
133
- """
150
+ """Hook method that runs unconditionally in the beginning of each test scenario."""
134
151
 
135
152
  def teardown(self) -> None:
136
153
  pass
@@ -138,19 +155,11 @@ class APIStateMachine(RuleBasedStateMachine):
138
155
  # To provide the return type in the rendered documentation
139
156
  teardown.__doc__ = RuleBasedStateMachine.teardown.__doc__
140
157
 
141
- def transform(self, result: StepResult, direction: Direction, case: Case) -> Case:
142
- raise NotImplementedError
143
-
144
- def _step(self, case: Case, previous: StepResult | None = None, link: Direction | None = None) -> StepResult | None:
145
- # This method is a proxy that is used under the hood during the state machine initialization.
146
- # The whole point of having it is to make it possible to override `step`; otherwise, custom "step" is ignored.
147
- # It happens because, at the point of initialization, the final class is not yet created.
158
+ def _step(self, input: StepInput) -> StepOutput | None:
148
159
  __tracebackhide__ = True
149
- if previous is not None and link is not None:
150
- return self.step(case, (previous, link))
151
- return self.step(case, None)
160
+ return self.step(input)
152
161
 
153
- def step(self, case: Case, previous: tuple[StepResult, Direction] | None = None) -> StepResult | None:
162
+ def step(self, input: StepInput) -> StepOutput:
154
163
  """A single state machine step.
155
164
 
156
165
  :param Case case: Generated test case data that should be sent in an API call to the tested API operation.
@@ -159,20 +168,13 @@ class APIStateMachine(RuleBasedStateMachine):
159
168
  Schemathesis prepares data, makes a call and validates the received response.
160
169
  It is the most high-level point to extend the testing process. You probably don't need it in most cases.
161
170
  """
162
- from ..specs.openapi.checks import use_after_free
163
-
164
171
  __tracebackhide__ = True
165
- if previous is not None:
166
- result, direction = previous
167
- case = self.transform(result, direction, case)
168
- self.before_call(case)
169
- kwargs = self.get_call_kwargs(case)
170
- start = time.monotonic()
171
- response = self.call(case, **kwargs)
172
- elapsed = time.monotonic() - start
173
- self.after_call(response, case)
174
- self.validate_response(response, case, additional_checks=(use_after_free,))
175
- return self.store_result(response, case, elapsed)
172
+ self.before_call(input.case)
173
+ kwargs = self.get_call_kwargs(input.case)
174
+ response = self.call(input.case, **kwargs)
175
+ self.after_call(response, input.case)
176
+ self.validate_response(response, input.case)
177
+ return StepOutput(response, input.case)
176
178
 
177
179
  def before_call(self, case: Case) -> None:
178
180
  """Hook method for modifying the case data before making a request.
@@ -199,7 +201,7 @@ class APIStateMachine(RuleBasedStateMachine):
199
201
  case.body["is_fake"] = True
200
202
  """
201
203
 
202
- def after_call(self, response: GenericResponse, case: Case) -> None:
204
+ def after_call(self, response: Response, case: Case) -> None:
203
205
  """Hook method for additional actions with case or response instances.
204
206
 
205
207
  :param response: Response from the application under test.
@@ -232,7 +234,7 @@ class APIStateMachine(RuleBasedStateMachine):
232
234
  # PATCH /users/{user_id} -> 500
233
235
  """
234
236
 
235
- def call(self, case: Case, **kwargs: Any) -> GenericResponse:
237
+ def call(self, case: Case, **kwargs: Any) -> Response:
236
238
  """Make a request to the API.
237
239
 
238
240
  :param Case case: Generated test case data that should be sent in an API call to the tested API operation.
@@ -265,14 +267,14 @@ class APIStateMachine(RuleBasedStateMachine):
265
267
  return {}
266
268
 
267
269
  def validate_response(
268
- self, response: GenericResponse, case: Case, additional_checks: tuple[CheckFunction, ...] = ()
270
+ self, response: Response, case: Case, additional_checks: list[CheckFunction] | None = None
269
271
  ) -> None:
270
272
  """Validate an API response.
271
273
 
272
274
  :param response: Response from the application under test.
273
275
  :param Case case: Generated test case data that should be sent in an API call to the tested API operation.
274
276
  :param additional_checks: A list of checks that will be run together with the default ones.
275
- :raises CheckFailed: If any of the supplied checks failed.
277
+ :raises FailureGroup: If any of the supplied checks failed.
276
278
 
277
279
  If you need to change the default checks or provide custom validation rules, you can do it here.
278
280
 
@@ -298,30 +300,3 @@ class APIStateMachine(RuleBasedStateMachine):
298
300
  """
299
301
  __tracebackhide__ = True
300
302
  case.validate_response(response, additional_checks=additional_checks)
301
-
302
- def store_result(self, response: GenericResponse, case: Case, elapsed: float) -> StepResult:
303
- return StepResult(response, case, elapsed)
304
-
305
-
306
- def _print_case(case: Case, kwargs: dict[str, Any]) -> str:
307
- from requests.structures import CaseInsensitiveDict
308
-
309
- operation = f"state.schema['{case.operation.path}']['{case.operation.method.upper()}']"
310
- headers = case.headers or CaseInsensitiveDict()
311
- headers.update(kwargs.get("headers", {}))
312
- case.headers = headers
313
- data = [
314
- f"{name}={getattr(case, name)!r}"
315
- for name in ("path_parameters", "headers", "cookies", "query", "body", "media_type")
316
- if getattr(case, name) not in (None, NOT_SET)
317
- ]
318
- return f"{operation}.make_case({', '.join(data)})"
319
-
320
-
321
- class Direction:
322
- name: str
323
- status_code: str
324
- operation: APIOperation
325
-
326
- def set_data(self, case: Case, elapsed: float, **kwargs: Any) -> None:
327
- raise NotImplementedError
@@ -0,0 +1,69 @@
1
+ """Support for Targeted Property-Based Testing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Callable, Sequence
7
+
8
+ from schemathesis.core.registries import Registry
9
+ from schemathesis.core.transport import Response
10
+ from schemathesis.generation.case import Case
11
+
12
+
13
+ @dataclass
14
+ class TargetContext:
15
+ case: Case
16
+ response: Response
17
+
18
+ __slots__ = ("case", "response")
19
+
20
+
21
+ TargetFunction = Callable[[TargetContext], float]
22
+
23
+ TARGETS = Registry[TargetFunction]()
24
+ target = TARGETS.register
25
+
26
+
27
+ @target
28
+ def response_time(ctx: TargetContext) -> float:
29
+ """Response time as a metric to maximize."""
30
+ return ctx.response.elapsed
31
+
32
+
33
+ class TargetMetricCollector:
34
+ """Collect multiple observations for target metrics."""
35
+
36
+ __slots__ = ("targets", "observations")
37
+
38
+ def __init__(self, targets: list[TargetFunction] | None = None) -> None:
39
+ self.targets = targets or []
40
+ self.observations: dict[str, list[float]] = {target.__name__: [] for target in self.targets}
41
+
42
+ def reset(self) -> None:
43
+ """Reset all collected observations."""
44
+ for target in self.targets:
45
+ self.observations[target.__name__].clear()
46
+
47
+ def store(self, case: Case, response: Response) -> None:
48
+ """Calculate target metrics & store them."""
49
+ context = TargetContext(case=case, response=response)
50
+ for target in self.targets:
51
+ self.observations[target.__name__].append(target(context))
52
+
53
+ def maximize(self) -> None:
54
+ """Give feedback to the Hypothesis engine, so it maximizes the aggregated metrics."""
55
+ import hypothesis
56
+
57
+ for target in self.targets:
58
+ # Currently aggregation is just a sum
59
+ metric = sum(self.observations[target.__name__])
60
+ hypothesis.target(metric, label=target.__name__)
61
+
62
+
63
+ def run(targets: Sequence[TargetFunction], case: Case, response: Response) -> None:
64
+ import hypothesis
65
+
66
+ context = TargetContext(case=case, response=response)
67
+ for target in targets:
68
+ value = target(context)
69
+ hypothesis.target(value, label=target.__name__)
@@ -0,0 +1,15 @@
1
+ from schemathesis.graphql.loaders import from_asgi, from_dict, from_file, from_path, from_url, from_wsgi
2
+
3
+ from ..specs.graphql import nodes
4
+ from ..specs.graphql.scalars import scalar
5
+
6
+ __all__ = [
7
+ "from_url",
8
+ "from_asgi",
9
+ "from_wsgi",
10
+ "from_file",
11
+ "from_path",
12
+ "from_dict",
13
+ "nodes",
14
+ "scalar",
15
+ ]