schemathesis 3.35.4__py3-none-any.whl → 3.36.0__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 +5 -5
- schemathesis/_hypothesis.py +12 -6
- schemathesis/_override.py +4 -4
- schemathesis/auths.py +1 -1
- schemathesis/checks.py +8 -5
- schemathesis/cli/__init__.py +23 -26
- schemathesis/cli/callbacks.py +6 -4
- schemathesis/cli/cassettes.py +67 -41
- schemathesis/cli/context.py +7 -6
- schemathesis/cli/junitxml.py +1 -1
- schemathesis/cli/options.py +7 -4
- schemathesis/cli/output/default.py +5 -5
- schemathesis/cli/reporting.py +4 -2
- schemathesis/code_samples.py +4 -3
- schemathesis/contrib/unique_data.py +1 -2
- schemathesis/exceptions.py +4 -3
- schemathesis/extra/_flask.py +4 -1
- schemathesis/extra/pytest_plugin.py +6 -3
- schemathesis/failures.py +2 -1
- schemathesis/filters.py +2 -2
- schemathesis/generation/__init__.py +2 -2
- schemathesis/generation/_hypothesis.py +1 -1
- schemathesis/generation/coverage.py +53 -12
- schemathesis/graphql.py +0 -1
- schemathesis/hooks.py +3 -3
- schemathesis/internal/checks.py +53 -0
- schemathesis/lazy.py +10 -7
- schemathesis/loaders.py +3 -3
- schemathesis/models.py +59 -23
- schemathesis/runner/__init__.py +12 -6
- schemathesis/runner/events.py +1 -1
- schemathesis/runner/impl/context.py +72 -0
- schemathesis/runner/impl/core.py +105 -67
- schemathesis/runner/impl/solo.py +17 -20
- schemathesis/runner/impl/threadpool.py +65 -72
- schemathesis/runner/serialization.py +4 -3
- schemathesis/sanitization.py +2 -1
- schemathesis/schemas.py +20 -22
- schemathesis/serializers.py +2 -0
- schemathesis/service/client.py +1 -1
- schemathesis/service/events.py +4 -1
- schemathesis/service/extensions.py +2 -2
- schemathesis/service/hosts.py +4 -2
- schemathesis/service/models.py +3 -3
- schemathesis/service/report.py +3 -3
- schemathesis/service/serialization.py +4 -2
- schemathesis/specs/graphql/loaders.py +5 -4
- schemathesis/specs/graphql/schemas.py +13 -8
- schemathesis/specs/openapi/checks.py +76 -27
- schemathesis/specs/openapi/definitions.py +1 -5
- schemathesis/specs/openapi/examples.py +92 -2
- schemathesis/specs/openapi/expressions/__init__.py +7 -0
- schemathesis/specs/openapi/expressions/extractors.py +4 -1
- schemathesis/specs/openapi/expressions/nodes.py +5 -3
- schemathesis/specs/openapi/links.py +4 -4
- schemathesis/specs/openapi/loaders.py +6 -5
- schemathesis/specs/openapi/negative/__init__.py +5 -3
- schemathesis/specs/openapi/negative/mutations.py +5 -4
- schemathesis/specs/openapi/parameters.py +4 -2
- schemathesis/specs/openapi/schemas.py +28 -13
- schemathesis/specs/openapi/security.py +6 -4
- schemathesis/specs/openapi/stateful/__init__.py +2 -2
- schemathesis/specs/openapi/stateful/statistic.py +3 -3
- schemathesis/specs/openapi/stateful/types.py +3 -2
- schemathesis/stateful/__init__.py +3 -3
- schemathesis/stateful/config.py +2 -1
- schemathesis/stateful/context.py +13 -3
- schemathesis/stateful/events.py +3 -3
- schemathesis/stateful/runner.py +24 -6
- schemathesis/stateful/sink.py +1 -1
- schemathesis/stateful/state_machine.py +7 -6
- schemathesis/stateful/statistic.py +3 -1
- schemathesis/stateful/validation.py +10 -5
- schemathesis/transports/__init__.py +2 -2
- schemathesis/transports/asgi.py +7 -0
- schemathesis/transports/auth.py +2 -1
- schemathesis/transports/content_types.py +1 -1
- schemathesis/transports/responses.py +2 -1
- schemathesis/utils.py +4 -2
- {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/METADATA +1 -1
- schemathesis-3.36.0.dist-info/RECORD +157 -0
- schemathesis-3.35.4.dist-info/RECORD +0 -154
- {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/WHEEL +0 -0
- {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/licenses/LICENSE +0 -0
schemathesis/models.py
CHANGED
|
@@ -9,7 +9,6 @@ from dataclasses import dataclass, field
|
|
|
9
9
|
from enum import Enum
|
|
10
10
|
from functools import lru_cache, partial
|
|
11
11
|
from itertools import chain
|
|
12
|
-
from logging import LogRecord
|
|
13
12
|
from typing import (
|
|
14
13
|
TYPE_CHECKING,
|
|
15
14
|
Any,
|
|
@@ -18,7 +17,6 @@ from typing import (
|
|
|
18
17
|
Generic,
|
|
19
18
|
Iterator,
|
|
20
19
|
NoReturn,
|
|
21
|
-
Optional,
|
|
22
20
|
Sequence,
|
|
23
21
|
Type,
|
|
24
22
|
TypeVar,
|
|
@@ -28,7 +26,6 @@ from urllib.parse import quote, unquote, urljoin, urlsplit, urlunsplit
|
|
|
28
26
|
|
|
29
27
|
from . import serializers
|
|
30
28
|
from ._dependency_versions import IS_WERKZEUG_ABOVE_3
|
|
31
|
-
from .auths import AuthStorage
|
|
32
29
|
from .code_samples import CodeSampleStyle
|
|
33
30
|
from .constants import (
|
|
34
31
|
NOT_SET,
|
|
@@ -38,7 +35,6 @@ from .constants import (
|
|
|
38
35
|
)
|
|
39
36
|
from .exceptions import (
|
|
40
37
|
CheckFailed,
|
|
41
|
-
FailureContext,
|
|
42
38
|
OperationSchemaError,
|
|
43
39
|
SerializationNotPossible,
|
|
44
40
|
SkipTest,
|
|
@@ -49,24 +45,29 @@ from .exceptions import (
|
|
|
49
45
|
)
|
|
50
46
|
from .generation import DataGenerationMethod, GenerationConfig, generate_random_case_id
|
|
51
47
|
from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, dispatch
|
|
48
|
+
from .internal.checks import CheckContext
|
|
52
49
|
from .internal.copy import fast_deepcopy
|
|
53
50
|
from .internal.deprecation import deprecated_function, deprecated_property
|
|
54
51
|
from .internal.output import prepare_response_payload
|
|
55
52
|
from .parameters import Parameter, ParameterSet, PayloadAlternatives
|
|
56
53
|
from .sanitization import sanitize_request, sanitize_response
|
|
57
|
-
from .serializers import Serializer
|
|
58
54
|
from .transports import ASGITransport, RequestsTransport, WSGITransport, deserialize_payload, serialize_payload
|
|
59
55
|
from .types import Body, Cookies, FormData, Headers, NotSet, PathParameters, Query
|
|
60
56
|
|
|
61
57
|
if TYPE_CHECKING:
|
|
62
58
|
import unittest
|
|
59
|
+
from logging import LogRecord
|
|
63
60
|
|
|
64
61
|
import requests.auth
|
|
65
62
|
import werkzeug
|
|
66
63
|
from hypothesis import strategies as st
|
|
67
64
|
from requests.structures import CaseInsensitiveDict
|
|
68
65
|
|
|
66
|
+
from .auths import AuthStorage
|
|
67
|
+
from .failures import FailureContext
|
|
68
|
+
from .internal.checks import CheckFunction
|
|
69
69
|
from .schemas import BaseSchema
|
|
70
|
+
from .serializers import Serializer
|
|
70
71
|
from .stateful import Stateful, StatefulTest
|
|
71
72
|
from .transports.responses import GenericResponse, WSGIResponse
|
|
72
73
|
|
|
@@ -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:
|
|
@@ -995,7 +1010,7 @@ class Interaction:
|
|
|
995
1010
|
"""A single interaction with the target app."""
|
|
996
1011
|
|
|
997
1012
|
request: Request
|
|
998
|
-
response: Response
|
|
1013
|
+
response: Response | None
|
|
999
1014
|
checks: list[Check]
|
|
1000
1015
|
status: Status
|
|
1001
1016
|
data_generation_method: DataGenerationMethod
|
|
@@ -1003,10 +1018,28 @@ class Interaction:
|
|
|
1003
1018
|
recorded_at: str = field(default_factory=lambda: datetime.datetime.now(TIMEZONE).isoformat())
|
|
1004
1019
|
|
|
1005
1020
|
@classmethod
|
|
1006
|
-
def from_requests(
|
|
1021
|
+
def from_requests(
|
|
1022
|
+
cls,
|
|
1023
|
+
case: Case,
|
|
1024
|
+
response: requests.Response | None,
|
|
1025
|
+
status: Status,
|
|
1026
|
+
checks: list[Check],
|
|
1027
|
+
headers: dict[str, Any] | None,
|
|
1028
|
+
session: requests.Session | None,
|
|
1029
|
+
) -> Interaction:
|
|
1030
|
+
if response is not None:
|
|
1031
|
+
prepared = response.request
|
|
1032
|
+
request = Request.from_prepared_request(prepared)
|
|
1033
|
+
else:
|
|
1034
|
+
import requests
|
|
1035
|
+
|
|
1036
|
+
if session is None:
|
|
1037
|
+
session = requests.Session()
|
|
1038
|
+
session.headers.update(headers or {})
|
|
1039
|
+
request = Request.from_case(case, session)
|
|
1007
1040
|
return cls(
|
|
1008
|
-
request=
|
|
1009
|
-
response=Response.from_requests(response),
|
|
1041
|
+
request=request,
|
|
1042
|
+
response=Response.from_requests(response) if response is not None else None,
|
|
1010
1043
|
status=status,
|
|
1011
1044
|
checks=checks,
|
|
1012
1045
|
data_generation_method=cast(DataGenerationMethod, case.data_generation_method),
|
|
@@ -1017,9 +1050,9 @@ class Interaction:
|
|
|
1017
1050
|
def from_wsgi(
|
|
1018
1051
|
cls,
|
|
1019
1052
|
case: Case,
|
|
1020
|
-
response: WSGIResponse,
|
|
1053
|
+
response: WSGIResponse | None,
|
|
1021
1054
|
headers: dict[str, Any],
|
|
1022
|
-
elapsed: float,
|
|
1055
|
+
elapsed: float | None,
|
|
1023
1056
|
status: Status,
|
|
1024
1057
|
checks: list[Check],
|
|
1025
1058
|
) -> Interaction:
|
|
@@ -1029,7 +1062,7 @@ class Interaction:
|
|
|
1029
1062
|
session.headers.update(headers)
|
|
1030
1063
|
return cls(
|
|
1031
1064
|
request=Request.from_case(case, session),
|
|
1032
|
-
response=Response.from_wsgi(response, elapsed),
|
|
1065
|
+
response=Response.from_wsgi(response, elapsed) if response is not None and elapsed is not None else None,
|
|
1033
1066
|
status=status,
|
|
1034
1067
|
checks=checks,
|
|
1035
1068
|
data_generation_method=cast(DataGenerationMethod, case.data_generation_method),
|
|
@@ -1119,16 +1152,22 @@ class TestResult:
|
|
|
1119
1152
|
self.errors.append(exception)
|
|
1120
1153
|
|
|
1121
1154
|
def store_requests_response(
|
|
1122
|
-
self,
|
|
1155
|
+
self,
|
|
1156
|
+
case: Case,
|
|
1157
|
+
response: requests.Response | None,
|
|
1158
|
+
status: Status,
|
|
1159
|
+
checks: list[Check],
|
|
1160
|
+
headers: dict[str, Any] | None,
|
|
1161
|
+
session: requests.Session | None,
|
|
1123
1162
|
) -> None:
|
|
1124
|
-
self.interactions.append(Interaction.from_requests(case, response, status, checks))
|
|
1163
|
+
self.interactions.append(Interaction.from_requests(case, response, status, checks, headers, session))
|
|
1125
1164
|
|
|
1126
1165
|
def store_wsgi_response(
|
|
1127
1166
|
self,
|
|
1128
1167
|
case: Case,
|
|
1129
|
-
response: WSGIResponse,
|
|
1168
|
+
response: WSGIResponse | None,
|
|
1130
1169
|
headers: dict[str, Any],
|
|
1131
|
-
elapsed: float,
|
|
1170
|
+
elapsed: float | None,
|
|
1132
1171
|
status: Status,
|
|
1133
1172
|
checks: list[Check],
|
|
1134
1173
|
) -> None:
|
|
@@ -1209,6 +1248,3 @@ class TestResultSet:
|
|
|
1209
1248
|
def add_warning(self, warning: str) -> None:
|
|
1210
1249
|
"""Add a new warning to the warnings list."""
|
|
1211
1250
|
self.warnings.append(warning)
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
CheckFunction = Callable[["GenericResponse", Case], Optional[bool]]
|
schemathesis/runner/__init__.py
CHANGED
|
@@ -4,7 +4,6 @@ from random import Random
|
|
|
4
4
|
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable
|
|
5
5
|
from urllib.parse import urlparse
|
|
6
6
|
|
|
7
|
-
from .._override import CaseOverride
|
|
8
7
|
from ..constants import (
|
|
9
8
|
DEFAULT_DEADLINE,
|
|
10
9
|
DEFAULT_STATEFUL_RECURSION_LIMIT,
|
|
@@ -22,16 +21,17 @@ from ..targets import DEFAULT_TARGETS, Target
|
|
|
22
21
|
from ..transports import RequestConfig
|
|
23
22
|
from ..transports.auth import get_requests_auth
|
|
24
23
|
from ..types import Filter, NotSet, RawAuth, RequestCert
|
|
24
|
+
from . import events
|
|
25
25
|
from .probes import ProbeConfig
|
|
26
26
|
|
|
27
27
|
if TYPE_CHECKING:
|
|
28
28
|
import hypothesis
|
|
29
29
|
|
|
30
|
+
from .._override import CaseOverride
|
|
30
31
|
from ..models import CheckFunction
|
|
31
32
|
from ..schemas import BaseSchema
|
|
32
33
|
from ..service.client import ServiceClient
|
|
33
34
|
from ..stateful import Stateful
|
|
34
|
-
from . import events
|
|
35
35
|
from .impl import BaseRunner
|
|
36
36
|
|
|
37
37
|
|
|
@@ -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,
|
|
@@ -357,9 +358,9 @@ def from_schema(
|
|
|
357
358
|
service_client: ServiceClient | None = None,
|
|
358
359
|
) -> BaseRunner:
|
|
359
360
|
import hypothesis
|
|
360
|
-
from starlette.applications import Starlette
|
|
361
361
|
|
|
362
362
|
from ..checks import DEFAULT_CHECKS
|
|
363
|
+
from ..transports.asgi import is_asgi_app
|
|
363
364
|
from .impl import (
|
|
364
365
|
SingleThreadASGIRunner,
|
|
365
366
|
SingleThreadRunner,
|
|
@@ -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,
|
|
@@ -414,7 +415,7 @@ def from_schema(
|
|
|
414
415
|
probe_config=probe_config,
|
|
415
416
|
service_client=service_client,
|
|
416
417
|
)
|
|
417
|
-
if
|
|
418
|
+
if is_asgi_app(schema.app):
|
|
418
419
|
return ThreadPoolASGIRunner(
|
|
419
420
|
schema=schema,
|
|
420
421
|
checks=checks,
|
|
@@ -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,
|
|
@@ -490,7 +494,7 @@ def from_schema(
|
|
|
490
494
|
probe_config=probe_config,
|
|
491
495
|
service_client=service_client,
|
|
492
496
|
)
|
|
493
|
-
if
|
|
497
|
+
if is_asgi_app(schema.app):
|
|
494
498
|
return SingleThreadASGIRunner(
|
|
495
499
|
schema=schema,
|
|
496
500
|
checks=checks,
|
|
@@ -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,
|
schemathesis/runner/events.py
CHANGED
|
@@ -7,12 +7,12 @@ from dataclasses import asdict, dataclass, field
|
|
|
7
7
|
from typing import TYPE_CHECKING, Any
|
|
8
8
|
|
|
9
9
|
from ..exceptions import RuntimeErrorType, SchemaError, SchemaErrorType, format_exception
|
|
10
|
-
from ..generation import DataGenerationMethod
|
|
11
10
|
from ..internal.datetime import current_datetime
|
|
12
11
|
from ..internal.result import Err, Ok, Result
|
|
13
12
|
from .serialization import SerializedError, SerializedTestResult
|
|
14
13
|
|
|
15
14
|
if TYPE_CHECKING:
|
|
15
|
+
from ..generation import DataGenerationMethod
|
|
16
16
|
from ..models import APIOperation, Status, TestResult, TestResultSet
|
|
17
17
|
from ..schemas import BaseSchema, Specification
|
|
18
18
|
from ..service.models import AnalysisResult
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from ...constants import NOT_SET
|
|
7
|
+
from ...models import TestResult, TestResultSet
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
import threading
|
|
11
|
+
|
|
12
|
+
from ...exceptions import OperationSchemaError
|
|
13
|
+
from ...models import Case
|
|
14
|
+
from ...types import NotSet
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class RunnerContext:
|
|
19
|
+
"""Holds context shared for a test run."""
|
|
20
|
+
|
|
21
|
+
data: TestResultSet
|
|
22
|
+
seed: int | None
|
|
23
|
+
stop_event: threading.Event
|
|
24
|
+
unique_data: bool
|
|
25
|
+
outcome_cache: dict[int, BaseException | None]
|
|
26
|
+
|
|
27
|
+
__slots__ = ("data", "seed", "stop_event", "unique_data", "outcome_cache")
|
|
28
|
+
|
|
29
|
+
def __init__(self, *, seed: int | None, stop_event: threading.Event, unique_data: bool) -> None:
|
|
30
|
+
self.data = TestResultSet(seed=seed)
|
|
31
|
+
self.seed = seed
|
|
32
|
+
self.stop_event = stop_event
|
|
33
|
+
self.outcome_cache = {}
|
|
34
|
+
self.unique_data = unique_data
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def is_stopped(self) -> bool:
|
|
38
|
+
return self.stop_event.is_set()
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def has_all_not_found(self) -> bool:
|
|
42
|
+
"""Check if all responses are 404."""
|
|
43
|
+
has_not_found = False
|
|
44
|
+
for entry in self.data.results:
|
|
45
|
+
for check in entry.checks:
|
|
46
|
+
if check.response is not None:
|
|
47
|
+
if check.response.status_code == 404:
|
|
48
|
+
has_not_found = True
|
|
49
|
+
else:
|
|
50
|
+
# There are non-404 responses, no reason to check any other response
|
|
51
|
+
return False
|
|
52
|
+
# Only happens if all responses are 404, or there are no responses at all.
|
|
53
|
+
# In the first case, it returns True, for the latter - False
|
|
54
|
+
return has_not_found
|
|
55
|
+
|
|
56
|
+
def add_result(self, result: TestResult) -> None:
|
|
57
|
+
self.data.append(result)
|
|
58
|
+
|
|
59
|
+
def add_generic_error(self, error: OperationSchemaError) -> None:
|
|
60
|
+
self.data.generic_errors.append(error)
|
|
61
|
+
|
|
62
|
+
def add_warning(self, message: str) -> None:
|
|
63
|
+
self.data.add_warning(message)
|
|
64
|
+
|
|
65
|
+
def cache_outcome(self, case: Case, outcome: BaseException | None) -> None:
|
|
66
|
+
self.outcome_cache[hash(case)] = outcome
|
|
67
|
+
|
|
68
|
+
def get_cached_outcome(self, case: Case) -> BaseException | None | NotSet:
|
|
69
|
+
return self.outcome_cache.get(hash(case), NOT_SET)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
ALL_NOT_FOUND_WARNING_MESSAGE = "All API responses have a 404 status code. Did you specify the proper API location?"
|