schemathesis 3.39.15__py3-none-any.whl → 4.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +41 -79
- schemathesis/auths.py +111 -122
- schemathesis/checks.py +169 -60
- schemathesis/cli/__init__.py +15 -2117
- schemathesis/cli/commands/__init__.py +85 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +590 -0
- schemathesis/cli/commands/run/context.py +204 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
- schemathesis/cli/commands/run/handlers/output.py +1628 -0
- schemathesis/cli/commands/run/loaders.py +114 -0
- schemathesis/cli/commands/run/validation.py +246 -0
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +84 -0
- schemathesis/cli/{options.py → ext/options.py} +36 -34
- schemathesis/config/__init__.py +189 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +99 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +156 -0
- schemathesis/config/_generation.py +149 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +327 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +187 -0
- schemathesis/config/_projects.py +527 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +885 -0
- schemathesis/core/__init__.py +67 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +459 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/{throttling.py → core/rate_limit.py} +16 -17
- schemathesis/core/registries.py +31 -0
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +54 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +118 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +169 -0
- schemathesis/engine/errors.py +464 -0
- schemathesis/engine/events.py +258 -0
- schemathesis/engine/phases/__init__.py +88 -0
- schemathesis/{runner → engine/phases}/probes.py +52 -68
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +356 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +212 -0
- schemathesis/engine/phases/unit/_executor.py +416 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +247 -0
- schemathesis/errors.py +43 -0
- schemathesis/filters.py +17 -98
- schemathesis/generation/__init__.py +5 -33
- schemathesis/generation/case.py +317 -0
- schemathesis/generation/coverage.py +282 -175
- schemathesis/generation/hypothesis/__init__.py +36 -0
- schemathesis/generation/hypothesis/builder.py +800 -0
- schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +116 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +278 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +284 -0
- schemathesis/hooks.py +80 -101
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +455 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +313 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +281 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -273
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +42 -6
- schemathesis/specs/graphql/schemas.py +141 -137
- schemathesis/specs/graphql/validation.py +11 -17
- schemathesis/specs/openapi/__init__.py +6 -1
- schemathesis/specs/openapi/_cache.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +142 -156
- schemathesis/specs/openapi/checks.py +368 -257
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +23 -21
- schemathesis/specs/openapi/expressions/__init__.py +31 -19
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/lexer.py +1 -1
- schemathesis/specs/openapi/expressions/nodes.py +36 -41
- schemathesis/specs/openapi/expressions/parser.py +1 -1
- schemathesis/specs/openapi/formats.py +35 -7
- schemathesis/specs/openapi/media_types.py +53 -12
- schemathesis/specs/openapi/negative/__init__.py +7 -4
- schemathesis/specs/openapi/negative/mutations.py +6 -5
- schemathesis/specs/openapi/parameters.py +7 -10
- schemathesis/specs/openapi/patterns.py +94 -31
- schemathesis/specs/openapi/references.py +12 -53
- schemathesis/specs/openapi/schemas.py +238 -308
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +12 -6
- schemathesis/specs/openapi/stateful/__init__.py +268 -133
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/links.py +209 -0
- schemathesis/transport/__init__.py +142 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +124 -0
- schemathesis/transport/requests.py +244 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -11
- schemathesis/transport/wsgi.py +171 -0
- schemathesis-4.0.0.dist-info/METADATA +204 -0
- schemathesis-4.0.0.dist-info/RECORD +164 -0
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -712
- schemathesis/_override.py +0 -50
- schemathesis/_patches.py +0 -21
- schemathesis/_rate_limiter.py +0 -7
- schemathesis/cli/callbacks.py +0 -466
- schemathesis/cli/cassettes.py +0 -561
- schemathesis/cli/context.py +0 -75
- schemathesis/cli/debug.py +0 -27
- schemathesis/cli/handlers.py +0 -19
- schemathesis/cli/junitxml.py +0 -124
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -920
- schemathesis/cli/output/short.py +0 -59
- schemathesis/cli/reporting.py +0 -79
- schemathesis/cli/sanitization.py +0 -26
- schemathesis/code_samples.py +0 -151
- schemathesis/constants.py +0 -54
- schemathesis/contrib/__init__.py +0 -11
- schemathesis/contrib/openapi/__init__.py +0 -11
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -16
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -571
- schemathesis/experimental/__init__.py +0 -109
- schemathesis/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -284
- schemathesis/fixups/__init__.py +0 -37
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -28
- schemathesis/generation/_methods.py +0 -44
- schemathesis/graphql.py +0 -3
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/checks.py +0 -86
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -37
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- schemathesis/internal/output.py +0 -68
- schemathesis/internal/transformation.py +0 -26
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -474
- schemathesis/loaders.py +0 -122
- schemathesis/models.py +0 -1341
- schemathesis/parameters.py +0 -90
- schemathesis/runner/__init__.py +0 -605
- schemathesis/runner/events.py +0 -389
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/context.py +0 -88
- schemathesis/runner/impl/core.py +0 -1280
- schemathesis/runner/impl/solo.py +0 -80
- schemathesis/runner/impl/threadpool.py +0 -391
- schemathesis/runner/serialization.py +0 -544
- schemathesis/sanitization.py +0 -252
- schemathesis/serializers.py +0 -328
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -202
- schemathesis/service/client.py +0 -133
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -61
- schemathesis/service/extensions.py +0 -224
- schemathesis/service/hosts.py +0 -111
- schemathesis/service/metadata.py +0 -71
- schemathesis/service/models.py +0 -258
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -173
- schemathesis/service/usage.py +0 -66
- schemathesis/specs/graphql/loaders.py +0 -364
- schemathesis/specs/openapi/expressions/context.py +0 -16
- schemathesis/specs/openapi/links.py +0 -389
- schemathesis/specs/openapi/loaders.py +0 -707
- schemathesis/specs/openapi/stateful/statistic.py +0 -198
- schemathesis/specs/openapi/stateful/types.py +0 -14
- schemathesis/specs/openapi/validation.py +0 -26
- schemathesis/stateful/__init__.py +0 -147
- schemathesis/stateful/config.py +0 -97
- schemathesis/stateful/context.py +0 -135
- schemathesis/stateful/events.py +0 -274
- schemathesis/stateful/runner.py +0 -309
- schemathesis/stateful/sink.py +0 -68
- schemathesis/stateful/state_machine.py +0 -328
- schemathesis/stateful/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -369
- schemathesis/transports/asgi.py +0 -7
- schemathesis/transports/auth.py +0 -38
- schemathesis/transports/headers.py +0 -36
- schemathesis/transports/responses.py +0 -57
- schemathesis/types.py +0 -44
- schemathesis/utils.py +0 -164
- schemathesis-3.39.15.dist-info/METADATA +0 -293
- schemathesis-3.39.15.dist-info/RECORD +0 -160
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
- /schemathesis/{internal → core}/result.py +0 -0
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
schemathesis/runner/impl/core.py
DELETED
@@ -1,1280 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import functools
|
4
|
-
import logging
|
5
|
-
import operator
|
6
|
-
import re
|
7
|
-
import threading
|
8
|
-
import time
|
9
|
-
import unittest
|
10
|
-
import uuid
|
11
|
-
import warnings
|
12
|
-
from contextlib import contextmanager
|
13
|
-
from dataclasses import dataclass, field
|
14
|
-
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, List, Literal, cast
|
15
|
-
from warnings import WarningMessage, catch_warnings
|
16
|
-
|
17
|
-
import hypothesis
|
18
|
-
import requests
|
19
|
-
from _pytest.logging import LogCaptureHandler, catching_logs
|
20
|
-
from hypothesis.errors import HypothesisException, InvalidArgument
|
21
|
-
from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
|
22
|
-
from jsonschema.exceptions import SchemaError as JsonSchemaError
|
23
|
-
from jsonschema.exceptions import ValidationError
|
24
|
-
from requests.structures import CaseInsensitiveDict
|
25
|
-
from urllib3.exceptions import InsecureRequestWarning
|
26
|
-
|
27
|
-
from ... import experimental, failures, hooks
|
28
|
-
from ..._compat import MultipleFailures
|
29
|
-
from ..._hypothesis import (
|
30
|
-
get_invalid_example_headers_mark,
|
31
|
-
get_invalid_regex_mark,
|
32
|
-
get_non_serializable_mark,
|
33
|
-
has_unsatisfied_example_mark,
|
34
|
-
)
|
35
|
-
from ...auths import unregister as unregister_auth
|
36
|
-
from ...checks import _make_max_response_time_failure_message
|
37
|
-
from ...constants import (
|
38
|
-
DEFAULT_STATEFUL_RECURSION_LIMIT,
|
39
|
-
RECURSIVE_REFERENCE_ERROR_MESSAGE,
|
40
|
-
SERIALIZERS_SUGGESTION_MESSAGE,
|
41
|
-
USER_AGENT,
|
42
|
-
)
|
43
|
-
from ...exceptions import (
|
44
|
-
CheckFailed,
|
45
|
-
DeadlineExceeded,
|
46
|
-
InternalError,
|
47
|
-
InvalidHeadersExample,
|
48
|
-
InvalidRegularExpression,
|
49
|
-
NonCheckError,
|
50
|
-
OperationSchemaError,
|
51
|
-
RecursiveReferenceError,
|
52
|
-
SerializationNotPossible,
|
53
|
-
SkipTest,
|
54
|
-
format_exception,
|
55
|
-
get_grouped_exception,
|
56
|
-
maybe_set_assertion_message,
|
57
|
-
)
|
58
|
-
from ...generation import DataGenerationMethod, GenerationConfig
|
59
|
-
from ...hooks import HookContext, get_all_by_name
|
60
|
-
from ...internal.checks import CheckConfig, CheckContext
|
61
|
-
from ...internal.datetime import current_datetime
|
62
|
-
from ...internal.result import Err, Ok, Result
|
63
|
-
from ...models import APIOperation, Case, Check, Status, TestResult
|
64
|
-
from ...runner import events
|
65
|
-
from ...service import extensions
|
66
|
-
from ...service.models import AnalysisResult, AnalysisSuccess
|
67
|
-
from ...specs.openapi import formats
|
68
|
-
from ...stateful import Feedback, Stateful
|
69
|
-
from ...stateful import events as stateful_events
|
70
|
-
from ...stateful import runner as stateful_runner
|
71
|
-
from ...targets import Target, TargetContext
|
72
|
-
from ...transports import RequestConfig, RequestsTransport
|
73
|
-
from ...transports.auth import get_requests_auth, prepare_wsgi_headers
|
74
|
-
from ...utils import capture_hypothesis_output
|
75
|
-
from .. import probes
|
76
|
-
from ..serialization import SerializedTestResult
|
77
|
-
from .context import RunnerContext
|
78
|
-
|
79
|
-
if TYPE_CHECKING:
|
80
|
-
from types import TracebackType
|
81
|
-
|
82
|
-
from requests.auth import HTTPDigestAuth
|
83
|
-
|
84
|
-
from ..._override import CaseOverride
|
85
|
-
from ...internal.checks import CheckFunction
|
86
|
-
from ...schemas import BaseSchema
|
87
|
-
from ...service.client import ServiceClient
|
88
|
-
from ...transports.responses import GenericResponse, WSGIResponse
|
89
|
-
from ...types import RawAuth
|
90
|
-
|
91
|
-
|
92
|
-
def _should_count_towards_stop(event: events.ExecutionEvent) -> bool:
|
93
|
-
return isinstance(event, events.AfterExecution) and event.status in (Status.error, Status.failure)
|
94
|
-
|
95
|
-
|
96
|
-
@dataclass
|
97
|
-
class BaseRunner:
|
98
|
-
schema: BaseSchema
|
99
|
-
checks: Iterable[CheckFunction]
|
100
|
-
max_response_time: int | None
|
101
|
-
targets: Iterable[Target]
|
102
|
-
hypothesis_settings: hypothesis.settings
|
103
|
-
generation_config: GenerationConfig | None
|
104
|
-
probe_config: probes.ProbeConfig
|
105
|
-
checks_config: CheckConfig
|
106
|
-
request_config: RequestConfig = field(default_factory=RequestConfig)
|
107
|
-
override: CaseOverride | None = None
|
108
|
-
auth: RawAuth | None = None
|
109
|
-
auth_type: str | None = None
|
110
|
-
headers: dict[str, Any] | None = None
|
111
|
-
store_interactions: bool = False
|
112
|
-
seed: int | None = None
|
113
|
-
exit_first: bool = False
|
114
|
-
no_failfast: bool = False
|
115
|
-
max_failures: int | None = None
|
116
|
-
started_at: str = field(default_factory=current_datetime)
|
117
|
-
unique_data: bool = False
|
118
|
-
dry_run: bool = False
|
119
|
-
stateful: Stateful | None = None
|
120
|
-
stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT
|
121
|
-
count_operations: bool = True
|
122
|
-
count_links: bool = True
|
123
|
-
service_client: ServiceClient | None = None
|
124
|
-
_failures_counter: int = 0
|
125
|
-
_is_stopping_due_to_failure_limit: bool = False
|
126
|
-
|
127
|
-
def execute(self) -> EventStream:
|
128
|
-
"""Common logic for all runners."""
|
129
|
-
event = threading.Event()
|
130
|
-
return EventStream(self._generate_events(event), event)
|
131
|
-
|
132
|
-
def _generate_events(self, stop_event: threading.Event) -> Generator[events.ExecutionEvent, None, None]:
|
133
|
-
# If auth is explicitly provided, then the global provider is ignored
|
134
|
-
if self.auth is not None:
|
135
|
-
unregister_auth()
|
136
|
-
ctx = RunnerContext(
|
137
|
-
auth=self.auth,
|
138
|
-
seed=self.seed,
|
139
|
-
stop_event=stop_event,
|
140
|
-
unique_data=self.unique_data,
|
141
|
-
checks_config=self.checks_config,
|
142
|
-
override=self.override,
|
143
|
-
no_failfast=self.no_failfast,
|
144
|
-
)
|
145
|
-
start_time = time.monotonic()
|
146
|
-
initialized = None
|
147
|
-
__probes = None
|
148
|
-
__analysis: Result[AnalysisResult, Exception] | None = None
|
149
|
-
|
150
|
-
def _should_warn_about_only_4xx(result: TestResult) -> bool:
|
151
|
-
if all(check.response is None for check in result.checks):
|
152
|
-
return False
|
153
|
-
# Don't duplicate auth warnings
|
154
|
-
if {check.response.status_code for check in result.checks if check.response is not None} <= {401, 403}:
|
155
|
-
return False
|
156
|
-
# At this point we know we only have 4xx responses
|
157
|
-
return True
|
158
|
-
|
159
|
-
def _check_warnings() -> None:
|
160
|
-
# Warn if all positive test cases got 4xx in return and no failure was found
|
161
|
-
def all_positive_are_rejected(result: TestResult) -> bool:
|
162
|
-
seen_positive = False
|
163
|
-
for check in result.checks:
|
164
|
-
if check.example.data_generation_method != DataGenerationMethod.positive:
|
165
|
-
continue
|
166
|
-
seen_positive = True
|
167
|
-
if check.response is None:
|
168
|
-
continue
|
169
|
-
# At least one positive response for positive test case
|
170
|
-
if 200 <= check.response.status_code < 300:
|
171
|
-
return False
|
172
|
-
# If there are positive test cases, and we ended up here, then there are no 2xx responses for them
|
173
|
-
# Otherwise, there are no positive test cases at all and this check should pass
|
174
|
-
return seen_positive
|
175
|
-
|
176
|
-
for result in ctx.data.results:
|
177
|
-
# Only warn about 4xx responses in successful positive test scenarios
|
178
|
-
if (
|
179
|
-
all(check.value == Status.success for check in result.checks)
|
180
|
-
and DataGenerationMethod.positive in result.data_generation_method
|
181
|
-
and all_positive_are_rejected(result)
|
182
|
-
and _should_warn_about_only_4xx(result)
|
183
|
-
):
|
184
|
-
ctx.add_warning(
|
185
|
-
f"`{result.verbose_name}` returned only 4xx responses during unit tests. Check base URL or adjust data generation settings"
|
186
|
-
)
|
187
|
-
|
188
|
-
def _initialize() -> events.Initialized:
|
189
|
-
nonlocal initialized
|
190
|
-
initialized = events.Initialized.from_schema(
|
191
|
-
schema=self.schema,
|
192
|
-
count_operations=self.count_operations,
|
193
|
-
count_links=self.count_links,
|
194
|
-
seed=ctx.seed,
|
195
|
-
start_time=start_time,
|
196
|
-
)
|
197
|
-
return initialized
|
198
|
-
|
199
|
-
def _finish() -> events.Finished:
|
200
|
-
_check_warnings()
|
201
|
-
return events.Finished.from_results(results=ctx.data, running_time=time.monotonic() - start_time)
|
202
|
-
|
203
|
-
def _before_probes() -> events.BeforeProbing:
|
204
|
-
return events.BeforeProbing()
|
205
|
-
|
206
|
-
def _run_probes() -> None:
|
207
|
-
if not self.dry_run:
|
208
|
-
nonlocal __probes
|
209
|
-
|
210
|
-
__probes = run_probes(self.schema, self.probe_config)
|
211
|
-
|
212
|
-
def _after_probes() -> events.AfterProbing:
|
213
|
-
_probes = cast(List[probes.ProbeRun], __probes)
|
214
|
-
return events.AfterProbing(probes=_probes)
|
215
|
-
|
216
|
-
def _before_analysis() -> events.BeforeAnalysis:
|
217
|
-
return events.BeforeAnalysis()
|
218
|
-
|
219
|
-
def _run_analysis() -> None:
|
220
|
-
nonlocal __analysis, __probes
|
221
|
-
|
222
|
-
if self.service_client is not None:
|
223
|
-
try:
|
224
|
-
_probes = cast(List[probes.ProbeRun], __probes)
|
225
|
-
result = self.service_client.analyze_schema(_probes, self.schema.raw_schema)
|
226
|
-
if isinstance(result, AnalysisSuccess):
|
227
|
-
extensions.apply(result.extensions, self.schema)
|
228
|
-
__analysis = Ok(result)
|
229
|
-
except Exception as exc:
|
230
|
-
__analysis = Err(exc)
|
231
|
-
|
232
|
-
def _after_analysis() -> events.AfterAnalysis:
|
233
|
-
return events.AfterAnalysis(analysis=__analysis)
|
234
|
-
|
235
|
-
if ctx.is_stopped:
|
236
|
-
yield _finish()
|
237
|
-
return
|
238
|
-
|
239
|
-
for event_factory in (
|
240
|
-
_initialize,
|
241
|
-
_before_probes,
|
242
|
-
_run_probes,
|
243
|
-
_after_probes,
|
244
|
-
_before_analysis,
|
245
|
-
_run_analysis,
|
246
|
-
_after_analysis,
|
247
|
-
):
|
248
|
-
event = event_factory()
|
249
|
-
if event is not None:
|
250
|
-
yield event
|
251
|
-
if ctx.is_stopped:
|
252
|
-
yield _finish() # type: ignore[unreachable]
|
253
|
-
return
|
254
|
-
|
255
|
-
try:
|
256
|
-
warnings.simplefilter("ignore", InsecureRequestWarning)
|
257
|
-
if not experimental.STATEFUL_ONLY.is_enabled:
|
258
|
-
yield from self._execute(ctx)
|
259
|
-
if not self._is_stopping_due_to_failure_limit:
|
260
|
-
yield from self._run_stateful_tests(ctx)
|
261
|
-
except KeyboardInterrupt:
|
262
|
-
yield events.Interrupted()
|
263
|
-
|
264
|
-
yield _finish()
|
265
|
-
|
266
|
-
def _should_stop(self, event: events.ExecutionEvent) -> bool:
|
267
|
-
result = self.__should_stop(event)
|
268
|
-
if result:
|
269
|
-
self._is_stopping_due_to_failure_limit = True
|
270
|
-
return result
|
271
|
-
|
272
|
-
def __should_stop(self, event: events.ExecutionEvent) -> bool:
|
273
|
-
if _should_count_towards_stop(event):
|
274
|
-
if self.exit_first:
|
275
|
-
return True
|
276
|
-
if self.max_failures is not None:
|
277
|
-
self._failures_counter += 1
|
278
|
-
return self._failures_counter >= self.max_failures
|
279
|
-
return False
|
280
|
-
|
281
|
-
def _execute(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
|
282
|
-
raise NotImplementedError
|
283
|
-
|
284
|
-
def _run_stateful_tests(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
|
285
|
-
# Run new-style stateful tests
|
286
|
-
if self.stateful is not None and experimental.STATEFUL_TEST_RUNNER.is_enabled and self.schema.links_count > 0:
|
287
|
-
result = TestResult(
|
288
|
-
method="",
|
289
|
-
path="",
|
290
|
-
verbose_name="Stateful tests",
|
291
|
-
seed=ctx.seed,
|
292
|
-
data_generation_method=self.schema.data_generation_methods,
|
293
|
-
)
|
294
|
-
headers = self.headers or {}
|
295
|
-
if isinstance(self.schema.transport, RequestsTransport):
|
296
|
-
auth = get_requests_auth(self.auth, self.auth_type)
|
297
|
-
else:
|
298
|
-
auth = None
|
299
|
-
headers = prepare_wsgi_headers(headers, self.auth, self.auth_type)
|
300
|
-
config = stateful_runner.StatefulTestRunnerConfig(
|
301
|
-
checks=tuple(self.checks),
|
302
|
-
headers=headers,
|
303
|
-
hypothesis_settings=self.hypothesis_settings,
|
304
|
-
exit_first=self.exit_first,
|
305
|
-
max_failures=None if self.max_failures is None else self.max_failures - self._failures_counter,
|
306
|
-
request=self.request_config,
|
307
|
-
auth=auth,
|
308
|
-
seed=ctx.seed,
|
309
|
-
override=self.override,
|
310
|
-
)
|
311
|
-
state_machine = self.schema.as_state_machine()
|
312
|
-
runner = state_machine.runner(config=config)
|
313
|
-
status = Status.success
|
314
|
-
|
315
|
-
def from_step_status(step_status: stateful_events.StepStatus) -> Status:
|
316
|
-
return {
|
317
|
-
stateful_events.StepStatus.SUCCESS: Status.success,
|
318
|
-
stateful_events.StepStatus.FAILURE: Status.failure,
|
319
|
-
stateful_events.StepStatus.ERROR: Status.error,
|
320
|
-
stateful_events.StepStatus.INTERRUPTED: Status.error,
|
321
|
-
}[step_status]
|
322
|
-
|
323
|
-
if self.store_interactions:
|
324
|
-
if isinstance(state_machine.schema.transport, RequestsTransport):
|
325
|
-
|
326
|
-
def on_step_finished(event: stateful_events.StepFinished) -> None:
|
327
|
-
if event.response is not None and event.status is not None:
|
328
|
-
response = cast(requests.Response, event.response)
|
329
|
-
result.store_requests_response(
|
330
|
-
status=from_step_status(event.status),
|
331
|
-
case=event.case,
|
332
|
-
response=response,
|
333
|
-
checks=event.checks,
|
334
|
-
headers=headers,
|
335
|
-
session=None,
|
336
|
-
)
|
337
|
-
|
338
|
-
else:
|
339
|
-
|
340
|
-
def on_step_finished(event: stateful_events.StepFinished) -> None:
|
341
|
-
from ...transports.responses import WSGIResponse
|
342
|
-
|
343
|
-
if event.response is not None and event.status is not None:
|
344
|
-
response = cast(WSGIResponse, event.response)
|
345
|
-
result.store_wsgi_response(
|
346
|
-
status=from_step_status(event.status),
|
347
|
-
case=event.case,
|
348
|
-
response=response,
|
349
|
-
headers=headers,
|
350
|
-
elapsed=response.elapsed.total_seconds(),
|
351
|
-
checks=event.checks,
|
352
|
-
)
|
353
|
-
else:
|
354
|
-
|
355
|
-
def on_step_finished(event: stateful_events.StepFinished) -> None:
|
356
|
-
return None
|
357
|
-
|
358
|
-
test_start_time: float | None = None
|
359
|
-
test_elapsed_time: float | None = None
|
360
|
-
|
361
|
-
for stateful_event in runner.execute():
|
362
|
-
if isinstance(stateful_event, stateful_events.SuiteFinished):
|
363
|
-
if stateful_event.failures and status != Status.error:
|
364
|
-
status = Status.failure
|
365
|
-
elif isinstance(stateful_event, stateful_events.RunStarted):
|
366
|
-
test_start_time = stateful_event.timestamp
|
367
|
-
elif isinstance(stateful_event, stateful_events.RunFinished):
|
368
|
-
test_elapsed_time = stateful_event.timestamp - cast(float, test_start_time)
|
369
|
-
elif isinstance(stateful_event, stateful_events.StepFinished):
|
370
|
-
result.checks.extend(stateful_event.checks)
|
371
|
-
on_step_finished(stateful_event)
|
372
|
-
elif isinstance(stateful_event, stateful_events.Errored):
|
373
|
-
status = Status.error
|
374
|
-
result.add_error(stateful_event.exception)
|
375
|
-
yield events.StatefulEvent(data=stateful_event)
|
376
|
-
ctx.add_result(result)
|
377
|
-
yield events.AfterStatefulExecution(
|
378
|
-
status=status,
|
379
|
-
result=SerializedTestResult.from_test_result(result),
|
380
|
-
elapsed_time=cast(float, test_elapsed_time),
|
381
|
-
data_generation_method=self.schema.data_generation_methods,
|
382
|
-
)
|
383
|
-
|
384
|
-
def _run_tests(
|
385
|
-
self,
|
386
|
-
maker: Callable,
|
387
|
-
test_func: Callable,
|
388
|
-
settings: hypothesis.settings,
|
389
|
-
generation_config: GenerationConfig | None,
|
390
|
-
ctx: RunnerContext,
|
391
|
-
recursion_level: int = 0,
|
392
|
-
headers: dict[str, Any] | None = None,
|
393
|
-
**kwargs: Any,
|
394
|
-
) -> Generator[events.ExecutionEvent, None, None]:
|
395
|
-
"""Run tests and recursively run additional tests."""
|
396
|
-
if recursion_level > self.stateful_recursion_limit:
|
397
|
-
return
|
398
|
-
|
399
|
-
def as_strategy_kwargs(_operation: APIOperation) -> dict[str, Any]:
|
400
|
-
kw = {}
|
401
|
-
if self.override is not None:
|
402
|
-
for location, entry in self.override.for_operation(_operation).items():
|
403
|
-
if entry:
|
404
|
-
kw[location] = entry
|
405
|
-
if headers:
|
406
|
-
kw["headers"] = {key: value for key, value in headers.items() if key.lower() != "user-agent"}
|
407
|
-
return kw
|
408
|
-
|
409
|
-
for result in maker(
|
410
|
-
test_func,
|
411
|
-
settings=settings,
|
412
|
-
generation_config=generation_config,
|
413
|
-
seed=ctx.seed,
|
414
|
-
as_strategy_kwargs=as_strategy_kwargs,
|
415
|
-
):
|
416
|
-
if isinstance(result, Ok):
|
417
|
-
operation, test = result.ok()
|
418
|
-
if self.stateful is not None and not experimental.STATEFUL_TEST_RUNNER.is_enabled:
|
419
|
-
feedback = Feedback(self.stateful, operation)
|
420
|
-
else:
|
421
|
-
feedback = None
|
422
|
-
# Track whether `BeforeExecution` was already emitted.
|
423
|
-
# Schema error may happen before / after `BeforeExecution`, but it should be emitted only once
|
424
|
-
# and the `AfterExecution` event should have the same correlation id as previous `BeforeExecution`
|
425
|
-
before_execution_correlation_id = None
|
426
|
-
try:
|
427
|
-
for event in run_test(
|
428
|
-
operation,
|
429
|
-
test,
|
430
|
-
ctx=ctx,
|
431
|
-
feedback=feedback,
|
432
|
-
recursion_level=recursion_level,
|
433
|
-
data_generation_methods=self.schema.data_generation_methods,
|
434
|
-
headers=headers,
|
435
|
-
**kwargs,
|
436
|
-
):
|
437
|
-
yield event
|
438
|
-
if isinstance(event, events.BeforeExecution):
|
439
|
-
before_execution_correlation_id = event.correlation_id
|
440
|
-
if isinstance(event, events.Interrupted):
|
441
|
-
return
|
442
|
-
# Additional tests, generated via the `feedback` instance
|
443
|
-
if feedback is not None:
|
444
|
-
yield from self._run_tests(
|
445
|
-
feedback.get_stateful_tests,
|
446
|
-
test_func,
|
447
|
-
settings=settings,
|
448
|
-
generation_config=generation_config,
|
449
|
-
recursion_level=recursion_level + 1,
|
450
|
-
ctx=ctx,
|
451
|
-
headers=headers,
|
452
|
-
**kwargs,
|
453
|
-
)
|
454
|
-
except OperationSchemaError as exc:
|
455
|
-
yield from handle_schema_error(
|
456
|
-
exc,
|
457
|
-
ctx,
|
458
|
-
self.schema.data_generation_methods,
|
459
|
-
recursion_level,
|
460
|
-
before_execution_correlation_id=before_execution_correlation_id,
|
461
|
-
)
|
462
|
-
else:
|
463
|
-
# Schema errors
|
464
|
-
yield from handle_schema_error(result.err(), ctx, self.schema.data_generation_methods, recursion_level)
|
465
|
-
|
466
|
-
|
467
|
-
def run_probes(schema: BaseSchema, config: probes.ProbeConfig) -> list[probes.ProbeRun]:
|
468
|
-
"""Discover capabilities of the tested app."""
|
469
|
-
results = probes.run(schema, config)
|
470
|
-
for result in results:
|
471
|
-
if isinstance(result.probe, probes.NullByteInHeader) and result.is_failure:
|
472
|
-
from ...specs.openapi.formats import HEADER_FORMAT, header_values
|
473
|
-
|
474
|
-
formats.register(HEADER_FORMAT, header_values(blacklist_characters="\n\r\x00"))
|
475
|
-
return results
|
476
|
-
|
477
|
-
|
478
|
-
@dataclass
|
479
|
-
class EventStream:
|
480
|
-
"""Schemathesis event stream.
|
481
|
-
|
482
|
-
Provides an API to control the execution flow.
|
483
|
-
"""
|
484
|
-
|
485
|
-
generator: Generator[events.ExecutionEvent, None, None]
|
486
|
-
stop_event: threading.Event
|
487
|
-
|
488
|
-
def __next__(self) -> events.ExecutionEvent:
|
489
|
-
return next(self.generator)
|
490
|
-
|
491
|
-
def __iter__(self) -> Generator[events.ExecutionEvent, None, None]:
|
492
|
-
return self.generator
|
493
|
-
|
494
|
-
def stop(self) -> None:
|
495
|
-
"""Stop the event stream.
|
496
|
-
|
497
|
-
Its next value will be the last one (Finished).
|
498
|
-
"""
|
499
|
-
self.stop_event.set()
|
500
|
-
|
501
|
-
def finish(self) -> events.ExecutionEvent:
|
502
|
-
"""Stop the event stream & return the last event."""
|
503
|
-
self.stop()
|
504
|
-
return next(self)
|
505
|
-
|
506
|
-
|
507
|
-
def handle_schema_error(
|
508
|
-
error: OperationSchemaError,
|
509
|
-
ctx: RunnerContext,
|
510
|
-
data_generation_methods: Iterable[DataGenerationMethod],
|
511
|
-
recursion_level: int,
|
512
|
-
*,
|
513
|
-
before_execution_correlation_id: str | None = None,
|
514
|
-
) -> Generator[events.ExecutionEvent, None, None]:
|
515
|
-
if error.method is not None:
|
516
|
-
assert error.path is not None
|
517
|
-
assert error.full_path is not None
|
518
|
-
data_generation_methods = list(data_generation_methods)
|
519
|
-
method = error.method.upper()
|
520
|
-
verbose_name = f"{method} {error.full_path}"
|
521
|
-
result = TestResult(
|
522
|
-
method=method,
|
523
|
-
path=error.full_path,
|
524
|
-
verbose_name=verbose_name,
|
525
|
-
data_generation_method=data_generation_methods,
|
526
|
-
)
|
527
|
-
result.add_error(error)
|
528
|
-
# It might be already emitted - reuse its correlation id
|
529
|
-
if before_execution_correlation_id is not None:
|
530
|
-
correlation_id = before_execution_correlation_id
|
531
|
-
else:
|
532
|
-
correlation_id = uuid.uuid4().hex
|
533
|
-
yield events.BeforeExecution(
|
534
|
-
method=method,
|
535
|
-
path=error.full_path,
|
536
|
-
verbose_name=verbose_name,
|
537
|
-
relative_path=error.path,
|
538
|
-
recursion_level=recursion_level,
|
539
|
-
data_generation_method=data_generation_methods,
|
540
|
-
correlation_id=correlation_id,
|
541
|
-
)
|
542
|
-
yield events.AfterExecution(
|
543
|
-
method=method,
|
544
|
-
path=error.full_path,
|
545
|
-
relative_path=error.path,
|
546
|
-
verbose_name=verbose_name,
|
547
|
-
status=Status.error,
|
548
|
-
result=SerializedTestResult.from_test_result(result),
|
549
|
-
data_generation_method=data_generation_methods,
|
550
|
-
elapsed_time=0.0,
|
551
|
-
hypothesis_output=[],
|
552
|
-
correlation_id=correlation_id,
|
553
|
-
)
|
554
|
-
ctx.add_result(result)
|
555
|
-
else:
|
556
|
-
# When there is no `method`, then the schema error may cover multiple operations, and we can't display it in
|
557
|
-
# the progress bar
|
558
|
-
ctx.add_generic_error(error)
|
559
|
-
|
560
|
-
|
561
|
-
def run_test(
|
562
|
-
operation: APIOperation,
|
563
|
-
test: Callable,
|
564
|
-
checks: Iterable[CheckFunction],
|
565
|
-
data_generation_methods: Iterable[DataGenerationMethod],
|
566
|
-
targets: Iterable[Target],
|
567
|
-
ctx: RunnerContext,
|
568
|
-
headers: dict[str, Any] | None,
|
569
|
-
recursion_level: int,
|
570
|
-
**kwargs: Any,
|
571
|
-
) -> Generator[events.ExecutionEvent, None, None]:
|
572
|
-
"""A single test run with all error handling needed."""
|
573
|
-
data_generation_methods = list(data_generation_methods)
|
574
|
-
result = TestResult(
|
575
|
-
method=operation.method.upper(),
|
576
|
-
path=operation.full_path,
|
577
|
-
verbose_name=operation.verbose_name,
|
578
|
-
data_generation_method=data_generation_methods,
|
579
|
-
)
|
580
|
-
# To simplify connecting `before` and `after` events in external systems
|
581
|
-
correlation_id = uuid.uuid4().hex
|
582
|
-
yield events.BeforeExecution.from_operation(
|
583
|
-
operation=operation,
|
584
|
-
recursion_level=recursion_level,
|
585
|
-
data_generation_method=data_generation_methods,
|
586
|
-
correlation_id=correlation_id,
|
587
|
-
)
|
588
|
-
hypothesis_output: list[str] = []
|
589
|
-
errors: list[Exception] = []
|
590
|
-
test_start_time = time.monotonic()
|
591
|
-
setup_hypothesis_database_key(test, operation)
|
592
|
-
|
593
|
-
def _on_flaky(exc: Exception) -> Status:
|
594
|
-
if isinstance(exc.__cause__, hypothesis.errors.DeadlineExceeded):
|
595
|
-
status = Status.error
|
596
|
-
result.add_error(DeadlineExceeded.from_exc(exc.__cause__))
|
597
|
-
elif (
|
598
|
-
hasattr(hypothesis.errors, "FlakyFailure")
|
599
|
-
and isinstance(exc, hypothesis.errors.FlakyFailure)
|
600
|
-
and any(isinstance(subexc, hypothesis.errors.DeadlineExceeded) for subexc in exc.exceptions)
|
601
|
-
):
|
602
|
-
for sub_exc in exc.exceptions:
|
603
|
-
if isinstance(sub_exc, hypothesis.errors.DeadlineExceeded):
|
604
|
-
result.add_error(DeadlineExceeded.from_exc(sub_exc))
|
605
|
-
status = Status.error
|
606
|
-
elif errors:
|
607
|
-
status = Status.error
|
608
|
-
add_errors(result, errors)
|
609
|
-
else:
|
610
|
-
status = Status.failure
|
611
|
-
result.mark_flaky()
|
612
|
-
return status
|
613
|
-
|
614
|
-
try:
|
615
|
-
with catch_warnings(record=True) as warnings, capture_hypothesis_output() as hypothesis_output:
|
616
|
-
test(
|
617
|
-
ctx=ctx,
|
618
|
-
checks=checks,
|
619
|
-
targets=targets,
|
620
|
-
result=result,
|
621
|
-
errors=errors,
|
622
|
-
headers=headers,
|
623
|
-
data_generation_methods=data_generation_methods,
|
624
|
-
**kwargs,
|
625
|
-
)
|
626
|
-
# Test body was not executed at all - Hypothesis did not generate any tests, but there is no error
|
627
|
-
if not result.is_executed:
|
628
|
-
status = Status.skip
|
629
|
-
result.mark_skipped(None)
|
630
|
-
else:
|
631
|
-
status = Status.success
|
632
|
-
except unittest.case.SkipTest as exc:
|
633
|
-
# Newer Hypothesis versions raise this exception if no tests were executed
|
634
|
-
status = Status.skip
|
635
|
-
result.mark_skipped(exc)
|
636
|
-
except CheckFailed:
|
637
|
-
status = Status.failure
|
638
|
-
except NonCheckError:
|
639
|
-
# It could be an error in user-defined extensions, network errors or internal Schemathesis errors
|
640
|
-
status = Status.error
|
641
|
-
result.mark_errored()
|
642
|
-
for error in deduplicate_errors(errors):
|
643
|
-
result.add_error(error)
|
644
|
-
except hypothesis.errors.Flaky as exc:
|
645
|
-
status = _on_flaky(exc)
|
646
|
-
except MultipleFailures:
|
647
|
-
# Schemathesis may detect multiple errors that come from different check results
|
648
|
-
# They raise different "grouped" exceptions
|
649
|
-
if errors:
|
650
|
-
status = Status.error
|
651
|
-
add_errors(result, errors)
|
652
|
-
else:
|
653
|
-
status = Status.failure
|
654
|
-
except hypothesis.errors.Unsatisfiable:
|
655
|
-
# We need more clear error message here
|
656
|
-
status = Status.error
|
657
|
-
result.add_error(hypothesis.errors.Unsatisfiable("Failed to generate test cases for this API operation"))
|
658
|
-
except KeyboardInterrupt:
|
659
|
-
yield events.Interrupted()
|
660
|
-
return
|
661
|
-
except SkipTest as exc:
|
662
|
-
status = Status.skip
|
663
|
-
result.mark_skipped(exc)
|
664
|
-
except AssertionError as exc: # May come from `hypothesis-jsonschema` or `hypothesis`
|
665
|
-
status = Status.error
|
666
|
-
try:
|
667
|
-
operation.schema.validate()
|
668
|
-
msg = "Unexpected error during testing of this API operation"
|
669
|
-
exc_msg = str(exc)
|
670
|
-
if exc_msg:
|
671
|
-
msg += f": {exc_msg}"
|
672
|
-
try:
|
673
|
-
raise InternalError(msg) from exc
|
674
|
-
except InternalError as exc:
|
675
|
-
error = exc
|
676
|
-
except ValidationError as exc:
|
677
|
-
error = OperationSchemaError.from_jsonschema_error(
|
678
|
-
exc,
|
679
|
-
path=operation.path,
|
680
|
-
method=operation.method,
|
681
|
-
full_path=operation.schema.get_full_path(operation.path),
|
682
|
-
)
|
683
|
-
result.add_error(error)
|
684
|
-
except HypothesisRefResolutionError:
|
685
|
-
status = Status.error
|
686
|
-
result.add_error(RecursiveReferenceError(RECURSIVE_REFERENCE_ERROR_MESSAGE))
|
687
|
-
except InvalidArgument as error:
|
688
|
-
status = Status.error
|
689
|
-
message = get_invalid_regular_expression_message(warnings)
|
690
|
-
if message:
|
691
|
-
# `hypothesis-jsonschema` emits a warning on invalid regular expression syntax
|
692
|
-
result.add_error(InvalidRegularExpression.from_hypothesis_jsonschema_message(message))
|
693
|
-
else:
|
694
|
-
result.add_error(error)
|
695
|
-
except hypothesis.errors.DeadlineExceeded as error:
|
696
|
-
status = Status.error
|
697
|
-
result.add_error(DeadlineExceeded.from_exc(error))
|
698
|
-
except JsonSchemaError as error:
|
699
|
-
status = Status.error
|
700
|
-
result.add_error(InvalidRegularExpression.from_schema_error(error, from_examples=False))
|
701
|
-
except Exception as error:
|
702
|
-
status = Status.error
|
703
|
-
# Likely a YAML parsing issue. E.g. `00:00:00.00` (without quotes) is parsed as float `0.0`
|
704
|
-
if str(error) == "first argument must be string or compiled pattern":
|
705
|
-
result.add_error(
|
706
|
-
InvalidRegularExpression(
|
707
|
-
"Invalid `pattern` value: expected a string. "
|
708
|
-
"If your schema is in YAML, ensure `pattern` values are quoted",
|
709
|
-
is_valid_type=False,
|
710
|
-
)
|
711
|
-
)
|
712
|
-
else:
|
713
|
-
result.add_error(error)
|
714
|
-
if status == Status.success and ctx.no_failfast and any(check.value == Status.failure for check in result.checks):
|
715
|
-
status = Status.failure
|
716
|
-
if has_unsatisfied_example_mark(test):
|
717
|
-
status = Status.error
|
718
|
-
result.add_error(
|
719
|
-
hypothesis.errors.Unsatisfiable("Failed to generate test cases from examples for this API operation")
|
720
|
-
)
|
721
|
-
non_serializable = get_non_serializable_mark(test)
|
722
|
-
if non_serializable is not None and status != Status.error:
|
723
|
-
status = Status.error
|
724
|
-
media_types = ", ".join(non_serializable.media_types)
|
725
|
-
result.add_error(
|
726
|
-
SerializationNotPossible(
|
727
|
-
"Failed to generate test cases from examples for this API operation because of"
|
728
|
-
f" unsupported payload media types: {media_types}\n{SERIALIZERS_SUGGESTION_MESSAGE}",
|
729
|
-
media_types=non_serializable.media_types,
|
730
|
-
)
|
731
|
-
)
|
732
|
-
invalid_regex = get_invalid_regex_mark(test)
|
733
|
-
if invalid_regex is not None and status != Status.error:
|
734
|
-
status = Status.error
|
735
|
-
result.add_error(InvalidRegularExpression.from_schema_error(invalid_regex, from_examples=True))
|
736
|
-
invalid_headers = get_invalid_example_headers_mark(test)
|
737
|
-
if invalid_headers:
|
738
|
-
status = Status.error
|
739
|
-
result.add_error(InvalidHeadersExample.from_headers(invalid_headers))
|
740
|
-
test_elapsed_time = time.monotonic() - test_start_time
|
741
|
-
# DEPRECATED: Seed is the same per test run
|
742
|
-
# Fetch seed value, hypothesis generates it during test execution
|
743
|
-
# It may be `None` if the `derandomize` config option is set to `True`
|
744
|
-
result.seed = getattr(test, "_hypothesis_internal_use_seed", None) or getattr(
|
745
|
-
test, "_hypothesis_internal_use_generated_seed", None
|
746
|
-
)
|
747
|
-
ctx.add_result(result)
|
748
|
-
for status_code in (401, 403):
|
749
|
-
if has_too_many_responses_with_status(result, status_code):
|
750
|
-
ctx.add_warning(TOO_MANY_RESPONSES_WARNING_TEMPLATE.format(f"`{operation.verbose_name}`", status_code))
|
751
|
-
yield events.AfterExecution.from_result(
|
752
|
-
result=result,
|
753
|
-
status=status,
|
754
|
-
elapsed_time=test_elapsed_time,
|
755
|
-
hypothesis_output=hypothesis_output,
|
756
|
-
operation=operation,
|
757
|
-
data_generation_method=data_generation_methods,
|
758
|
-
correlation_id=correlation_id,
|
759
|
-
)
|
760
|
-
|
761
|
-
|
762
|
-
TOO_MANY_RESPONSES_WARNING_TEMPLATE = (
|
763
|
-
"Most of the responses from {} have a {} status code. Did you specify proper API credentials?"
|
764
|
-
)
|
765
|
-
TOO_MANY_RESPONSES_THRESHOLD = 0.9
|
766
|
-
|
767
|
-
|
768
|
-
def has_too_many_responses_with_status(result: TestResult, status_code: int) -> bool:
|
769
|
-
# It is faster than creating an intermediate list
|
770
|
-
unauthorized_count = 0
|
771
|
-
total = 0
|
772
|
-
for check in result.checks:
|
773
|
-
if check.response is not None:
|
774
|
-
if check.response.status_code == status_code:
|
775
|
-
unauthorized_count += 1
|
776
|
-
total += 1
|
777
|
-
if not total:
|
778
|
-
return False
|
779
|
-
return unauthorized_count / total >= TOO_MANY_RESPONSES_THRESHOLD
|
780
|
-
|
781
|
-
|
782
|
-
def setup_hypothesis_database_key(test: Callable, operation: APIOperation) -> None:
|
783
|
-
"""Make Hypothesis use separate database entries for every API operation.
|
784
|
-
|
785
|
-
It increases the effectiveness of the Hypothesis database in the CLI.
|
786
|
-
"""
|
787
|
-
# Hypothesis's function digest depends on the test function signature. To reflect it for the web API case,
|
788
|
-
# we use all API operation parameters in the digest.
|
789
|
-
extra = operation.verbose_name.encode("utf8")
|
790
|
-
for parameter in operation.iter_parameters():
|
791
|
-
extra += parameter.serialize(operation).encode("utf8")
|
792
|
-
test.hypothesis.inner_test._hypothesis_internal_add_digest = extra # type: ignore
|
793
|
-
|
794
|
-
|
795
|
-
def get_invalid_regular_expression_message(warnings: list[WarningMessage]) -> str | None:
|
796
|
-
for warning in warnings:
|
797
|
-
message = str(warning.message)
|
798
|
-
if "is not valid syntax for a Python regular expression" in message:
|
799
|
-
return message
|
800
|
-
return None
|
801
|
-
|
802
|
-
|
803
|
-
MEMORY_ADDRESS_RE = re.compile("0x[0-9a-fA-F]+")
|
804
|
-
URL_IN_ERROR_MESSAGE_RE = re.compile(r"Max retries exceeded with url: .*? \(Caused by")
|
805
|
-
|
806
|
-
|
807
|
-
def add_errors(result: TestResult, errors: list[Exception]) -> None:
|
808
|
-
group_errors(errors)
|
809
|
-
for error in deduplicate_errors(errors):
|
810
|
-
result.add_error(error)
|
811
|
-
|
812
|
-
|
813
|
-
def group_errors(errors: list[Exception]) -> None:
|
814
|
-
"""Group errors of the same kind info a single one, avoiding duplicate error messages."""
|
815
|
-
serialization_errors = [error for error in errors if isinstance(error, SerializationNotPossible)]
|
816
|
-
if len(serialization_errors) > 1:
|
817
|
-
errors[:] = [error for error in errors if not isinstance(error, SerializationNotPossible)]
|
818
|
-
media_types: list[str] = functools.reduce(
|
819
|
-
operator.iadd, (entry.media_types for entry in serialization_errors), []
|
820
|
-
)
|
821
|
-
errors.append(SerializationNotPossible.from_media_types(*media_types))
|
822
|
-
|
823
|
-
|
824
|
-
def canonicalize_error_message(error: Exception, include_traceback: bool = True) -> str:
|
825
|
-
message = format_exception(error, include_traceback)
|
826
|
-
# Replace memory addresses with a fixed string
|
827
|
-
message = MEMORY_ADDRESS_RE.sub("0xbaaaaaaaaaad", message)
|
828
|
-
return URL_IN_ERROR_MESSAGE_RE.sub("", message)
|
829
|
-
|
830
|
-
|
831
|
-
def deduplicate_errors(errors: list[Exception]) -> Generator[Exception, None, None]:
|
832
|
-
"""Deduplicate errors by their messages + tracebacks."""
|
833
|
-
seen = set()
|
834
|
-
for error in errors:
|
835
|
-
message = canonicalize_error_message(error)
|
836
|
-
if message in seen:
|
837
|
-
continue
|
838
|
-
seen.add(message)
|
839
|
-
yield error
|
840
|
-
|
841
|
-
|
842
|
-
def run_checks(
|
843
|
-
*,
|
844
|
-
case: Case,
|
845
|
-
ctx: CheckContext,
|
846
|
-
checks: Iterable[CheckFunction],
|
847
|
-
check_results: list[Check],
|
848
|
-
result: TestResult,
|
849
|
-
response: GenericResponse,
|
850
|
-
elapsed_time: float,
|
851
|
-
max_response_time: int | None = None,
|
852
|
-
no_failfast: bool,
|
853
|
-
) -> None:
|
854
|
-
errors = []
|
855
|
-
|
856
|
-
def add_single_failure(error: AssertionError) -> None:
|
857
|
-
msg = maybe_set_assertion_message(error, check_name)
|
858
|
-
errors.append(error)
|
859
|
-
if isinstance(error, CheckFailed):
|
860
|
-
context = error.context
|
861
|
-
else:
|
862
|
-
context = None
|
863
|
-
check_results.append(result.add_failure(check_name, copied_case, response, elapsed_time, msg, context))
|
864
|
-
|
865
|
-
for check in checks:
|
866
|
-
check_name = check.__name__
|
867
|
-
copied_case = case.partial_deepcopy()
|
868
|
-
try:
|
869
|
-
skip_check = check(ctx, response, copied_case)
|
870
|
-
if not skip_check:
|
871
|
-
check_result = result.add_success(check_name, copied_case, response, elapsed_time)
|
872
|
-
check_results.append(check_result)
|
873
|
-
except AssertionError as exc:
|
874
|
-
add_single_failure(exc)
|
875
|
-
except MultipleFailures as exc:
|
876
|
-
for exception in exc.exceptions:
|
877
|
-
add_single_failure(exception)
|
878
|
-
|
879
|
-
if max_response_time:
|
880
|
-
if elapsed_time > max_response_time:
|
881
|
-
message = _make_max_response_time_failure_message(elapsed_time, max_response_time)
|
882
|
-
errors.append(AssertionError(message))
|
883
|
-
result.add_failure(
|
884
|
-
"max_response_time",
|
885
|
-
case,
|
886
|
-
response,
|
887
|
-
elapsed_time,
|
888
|
-
message,
|
889
|
-
failures.ResponseTimeExceeded(message=message, elapsed=elapsed_time, deadline=max_response_time),
|
890
|
-
)
|
891
|
-
else:
|
892
|
-
result.add_success("max_response_time", case, response, elapsed_time)
|
893
|
-
|
894
|
-
if errors and not no_failfast:
|
895
|
-
raise get_grouped_exception(case.operation.verbose_name, *errors)(causes=tuple(errors))
|
896
|
-
|
897
|
-
|
898
|
-
def run_targets(targets: Iterable[Callable], context: TargetContext) -> None:
|
899
|
-
for target in targets:
|
900
|
-
value = target(context)
|
901
|
-
hypothesis.target(value, label=target.__name__)
|
902
|
-
|
903
|
-
|
904
|
-
def add_cases(case: Case, response: GenericResponse, test: Callable, *args: Any) -> None:
|
905
|
-
context = HookContext(case.operation)
|
906
|
-
for case_hook in get_all_by_name("add_case"):
|
907
|
-
_case = case_hook(context, case.partial_deepcopy(), response)
|
908
|
-
# run additional test if _case is not an empty value
|
909
|
-
if _case:
|
910
|
-
test(_case, *args)
|
911
|
-
|
912
|
-
|
913
|
-
@dataclass
|
914
|
-
class ErrorCollector:
|
915
|
-
"""Collect exceptions that are not related to failed checks.
|
916
|
-
|
917
|
-
Such exceptions may be considered as multiple failures or flakiness by Hypothesis. In both cases, Hypothesis hides
|
918
|
-
exception information that, in our case, is helpful for the end-user. It either indicates errors in user-defined
|
919
|
-
extensions, network-related errors, or internal Schemathesis errors. In all cases, this information is useful for
|
920
|
-
debugging.
|
921
|
-
|
922
|
-
To mitigate this, we gather all exceptions manually via this context manager to avoid interfering with the test
|
923
|
-
function signatures, which are used by Hypothesis.
|
924
|
-
"""
|
925
|
-
|
926
|
-
errors: list[Exception]
|
927
|
-
|
928
|
-
def __enter__(self) -> ErrorCollector:
|
929
|
-
return self
|
930
|
-
|
931
|
-
def __exit__(
|
932
|
-
self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None
|
933
|
-
) -> Literal[False]:
|
934
|
-
# Don't do anything special if:
|
935
|
-
# - Tests are successful
|
936
|
-
# - Checks failed
|
937
|
-
# - The testing process is interrupted
|
938
|
-
if not exc_type or issubclass(exc_type, CheckFailed) or not issubclass(exc_type, Exception):
|
939
|
-
return False
|
940
|
-
# These exceptions are needed for control flow on the Hypothesis side. E.g. rejecting unsatisfiable examples
|
941
|
-
if isinstance(exc_val, HypothesisException):
|
942
|
-
raise
|
943
|
-
# Exception value is not `None` and is a subclass of `Exception` at this point
|
944
|
-
exc_val = cast(Exception, exc_val)
|
945
|
-
self.errors.append(exc_val.with_traceback(exc_tb))
|
946
|
-
raise NonCheckError from None
|
947
|
-
|
948
|
-
|
949
|
-
def _force_data_generation_method(values: list[DataGenerationMethod], case: Case) -> None:
|
950
|
-
# Set data generation method to the one that actually used
|
951
|
-
data_generation_method = cast(DataGenerationMethod, case.data_generation_method)
|
952
|
-
values[:] = [data_generation_method]
|
953
|
-
|
954
|
-
|
955
|
-
def cached_test_func(f: Callable) -> Callable:
|
956
|
-
def wrapped(*, ctx: RunnerContext, case: Case, **kwargs: Any) -> None:
|
957
|
-
if ctx.unique_data:
|
958
|
-
cached = ctx.get_cached_outcome(case)
|
959
|
-
if isinstance(cached, BaseException):
|
960
|
-
raise cached
|
961
|
-
elif cached is None:
|
962
|
-
return None
|
963
|
-
try:
|
964
|
-
f(ctx=ctx, case=case, **kwargs)
|
965
|
-
except BaseException as exc:
|
966
|
-
ctx.cache_outcome(case, exc)
|
967
|
-
raise
|
968
|
-
else:
|
969
|
-
ctx.cache_outcome(case, None)
|
970
|
-
else:
|
971
|
-
f(ctx=ctx, case=case, **kwargs)
|
972
|
-
|
973
|
-
wrapped.__name__ = f.__name__
|
974
|
-
|
975
|
-
return wrapped
|
976
|
-
|
977
|
-
|
978
|
-
@cached_test_func
|
979
|
-
def network_test(
|
980
|
-
*,
|
981
|
-
ctx: RunnerContext,
|
982
|
-
case: Case,
|
983
|
-
checks: Iterable[CheckFunction],
|
984
|
-
targets: Iterable[Target],
|
985
|
-
result: TestResult,
|
986
|
-
session: requests.Session,
|
987
|
-
request_config: RequestConfig,
|
988
|
-
store_interactions: bool,
|
989
|
-
headers: dict[str, Any] | None,
|
990
|
-
feedback: Feedback | None,
|
991
|
-
max_response_time: int | None,
|
992
|
-
data_generation_methods: list[DataGenerationMethod],
|
993
|
-
dry_run: bool,
|
994
|
-
errors: list[Exception],
|
995
|
-
) -> None:
|
996
|
-
"""A single test body will be executed against the target."""
|
997
|
-
with ErrorCollector(errors):
|
998
|
-
_force_data_generation_method(data_generation_methods, case)
|
999
|
-
result.mark_executed()
|
1000
|
-
headers = headers or {}
|
1001
|
-
if "user-agent" not in {header.lower() for header in headers}:
|
1002
|
-
headers["User-Agent"] = USER_AGENT
|
1003
|
-
if not dry_run:
|
1004
|
-
args = (
|
1005
|
-
ctx,
|
1006
|
-
checks,
|
1007
|
-
targets,
|
1008
|
-
result,
|
1009
|
-
session,
|
1010
|
-
request_config,
|
1011
|
-
store_interactions,
|
1012
|
-
headers,
|
1013
|
-
feedback,
|
1014
|
-
max_response_time,
|
1015
|
-
)
|
1016
|
-
response = _network_test(case, *args)
|
1017
|
-
add_cases(case, response, _network_test, *args)
|
1018
|
-
elif store_interactions:
|
1019
|
-
result.store_requests_response(case, None, Status.skip, [], headers=headers, session=session)
|
1020
|
-
|
1021
|
-
|
1022
|
-
def _network_test(
|
1023
|
-
case: Case,
|
1024
|
-
ctx: RunnerContext,
|
1025
|
-
checks: Iterable[CheckFunction],
|
1026
|
-
targets: Iterable[Target],
|
1027
|
-
result: TestResult,
|
1028
|
-
session: requests.Session,
|
1029
|
-
request_config: RequestConfig,
|
1030
|
-
store_interactions: bool,
|
1031
|
-
headers: dict[str, Any] | None,
|
1032
|
-
feedback: Feedback | None,
|
1033
|
-
max_response_time: int | None,
|
1034
|
-
) -> requests.Response:
|
1035
|
-
check_results: list[Check] = []
|
1036
|
-
hook_context = HookContext(operation=case.operation)
|
1037
|
-
kwargs: dict[str, Any] = {
|
1038
|
-
"session": session,
|
1039
|
-
"headers": headers,
|
1040
|
-
"timeout": request_config.prepared_timeout,
|
1041
|
-
"verify": request_config.tls_verify,
|
1042
|
-
"cert": request_config.cert,
|
1043
|
-
}
|
1044
|
-
if request_config.proxy is not None:
|
1045
|
-
kwargs["proxies"] = {"all": request_config.proxy}
|
1046
|
-
hooks.dispatch("process_call_kwargs", hook_context, case, kwargs)
|
1047
|
-
try:
|
1048
|
-
response = case.call(**kwargs)
|
1049
|
-
except CheckFailed as exc:
|
1050
|
-
check_name = "request_timeout"
|
1051
|
-
requests_kwargs = RequestsTransport().serialize_case(case, base_url=case.get_full_base_url(), headers=headers)
|
1052
|
-
request = requests.Request(**requests_kwargs).prepare()
|
1053
|
-
elapsed = cast(
|
1054
|
-
float, request_config.prepared_timeout
|
1055
|
-
) # It is defined and not empty, since the exception happened
|
1056
|
-
check_result = result.add_failure(
|
1057
|
-
check_name, case, None, elapsed, f"Response timed out after {1000 * elapsed:.2f}ms", exc.context, request
|
1058
|
-
)
|
1059
|
-
check_results.append(check_result)
|
1060
|
-
if store_interactions:
|
1061
|
-
result.store_requests_response(case, None, Status.failure, [check_result], headers=headers, session=session)
|
1062
|
-
raise exc
|
1063
|
-
context = TargetContext(case=case, response=response, response_time=response.elapsed.total_seconds())
|
1064
|
-
run_targets(targets, context)
|
1065
|
-
status = Status.success
|
1066
|
-
|
1067
|
-
check_ctx = CheckContext(
|
1068
|
-
override=ctx.override,
|
1069
|
-
auth=ctx.auth,
|
1070
|
-
headers=CaseInsensitiveDict(headers) if headers else None,
|
1071
|
-
config=ctx.checks_config,
|
1072
|
-
transport_kwargs=kwargs,
|
1073
|
-
)
|
1074
|
-
try:
|
1075
|
-
run_checks(
|
1076
|
-
case=case,
|
1077
|
-
ctx=check_ctx,
|
1078
|
-
checks=checks,
|
1079
|
-
check_results=check_results,
|
1080
|
-
result=result,
|
1081
|
-
response=response,
|
1082
|
-
elapsed_time=context.response_time * 1000,
|
1083
|
-
max_response_time=max_response_time,
|
1084
|
-
no_failfast=ctx.no_failfast,
|
1085
|
-
)
|
1086
|
-
except CheckFailed:
|
1087
|
-
status = Status.failure
|
1088
|
-
raise
|
1089
|
-
finally:
|
1090
|
-
if feedback is not None:
|
1091
|
-
feedback.add_test_case(case, response)
|
1092
|
-
if store_interactions:
|
1093
|
-
result.store_requests_response(case, response, status, check_results, headers=headers, session=session)
|
1094
|
-
return response
|
1095
|
-
|
1096
|
-
|
1097
|
-
@contextmanager
|
1098
|
-
def get_session(auth: HTTPDigestAuth | RawAuth | None = None) -> Generator[requests.Session, None, None]:
|
1099
|
-
with requests.Session() as session:
|
1100
|
-
if auth is not None:
|
1101
|
-
session.auth = auth
|
1102
|
-
yield session
|
1103
|
-
|
1104
|
-
|
1105
|
-
@cached_test_func
|
1106
|
-
def wsgi_test(
|
1107
|
-
ctx: RunnerContext,
|
1108
|
-
case: Case,
|
1109
|
-
checks: Iterable[CheckFunction],
|
1110
|
-
targets: Iterable[Target],
|
1111
|
-
result: TestResult,
|
1112
|
-
auth: RawAuth | None,
|
1113
|
-
auth_type: str | None,
|
1114
|
-
headers: dict[str, Any] | None,
|
1115
|
-
store_interactions: bool,
|
1116
|
-
feedback: Feedback | None,
|
1117
|
-
max_response_time: int | None,
|
1118
|
-
data_generation_methods: list[DataGenerationMethod],
|
1119
|
-
dry_run: bool,
|
1120
|
-
errors: list[Exception],
|
1121
|
-
) -> None:
|
1122
|
-
with ErrorCollector(errors):
|
1123
|
-
_force_data_generation_method(data_generation_methods, case)
|
1124
|
-
result.mark_executed()
|
1125
|
-
headers = prepare_wsgi_headers(headers, auth, auth_type)
|
1126
|
-
if not dry_run:
|
1127
|
-
args = (
|
1128
|
-
ctx,
|
1129
|
-
checks,
|
1130
|
-
targets,
|
1131
|
-
result,
|
1132
|
-
headers,
|
1133
|
-
store_interactions,
|
1134
|
-
feedback,
|
1135
|
-
max_response_time,
|
1136
|
-
)
|
1137
|
-
response = _wsgi_test(case, *args)
|
1138
|
-
add_cases(case, response, _wsgi_test, *args)
|
1139
|
-
elif store_interactions:
|
1140
|
-
result.store_wsgi_response(case, None, headers, None, Status.skip, [])
|
1141
|
-
|
1142
|
-
|
1143
|
-
def _wsgi_test(
|
1144
|
-
case: Case,
|
1145
|
-
ctx: RunnerContext,
|
1146
|
-
checks: Iterable[CheckFunction],
|
1147
|
-
targets: Iterable[Target],
|
1148
|
-
result: TestResult,
|
1149
|
-
headers: dict[str, Any],
|
1150
|
-
store_interactions: bool,
|
1151
|
-
feedback: Feedback | None,
|
1152
|
-
max_response_time: int | None,
|
1153
|
-
) -> WSGIResponse:
|
1154
|
-
from ...transports.responses import WSGIResponse
|
1155
|
-
|
1156
|
-
with catching_logs(LogCaptureHandler(), level=logging.DEBUG) as recorded:
|
1157
|
-
hook_context = HookContext(operation=case.operation)
|
1158
|
-
kwargs: dict[str, Any] = {"headers": headers}
|
1159
|
-
hooks.dispatch("process_call_kwargs", hook_context, case, kwargs)
|
1160
|
-
response = cast(WSGIResponse, case.call(**kwargs))
|
1161
|
-
context = TargetContext(case=case, response=response, response_time=response.elapsed.total_seconds())
|
1162
|
-
run_targets(targets, context)
|
1163
|
-
result.logs.extend(recorded.records)
|
1164
|
-
status = Status.success
|
1165
|
-
check_results: list[Check] = []
|
1166
|
-
check_ctx = CheckContext(
|
1167
|
-
override=ctx.override,
|
1168
|
-
auth=ctx.auth,
|
1169
|
-
headers=CaseInsensitiveDict(headers) if headers else None,
|
1170
|
-
config=ctx.checks_config,
|
1171
|
-
transport_kwargs=kwargs,
|
1172
|
-
)
|
1173
|
-
try:
|
1174
|
-
run_checks(
|
1175
|
-
case=case,
|
1176
|
-
ctx=check_ctx,
|
1177
|
-
checks=checks,
|
1178
|
-
check_results=check_results,
|
1179
|
-
result=result,
|
1180
|
-
response=response,
|
1181
|
-
elapsed_time=context.response_time * 1000,
|
1182
|
-
max_response_time=max_response_time,
|
1183
|
-
no_failfast=ctx.no_failfast,
|
1184
|
-
)
|
1185
|
-
except CheckFailed:
|
1186
|
-
status = Status.failure
|
1187
|
-
raise
|
1188
|
-
finally:
|
1189
|
-
if feedback is not None:
|
1190
|
-
feedback.add_test_case(case, response)
|
1191
|
-
if store_interactions:
|
1192
|
-
result.store_wsgi_response(case, response, headers, response.elapsed.total_seconds(), status, check_results)
|
1193
|
-
return response
|
1194
|
-
|
1195
|
-
|
1196
|
-
@cached_test_func
|
1197
|
-
def asgi_test(
|
1198
|
-
ctx: RunnerContext,
|
1199
|
-
case: Case,
|
1200
|
-
checks: Iterable[CheckFunction],
|
1201
|
-
targets: Iterable[Target],
|
1202
|
-
result: TestResult,
|
1203
|
-
store_interactions: bool,
|
1204
|
-
headers: dict[str, Any] | None,
|
1205
|
-
feedback: Feedback | None,
|
1206
|
-
max_response_time: int | None,
|
1207
|
-
data_generation_methods: list[DataGenerationMethod],
|
1208
|
-
dry_run: bool,
|
1209
|
-
errors: list[Exception],
|
1210
|
-
) -> None:
|
1211
|
-
"""A single test body will be executed against the target."""
|
1212
|
-
with ErrorCollector(errors):
|
1213
|
-
_force_data_generation_method(data_generation_methods, case)
|
1214
|
-
result.mark_executed()
|
1215
|
-
headers = headers or {}
|
1216
|
-
|
1217
|
-
if not dry_run:
|
1218
|
-
args = (
|
1219
|
-
ctx,
|
1220
|
-
checks,
|
1221
|
-
targets,
|
1222
|
-
result,
|
1223
|
-
store_interactions,
|
1224
|
-
headers,
|
1225
|
-
feedback,
|
1226
|
-
max_response_time,
|
1227
|
-
)
|
1228
|
-
response = _asgi_test(case, *args)
|
1229
|
-
add_cases(case, response, _asgi_test, *args)
|
1230
|
-
elif store_interactions:
|
1231
|
-
result.store_requests_response(case, None, Status.skip, [], headers=headers, session=None)
|
1232
|
-
|
1233
|
-
|
1234
|
-
def _asgi_test(
|
1235
|
-
case: Case,
|
1236
|
-
ctx: RunnerContext,
|
1237
|
-
checks: Iterable[CheckFunction],
|
1238
|
-
targets: Iterable[Target],
|
1239
|
-
result: TestResult,
|
1240
|
-
store_interactions: bool,
|
1241
|
-
headers: dict[str, Any] | None,
|
1242
|
-
feedback: Feedback | None,
|
1243
|
-
max_response_time: int | None,
|
1244
|
-
) -> requests.Response:
|
1245
|
-
hook_context = HookContext(operation=case.operation)
|
1246
|
-
kwargs: dict[str, Any] = {"headers": headers}
|
1247
|
-
hooks.dispatch("process_call_kwargs", hook_context, case, kwargs)
|
1248
|
-
response = case.call(**kwargs)
|
1249
|
-
context = TargetContext(case=case, response=response, response_time=response.elapsed.total_seconds())
|
1250
|
-
run_targets(targets, context)
|
1251
|
-
status = Status.success
|
1252
|
-
check_results: list[Check] = []
|
1253
|
-
check_ctx = CheckContext(
|
1254
|
-
override=ctx.override,
|
1255
|
-
auth=ctx.auth,
|
1256
|
-
headers=CaseInsensitiveDict(headers) if headers else None,
|
1257
|
-
config=ctx.checks_config,
|
1258
|
-
transport_kwargs=kwargs,
|
1259
|
-
)
|
1260
|
-
try:
|
1261
|
-
run_checks(
|
1262
|
-
case=case,
|
1263
|
-
ctx=check_ctx,
|
1264
|
-
checks=checks,
|
1265
|
-
check_results=check_results,
|
1266
|
-
result=result,
|
1267
|
-
response=response,
|
1268
|
-
elapsed_time=context.response_time * 1000,
|
1269
|
-
max_response_time=max_response_time,
|
1270
|
-
no_failfast=ctx.no_failfast,
|
1271
|
-
)
|
1272
|
-
except CheckFailed:
|
1273
|
-
status = Status.failure
|
1274
|
-
raise
|
1275
|
-
finally:
|
1276
|
-
if feedback is not None:
|
1277
|
-
feedback.add_test_case(case, response)
|
1278
|
-
if store_interactions:
|
1279
|
-
result.store_requests_response(case, response, status, check_results, headers, session=None)
|
1280
|
-
return response
|