schemathesis 4.0.0a11__py3-none-any.whl → 4.0.0a12__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.
Files changed (62) hide show
  1. schemathesis/__init__.py +28 -25
  2. schemathesis/auths.py +65 -24
  3. schemathesis/checks.py +60 -36
  4. schemathesis/cli/commands/run/__init__.py +23 -21
  5. schemathesis/cli/commands/run/context.py +6 -1
  6. schemathesis/cli/commands/run/events.py +7 -1
  7. schemathesis/cli/commands/run/executor.py +12 -7
  8. schemathesis/cli/commands/run/handlers/output.py +175 -80
  9. schemathesis/cli/commands/run/validation.py +21 -6
  10. schemathesis/config/__init__.py +2 -1
  11. schemathesis/config/_generation.py +12 -13
  12. schemathesis/config/_operations.py +14 -0
  13. schemathesis/config/_phases.py +41 -5
  14. schemathesis/config/_projects.py +28 -0
  15. schemathesis/config/_report.py +6 -2
  16. schemathesis/config/_warnings.py +25 -0
  17. schemathesis/config/schema.json +49 -1
  18. schemathesis/core/errors.py +5 -2
  19. schemathesis/core/transport.py +36 -1
  20. schemathesis/engine/context.py +1 -0
  21. schemathesis/engine/errors.py +60 -1
  22. schemathesis/engine/events.py +10 -2
  23. schemathesis/engine/phases/probes.py +3 -0
  24. schemathesis/engine/phases/stateful/__init__.py +2 -1
  25. schemathesis/engine/phases/stateful/_executor.py +38 -5
  26. schemathesis/engine/phases/stateful/context.py +2 -2
  27. schemathesis/engine/phases/unit/_executor.py +36 -7
  28. schemathesis/generation/__init__.py +0 -3
  29. schemathesis/generation/case.py +1 -0
  30. schemathesis/generation/coverage.py +1 -1
  31. schemathesis/generation/hypothesis/builder.py +31 -7
  32. schemathesis/generation/metrics.py +93 -0
  33. schemathesis/generation/modes.py +0 -8
  34. schemathesis/generation/stateful/__init__.py +4 -0
  35. schemathesis/generation/stateful/state_machine.py +1 -0
  36. schemathesis/graphql/loaders.py +138 -4
  37. schemathesis/hooks.py +62 -35
  38. schemathesis/openapi/loaders.py +120 -4
  39. schemathesis/pytest/loaders.py +24 -0
  40. schemathesis/pytest/plugin.py +22 -0
  41. schemathesis/schemas.py +9 -6
  42. schemathesis/specs/graphql/scalars.py +37 -3
  43. schemathesis/specs/graphql/schemas.py +12 -3
  44. schemathesis/specs/openapi/_hypothesis.py +14 -20
  45. schemathesis/specs/openapi/checks.py +21 -18
  46. schemathesis/specs/openapi/formats.py +30 -3
  47. schemathesis/specs/openapi/media_types.py +44 -1
  48. schemathesis/specs/openapi/schemas.py +8 -2
  49. schemathesis/specs/openapi/stateful/__init__.py +2 -1
  50. schemathesis/transport/__init__.py +54 -16
  51. schemathesis/transport/prepare.py +31 -7
  52. schemathesis/transport/requests.py +9 -8
  53. schemathesis/transport/wsgi.py +8 -8
  54. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0a12.dist-info}/METADATA +44 -90
  55. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0a12.dist-info}/RECORD +58 -60
  56. schemathesis/contrib/__init__.py +0 -9
  57. schemathesis/contrib/openapi/__init__.py +0 -9
  58. schemathesis/contrib/openapi/fill_missing_examples.py +0 -20
  59. schemathesis/generation/targets.py +0 -69
  60. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0a12.dist-info}/WHEEL +0 -0
  61. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0a12.dist-info}/entry_points.txt +0 -0
  62. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0a12.dist-info}/licenses/LICENSE +0 -0
@@ -39,24 +39,59 @@ class PhaseConfig(DiffBase):
39
39
  )
40
40
 
41
41
 
42
+ @dataclass(repr=False)
43
+ class ExamplesPhaseConfig(DiffBase):
44
+ enabled: bool
45
+ fill_missing: bool
46
+ generation: GenerationConfig
47
+ checks: ChecksConfig
48
+
49
+ __slots__ = ("enabled", "fill_missing", "generation", "checks")
50
+
51
+ def __init__(
52
+ self,
53
+ *,
54
+ enabled: bool = True,
55
+ fill_missing: bool = False,
56
+ generation: GenerationConfig | None = None,
57
+ checks: ChecksConfig | None = None,
58
+ ) -> None:
59
+ self.enabled = enabled
60
+ self.fill_missing = fill_missing
61
+ self.generation = generation or GenerationConfig()
62
+ self.checks = checks or ChecksConfig()
63
+
64
+ @classmethod
65
+ def from_dict(cls, data: dict[str, Any]) -> ExamplesPhaseConfig:
66
+ return cls(
67
+ enabled=data.get("enabled", True),
68
+ fill_missing=data.get("fill-missing", False),
69
+ generation=GenerationConfig.from_dict(data.get("generation", {})),
70
+ checks=ChecksConfig.from_dict(data.get("checks", {})),
71
+ )
72
+
73
+
42
74
  @dataclass(repr=False)
43
75
  class CoveragePhaseConfig(DiffBase):
44
76
  enabled: bool
77
+ generate_duplicate_query_parameters: bool
45
78
  generation: GenerationConfig
46
79
  checks: ChecksConfig
47
80
  unexpected_methods: set[str]
48
81
 
49
- __slots__ = ("enabled", "generation", "checks", "unexpected_methods")
82
+ __slots__ = ("enabled", "generate_duplicate_query_parameters", "generation", "checks", "unexpected_methods")
50
83
 
51
84
  def __init__(
52
85
  self,
53
86
  *,
54
87
  enabled: bool = True,
88
+ generate_duplicate_query_parameters: bool = False,
55
89
  generation: GenerationConfig | None = None,
56
90
  checks: ChecksConfig | None = None,
57
91
  unexpected_methods: set[str] | None = None,
58
92
  ) -> None:
59
93
  self.enabled = enabled
94
+ self.generate_duplicate_query_parameters = generate_duplicate_query_parameters
60
95
  self.unexpected_methods = unexpected_methods or DEFAULT_UNEXPECTED_METHODS
61
96
  self.generation = generation or GenerationConfig()
62
97
  self.checks = checks or ChecksConfig()
@@ -65,6 +100,7 @@ class CoveragePhaseConfig(DiffBase):
65
100
  def from_dict(cls, data: dict[str, Any]) -> CoveragePhaseConfig:
66
101
  return cls(
67
102
  enabled=data.get("enabled", True),
103
+ generate_duplicate_query_parameters=data.get("generate-duplicate-query-parameters", False),
68
104
  unexpected_methods={method.lower() for method in data.get("unexpected-methods", [])}
69
105
  if "unexpected-methods" in data
70
106
  else None,
@@ -107,7 +143,7 @@ class StatefulPhaseConfig(DiffBase):
107
143
 
108
144
  @dataclass(repr=False)
109
145
  class PhasesConfig(DiffBase):
110
- examples: PhaseConfig
146
+ examples: ExamplesPhaseConfig
111
147
  coverage: CoveragePhaseConfig
112
148
  fuzzing: PhaseConfig
113
149
  stateful: StatefulPhaseConfig
@@ -117,12 +153,12 @@ class PhasesConfig(DiffBase):
117
153
  def __init__(
118
154
  self,
119
155
  *,
120
- examples: PhaseConfig | None = None,
156
+ examples: ExamplesPhaseConfig | None = None,
121
157
  coverage: CoveragePhaseConfig | None = None,
122
158
  fuzzing: PhaseConfig | None = None,
123
159
  stateful: StatefulPhaseConfig | None = None,
124
160
  ) -> None:
125
- self.examples = examples or PhaseConfig()
161
+ self.examples = examples or ExamplesPhaseConfig()
126
162
  self.coverage = coverage or CoveragePhaseConfig()
127
163
  self.fuzzing = fuzzing or PhaseConfig()
128
164
  self.stateful = stateful or StatefulPhaseConfig()
@@ -138,7 +174,7 @@ class PhasesConfig(DiffBase):
138
174
  @classmethod
139
175
  def from_dict(cls, data: dict[str, Any]) -> PhasesConfig:
140
176
  return cls(
141
- examples=PhaseConfig.from_dict(data.get("examples", {})),
177
+ examples=ExamplesPhaseConfig.from_dict(data.get("examples", {})),
142
178
  coverage=CoveragePhaseConfig.from_dict(data.get("coverage", {})),
143
179
  fuzzing=PhaseConfig.from_dict(data.get("fuzzing", {})),
144
180
  stateful=StatefulPhaseConfig.from_dict(data.get("stateful", {})),
@@ -17,6 +17,7 @@ from schemathesis.config._parameters import load_parameters
17
17
  from schemathesis.config._phases import PhasesConfig
18
18
  from schemathesis.config._rate_limit import build_limiter
19
19
  from schemathesis.config._report import ReportsConfig
20
+ from schemathesis.config._warnings import SchemathesisWarning, resolve_warnings
20
21
  from schemathesis.core import HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER, hooks
21
22
  from schemathesis.core.validation import validate_base_url
22
23
 
@@ -57,6 +58,7 @@ class ProjectConfig(DiffBase):
57
58
  request_cert: str | None
58
59
  request_cert_key: str | None
59
60
  parameters: dict[str, Any]
61
+ warnings: list[SchemathesisWarning] | None
60
62
  auth: AuthConfig
61
63
  checks: ChecksConfig
62
64
  phases: PhasesConfig
@@ -78,6 +80,7 @@ class ProjectConfig(DiffBase):
78
80
  "request_cert",
79
81
  "request_cert_key",
80
82
  "parameters",
83
+ "warnings",
81
84
  "auth",
82
85
  "checks",
83
86
  "phases",
@@ -101,6 +104,7 @@ class ProjectConfig(DiffBase):
101
104
  request_cert: str | None = None,
102
105
  request_cert_key: str | None = None,
103
106
  parameters: dict[str, Any] | None = None,
107
+ warnings: bool | list[SchemathesisWarning] | None = None,
104
108
  auth: AuthConfig | None = None,
105
109
  checks: ChecksConfig | None = None,
106
110
  phases: PhasesConfig | None = None,
@@ -133,6 +137,7 @@ class ProjectConfig(DiffBase):
133
137
  self.request_cert = request_cert
134
138
  self.request_cert_key = request_cert_key
135
139
  self.parameters = parameters or {}
140
+ self._set_warnings(warnings)
136
141
  self.auth = auth or AuthConfig()
137
142
  self.checks = checks or ChecksConfig()
138
143
  self.phases = phases or PhasesConfig()
@@ -157,6 +162,7 @@ class ProjectConfig(DiffBase):
157
162
  request_cert_key=resolve(data.get("request-cert-key")),
158
163
  parameters=load_parameters(data),
159
164
  auth=AuthConfig.from_dict(data.get("auth", {})),
165
+ warnings=resolve_warnings(data.get("warnings")),
160
166
  checks=ChecksConfig.from_dict(data.get("checks", {})),
161
167
  phases=PhasesConfig.from_dict(data.get("phases", {})),
162
168
  generation=GenerationConfig.from_dict(data.get("generation", {})),
@@ -165,6 +171,14 @@ class ProjectConfig(DiffBase):
165
171
  ),
166
172
  )
167
173
 
174
+ def _set_warnings(self, warnings: bool | list[SchemathesisWarning] | None) -> None:
175
+ if warnings is False:
176
+ self.warnings = []
177
+ elif warnings is True:
178
+ self.warnings = list(SchemathesisWarning)
179
+ else:
180
+ self.warnings = warnings
181
+
168
182
  def update(
169
183
  self,
170
184
  *,
@@ -180,6 +194,7 @@ class ProjectConfig(DiffBase):
180
194
  request_cert_key: str | None = None,
181
195
  proxy: str | None = None,
182
196
  suppress_health_check: list[HealthCheck] | None = None,
197
+ warnings: bool | list[SchemathesisWarning] | None = None,
183
198
  ) -> None:
184
199
  if base_url is not None:
185
200
  _validate_base_url(base_url)
@@ -223,6 +238,9 @@ class ProjectConfig(DiffBase):
223
238
  if suppress_health_check is not None:
224
239
  self.suppress_health_check = suppress_health_check
225
240
 
241
+ if warnings is not None:
242
+ self._set_warnings(warnings)
243
+
226
244
  def auth_for(self, *, operation: APIOperation | None = None) -> tuple[str, str] | None:
227
245
  """Get auth credentials, prioritizing operation-specific configs."""
228
246
  if operation is not None:
@@ -291,6 +309,16 @@ class ProjectConfig(DiffBase):
291
309
  return self.rate_limit
292
310
  return None
293
311
 
312
+ def warnings_for(self, *, operation: APIOperation | None = None) -> list[SchemathesisWarning]:
313
+ # Operation can be absent on some non-fatal errors due to schema parsing
314
+ if operation is not None:
315
+ config = self.operations.get_for_operation(operation=operation)
316
+ if config.warnings is not None:
317
+ return config.warnings
318
+ if self.warnings is None:
319
+ return list(SchemathesisWarning)
320
+ return self.warnings
321
+
294
322
  def phases_for(self, *, operation: APIOperation | None) -> PhasesConfig:
295
323
  configs = []
296
324
  if operation is not None:
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import datetime
3
4
  from dataclasses import dataclass
4
5
  from enum import Enum
5
6
  from pathlib import Path
@@ -55,8 +56,9 @@ class ReportsConfig(DiffBase):
55
56
  junit: ReportConfig
56
57
  vcr: ReportConfig
57
58
  har: ReportConfig
59
+ _timestamp: str
58
60
 
59
- __slots__ = ("directory", "preserve_bytes", "junit", "vcr", "har")
61
+ __slots__ = ("directory", "preserve_bytes", "junit", "vcr", "har", "_timestamp")
60
62
 
61
63
  def __init__(
62
64
  self,
@@ -72,6 +74,7 @@ class ReportsConfig(DiffBase):
72
74
  self.junit = junit or ReportConfig()
73
75
  self.vcr = vcr or ReportConfig()
74
76
  self.har = har or ReportConfig()
77
+ self._timestamp = datetime.datetime.now().strftime("%Y%m%dT%H%M%SZ")
75
78
 
76
79
  @classmethod
77
80
  def from_dict(cls, data: dict[str, Any]) -> ReportsConfig:
@@ -113,4 +116,5 @@ class ReportsConfig(DiffBase):
113
116
  report: ReportConfig = getattr(self, format.value)
114
117
  if report.path is not None:
115
118
  return report.path
116
- return self.directory / f"{format.value}.{format.extension}"
119
+
120
+ return self.directory / f"{format.value}-{self._timestamp}.{format.extension}"
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+
5
+ from schemathesis.config._env import resolve
6
+
7
+
8
+ class SchemathesisWarning(str, enum.Enum):
9
+ MISSING_AUTH = "missing_auth"
10
+ MISSING_TEST_DATA = "missing_test_data"
11
+ VALIDATION_MISMATCH = "validation_mismatch"
12
+
13
+ @classmethod
14
+ def from_str(cls, value: str) -> SchemathesisWarning:
15
+ return {
16
+ "missing_auth": cls.MISSING_AUTH,
17
+ "missing_test_data": cls.MISSING_TEST_DATA,
18
+ "validation_mismatch": cls.VALIDATION_MISMATCH,
19
+ }[value.lower()]
20
+
21
+
22
+ def resolve_warnings(value: bool | list[str] | None) -> bool | list[SchemathesisWarning] | None:
23
+ if isinstance(value, list):
24
+ return [SchemathesisWarning.from_str(resolve(item)) for item in value]
25
+ return value
@@ -121,6 +121,9 @@
121
121
  },
122
122
  "request-cert-key": {
123
123
  "type": "string"
124
+ },
125
+ "warnings": {
126
+ "$ref": "#/$defs/WarningConfig"
124
127
  }
125
128
  },
126
129
  "$defs": {
@@ -242,7 +245,7 @@
242
245
  "additionalProperties": false,
243
246
  "properties": {
244
247
  "examples": {
245
- "$ref": "#/$defs/PhaseConfig"
248
+ "$ref": "#/$defs/ExamplesPhaseConfig"
246
249
  },
247
250
  "coverage": {
248
251
  "$ref": "#/$defs/CoveragePhaseConfig"
@@ -270,6 +273,24 @@
270
273
  }
271
274
  }
272
275
  },
276
+ "ExamplesPhaseConfig": {
277
+ "type": "object",
278
+ "additionalProperties": false,
279
+ "properties": {
280
+ "enabled": {
281
+ "type": "boolean"
282
+ },
283
+ "fill-missing": {
284
+ "type": "boolean"
285
+ },
286
+ "generation": {
287
+ "$ref": "#/$defs/GenerationConfig"
288
+ },
289
+ "checks": {
290
+ "$ref": "#/$defs/ChecksConfig"
291
+ }
292
+ }
293
+ },
273
294
  "StatefulPhaseConfig": {
274
295
  "type": "object",
275
296
  "additionalProperties": false,
@@ -296,6 +317,9 @@
296
317
  "enabled": {
297
318
  "type": "boolean"
298
319
  },
320
+ "generate-duplicate-query-parameters": {
321
+ "type": "boolean"
322
+ },
299
323
  "unexpected-methods": {
300
324
  "type": "array",
301
325
  "items": {
@@ -494,12 +518,33 @@
494
518
  },
495
519
  "request-cert-key": {
496
520
  "type": "string"
521
+ },
522
+ "warnings": {
523
+ "$ref": "#/$defs/WarningConfig"
497
524
  }
498
525
  },
499
526
  "required": [
500
527
  "title"
501
528
  ]
502
529
  },
530
+ "WarningConfig": {
531
+ "anyOf": [
532
+ {
533
+ "type": "boolean"
534
+ },
535
+ {
536
+ "type": "array",
537
+ "uniqueItems": true,
538
+ "items": {
539
+ "enum": [
540
+ "missing_auth",
541
+ "missing_test_data",
542
+ "validation_mismatch"
543
+ ]
544
+ }
545
+ }
546
+ ]
547
+ },
503
548
  "OperationConfig": {
504
549
  "type": "object",
505
550
  "additionalProperties": false,
@@ -541,6 +586,9 @@
541
586
  "request-cert-key": {
542
587
  "type": "string"
543
588
  },
589
+ "warnings": {
590
+ "$ref": "#/$defs/WarningConfig"
591
+ },
544
592
  "checks": {
545
593
  "$ref": "#/$defs/ChecksConfig"
546
594
  },
@@ -403,7 +403,10 @@ def get_request_error_extras(exc: RequestException) -> list[str]:
403
403
  return [reason.strip()]
404
404
  return [" ".join(map(_clean_inner_request_message, inner.args))]
405
405
  if isinstance(exc, ChunkedEncodingError):
406
- return [str(exc.args[0].args[1])]
406
+ args = exc.args[0].args
407
+ if len(args) == 1:
408
+ return [str(args[0])]
409
+ return [str(args[1])]
407
410
  return []
408
411
 
409
412
 
@@ -438,7 +441,7 @@ def split_traceback(traceback: str) -> list[str]:
438
441
 
439
442
 
440
443
  def format_exception(
441
- error: Exception,
444
+ error: BaseException,
442
445
  *,
443
446
  with_traceback: bool = False,
444
447
  skip_frames: int = 0,
@@ -27,7 +27,30 @@ def prepare_urlencoded(data: Any) -> Any:
27
27
 
28
28
 
29
29
  class Response:
30
- """Unified response for both testing and reporting purposes."""
30
+ """HTTP response wrapper that normalizes different transport implementations.
31
+
32
+ Provides a consistent interface for accessing response data whether the request
33
+ was made via HTTP, ASGI, or WSGI transports.
34
+ """
35
+
36
+ status_code: int
37
+ """HTTP status code (e.g., 200, 404, 500)."""
38
+ headers: dict[str, list[str]]
39
+ """Response headers with lowercase keys and list values."""
40
+ content: bytes
41
+ """Raw response body as bytes."""
42
+ request: requests.PreparedRequest
43
+ """The request that generated this response."""
44
+ elapsed: float
45
+ """Response time in seconds."""
46
+ verify: bool
47
+ """Whether TLS verification was enabled for the request."""
48
+ message: str
49
+ """HTTP status message (e.g., "OK", "Not Found")."""
50
+ http_version: str
51
+ """HTTP protocol version ("1.0" or "1.1")."""
52
+ encoding: str | None
53
+ """Character encoding for text content, if detected."""
31
54
 
32
55
  __slots__ = (
33
56
  "status_code",
@@ -90,19 +113,31 @@ class Response:
90
113
 
91
114
  @property
92
115
  def text(self) -> str:
116
+ """Decode response content as text using the detected or default encoding."""
93
117
  return self.content.decode(self.encoding if self.encoding else "utf-8")
94
118
 
95
119
  def json(self) -> Any:
120
+ """Parse response content as JSON.
121
+
122
+ Returns:
123
+ Parsed JSON data (dict, list, or primitive types)
124
+
125
+ Raises:
126
+ json.JSONDecodeError: If content is not valid JSON
127
+
128
+ """
96
129
  if self._json is None:
97
130
  self._json = json.loads(self.text)
98
131
  return self._json
99
132
 
100
133
  @property
101
134
  def body_size(self) -> int | None:
135
+ """Size of response body in bytes, or None if no content."""
102
136
  return len(self.content) if self.content else None
103
137
 
104
138
  @property
105
139
  def encoded_body(self) -> str | None:
140
+ """Base64-encoded response body for binary-safe serialization."""
106
141
  if self._encoded_body is None and self.content:
107
142
  self._encoded_body = base64.b64encode(self.content).decode()
108
143
  return self._encoded_body
@@ -80,6 +80,7 @@ class EngineContext:
80
80
  import requests
81
81
 
82
82
  session = requests.Session()
83
+ session.headers = {}
83
84
  config = self.config
84
85
 
85
86
  session.verify = config.tls_verify_for(operation=operation)
@@ -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:
@@ -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
@@ -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__(self, *, error: Exception, phase: PhaseName, label: str, related_to_operation: bool) -> None:
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="Stateful tests", related_to_operation=False)
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