schemathesis 3.35.5__py3-none-any.whl → 3.36.1__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/_hypothesis.py +5 -0
- schemathesis/auths.py +1 -0
- schemathesis/checks.py +8 -5
- schemathesis/cli/__init__.py +4 -13
- schemathesis/contrib/unique_data.py +1 -2
- schemathesis/generation/coverage.py +81 -8
- schemathesis/internal/checks.py +55 -0
- schemathesis/models.py +24 -10
- schemathesis/runner/__init__.py +7 -1
- schemathesis/runner/impl/context.py +22 -4
- schemathesis/runner/impl/core.py +63 -12
- schemathesis/runner/impl/solo.py +1 -1
- schemathesis/runner/impl/threadpool.py +3 -4
- schemathesis/schemas.py +2 -2
- schemathesis/specs/graphql/loaders.py +2 -2
- schemathesis/specs/graphql/schemas.py +9 -5
- schemathesis/specs/openapi/checks.py +79 -27
- schemathesis/specs/openapi/loaders.py +2 -2
- schemathesis/specs/openapi/schemas.py +19 -3
- schemathesis/stateful/config.py +1 -0
- schemathesis/stateful/context.py +10 -0
- schemathesis/stateful/runner.py +19 -2
- schemathesis/stateful/state_machine.py +2 -1
- schemathesis/stateful/validation.py +9 -4
- {schemathesis-3.35.5.dist-info → schemathesis-3.36.1.dist-info}/METADATA +2 -1
- {schemathesis-3.35.5.dist-info → schemathesis-3.36.1.dist-info}/RECORD +29 -28
- {schemathesis-3.35.5.dist-info → schemathesis-3.36.1.dist-info}/WHEEL +0 -0
- {schemathesis-3.35.5.dist-info → schemathesis-3.36.1.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.35.5.dist-info → schemathesis-3.36.1.dist-info}/licenses/LICENSE +0 -0
schemathesis/_hypothesis.py
CHANGED
|
@@ -245,6 +245,11 @@ def _iter_coverage_cases(
|
|
|
245
245
|
if operation.body:
|
|
246
246
|
for body in operation.body:
|
|
247
247
|
schema = body.as_json_schema(operation)
|
|
248
|
+
# Definition could be a list for Open API 2.0
|
|
249
|
+
definition = body.definition if isinstance(body.definition, dict) else {}
|
|
250
|
+
examples = [example["value"] for example in definition.get("examples", {}).values() if "value" in example]
|
|
251
|
+
if examples:
|
|
252
|
+
schema.setdefault("examples", []).extend(examples)
|
|
248
253
|
gen = coverage.cover_schema_iter(ctx, schema)
|
|
249
254
|
value = next(gen, NOT_SET)
|
|
250
255
|
if isinstance(value, NotSet):
|
schemathesis/auths.py
CHANGED
schemathesis/checks.py
CHANGED
|
@@ -15,11 +15,12 @@ from .specs.openapi.checks import (
|
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
if TYPE_CHECKING:
|
|
18
|
-
from .
|
|
18
|
+
from .internal.checks import CheckContext, CheckFunction
|
|
19
|
+
from .models import Case
|
|
19
20
|
from .transports.responses import GenericResponse
|
|
20
21
|
|
|
21
22
|
|
|
22
|
-
def not_a_server_error(response: GenericResponse, case: Case) -> bool | None:
|
|
23
|
+
def not_a_server_error(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
|
|
23
24
|
"""A check to verify that the response is not a server-side error."""
|
|
24
25
|
from .specs.graphql.schemas import GraphQLCase
|
|
25
26
|
from .specs.graphql.validation import validate_graphql_response
|
|
@@ -64,14 +65,16 @@ def register(check: CheckFunction) -> CheckFunction:
|
|
|
64
65
|
.. code-block:: python
|
|
65
66
|
|
|
66
67
|
@schemathesis.check
|
|
67
|
-
def new_check(response, case):
|
|
68
|
+
def new_check(ctx, response, case):
|
|
68
69
|
# some awesome assertions!
|
|
69
70
|
...
|
|
70
71
|
"""
|
|
71
72
|
from . import cli
|
|
73
|
+
from .internal.checks import wrap_check
|
|
72
74
|
|
|
75
|
+
_check = wrap_check(check)
|
|
73
76
|
global ALL_CHECKS
|
|
74
77
|
|
|
75
|
-
ALL_CHECKS += (
|
|
76
|
-
cli.CHECKS_TYPE.choices += (
|
|
78
|
+
ALL_CHECKS += (_check,)
|
|
79
|
+
cli.CHECKS_TYPE.choices += (_check.__name__,) # type: ignore
|
|
77
80
|
return check
|
schemathesis/cli/__init__.py
CHANGED
|
@@ -104,13 +104,6 @@ DEPRECATED_SHOW_ERROR_TRACEBACKS_OPTION_WARNING = (
|
|
|
104
104
|
"Warning: Option `--show-errors-tracebacks` is deprecated and will be removed in Schemathesis 4.0. "
|
|
105
105
|
"Use `--show-trace` instead"
|
|
106
106
|
)
|
|
107
|
-
DEPRECATED_CONTRIB_UNIQUE_DATA_OPTION_WARNING = (
|
|
108
|
-
"The `--contrib-unique-data` CLI option and the corresponding `schemathesis.contrib.unique_data` hook "
|
|
109
|
-
"are **DEPRECATED**. The concept of this feature does not fit the core principles of Hypothesis where "
|
|
110
|
-
"strategies are configurable on a per-example basis but this feature implies uniqueness across examples. "
|
|
111
|
-
"This leads to cryptic error messages about external state and flaky test runs, "
|
|
112
|
-
"therefore it will be removed in Schemathesis 4.0"
|
|
113
|
-
)
|
|
114
107
|
CASSETTES_PATH_INVALID_USAGE_MESSAGE = "Can't use `--store-network-log` and `--cassette-path` simultaneously"
|
|
115
108
|
COLOR_OPTIONS_INVALID_USAGE_MESSAGE = "Can't use `--no-color` and `--force-color` simultaneously"
|
|
116
109
|
PHASES_INVALID_USAGE_MESSAGE = "Can't use `--hypothesis-phases` and `--hypothesis-no-phases` simultaneously"
|
|
@@ -439,7 +432,7 @@ REPORT_TO_SERVICE = ReportToService()
|
|
|
439
432
|
"-A",
|
|
440
433
|
type=click.Choice(["basic", "digest"], case_sensitive=False),
|
|
441
434
|
default="basic",
|
|
442
|
-
help="Specify the authentication method",
|
|
435
|
+
help="Specify the authentication method. For custom authentication methods, see our Authentication documentation: https://schemathesis.readthedocs.io/en/stable/auth.html#custom-auth",
|
|
443
436
|
show_default=True,
|
|
444
437
|
metavar="",
|
|
445
438
|
)
|
|
@@ -949,9 +942,6 @@ def run(
|
|
|
949
942
|
entry for health_check in hypothesis_suppress_health_check for entry in health_check.as_hypothesis()
|
|
950
943
|
]
|
|
951
944
|
|
|
952
|
-
if contrib_unique_data:
|
|
953
|
-
click.secho(DEPRECATED_CONTRIB_UNIQUE_DATA_OPTION_WARNING, fg="yellow")
|
|
954
|
-
|
|
955
945
|
if show_errors_tracebacks:
|
|
956
946
|
click.secho(DEPRECATED_SHOW_ERROR_TRACEBACKS_OPTION_WARNING, fg="yellow")
|
|
957
947
|
show_trace = show_errors_tracebacks
|
|
@@ -1160,8 +1150,6 @@ def run(
|
|
|
1160
1150
|
else:
|
|
1161
1151
|
_fixups.install(fixups)
|
|
1162
1152
|
|
|
1163
|
-
if contrib_unique_data:
|
|
1164
|
-
contrib.unique_data.install()
|
|
1165
1153
|
if contrib_openapi_formats_uuid:
|
|
1166
1154
|
contrib.openapi.formats.uuid.install()
|
|
1167
1155
|
if contrib_openapi_fill_missing_examples:
|
|
@@ -1197,6 +1185,7 @@ def run(
|
|
|
1197
1185
|
seed=hypothesis_seed,
|
|
1198
1186
|
exit_first=exit_first,
|
|
1199
1187
|
max_failures=max_failures,
|
|
1188
|
+
unique_data=contrib_unique_data,
|
|
1200
1189
|
dry_run=dry_run,
|
|
1201
1190
|
store_interactions=cassette_path is not None,
|
|
1202
1191
|
checks=selected_checks,
|
|
@@ -1321,6 +1310,7 @@ def into_event_stream(
|
|
|
1321
1310
|
exit_first: bool,
|
|
1322
1311
|
max_failures: int | None,
|
|
1323
1312
|
rate_limit: str | None,
|
|
1313
|
+
unique_data: bool,
|
|
1324
1314
|
dry_run: bool,
|
|
1325
1315
|
store_interactions: bool,
|
|
1326
1316
|
stateful: Stateful | None,
|
|
@@ -1364,6 +1354,7 @@ def into_event_stream(
|
|
|
1364
1354
|
exit_first=exit_first,
|
|
1365
1355
|
max_failures=max_failures,
|
|
1366
1356
|
started_at=started_at,
|
|
1357
|
+
unique_data=unique_data,
|
|
1367
1358
|
dry_run=dry_run,
|
|
1368
1359
|
store_interactions=store_interactions,
|
|
1369
1360
|
checks=checks,
|
|
@@ -13,8 +13,7 @@ if TYPE_CHECKING:
|
|
|
13
13
|
|
|
14
14
|
def install() -> None:
|
|
15
15
|
warnings.warn(
|
|
16
|
-
"The
|
|
17
|
-
"are **DEPRECATED**. The concept of this feature does not fit the core principles of Hypothesis where "
|
|
16
|
+
"The `schemathesis.contrib.unique_data` hook is **DEPRECATED**. The concept of this feature does not fit the core principles of Hypothesis where "
|
|
18
17
|
"strategies are configurable on a per-example basis but this feature implies uniqueness across examples. "
|
|
19
18
|
"This leads to cryptic error messages about external state and flaky test runs, "
|
|
20
19
|
"therefore it will be removed in Schemathesis 4.0",
|
|
@@ -4,7 +4,8 @@ import json
|
|
|
4
4
|
from contextlib import contextmanager, suppress
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
from functools import lru_cache
|
|
7
|
-
from
|
|
7
|
+
from itertools import combinations
|
|
8
|
+
from typing import Any, Generator, Iterator, TypeVar, cast
|
|
8
9
|
|
|
9
10
|
import jsonschema
|
|
10
11
|
from hypothesis import strategies as st
|
|
@@ -162,6 +163,7 @@ def cover_schema_iter(ctx: CoverageContext, schema: dict | bool) -> Generator[Ge
|
|
|
162
163
|
schema = {}
|
|
163
164
|
else:
|
|
164
165
|
types = schema.get("type", [])
|
|
166
|
+
push_examples_to_properties(schema)
|
|
165
167
|
if not isinstance(types, list):
|
|
166
168
|
types = [types] # type: ignore[unreachable]
|
|
167
169
|
if not types:
|
|
@@ -238,6 +240,8 @@ def _get_properties(schema: dict | bool) -> dict | bool:
|
|
|
238
240
|
if isinstance(schema, dict):
|
|
239
241
|
if "example" in schema:
|
|
240
242
|
return {"const": schema["example"]}
|
|
243
|
+
if "default" in schema:
|
|
244
|
+
return {"const": schema["default"]}
|
|
241
245
|
if schema.get("examples"):
|
|
242
246
|
return {"enum": schema["examples"]}
|
|
243
247
|
if schema.get("type") == "object":
|
|
@@ -262,18 +266,33 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
|
262
266
|
"""Generate positive string values."""
|
|
263
267
|
# Boundary and near boundary values
|
|
264
268
|
min_length = schema.get("minLength")
|
|
269
|
+
if min_length == 0:
|
|
270
|
+
min_length = None
|
|
265
271
|
max_length = schema.get("maxLength")
|
|
266
272
|
example = schema.get("example")
|
|
267
273
|
examples = schema.get("examples")
|
|
268
|
-
|
|
274
|
+
default = schema.get("default")
|
|
275
|
+
if example or examples or default:
|
|
269
276
|
if example:
|
|
270
277
|
yield PositiveValue(example)
|
|
271
278
|
if examples:
|
|
272
279
|
for example in examples:
|
|
273
280
|
yield PositiveValue(example)
|
|
281
|
+
if (
|
|
282
|
+
default
|
|
283
|
+
and not (example is not None and default == example)
|
|
284
|
+
and not (examples is not None and any(default == ex for ex in examples))
|
|
285
|
+
):
|
|
286
|
+
yield PositiveValue(default)
|
|
274
287
|
elif not min_length and not max_length:
|
|
275
288
|
# Default positive value
|
|
276
289
|
yield PositiveValue(ctx.generate_from_schema(schema))
|
|
290
|
+
elif "pattern" in schema:
|
|
291
|
+
# Without merging `maxLength` & `minLength` into a regex it is problematic
|
|
292
|
+
# to generate a valid value as the unredlying machinery will resort to filtering
|
|
293
|
+
# and it is unlikely that it will generate a string of that length
|
|
294
|
+
yield PositiveValue(ctx.generate_from_schema(schema))
|
|
295
|
+
return
|
|
277
296
|
|
|
278
297
|
seen = set()
|
|
279
298
|
|
|
@@ -327,13 +346,20 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
|
327
346
|
multiple_of = schema.get("multipleOf")
|
|
328
347
|
example = schema.get("example")
|
|
329
348
|
examples = schema.get("examples")
|
|
349
|
+
default = schema.get("default")
|
|
330
350
|
|
|
331
|
-
if example or examples:
|
|
351
|
+
if example or examples or default:
|
|
332
352
|
if example:
|
|
333
353
|
yield PositiveValue(example)
|
|
334
354
|
if examples:
|
|
335
355
|
for example in examples:
|
|
336
356
|
yield PositiveValue(example)
|
|
357
|
+
if (
|
|
358
|
+
default
|
|
359
|
+
and not (example is not None and default == example)
|
|
360
|
+
and not (examples is not None and any(default == ex for ex in examples))
|
|
361
|
+
):
|
|
362
|
+
yield PositiveValue(default)
|
|
337
363
|
elif not minimum and not maximum:
|
|
338
364
|
# Default positive value
|
|
339
365
|
yield PositiveValue(ctx.generate_from_schema(schema))
|
|
@@ -382,13 +408,20 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
|
|
|
382
408
|
seen = set()
|
|
383
409
|
example = schema.get("example")
|
|
384
410
|
examples = schema.get("examples")
|
|
411
|
+
default = schema.get("default")
|
|
385
412
|
|
|
386
|
-
if example or examples:
|
|
413
|
+
if example or examples or default:
|
|
387
414
|
if example:
|
|
388
415
|
yield PositiveValue(example)
|
|
389
416
|
if examples:
|
|
390
417
|
for example in examples:
|
|
391
418
|
yield PositiveValue(example)
|
|
419
|
+
if (
|
|
420
|
+
default
|
|
421
|
+
and not (example is not None and default == example)
|
|
422
|
+
and not (examples is not None and any(default == ex for ex in examples))
|
|
423
|
+
):
|
|
424
|
+
yield PositiveValue(default)
|
|
392
425
|
else:
|
|
393
426
|
yield PositiveValue(template)
|
|
394
427
|
seen.add(len(template))
|
|
@@ -425,19 +458,41 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
|
|
|
425
458
|
def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Generator[GeneratedValue, None, None]:
|
|
426
459
|
example = schema.get("example")
|
|
427
460
|
examples = schema.get("examples")
|
|
461
|
+
default = schema.get("default")
|
|
428
462
|
|
|
429
|
-
if example or examples:
|
|
463
|
+
if example or examples or default:
|
|
430
464
|
if example:
|
|
431
465
|
yield PositiveValue(example)
|
|
432
466
|
if examples:
|
|
433
467
|
for example in examples:
|
|
434
468
|
yield PositiveValue(example)
|
|
469
|
+
if (
|
|
470
|
+
default
|
|
471
|
+
and not (example is not None and default == example)
|
|
472
|
+
and not (examples is not None and any(default == ex for ex in examples))
|
|
473
|
+
):
|
|
474
|
+
yield PositiveValue(default)
|
|
475
|
+
|
|
435
476
|
else:
|
|
436
477
|
yield PositiveValue(template)
|
|
437
|
-
|
|
478
|
+
|
|
438
479
|
properties = schema.get("properties", {})
|
|
439
|
-
|
|
440
|
-
|
|
480
|
+
required = set(schema.get("required", []))
|
|
481
|
+
optional = list(set(properties) - required)
|
|
482
|
+
optional.sort()
|
|
483
|
+
|
|
484
|
+
# Generate combinations with required properties and one optional property
|
|
485
|
+
for name in optional:
|
|
486
|
+
combo = {k: v for k, v in template.items() if k in required or k == name}
|
|
487
|
+
if combo != template:
|
|
488
|
+
yield PositiveValue(combo)
|
|
489
|
+
# Generate one combination for each size from 2 to N-1
|
|
490
|
+
for selection in select_combinations(optional):
|
|
491
|
+
combo = {k: v for k, v in template.items() if k in required or k in selection}
|
|
492
|
+
yield PositiveValue(combo)
|
|
493
|
+
# Generate only required properties
|
|
494
|
+
if set(properties) != required:
|
|
495
|
+
only_required = {k: v for k, v in template.items() if k in required}
|
|
441
496
|
yield PositiveValue(only_required)
|
|
442
497
|
seen = set()
|
|
443
498
|
for name, sub_schema in properties.items():
|
|
@@ -450,6 +505,11 @@ def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Gene
|
|
|
450
505
|
seen.clear()
|
|
451
506
|
|
|
452
507
|
|
|
508
|
+
def select_combinations(optional: list[str]) -> Iterator[tuple[str, ...]]:
|
|
509
|
+
for size in range(2, len(optional)):
|
|
510
|
+
yield next(combinations(optional, size))
|
|
511
|
+
|
|
512
|
+
|
|
453
513
|
def _negative_enum(ctx: CoverageContext, value: list) -> Generator[GeneratedValue, None, None]:
|
|
454
514
|
strategy = JSON_STRATEGY.filter(lambda x: x not in value)
|
|
455
515
|
# The exact negative value is not important here
|
|
@@ -528,3 +588,16 @@ def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Gene
|
|
|
528
588
|
value = ctx.generate_from(negative_strategy, cached=True)
|
|
529
589
|
yield NegativeValue(value)
|
|
530
590
|
seen.add(_to_hashable_key(value))
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def push_examples_to_properties(schema: dict[str, Any]) -> None:
|
|
594
|
+
"""Push examples from the top-level 'examples' field to the corresponding properties."""
|
|
595
|
+
if "examples" in schema and "properties" in schema:
|
|
596
|
+
properties = schema["properties"]
|
|
597
|
+
for example in schema["examples"]:
|
|
598
|
+
for prop, value in example.items():
|
|
599
|
+
if prop in properties:
|
|
600
|
+
if "examples" not in properties[prop]:
|
|
601
|
+
properties[prop]["examples"] = []
|
|
602
|
+
if value not in schema["properties"][prop]["examples"]:
|
|
603
|
+
properties[prop]["examples"].append(value)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import warnings
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING, Callable, Optional
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..types import RawAuth
|
|
10
|
+
from ..models import Case
|
|
11
|
+
from ..transports.responses import GenericResponse
|
|
12
|
+
from requests.structures import CaseInsensitiveDict
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
CheckFunction = Callable[["CheckContext", "GenericResponse", "Case"], Optional[bool]]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class CheckContext:
|
|
20
|
+
"""Context for Schemathesis checks.
|
|
21
|
+
|
|
22
|
+
Provides access to broader test execution data beyond individual test cases.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
auth: RawAuth | None = None
|
|
26
|
+
headers: CaseInsensitiveDict | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def wrap_check(check: Callable) -> CheckFunction:
|
|
30
|
+
"""Make older checks compatible with the new signature."""
|
|
31
|
+
signature = inspect.signature(check)
|
|
32
|
+
parameters = len(signature.parameters)
|
|
33
|
+
|
|
34
|
+
if parameters == 3:
|
|
35
|
+
# New style check, return as is
|
|
36
|
+
return check
|
|
37
|
+
|
|
38
|
+
if parameters == 2:
|
|
39
|
+
# Old style check, wrap it
|
|
40
|
+
warnings.warn(
|
|
41
|
+
f"The check function '{check.__name__}' uses an outdated signature. "
|
|
42
|
+
"Please update it to accept 'ctx' as the first argument: "
|
|
43
|
+
"(ctx: CheckContext, response: GenericResponse, case: Case) -> Optional[bool]",
|
|
44
|
+
DeprecationWarning,
|
|
45
|
+
stacklevel=2,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def wrapper(_: CheckContext, response: GenericResponse, case: Case) -> Optional[bool]:
|
|
49
|
+
return check(response, case)
|
|
50
|
+
|
|
51
|
+
wrapper.__name__ = check.__name__
|
|
52
|
+
|
|
53
|
+
return wrapper
|
|
54
|
+
|
|
55
|
+
raise ValueError(f"Invalid check function signature. Expected 2 or 3 parameters, got {parameters}")
|
schemathesis/models.py
CHANGED
|
@@ -17,7 +17,6 @@ from typing import (
|
|
|
17
17
|
Generic,
|
|
18
18
|
Iterator,
|
|
19
19
|
NoReturn,
|
|
20
|
-
Optional,
|
|
21
20
|
Sequence,
|
|
22
21
|
Type,
|
|
23
22
|
TypeVar,
|
|
@@ -46,6 +45,7 @@ from .exceptions import (
|
|
|
46
45
|
)
|
|
47
46
|
from .generation import DataGenerationMethod, GenerationConfig, generate_random_case_id
|
|
48
47
|
from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, dispatch
|
|
48
|
+
from .internal.checks import CheckContext
|
|
49
49
|
from .internal.copy import fast_deepcopy
|
|
50
50
|
from .internal.deprecation import deprecated_function, deprecated_property
|
|
51
51
|
from .internal.output import prepare_response_payload
|
|
@@ -65,6 +65,7 @@ if TYPE_CHECKING:
|
|
|
65
65
|
|
|
66
66
|
from .auths import AuthStorage
|
|
67
67
|
from .failures import FailureContext
|
|
68
|
+
from .internal.checks import CheckFunction
|
|
68
69
|
from .schemas import BaseSchema
|
|
69
70
|
from .serializers import Serializer
|
|
70
71
|
from .stateful import Stateful, StatefulTest
|
|
@@ -135,8 +136,7 @@ def prepare_request_data(kwargs: dict[str, Any]) -> PreparedRequestData:
|
|
|
135
136
|
)
|
|
136
137
|
|
|
137
138
|
|
|
138
|
-
|
|
139
|
-
class TestPhase(Enum):
|
|
139
|
+
class TestPhase(str, Enum):
|
|
140
140
|
__test__ = False
|
|
141
141
|
|
|
142
142
|
EXPLICIT = "explicit"
|
|
@@ -183,6 +183,7 @@ class Case:
|
|
|
183
183
|
# The way the case was generated (None for manually crafted ones)
|
|
184
184
|
data_generation_method: DataGenerationMethod | None = None
|
|
185
185
|
_auth: requests.auth.AuthBase | None = None
|
|
186
|
+
_has_explicit_auth: bool = False
|
|
186
187
|
|
|
187
188
|
def __repr__(self) -> str:
|
|
188
189
|
parts = [f"{self.__class__.__name__}("]
|
|
@@ -421,6 +422,7 @@ class Case:
|
|
|
421
422
|
additional_checks: tuple[CheckFunction, ...] = (),
|
|
422
423
|
excluded_checks: tuple[CheckFunction, ...] = (),
|
|
423
424
|
code_sample_style: str | None = None,
|
|
425
|
+
headers: dict[str, Any] | None = None,
|
|
424
426
|
) -> None:
|
|
425
427
|
"""Validate application response.
|
|
426
428
|
|
|
@@ -434,17 +436,30 @@ class Case:
|
|
|
434
436
|
:param code_sample_style: Controls the style of code samples for failure reproduction.
|
|
435
437
|
"""
|
|
436
438
|
__tracebackhide__ = True
|
|
439
|
+
from requests.structures import CaseInsensitiveDict
|
|
440
|
+
|
|
437
441
|
from .checks import ALL_CHECKS
|
|
442
|
+
from .internal.checks import wrap_check
|
|
438
443
|
from .transports.responses import get_payload, get_reason
|
|
439
444
|
|
|
440
|
-
|
|
445
|
+
if checks:
|
|
446
|
+
_checks = tuple(wrap_check(check) for check in checks)
|
|
447
|
+
else:
|
|
448
|
+
_checks = checks
|
|
449
|
+
if additional_checks:
|
|
450
|
+
_additional_checks = tuple(wrap_check(check) for check in additional_checks)
|
|
451
|
+
else:
|
|
452
|
+
_additional_checks = additional_checks
|
|
453
|
+
|
|
454
|
+
checks = _checks or ALL_CHECKS
|
|
441
455
|
checks = tuple(check for check in checks if check not in excluded_checks)
|
|
442
|
-
additional_checks = tuple(check for check in
|
|
456
|
+
additional_checks = tuple(check for check in _additional_checks if check not in excluded_checks)
|
|
443
457
|
failed_checks = []
|
|
458
|
+
ctx = CheckContext(headers=CaseInsensitiveDict(headers) if headers else None)
|
|
444
459
|
for check in chain(checks, additional_checks):
|
|
445
460
|
copied_case = self.partial_deepcopy()
|
|
446
461
|
try:
|
|
447
|
-
check(response, copied_case)
|
|
462
|
+
check(ctx, response, copied_case)
|
|
448
463
|
except AssertionError as exc:
|
|
449
464
|
maybe_set_assertion_message(exc, check.__name__)
|
|
450
465
|
failed_checks.append(exc)
|
|
@@ -514,7 +529,7 @@ class Case:
|
|
|
514
529
|
) -> requests.Response:
|
|
515
530
|
__tracebackhide__ = True
|
|
516
531
|
response = self.call(base_url, session, headers, **kwargs)
|
|
517
|
-
self.validate_response(response, checks, code_sample_style=code_sample_style)
|
|
532
|
+
self.validate_response(response, checks, code_sample_style=code_sample_style, headers=headers)
|
|
518
533
|
return response
|
|
519
534
|
|
|
520
535
|
def _get_url(self, base_url: str | None) -> str:
|
|
@@ -547,6 +562,8 @@ class Case:
|
|
|
547
562
|
body=fast_deepcopy(self.body),
|
|
548
563
|
generation_time=self.generation_time,
|
|
549
564
|
id=self.id,
|
|
565
|
+
_auth=self._auth,
|
|
566
|
+
_has_explicit_auth=self._has_explicit_auth,
|
|
550
567
|
)
|
|
551
568
|
|
|
552
569
|
|
|
@@ -1233,6 +1250,3 @@ class TestResultSet:
|
|
|
1233
1250
|
def add_warning(self, warning: str) -> None:
|
|
1234
1251
|
"""Add a new warning to the warnings list."""
|
|
1235
1252
|
self.warnings.append(warning)
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
CheckFunction = Callable[["GenericResponse", Case], Optional[bool]]
|
schemathesis/runner/__init__.py
CHANGED
|
@@ -347,6 +347,7 @@ def from_schema(
|
|
|
347
347
|
exit_first: bool = False,
|
|
348
348
|
max_failures: int | None = None,
|
|
349
349
|
started_at: str | None = None,
|
|
350
|
+
unique_data: bool = False,
|
|
350
351
|
dry_run: bool = False,
|
|
351
352
|
store_interactions: bool = False,
|
|
352
353
|
stateful: Stateful | None = None,
|
|
@@ -373,7 +374,6 @@ def from_schema(
|
|
|
373
374
|
probe_config = probe_config or ProbeConfig()
|
|
374
375
|
|
|
375
376
|
hypothesis_settings = hypothesis_settings or hypothesis.settings(deadline=DEFAULT_DEADLINE)
|
|
376
|
-
generation_config = generation_config or GenerationConfig()
|
|
377
377
|
request_config = RequestConfig(
|
|
378
378
|
timeout=request_timeout,
|
|
379
379
|
tls_verify=request_tls_verify,
|
|
@@ -405,6 +405,7 @@ def from_schema(
|
|
|
405
405
|
exit_first=exit_first,
|
|
406
406
|
max_failures=max_failures,
|
|
407
407
|
started_at=started_at,
|
|
408
|
+
unique_data=unique_data,
|
|
408
409
|
dry_run=dry_run,
|
|
409
410
|
store_interactions=store_interactions,
|
|
410
411
|
stateful=stateful,
|
|
@@ -430,6 +431,7 @@ def from_schema(
|
|
|
430
431
|
exit_first=exit_first,
|
|
431
432
|
max_failures=max_failures,
|
|
432
433
|
started_at=started_at,
|
|
434
|
+
unique_data=unique_data,
|
|
433
435
|
dry_run=dry_run,
|
|
434
436
|
store_interactions=store_interactions,
|
|
435
437
|
stateful=stateful,
|
|
@@ -455,6 +457,7 @@ def from_schema(
|
|
|
455
457
|
exit_first=exit_first,
|
|
456
458
|
max_failures=max_failures,
|
|
457
459
|
started_at=started_at,
|
|
460
|
+
unique_data=unique_data,
|
|
458
461
|
dry_run=dry_run,
|
|
459
462
|
store_interactions=store_interactions,
|
|
460
463
|
stateful=stateful,
|
|
@@ -481,6 +484,7 @@ def from_schema(
|
|
|
481
484
|
exit_first=exit_first,
|
|
482
485
|
max_failures=max_failures,
|
|
483
486
|
started_at=started_at,
|
|
487
|
+
unique_data=unique_data,
|
|
484
488
|
dry_run=dry_run,
|
|
485
489
|
store_interactions=store_interactions,
|
|
486
490
|
stateful=stateful,
|
|
@@ -506,6 +510,7 @@ def from_schema(
|
|
|
506
510
|
exit_first=exit_first,
|
|
507
511
|
max_failures=max_failures,
|
|
508
512
|
started_at=started_at,
|
|
513
|
+
unique_data=unique_data,
|
|
509
514
|
dry_run=dry_run,
|
|
510
515
|
store_interactions=store_interactions,
|
|
511
516
|
stateful=stateful,
|
|
@@ -530,6 +535,7 @@ def from_schema(
|
|
|
530
535
|
exit_first=exit_first,
|
|
531
536
|
max_failures=max_failures,
|
|
532
537
|
started_at=started_at,
|
|
538
|
+
unique_data=unique_data,
|
|
533
539
|
dry_run=dry_run,
|
|
534
540
|
store_interactions=store_interactions,
|
|
535
541
|
stateful=stateful,
|
|
@@ -1,29 +1,41 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
4
5
|
|
|
6
|
+
from ...constants import NOT_SET
|
|
5
7
|
from ...models import TestResult, TestResultSet
|
|
6
|
-
from typing import TYPE_CHECKING
|
|
7
8
|
|
|
8
9
|
if TYPE_CHECKING:
|
|
9
|
-
from ...exceptions import OperationSchemaError
|
|
10
10
|
import threading
|
|
11
11
|
|
|
12
|
+
from ...exceptions import OperationSchemaError
|
|
13
|
+
from ...models import Case
|
|
14
|
+
from ...types import NotSet, RawAuth
|
|
15
|
+
|
|
12
16
|
|
|
13
17
|
@dataclass
|
|
14
18
|
class RunnerContext:
|
|
15
19
|
"""Holds context shared for a test run."""
|
|
16
20
|
|
|
17
21
|
data: TestResultSet
|
|
22
|
+
auth: RawAuth | None
|
|
18
23
|
seed: int | None
|
|
19
24
|
stop_event: threading.Event
|
|
25
|
+
unique_data: bool
|
|
26
|
+
outcome_cache: dict[int, BaseException | None]
|
|
20
27
|
|
|
21
|
-
__slots__ = ("data", "seed", "stop_event")
|
|
28
|
+
__slots__ = ("data", "auth", "seed", "stop_event", "unique_data", "outcome_cache")
|
|
22
29
|
|
|
23
|
-
def __init__(
|
|
30
|
+
def __init__(
|
|
31
|
+
self, *, seed: int | None, auth: RawAuth | None, stop_event: threading.Event, unique_data: bool
|
|
32
|
+
) -> None:
|
|
24
33
|
self.data = TestResultSet(seed=seed)
|
|
34
|
+
self.auth = auth
|
|
25
35
|
self.seed = seed
|
|
26
36
|
self.stop_event = stop_event
|
|
37
|
+
self.outcome_cache = {}
|
|
38
|
+
self.unique_data = unique_data
|
|
27
39
|
|
|
28
40
|
@property
|
|
29
41
|
def is_stopped(self) -> bool:
|
|
@@ -54,5 +66,11 @@ class RunnerContext:
|
|
|
54
66
|
def add_warning(self, message: str) -> None:
|
|
55
67
|
self.data.add_warning(message)
|
|
56
68
|
|
|
69
|
+
def cache_outcome(self, case: Case, outcome: BaseException | None) -> None:
|
|
70
|
+
self.outcome_cache[hash(case)] = outcome
|
|
71
|
+
|
|
72
|
+
def get_cached_outcome(self, case: Case) -> BaseException | None | NotSet:
|
|
73
|
+
return self.outcome_cache.get(hash(case), NOT_SET)
|
|
74
|
+
|
|
57
75
|
|
|
58
76
|
ALL_NOT_FOUND_WARNING_MESSAGE = "All API responses have a 404 status code. Did you specify the proper API location?"
|