schemathesis 4.0.0a10__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 (111) hide show
  1. schemathesis/__init__.py +29 -30
  2. schemathesis/auths.py +65 -24
  3. schemathesis/checks.py +73 -39
  4. schemathesis/cli/commands/__init__.py +51 -3
  5. schemathesis/cli/commands/data.py +10 -0
  6. schemathesis/cli/commands/run/__init__.py +163 -274
  7. schemathesis/cli/commands/run/context.py +8 -4
  8. schemathesis/cli/commands/run/events.py +11 -1
  9. schemathesis/cli/commands/run/executor.py +70 -78
  10. schemathesis/cli/commands/run/filters.py +15 -165
  11. schemathesis/cli/commands/run/handlers/cassettes.py +105 -104
  12. schemathesis/cli/commands/run/handlers/junitxml.py +5 -4
  13. schemathesis/cli/commands/run/handlers/output.py +195 -121
  14. schemathesis/cli/commands/run/loaders.py +35 -50
  15. schemathesis/cli/commands/run/validation.py +52 -162
  16. schemathesis/cli/core.py +5 -3
  17. schemathesis/cli/ext/fs.py +7 -5
  18. schemathesis/cli/ext/options.py +0 -21
  19. schemathesis/config/__init__.py +189 -0
  20. schemathesis/config/_auth.py +51 -0
  21. schemathesis/config/_checks.py +268 -0
  22. schemathesis/config/_diff_base.py +99 -0
  23. schemathesis/config/_env.py +21 -0
  24. schemathesis/config/_error.py +156 -0
  25. schemathesis/config/_generation.py +149 -0
  26. schemathesis/config/_health_check.py +24 -0
  27. schemathesis/config/_operations.py +327 -0
  28. schemathesis/config/_output.py +171 -0
  29. schemathesis/config/_parameters.py +19 -0
  30. schemathesis/config/_phases.py +187 -0
  31. schemathesis/config/_projects.py +523 -0
  32. schemathesis/config/_rate_limit.py +17 -0
  33. schemathesis/config/_report.py +120 -0
  34. schemathesis/config/_validator.py +9 -0
  35. schemathesis/config/_warnings.py +25 -0
  36. schemathesis/config/schema.json +885 -0
  37. schemathesis/core/__init__.py +2 -0
  38. schemathesis/core/compat.py +16 -9
  39. schemathesis/core/errors.py +24 -4
  40. schemathesis/core/failures.py +6 -7
  41. schemathesis/core/hooks.py +20 -0
  42. schemathesis/core/output/__init__.py +14 -37
  43. schemathesis/core/output/sanitization.py +3 -146
  44. schemathesis/core/transport.py +36 -1
  45. schemathesis/core/validation.py +16 -0
  46. schemathesis/engine/__init__.py +2 -4
  47. schemathesis/engine/context.py +42 -43
  48. schemathesis/engine/core.py +7 -5
  49. schemathesis/engine/errors.py +60 -1
  50. schemathesis/engine/events.py +10 -2
  51. schemathesis/engine/phases/__init__.py +10 -0
  52. schemathesis/engine/phases/probes.py +11 -8
  53. schemathesis/engine/phases/stateful/__init__.py +2 -1
  54. schemathesis/engine/phases/stateful/_executor.py +104 -46
  55. schemathesis/engine/phases/stateful/context.py +2 -2
  56. schemathesis/engine/phases/unit/__init__.py +23 -15
  57. schemathesis/engine/phases/unit/_executor.py +110 -21
  58. schemathesis/engine/phases/unit/_pool.py +1 -1
  59. schemathesis/errors.py +2 -0
  60. schemathesis/filters.py +2 -3
  61. schemathesis/generation/__init__.py +5 -33
  62. schemathesis/generation/case.py +6 -3
  63. schemathesis/generation/coverage.py +154 -124
  64. schemathesis/generation/hypothesis/builder.py +70 -20
  65. schemathesis/generation/meta.py +3 -3
  66. schemathesis/generation/metrics.py +93 -0
  67. schemathesis/generation/modes.py +0 -8
  68. schemathesis/generation/overrides.py +37 -1
  69. schemathesis/generation/stateful/__init__.py +4 -0
  70. schemathesis/generation/stateful/state_machine.py +9 -1
  71. schemathesis/graphql/loaders.py +159 -16
  72. schemathesis/hooks.py +62 -35
  73. schemathesis/openapi/checks.py +12 -8
  74. schemathesis/openapi/generation/filters.py +10 -8
  75. schemathesis/openapi/loaders.py +142 -17
  76. schemathesis/pytest/lazy.py +2 -5
  77. schemathesis/pytest/loaders.py +24 -0
  78. schemathesis/pytest/plugin.py +33 -2
  79. schemathesis/schemas.py +21 -66
  80. schemathesis/specs/graphql/scalars.py +37 -3
  81. schemathesis/specs/graphql/schemas.py +23 -18
  82. schemathesis/specs/openapi/_hypothesis.py +26 -28
  83. schemathesis/specs/openapi/checks.py +37 -36
  84. schemathesis/specs/openapi/examples.py +4 -3
  85. schemathesis/specs/openapi/formats.py +32 -5
  86. schemathesis/specs/openapi/media_types.py +44 -1
  87. schemathesis/specs/openapi/negative/__init__.py +2 -2
  88. schemathesis/specs/openapi/patterns.py +46 -16
  89. schemathesis/specs/openapi/references.py +2 -3
  90. schemathesis/specs/openapi/schemas.py +19 -22
  91. schemathesis/specs/openapi/stateful/__init__.py +12 -6
  92. schemathesis/transport/__init__.py +54 -16
  93. schemathesis/transport/prepare.py +38 -13
  94. schemathesis/transport/requests.py +12 -9
  95. schemathesis/transport/wsgi.py +11 -12
  96. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/METADATA +50 -97
  97. schemathesis-4.0.0a12.dist-info/RECORD +164 -0
  98. schemathesis/cli/commands/run/checks.py +0 -79
  99. schemathesis/cli/commands/run/hypothesis.py +0 -78
  100. schemathesis/cli/commands/run/reports.py +0 -72
  101. schemathesis/cli/hooks.py +0 -36
  102. schemathesis/contrib/__init__.py +0 -9
  103. schemathesis/contrib/openapi/__init__.py +0 -9
  104. schemathesis/contrib/openapi/fill_missing_examples.py +0 -20
  105. schemathesis/engine/config.py +0 -59
  106. schemathesis/experimental/__init__.py +0 -72
  107. schemathesis/generation/targets.py +0 -69
  108. schemathesis-4.0.0a10.dist-info/RECORD +0 -153
  109. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/WHEEL +0 -0
  110. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/entry_points.txt +0 -0
  111. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import os
5
4
  from dataclasses import dataclass, field
6
5
  from enum import Enum
7
6
  from functools import wraps
@@ -10,22 +9,25 @@ from time import perf_counter
10
9
  from typing import Any, Callable, Generator, Mapping
11
10
 
12
11
  import hypothesis
13
- from hypothesis import Phase
12
+ from hypothesis import Phase, Verbosity
14
13
  from hypothesis import strategies as st
15
14
  from hypothesis._settings import all_settings
16
15
  from hypothesis.errors import Unsatisfiable
17
16
  from jsonschema.exceptions import SchemaError
17
+ from requests.models import CaseInsensitiveDict
18
18
 
19
19
  from schemathesis import auths
20
20
  from schemathesis.auths import AuthStorage, AuthStorageMark
21
- from schemathesis.core import NOT_SET, NotSet, SpecificationFeature, media_types, string_to_boolean
21
+ from schemathesis.config import ProjectConfig
22
+ from schemathesis.core import NOT_SET, NotSet, SpecificationFeature, media_types
22
23
  from schemathesis.core.errors import InvalidSchema, SerializationNotPossible
23
24
  from schemathesis.core.marks import Mark
24
25
  from schemathesis.core.transport import prepare_urlencoded
25
26
  from schemathesis.core.validation import has_invalid_characters, is_latin_1_encodable
26
- from schemathesis.generation import GenerationConfig, GenerationMode, coverage
27
+ from schemathesis.generation import GenerationMode, coverage
27
28
  from schemathesis.generation.case import Case
28
29
  from schemathesis.generation.hypothesis import DEFAULT_DEADLINE, examples, setup, strategies
30
+ from schemathesis.generation.hypothesis.examples import add_single_example
29
31
  from schemathesis.generation.hypothesis.given import GivenInput
30
32
  from schemathesis.generation.meta import (
31
33
  CaseMetadata,
@@ -49,7 +51,7 @@ class HypothesisTestMode(str, Enum):
49
51
 
50
52
  @dataclass
51
53
  class HypothesisTestConfig:
52
- generation: GenerationConfig
54
+ project: ProjectConfig
53
55
  modes: list[HypothesisTestMode]
54
56
  settings: hypothesis.settings | None = None
55
57
  seed: int | None = None
@@ -71,11 +73,11 @@ def create_test(
71
73
  strategy_kwargs = {
72
74
  "hooks": hook_dispatcher,
73
75
  "auth_storage": auth_storage,
74
- "generation_config": config.generation,
75
76
  **config.as_strategy_kwargs,
76
77
  }
78
+ generation = config.project.generation_for(operation=operation)
77
79
  strategy = strategies.combine(
78
- [operation.as_strategy(generation_mode=mode, **strategy_kwargs) for mode in config.generation.modes]
80
+ [operation.as_strategy(generation_mode=mode, **strategy_kwargs) for mode in generation.modes]
79
81
  )
80
82
 
81
83
  hypothesis_test = create_base_test(
@@ -95,6 +97,9 @@ def create_test(
95
97
  if settings.deadline == default.deadline:
96
98
  settings = hypothesis.settings(settings, deadline=DEFAULT_DEADLINE)
97
99
 
100
+ if settings.verbosity == default.verbosity:
101
+ settings = hypothesis.settings(settings, verbosity=Verbosity.quiet)
102
+
98
103
  if config.settings is not None:
99
104
  # Merge the user-provided settings with the current ones
100
105
  settings = hypothesis.settings(
@@ -125,25 +130,31 @@ def create_test(
125
130
  and Phase.explicit in settings.phases
126
131
  and specification.supports_feature(SpecificationFeature.EXAMPLES)
127
132
  ):
128
- hypothesis_test = add_examples(hypothesis_test, operation, hook_dispatcher=hook_dispatcher, **strategy_kwargs)
129
-
130
- disable_coverage = string_to_boolean(os.getenv("SCHEMATHESIS_DISABLE_COVERAGE", ""))
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
+ )
131
141
 
132
142
  if (
133
- not disable_coverage
134
- and HypothesisTestMode.COVERAGE in config.modes
143
+ HypothesisTestMode.COVERAGE in config.modes
135
144
  and Phase.explicit in settings.phases
136
145
  and specification.supports_feature(SpecificationFeature.COVERAGE)
137
146
  and not config.given_args
138
147
  and not config.given_kwargs
139
148
  ):
149
+ phases_config = config.project.phases_for(operation=operation)
140
150
  hypothesis_test = add_coverage(
141
151
  hypothesis_test,
142
152
  operation,
143
- config.generation.modes,
153
+ generation.modes,
144
154
  auth_storage,
145
155
  config.as_strategy_kwargs,
146
- config.generation.unexpected_methods,
156
+ generate_duplicate_query_parameters=phases_config.coverage.generate_duplicate_query_parameters,
157
+ unexpected_methods=phases_config.coverage.unexpected_methods,
147
158
  )
148
159
 
149
160
  setattr(hypothesis_test, SETTINGS_ATTRIBUTE_NAME, settings)
@@ -189,7 +200,11 @@ def make_async_test(test: Callable) -> Callable:
189
200
 
190
201
 
191
202
  def add_examples(
192
- 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,
193
208
  ) -> Callable:
194
209
  """Add examples to the Hypothesis test, if they are specified in the schema."""
195
210
  from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
@@ -213,7 +228,11 @@ def add_examples(
213
228
  if isinstance(exc, SchemaError):
214
229
  InvalidRegexMark.set(test, exc)
215
230
 
216
- 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
217
236
  GLOBAL_HOOK_DISPATCHER.dispatch("before_add_examples", context, result)
218
237
  operation.schema.hooks.dispatch("before_add_examples", context, result)
219
238
  if hook_dispatcher:
@@ -247,6 +266,7 @@ def add_coverage(
247
266
  generation_modes: list[GenerationMode],
248
267
  auth_storage: AuthStorage | None,
249
268
  as_strategy_kwargs: dict[str, Any],
269
+ generate_duplicate_query_parameters: bool,
250
270
  unexpected_methods: set[str] | None = None,
251
271
  ) -> Callable:
252
272
  from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
@@ -260,7 +280,9 @@ def add_coverage(
260
280
  for container in LOCATION_TO_CONTAINER.values()
261
281
  if container in as_strategy_kwargs
262
282
  }
263
- 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
+ ):
264
286
  if case.media_type and operation.schema.transport.get_first_matching_media_type(case.media_type) is None:
265
287
  continue
266
288
  adjust_urlencoded_payload(case)
@@ -388,7 +410,7 @@ def _stringify_value(val: Any, container_name: str) -> Any:
388
410
  # Having a list here ensures there will be multiple query parameters wit the same name
389
411
  return [_stringify_value(item, container_name) for item in val]
390
412
  # use comma-separated values style for arrays
391
- return ",".join(_stringify_value(sub, container_name) for sub in val)
413
+ return ",".join(str(_stringify_value(sub, container_name)) for sub in val)
392
414
  if isinstance(val, dict):
393
415
  return {key: _stringify_value(sub, container_name) for key, sub in val.items()}
394
416
  return val
@@ -397,6 +419,7 @@ def _stringify_value(val: Any, container_name: str) -> Any:
397
419
  def _iter_coverage_cases(
398
420
  operation: APIOperation,
399
421
  generation_modes: list[GenerationMode],
422
+ generate_duplicate_query_parameters: bool,
400
423
  unexpected_methods: set[str] | None = None,
401
424
  ) -> Generator[Case, None, None]:
402
425
  from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
@@ -411,6 +434,10 @@ def _iter_coverage_cases(
411
434
  responses = find_in_responses(operation)
412
435
  # NOTE: The HEAD method is excluded
413
436
  unexpected_methods = unexpected_methods or {"get", "put", "post", "delete", "options", "patch", "trace"}
437
+
438
+ seen_negative = coverage.HashSet()
439
+ seen_positive = coverage.HashSet()
440
+
414
441
  for parameter in operation.iter_parameters():
415
442
  location = parameter.location
416
443
  name = parameter.name
@@ -473,7 +500,7 @@ def _iter_coverage_cases(
473
500
  meta=CaseMetadata(
474
501
  generation=GenerationInfo(
475
502
  time=instant.elapsed,
476
- mode=value.generation_mode,
503
+ mode=next_value.generation_mode,
477
504
  ),
478
505
  components=data.components,
479
506
  phase=PhaseInfo.coverage(
@@ -488,6 +515,7 @@ def _iter_coverage_cases(
488
515
  break
489
516
  elif GenerationMode.POSITIVE in generation_modes:
490
517
  data = template.unmodified()
518
+ seen_positive.insert(data.kwargs)
491
519
  yield operation.Case(
492
520
  **data.kwargs,
493
521
  meta=CaseMetadata(
@@ -510,6 +538,12 @@ def _iter_coverage_cases(
510
538
  except StopIteration:
511
539
  break
512
540
 
541
+ if value.generation_mode == GenerationMode.NEGATIVE:
542
+ seen_negative.insert(data.kwargs)
543
+ elif value.generation_mode == GenerationMode.POSITIVE and not seen_positive.insert(data.kwargs):
544
+ # Was already generated before
545
+ continue
546
+
513
547
  yield operation.Case(
514
548
  **data.kwargs,
515
549
  meta=CaseMetadata(
@@ -539,7 +573,7 @@ def _iter_coverage_cases(
539
573
  ),
540
574
  )
541
575
  # Generate duplicate query parameters
542
- if operation.query:
576
+ if generate_duplicate_query_parameters and operation.query:
543
577
  container = template["query"]
544
578
  for parameter in operation.query:
545
579
  instant = Instant()
@@ -689,6 +723,9 @@ def _iter_coverage_cases(
689
723
  if GenerationMode.NEGATIVE in generation_modes:
690
724
  subschema = _combination_schema(only_required, required, parameter_set)
691
725
  for case in _yield_negative(subschema, location, container_name):
726
+ kwargs = _case_to_kwargs(case)
727
+ if not seen_negative.insert(kwargs):
728
+ continue
692
729
  assert case.meta is not None
693
730
  assert isinstance(case.meta.phase.data, CoveragePhaseData)
694
731
  # Already generated in one of the blocks above
@@ -738,6 +775,19 @@ def _iter_coverage_cases(
738
775
  )
739
776
 
740
777
 
778
+ def _case_to_kwargs(case: Case) -> dict:
779
+ from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
780
+
781
+ kwargs = {}
782
+ for container_name in LOCATION_TO_CONTAINER.values():
783
+ value = getattr(case, container_name)
784
+ if isinstance(value, CaseInsensitiveDict):
785
+ kwargs[container_name] = dict(value)
786
+ elif value and value is not NOT_SET:
787
+ kwargs[container_name] = value
788
+ return kwargs
789
+
790
+
741
791
  def find_invalid_headers(headers: Mapping) -> Generator[tuple[str, str], None, None]:
742
792
  for name, value in headers.items():
743
793
  if not is_latin_1_encodable(value) or has_invalid_characters(name, value):
@@ -9,9 +9,9 @@ from schemathesis.generation import GenerationMode
9
9
  class TestPhase(str, Enum):
10
10
  __test__ = False
11
11
 
12
- EXPLICIT = "explicit"
12
+ EXAMPLES = "examples"
13
13
  COVERAGE = "coverage"
14
- GENERATE = "generate"
14
+ FUZZING = "fuzzing"
15
15
 
16
16
 
17
17
  class ComponentKind(str, Enum):
@@ -81,7 +81,7 @@ class PhaseInfo:
81
81
 
82
82
  @classmethod
83
83
  def generate(cls) -> PhaseInfo:
84
- return cls(name=TestPhase.GENERATE, data=GeneratePhaseData())
84
+ return cls(name=TestPhase.FUZZING, data=GeneratePhaseData())
85
85
 
86
86
 
87
87
  @dataclass
@@ -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
@@ -4,6 +4,7 @@ from collections.abc import Mapping
4
4
  from dataclasses import dataclass
5
5
  from typing import TYPE_CHECKING, Any, Callable
6
6
 
7
+ from schemathesis.config import ProjectConfig
7
8
  from schemathesis.core.errors import IncorrectUsage
8
9
  from schemathesis.core.marks import Mark
9
10
  from schemathesis.core.transforms import diff
@@ -11,7 +12,7 @@ from schemathesis.generation.meta import ComponentKind
11
12
 
12
13
  if TYPE_CHECKING:
13
14
  from schemathesis.generation.case import Case
14
- from schemathesis.schemas import APIOperation, ParameterSet
15
+ from schemathesis.schemas import APIOperation, Parameter, ParameterSet
15
16
 
16
17
 
17
18
  @dataclass
@@ -41,6 +42,41 @@ class Override:
41
42
  )
42
43
 
43
44
 
45
+ def for_operation(config: ProjectConfig, *, operation: APIOperation) -> Override:
46
+ operation_config = config.operations.get_for_operation(operation)
47
+
48
+ output = Override(query={}, headers={}, cookies={}, path_parameters={})
49
+ groups = [
50
+ (output.query, operation.query),
51
+ (output.headers, operation.headers),
52
+ (output.cookies, operation.cookies),
53
+ (output.path_parameters, operation.path_parameters),
54
+ ]
55
+ for container, params in groups:
56
+ for param in params:
57
+ # Attempt to get the override from the operation-specific configuration.
58
+ value = None
59
+ if operation_config:
60
+ value = _get_override_value(param, operation_config.parameters)
61
+ # Fallback to the global project configuration.
62
+ if value is None:
63
+ value = _get_override_value(param, config.parameters)
64
+ if value is not None:
65
+ container[param.name] = value
66
+
67
+ return output
68
+
69
+
70
+ def _get_override_value(param: Parameter, parameters: dict[str, Any]) -> Any:
71
+ key = param.name
72
+ full_key = f"{param.location}.{param.name}"
73
+ if key in parameters:
74
+ return parameters[key]
75
+ elif full_key in parameters:
76
+ return parameters[full_key]
77
+ return None
78
+
79
+
44
80
  def _for_parameters(overridden: dict[str, str], defined: ParameterSet) -> dict[str, str]:
45
81
  output = {}
46
82
  for param in defined:
@@ -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)
@@ -10,6 +10,7 @@ from hypothesis.errors import InvalidDefinition
10
10
  from hypothesis.stateful import RuleBasedStateMachine
11
11
 
12
12
  from schemathesis.checks import CheckFunction
13
+ from schemathesis.core import DEFAULT_STATEFUL_STEP_COUNT
13
14
  from schemathesis.core.errors import NoLinksFound
14
15
  from schemathesis.core.result import Result
15
16
  from schemathesis.core.transport import Response
@@ -22,7 +23,6 @@ if TYPE_CHECKING:
22
23
  from schemathesis.schemas import BaseSchema
23
24
 
24
25
 
25
- DEFAULT_STATEFUL_STEP_COUNT = 6
26
26
  DEFAULT_STATE_MACHINE_SETTINGS = hypothesis.settings(
27
27
  phases=[hypothesis.Phase.generate],
28
28
  deadline=None,
@@ -184,11 +184,19 @@ class APIStateMachine(RuleBasedStateMachine):
184
184
  if target is not None:
185
185
  super()._add_result_to_targets((target,), result)
186
186
 
187
+ def _add_results_to_targets(self, targets: tuple[str, ...], results: list[StepOutput]) -> None:
188
+ # Hypothesis >6.131.15
189
+ for result in results:
190
+ target = self._get_target_for_result(result)
191
+ if target is not None:
192
+ super()._add_results_to_targets((target,), [result])
193
+
187
194
  @classmethod
188
195
  def run(cls, *, settings: hypothesis.settings | None = None) -> None:
189
196
  """Run state machine as a test."""
190
197
  from . import run_state_machine_as_test
191
198
 
199
+ __tracebackhide__ = True
192
200
  return run_state_machine_as_test(cls, settings=settings)
193
201
 
194
202
  def setup(self) -> None: