schemathesis 4.0.0a11__py3-none-any.whl → 4.0.0b1__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 +35 -27
- schemathesis/auths.py +85 -54
- schemathesis/checks.py +65 -36
- schemathesis/cli/commands/run/__init__.py +32 -27
- schemathesis/cli/commands/run/context.py +6 -1
- schemathesis/cli/commands/run/events.py +7 -1
- schemathesis/cli/commands/run/executor.py +12 -7
- schemathesis/cli/commands/run/handlers/output.py +188 -80
- schemathesis/cli/commands/run/validation.py +21 -6
- schemathesis/cli/constants.py +1 -1
- schemathesis/config/__init__.py +2 -1
- schemathesis/config/_generation.py +12 -13
- schemathesis/config/_operations.py +14 -0
- schemathesis/config/_phases.py +41 -5
- schemathesis/config/_projects.py +33 -1
- schemathesis/config/_report.py +6 -2
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +49 -1
- schemathesis/core/errors.py +15 -19
- schemathesis/core/transport.py +117 -2
- schemathesis/engine/context.py +1 -0
- schemathesis/engine/errors.py +61 -2
- schemathesis/engine/events.py +10 -2
- schemathesis/engine/phases/probes.py +3 -0
- schemathesis/engine/phases/stateful/__init__.py +2 -1
- schemathesis/engine/phases/stateful/_executor.py +38 -5
- schemathesis/engine/phases/stateful/context.py +2 -2
- schemathesis/engine/phases/unit/_executor.py +36 -7
- schemathesis/generation/__init__.py +0 -3
- schemathesis/generation/case.py +153 -28
- schemathesis/generation/coverage.py +1 -1
- schemathesis/generation/hypothesis/builder.py +43 -19
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +0 -8
- schemathesis/generation/overrides.py +11 -27
- schemathesis/generation/stateful/__init__.py +17 -0
- schemathesis/generation/stateful/state_machine.py +32 -108
- schemathesis/graphql/loaders.py +152 -8
- schemathesis/hooks.py +63 -39
- schemathesis/openapi/checks.py +82 -20
- schemathesis/openapi/generation/filters.py +9 -2
- schemathesis/openapi/loaders.py +134 -8
- schemathesis/pytest/lazy.py +4 -31
- schemathesis/pytest/loaders.py +24 -0
- schemathesis/pytest/plugin.py +38 -6
- schemathesis/schemas.py +161 -94
- schemathesis/specs/graphql/scalars.py +37 -3
- schemathesis/specs/graphql/schemas.py +18 -9
- schemathesis/specs/openapi/_hypothesis.py +53 -34
- schemathesis/specs/openapi/checks.py +111 -47
- schemathesis/specs/openapi/expressions/nodes.py +1 -1
- schemathesis/specs/openapi/formats.py +30 -3
- schemathesis/specs/openapi/media_types.py +44 -1
- schemathesis/specs/openapi/negative/__init__.py +5 -3
- schemathesis/specs/openapi/negative/mutations.py +2 -2
- schemathesis/specs/openapi/parameters.py +0 -3
- schemathesis/specs/openapi/schemas.py +14 -93
- schemathesis/specs/openapi/stateful/__init__.py +2 -1
- schemathesis/specs/openapi/stateful/links.py +1 -63
- schemathesis/transport/__init__.py +54 -16
- schemathesis/transport/prepare.py +31 -7
- schemathesis/transport/requests.py +21 -9
- schemathesis/transport/serialization.py +0 -4
- schemathesis/transport/wsgi.py +15 -8
- {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0b1.dist-info}/METADATA +45 -87
- {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0b1.dist-info}/RECORD +69 -71
- schemathesis/contrib/__init__.py +0 -9
- schemathesis/contrib/openapi/__init__.py +0 -9
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -20
- schemathesis/generation/targets.py +0 -69
- {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0b1.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0b1.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0b1.dist-info}/licenses/LICENSE +0 -0
schemathesis/engine/errors.py
CHANGED
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
8
8
|
|
9
9
|
import enum
|
10
10
|
import re
|
11
|
+
from dataclasses import dataclass
|
11
12
|
from functools import cached_property
|
12
13
|
from typing import TYPE_CHECKING, Callable, Iterator, Sequence, cast
|
13
14
|
|
@@ -24,6 +25,8 @@ from schemathesis.core.errors import (
|
|
24
25
|
|
25
26
|
if TYPE_CHECKING:
|
26
27
|
import hypothesis.errors
|
28
|
+
import requests
|
29
|
+
from requests.exceptions import ChunkedEncodingError
|
27
30
|
|
28
31
|
__all__ = ["EngineErrorInfo", "DeadlineExceeded", "UnsupportedRecursiveReference", "UnexpectedError"]
|
29
32
|
|
@@ -61,8 +64,9 @@ class EngineErrorInfo:
|
|
61
64
|
It serves as a caching wrapper around exceptions to avoid repeated computations.
|
62
65
|
"""
|
63
66
|
|
64
|
-
def __init__(self, error: Exception) -> None:
|
67
|
+
def __init__(self, error: Exception, code_sample: str | None = None) -> None:
|
65
68
|
self._error = error
|
69
|
+
self._code_sample = code_sample
|
66
70
|
|
67
71
|
def __str__(self) -> str:
|
68
72
|
return self._error_repr
|
@@ -212,6 +216,9 @@ class EngineErrorInfo:
|
|
212
216
|
message.append("") # Empty line before extras
|
213
217
|
message.extend(f"{indent}{extra}" for extra in extras)
|
214
218
|
|
219
|
+
if self._code_sample is not None:
|
220
|
+
message.append(f"\nReproduce with: \n\n {self._code_sample}")
|
221
|
+
|
215
222
|
# Suggestion
|
216
223
|
suggestion = get_runtime_error_suggestion(self._kind, bold=bold)
|
217
224
|
if suggestion is not None:
|
@@ -254,7 +261,7 @@ def get_runtime_error_suggestion(error_type: RuntimeErrorKind, bold: Callable[[s
|
|
254
261
|
RuntimeErrorKind.SCHEMA_INVALID_REGULAR_EXPRESSION: "Ensure your regex is compatible with Python's syntax.\n"
|
255
262
|
"For guidance, visit: https://docs.python.org/3/library/re.html",
|
256
263
|
RuntimeErrorKind.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR: "Define a custom strategy for it.\n"
|
257
|
-
"For guidance, visit: https://schemathesis.readthedocs.io/en/
|
264
|
+
"For guidance, visit: https://schemathesis.readthedocs.io/en/latest/guides/graphql-custom-scalars/",
|
258
265
|
RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE: _format_health_check_suggestion("data_too_large"),
|
259
266
|
RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH: _format_health_check_suggestion("filter_too_much"),
|
260
267
|
RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW: _format_health_check_suggestion("too_slow"),
|
@@ -403,3 +410,55 @@ def canonicalize_error_message(error: Exception, with_traceback: bool = True) ->
|
|
403
410
|
message = MEMORY_ADDRESS_RE.sub("0xbaaaaaaaaaad", message)
|
404
411
|
# Remove URL information
|
405
412
|
return URL_IN_ERROR_MESSAGE_RE.sub("", message)
|
413
|
+
|
414
|
+
|
415
|
+
def clear_hypothesis_notes(exc: Exception) -> None:
|
416
|
+
notes = getattr(exc, "__notes__", [])
|
417
|
+
if any("while generating" in note for note in notes):
|
418
|
+
notes.clear()
|
419
|
+
|
420
|
+
|
421
|
+
def is_unrecoverable_network_error(exc: Exception) -> bool:
|
422
|
+
from http.client import RemoteDisconnected
|
423
|
+
|
424
|
+
from urllib3.exceptions import ProtocolError
|
425
|
+
|
426
|
+
def has_connection_reset(inner: BaseException) -> bool:
|
427
|
+
exc_str = str(inner)
|
428
|
+
if any(pattern in exc_str for pattern in ["Connection reset by peer", "[Errno 104]", "ECONNRESET"]):
|
429
|
+
return True
|
430
|
+
|
431
|
+
if inner.__context__ is not None:
|
432
|
+
return has_connection_reset(inner.__context__)
|
433
|
+
|
434
|
+
return False
|
435
|
+
|
436
|
+
if isinstance(exc.__context__, ProtocolError):
|
437
|
+
if len(exc.__context__.args) == 2 and isinstance(exc.__context__.args[1], RemoteDisconnected):
|
438
|
+
return True
|
439
|
+
if len(exc.__context__.args) == 1 and exc.__context__.args[0] == "Response ended prematurely":
|
440
|
+
return True
|
441
|
+
|
442
|
+
return has_connection_reset(exc)
|
443
|
+
|
444
|
+
|
445
|
+
@dataclass()
|
446
|
+
class UnrecoverableNetworkError:
|
447
|
+
error: requests.ConnectionError | ChunkedEncodingError
|
448
|
+
code_sample: str
|
449
|
+
|
450
|
+
__slots__ = ("error", "code_sample")
|
451
|
+
|
452
|
+
def __init__(self, error: requests.ConnectionError | ChunkedEncodingError, code_sample: str) -> None:
|
453
|
+
self.error = error
|
454
|
+
self.code_sample = code_sample
|
455
|
+
|
456
|
+
|
457
|
+
@dataclass
|
458
|
+
class TestingState:
|
459
|
+
unrecoverable_network_error: UnrecoverableNetworkError | None
|
460
|
+
|
461
|
+
__slots__ = ("unrecoverable_network_error",)
|
462
|
+
|
463
|
+
def __init__(self) -> None:
|
464
|
+
self.unrecoverable_network_error = None
|
schemathesis/engine/events.py
CHANGED
@@ -200,10 +200,18 @@ class NonFatalError(EngineEvent):
|
|
200
200
|
|
201
201
|
__slots__ = ("id", "timestamp", "info", "value", "phase", "label", "related_to_operation")
|
202
202
|
|
203
|
-
def __init__(
|
203
|
+
def __init__(
|
204
|
+
self,
|
205
|
+
*,
|
206
|
+
error: Exception,
|
207
|
+
phase: PhaseName,
|
208
|
+
label: str,
|
209
|
+
related_to_operation: bool,
|
210
|
+
code_sample: str | None = None,
|
211
|
+
) -> None:
|
204
212
|
self.id = uuid.uuid4()
|
205
213
|
self.timestamp = time.time()
|
206
|
-
self.info = EngineErrorInfo(error=error)
|
214
|
+
self.info = EngineErrorInfo(error=error, code_sample=code_sample)
|
207
215
|
self.value = error
|
208
216
|
self.phase = phase
|
209
217
|
self.label = label
|
@@ -16,6 +16,7 @@ from typing import TYPE_CHECKING
|
|
16
16
|
from schemathesis.core.result import Err, Ok, Result
|
17
17
|
from schemathesis.core.transport import USER_AGENT
|
18
18
|
from schemathesis.engine import Status, events
|
19
|
+
from schemathesis.transport.prepare import get_default_headers
|
19
20
|
|
20
21
|
if TYPE_CHECKING:
|
21
22
|
import requests
|
@@ -134,6 +135,8 @@ def send(probe: Probe, ctx: EngineContext) -> ProbeRun:
|
|
134
135
|
request = probe.prepare_request(session, Request(), ctx.schema)
|
135
136
|
request.headers[HEADER_NAME] = probe.name
|
136
137
|
request.headers["User-Agent"] = USER_AGENT
|
138
|
+
for header, value in get_default_headers().items():
|
139
|
+
request.headers.setdefault(header, value)
|
137
140
|
with warnings.catch_warnings():
|
138
141
|
warnings.simplefilter("ignore", InsecureRequestWarning)
|
139
142
|
response = session.send(request, timeout=ctx.config.request_timeout or 2)
|
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING
|
|
6
6
|
|
7
7
|
from schemathesis.engine import Status, events
|
8
8
|
from schemathesis.engine.phases import Phase, PhaseName, PhaseSkipReason
|
9
|
+
from schemathesis.generation.stateful import STATEFUL_TESTS_LABEL
|
9
10
|
|
10
11
|
if TYPE_CHECKING:
|
11
12
|
from schemathesis.engine.context import EngineContext
|
@@ -19,7 +20,7 @@ def execute(engine: EngineContext, phase: Phase) -> events.EventGenerator:
|
|
19
20
|
try:
|
20
21
|
state_machine = engine.schema.as_state_machine()
|
21
22
|
except Exception as exc:
|
22
|
-
yield events.NonFatalError(error=exc, phase=phase.name, label=
|
23
|
+
yield events.NonFatalError(error=exc, phase=phase.name, label=STATEFUL_TESTS_LABEL, related_to_operation=False)
|
23
24
|
yield events.PhaseFinished(phase=phase, status=Status.ERROR, payload=None)
|
24
25
|
return
|
25
26
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
from __future__ import annotations
|
1
|
+
from __future__ import annotations # noqa: I001
|
2
2
|
|
3
3
|
import queue
|
4
4
|
import time
|
@@ -8,9 +8,11 @@ from typing import Any
|
|
8
8
|
from warnings import catch_warnings
|
9
9
|
|
10
10
|
import hypothesis
|
11
|
+
import requests
|
11
12
|
from hypothesis.control import current_build_context
|
12
13
|
from hypothesis.errors import Flaky, Unsatisfiable
|
13
14
|
from hypothesis.stateful import Rule
|
15
|
+
from requests.exceptions import ChunkedEncodingError
|
14
16
|
from requests.structures import CaseInsensitiveDict
|
15
17
|
|
16
18
|
from schemathesis.checks import CheckContext, CheckFunction, run_checks
|
@@ -19,19 +21,26 @@ from schemathesis.core.transport import Response
|
|
19
21
|
from schemathesis.engine import Status, events
|
20
22
|
from schemathesis.engine.context import EngineContext
|
21
23
|
from schemathesis.engine.control import ExecutionControl
|
24
|
+
from schemathesis.engine.errors import (
|
25
|
+
TestingState,
|
26
|
+
UnrecoverableNetworkError,
|
27
|
+
clear_hypothesis_notes,
|
28
|
+
is_unrecoverable_network_error,
|
29
|
+
)
|
22
30
|
from schemathesis.engine.phases import PhaseName
|
23
31
|
from schemathesis.engine.phases.stateful.context import StatefulContext
|
24
32
|
from schemathesis.engine.recorder import ScenarioRecorder
|
25
33
|
from schemathesis.generation import overrides
|
26
34
|
from schemathesis.generation.case import Case
|
27
35
|
from schemathesis.generation.hypothesis.reporting import ignore_hypothesis_output
|
36
|
+
from schemathesis.generation.stateful import STATEFUL_TESTS_LABEL
|
28
37
|
from schemathesis.generation.stateful.state_machine import (
|
29
38
|
DEFAULT_STATE_MACHINE_SETTINGS,
|
30
39
|
APIStateMachine,
|
31
40
|
StepInput,
|
32
41
|
StepOutput,
|
33
42
|
)
|
34
|
-
from schemathesis.generation.
|
43
|
+
from schemathesis.generation.metrics import MetricCollector
|
35
44
|
|
36
45
|
|
37
46
|
def _get_hypothesis_settings_kwargs_override(settings: hypothesis.settings) -> dict[str, Any]:
|
@@ -72,7 +81,8 @@ def execute_state_machine_loop(
|
|
72
81
|
hypothesis_settings = hypothesis.settings(configured_hypothesis_settings, **kwargs)
|
73
82
|
generation = engine.config.generation_for(phase="stateful")
|
74
83
|
|
75
|
-
ctx = StatefulContext(metric_collector=
|
84
|
+
ctx = StatefulContext(metric_collector=MetricCollector(metrics=generation.maximize))
|
85
|
+
state = TestingState()
|
76
86
|
|
77
87
|
# Caches for validate_response to avoid repeated config lookups per operation
|
78
88
|
_check_context_cache: dict[str, CachedCheckContextData] = {}
|
@@ -123,6 +133,20 @@ def execute_state_machine_loop(
|
|
123
133
|
ctx.step_failed()
|
124
134
|
raise
|
125
135
|
except Exception as exc:
|
136
|
+
if isinstance(exc, (requests.ConnectionError, ChunkedEncodingError)) and is_unrecoverable_network_error(
|
137
|
+
exc
|
138
|
+
):
|
139
|
+
transport_kwargs = engine.get_transport_kwargs(operation=input.case.operation)
|
140
|
+
if exc.request is not None:
|
141
|
+
headers = {key: value[0] for key, value in exc.request.headers.items()}
|
142
|
+
else:
|
143
|
+
headers = {**dict(input.case.headers or {}), **transport_kwargs.get("headers", {})}
|
144
|
+
verify = transport_kwargs.get("verify", True)
|
145
|
+
state.unrecoverable_network_error = UnrecoverableNetworkError(
|
146
|
+
error=exc,
|
147
|
+
code_sample=input.case.as_curl_command(headers=headers, verify=verify),
|
148
|
+
)
|
149
|
+
|
126
150
|
if generation.unique_inputs:
|
127
151
|
ctx.store_step_outcome(input.case, exc)
|
128
152
|
ctx.step_errored()
|
@@ -172,7 +196,7 @@ def execute_state_machine_loop(
|
|
172
196
|
case=case,
|
173
197
|
stateful_ctx=ctx,
|
174
198
|
check_ctx=check_ctx,
|
175
|
-
checks=check_ctx.
|
199
|
+
checks=check_ctx._checks,
|
176
200
|
control=engine.control,
|
177
201
|
recorder=self.recorder,
|
178
202
|
additional_checks=additional_checks,
|
@@ -259,11 +283,20 @@ def execute_state_machine_loop(
|
|
259
283
|
# Avoid infinite restarts
|
260
284
|
break
|
261
285
|
continue
|
286
|
+
clear_hypothesis_notes(exc)
|
262
287
|
# Any other exception is an inner error and the test run should be stopped
|
263
288
|
suite_status = Status.ERROR
|
289
|
+
code_sample: str | None = None
|
290
|
+
if state.unrecoverable_network_error is not None:
|
291
|
+
exc = state.unrecoverable_network_error.error
|
292
|
+
code_sample = state.unrecoverable_network_error.code_sample
|
264
293
|
event_queue.put(
|
265
294
|
events.NonFatalError(
|
266
|
-
error=exc,
|
295
|
+
error=exc,
|
296
|
+
phase=PhaseName.STATEFUL_TESTING,
|
297
|
+
label=STATEFUL_TESTS_LABEL,
|
298
|
+
related_to_operation=False,
|
299
|
+
code_sample=code_sample,
|
267
300
|
)
|
268
301
|
)
|
269
302
|
break
|
@@ -7,7 +7,7 @@ from schemathesis.core.failures import Failure
|
|
7
7
|
from schemathesis.core.transport import Response
|
8
8
|
from schemathesis.engine import Status
|
9
9
|
from schemathesis.generation.case import Case
|
10
|
-
from schemathesis.generation.
|
10
|
+
from schemathesis.generation.metrics import MetricCollector
|
11
11
|
|
12
12
|
|
13
13
|
@dataclass
|
@@ -27,7 +27,7 @@ class StatefulContext:
|
|
27
27
|
# The total number of completed test scenario
|
28
28
|
completed_scenarios: int = 0
|
29
29
|
# Metrics collector for targeted testing
|
30
|
-
metric_collector:
|
30
|
+
metric_collector: MetricCollector = field(default_factory=MetricCollector)
|
31
31
|
step_outcomes: dict[int, BaseException | None] = field(default_factory=dict)
|
32
32
|
|
33
33
|
@property
|
@@ -11,6 +11,7 @@ from hypothesis.errors import InvalidArgument
|
|
11
11
|
from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
|
12
12
|
from jsonschema.exceptions import SchemaError as JsonSchemaError
|
13
13
|
from jsonschema.exceptions import ValidationError
|
14
|
+
from requests.exceptions import ChunkedEncodingError
|
14
15
|
from requests.structures import CaseInsensitiveDict
|
15
16
|
|
16
17
|
from schemathesis.checks import CheckContext, run_checks
|
@@ -33,13 +34,17 @@ from schemathesis.engine import Status, events
|
|
33
34
|
from schemathesis.engine.context import EngineContext
|
34
35
|
from schemathesis.engine.errors import (
|
35
36
|
DeadlineExceeded,
|
37
|
+
TestingState,
|
36
38
|
UnexpectedError,
|
39
|
+
UnrecoverableNetworkError,
|
37
40
|
UnsupportedRecursiveReference,
|
41
|
+
clear_hypothesis_notes,
|
38
42
|
deduplicate_errors,
|
43
|
+
is_unrecoverable_network_error,
|
39
44
|
)
|
40
45
|
from schemathesis.engine.phases import PhaseName
|
41
46
|
from schemathesis.engine.recorder import ScenarioRecorder
|
42
|
-
from schemathesis.generation import
|
47
|
+
from schemathesis.generation import metrics, overrides
|
43
48
|
from schemathesis.generation.case import Case
|
44
49
|
from schemathesis.generation.hypothesis.builder import (
|
45
50
|
InvalidHeadersExampleMark,
|
@@ -70,9 +75,12 @@ def run_test(
|
|
70
75
|
skip_reason = None
|
71
76
|
test_start_time = time.monotonic()
|
72
77
|
recorder = ScenarioRecorder(label=operation.label)
|
78
|
+
state = TestingState()
|
73
79
|
|
74
|
-
def non_fatal_error(error: Exception) -> events.NonFatalError:
|
75
|
-
return events.NonFatalError(
|
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
|
+
)
|
76
84
|
|
77
85
|
def scenario_finished(status: Status) -> events.ScenarioFinished:
|
78
86
|
return events.ScenarioFinished(
|
@@ -111,6 +119,7 @@ def run_test(
|
|
111
119
|
with catch_warnings(record=True) as warnings, ignore_hypothesis_output():
|
112
120
|
test_function(
|
113
121
|
ctx=ctx,
|
122
|
+
state=state,
|
114
123
|
errors=errors,
|
115
124
|
check_ctx=check_ctx,
|
116
125
|
recorder=recorder,
|
@@ -198,6 +207,7 @@ def run_test(
|
|
198
207
|
yield non_fatal_error(InvalidRegexPattern.from_schema_error(exc, from_examples=False))
|
199
208
|
except Exception as exc:
|
200
209
|
status = Status.ERROR
|
210
|
+
clear_hypothesis_notes(exc)
|
201
211
|
# Likely a YAML parsing issue. E.g. `00:00:00.00` (without quotes) is parsed as float `0.0`
|
202
212
|
if str(exc) == "first argument must be string or compiled pattern":
|
203
213
|
yield non_fatal_error(
|
@@ -207,7 +217,10 @@ def run_test(
|
|
207
217
|
)
|
208
218
|
)
|
209
219
|
else:
|
210
|
-
|
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)
|
211
224
|
if (
|
212
225
|
status == Status.SUCCESS
|
213
226
|
and continue_on_failure
|
@@ -270,6 +283,7 @@ def cached_test_func(f: Callable) -> Callable:
|
|
270
283
|
def wrapped(
|
271
284
|
*,
|
272
285
|
ctx: EngineContext,
|
286
|
+
state: TestingState,
|
273
287
|
case: Case,
|
274
288
|
errors: list[Exception],
|
275
289
|
check_ctx: CheckContext,
|
@@ -313,6 +327,21 @@ def cached_test_func(f: Callable) -> Callable:
|
|
313
327
|
except (KeyboardInterrupt, Failure):
|
314
328
|
raise
|
315
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
|
316
345
|
errors.append(exc)
|
317
346
|
raise UnexpectedError from None
|
318
347
|
|
@@ -334,14 +363,14 @@ def test_func(
|
|
334
363
|
recorder.record_case(parent_id=None, transition=None, case=case)
|
335
364
|
try:
|
336
365
|
response = case.call(**transport_kwargs)
|
337
|
-
except (requests.Timeout, requests.ConnectionError) as error:
|
366
|
+
except (requests.Timeout, requests.ConnectionError, ChunkedEncodingError) as error:
|
338
367
|
if isinstance(error.request, requests.Request):
|
339
368
|
recorder.record_request(case_id=case.id, request=error.request.prepare())
|
340
369
|
elif isinstance(error.request, requests.PreparedRequest):
|
341
370
|
recorder.record_request(case_id=case.id, request=error.request)
|
342
371
|
raise
|
343
372
|
recorder.record_response(case_id=case.id, response=response)
|
344
|
-
|
373
|
+
metrics.maximize(generation.maximize, case=case, response=response)
|
345
374
|
validate_response(
|
346
375
|
case=case,
|
347
376
|
ctx=check_ctx,
|
@@ -378,7 +407,7 @@ def validate_response(
|
|
378
407
|
case=case,
|
379
408
|
response=response,
|
380
409
|
ctx=ctx,
|
381
|
-
checks=ctx.
|
410
|
+
checks=ctx._checks,
|
382
411
|
on_failure=on_failure,
|
383
412
|
on_success=on_success,
|
384
413
|
)
|
@@ -10,9 +10,6 @@ __all__ = [
|
|
10
10
|
]
|
11
11
|
|
12
12
|
|
13
|
-
DEFAULT_GENERATOR_MODES = [GenerationMode.default()]
|
14
|
-
|
15
|
-
|
16
13
|
CASE_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
17
14
|
BASE = len(CASE_ID_ALPHABET)
|
18
15
|
# Separate `Random` as Hypothesis might interfere with the default one
|