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.
- schemathesis/__init__.py +6 -6
- schemathesis/_compat.py +2 -2
- schemathesis/_dependency_versions.py +4 -2
- schemathesis/_hypothesis.py +369 -56
- schemathesis/_lazy_import.py +1 -0
- schemathesis/_override.py +5 -4
- schemathesis/_patches.py +21 -0
- schemathesis/_rate_limiter.py +7 -0
- schemathesis/_xml.py +75 -22
- schemathesis/auths.py +78 -16
- schemathesis/checks.py +21 -9
- schemathesis/cli/__init__.py +783 -432
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/callbacks.py +58 -13
- schemathesis/cli/cassettes.py +233 -47
- schemathesis/cli/constants.py +8 -2
- schemathesis/cli/context.py +22 -5
- schemathesis/cli/debug.py +2 -1
- schemathesis/cli/handlers.py +4 -1
- schemathesis/cli/junitxml.py +103 -22
- schemathesis/cli/options.py +15 -4
- schemathesis/cli/output/default.py +258 -112
- schemathesis/cli/output/short.py +23 -8
- schemathesis/cli/reporting.py +79 -0
- schemathesis/cli/sanitization.py +6 -0
- schemathesis/code_samples.py +5 -3
- schemathesis/constants.py +1 -0
- schemathesis/contrib/openapi/__init__.py +1 -1
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
- schemathesis/contrib/openapi/formats/uuid.py +2 -1
- schemathesis/contrib/unique_data.py +3 -3
- schemathesis/exceptions.py +76 -65
- schemathesis/experimental/__init__.py +35 -0
- schemathesis/extra/_aiohttp.py +1 -0
- schemathesis/extra/_flask.py +4 -1
- schemathesis/extra/_server.py +1 -0
- schemathesis/extra/pytest_plugin.py +17 -25
- schemathesis/failures.py +77 -9
- schemathesis/filters.py +185 -8
- schemathesis/fixups/__init__.py +1 -0
- schemathesis/fixups/fast_api.py +2 -2
- schemathesis/fixups/utf8_bom.py +1 -2
- schemathesis/generation/__init__.py +20 -36
- schemathesis/generation/_hypothesis.py +59 -0
- schemathesis/generation/_methods.py +44 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/graphql.py +0 -1
- schemathesis/hooks.py +89 -12
- schemathesis/internal/checks.py +84 -0
- schemathesis/internal/copy.py +22 -3
- schemathesis/internal/deprecation.py +6 -2
- schemathesis/internal/diff.py +15 -0
- schemathesis/internal/extensions.py +27 -0
- schemathesis/internal/jsonschema.py +2 -1
- schemathesis/internal/output.py +68 -0
- schemathesis/internal/result.py +1 -1
- schemathesis/internal/transformation.py +11 -0
- schemathesis/lazy.py +138 -25
- schemathesis/loaders.py +7 -5
- schemathesis/models.py +318 -211
- schemathesis/parameters.py +4 -0
- schemathesis/runner/__init__.py +50 -15
- schemathesis/runner/events.py +65 -5
- schemathesis/runner/impl/context.py +104 -0
- schemathesis/runner/impl/core.py +388 -177
- schemathesis/runner/impl/solo.py +19 -29
- schemathesis/runner/impl/threadpool.py +70 -79
- schemathesis/runner/probes.py +11 -9
- schemathesis/runner/serialization.py +150 -17
- schemathesis/sanitization.py +5 -1
- schemathesis/schemas.py +170 -102
- schemathesis/serializers.py +7 -2
- schemathesis/service/ci.py +1 -0
- schemathesis/service/client.py +39 -6
- schemathesis/service/events.py +5 -1
- schemathesis/service/extensions.py +224 -0
- schemathesis/service/hosts.py +6 -2
- schemathesis/service/metadata.py +25 -0
- schemathesis/service/models.py +211 -2
- schemathesis/service/report.py +6 -6
- schemathesis/service/serialization.py +45 -71
- schemathesis/service/usage.py +1 -0
- schemathesis/specs/graphql/_cache.py +26 -0
- schemathesis/specs/graphql/loaders.py +25 -5
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +2 -2
- schemathesis/specs/graphql/schemas.py +130 -100
- schemathesis/specs/graphql/validation.py +1 -2
- schemathesis/specs/openapi/__init__.py +1 -0
- schemathesis/specs/openapi/_cache.py +123 -0
- schemathesis/specs/openapi/_hypothesis.py +78 -60
- schemathesis/specs/openapi/checks.py +504 -25
- schemathesis/specs/openapi/converter.py +31 -4
- schemathesis/specs/openapi/definitions.py +10 -17
- schemathesis/specs/openapi/examples.py +126 -12
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +1 -1
- schemathesis/specs/openapi/expressions/extractors.py +26 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +29 -6
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +44 -0
- schemathesis/specs/openapi/links.py +125 -42
- schemathesis/specs/openapi/loaders.py +77 -36
- schemathesis/specs/openapi/media_types.py +34 -0
- schemathesis/specs/openapi/negative/__init__.py +6 -3
- schemathesis/specs/openapi/negative/mutations.py +21 -6
- schemathesis/specs/openapi/parameters.py +39 -25
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +37 -7
- schemathesis/specs/openapi/schemas.py +360 -241
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +1 -0
- schemathesis/specs/openapi/stateful/__init__.py +198 -70
- schemathesis/specs/openapi/stateful/statistic.py +198 -0
- schemathesis/specs/openapi/stateful/types.py +14 -0
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/specs/openapi/validation.py +1 -0
- schemathesis/stateful/__init__.py +35 -21
- schemathesis/stateful/config.py +97 -0
- schemathesis/stateful/context.py +135 -0
- schemathesis/stateful/events.py +274 -0
- schemathesis/stateful/runner.py +309 -0
- schemathesis/stateful/sink.py +68 -0
- schemathesis/stateful/state_machine.py +67 -38
- schemathesis/stateful/statistic.py +22 -0
- schemathesis/stateful/validation.py +100 -0
- schemathesis/targets.py +33 -1
- schemathesis/throttling.py +25 -5
- schemathesis/transports/__init__.py +354 -0
- schemathesis/transports/asgi.py +7 -0
- schemathesis/transports/auth.py +25 -2
- schemathesis/transports/content_types.py +3 -1
- schemathesis/transports/headers.py +2 -1
- schemathesis/transports/responses.py +9 -4
- schemathesis/types.py +9 -0
- schemathesis/utils.py +11 -16
- schemathesis-3.39.7.dist-info/METADATA +293 -0
- schemathesis-3.39.7.dist-info/RECORD +160 -0
- {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis-3.25.6.dist-info/METADATA +0 -356
- schemathesis-3.25.6.dist-info/RECORD +0 -134
- {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
+
}
|