schemathesis 3.25.6__py3-none-any.whl → 3.39.7__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 (146) hide show
  1. schemathesis/__init__.py +6 -6
  2. schemathesis/_compat.py +2 -2
  3. schemathesis/_dependency_versions.py +4 -2
  4. schemathesis/_hypothesis.py +369 -56
  5. schemathesis/_lazy_import.py +1 -0
  6. schemathesis/_override.py +5 -4
  7. schemathesis/_patches.py +21 -0
  8. schemathesis/_rate_limiter.py +7 -0
  9. schemathesis/_xml.py +75 -22
  10. schemathesis/auths.py +78 -16
  11. schemathesis/checks.py +21 -9
  12. schemathesis/cli/__init__.py +783 -432
  13. schemathesis/cli/__main__.py +4 -0
  14. schemathesis/cli/callbacks.py +58 -13
  15. schemathesis/cli/cassettes.py +233 -47
  16. schemathesis/cli/constants.py +8 -2
  17. schemathesis/cli/context.py +22 -5
  18. schemathesis/cli/debug.py +2 -1
  19. schemathesis/cli/handlers.py +4 -1
  20. schemathesis/cli/junitxml.py +103 -22
  21. schemathesis/cli/options.py +15 -4
  22. schemathesis/cli/output/default.py +258 -112
  23. schemathesis/cli/output/short.py +23 -8
  24. schemathesis/cli/reporting.py +79 -0
  25. schemathesis/cli/sanitization.py +6 -0
  26. schemathesis/code_samples.py +5 -3
  27. schemathesis/constants.py +1 -0
  28. schemathesis/contrib/openapi/__init__.py +1 -1
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
  30. schemathesis/contrib/openapi/formats/uuid.py +2 -1
  31. schemathesis/contrib/unique_data.py +3 -3
  32. schemathesis/exceptions.py +76 -65
  33. schemathesis/experimental/__init__.py +35 -0
  34. schemathesis/extra/_aiohttp.py +1 -0
  35. schemathesis/extra/_flask.py +4 -1
  36. schemathesis/extra/_server.py +1 -0
  37. schemathesis/extra/pytest_plugin.py +17 -25
  38. schemathesis/failures.py +77 -9
  39. schemathesis/filters.py +185 -8
  40. schemathesis/fixups/__init__.py +1 -0
  41. schemathesis/fixups/fast_api.py +2 -2
  42. schemathesis/fixups/utf8_bom.py +1 -2
  43. schemathesis/generation/__init__.py +20 -36
  44. schemathesis/generation/_hypothesis.py +59 -0
  45. schemathesis/generation/_methods.py +44 -0
  46. schemathesis/generation/coverage.py +931 -0
  47. schemathesis/graphql.py +0 -1
  48. schemathesis/hooks.py +89 -12
  49. schemathesis/internal/checks.py +84 -0
  50. schemathesis/internal/copy.py +22 -3
  51. schemathesis/internal/deprecation.py +6 -2
  52. schemathesis/internal/diff.py +15 -0
  53. schemathesis/internal/extensions.py +27 -0
  54. schemathesis/internal/jsonschema.py +2 -1
  55. schemathesis/internal/output.py +68 -0
  56. schemathesis/internal/result.py +1 -1
  57. schemathesis/internal/transformation.py +11 -0
  58. schemathesis/lazy.py +138 -25
  59. schemathesis/loaders.py +7 -5
  60. schemathesis/models.py +318 -211
  61. schemathesis/parameters.py +4 -0
  62. schemathesis/runner/__init__.py +50 -15
  63. schemathesis/runner/events.py +65 -5
  64. schemathesis/runner/impl/context.py +104 -0
  65. schemathesis/runner/impl/core.py +388 -177
  66. schemathesis/runner/impl/solo.py +19 -29
  67. schemathesis/runner/impl/threadpool.py +70 -79
  68. schemathesis/runner/probes.py +11 -9
  69. schemathesis/runner/serialization.py +150 -17
  70. schemathesis/sanitization.py +5 -1
  71. schemathesis/schemas.py +170 -102
  72. schemathesis/serializers.py +7 -2
  73. schemathesis/service/ci.py +1 -0
  74. schemathesis/service/client.py +39 -6
  75. schemathesis/service/events.py +5 -1
  76. schemathesis/service/extensions.py +224 -0
  77. schemathesis/service/hosts.py +6 -2
  78. schemathesis/service/metadata.py +25 -0
  79. schemathesis/service/models.py +211 -2
  80. schemathesis/service/report.py +6 -6
  81. schemathesis/service/serialization.py +45 -71
  82. schemathesis/service/usage.py +1 -0
  83. schemathesis/specs/graphql/_cache.py +26 -0
  84. schemathesis/specs/graphql/loaders.py +25 -5
  85. schemathesis/specs/graphql/nodes.py +1 -0
  86. schemathesis/specs/graphql/scalars.py +2 -2
  87. schemathesis/specs/graphql/schemas.py +130 -100
  88. schemathesis/specs/graphql/validation.py +1 -2
  89. schemathesis/specs/openapi/__init__.py +1 -0
  90. schemathesis/specs/openapi/_cache.py +123 -0
  91. schemathesis/specs/openapi/_hypothesis.py +78 -60
  92. schemathesis/specs/openapi/checks.py +504 -25
  93. schemathesis/specs/openapi/converter.py +31 -4
  94. schemathesis/specs/openapi/definitions.py +10 -17
  95. schemathesis/specs/openapi/examples.py +126 -12
  96. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  97. schemathesis/specs/openapi/expressions/context.py +1 -1
  98. schemathesis/specs/openapi/expressions/extractors.py +26 -0
  99. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  100. schemathesis/specs/openapi/expressions/nodes.py +29 -6
  101. schemathesis/specs/openapi/expressions/parser.py +26 -5
  102. schemathesis/specs/openapi/formats.py +44 -0
  103. schemathesis/specs/openapi/links.py +125 -42
  104. schemathesis/specs/openapi/loaders.py +77 -36
  105. schemathesis/specs/openapi/media_types.py +34 -0
  106. schemathesis/specs/openapi/negative/__init__.py +6 -3
  107. schemathesis/specs/openapi/negative/mutations.py +21 -6
  108. schemathesis/specs/openapi/parameters.py +39 -25
  109. schemathesis/specs/openapi/patterns.py +137 -0
  110. schemathesis/specs/openapi/references.py +37 -7
  111. schemathesis/specs/openapi/schemas.py +360 -241
  112. schemathesis/specs/openapi/security.py +25 -7
  113. schemathesis/specs/openapi/serialization.py +1 -0
  114. schemathesis/specs/openapi/stateful/__init__.py +198 -70
  115. schemathesis/specs/openapi/stateful/statistic.py +198 -0
  116. schemathesis/specs/openapi/stateful/types.py +14 -0
  117. schemathesis/specs/openapi/utils.py +6 -1
  118. schemathesis/specs/openapi/validation.py +1 -0
  119. schemathesis/stateful/__init__.py +35 -21
  120. schemathesis/stateful/config.py +97 -0
  121. schemathesis/stateful/context.py +135 -0
  122. schemathesis/stateful/events.py +274 -0
  123. schemathesis/stateful/runner.py +309 -0
  124. schemathesis/stateful/sink.py +68 -0
  125. schemathesis/stateful/state_machine.py +67 -38
  126. schemathesis/stateful/statistic.py +22 -0
  127. schemathesis/stateful/validation.py +100 -0
  128. schemathesis/targets.py +33 -1
  129. schemathesis/throttling.py +25 -5
  130. schemathesis/transports/__init__.py +354 -0
  131. schemathesis/transports/asgi.py +7 -0
  132. schemathesis/transports/auth.py +25 -2
  133. schemathesis/transports/content_types.py +3 -1
  134. schemathesis/transports/headers.py +2 -1
  135. schemathesis/transports/responses.py +9 -4
  136. schemathesis/types.py +9 -0
  137. schemathesis/utils.py +11 -16
  138. schemathesis-3.39.7.dist-info/METADATA +293 -0
  139. schemathesis-3.39.7.dist-info/RECORD +160 -0
  140. {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
  141. schemathesis/specs/openapi/filters.py +0 -49
  142. schemathesis/specs/openapi/stateful/links.py +0 -92
  143. schemathesis-3.25.6.dist-info/METADATA +0 -356
  144. schemathesis-3.25.6.dist-info/RECORD +0 -134
  145. {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
  146. {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
@@ -1,21 +1,27 @@
1
1
  from __future__ import annotations
2
+
2
3
  import enum
3
4
  import json
4
5
  from dataclasses import dataclass, field
5
6
  from typing import TYPE_CHECKING, Any, Callable, Generator
6
7
 
7
- from .. import GenerationConfig
8
- from ..exceptions import OperationSchemaError
9
- from ..models import APIOperation, Case
10
8
  from ..constants import NOT_SET
11
9
  from ..internal.result import Ok, Result
12
10
 
13
11
  if TYPE_CHECKING:
14
12
  import hypothesis
13
+
14
+ from .. import GenerationConfig
15
+ from ..exceptions import OperationSchemaError
16
+ from ..models import APIOperation, Case
15
17
  from ..transports.responses import GenericResponse
16
18
  from .state_machine import APIStateMachine
17
19
 
18
20
 
21
+ class UnresolvableLink(Exception):
22
+ """Raised when a link cannot be resolved."""
23
+
24
+
19
25
  @enum.unique
20
26
  class Stateful(enum.Enum):
21
27
  none = 1
@@ -54,6 +60,9 @@ class StatefulTest:
54
60
  def parse(self, case: Case, response: GenericResponse) -> ParsedData:
55
61
  raise NotImplementedError
56
62
 
63
+ def is_match(self) -> bool:
64
+ raise NotImplementedError
65
+
57
66
  def make_operation(self, collected: list[ParsedData]) -> APIOperation:
58
67
  raise NotImplementedError
59
68
 
@@ -70,8 +79,12 @@ class StatefulData:
70
79
 
71
80
  def store(self, case: Case, response: GenericResponse) -> None:
72
81
  """Parse and store data for a stateful test."""
73
- parsed = self.stateful_test.parse(case, response)
74
- self.container.append(parsed)
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
75
88
 
76
89
 
77
90
  @dataclass
@@ -103,22 +116,23 @@ class Feedback:
103
116
  from .._hypothesis import create_test
104
117
 
105
118
  for data in self.stateful_tests.values():
106
- operation = data.make_operation()
107
- _as_strategy_kwargs: dict[str, Any] | None
108
- if callable(as_strategy_kwargs):
109
- _as_strategy_kwargs = as_strategy_kwargs(operation)
110
- else:
111
- _as_strategy_kwargs = as_strategy_kwargs
112
- test_function = create_test(
113
- operation=operation,
114
- test=test,
115
- settings=settings,
116
- seed=seed,
117
- data_generation_methods=operation.schema.data_generation_methods,
118
- generation_config=generation_config,
119
- as_strategy_kwargs=_as_strategy_kwargs,
120
- )
121
- yield Ok((operation, test_function))
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))
122
136
 
123
137
 
124
138
  def run_state_machine_as_test(
@@ -0,0 +1,97 @@
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
@@ -0,0 +1,135 @@
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)
@@ -0,0 +1,274 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from dataclasses import asdict as _asdict
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from ..exceptions import format_exception
10
+
11
+ if TYPE_CHECKING:
12
+ from ..models import Case, Check
13
+ from ..transports.responses import GenericResponse
14
+ from .state_machine import APIStateMachine
15
+
16
+
17
+ class RunStatus(str, Enum):
18
+ """Status of the state machine run."""
19
+
20
+ SUCCESS = "success"
21
+ FAILURE = "failure"
22
+ ERROR = "error"
23
+ INTERRUPTED = "interrupted"
24
+
25
+
26
+ @dataclass
27
+ class StatefulEvent:
28
+ """Basic stateful test event."""
29
+
30
+ timestamp: float
31
+
32
+ __slots__ = ("timestamp",)
33
+
34
+ def asdict(self) -> dict[str, Any]:
35
+ return _asdict(self)
36
+
37
+
38
+ @dataclass
39
+ class RunStarted(StatefulEvent):
40
+ """Before executing all scenarios."""
41
+
42
+ started_at: float
43
+ state_machine: type[APIStateMachine]
44
+
45
+ __slots__ = ("state_machine", "timestamp", "started_at")
46
+
47
+ def __init__(self, *, state_machine: type[APIStateMachine]) -> None:
48
+ self.state_machine = state_machine
49
+ self.started_at = time.time()
50
+ self.timestamp = time.monotonic()
51
+
52
+ def asdict(self) -> dict[str, Any]:
53
+ return {
54
+ "timestamp": self.timestamp,
55
+ "started_at": self.started_at,
56
+ }
57
+
58
+
59
+ @dataclass
60
+ class RunFinished(StatefulEvent):
61
+ """After executing all scenarios."""
62
+
63
+ status: RunStatus
64
+
65
+ __slots__ = ("timestamp", "status")
66
+
67
+ def __init__(self, *, status: RunStatus) -> None:
68
+ self.status = status
69
+ self.timestamp = time.monotonic()
70
+
71
+
72
+ class SuiteStatus(str, Enum):
73
+ """Status of the suite execution."""
74
+
75
+ SUCCESS = "success"
76
+ FAILURE = "failure"
77
+ ERROR = "error"
78
+ INTERRUPTED = "interrupted"
79
+
80
+
81
+ @dataclass
82
+ class SuiteStarted(StatefulEvent):
83
+ """Before executing a set of scenarios."""
84
+
85
+ __slots__ = ("timestamp",)
86
+
87
+ def __init__(self) -> None:
88
+ self.timestamp = time.monotonic()
89
+
90
+
91
+ @dataclass
92
+ class SuiteFinished(StatefulEvent):
93
+ """After executing a set of scenarios."""
94
+
95
+ status: SuiteStatus
96
+ failures: list[Check]
97
+
98
+ __slots__ = ("timestamp", "status", "failures")
99
+
100
+ def __init__(self, *, status: SuiteStatus, failures: list[Check]) -> None:
101
+ self.status = status
102
+ self.failures = failures
103
+ self.timestamp = time.monotonic()
104
+
105
+ def asdict(self) -> dict[str, Any]:
106
+ from ..runner.serialization import SerializedCheck, _serialize_check
107
+
108
+ return {
109
+ "timestamp": self.timestamp,
110
+ "status": self.status,
111
+ "failures": [_serialize_check(SerializedCheck.from_check(failure)) for failure in self.failures],
112
+ }
113
+
114
+
115
+ class ScenarioStatus(str, Enum):
116
+ """Status of a single scenario execution."""
117
+
118
+ SUCCESS = "success"
119
+ FAILURE = "failure"
120
+ ERROR = "error"
121
+ # Rejected by Hypothesis
122
+ REJECTED = "rejected"
123
+ INTERRUPTED = "interrupted"
124
+
125
+
126
+ @dataclass
127
+ class ScenarioStarted(StatefulEvent):
128
+ """Before a single state machine execution."""
129
+
130
+ # Whether this is a scenario that tries to reproduce a failure
131
+ is_final: bool
132
+
133
+ __slots__ = ("timestamp", "is_final")
134
+
135
+ def __init__(self, *, is_final: bool) -> None:
136
+ self.is_final = is_final
137
+ self.timestamp = time.monotonic()
138
+
139
+
140
+ @dataclass
141
+ class ScenarioFinished(StatefulEvent):
142
+ """After a single state machine execution."""
143
+
144
+ status: ScenarioStatus
145
+ # Whether this is a scenario that tries to reproduce a failure
146
+ is_final: bool
147
+
148
+ __slots__ = ("timestamp", "status", "is_final")
149
+
150
+ def __init__(self, *, status: ScenarioStatus, is_final: bool) -> None:
151
+ self.status = status
152
+ self.is_final = is_final
153
+ self.timestamp = time.monotonic()
154
+
155
+
156
+ class StepStatus(str, Enum):
157
+ """Status of a single state machine step."""
158
+
159
+ SUCCESS = "success"
160
+ FAILURE = "failure"
161
+ ERROR = "error"
162
+ INTERRUPTED = "interrupted"
163
+
164
+
165
+ @dataclass
166
+ class StepStarted(StatefulEvent):
167
+ """Before a single state machine step."""
168
+
169
+ __slots__ = ("timestamp",)
170
+
171
+ def __init__(self) -> None:
172
+ self.timestamp = time.monotonic()
173
+
174
+
175
+ @dataclass
176
+ class TransitionId:
177
+ """Id of the the that was hit."""
178
+
179
+ name: str
180
+ # Status code as defined in the transition, i.e. may be `default`
181
+ status_code: str
182
+ source: str
183
+
184
+ __slots__ = ("name", "status_code", "source")
185
+
186
+
187
+ @dataclass
188
+ class ResponseData:
189
+ """Common data for responses."""
190
+
191
+ status_code: int
192
+ elapsed: float
193
+ __slots__ = ("status_code", "elapsed")
194
+
195
+
196
+ @dataclass
197
+ class StepFinished(StatefulEvent):
198
+ """After a single state machine step."""
199
+
200
+ status: StepStatus | None
201
+ transition_id: TransitionId | None
202
+ target: str
203
+ case: Case
204
+ response: GenericResponse | None
205
+ checks: list[Check]
206
+
207
+ __slots__ = ("timestamp", "status", "transition_id", "target", "case", "response", "checks")
208
+
209
+ def __init__(
210
+ self,
211
+ *,
212
+ status: StepStatus | None,
213
+ transition_id: TransitionId | None,
214
+ target: str,
215
+ case: Case,
216
+ response: GenericResponse | None,
217
+ checks: list[Check],
218
+ ) -> None:
219
+ self.status = status
220
+ self.transition_id = transition_id
221
+ self.target = target
222
+ self.case = case
223
+ self.response = response
224
+ self.checks = checks
225
+ self.timestamp = time.monotonic()
226
+
227
+ def asdict(self) -> dict[str, Any]:
228
+ return {
229
+ "timestamp": self.timestamp,
230
+ "status": self.status,
231
+ "transition_id": {
232
+ "name": self.transition_id.name,
233
+ "status_code": self.transition_id.status_code,
234
+ "source": self.transition_id.source,
235
+ }
236
+ if self.transition_id is not None
237
+ else None,
238
+ "target": self.target,
239
+ "response": {
240
+ "status_code": self.response.status_code,
241
+ "elapsed": self.response.elapsed.total_seconds(),
242
+ }
243
+ if self.response is not None
244
+ else None,
245
+ }
246
+
247
+
248
+ @dataclass
249
+ class Interrupted(StatefulEvent):
250
+ """The state machine execution was interrupted."""
251
+
252
+ __slots__ = ("timestamp",)
253
+
254
+ def __init__(self) -> None:
255
+ self.timestamp = time.monotonic()
256
+
257
+
258
+ @dataclass
259
+ class Errored(StatefulEvent):
260
+ """An error occurred during the state machine execution."""
261
+
262
+ exception: Exception
263
+
264
+ __slots__ = ("timestamp", "exception")
265
+
266
+ def __init__(self, *, exception: Exception) -> None:
267
+ self.exception = exception
268
+ self.timestamp = time.monotonic()
269
+
270
+ def asdict(self) -> dict[str, Any]:
271
+ return {
272
+ "timestamp": self.timestamp,
273
+ "exception": format_exception(self.exception, True),
274
+ }