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
@@ -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.targets import TargetMetricCollector
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=TargetMetricCollector(targets=generation.maximize))
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.checks,
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, phase=PhaseName.STATEFUL_TESTING, label="Stateful tests", related_to_operation=False
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.targets import TargetMetricCollector
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: TargetMetricCollector = field(default_factory=TargetMetricCollector)
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 overrides, targets
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(error=error, phase=phase, label=operation.label, related_to_operation=True)
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
- yield non_fatal_error(exc)
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
- targets.run(generation.maximize, case=case, response=response)
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.checks,
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
@@ -167,6 +167,7 @@ class Case:
167
167
  curl=curl,
168
168
  config=self.operation.schema.config.output,
169
169
  )
170
+ message += "\n\n"
170
171
  raise FailureGroup(_failures, message) from None
171
172
 
172
173
  def call_and_validate(
@@ -121,7 +121,7 @@ class CoverageContext:
121
121
  path: list[str | int] | None = None,
122
122
  ) -> None:
123
123
  self.location = location
124
- self.generation_modes = generation_modes if generation_modes is not None else GenerationMode.all()
124
+ self.generation_modes = generation_modes if generation_modes is not None else list(GenerationMode)
125
125
  self.path = path or []
126
126
 
127
127
  @contextmanager
@@ -9,7 +9,7 @@ from time import perf_counter
9
9
  from typing import Any, Callable, Generator, Mapping
10
10
 
11
11
  import hypothesis
12
- from hypothesis import Phase
12
+ from hypothesis import Phase, Verbosity
13
13
  from hypothesis import strategies as st
14
14
  from hypothesis._settings import all_settings
15
15
  from hypothesis.errors import Unsatisfiable
@@ -27,6 +27,7 @@ from schemathesis.core.validation import has_invalid_characters, is_latin_1_enco
27
27
  from schemathesis.generation import GenerationMode, coverage
28
28
  from schemathesis.generation.case import Case
29
29
  from schemathesis.generation.hypothesis import DEFAULT_DEADLINE, examples, setup, strategies
30
+ from schemathesis.generation.hypothesis.examples import add_single_example
30
31
  from schemathesis.generation.hypothesis.given import GivenInput
31
32
  from schemathesis.generation.meta import (
32
33
  CaseMetadata,
@@ -96,6 +97,9 @@ def create_test(
96
97
  if settings.deadline == default.deadline:
97
98
  settings = hypothesis.settings(settings, deadline=DEFAULT_DEADLINE)
98
99
 
100
+ if settings.verbosity == default.verbosity:
101
+ settings = hypothesis.settings(settings, verbosity=Verbosity.quiet)
102
+
99
103
  if config.settings is not None:
100
104
  # Merge the user-provided settings with the current ones
101
105
  settings = hypothesis.settings(
@@ -126,7 +130,14 @@ def create_test(
126
130
  and Phase.explicit in settings.phases
127
131
  and specification.supports_feature(SpecificationFeature.EXAMPLES)
128
132
  ):
129
- hypothesis_test = add_examples(hypothesis_test, operation, hook_dispatcher=hook_dispatcher, **strategy_kwargs)
133
+ phases_config = config.project.phases_for(operation=operation)
134
+ hypothesis_test = add_examples(
135
+ hypothesis_test,
136
+ operation,
137
+ fill_missing=phases_config.examples.fill_missing,
138
+ hook_dispatcher=hook_dispatcher,
139
+ **strategy_kwargs,
140
+ )
130
141
 
131
142
  if (
132
143
  HypothesisTestMode.COVERAGE in config.modes
@@ -142,7 +153,8 @@ def create_test(
142
153
  generation.modes,
143
154
  auth_storage,
144
155
  config.as_strategy_kwargs,
145
- phases_config.coverage.unexpected_methods,
156
+ generate_duplicate_query_parameters=phases_config.coverage.generate_duplicate_query_parameters,
157
+ unexpected_methods=phases_config.coverage.unexpected_methods,
146
158
  )
147
159
 
148
160
  setattr(hypothesis_test, SETTINGS_ATTRIBUTE_NAME, settings)
@@ -188,7 +200,11 @@ def make_async_test(test: Callable) -> Callable:
188
200
 
189
201
 
190
202
  def add_examples(
191
- test: Callable, operation: APIOperation, hook_dispatcher: HookDispatcher | None = None, **kwargs: Any
203
+ test: Callable,
204
+ operation: APIOperation,
205
+ fill_missing: bool,
206
+ hook_dispatcher: HookDispatcher | None = None,
207
+ **kwargs: Any,
192
208
  ) -> Callable:
193
209
  """Add examples to the Hypothesis test, if they are specified in the schema."""
194
210
  from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
@@ -212,7 +228,11 @@ def add_examples(
212
228
  if isinstance(exc, SchemaError):
213
229
  InvalidRegexMark.set(test, exc)
214
230
 
215
- context = HookContext(operation) # context should be passed here instead
231
+ if fill_missing and not result:
232
+ strategy = operation.as_strategy()
233
+ add_single_example(strategy, result)
234
+
235
+ context = HookContext(operation=operation) # context should be passed here instead
216
236
  GLOBAL_HOOK_DISPATCHER.dispatch("before_add_examples", context, result)
217
237
  operation.schema.hooks.dispatch("before_add_examples", context, result)
218
238
  if hook_dispatcher:
@@ -246,6 +266,7 @@ def add_coverage(
246
266
  generation_modes: list[GenerationMode],
247
267
  auth_storage: AuthStorage | None,
248
268
  as_strategy_kwargs: dict[str, Any],
269
+ generate_duplicate_query_parameters: bool,
249
270
  unexpected_methods: set[str] | None = None,
250
271
  ) -> Callable:
251
272
  from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
@@ -259,7 +280,9 @@ def add_coverage(
259
280
  for container in LOCATION_TO_CONTAINER.values()
260
281
  if container in as_strategy_kwargs
261
282
  }
262
- for case in _iter_coverage_cases(operation, generation_modes, unexpected_methods):
283
+ for case in _iter_coverage_cases(
284
+ operation, generation_modes, generate_duplicate_query_parameters, unexpected_methods
285
+ ):
263
286
  if case.media_type and operation.schema.transport.get_first_matching_media_type(case.media_type) is None:
264
287
  continue
265
288
  adjust_urlencoded_payload(case)
@@ -396,6 +419,7 @@ def _stringify_value(val: Any, container_name: str) -> Any:
396
419
  def _iter_coverage_cases(
397
420
  operation: APIOperation,
398
421
  generation_modes: list[GenerationMode],
422
+ generate_duplicate_query_parameters: bool,
399
423
  unexpected_methods: set[str] | None = None,
400
424
  ) -> Generator[Case, None, None]:
401
425
  from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
@@ -549,7 +573,7 @@ def _iter_coverage_cases(
549
573
  ),
550
574
  )
551
575
  # Generate duplicate query parameters
552
- if operation.query:
576
+ if generate_duplicate_query_parameters and operation.query:
553
577
  container = template["query"]
554
578
  for parameter in operation.query:
555
579
  instant = Instant()
@@ -0,0 +1,93 @@
1
+ """Support for Targeted Property-Based Testing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Callable, Sequence
7
+
8
+ from schemathesis.core.registries import Registry
9
+ from schemathesis.core.transport import Response
10
+ from schemathesis.generation.case import Case
11
+
12
+
13
+ @dataclass
14
+ class MetricContext:
15
+ """Context for evaluating a metric on a single test execution.
16
+
17
+ This object bundles together the test `case` that was sent and
18
+ the corresponding HTTP `response`. Metric functions receive an
19
+ instance of `MetricContext` to compute a numeric score.
20
+ """
21
+
22
+ case: Case
23
+ """Generated test case."""
24
+ response: Response
25
+ """The HTTP response returned by the server for this test case."""
26
+
27
+ __slots__ = ("case", "response")
28
+
29
+
30
+ MetricFunction = Callable[[MetricContext], float]
31
+
32
+ METRICS = Registry[MetricFunction]()
33
+
34
+
35
+ def metric(func: MetricFunction) -> MetricFunction:
36
+ """Decorator to register a custom metric for targeted property-based testing.
37
+
38
+ Example:
39
+ ```python
40
+ import schemathesis
41
+
42
+ @schemathesis.metric
43
+ def response_size(ctx: schemathesis.MetricContext) -> float:
44
+ return float(len(ctx.response.content))
45
+ ```
46
+
47
+ """
48
+ return METRICS.register(func)
49
+
50
+
51
+ @metric
52
+ def response_time(ctx: MetricContext) -> float:
53
+ """Response time as a metric to maximize."""
54
+ return ctx.response.elapsed
55
+
56
+
57
+ class MetricCollector:
58
+ """Collect multiple observations for metrics."""
59
+
60
+ __slots__ = ("metrics", "observations")
61
+
62
+ def __init__(self, metrics: list[MetricFunction] | None = None) -> None:
63
+ self.metrics = metrics or []
64
+ self.observations: dict[str, list[float]] = {metric.__name__: [] for metric in self.metrics}
65
+
66
+ def reset(self) -> None:
67
+ """Reset all collected observations."""
68
+ for metric in self.metrics:
69
+ self.observations[metric.__name__].clear()
70
+
71
+ def store(self, case: Case, response: Response) -> None:
72
+ """Calculate metrics & store them."""
73
+ ctx = MetricContext(case=case, response=response)
74
+ for metric in self.metrics:
75
+ self.observations[metric.__name__].append(metric(ctx))
76
+
77
+ def maximize(self) -> None:
78
+ """Give feedback to the Hypothesis engine, so it maximizes the aggregated metrics."""
79
+ import hypothesis
80
+
81
+ for metric in self.metrics:
82
+ # Currently aggregation is just a sum
83
+ value = sum(self.observations[metric.__name__])
84
+ hypothesis.target(value, label=metric.__name__)
85
+
86
+
87
+ def maximize(metrics: Sequence[MetricFunction], case: Case, response: Response) -> None:
88
+ import hypothesis
89
+
90
+ ctx = MetricContext(case=case, response=response)
91
+ for metric in metrics:
92
+ value = metric(ctx)
93
+ hypothesis.target(value, label=metric.__name__)
@@ -11,14 +11,6 @@ class GenerationMode(str, Enum):
11
11
  # Doesn't fit the API schema
12
12
  NEGATIVE = "negative"
13
13
 
14
- @classmethod
15
- def default(cls) -> GenerationMode:
16
- return cls.POSITIVE
17
-
18
- @classmethod
19
- def all(cls) -> list[GenerationMode]:
20
- return list(GenerationMode)
21
-
22
14
  @property
23
15
  def is_positive(self) -> bool:
24
16
  return self == GenerationMode.POSITIVE
@@ -7,6 +7,8 @@ if TYPE_CHECKING:
7
7
 
8
8
  from schemathesis.generation.stateful.state_machine import APIStateMachine
9
9
 
10
+ STATEFUL_TESTS_LABEL = "Stateful tests"
11
+
10
12
 
11
13
  def run_state_machine_as_test(
12
14
  state_machine_factory: type[APIStateMachine], *, settings: hypothesis.settings | None = None
@@ -17,4 +19,6 @@ def run_state_machine_as_test(
17
19
  """
18
20
  from hypothesis.stateful import run_state_machine_as_test as _run_state_machine_as_test
19
21
 
22
+ __tracebackhide__ = True
23
+
20
24
  return _run_state_machine_as_test(state_machine_factory, settings=settings, _min_steps=2)
@@ -196,6 +196,7 @@ class APIStateMachine(RuleBasedStateMachine):
196
196
  """Run state machine as a test."""
197
197
  from . import run_state_machine_as_test
198
198
 
199
+ __tracebackhide__ = True
199
200
  return run_state_machine_as_test(cls, settings=settings)
200
201
 
201
202
  def setup(self) -> None: