schemathesis 3.13.0__py3-none-any.whl → 4.4.2__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 +53 -25
- schemathesis/auths.py +507 -0
- schemathesis/checks.py +190 -25
- schemathesis/cli/__init__.py +27 -1016
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +133 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +602 -0
- schemathesis/cli/commands/run/context.py +228 -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 +45 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
- schemathesis/cli/commands/run/handlers/output.py +1750 -0
- schemathesis/cli/commands/run/loaders.py +118 -0
- schemathesis/cli/commands/run/validation.py +256 -0
- schemathesis/cli/constants.py +5 -0
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +203 -0
- schemathesis/cli/ext/options.py +81 -0
- schemathesis/config/__init__.py +202 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +101 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +163 -0
- schemathesis/config/_generation.py +157 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +335 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +253 -0
- schemathesis/config/_projects.py +543 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +89 -0
- schemathesis/config/schema.json +975 -0
- schemathesis/core/__init__.py +72 -0
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +100 -0
- schemathesis/core/deserialization.py +210 -0
- schemathesis/core/errors.py +588 -0
- schemathesis/core/failures.py +316 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +183 -0
- schemathesis/core/jsonschema/keywords.py +40 -0
- schemathesis/core/jsonschema/references.py +222 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/lazy_import.py +15 -0
- schemathesis/core/loaders.py +107 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/core/media_types.py +79 -0
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/core/parameters.py +45 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +34 -0
- schemathesis/core/result.py +27 -0
- schemathesis/core/schema_analysis.py +17 -0
- schemathesis/core/shell.py +203 -0
- schemathesis/core/transforms.py +144 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +73 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +152 -0
- schemathesis/engine/control.py +44 -0
- schemathesis/engine/core.py +201 -0
- schemathesis/engine/errors.py +446 -0
- schemathesis/engine/events.py +284 -0
- schemathesis/engine/observations.py +42 -0
- schemathesis/engine/phases/__init__.py +108 -0
- schemathesis/engine/phases/analysis.py +28 -0
- schemathesis/engine/phases/probes.py +172 -0
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +364 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +220 -0
- schemathesis/engine/phases/unit/_executor.py +459 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +254 -0
- schemathesis/errors.py +47 -0
- schemathesis/filters.py +395 -0
- schemathesis/generation/__init__.py +25 -0
- schemathesis/generation/case.py +478 -0
- schemathesis/generation/coverage.py +1528 -0
- schemathesis/generation/hypothesis/__init__.py +121 -0
- schemathesis/generation/hypothesis/builder.py +992 -0
- schemathesis/generation/hypothesis/examples.py +56 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +285 -0
- schemathesis/generation/meta.py +227 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +127 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +294 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +285 -0
- schemathesis/hooks.py +270 -91
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +467 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +315 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +341 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/pytest/plugin.py +357 -0
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +683 -247
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/nodes.py +27 -0
- schemathesis/specs/graphql/scalars.py +86 -0
- schemathesis/specs/graphql/schemas.py +395 -123
- schemathesis/specs/graphql/validation.py +33 -0
- schemathesis/specs/openapi/__init__.py +9 -1
- schemathesis/specs/openapi/_hypothesis.py +578 -317
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +729 -0
- schemathesis/specs/openapi/adapter/protocol.py +59 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +368 -0
- schemathesis/specs/openapi/adapter/security.py +144 -0
- schemathesis/specs/openapi/adapter/v2.py +30 -0
- schemathesis/specs/openapi/adapter/v3_0.py +30 -0
- schemathesis/specs/openapi/adapter/v3_1.py +30 -0
- schemathesis/specs/openapi/analysis.py +96 -0
- schemathesis/specs/openapi/checks.py +753 -74
- schemathesis/specs/openapi/converter.py +176 -37
- schemathesis/specs/openapi/definitions.py +599 -4
- schemathesis/specs/openapi/examples.py +581 -165
- schemathesis/specs/openapi/expressions/__init__.py +52 -5
- schemathesis/specs/openapi/expressions/extractors.py +25 -0
- schemathesis/specs/openapi/expressions/lexer.py +34 -31
- schemathesis/specs/openapi/expressions/nodes.py +97 -46
- schemathesis/specs/openapi/expressions/parser.py +35 -13
- schemathesis/specs/openapi/formats.py +122 -0
- schemathesis/specs/openapi/media_types.py +75 -0
- schemathesis/specs/openapi/negative/__init__.py +117 -68
- schemathesis/specs/openapi/negative/mutations.py +294 -104
- schemathesis/specs/openapi/negative/utils.py +3 -6
- schemathesis/specs/openapi/patterns.py +458 -0
- schemathesis/specs/openapi/references.py +60 -81
- schemathesis/specs/openapi/schemas.py +648 -650
- schemathesis/specs/openapi/serialization.py +53 -30
- schemathesis/specs/openapi/stateful/__init__.py +404 -69
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
- schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
- schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
- schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
- schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
- schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
- schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
- schemathesis/specs/openapi/stateful/inference.py +254 -0
- schemathesis/specs/openapi/stateful/links.py +219 -78
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/common.py +23 -0
- schemathesis/specs/openapi/types/v2.py +129 -0
- schemathesis/specs/openapi/types/v3.py +134 -0
- schemathesis/specs/openapi/utils.py +7 -6
- schemathesis/specs/openapi/warnings.py +75 -0
- schemathesis/transport/__init__.py +224 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +126 -0
- schemathesis/transport/requests.py +278 -0
- schemathesis/transport/serialization.py +329 -0
- schemathesis/transport/wsgi.py +175 -0
- schemathesis-4.4.2.dist-info/METADATA +213 -0
- schemathesis-4.4.2.dist-info/RECORD +192 -0
- {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
- schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
- {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
- schemathesis/_compat.py +0 -41
- schemathesis/_hypothesis.py +0 -115
- schemathesis/cli/callbacks.py +0 -188
- schemathesis/cli/cassettes.py +0 -253
- schemathesis/cli/context.py +0 -36
- schemathesis/cli/debug.py +0 -21
- schemathesis/cli/handlers.py +0 -11
- schemathesis/cli/junitxml.py +0 -41
- schemathesis/cli/options.py +0 -51
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -508
- schemathesis/cli/output/short.py +0 -40
- schemathesis/constants.py +0 -79
- schemathesis/exceptions.py +0 -207
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -16
- schemathesis/extra/pytest_plugin.py +0 -216
- schemathesis/failures.py +0 -131
- schemathesis/fixups/__init__.py +0 -29
- schemathesis/fixups/fast_api.py +0 -30
- schemathesis/lazy.py +0 -227
- schemathesis/models.py +0 -1041
- schemathesis/parameters.py +0 -88
- schemathesis/runner/__init__.py +0 -460
- schemathesis/runner/events.py +0 -240
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -755
- schemathesis/runner/impl/solo.py +0 -85
- schemathesis/runner/impl/threadpool.py +0 -367
- schemathesis/runner/serialization.py +0 -189
- schemathesis/serializers.py +0 -233
- schemathesis/service/__init__.py +0 -3
- schemathesis/service/client.py +0 -46
- schemathesis/service/constants.py +0 -12
- schemathesis/service/events.py +0 -39
- schemathesis/service/handler.py +0 -39
- schemathesis/service/models.py +0 -7
- schemathesis/service/serialization.py +0 -153
- schemathesis/service/worker.py +0 -40
- schemathesis/specs/graphql/loaders.py +0 -215
- schemathesis/specs/openapi/constants.py +0 -7
- schemathesis/specs/openapi/expressions/context.py +0 -12
- schemathesis/specs/openapi/expressions/pointers.py +0 -29
- schemathesis/specs/openapi/filters.py +0 -44
- schemathesis/specs/openapi/links.py +0 -302
- schemathesis/specs/openapi/loaders.py +0 -453
- schemathesis/specs/openapi/parameters.py +0 -413
- schemathesis/specs/openapi/security.py +0 -129
- schemathesis/specs/openapi/validation.py +0 -24
- schemathesis/stateful.py +0 -349
- schemathesis/targets.py +0 -32
- schemathesis/types.py +0 -38
- schemathesis/utils.py +0 -436
- schemathesis-3.13.0.dist-info/METADATA +0 -202
- schemathesis-3.13.0.dist-info/RECORD +0 -91
- schemathesis-3.13.0.dist-info/entry_points.txt +0 -6
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
schemathesis/runner/impl/core.py
DELETED
|
@@ -1,755 +0,0 @@
|
|
|
1
|
-
# pylint: disable=too-many-statements,too-many-branches
|
|
2
|
-
import logging
|
|
3
|
-
import threading
|
|
4
|
-
import time
|
|
5
|
-
import uuid
|
|
6
|
-
from contextlib import contextmanager
|
|
7
|
-
from types import TracebackType
|
|
8
|
-
from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Type, Union, cast
|
|
9
|
-
from warnings import WarningMessage, catch_warnings
|
|
10
|
-
|
|
11
|
-
import attr
|
|
12
|
-
import hypothesis
|
|
13
|
-
import requests
|
|
14
|
-
from _pytest.logging import LogCaptureHandler, catching_logs
|
|
15
|
-
from hypothesis.errors import HypothesisException, InvalidArgument
|
|
16
|
-
from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
|
|
17
|
-
from requests.auth import HTTPDigestAuth, _basic_auth_str
|
|
18
|
-
|
|
19
|
-
from ... import failures, hooks
|
|
20
|
-
from ...constants import (
|
|
21
|
-
DEFAULT_STATEFUL_RECURSION_LIMIT,
|
|
22
|
-
RECURSIVE_REFERENCE_ERROR_MESSAGE,
|
|
23
|
-
USER_AGENT,
|
|
24
|
-
DataGenerationMethod,
|
|
25
|
-
)
|
|
26
|
-
from ...exceptions import (
|
|
27
|
-
CheckFailed,
|
|
28
|
-
DeadlineExceeded,
|
|
29
|
-
InvalidRegularExpression,
|
|
30
|
-
InvalidSchema,
|
|
31
|
-
NonCheckError,
|
|
32
|
-
get_grouped_exception,
|
|
33
|
-
)
|
|
34
|
-
from ...hooks import HookContext, get_all_by_name
|
|
35
|
-
from ...models import APIOperation, Case, Check, CheckFunction, Status, TestResult, TestResultSet
|
|
36
|
-
from ...runner import events
|
|
37
|
-
from ...schemas import BaseSchema
|
|
38
|
-
from ...stateful import Feedback, Stateful
|
|
39
|
-
from ...targets import Target, TargetContext
|
|
40
|
-
from ...types import RawAuth, RequestCert
|
|
41
|
-
from ...utils import (
|
|
42
|
-
GenericResponse,
|
|
43
|
-
Ok,
|
|
44
|
-
WSGIResponse,
|
|
45
|
-
capture_hypothesis_output,
|
|
46
|
-
format_exception,
|
|
47
|
-
maybe_set_assertion_message,
|
|
48
|
-
)
|
|
49
|
-
from ..serialization import SerializedTestResult
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@attr.s # pragma: no mutate
|
|
53
|
-
class BaseRunner:
|
|
54
|
-
schema: BaseSchema = attr.ib() # pragma: no mutate
|
|
55
|
-
checks: Iterable[CheckFunction] = attr.ib() # pragma: no mutate
|
|
56
|
-
max_response_time: Optional[int] = attr.ib() # pragma: no mutate
|
|
57
|
-
targets: Iterable[Target] = attr.ib() # pragma: no mutate
|
|
58
|
-
hypothesis_settings: hypothesis.settings = attr.ib() # pragma: no mutate
|
|
59
|
-
auth: Optional[RawAuth] = attr.ib(default=None) # pragma: no mutate
|
|
60
|
-
auth_type: Optional[str] = attr.ib(default=None) # pragma: no mutate
|
|
61
|
-
headers: Optional[Dict[str, Any]] = attr.ib(default=None) # pragma: no mutate
|
|
62
|
-
request_timeout: Optional[int] = attr.ib(default=None) # pragma: no mutate
|
|
63
|
-
store_interactions: bool = attr.ib(default=False) # pragma: no mutate
|
|
64
|
-
seed: Optional[int] = attr.ib(default=None) # pragma: no mutate
|
|
65
|
-
exit_first: bool = attr.ib(default=False) # pragma: no mutate
|
|
66
|
-
dry_run: bool = attr.ib(default=False) # pragma: no mutate
|
|
67
|
-
stateful: Optional[Stateful] = attr.ib(default=None) # pragma: no mutate
|
|
68
|
-
stateful_recursion_limit: int = attr.ib(default=DEFAULT_STATEFUL_RECURSION_LIMIT) # pragma: no mutate
|
|
69
|
-
count_operations: bool = attr.ib(default=True) # pragma: no mutate
|
|
70
|
-
|
|
71
|
-
def execute(self) -> "EventStream":
|
|
72
|
-
"""Common logic for all runners."""
|
|
73
|
-
event = threading.Event()
|
|
74
|
-
return EventStream(self._generate_events(event), event)
|
|
75
|
-
|
|
76
|
-
def _generate_events(self, stop_event: threading.Event) -> Generator[events.ExecutionEvent, None, None]:
|
|
77
|
-
results = TestResultSet()
|
|
78
|
-
|
|
79
|
-
initialized = events.Initialized.from_schema(schema=self.schema, count_operations=self.count_operations)
|
|
80
|
-
|
|
81
|
-
def _finish() -> events.Finished:
|
|
82
|
-
return events.Finished.from_results(results=results, running_time=time.monotonic() - initialized.start_time)
|
|
83
|
-
|
|
84
|
-
if stop_event.is_set():
|
|
85
|
-
yield _finish()
|
|
86
|
-
return
|
|
87
|
-
|
|
88
|
-
yield initialized
|
|
89
|
-
|
|
90
|
-
if stop_event.is_set():
|
|
91
|
-
yield _finish()
|
|
92
|
-
return
|
|
93
|
-
|
|
94
|
-
try:
|
|
95
|
-
for event in self._execute(results, stop_event):
|
|
96
|
-
yield event
|
|
97
|
-
except KeyboardInterrupt:
|
|
98
|
-
yield events.Interrupted()
|
|
99
|
-
|
|
100
|
-
yield _finish()
|
|
101
|
-
|
|
102
|
-
def _should_stop(self, event: events.ExecutionEvent) -> bool:
|
|
103
|
-
return (
|
|
104
|
-
self.exit_first
|
|
105
|
-
and isinstance(event, events.AfterExecution)
|
|
106
|
-
and event.status in (Status.error, Status.failure)
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
def _execute(
|
|
110
|
-
self, results: TestResultSet, stop_event: threading.Event
|
|
111
|
-
) -> Generator[events.ExecutionEvent, None, None]:
|
|
112
|
-
raise NotImplementedError
|
|
113
|
-
|
|
114
|
-
def _run_tests(
|
|
115
|
-
self,
|
|
116
|
-
maker: Callable,
|
|
117
|
-
template: Callable,
|
|
118
|
-
settings: hypothesis.settings,
|
|
119
|
-
seed: Optional[int],
|
|
120
|
-
results: TestResultSet,
|
|
121
|
-
recursion_level: int = 0,
|
|
122
|
-
**kwargs: Any,
|
|
123
|
-
) -> Generator[events.ExecutionEvent, None, None]:
|
|
124
|
-
"""Run tests and recursively run additional tests."""
|
|
125
|
-
if recursion_level > self.stateful_recursion_limit:
|
|
126
|
-
return
|
|
127
|
-
for result, data_generation_method in maker(template, settings, seed):
|
|
128
|
-
if isinstance(result, Ok):
|
|
129
|
-
operation, test = result.ok()
|
|
130
|
-
feedback = Feedback(self.stateful, operation)
|
|
131
|
-
for event in run_test(
|
|
132
|
-
operation,
|
|
133
|
-
test,
|
|
134
|
-
results=results,
|
|
135
|
-
feedback=feedback,
|
|
136
|
-
recursion_level=recursion_level,
|
|
137
|
-
data_generation_method=data_generation_method,
|
|
138
|
-
**kwargs,
|
|
139
|
-
):
|
|
140
|
-
yield event
|
|
141
|
-
if isinstance(event, events.Interrupted):
|
|
142
|
-
return
|
|
143
|
-
# Additional tests, generated via the `feedback` instance
|
|
144
|
-
yield from self._run_tests(
|
|
145
|
-
feedback.get_stateful_tests,
|
|
146
|
-
template,
|
|
147
|
-
settings,
|
|
148
|
-
seed,
|
|
149
|
-
recursion_level=recursion_level + 1,
|
|
150
|
-
results=results,
|
|
151
|
-
**kwargs,
|
|
152
|
-
)
|
|
153
|
-
else:
|
|
154
|
-
# Schema errors
|
|
155
|
-
yield from handle_schema_error(result.err(), results, data_generation_method, recursion_level)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
159
|
-
class EventStream:
|
|
160
|
-
"""Schemathesis event stream.
|
|
161
|
-
|
|
162
|
-
Provides an API to control the execution flow.
|
|
163
|
-
"""
|
|
164
|
-
|
|
165
|
-
generator: Generator[events.ExecutionEvent, None, None] = attr.ib() # pragma: no mutate
|
|
166
|
-
stop_event: threading.Event = attr.ib() # pragma: no mutate
|
|
167
|
-
|
|
168
|
-
def __next__(self) -> events.ExecutionEvent:
|
|
169
|
-
return next(self.generator)
|
|
170
|
-
|
|
171
|
-
def __iter__(self) -> Generator[events.ExecutionEvent, None, None]:
|
|
172
|
-
return self.generator
|
|
173
|
-
|
|
174
|
-
def stop(self) -> None:
|
|
175
|
-
"""Stop the event stream.
|
|
176
|
-
|
|
177
|
-
Its next value will be the last one (Finished).
|
|
178
|
-
"""
|
|
179
|
-
self.stop_event.set()
|
|
180
|
-
|
|
181
|
-
def finish(self) -> events.ExecutionEvent:
|
|
182
|
-
"""Stop the event stream & return the last event."""
|
|
183
|
-
self.stop()
|
|
184
|
-
return next(self)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
def handle_schema_error(
|
|
188
|
-
error: InvalidSchema, results: TestResultSet, data_generation_method: DataGenerationMethod, recursion_level: int
|
|
189
|
-
) -> Generator[events.ExecutionEvent, None, None]:
|
|
190
|
-
if error.method is not None:
|
|
191
|
-
assert error.path is not None
|
|
192
|
-
assert error.full_path is not None
|
|
193
|
-
method = error.method.upper()
|
|
194
|
-
verbose_name = f"{method} {error.path}"
|
|
195
|
-
result = TestResult(
|
|
196
|
-
method=method,
|
|
197
|
-
path=error.full_path,
|
|
198
|
-
verbose_name=verbose_name,
|
|
199
|
-
data_generation_method=data_generation_method,
|
|
200
|
-
)
|
|
201
|
-
result.add_error(error)
|
|
202
|
-
correlation_id = uuid.uuid4().hex
|
|
203
|
-
yield events.BeforeExecution(
|
|
204
|
-
method=method,
|
|
205
|
-
path=error.full_path,
|
|
206
|
-
verbose_name=verbose_name,
|
|
207
|
-
relative_path=error.path,
|
|
208
|
-
recursion_level=recursion_level,
|
|
209
|
-
data_generation_method=data_generation_method,
|
|
210
|
-
correlation_id=correlation_id,
|
|
211
|
-
)
|
|
212
|
-
yield events.AfterExecution(
|
|
213
|
-
method=method,
|
|
214
|
-
path=error.full_path,
|
|
215
|
-
relative_path=error.path,
|
|
216
|
-
verbose_name=verbose_name,
|
|
217
|
-
status=Status.error,
|
|
218
|
-
result=SerializedTestResult.from_test_result(result),
|
|
219
|
-
data_generation_method=data_generation_method,
|
|
220
|
-
elapsed_time=0.0,
|
|
221
|
-
hypothesis_output=[],
|
|
222
|
-
correlation_id=correlation_id,
|
|
223
|
-
)
|
|
224
|
-
results.append(result)
|
|
225
|
-
else:
|
|
226
|
-
# When there is no `method`, then the schema error may cover multiple operations and we can't display it in
|
|
227
|
-
# the progress bar
|
|
228
|
-
results.generic_errors.append(error)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
def run_test( # pylint: disable=too-many-locals
|
|
232
|
-
operation: APIOperation,
|
|
233
|
-
test: Callable,
|
|
234
|
-
checks: Iterable[CheckFunction],
|
|
235
|
-
data_generation_method: DataGenerationMethod,
|
|
236
|
-
targets: Iterable[Target],
|
|
237
|
-
results: TestResultSet,
|
|
238
|
-
headers: Optional[Dict[str, Any]],
|
|
239
|
-
recursion_level: int,
|
|
240
|
-
**kwargs: Any,
|
|
241
|
-
) -> Generator[events.ExecutionEvent, None, None]:
|
|
242
|
-
"""A single test run with all error handling needed."""
|
|
243
|
-
result = TestResult(
|
|
244
|
-
method=operation.method.upper(),
|
|
245
|
-
path=operation.full_path,
|
|
246
|
-
verbose_name=operation.verbose_name,
|
|
247
|
-
overridden_headers=headers,
|
|
248
|
-
data_generation_method=data_generation_method,
|
|
249
|
-
)
|
|
250
|
-
# To simplify connecting `before` and `after` events in external systems
|
|
251
|
-
correlation_id = uuid.uuid4().hex
|
|
252
|
-
yield events.BeforeExecution.from_operation(
|
|
253
|
-
operation=operation,
|
|
254
|
-
recursion_level=recursion_level,
|
|
255
|
-
data_generation_method=data_generation_method,
|
|
256
|
-
correlation_id=correlation_id,
|
|
257
|
-
)
|
|
258
|
-
hypothesis_output: List[str] = []
|
|
259
|
-
errors: List[Exception] = []
|
|
260
|
-
test_start_time = time.monotonic()
|
|
261
|
-
setup_hypothesis_database_key(test, operation)
|
|
262
|
-
try:
|
|
263
|
-
with catch_warnings(record=True) as warnings, capture_hypothesis_output() as hypothesis_output:
|
|
264
|
-
test(checks, targets, result, errors=errors, headers=headers, **kwargs)
|
|
265
|
-
status = Status.success
|
|
266
|
-
except CheckFailed:
|
|
267
|
-
status = Status.failure
|
|
268
|
-
except NonCheckError:
|
|
269
|
-
# It could be an error in user-defined extensions, network errors or internal Schemathesis errors
|
|
270
|
-
status = Status.error
|
|
271
|
-
result.mark_errored()
|
|
272
|
-
for error in deduplicate_errors(errors):
|
|
273
|
-
result.add_error(error)
|
|
274
|
-
except hypothesis.errors.MultipleFailures:
|
|
275
|
-
# Schemathesis may detect multiple errors that come from different check results
|
|
276
|
-
# They raise different "grouped" exceptions, and `MultipleFailures` is risen as the result
|
|
277
|
-
status = Status.failure
|
|
278
|
-
except hypothesis.errors.Flaky:
|
|
279
|
-
status = Status.error
|
|
280
|
-
result.mark_errored()
|
|
281
|
-
result.add_error(
|
|
282
|
-
hypothesis.errors.Flaky(
|
|
283
|
-
"Tests on this API operation produce unreliable results: \n"
|
|
284
|
-
"Falsified on the first call but did not on a subsequent one"
|
|
285
|
-
),
|
|
286
|
-
result.checks[-1].example if result.checks else None,
|
|
287
|
-
)
|
|
288
|
-
except hypothesis.errors.Unsatisfiable:
|
|
289
|
-
# We need more clear error message here
|
|
290
|
-
status = Status.error
|
|
291
|
-
result.add_error(hypothesis.errors.Unsatisfiable("Unable to satisfy schema parameters for this API operation"))
|
|
292
|
-
except KeyboardInterrupt:
|
|
293
|
-
yield events.Interrupted()
|
|
294
|
-
return
|
|
295
|
-
except AssertionError as exc: # comes from `hypothesis-jsonschema`
|
|
296
|
-
error = reraise(exc)
|
|
297
|
-
status = Status.error
|
|
298
|
-
result.add_error(error)
|
|
299
|
-
except HypothesisRefResolutionError:
|
|
300
|
-
status = Status.error
|
|
301
|
-
result.add_error(hypothesis.errors.Unsatisfiable(RECURSIVE_REFERENCE_ERROR_MESSAGE))
|
|
302
|
-
except InvalidArgument as error:
|
|
303
|
-
status = Status.error
|
|
304
|
-
message = get_invalid_regular_expression_message(warnings)
|
|
305
|
-
if message:
|
|
306
|
-
# `hypothesis-jsonschema` emits a warning on invalid regular expression syntax
|
|
307
|
-
result.add_error(InvalidRegularExpression(message))
|
|
308
|
-
else:
|
|
309
|
-
result.add_error(error)
|
|
310
|
-
except hypothesis.errors.DeadlineExceeded as error:
|
|
311
|
-
status = Status.error
|
|
312
|
-
result.add_error(DeadlineExceeded.from_exc(error))
|
|
313
|
-
except Exception as error:
|
|
314
|
-
status = Status.error
|
|
315
|
-
result.add_error(error)
|
|
316
|
-
test_elapsed_time = time.monotonic() - test_start_time
|
|
317
|
-
# Fetch seed value, hypothesis generates it during test execution
|
|
318
|
-
# It may be `None` if the `derandomize` config option is set to `True`
|
|
319
|
-
result.seed = getattr(test, "_hypothesis_internal_use_seed", None) or getattr(
|
|
320
|
-
test, "_hypothesis_internal_use_generated_seed", None
|
|
321
|
-
)
|
|
322
|
-
results.append(result)
|
|
323
|
-
yield events.AfterExecution.from_result(
|
|
324
|
-
result=result,
|
|
325
|
-
status=status,
|
|
326
|
-
elapsed_time=test_elapsed_time,
|
|
327
|
-
hypothesis_output=hypothesis_output,
|
|
328
|
-
operation=operation,
|
|
329
|
-
data_generation_method=data_generation_method,
|
|
330
|
-
correlation_id=correlation_id,
|
|
331
|
-
)
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
def setup_hypothesis_database_key(test: Callable, operation: APIOperation) -> None:
|
|
335
|
-
"""Make Hypothesis use separate database entries for every API operation.
|
|
336
|
-
|
|
337
|
-
It increases the effectiveness of the Hypothesis database in the CLI.
|
|
338
|
-
"""
|
|
339
|
-
# Hypothesis's function digest depends on the test function signature. To reflect it for the web API case,
|
|
340
|
-
# we use all API operation parameters in the digest.
|
|
341
|
-
extra = operation.verbose_name.encode("utf8")
|
|
342
|
-
for parameter in operation.definition.parameters:
|
|
343
|
-
extra += parameter.serialize().encode("utf8")
|
|
344
|
-
test.hypothesis.inner_test._hypothesis_internal_add_digest = extra # type: ignore
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
def get_invalid_regular_expression_message(warnings: List[WarningMessage]) -> Optional[str]:
|
|
348
|
-
for warning in warnings:
|
|
349
|
-
message = str(warning.message)
|
|
350
|
-
if "is not valid syntax for a Python regular expression" in message:
|
|
351
|
-
return message
|
|
352
|
-
return None
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
def reraise(error: AssertionError) -> InvalidSchema:
|
|
356
|
-
traceback = format_exception(error, True)
|
|
357
|
-
if "assert type_ in TYPE_STRINGS" in traceback:
|
|
358
|
-
message = "Invalid type name"
|
|
359
|
-
else:
|
|
360
|
-
message = "Unknown schema error"
|
|
361
|
-
try:
|
|
362
|
-
raise InvalidSchema(message) from error
|
|
363
|
-
except InvalidSchema as exc:
|
|
364
|
-
return exc
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
def deduplicate_errors(errors: List[Exception]) -> Generator[Exception, None, None]:
|
|
368
|
-
"""Deduplicate errors by their messages + tracebacks."""
|
|
369
|
-
seen = set()
|
|
370
|
-
for error in errors:
|
|
371
|
-
message = format_exception(error, True)
|
|
372
|
-
if message in seen:
|
|
373
|
-
continue
|
|
374
|
-
seen.add(message)
|
|
375
|
-
yield error
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
def run_checks(
|
|
379
|
-
case: Case,
|
|
380
|
-
checks: Iterable[CheckFunction],
|
|
381
|
-
check_results: List[Check],
|
|
382
|
-
result: TestResult,
|
|
383
|
-
response: GenericResponse,
|
|
384
|
-
elapsed_time: float,
|
|
385
|
-
max_response_time: Optional[int] = None,
|
|
386
|
-
) -> None:
|
|
387
|
-
errors = []
|
|
388
|
-
|
|
389
|
-
for check in checks:
|
|
390
|
-
check_name = check.__name__
|
|
391
|
-
try:
|
|
392
|
-
skip_check = check(response, case)
|
|
393
|
-
if not skip_check:
|
|
394
|
-
check_result = result.add_success(check_name, case, response, elapsed_time)
|
|
395
|
-
check_results.append(check_result)
|
|
396
|
-
except AssertionError as exc:
|
|
397
|
-
message = maybe_set_assertion_message(exc, check_name)
|
|
398
|
-
errors.append(exc)
|
|
399
|
-
if isinstance(exc, CheckFailed):
|
|
400
|
-
context = exc.context
|
|
401
|
-
else:
|
|
402
|
-
context = None
|
|
403
|
-
check_result = result.add_failure(check_name, case, response, elapsed_time, message, context)
|
|
404
|
-
check_results.append(check_result)
|
|
405
|
-
|
|
406
|
-
if max_response_time:
|
|
407
|
-
if elapsed_time > max_response_time:
|
|
408
|
-
message = f"Response time exceeded the limit of {max_response_time} ms"
|
|
409
|
-
errors.append(AssertionError(message))
|
|
410
|
-
result.add_failure(
|
|
411
|
-
"max_response_time",
|
|
412
|
-
case,
|
|
413
|
-
response,
|
|
414
|
-
elapsed_time,
|
|
415
|
-
message,
|
|
416
|
-
failures.ResponseTimeExceeded(elapsed=elapsed_time, deadline=max_response_time),
|
|
417
|
-
)
|
|
418
|
-
else:
|
|
419
|
-
result.add_success("max_response_time", case, response, elapsed_time)
|
|
420
|
-
|
|
421
|
-
if errors:
|
|
422
|
-
raise get_grouped_exception(case.operation.verbose_name, *errors)
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
def run_targets(targets: Iterable[Callable], context: TargetContext) -> None:
|
|
426
|
-
for target in targets:
|
|
427
|
-
value = target(context)
|
|
428
|
-
hypothesis.target(value, label=target.__name__)
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
def add_cases(case: Case, response: GenericResponse, test: Callable, *args: Any) -> None:
|
|
432
|
-
context = HookContext(case.operation)
|
|
433
|
-
for case_hook in get_all_by_name("add_case"):
|
|
434
|
-
_case = case_hook(context, case.partial_deepcopy(), response)
|
|
435
|
-
# run additional test if _case is not an empty value
|
|
436
|
-
if _case:
|
|
437
|
-
test(_case, *args)
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
441
|
-
class ErrorCollector:
|
|
442
|
-
"""Collect exceptions that are not related to failed checks.
|
|
443
|
-
|
|
444
|
-
Such exceptions may be considered as multiple failures or flakiness by Hypothesis. In both cases, Hypothesis hides
|
|
445
|
-
exception information that, in our case, is helpful for the end-user. It either indicates errors in user-defined
|
|
446
|
-
extensions, network-related errors, or internal Schemathesis errors. In all cases, this information is useful for
|
|
447
|
-
debugging.
|
|
448
|
-
|
|
449
|
-
To mitigate this, we gather all exceptions manually via this context manager to avoid interfering with the test
|
|
450
|
-
function signatures, which are used by Hypothesis.
|
|
451
|
-
"""
|
|
452
|
-
|
|
453
|
-
errors: List[Exception] = attr.ib() # pragma: no mutate
|
|
454
|
-
|
|
455
|
-
def __enter__(self) -> "ErrorCollector":
|
|
456
|
-
return self
|
|
457
|
-
|
|
458
|
-
# Typing: The return type suggested by mypy is `Literal[False]`, but I don't want to introduce dependency on the
|
|
459
|
-
# `typing_extensions` package for Python 3.7
|
|
460
|
-
def __exit__(
|
|
461
|
-
self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType]
|
|
462
|
-
) -> Any:
|
|
463
|
-
# Don't do anything special if:
|
|
464
|
-
# - Tests are successful
|
|
465
|
-
# - Checks failed
|
|
466
|
-
# - The testing process is interrupted
|
|
467
|
-
if not exc_type or issubclass(exc_type, CheckFailed) or not issubclass(exc_type, Exception):
|
|
468
|
-
return False
|
|
469
|
-
# These exceptions are needed for control flow on the Hypothesis side. E.g. rejecting unsatisfiable examples
|
|
470
|
-
if isinstance(exc_val, HypothesisException):
|
|
471
|
-
raise
|
|
472
|
-
# Exception value is not `None` and is a subclass of `Exception` at this point
|
|
473
|
-
exc_val = cast(Exception, exc_val)
|
|
474
|
-
self.errors.append(exc_val.with_traceback(exc_tb))
|
|
475
|
-
raise NonCheckError from None
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
def network_test(
|
|
479
|
-
case: Case,
|
|
480
|
-
checks: Iterable[CheckFunction],
|
|
481
|
-
targets: Iterable[Target],
|
|
482
|
-
result: TestResult,
|
|
483
|
-
session: requests.Session,
|
|
484
|
-
request_timeout: Optional[int],
|
|
485
|
-
request_tls_verify: bool,
|
|
486
|
-
request_cert: Optional[RequestCert],
|
|
487
|
-
store_interactions: bool,
|
|
488
|
-
headers: Optional[Dict[str, Any]],
|
|
489
|
-
feedback: Feedback,
|
|
490
|
-
max_response_time: Optional[int],
|
|
491
|
-
dry_run: bool,
|
|
492
|
-
errors: List[Exception],
|
|
493
|
-
) -> None:
|
|
494
|
-
"""A single test body will be executed against the target."""
|
|
495
|
-
with ErrorCollector(errors):
|
|
496
|
-
headers = headers or {}
|
|
497
|
-
if "user-agent" not in {header.lower() for header in headers}:
|
|
498
|
-
headers["User-Agent"] = USER_AGENT
|
|
499
|
-
timeout = prepare_timeout(request_timeout)
|
|
500
|
-
if not dry_run:
|
|
501
|
-
response = _network_test(
|
|
502
|
-
case,
|
|
503
|
-
checks,
|
|
504
|
-
targets,
|
|
505
|
-
result,
|
|
506
|
-
session,
|
|
507
|
-
timeout,
|
|
508
|
-
store_interactions,
|
|
509
|
-
headers,
|
|
510
|
-
feedback,
|
|
511
|
-
request_tls_verify,
|
|
512
|
-
request_cert,
|
|
513
|
-
max_response_time,
|
|
514
|
-
)
|
|
515
|
-
add_cases(
|
|
516
|
-
case,
|
|
517
|
-
response,
|
|
518
|
-
_network_test,
|
|
519
|
-
checks,
|
|
520
|
-
targets,
|
|
521
|
-
result,
|
|
522
|
-
session,
|
|
523
|
-
timeout,
|
|
524
|
-
store_interactions,
|
|
525
|
-
headers,
|
|
526
|
-
feedback,
|
|
527
|
-
request_tls_verify,
|
|
528
|
-
request_cert,
|
|
529
|
-
max_response_time,
|
|
530
|
-
)
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
def _network_test(
|
|
534
|
-
case: Case,
|
|
535
|
-
checks: Iterable[CheckFunction],
|
|
536
|
-
targets: Iterable[Target],
|
|
537
|
-
result: TestResult,
|
|
538
|
-
session: requests.Session,
|
|
539
|
-
timeout: Optional[float],
|
|
540
|
-
store_interactions: bool,
|
|
541
|
-
headers: Optional[Dict[str, Any]],
|
|
542
|
-
feedback: Feedback,
|
|
543
|
-
request_tls_verify: bool,
|
|
544
|
-
request_cert: Optional[RequestCert],
|
|
545
|
-
max_response_time: Optional[int],
|
|
546
|
-
) -> requests.Response:
|
|
547
|
-
check_results: List[Check] = []
|
|
548
|
-
try:
|
|
549
|
-
hook_context = HookContext(operation=case.operation)
|
|
550
|
-
hooks.dispatch("before_call", hook_context, case)
|
|
551
|
-
kwargs: Dict[str, Any] = {
|
|
552
|
-
"session": session,
|
|
553
|
-
"headers": headers,
|
|
554
|
-
"timeout": timeout,
|
|
555
|
-
"verify": request_tls_verify,
|
|
556
|
-
"cert": request_cert,
|
|
557
|
-
}
|
|
558
|
-
hooks.dispatch("process_call_kwargs", hook_context, case, kwargs)
|
|
559
|
-
response = case.call(**kwargs)
|
|
560
|
-
hooks.dispatch("after_call", hook_context, case, response)
|
|
561
|
-
except CheckFailed as exc:
|
|
562
|
-
check_name = "request_timeout"
|
|
563
|
-
requests_kwargs = case.as_requests_kwargs(base_url=case.get_full_base_url(), headers=headers)
|
|
564
|
-
request = requests.Request(**requests_kwargs).prepare()
|
|
565
|
-
elapsed = cast(float, timeout) # It is defined and not empty, since the exception happened
|
|
566
|
-
check_result = result.add_failure(
|
|
567
|
-
check_name, case, None, elapsed, f"Response timed out after {1000 * elapsed:.2f}ms", exc.context, request
|
|
568
|
-
)
|
|
569
|
-
check_results.append(check_result)
|
|
570
|
-
raise exc
|
|
571
|
-
context = TargetContext(case=case, response=response, response_time=response.elapsed.total_seconds())
|
|
572
|
-
run_targets(targets, context)
|
|
573
|
-
status = Status.success
|
|
574
|
-
try:
|
|
575
|
-
run_checks(case, checks, check_results, result, response, context.response_time * 1000, max_response_time)
|
|
576
|
-
except CheckFailed:
|
|
577
|
-
status = Status.failure
|
|
578
|
-
raise
|
|
579
|
-
finally:
|
|
580
|
-
if store_interactions:
|
|
581
|
-
result.store_requests_response(response, status, check_results)
|
|
582
|
-
feedback.add_test_case(case, response)
|
|
583
|
-
return response
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
@contextmanager
|
|
587
|
-
def get_session(auth: Optional[Union[HTTPDigestAuth, RawAuth]] = None) -> Generator[requests.Session, None, None]:
|
|
588
|
-
with requests.Session() as session:
|
|
589
|
-
if auth is not None:
|
|
590
|
-
session.auth = auth
|
|
591
|
-
yield session
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
def prepare_timeout(timeout: Optional[int]) -> Optional[float]:
|
|
595
|
-
"""Request timeout is in milliseconds, but `requests` uses seconds."""
|
|
596
|
-
output: Optional[Union[int, float]] = timeout
|
|
597
|
-
if timeout is not None:
|
|
598
|
-
output = timeout / 1000
|
|
599
|
-
return output
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
def wsgi_test(
|
|
603
|
-
case: Case,
|
|
604
|
-
checks: Iterable[CheckFunction],
|
|
605
|
-
targets: Iterable[Target],
|
|
606
|
-
result: TestResult,
|
|
607
|
-
auth: Optional[RawAuth],
|
|
608
|
-
auth_type: Optional[str],
|
|
609
|
-
headers: Optional[Dict[str, Any]],
|
|
610
|
-
store_interactions: bool,
|
|
611
|
-
feedback: Feedback,
|
|
612
|
-
max_response_time: Optional[int],
|
|
613
|
-
dry_run: bool,
|
|
614
|
-
errors: List[Exception],
|
|
615
|
-
) -> None:
|
|
616
|
-
with ErrorCollector(errors):
|
|
617
|
-
headers = _prepare_wsgi_headers(headers, auth, auth_type)
|
|
618
|
-
if not dry_run:
|
|
619
|
-
response = _wsgi_test(
|
|
620
|
-
case, checks, targets, result, headers, store_interactions, feedback, max_response_time
|
|
621
|
-
)
|
|
622
|
-
add_cases(
|
|
623
|
-
case,
|
|
624
|
-
response,
|
|
625
|
-
_wsgi_test,
|
|
626
|
-
checks,
|
|
627
|
-
targets,
|
|
628
|
-
result,
|
|
629
|
-
headers,
|
|
630
|
-
store_interactions,
|
|
631
|
-
feedback,
|
|
632
|
-
max_response_time,
|
|
633
|
-
)
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
def _wsgi_test(
|
|
637
|
-
case: Case,
|
|
638
|
-
checks: Iterable[CheckFunction],
|
|
639
|
-
targets: Iterable[Target],
|
|
640
|
-
result: TestResult,
|
|
641
|
-
headers: Dict[str, Any],
|
|
642
|
-
store_interactions: bool,
|
|
643
|
-
feedback: Feedback,
|
|
644
|
-
max_response_time: Optional[int],
|
|
645
|
-
) -> WSGIResponse:
|
|
646
|
-
with catching_logs(LogCaptureHandler(), level=logging.DEBUG) as recorded:
|
|
647
|
-
start = time.monotonic()
|
|
648
|
-
hook_context = HookContext(operation=case.operation)
|
|
649
|
-
hooks.dispatch("before_call", hook_context, case)
|
|
650
|
-
kwargs = {"headers": headers}
|
|
651
|
-
hooks.dispatch("process_call_kwargs", hook_context, case, kwargs)
|
|
652
|
-
response = case.call_wsgi(**kwargs)
|
|
653
|
-
hooks.dispatch("after_call", hook_context, case, response)
|
|
654
|
-
elapsed = time.monotonic() - start
|
|
655
|
-
context = TargetContext(case=case, response=response, response_time=elapsed)
|
|
656
|
-
run_targets(targets, context)
|
|
657
|
-
result.logs.extend(recorded.records)
|
|
658
|
-
status = Status.success
|
|
659
|
-
check_results: List[Check] = []
|
|
660
|
-
try:
|
|
661
|
-
run_checks(case, checks, check_results, result, response, context.response_time * 1000, max_response_time)
|
|
662
|
-
except CheckFailed:
|
|
663
|
-
status = Status.failure
|
|
664
|
-
raise
|
|
665
|
-
finally:
|
|
666
|
-
if store_interactions:
|
|
667
|
-
result.store_wsgi_response(case, response, headers, elapsed, status, check_results)
|
|
668
|
-
feedback.add_test_case(case, response)
|
|
669
|
-
return response
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
def _prepare_wsgi_headers(
|
|
673
|
-
headers: Optional[Dict[str, Any]], auth: Optional[RawAuth], auth_type: Optional[str]
|
|
674
|
-
) -> Dict[str, Any]:
|
|
675
|
-
headers = headers or {}
|
|
676
|
-
if "user-agent" not in {header.lower() for header in headers}:
|
|
677
|
-
headers["User-Agent"] = USER_AGENT
|
|
678
|
-
wsgi_auth = get_wsgi_auth(auth, auth_type)
|
|
679
|
-
if wsgi_auth:
|
|
680
|
-
headers["Authorization"] = wsgi_auth
|
|
681
|
-
return headers
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
def get_wsgi_auth(auth: Optional[RawAuth], auth_type: Optional[str]) -> Optional[str]:
|
|
685
|
-
if auth:
|
|
686
|
-
if auth_type == "digest":
|
|
687
|
-
raise ValueError("Digest auth is not supported for WSGI apps")
|
|
688
|
-
return _basic_auth_str(*auth)
|
|
689
|
-
return None
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
def asgi_test(
|
|
693
|
-
case: Case,
|
|
694
|
-
checks: Iterable[CheckFunction],
|
|
695
|
-
targets: Iterable[Target],
|
|
696
|
-
result: TestResult,
|
|
697
|
-
store_interactions: bool,
|
|
698
|
-
headers: Optional[Dict[str, Any]],
|
|
699
|
-
feedback: Feedback,
|
|
700
|
-
max_response_time: Optional[int],
|
|
701
|
-
dry_run: bool,
|
|
702
|
-
errors: List[Exception],
|
|
703
|
-
) -> None:
|
|
704
|
-
"""A single test body will be executed against the target."""
|
|
705
|
-
with ErrorCollector(errors):
|
|
706
|
-
headers = headers or {}
|
|
707
|
-
|
|
708
|
-
if not dry_run:
|
|
709
|
-
response = _asgi_test(
|
|
710
|
-
case, checks, targets, result, store_interactions, headers, feedback, max_response_time
|
|
711
|
-
)
|
|
712
|
-
add_cases(
|
|
713
|
-
case,
|
|
714
|
-
response,
|
|
715
|
-
_asgi_test,
|
|
716
|
-
checks,
|
|
717
|
-
targets,
|
|
718
|
-
result,
|
|
719
|
-
store_interactions,
|
|
720
|
-
headers,
|
|
721
|
-
feedback,
|
|
722
|
-
max_response_time,
|
|
723
|
-
)
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
def _asgi_test(
|
|
727
|
-
case: Case,
|
|
728
|
-
checks: Iterable[CheckFunction],
|
|
729
|
-
targets: Iterable[Target],
|
|
730
|
-
result: TestResult,
|
|
731
|
-
store_interactions: bool,
|
|
732
|
-
headers: Optional[Dict[str, Any]],
|
|
733
|
-
feedback: Feedback,
|
|
734
|
-
max_response_time: Optional[int],
|
|
735
|
-
) -> requests.Response:
|
|
736
|
-
hook_context = HookContext(operation=case.operation)
|
|
737
|
-
hooks.dispatch("before_call", hook_context, case)
|
|
738
|
-
kwargs: Dict[str, Any] = {"headers": headers}
|
|
739
|
-
hooks.dispatch("process_call_kwargs", hook_context, case, kwargs)
|
|
740
|
-
response = case.call_asgi(**kwargs)
|
|
741
|
-
hooks.dispatch("after_call", hook_context, case, response)
|
|
742
|
-
context = TargetContext(case=case, response=response, response_time=response.elapsed.total_seconds())
|
|
743
|
-
run_targets(targets, context)
|
|
744
|
-
status = Status.success
|
|
745
|
-
check_results: List[Check] = []
|
|
746
|
-
try:
|
|
747
|
-
run_checks(case, checks, check_results, result, response, context.response_time * 1000, max_response_time)
|
|
748
|
-
except CheckFailed:
|
|
749
|
-
status = Status.failure
|
|
750
|
-
raise
|
|
751
|
-
finally:
|
|
752
|
-
if store_interactions:
|
|
753
|
-
result.store_requests_response(response, status, check_results)
|
|
754
|
-
feedback.add_test_case(case, response)
|
|
755
|
-
return response
|