schemathesis 3.15.4__py3-none-any.whl → 4.4.2__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 +53 -25
- schemathesis/auths.py +507 -0
- schemathesis/checks.py +190 -25
- schemathesis/cli/__init__.py +27 -1219
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +133 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +602 -0
- schemathesis/cli/commands/run/context.py +228 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +45 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
- schemathesis/cli/commands/run/handlers/output.py +1750 -0
- schemathesis/cli/commands/run/loaders.py +118 -0
- schemathesis/cli/commands/run/validation.py +256 -0
- schemathesis/cli/constants.py +5 -0
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +203 -0
- schemathesis/cli/ext/options.py +81 -0
- schemathesis/config/__init__.py +202 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +101 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +163 -0
- schemathesis/config/_generation.py +157 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +335 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +253 -0
- schemathesis/config/_projects.py +543 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +89 -0
- schemathesis/config/schema.json +975 -0
- schemathesis/core/__init__.py +72 -0
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +100 -0
- schemathesis/core/deserialization.py +210 -0
- schemathesis/core/errors.py +588 -0
- schemathesis/core/failures.py +316 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +183 -0
- schemathesis/core/jsonschema/keywords.py +40 -0
- schemathesis/core/jsonschema/references.py +222 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/lazy_import.py +15 -0
- schemathesis/core/loaders.py +107 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/core/media_types.py +79 -0
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/core/parameters.py +45 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +34 -0
- schemathesis/core/result.py +27 -0
- schemathesis/core/schema_analysis.py +17 -0
- schemathesis/core/shell.py +203 -0
- schemathesis/core/transforms.py +144 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +73 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +152 -0
- schemathesis/engine/control.py +44 -0
- schemathesis/engine/core.py +201 -0
- schemathesis/engine/errors.py +446 -0
- schemathesis/engine/events.py +284 -0
- schemathesis/engine/observations.py +42 -0
- schemathesis/engine/phases/__init__.py +108 -0
- schemathesis/engine/phases/analysis.py +28 -0
- schemathesis/engine/phases/probes.py +172 -0
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +364 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +220 -0
- schemathesis/engine/phases/unit/_executor.py +459 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +254 -0
- schemathesis/errors.py +47 -0
- schemathesis/filters.py +395 -0
- schemathesis/generation/__init__.py +25 -0
- schemathesis/generation/case.py +478 -0
- schemathesis/generation/coverage.py +1528 -0
- schemathesis/generation/hypothesis/__init__.py +121 -0
- schemathesis/generation/hypothesis/builder.py +992 -0
- schemathesis/generation/hypothesis/examples.py +56 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +285 -0
- schemathesis/generation/meta.py +227 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +127 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +294 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +285 -0
- schemathesis/hooks.py +270 -91
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +467 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +315 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +341 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/pytest/plugin.py +357 -0
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +682 -257
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/nodes.py +26 -2
- schemathesis/specs/graphql/scalars.py +77 -12
- schemathesis/specs/graphql/schemas.py +367 -148
- schemathesis/specs/graphql/validation.py +33 -0
- schemathesis/specs/openapi/__init__.py +9 -1
- schemathesis/specs/openapi/_hypothesis.py +555 -318
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +729 -0
- schemathesis/specs/openapi/adapter/protocol.py +59 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +368 -0
- schemathesis/specs/openapi/adapter/security.py +144 -0
- schemathesis/specs/openapi/adapter/v2.py +30 -0
- schemathesis/specs/openapi/adapter/v3_0.py +30 -0
- schemathesis/specs/openapi/adapter/v3_1.py +30 -0
- schemathesis/specs/openapi/analysis.py +96 -0
- schemathesis/specs/openapi/checks.py +748 -82
- schemathesis/specs/openapi/converter.py +176 -37
- schemathesis/specs/openapi/definitions.py +599 -4
- schemathesis/specs/openapi/examples.py +581 -165
- schemathesis/specs/openapi/expressions/__init__.py +52 -5
- schemathesis/specs/openapi/expressions/extractors.py +25 -0
- schemathesis/specs/openapi/expressions/lexer.py +34 -31
- schemathesis/specs/openapi/expressions/nodes.py +97 -46
- schemathesis/specs/openapi/expressions/parser.py +35 -13
- schemathesis/specs/openapi/formats.py +122 -0
- schemathesis/specs/openapi/media_types.py +75 -0
- schemathesis/specs/openapi/negative/__init__.py +93 -73
- schemathesis/specs/openapi/negative/mutations.py +294 -103
- schemathesis/specs/openapi/negative/utils.py +0 -9
- schemathesis/specs/openapi/patterns.py +458 -0
- schemathesis/specs/openapi/references.py +60 -81
- schemathesis/specs/openapi/schemas.py +647 -666
- schemathesis/specs/openapi/serialization.py +53 -30
- schemathesis/specs/openapi/stateful/__init__.py +403 -68
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
- schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
- schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
- schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
- schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
- schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
- schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
- schemathesis/specs/openapi/stateful/inference.py +254 -0
- schemathesis/specs/openapi/stateful/links.py +219 -78
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/common.py +23 -0
- schemathesis/specs/openapi/types/v2.py +129 -0
- schemathesis/specs/openapi/types/v3.py +134 -0
- schemathesis/specs/openapi/utils.py +7 -6
- schemathesis/specs/openapi/warnings.py +75 -0
- schemathesis/transport/__init__.py +224 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +126 -0
- schemathesis/transport/requests.py +278 -0
- schemathesis/transport/serialization.py +329 -0
- schemathesis/transport/wsgi.py +175 -0
- schemathesis-4.4.2.dist-info/METADATA +213 -0
- schemathesis-4.4.2.dist-info/RECORD +192 -0
- {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
- schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
- {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
- schemathesis/_compat.py +0 -57
- schemathesis/_hypothesis.py +0 -123
- schemathesis/auth.py +0 -214
- schemathesis/cli/callbacks.py +0 -240
- schemathesis/cli/cassettes.py +0 -351
- schemathesis/cli/context.py +0 -38
- schemathesis/cli/debug.py +0 -21
- schemathesis/cli/handlers.py +0 -11
- schemathesis/cli/junitxml.py +0 -41
- schemathesis/cli/options.py +0 -70
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -521
- schemathesis/cli/output/short.py +0 -40
- schemathesis/constants.py +0 -88
- schemathesis/exceptions.py +0 -257
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -16
- schemathesis/extra/pytest_plugin.py +0 -251
- schemathesis/failures.py +0 -145
- schemathesis/fixups/__init__.py +0 -29
- schemathesis/fixups/fast_api.py +0 -30
- schemathesis/graphql.py +0 -5
- schemathesis/internal.py +0 -6
- schemathesis/lazy.py +0 -301
- schemathesis/models.py +0 -1113
- schemathesis/parameters.py +0 -91
- schemathesis/runner/__init__.py +0 -470
- schemathesis/runner/events.py +0 -242
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -791
- schemathesis/runner/impl/solo.py +0 -85
- schemathesis/runner/impl/threadpool.py +0 -367
- schemathesis/runner/serialization.py +0 -206
- schemathesis/serializers.py +0 -253
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -10
- schemathesis/service/client.py +0 -62
- schemathesis/service/constants.py +0 -25
- schemathesis/service/events.py +0 -39
- schemathesis/service/handler.py +0 -46
- schemathesis/service/hosts.py +0 -74
- schemathesis/service/metadata.py +0 -42
- schemathesis/service/models.py +0 -21
- schemathesis/service/serialization.py +0 -184
- schemathesis/service/worker.py +0 -39
- schemathesis/specs/graphql/loaders.py +0 -215
- schemathesis/specs/openapi/constants.py +0 -7
- schemathesis/specs/openapi/expressions/context.py +0 -12
- schemathesis/specs/openapi/expressions/pointers.py +0 -29
- schemathesis/specs/openapi/filters.py +0 -44
- schemathesis/specs/openapi/links.py +0 -303
- schemathesis/specs/openapi/loaders.py +0 -453
- schemathesis/specs/openapi/parameters.py +0 -430
- schemathesis/specs/openapi/security.py +0 -129
- schemathesis/specs/openapi/validation.py +0 -24
- schemathesis/stateful.py +0 -358
- schemathesis/targets.py +0 -32
- schemathesis/types.py +0 -38
- schemathesis/utils.py +0 -475
- schemathesis-3.15.4.dist-info/METADATA +0 -202
- schemathesis-3.15.4.dist-info/RECORD +0 -99
- schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
schemathesis/runner/impl/solo.py
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import threading
|
|
2
|
-
from typing import Generator, Optional, Union
|
|
3
|
-
|
|
4
|
-
import attr
|
|
5
|
-
|
|
6
|
-
from ...models import TestResultSet
|
|
7
|
-
from ...types import RequestCert
|
|
8
|
-
from ...utils import get_requests_auth
|
|
9
|
-
from .. import events
|
|
10
|
-
from .core import BaseRunner, asgi_test, get_session, network_test, wsgi_test
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
14
|
-
class SingleThreadRunner(BaseRunner):
|
|
15
|
-
"""Fast runner that runs tests sequentially in the main thread."""
|
|
16
|
-
|
|
17
|
-
request_tls_verify: Union[bool, str] = attr.ib(default=True) # pragma: no mutate
|
|
18
|
-
request_cert: Optional[RequestCert] = attr.ib(default=None) # pragma: no mutate
|
|
19
|
-
|
|
20
|
-
def _execute(
|
|
21
|
-
self, results: TestResultSet, stop_event: threading.Event
|
|
22
|
-
) -> Generator[events.ExecutionEvent, None, None]:
|
|
23
|
-
for event in self._execute_impl(results):
|
|
24
|
-
yield event
|
|
25
|
-
if stop_event.is_set() or self._should_stop(event):
|
|
26
|
-
break
|
|
27
|
-
|
|
28
|
-
def _execute_impl(self, results: TestResultSet) -> Generator[events.ExecutionEvent, None, None]:
|
|
29
|
-
auth = get_requests_auth(self.auth, self.auth_type)
|
|
30
|
-
with get_session(auth) as session:
|
|
31
|
-
yield from self._run_tests(
|
|
32
|
-
self.schema.get_all_tests,
|
|
33
|
-
network_test,
|
|
34
|
-
self.hypothesis_settings,
|
|
35
|
-
self.seed,
|
|
36
|
-
checks=self.checks,
|
|
37
|
-
max_response_time=self.max_response_time,
|
|
38
|
-
targets=self.targets,
|
|
39
|
-
results=results,
|
|
40
|
-
session=session,
|
|
41
|
-
headers=self.headers,
|
|
42
|
-
request_timeout=self.request_timeout,
|
|
43
|
-
request_tls_verify=self.request_tls_verify,
|
|
44
|
-
request_cert=self.request_cert,
|
|
45
|
-
store_interactions=self.store_interactions,
|
|
46
|
-
dry_run=self.dry_run,
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
51
|
-
class SingleThreadWSGIRunner(SingleThreadRunner):
|
|
52
|
-
def _execute_impl(self, results: TestResultSet) -> Generator[events.ExecutionEvent, None, None]:
|
|
53
|
-
yield from self._run_tests(
|
|
54
|
-
self.schema.get_all_tests,
|
|
55
|
-
wsgi_test,
|
|
56
|
-
self.hypothesis_settings,
|
|
57
|
-
self.seed,
|
|
58
|
-
checks=self.checks,
|
|
59
|
-
max_response_time=self.max_response_time,
|
|
60
|
-
targets=self.targets,
|
|
61
|
-
results=results,
|
|
62
|
-
auth=self.auth,
|
|
63
|
-
auth_type=self.auth_type,
|
|
64
|
-
headers=self.headers,
|
|
65
|
-
store_interactions=self.store_interactions,
|
|
66
|
-
dry_run=self.dry_run,
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
71
|
-
class SingleThreadASGIRunner(SingleThreadRunner):
|
|
72
|
-
def _execute_impl(self, results: TestResultSet) -> Generator[events.ExecutionEvent, None, None]:
|
|
73
|
-
yield from self._run_tests(
|
|
74
|
-
self.schema.get_all_tests,
|
|
75
|
-
asgi_test,
|
|
76
|
-
self.hypothesis_settings,
|
|
77
|
-
self.seed,
|
|
78
|
-
checks=self.checks,
|
|
79
|
-
max_response_time=self.max_response_time,
|
|
80
|
-
targets=self.targets,
|
|
81
|
-
results=results,
|
|
82
|
-
headers=self.headers,
|
|
83
|
-
store_interactions=self.store_interactions,
|
|
84
|
-
dry_run=self.dry_run,
|
|
85
|
-
)
|
|
@@ -1,367 +0,0 @@
|
|
|
1
|
-
import ctypes
|
|
2
|
-
import queue
|
|
3
|
-
import threading
|
|
4
|
-
import time
|
|
5
|
-
from queue import Queue
|
|
6
|
-
from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Union, cast
|
|
7
|
-
|
|
8
|
-
import attr
|
|
9
|
-
import hypothesis
|
|
10
|
-
|
|
11
|
-
from ..._hypothesis import create_test
|
|
12
|
-
from ...models import CheckFunction, TestResultSet
|
|
13
|
-
from ...stateful import Feedback, Stateful
|
|
14
|
-
from ...targets import Target
|
|
15
|
-
from ...types import RawAuth, RequestCert
|
|
16
|
-
from ...utils import Ok, capture_hypothesis_output, get_requests_auth
|
|
17
|
-
from .. import events
|
|
18
|
-
from .core import BaseRunner, asgi_test, get_session, handle_schema_error, network_test, run_test, wsgi_test
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def _run_task(
|
|
22
|
-
test_template: Callable,
|
|
23
|
-
tasks_queue: Queue,
|
|
24
|
-
events_queue: Queue,
|
|
25
|
-
generator_done: threading.Event,
|
|
26
|
-
checks: Iterable[CheckFunction],
|
|
27
|
-
targets: Iterable[Target],
|
|
28
|
-
settings: hypothesis.settings,
|
|
29
|
-
seed: Optional[int],
|
|
30
|
-
results: TestResultSet,
|
|
31
|
-
stateful: Optional[Stateful],
|
|
32
|
-
stateful_recursion_limit: int,
|
|
33
|
-
**kwargs: Any,
|
|
34
|
-
) -> None:
|
|
35
|
-
def _run_tests(maker: Callable, recursion_level: int = 0) -> None:
|
|
36
|
-
if recursion_level > stateful_recursion_limit:
|
|
37
|
-
return
|
|
38
|
-
for _result, _data_generation_method in maker(test_template, settings, seed):
|
|
39
|
-
# `result` is always `Ok` here
|
|
40
|
-
_operation, test = _result.ok()
|
|
41
|
-
feedback = Feedback(stateful, _operation)
|
|
42
|
-
for _event in run_test(
|
|
43
|
-
_operation,
|
|
44
|
-
test,
|
|
45
|
-
checks,
|
|
46
|
-
data_generation_method,
|
|
47
|
-
targets,
|
|
48
|
-
results,
|
|
49
|
-
recursion_level=recursion_level,
|
|
50
|
-
feedback=feedback,
|
|
51
|
-
**kwargs,
|
|
52
|
-
):
|
|
53
|
-
events_queue.put(_event)
|
|
54
|
-
_run_tests(feedback.get_stateful_tests, recursion_level + 1)
|
|
55
|
-
|
|
56
|
-
with capture_hypothesis_output():
|
|
57
|
-
while True:
|
|
58
|
-
try:
|
|
59
|
-
result, data_generation_method = tasks_queue.get(timeout=0.001)
|
|
60
|
-
except queue.Empty:
|
|
61
|
-
# The queue is empty & there will be no more tasks
|
|
62
|
-
if generator_done.is_set():
|
|
63
|
-
break
|
|
64
|
-
# If there is a possibility for new tasks - try again
|
|
65
|
-
continue
|
|
66
|
-
if isinstance(result, Ok):
|
|
67
|
-
operation = result.ok()
|
|
68
|
-
test_function = create_test(
|
|
69
|
-
operation=operation,
|
|
70
|
-
test=test_template,
|
|
71
|
-
settings=settings,
|
|
72
|
-
seed=seed,
|
|
73
|
-
data_generation_method=data_generation_method,
|
|
74
|
-
)
|
|
75
|
-
items = (
|
|
76
|
-
Ok((operation, test_function)),
|
|
77
|
-
data_generation_method,
|
|
78
|
-
)
|
|
79
|
-
# This lambda ignores the input arguments to support the same interface for
|
|
80
|
-
# `feedback.get_stateful_tests`
|
|
81
|
-
_run_tests(lambda *_: (items,))
|
|
82
|
-
else:
|
|
83
|
-
for event in handle_schema_error(result.err(), results, data_generation_method, 0):
|
|
84
|
-
events_queue.put(event)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def thread_task(
|
|
88
|
-
tasks_queue: Queue,
|
|
89
|
-
events_queue: Queue,
|
|
90
|
-
generator_done: threading.Event,
|
|
91
|
-
checks: Iterable[CheckFunction],
|
|
92
|
-
targets: Iterable[Target],
|
|
93
|
-
settings: hypothesis.settings,
|
|
94
|
-
auth: Optional[RawAuth],
|
|
95
|
-
auth_type: Optional[str],
|
|
96
|
-
headers: Optional[Dict[str, Any]],
|
|
97
|
-
seed: Optional[int],
|
|
98
|
-
results: TestResultSet,
|
|
99
|
-
stateful: Optional[Stateful],
|
|
100
|
-
stateful_recursion_limit: int,
|
|
101
|
-
kwargs: Any,
|
|
102
|
-
) -> None:
|
|
103
|
-
"""A single task, that threads do.
|
|
104
|
-
|
|
105
|
-
Pretty similar to the default one-thread flow, but includes communication with the main thread via the events queue.
|
|
106
|
-
"""
|
|
107
|
-
prepared_auth = get_requests_auth(auth, auth_type)
|
|
108
|
-
with get_session(prepared_auth) as session:
|
|
109
|
-
_run_task(
|
|
110
|
-
network_test,
|
|
111
|
-
tasks_queue,
|
|
112
|
-
events_queue,
|
|
113
|
-
generator_done,
|
|
114
|
-
checks,
|
|
115
|
-
targets,
|
|
116
|
-
settings,
|
|
117
|
-
seed,
|
|
118
|
-
results,
|
|
119
|
-
stateful=stateful,
|
|
120
|
-
stateful_recursion_limit=stateful_recursion_limit,
|
|
121
|
-
session=session,
|
|
122
|
-
headers=headers,
|
|
123
|
-
**kwargs,
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def wsgi_thread_task(
|
|
128
|
-
tasks_queue: Queue,
|
|
129
|
-
events_queue: Queue,
|
|
130
|
-
generator_done: threading.Event,
|
|
131
|
-
checks: Iterable[CheckFunction],
|
|
132
|
-
targets: Iterable[Target],
|
|
133
|
-
settings: hypothesis.settings,
|
|
134
|
-
seed: Optional[int],
|
|
135
|
-
results: TestResultSet,
|
|
136
|
-
stateful: Optional[Stateful],
|
|
137
|
-
stateful_recursion_limit: int,
|
|
138
|
-
kwargs: Any,
|
|
139
|
-
) -> None:
|
|
140
|
-
_run_task(
|
|
141
|
-
wsgi_test,
|
|
142
|
-
tasks_queue,
|
|
143
|
-
events_queue,
|
|
144
|
-
generator_done,
|
|
145
|
-
checks,
|
|
146
|
-
targets,
|
|
147
|
-
settings,
|
|
148
|
-
seed,
|
|
149
|
-
results,
|
|
150
|
-
stateful=stateful,
|
|
151
|
-
stateful_recursion_limit=stateful_recursion_limit,
|
|
152
|
-
**kwargs,
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def asgi_thread_task(
|
|
157
|
-
tasks_queue: Queue,
|
|
158
|
-
events_queue: Queue,
|
|
159
|
-
generator_done: threading.Event,
|
|
160
|
-
checks: Iterable[CheckFunction],
|
|
161
|
-
targets: Iterable[Target],
|
|
162
|
-
settings: hypothesis.settings,
|
|
163
|
-
headers: Optional[Dict[str, Any]],
|
|
164
|
-
seed: Optional[int],
|
|
165
|
-
results: TestResultSet,
|
|
166
|
-
stateful: Optional[Stateful],
|
|
167
|
-
stateful_recursion_limit: int,
|
|
168
|
-
kwargs: Any,
|
|
169
|
-
) -> None:
|
|
170
|
-
_run_task(
|
|
171
|
-
asgi_test,
|
|
172
|
-
tasks_queue,
|
|
173
|
-
events_queue,
|
|
174
|
-
generator_done,
|
|
175
|
-
checks,
|
|
176
|
-
targets,
|
|
177
|
-
settings,
|
|
178
|
-
seed,
|
|
179
|
-
results,
|
|
180
|
-
stateful=stateful,
|
|
181
|
-
stateful_recursion_limit=stateful_recursion_limit,
|
|
182
|
-
headers=headers,
|
|
183
|
-
**kwargs,
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
def stop_worker(thread_id: int) -> None:
|
|
188
|
-
"""Raise an error in a thread, so it is possible to asynchronously stop thread execution."""
|
|
189
|
-
ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread_id), ctypes.py_object(SystemExit))
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
193
|
-
class ThreadPoolRunner(BaseRunner):
|
|
194
|
-
"""Spread different tests among multiple worker threads."""
|
|
195
|
-
|
|
196
|
-
workers_num: int = attr.ib(default=2) # pragma: no mutate
|
|
197
|
-
request_tls_verify: Union[bool, str] = attr.ib(default=True) # pragma: no mutate
|
|
198
|
-
request_cert: Optional[RequestCert] = attr.ib(default=None) # pragma: no mutate
|
|
199
|
-
|
|
200
|
-
def _execute(
|
|
201
|
-
self, results: TestResultSet, stop_event: threading.Event
|
|
202
|
-
) -> Generator[events.ExecutionEvent, None, None]:
|
|
203
|
-
"""All events come from a queue where different workers push their events."""
|
|
204
|
-
# Instead of generating all tests at once, we do it when there is a free worker to pick it up
|
|
205
|
-
# This is extremely important for memory consumption when testing large schemas
|
|
206
|
-
# IMPLEMENTATION NOTE:
|
|
207
|
-
# It would be better to have a separate producer thread and communicate via threading events.
|
|
208
|
-
# Though it is a bit more complex, so the current solution is suboptimal in terms of resources utilization,
|
|
209
|
-
# but good enough and easy enough to implement.
|
|
210
|
-
tasks_generator = (
|
|
211
|
-
(operation, data_generation_method)
|
|
212
|
-
for operation in self.schema.get_all_operations()
|
|
213
|
-
for data_generation_method in self.schema.data_generation_methods
|
|
214
|
-
)
|
|
215
|
-
generator_done = threading.Event()
|
|
216
|
-
tasks_queue: Queue = Queue()
|
|
217
|
-
# Add at least `workers_num` tasks first, so all workers are busy
|
|
218
|
-
for _ in range(self.workers_num):
|
|
219
|
-
try:
|
|
220
|
-
# SAFETY: Workers didn't start yet, direct modification is OK
|
|
221
|
-
tasks_queue.queue.append(next(tasks_generator))
|
|
222
|
-
except StopIteration:
|
|
223
|
-
generator_done.set()
|
|
224
|
-
break
|
|
225
|
-
# Events are pushed by workers via a separate queue
|
|
226
|
-
events_queue: Queue = Queue()
|
|
227
|
-
workers = self._init_workers(tasks_queue, events_queue, results, generator_done)
|
|
228
|
-
|
|
229
|
-
def stop_workers() -> None:
|
|
230
|
-
for worker in workers:
|
|
231
|
-
# workers are initialized at this point and `worker.ident` is set with an integer value
|
|
232
|
-
ident = cast(int, worker.ident)
|
|
233
|
-
stop_worker(ident)
|
|
234
|
-
worker.join()
|
|
235
|
-
|
|
236
|
-
is_finished = False
|
|
237
|
-
try:
|
|
238
|
-
while not is_finished:
|
|
239
|
-
# Sleep is needed for performance reasons
|
|
240
|
-
# each call to `is_alive` of an alive worker waits for a lock
|
|
241
|
-
# iterations without waiting are too frequent, and a lot of time will be spent on waiting for this locks
|
|
242
|
-
time.sleep(0.001)
|
|
243
|
-
is_finished = all(not worker.is_alive() for worker in workers)
|
|
244
|
-
while not events_queue.empty():
|
|
245
|
-
event = events_queue.get()
|
|
246
|
-
if stop_event.is_set() or isinstance(event, events.Interrupted) or self._should_stop(event):
|
|
247
|
-
# We could still have events in the queue, but ignore them to keep the logic simple
|
|
248
|
-
# for now, could be improved in the future to show more info in such corner cases
|
|
249
|
-
stop_workers()
|
|
250
|
-
is_finished = True
|
|
251
|
-
if stop_event.is_set():
|
|
252
|
-
# Discard the event. The invariant is: the next event after `stream.stop()` is `Finished`
|
|
253
|
-
break
|
|
254
|
-
yield event
|
|
255
|
-
# When we know that there are more tasks, put another task to the queue.
|
|
256
|
-
# The worker might not actually finish the current one yet, but we put the new one now, so
|
|
257
|
-
# the worker can immediately pick it up when the current one is done
|
|
258
|
-
if isinstance(event, events.BeforeExecution) and not generator_done.is_set():
|
|
259
|
-
try:
|
|
260
|
-
tasks_queue.put(next(tasks_generator))
|
|
261
|
-
except StopIteration:
|
|
262
|
-
generator_done.set()
|
|
263
|
-
except KeyboardInterrupt:
|
|
264
|
-
stop_workers()
|
|
265
|
-
yield events.Interrupted()
|
|
266
|
-
|
|
267
|
-
def _init_workers(
|
|
268
|
-
self, tasks_queue: Queue, events_queue: Queue, results: TestResultSet, generator_done: threading.Event
|
|
269
|
-
) -> List[threading.Thread]:
|
|
270
|
-
"""Initialize & start workers that will execute tests."""
|
|
271
|
-
workers = [
|
|
272
|
-
threading.Thread(
|
|
273
|
-
target=self._get_task(),
|
|
274
|
-
kwargs=self._get_worker_kwargs(tasks_queue, events_queue, results, generator_done),
|
|
275
|
-
name=f"schemathesis_{num}",
|
|
276
|
-
)
|
|
277
|
-
for num in range(self.workers_num)
|
|
278
|
-
]
|
|
279
|
-
for worker in workers:
|
|
280
|
-
worker.start()
|
|
281
|
-
return workers
|
|
282
|
-
|
|
283
|
-
def _get_task(self) -> Callable:
|
|
284
|
-
return thread_task
|
|
285
|
-
|
|
286
|
-
def _get_worker_kwargs(
|
|
287
|
-
self, tasks_queue: Queue, events_queue: Queue, results: TestResultSet, generator_done: threading.Event
|
|
288
|
-
) -> Dict[str, Any]:
|
|
289
|
-
return {
|
|
290
|
-
"tasks_queue": tasks_queue,
|
|
291
|
-
"events_queue": events_queue,
|
|
292
|
-
"generator_done": generator_done,
|
|
293
|
-
"checks": self.checks,
|
|
294
|
-
"targets": self.targets,
|
|
295
|
-
"settings": self.hypothesis_settings,
|
|
296
|
-
"auth": self.auth,
|
|
297
|
-
"auth_type": self.auth_type,
|
|
298
|
-
"headers": self.headers,
|
|
299
|
-
"seed": self.seed,
|
|
300
|
-
"results": results,
|
|
301
|
-
"stateful": self.stateful,
|
|
302
|
-
"stateful_recursion_limit": self.stateful_recursion_limit,
|
|
303
|
-
"kwargs": {
|
|
304
|
-
"request_timeout": self.request_timeout,
|
|
305
|
-
"request_tls_verify": self.request_tls_verify,
|
|
306
|
-
"request_cert": self.request_cert,
|
|
307
|
-
"store_interactions": self.store_interactions,
|
|
308
|
-
"max_response_time": self.max_response_time,
|
|
309
|
-
"dry_run": self.dry_run,
|
|
310
|
-
},
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
class ThreadPoolWSGIRunner(ThreadPoolRunner):
|
|
315
|
-
def _get_task(self) -> Callable:
|
|
316
|
-
return wsgi_thread_task
|
|
317
|
-
|
|
318
|
-
def _get_worker_kwargs(
|
|
319
|
-
self, tasks_queue: Queue, events_queue: Queue, results: TestResultSet, generator_done: threading.Event
|
|
320
|
-
) -> Dict[str, Any]:
|
|
321
|
-
return {
|
|
322
|
-
"tasks_queue": tasks_queue,
|
|
323
|
-
"events_queue": events_queue,
|
|
324
|
-
"generator_done": generator_done,
|
|
325
|
-
"checks": self.checks,
|
|
326
|
-
"targets": self.targets,
|
|
327
|
-
"settings": self.hypothesis_settings,
|
|
328
|
-
"seed": self.seed,
|
|
329
|
-
"results": results,
|
|
330
|
-
"stateful": self.stateful,
|
|
331
|
-
"stateful_recursion_limit": self.stateful_recursion_limit,
|
|
332
|
-
"kwargs": {
|
|
333
|
-
"auth": self.auth,
|
|
334
|
-
"auth_type": self.auth_type,
|
|
335
|
-
"headers": self.headers,
|
|
336
|
-
"store_interactions": self.store_interactions,
|
|
337
|
-
"max_response_time": self.max_response_time,
|
|
338
|
-
"dry_run": self.dry_run,
|
|
339
|
-
},
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
class ThreadPoolASGIRunner(ThreadPoolRunner):
|
|
344
|
-
def _get_task(self) -> Callable:
|
|
345
|
-
return asgi_thread_task
|
|
346
|
-
|
|
347
|
-
def _get_worker_kwargs(
|
|
348
|
-
self, tasks_queue: Queue, events_queue: Queue, results: TestResultSet, generator_done: threading.Event
|
|
349
|
-
) -> Dict[str, Any]:
|
|
350
|
-
return {
|
|
351
|
-
"tasks_queue": tasks_queue,
|
|
352
|
-
"events_queue": events_queue,
|
|
353
|
-
"generator_done": generator_done,
|
|
354
|
-
"checks": self.checks,
|
|
355
|
-
"targets": self.targets,
|
|
356
|
-
"settings": self.hypothesis_settings,
|
|
357
|
-
"headers": self.headers,
|
|
358
|
-
"seed": self.seed,
|
|
359
|
-
"results": results,
|
|
360
|
-
"stateful": self.stateful,
|
|
361
|
-
"stateful_recursion_limit": self.stateful_recursion_limit,
|
|
362
|
-
"kwargs": {
|
|
363
|
-
"store_interactions": self.store_interactions,
|
|
364
|
-
"max_response_time": self.max_response_time,
|
|
365
|
-
"dry_run": self.dry_run,
|
|
366
|
-
},
|
|
367
|
-
}
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
"""Transformation from Schemathesis-specific data structures to ones that can be serialized and sent over network.
|
|
2
|
-
|
|
3
|
-
They all consist of primitive types and don't have references to schemas, app, etc.
|
|
4
|
-
"""
|
|
5
|
-
import logging
|
|
6
|
-
from typing import Any, Dict, Generator, List, Optional, Set, Tuple
|
|
7
|
-
|
|
8
|
-
import attr
|
|
9
|
-
import requests
|
|
10
|
-
|
|
11
|
-
from ..exceptions import FailureContext, InternalError, make_unique_by_key
|
|
12
|
-
from ..models import Case, Check, Interaction, Request, Response, Status, TestResult
|
|
13
|
-
from ..utils import WSGIResponse, format_exception
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
17
|
-
class SerializedCase:
|
|
18
|
-
text_lines: List[str] = attr.ib() # pragma: no mutate
|
|
19
|
-
requests_code: str = attr.ib()
|
|
20
|
-
curl_code: str = attr.ib()
|
|
21
|
-
path_template: str = attr.ib()
|
|
22
|
-
path_parameters: Optional[Dict[str, Any]] = attr.ib()
|
|
23
|
-
query: Optional[Dict[str, Any]] = attr.ib()
|
|
24
|
-
cookies: Optional[Dict[str, Any]] = attr.ib()
|
|
25
|
-
verbose_name: str = attr.ib()
|
|
26
|
-
media_type: Optional[str] = attr.ib()
|
|
27
|
-
|
|
28
|
-
@classmethod
|
|
29
|
-
def from_case(cls, case: Case, headers: Optional[Dict[str, Any]]) -> "SerializedCase":
|
|
30
|
-
return cls(
|
|
31
|
-
text_lines=case.as_text_lines(headers),
|
|
32
|
-
requests_code=case.get_code_to_reproduce(headers),
|
|
33
|
-
curl_code=case.as_curl_command(headers),
|
|
34
|
-
path_template=case.path,
|
|
35
|
-
path_parameters=case.path_parameters,
|
|
36
|
-
query=case.query,
|
|
37
|
-
cookies=case.cookies,
|
|
38
|
-
verbose_name=case.operation.verbose_name,
|
|
39
|
-
media_type=case.media_type,
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
44
|
-
class SerializedCheck:
|
|
45
|
-
# Check name
|
|
46
|
-
name: str = attr.ib() # pragma: no mutate
|
|
47
|
-
# Check result
|
|
48
|
-
value: Status = attr.ib() # pragma: no mutate
|
|
49
|
-
request: Request = attr.ib() # pragma: no mutate
|
|
50
|
-
response: Optional[Response] = attr.ib() # pragma: no mutate
|
|
51
|
-
# Generated example
|
|
52
|
-
example: SerializedCase = attr.ib() # pragma: no mutate
|
|
53
|
-
message: Optional[str] = attr.ib(default=None) # pragma: no mutate
|
|
54
|
-
# Failure-specific context
|
|
55
|
-
context: Optional[FailureContext] = attr.ib(default=None) # pragma: no mutate
|
|
56
|
-
# Cases & responses that were made before this one
|
|
57
|
-
history: List["SerializedHistoryEntry"] = attr.ib(factory=list) # pragma: no mutate
|
|
58
|
-
|
|
59
|
-
@classmethod
|
|
60
|
-
def from_check(cls, check: Check) -> "SerializedCheck":
|
|
61
|
-
if check.response is not None:
|
|
62
|
-
request = Request.from_prepared_request(check.response.request)
|
|
63
|
-
elif check.request is not None:
|
|
64
|
-
# Response is not available, but it is not an error (only time-out behaves this way at the moment)
|
|
65
|
-
request = Request.from_prepared_request(check.request)
|
|
66
|
-
else:
|
|
67
|
-
raise InternalError("Can not find request data")
|
|
68
|
-
|
|
69
|
-
response: Optional[Response]
|
|
70
|
-
if isinstance(check.response, requests.Response):
|
|
71
|
-
response = Response.from_requests(check.response)
|
|
72
|
-
elif isinstance(check.response, WSGIResponse):
|
|
73
|
-
response = Response.from_wsgi(check.response, check.elapsed)
|
|
74
|
-
else:
|
|
75
|
-
response = None
|
|
76
|
-
headers = {key: value[0] for key, value in request.headers.items()}
|
|
77
|
-
history = []
|
|
78
|
-
case = check.example
|
|
79
|
-
while case.source is not None:
|
|
80
|
-
if isinstance(case.source.response, requests.Response):
|
|
81
|
-
history_response = Response.from_requests(case.source.response)
|
|
82
|
-
else:
|
|
83
|
-
history_response = Response.from_wsgi(case.source.response, case.source.elapsed)
|
|
84
|
-
entry = SerializedHistoryEntry(
|
|
85
|
-
case=SerializedCase.from_case(case.source.case, headers), response=history_response
|
|
86
|
-
)
|
|
87
|
-
history.append(entry)
|
|
88
|
-
case = case.source.case
|
|
89
|
-
return cls(
|
|
90
|
-
name=check.name,
|
|
91
|
-
value=check.value,
|
|
92
|
-
example=SerializedCase.from_case(check.example, headers),
|
|
93
|
-
message=check.message,
|
|
94
|
-
request=request,
|
|
95
|
-
response=response,
|
|
96
|
-
context=check.context,
|
|
97
|
-
history=history,
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
102
|
-
class SerializedHistoryEntry:
|
|
103
|
-
case: SerializedCase = attr.ib()
|
|
104
|
-
response: Response = attr.ib()
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
108
|
-
class SerializedError:
|
|
109
|
-
exception: str = attr.ib() # pragma: no mutate
|
|
110
|
-
exception_with_traceback: str = attr.ib() # pragma: no mutate
|
|
111
|
-
example: Optional[SerializedCase] = attr.ib() # pragma: no mutate
|
|
112
|
-
title: Optional[str] = attr.ib() # pragma: no mutate
|
|
113
|
-
|
|
114
|
-
@classmethod
|
|
115
|
-
def from_error(
|
|
116
|
-
cls, exception: Exception, case: Optional[Case], headers: Optional[Dict[str, Any]], title: Optional[str] = None
|
|
117
|
-
) -> "SerializedError":
|
|
118
|
-
return cls(
|
|
119
|
-
exception=format_exception(exception),
|
|
120
|
-
exception_with_traceback=format_exception(exception, True),
|
|
121
|
-
example=SerializedCase.from_case(case, headers) if case else None,
|
|
122
|
-
title=title,
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
127
|
-
class SerializedInteraction:
|
|
128
|
-
request: Request = attr.ib() # pragma: no mutate
|
|
129
|
-
response: Response = attr.ib() # pragma: no mutate
|
|
130
|
-
checks: List[SerializedCheck] = attr.ib() # pragma: no mutate
|
|
131
|
-
status: Status = attr.ib() # pragma: no mutate
|
|
132
|
-
recorded_at: str = attr.ib() # pragma: no mutate
|
|
133
|
-
|
|
134
|
-
@classmethod
|
|
135
|
-
def from_interaction(cls, interaction: Interaction) -> "SerializedInteraction":
|
|
136
|
-
return cls(
|
|
137
|
-
request=interaction.request,
|
|
138
|
-
response=interaction.response,
|
|
139
|
-
checks=[SerializedCheck.from_check(check) for check in interaction.checks],
|
|
140
|
-
status=interaction.status,
|
|
141
|
-
recorded_at=interaction.recorded_at,
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
146
|
-
class SerializedTestResult:
|
|
147
|
-
method: str = attr.ib() # pragma: no mutate
|
|
148
|
-
path: str = attr.ib() # pragma: no mutate
|
|
149
|
-
verbose_name: str = attr.ib() # pragma: no mutate
|
|
150
|
-
has_failures: bool = attr.ib() # pragma: no mutate
|
|
151
|
-
has_errors: bool = attr.ib() # pragma: no mutate
|
|
152
|
-
has_logs: bool = attr.ib() # pragma: no mutate
|
|
153
|
-
is_errored: bool = attr.ib() # pragma: no mutate
|
|
154
|
-
is_flaky: bool = attr.ib() # pragma: no mutate
|
|
155
|
-
is_skipped: bool = attr.ib() # pragma: no mutate
|
|
156
|
-
seed: Optional[int] = attr.ib() # pragma: no mutate
|
|
157
|
-
data_generation_method: str = attr.ib() # pragma: no mutate
|
|
158
|
-
checks: List[SerializedCheck] = attr.ib() # pragma: no mutate
|
|
159
|
-
logs: List[str] = attr.ib() # pragma: no mutate
|
|
160
|
-
errors: List[SerializedError] = attr.ib() # pragma: no mutate
|
|
161
|
-
interactions: List[SerializedInteraction] = attr.ib() # pragma: no mutate
|
|
162
|
-
|
|
163
|
-
@classmethod
|
|
164
|
-
def from_test_result(cls, result: TestResult) -> "SerializedTestResult":
|
|
165
|
-
formatter = logging.Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s")
|
|
166
|
-
return cls(
|
|
167
|
-
method=result.method,
|
|
168
|
-
path=result.path,
|
|
169
|
-
verbose_name=result.verbose_name,
|
|
170
|
-
has_failures=result.has_failures,
|
|
171
|
-
has_errors=result.has_errors,
|
|
172
|
-
has_logs=result.has_logs,
|
|
173
|
-
is_errored=result.is_errored,
|
|
174
|
-
is_flaky=result.is_flaky,
|
|
175
|
-
is_skipped=result.is_skipped,
|
|
176
|
-
seed=result.seed,
|
|
177
|
-
data_generation_method=result.data_generation_method.as_short_name(),
|
|
178
|
-
checks=[SerializedCheck.from_check(check) for check in result.checks],
|
|
179
|
-
logs=[formatter.format(record) for record in result.logs],
|
|
180
|
-
errors=[SerializedError.from_error(*error, headers=result.overridden_headers) for error in result.errors],
|
|
181
|
-
interactions=[SerializedInteraction.from_interaction(interaction) for interaction in result.interactions],
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def deduplicate_failures(checks: List[SerializedCheck]) -> List[SerializedCheck]:
|
|
186
|
-
"""Return only unique checks that should be displayed in the output."""
|
|
187
|
-
seen: Set[Tuple[Optional[str], ...]] = set()
|
|
188
|
-
unique_checks = []
|
|
189
|
-
for check in reversed(checks):
|
|
190
|
-
# There are also could be checks that didn't fail
|
|
191
|
-
if check.value == Status.failure:
|
|
192
|
-
key = make_unique_by_key(check.name, check.message, check.context)
|
|
193
|
-
if key not in seen:
|
|
194
|
-
unique_checks.append(check)
|
|
195
|
-
seen.add(key)
|
|
196
|
-
return unique_checks
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
def deduplicate_checks(checks: List[SerializedCheck]) -> Generator[SerializedCheck, None, None]:
|
|
200
|
-
"""Return only unique checks outcomes."""
|
|
201
|
-
seen: Set[Tuple[Optional[str], ...]] = set()
|
|
202
|
-
for check in reversed(checks):
|
|
203
|
-
key = make_unique_by_key(check.name, check.message, check.context)
|
|
204
|
-
if key not in seen:
|
|
205
|
-
yield check
|
|
206
|
-
seen.add(key)
|