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
@@ -0,0 +1,416 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import time
|
4
|
+
import unittest
|
5
|
+
import uuid
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable
|
7
|
+
from warnings import WarningMessage, catch_warnings
|
8
|
+
|
9
|
+
import requests
|
10
|
+
from hypothesis.errors import InvalidArgument
|
11
|
+
from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
|
12
|
+
from jsonschema.exceptions import SchemaError as JsonSchemaError
|
13
|
+
from jsonschema.exceptions import ValidationError
|
14
|
+
from requests.exceptions import ChunkedEncodingError
|
15
|
+
from requests.structures import CaseInsensitiveDict
|
16
|
+
|
17
|
+
from schemathesis.checks import CheckContext, run_checks
|
18
|
+
from schemathesis.config._generation import GenerationConfig
|
19
|
+
from schemathesis.core.compat import BaseExceptionGroup
|
20
|
+
from schemathesis.core.control import SkipTest
|
21
|
+
from schemathesis.core.errors import (
|
22
|
+
SERIALIZERS_SUGGESTION_MESSAGE,
|
23
|
+
InternalError,
|
24
|
+
InvalidHeadersExample,
|
25
|
+
InvalidRegexPattern,
|
26
|
+
InvalidRegexType,
|
27
|
+
InvalidSchema,
|
28
|
+
MalformedMediaType,
|
29
|
+
SerializationNotPossible,
|
30
|
+
)
|
31
|
+
from schemathesis.core.failures import Failure, FailureGroup
|
32
|
+
from schemathesis.core.transport import Response
|
33
|
+
from schemathesis.engine import Status, events
|
34
|
+
from schemathesis.engine.context import EngineContext
|
35
|
+
from schemathesis.engine.errors import (
|
36
|
+
DeadlineExceeded,
|
37
|
+
TestingState,
|
38
|
+
UnexpectedError,
|
39
|
+
UnrecoverableNetworkError,
|
40
|
+
UnsupportedRecursiveReference,
|
41
|
+
clear_hypothesis_notes,
|
42
|
+
deduplicate_errors,
|
43
|
+
is_unrecoverable_network_error,
|
44
|
+
)
|
45
|
+
from schemathesis.engine.phases import PhaseName
|
46
|
+
from schemathesis.engine.recorder import ScenarioRecorder
|
47
|
+
from schemathesis.generation import metrics, overrides
|
48
|
+
from schemathesis.generation.case import Case
|
49
|
+
from schemathesis.generation.hypothesis.builder import (
|
50
|
+
InvalidHeadersExampleMark,
|
51
|
+
InvalidRegexMark,
|
52
|
+
NonSerializableMark,
|
53
|
+
UnsatisfiableExampleMark,
|
54
|
+
)
|
55
|
+
from schemathesis.generation.hypothesis.reporting import ignore_hypothesis_output
|
56
|
+
|
57
|
+
if TYPE_CHECKING:
|
58
|
+
from schemathesis.schemas import APIOperation
|
59
|
+
|
60
|
+
|
61
|
+
def run_test(
|
62
|
+
*,
|
63
|
+
operation: APIOperation,
|
64
|
+
test_function: Callable,
|
65
|
+
ctx: EngineContext,
|
66
|
+
phase: PhaseName,
|
67
|
+
suite_id: uuid.UUID,
|
68
|
+
) -> events.EventGenerator:
|
69
|
+
"""A single test run with all error handling needed."""
|
70
|
+
import hypothesis.errors
|
71
|
+
|
72
|
+
scenario_started = events.ScenarioStarted(label=operation.label, phase=phase, suite_id=suite_id)
|
73
|
+
yield scenario_started
|
74
|
+
errors: list[Exception] = []
|
75
|
+
skip_reason = None
|
76
|
+
test_start_time = time.monotonic()
|
77
|
+
recorder = ScenarioRecorder(label=operation.label)
|
78
|
+
state = TestingState()
|
79
|
+
|
80
|
+
def non_fatal_error(error: Exception, code_sample: str | None = None) -> events.NonFatalError:
|
81
|
+
return events.NonFatalError(
|
82
|
+
error=error, phase=phase, label=operation.label, related_to_operation=True, code_sample=code_sample
|
83
|
+
)
|
84
|
+
|
85
|
+
def scenario_finished(status: Status) -> events.ScenarioFinished:
|
86
|
+
return events.ScenarioFinished(
|
87
|
+
id=scenario_started.id,
|
88
|
+
suite_id=suite_id,
|
89
|
+
phase=phase,
|
90
|
+
label=operation.label,
|
91
|
+
recorder=recorder,
|
92
|
+
status=status,
|
93
|
+
elapsed_time=time.monotonic() - test_start_time,
|
94
|
+
skip_reason=skip_reason,
|
95
|
+
is_final=False,
|
96
|
+
)
|
97
|
+
|
98
|
+
phase_name = phase.value.lower()
|
99
|
+
assert phase_name in ("examples", "coverage", "fuzzing", "stateful")
|
100
|
+
|
101
|
+
operation_config = ctx.config.operations.get_for_operation(operation)
|
102
|
+
continue_on_failure = operation_config.continue_on_failure or ctx.config.continue_on_failure or False
|
103
|
+
generation = ctx.config.generation_for(operation=operation, phase=phase_name)
|
104
|
+
override = overrides.for_operation(ctx.config, operation=operation)
|
105
|
+
auth = ctx.config.auth_for(operation=operation)
|
106
|
+
headers = ctx.config.headers_for(operation=operation)
|
107
|
+
transport_kwargs = ctx.get_transport_kwargs(operation=operation)
|
108
|
+
check_ctx = CheckContext(
|
109
|
+
override=override,
|
110
|
+
auth=auth,
|
111
|
+
headers=CaseInsensitiveDict(headers) if headers else None,
|
112
|
+
config=ctx.config.checks_config_for(operation=operation, phase=phase_name),
|
113
|
+
transport_kwargs=transport_kwargs,
|
114
|
+
recorder=recorder,
|
115
|
+
)
|
116
|
+
|
117
|
+
try:
|
118
|
+
setup_hypothesis_database_key(test_function, operation)
|
119
|
+
with catch_warnings(record=True) as warnings, ignore_hypothesis_output():
|
120
|
+
test_function(
|
121
|
+
ctx=ctx,
|
122
|
+
state=state,
|
123
|
+
errors=errors,
|
124
|
+
check_ctx=check_ctx,
|
125
|
+
recorder=recorder,
|
126
|
+
generation=generation,
|
127
|
+
transport_kwargs=transport_kwargs,
|
128
|
+
continue_on_failure=continue_on_failure,
|
129
|
+
)
|
130
|
+
# Test body was not executed at all - Hypothesis did not generate any tests, but there is no error
|
131
|
+
status = Status.SUCCESS
|
132
|
+
except (SkipTest, unittest.case.SkipTest) as exc:
|
133
|
+
status = Status.SKIP
|
134
|
+
skip_reason = {"Hypothesis has been told to run no examples for this test.": "No examples in schema"}.get(
|
135
|
+
str(exc), str(exc)
|
136
|
+
)
|
137
|
+
except (FailureGroup, Failure):
|
138
|
+
status = Status.FAILURE
|
139
|
+
except UnexpectedError:
|
140
|
+
# It could be an error in user-defined extensions, network errors or internal Schemathesis errors
|
141
|
+
status = Status.ERROR
|
142
|
+
for idx, err in enumerate(errors):
|
143
|
+
if isinstance(err, MalformedMediaType):
|
144
|
+
errors[idx] = InvalidSchema(str(err))
|
145
|
+
except hypothesis.errors.Flaky as exc:
|
146
|
+
if isinstance(exc.__cause__, hypothesis.errors.DeadlineExceeded):
|
147
|
+
status = Status.ERROR
|
148
|
+
yield non_fatal_error(DeadlineExceeded.from_exc(exc.__cause__))
|
149
|
+
elif isinstance(exc, hypothesis.errors.FlakyFailure) and any(
|
150
|
+
isinstance(subexc, hypothesis.errors.DeadlineExceeded) for subexc in exc.exceptions
|
151
|
+
):
|
152
|
+
for sub_exc in exc.exceptions:
|
153
|
+
if isinstance(sub_exc, hypothesis.errors.DeadlineExceeded):
|
154
|
+
yield non_fatal_error(DeadlineExceeded.from_exc(sub_exc))
|
155
|
+
status = Status.ERROR
|
156
|
+
elif errors:
|
157
|
+
status = Status.ERROR
|
158
|
+
else:
|
159
|
+
status = Status.FAILURE
|
160
|
+
except BaseExceptionGroup:
|
161
|
+
status = Status.ERROR
|
162
|
+
except hypothesis.errors.Unsatisfiable:
|
163
|
+
# We need more clear error message here
|
164
|
+
status = Status.ERROR
|
165
|
+
yield non_fatal_error(hypothesis.errors.Unsatisfiable("Failed to generate test cases for this API operation"))
|
166
|
+
except KeyboardInterrupt:
|
167
|
+
yield scenario_finished(Status.INTERRUPTED)
|
168
|
+
yield events.Interrupted(phase=phase)
|
169
|
+
return
|
170
|
+
except AssertionError as exc: # May come from `hypothesis-jsonschema` or `hypothesis`
|
171
|
+
status = Status.ERROR
|
172
|
+
try:
|
173
|
+
operation.schema.validate()
|
174
|
+
msg = "Unexpected error during testing of this API operation"
|
175
|
+
exc_msg = str(exc)
|
176
|
+
if exc_msg:
|
177
|
+
msg += f": {exc_msg}"
|
178
|
+
try:
|
179
|
+
raise InternalError(msg) from exc
|
180
|
+
except InternalError as exc:
|
181
|
+
yield non_fatal_error(exc)
|
182
|
+
except ValidationError as exc:
|
183
|
+
yield non_fatal_error(
|
184
|
+
InvalidSchema.from_jsonschema_error(
|
185
|
+
exc,
|
186
|
+
path=operation.path,
|
187
|
+
method=operation.method,
|
188
|
+
config=ctx.config.output,
|
189
|
+
)
|
190
|
+
)
|
191
|
+
except HypothesisRefResolutionError:
|
192
|
+
status = Status.ERROR
|
193
|
+
yield non_fatal_error(UnsupportedRecursiveReference())
|
194
|
+
except InvalidArgument as exc:
|
195
|
+
status = Status.ERROR
|
196
|
+
message = get_invalid_regular_expression_message(warnings)
|
197
|
+
if message:
|
198
|
+
# `hypothesis-jsonschema` emits a warning on invalid regular expression syntax
|
199
|
+
yield non_fatal_error(InvalidRegexPattern.from_hypothesis_jsonschema_message(message))
|
200
|
+
else:
|
201
|
+
yield non_fatal_error(exc)
|
202
|
+
except hypothesis.errors.DeadlineExceeded as exc:
|
203
|
+
status = Status.ERROR
|
204
|
+
yield non_fatal_error(DeadlineExceeded.from_exc(exc))
|
205
|
+
except JsonSchemaError as exc:
|
206
|
+
status = Status.ERROR
|
207
|
+
yield non_fatal_error(InvalidRegexPattern.from_schema_error(exc, from_examples=False))
|
208
|
+
except Exception as exc:
|
209
|
+
status = Status.ERROR
|
210
|
+
clear_hypothesis_notes(exc)
|
211
|
+
# Likely a YAML parsing issue. E.g. `00:00:00.00` (without quotes) is parsed as float `0.0`
|
212
|
+
if str(exc) == "first argument must be string or compiled pattern":
|
213
|
+
yield non_fatal_error(
|
214
|
+
InvalidRegexType(
|
215
|
+
"Invalid `pattern` value: expected a string. "
|
216
|
+
"If your schema is in YAML, ensure `pattern` values are quoted",
|
217
|
+
)
|
218
|
+
)
|
219
|
+
else:
|
220
|
+
code_sample: str | None = None
|
221
|
+
if state.unrecoverable_network_error is not None and state.unrecoverable_network_error.error is exc:
|
222
|
+
code_sample = state.unrecoverable_network_error.code_sample
|
223
|
+
yield non_fatal_error(exc, code_sample=code_sample)
|
224
|
+
if (
|
225
|
+
status == Status.SUCCESS
|
226
|
+
and continue_on_failure
|
227
|
+
and any(check.status == Status.FAILURE for checks in recorder.checks.values() for check in checks)
|
228
|
+
):
|
229
|
+
status = Status.FAILURE
|
230
|
+
if UnsatisfiableExampleMark.is_set(test_function):
|
231
|
+
status = Status.ERROR
|
232
|
+
yield non_fatal_error(
|
233
|
+
hypothesis.errors.Unsatisfiable("Failed to generate test cases from examples for this API operation")
|
234
|
+
)
|
235
|
+
non_serializable = NonSerializableMark.get(test_function)
|
236
|
+
if non_serializable is not None and status != Status.ERROR:
|
237
|
+
status = Status.ERROR
|
238
|
+
media_types = ", ".join(non_serializable.media_types)
|
239
|
+
yield non_fatal_error(
|
240
|
+
SerializationNotPossible(
|
241
|
+
"Failed to generate test cases from examples for this API operation because of"
|
242
|
+
f" unsupported payload media types: {media_types}\n{SERIALIZERS_SUGGESTION_MESSAGE}",
|
243
|
+
media_types=non_serializable.media_types,
|
244
|
+
)
|
245
|
+
)
|
246
|
+
|
247
|
+
invalid_regex = InvalidRegexMark.get(test_function)
|
248
|
+
if invalid_regex is not None and status != Status.ERROR:
|
249
|
+
status = Status.ERROR
|
250
|
+
yield non_fatal_error(InvalidRegexPattern.from_schema_error(invalid_regex, from_examples=True))
|
251
|
+
invalid_headers = InvalidHeadersExampleMark.get(test_function)
|
252
|
+
if invalid_headers:
|
253
|
+
status = Status.ERROR
|
254
|
+
yield non_fatal_error(InvalidHeadersExample.from_headers(invalid_headers))
|
255
|
+
for error in deduplicate_errors(errors):
|
256
|
+
yield non_fatal_error(error)
|
257
|
+
|
258
|
+
yield scenario_finished(status)
|
259
|
+
|
260
|
+
|
261
|
+
def setup_hypothesis_database_key(test: Callable, operation: APIOperation) -> None:
|
262
|
+
"""Make Hypothesis use separate database entries for every API operation.
|
263
|
+
|
264
|
+
It increases the effectiveness of the Hypothesis database in the CLI.
|
265
|
+
"""
|
266
|
+
# Hypothesis's function digest depends on the test function signature. To reflect it for the web API case,
|
267
|
+
# we use all API operation parameters in the digest.
|
268
|
+
extra = operation.label.encode("utf8")
|
269
|
+
for parameter in operation.iter_parameters():
|
270
|
+
extra += parameter.serialize(operation).encode("utf8")
|
271
|
+
test.hypothesis.inner_test._hypothesis_internal_add_digest = extra # type: ignore
|
272
|
+
|
273
|
+
|
274
|
+
def get_invalid_regular_expression_message(warnings: list[WarningMessage]) -> str | None:
|
275
|
+
for warning in warnings:
|
276
|
+
message = str(warning.message)
|
277
|
+
if "is not valid syntax for a Python regular expression" in message:
|
278
|
+
return message
|
279
|
+
return None
|
280
|
+
|
281
|
+
|
282
|
+
def cached_test_func(f: Callable) -> Callable:
|
283
|
+
def wrapped(
|
284
|
+
*,
|
285
|
+
ctx: EngineContext,
|
286
|
+
state: TestingState,
|
287
|
+
case: Case,
|
288
|
+
errors: list[Exception],
|
289
|
+
check_ctx: CheckContext,
|
290
|
+
recorder: ScenarioRecorder,
|
291
|
+
generation: GenerationConfig,
|
292
|
+
transport_kwargs: dict[str, Any],
|
293
|
+
continue_on_failure: bool,
|
294
|
+
) -> None:
|
295
|
+
try:
|
296
|
+
if ctx.has_to_stop:
|
297
|
+
raise KeyboardInterrupt
|
298
|
+
if generation.unique_inputs:
|
299
|
+
cached = ctx.get_cached_outcome(case)
|
300
|
+
if isinstance(cached, BaseException):
|
301
|
+
raise cached
|
302
|
+
elif cached is None:
|
303
|
+
return None
|
304
|
+
try:
|
305
|
+
f(
|
306
|
+
case=case,
|
307
|
+
check_ctx=check_ctx,
|
308
|
+
recorder=recorder,
|
309
|
+
generation=generation,
|
310
|
+
transport_kwargs=transport_kwargs,
|
311
|
+
continue_on_failure=continue_on_failure,
|
312
|
+
)
|
313
|
+
except BaseException as exc:
|
314
|
+
ctx.cache_outcome(case, exc)
|
315
|
+
raise
|
316
|
+
else:
|
317
|
+
ctx.cache_outcome(case, None)
|
318
|
+
else:
|
319
|
+
f(
|
320
|
+
case=case,
|
321
|
+
check_ctx=check_ctx,
|
322
|
+
recorder=recorder,
|
323
|
+
generation=generation,
|
324
|
+
transport_kwargs=transport_kwargs,
|
325
|
+
continue_on_failure=continue_on_failure,
|
326
|
+
)
|
327
|
+
except (KeyboardInterrupt, Failure):
|
328
|
+
raise
|
329
|
+
except Exception as exc:
|
330
|
+
if isinstance(exc, (requests.ConnectionError, ChunkedEncodingError)) and is_unrecoverable_network_error(
|
331
|
+
exc
|
332
|
+
):
|
333
|
+
# Server likely has crashed and does not accept any connections at all
|
334
|
+
# Don't report these error - only the original crash should be reported
|
335
|
+
if exc.request is not None:
|
336
|
+
headers = {key: value[0] for key, value in exc.request.headers.items()}
|
337
|
+
else:
|
338
|
+
headers = {**dict(case.headers or {}), **transport_kwargs.get("headers", {})}
|
339
|
+
verify = transport_kwargs.get("verify", True)
|
340
|
+
state.unrecoverable_network_error = UnrecoverableNetworkError(
|
341
|
+
error=exc,
|
342
|
+
code_sample=case.as_curl_command(headers=headers, verify=verify),
|
343
|
+
)
|
344
|
+
raise
|
345
|
+
errors.append(exc)
|
346
|
+
raise UnexpectedError from None
|
347
|
+
|
348
|
+
wrapped.__name__ = f.__name__
|
349
|
+
|
350
|
+
return wrapped
|
351
|
+
|
352
|
+
|
353
|
+
@cached_test_func
|
354
|
+
def test_func(
|
355
|
+
*,
|
356
|
+
case: Case,
|
357
|
+
check_ctx: CheckContext,
|
358
|
+
recorder: ScenarioRecorder,
|
359
|
+
generation: GenerationConfig,
|
360
|
+
transport_kwargs: dict[str, Any],
|
361
|
+
continue_on_failure: bool,
|
362
|
+
) -> None:
|
363
|
+
recorder.record_case(parent_id=None, transition=None, case=case)
|
364
|
+
try:
|
365
|
+
response = case.call(**transport_kwargs)
|
366
|
+
except (requests.Timeout, requests.ConnectionError, ChunkedEncodingError) as error:
|
367
|
+
if isinstance(error.request, requests.Request):
|
368
|
+
recorder.record_request(case_id=case.id, request=error.request.prepare())
|
369
|
+
elif isinstance(error.request, requests.PreparedRequest):
|
370
|
+
recorder.record_request(case_id=case.id, request=error.request)
|
371
|
+
raise
|
372
|
+
recorder.record_response(case_id=case.id, response=response)
|
373
|
+
metrics.maximize(generation.maximize, case=case, response=response)
|
374
|
+
validate_response(
|
375
|
+
case=case,
|
376
|
+
ctx=check_ctx,
|
377
|
+
response=response,
|
378
|
+
continue_on_failure=continue_on_failure,
|
379
|
+
recorder=recorder,
|
380
|
+
)
|
381
|
+
|
382
|
+
|
383
|
+
def validate_response(
|
384
|
+
*,
|
385
|
+
case: Case,
|
386
|
+
ctx: CheckContext,
|
387
|
+
response: Response,
|
388
|
+
continue_on_failure: bool,
|
389
|
+
recorder: ScenarioRecorder,
|
390
|
+
) -> None:
|
391
|
+
failures = set()
|
392
|
+
|
393
|
+
def on_failure(name: str, collected: set[Failure], failure: Failure) -> None:
|
394
|
+
collected.add(failure)
|
395
|
+
failure_data = recorder.find_failure_data(parent_id=case.id, failure=failure)
|
396
|
+
recorder.record_check_failure(
|
397
|
+
name=name,
|
398
|
+
case_id=failure_data.case.id,
|
399
|
+
code_sample=failure_data.case.as_curl_command(headers=failure_data.headers, verify=failure_data.verify),
|
400
|
+
failure=failure,
|
401
|
+
)
|
402
|
+
|
403
|
+
def on_success(name: str, _case: Case) -> None:
|
404
|
+
recorder.record_check_success(name=name, case_id=_case.id)
|
405
|
+
|
406
|
+
failures = run_checks(
|
407
|
+
case=case,
|
408
|
+
response=response,
|
409
|
+
ctx=ctx,
|
410
|
+
checks=ctx._checks,
|
411
|
+
on_failure=on_failure,
|
412
|
+
on_success=on_success,
|
413
|
+
)
|
414
|
+
|
415
|
+
if failures and not continue_on_failure:
|
416
|
+
raise FailureGroup(list(failures)) from None
|
@@ -0,0 +1,82 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import threading
|
4
|
+
import uuid
|
5
|
+
from queue import Queue
|
6
|
+
from types import TracebackType
|
7
|
+
from typing import TYPE_CHECKING, Callable
|
8
|
+
|
9
|
+
from schemathesis.core.result import Result
|
10
|
+
from schemathesis.engine.phases import PhaseName
|
11
|
+
|
12
|
+
if TYPE_CHECKING:
|
13
|
+
from schemathesis.engine.context import EngineContext
|
14
|
+
from schemathesis.generation.hypothesis.builder import HypothesisTestMode
|
15
|
+
|
16
|
+
|
17
|
+
class TaskProducer:
|
18
|
+
"""Produces test tasks for workers to execute."""
|
19
|
+
|
20
|
+
def __init__(self, ctx: EngineContext) -> None:
|
21
|
+
self.operations = ctx.schema.get_all_operations()
|
22
|
+
self.lock = threading.Lock()
|
23
|
+
|
24
|
+
def next_operation(self) -> Result | None:
|
25
|
+
"""Get next API operation in a thread-safe manner."""
|
26
|
+
with self.lock:
|
27
|
+
return next(self.operations, None)
|
28
|
+
|
29
|
+
|
30
|
+
class WorkerPool:
|
31
|
+
"""Manages a pool of worker threads."""
|
32
|
+
|
33
|
+
def __init__(
|
34
|
+
self,
|
35
|
+
workers_num: int,
|
36
|
+
producer: TaskProducer,
|
37
|
+
worker_factory: Callable,
|
38
|
+
ctx: EngineContext,
|
39
|
+
mode: HypothesisTestMode,
|
40
|
+
phase: PhaseName,
|
41
|
+
suite_id: uuid.UUID,
|
42
|
+
) -> None:
|
43
|
+
self.workers_num = workers_num
|
44
|
+
self.producer = producer
|
45
|
+
self.worker_factory = worker_factory
|
46
|
+
self.ctx = ctx
|
47
|
+
self.mode = mode
|
48
|
+
self.phase = phase
|
49
|
+
self.suite_id = suite_id
|
50
|
+
self.workers: list[threading.Thread] = []
|
51
|
+
self.events_queue: Queue = Queue()
|
52
|
+
|
53
|
+
def start(self) -> None:
|
54
|
+
"""Start all worker threads."""
|
55
|
+
for i in range(self.workers_num):
|
56
|
+
worker = threading.Thread(
|
57
|
+
target=self.worker_factory,
|
58
|
+
kwargs={
|
59
|
+
"ctx": self.ctx,
|
60
|
+
"mode": self.mode,
|
61
|
+
"phase": self.phase,
|
62
|
+
"events_queue": self.events_queue,
|
63
|
+
"producer": self.producer,
|
64
|
+
"suite_id": self.suite_id,
|
65
|
+
},
|
66
|
+
name=f"schemathesis_unit_tests_{i}",
|
67
|
+
daemon=True,
|
68
|
+
)
|
69
|
+
self.workers.append(worker)
|
70
|
+
worker.start()
|
71
|
+
|
72
|
+
def stop(self) -> None:
|
73
|
+
"""Stop all workers gracefully."""
|
74
|
+
for worker in self.workers:
|
75
|
+
worker.join()
|
76
|
+
|
77
|
+
def __enter__(self) -> WorkerPool:
|
78
|
+
self.start()
|
79
|
+
return self
|
80
|
+
|
81
|
+
def __exit__(self, ty: type[BaseException] | None, value: BaseException | None, tb: TracebackType | None) -> None:
|
82
|
+
self.stop()
|