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.
- schemathesis/__init__.py +29 -30
- schemathesis/auths.py +65 -24
- schemathesis/checks.py +73 -39
- schemathesis/cli/commands/__init__.py +51 -3
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +163 -274
- schemathesis/cli/commands/run/context.py +8 -4
- schemathesis/cli/commands/run/events.py +11 -1
- schemathesis/cli/commands/run/executor.py +70 -78
- schemathesis/cli/commands/run/filters.py +15 -165
- schemathesis/cli/commands/run/handlers/cassettes.py +105 -104
- schemathesis/cli/commands/run/handlers/junitxml.py +5 -4
- schemathesis/cli/commands/run/handlers/output.py +195 -121
- schemathesis/cli/commands/run/loaders.py +35 -50
- schemathesis/cli/commands/run/validation.py +52 -162
- schemathesis/cli/core.py +5 -3
- schemathesis/cli/ext/fs.py +7 -5
- schemathesis/cli/ext/options.py +0 -21
- schemathesis/config/__init__.py +189 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +99 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +156 -0
- schemathesis/config/_generation.py +149 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +327 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +187 -0
- schemathesis/config/_projects.py +523 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +885 -0
- schemathesis/core/__init__.py +2 -0
- schemathesis/core/compat.py +16 -9
- schemathesis/core/errors.py +24 -4
- schemathesis/core/failures.py +6 -7
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/output/__init__.py +14 -37
- schemathesis/core/output/sanitization.py +3 -146
- schemathesis/core/transport.py +36 -1
- schemathesis/core/validation.py +16 -0
- schemathesis/engine/__init__.py +2 -4
- schemathesis/engine/context.py +42 -43
- schemathesis/engine/core.py +7 -5
- schemathesis/engine/errors.py +60 -1
- schemathesis/engine/events.py +10 -2
- schemathesis/engine/phases/__init__.py +10 -0
- schemathesis/engine/phases/probes.py +11 -8
- schemathesis/engine/phases/stateful/__init__.py +2 -1
- schemathesis/engine/phases/stateful/_executor.py +104 -46
- schemathesis/engine/phases/stateful/context.py +2 -2
- schemathesis/engine/phases/unit/__init__.py +23 -15
- schemathesis/engine/phases/unit/_executor.py +110 -21
- schemathesis/engine/phases/unit/_pool.py +1 -1
- schemathesis/errors.py +2 -0
- schemathesis/filters.py +2 -3
- schemathesis/generation/__init__.py +5 -33
- schemathesis/generation/case.py +6 -3
- schemathesis/generation/coverage.py +154 -124
- schemathesis/generation/hypothesis/builder.py +70 -20
- schemathesis/generation/meta.py +3 -3
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +0 -8
- schemathesis/generation/overrides.py +37 -1
- schemathesis/generation/stateful/__init__.py +4 -0
- schemathesis/generation/stateful/state_machine.py +9 -1
- schemathesis/graphql/loaders.py +159 -16
- schemathesis/hooks.py +62 -35
- schemathesis/openapi/checks.py +12 -8
- schemathesis/openapi/generation/filters.py +10 -8
- schemathesis/openapi/loaders.py +142 -17
- schemathesis/pytest/lazy.py +2 -5
- schemathesis/pytest/loaders.py +24 -0
- schemathesis/pytest/plugin.py +33 -2
- schemathesis/schemas.py +21 -66
- schemathesis/specs/graphql/scalars.py +37 -3
- schemathesis/specs/graphql/schemas.py +23 -18
- schemathesis/specs/openapi/_hypothesis.py +26 -28
- schemathesis/specs/openapi/checks.py +37 -36
- schemathesis/specs/openapi/examples.py +4 -3
- schemathesis/specs/openapi/formats.py +32 -5
- schemathesis/specs/openapi/media_types.py +44 -1
- schemathesis/specs/openapi/negative/__init__.py +2 -2
- schemathesis/specs/openapi/patterns.py +46 -16
- schemathesis/specs/openapi/references.py +2 -3
- schemathesis/specs/openapi/schemas.py +19 -22
- schemathesis/specs/openapi/stateful/__init__.py +12 -6
- schemathesis/transport/__init__.py +54 -16
- schemathesis/transport/prepare.py +38 -13
- schemathesis/transport/requests.py +12 -9
- schemathesis/transport/wsgi.py +11 -12
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/METADATA +50 -97
- schemathesis-4.0.0a12.dist-info/RECORD +164 -0
- schemathesis/cli/commands/run/checks.py +0 -79
- schemathesis/cli/commands/run/hypothesis.py +0 -78
- schemathesis/cli/commands/run/reports.py +0 -72
- schemathesis/cli/hooks.py +0 -36
- schemathesis/contrib/__init__.py +0 -9
- schemathesis/contrib/openapi/__init__.py +0 -9
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -20
- schemathesis/engine/config.py +0 -59
- schemathesis/experimental/__init__.py +0 -72
- schemathesis/generation/targets.py +0 -69
- schemathesis-4.0.0a10.dist-info/RECORD +0 -153
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/entry_points.txt +0 -0
- {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.
|
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
|
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
|
-
|
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
|
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
|
-
|
129
|
-
|
130
|
-
|
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
|
-
|
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
|
-
|
153
|
+
generation.modes,
|
144
154
|
auth_storage,
|
145
155
|
config.as_strategy_kwargs,
|
146
|
-
|
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,
|
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
|
-
|
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(
|
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=
|
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):
|
schemathesis/generation/meta.py
CHANGED
@@ -9,9 +9,9 @@ from schemathesis.generation import GenerationMode
|
|
9
9
|
class TestPhase(str, Enum):
|
10
10
|
__test__ = False
|
11
11
|
|
12
|
-
|
12
|
+
EXAMPLES = "examples"
|
13
13
|
COVERAGE = "coverage"
|
14
|
-
|
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.
|
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__)
|
schemathesis/generation/modes.py
CHANGED
@@ -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:
|