schemathesis 3.25.6__py3-none-any.whl → 4.0.0a1__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 +27 -65
- schemathesis/auths.py +102 -82
- schemathesis/checks.py +126 -46
- schemathesis/cli/__init__.py +11 -1760
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +37 -0
- schemathesis/cli/commands/run/__init__.py +662 -0
- schemathesis/cli/commands/run/checks.py +80 -0
- schemathesis/cli/commands/run/context.py +117 -0
- schemathesis/cli/commands/run/events.py +35 -0
- schemathesis/cli/commands/run/executor.py +138 -0
- schemathesis/cli/commands/run/filters.py +194 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +746 -0
- schemathesis/cli/commands/run/hypothesis.py +105 -0
- schemathesis/cli/commands/run/loaders.py +129 -0
- schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
- schemathesis/cli/constants.py +5 -52
- schemathesis/cli/core.py +17 -0
- schemathesis/cli/ext/fs.py +14 -0
- schemathesis/cli/ext/groups.py +55 -0
- schemathesis/cli/{options.py → ext/options.py} +39 -10
- schemathesis/cli/hooks.py +36 -0
- schemathesis/contrib/__init__.py +1 -3
- schemathesis/contrib/openapi/__init__.py +1 -3
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
- schemathesis/core/__init__.py +58 -0
- schemathesis/core/compat.py +25 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +370 -0
- schemathesis/core/failures.py +285 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
- schemathesis/core/output/__init__.py +69 -0
- schemathesis/core/output/sanitization.py +197 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +31 -0
- schemathesis/{internal → core}/result.py +1 -1
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +108 -0
- schemathesis/core/validation.py +38 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +30 -0
- schemathesis/engine/config.py +59 -0
- schemathesis/engine/context.py +119 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +157 -0
- schemathesis/engine/errors.py +394 -0
- schemathesis/engine/events.py +337 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{runner → engine/phases}/probes.py +50 -67
- schemathesis/engine/phases/stateful/__init__.py +65 -0
- schemathesis/engine/phases/stateful/_executor.py +326 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +174 -0
- schemathesis/engine/phases/unit/_executor.py +321 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +241 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +18 -14
- schemathesis/filters.py +103 -14
- schemathesis/generation/__init__.py +21 -37
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/generation/hypothesis/__init__.py +30 -0
- schemathesis/generation/hypothesis/builder.py +585 -0
- schemathesis/generation/hypothesis/examples.py +50 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/modes.py +28 -0
- schemathesis/generation/overrides.py +96 -0
- schemathesis/generation/stateful/__init__.py +20 -0
- schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +115 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +99 -67
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +412 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +63 -0
- schemathesis/openapi/loaders.py +178 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +273 -0
- schemathesis/pytest/loaders.py +12 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -261
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +25 -0
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +7 -5
- schemathesis/specs/graphql/schemas.py +215 -187
- schemathesis/specs/graphql/validation.py +11 -18
- schemathesis/specs/openapi/__init__.py +7 -1
- schemathesis/specs/openapi/_cache.py +122 -0
- schemathesis/specs/openapi/_hypothesis.py +146 -165
- schemathesis/specs/openapi/checks.py +565 -67
- schemathesis/specs/openapi/converter.py +33 -6
- schemathesis/specs/openapi/definitions.py +11 -18
- schemathesis/specs/openapi/examples.py +139 -23
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +4 -6
- schemathesis/specs/openapi/expressions/extractors.py +23 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +38 -14
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +45 -0
- schemathesis/specs/openapi/links.py +65 -165
- schemathesis/specs/openapi/media_types.py +32 -0
- schemathesis/specs/openapi/negative/__init__.py +7 -3
- schemathesis/specs/openapi/negative/mutations.py +24 -8
- schemathesis/specs/openapi/parameters.py +46 -30
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +47 -57
- schemathesis/specs/openapi/schemas.py +478 -369
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +11 -6
- schemathesis/specs/openapi/stateful/__init__.py +185 -73
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/transport/__init__.py +104 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +99 -0
- schemathesis/transport/requests.py +221 -0
- schemathesis/{_xml.py → transport/serialization.py} +143 -28
- schemathesis/transport/wsgi.py +165 -0
- schemathesis-4.0.0a1.dist-info/METADATA +297 -0
- schemathesis-4.0.0a1.dist-info/RECORD +152 -0
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -17
- schemathesis/_hypothesis.py +0 -246
- schemathesis/_override.py +0 -49
- schemathesis/cli/cassettes.py +0 -375
- schemathesis/cli/context.py +0 -58
- schemathesis/cli/debug.py +0 -26
- schemathesis/cli/handlers.py +0 -16
- schemathesis/cli/junitxml.py +0 -43
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -790
- schemathesis/cli/output/short.py +0 -44
- schemathesis/cli/sanitization.py +0 -20
- schemathesis/code_samples.py +0 -149
- schemathesis/constants.py +0 -55
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -15
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -560
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -17
- schemathesis/failures.py +0 -209
- schemathesis/fixups/__init__.py +0 -36
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -29
- schemathesis/graphql.py +0 -4
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/copy.py +0 -13
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -34
- schemathesis/internal/jsonschema.py +0 -35
- schemathesis/internal/transformation.py +0 -15
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -361
- schemathesis/loaders.py +0 -120
- schemathesis/models.py +0 -1234
- schemathesis/parameters.py +0 -86
- schemathesis/runner/__init__.py +0 -570
- schemathesis/runner/events.py +0 -329
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -1035
- schemathesis/runner/impl/solo.py +0 -90
- schemathesis/runner/impl/threadpool.py +0 -400
- schemathesis/runner/serialization.py +0 -411
- schemathesis/sanitization.py +0 -248
- schemathesis/serializers.py +0 -323
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -201
- schemathesis/service/client.py +0 -100
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -57
- schemathesis/service/hosts.py +0 -107
- schemathesis/service/metadata.py +0 -46
- schemathesis/service/models.py +0 -49
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -199
- schemathesis/service/usage.py +0 -65
- schemathesis/specs/graphql/loaders.py +0 -344
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/loaders.py +0 -667
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis/specs/openapi/validation.py +0 -25
- schemathesis/stateful/__init__.py +0 -133
- schemathesis/targets.py +0 -45
- schemathesis/throttling.py +0 -41
- schemathesis/transports/__init__.py +0 -5
- schemathesis/transports/auth.py +0 -15
- schemathesis/transports/headers.py +0 -35
- schemathesis/transports/responses.py +0 -52
- schemathesis/types.py +0 -35
- schemathesis/utils.py +0 -169
- schemathesis-3.25.6.dist-info/METADATA +0 -356
- schemathesis-3.25.6.dist-info/RECORD +0 -134
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
schemathesis/service/events.py
DELETED
@@ -1,57 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
from dataclasses import dataclass
|
3
|
-
|
4
|
-
from . import ci
|
5
|
-
from ..exceptions import format_exception
|
6
|
-
|
7
|
-
|
8
|
-
class Event:
|
9
|
-
"""Signalling events coming from the Schemathesis.io worker.
|
10
|
-
|
11
|
-
The purpose is to communicate with the thread that writes to stdout.
|
12
|
-
"""
|
13
|
-
|
14
|
-
@property
|
15
|
-
def status(self) -> str:
|
16
|
-
return self.__class__.__name__.upper()
|
17
|
-
|
18
|
-
|
19
|
-
@dataclass
|
20
|
-
class Metadata(Event):
|
21
|
-
"""Meta-information about the report."""
|
22
|
-
|
23
|
-
size: int
|
24
|
-
ci_environment: ci.Environment | None
|
25
|
-
|
26
|
-
|
27
|
-
@dataclass
|
28
|
-
class Completed(Event):
|
29
|
-
"""Report uploaded successfully."""
|
30
|
-
|
31
|
-
message: str
|
32
|
-
next_url: str
|
33
|
-
|
34
|
-
|
35
|
-
@dataclass
|
36
|
-
class Error(Event):
|
37
|
-
"""Internal error inside the Schemathesis.io handler."""
|
38
|
-
|
39
|
-
exception: Exception
|
40
|
-
|
41
|
-
def get_message(self, include_traceback: bool = False) -> str:
|
42
|
-
return format_exception(self.exception, include_traceback=include_traceback)
|
43
|
-
|
44
|
-
|
45
|
-
@dataclass
|
46
|
-
class Failed(Event):
|
47
|
-
"""A client-side error which should be displayed to the user."""
|
48
|
-
|
49
|
-
detail: str
|
50
|
-
|
51
|
-
|
52
|
-
@dataclass
|
53
|
-
class Timeout(Event):
|
54
|
-
"""The handler did not finish its work in time.
|
55
|
-
|
56
|
-
This event is not created in the handler itself, but rather in the main thread code to uniform the processing.
|
57
|
-
"""
|
schemathesis/service/hosts.py
DELETED
@@ -1,107 +0,0 @@
|
|
1
|
-
"""Work with stored auth data."""
|
2
|
-
from __future__ import annotations
|
3
|
-
import enum
|
4
|
-
import tempfile
|
5
|
-
from dataclasses import dataclass
|
6
|
-
from pathlib import Path
|
7
|
-
from typing import Any
|
8
|
-
|
9
|
-
import tomli
|
10
|
-
import tomli_w
|
11
|
-
|
12
|
-
from ..types import PathLike
|
13
|
-
from .constants import DEFAULT_HOSTNAME, DEFAULT_HOSTS_PATH, HOSTS_FORMAT_VERSION
|
14
|
-
|
15
|
-
|
16
|
-
@dataclass
|
17
|
-
class HostData:
|
18
|
-
"""Stored data related to a host."""
|
19
|
-
|
20
|
-
hostname: str
|
21
|
-
hosts_file: PathLike
|
22
|
-
|
23
|
-
def load(self) -> dict[str, Any]:
|
24
|
-
return load(self.hosts_file).get(self.hostname, {})
|
25
|
-
|
26
|
-
@property
|
27
|
-
def correlation_id(self) -> str | None:
|
28
|
-
return self.load().get("correlation_id")
|
29
|
-
|
30
|
-
def store_correlation_id(self, correlation_id: str) -> None:
|
31
|
-
"""Store `correlation_id` in the hosts file."""
|
32
|
-
hosts = load(self.hosts_file)
|
33
|
-
data = hosts.setdefault(self.hostname, {})
|
34
|
-
data["correlation_id"] = correlation_id
|
35
|
-
_dump_hosts(self.hosts_file, hosts)
|
36
|
-
|
37
|
-
|
38
|
-
def store(token: str, hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) -> None:
|
39
|
-
"""Store a new token for a host."""
|
40
|
-
# Don't use any file-based locking for simplicity
|
41
|
-
hosts = load(hosts_file)
|
42
|
-
data = hosts.setdefault(hostname, {})
|
43
|
-
data.update(version=HOSTS_FORMAT_VERSION, token=token)
|
44
|
-
_dump_hosts(hosts_file, hosts)
|
45
|
-
|
46
|
-
|
47
|
-
def load(path: PathLike) -> dict[str, Any]:
|
48
|
-
"""Load the given hosts file.
|
49
|
-
|
50
|
-
Return an empty dict if it doesn't exist.
|
51
|
-
"""
|
52
|
-
from ..utils import _ensure_parent
|
53
|
-
|
54
|
-
try:
|
55
|
-
with open(path, "rb") as fd:
|
56
|
-
return tomli.load(fd)
|
57
|
-
except FileNotFoundError:
|
58
|
-
_ensure_parent(path)
|
59
|
-
return {}
|
60
|
-
except tomli.TOMLDecodeError:
|
61
|
-
return {}
|
62
|
-
|
63
|
-
|
64
|
-
def load_for_host(hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) -> dict[str, Any]:
|
65
|
-
"""Load all data associated with a hostname."""
|
66
|
-
return load(hosts_file).get(hostname, {})
|
67
|
-
|
68
|
-
|
69
|
-
@enum.unique
|
70
|
-
class RemoveAuth(enum.Enum):
|
71
|
-
success = 1
|
72
|
-
no_match = 2
|
73
|
-
no_hosts = 3
|
74
|
-
error = 4
|
75
|
-
|
76
|
-
|
77
|
-
def remove(hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) -> RemoveAuth:
|
78
|
-
"""Remove authentication for a Schemathesis.io host."""
|
79
|
-
try:
|
80
|
-
with open(hosts_file, "rb") as fd:
|
81
|
-
hosts = tomli.load(fd)
|
82
|
-
try:
|
83
|
-
hosts.pop(hostname)
|
84
|
-
_dump_hosts(hosts_file, hosts)
|
85
|
-
return RemoveAuth.success
|
86
|
-
except KeyError:
|
87
|
-
return RemoveAuth.no_match
|
88
|
-
except FileNotFoundError:
|
89
|
-
return RemoveAuth.no_hosts
|
90
|
-
except tomli.TOMLDecodeError:
|
91
|
-
return RemoveAuth.error
|
92
|
-
|
93
|
-
|
94
|
-
def get_token(hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) -> str | None:
|
95
|
-
"""Load a token for a host."""
|
96
|
-
return load_for_host(hostname, hosts_file).get("token")
|
97
|
-
|
98
|
-
|
99
|
-
def get_temporary_hosts_file() -> str:
|
100
|
-
temporary_dir = Path(tempfile.gettempdir()).resolve()
|
101
|
-
return str(temporary_dir / "schemathesis-hosts.toml")
|
102
|
-
|
103
|
-
|
104
|
-
def _dump_hosts(path: PathLike, hosts: dict[str, Any]) -> None:
|
105
|
-
"""Write hosts data to a file."""
|
106
|
-
with open(path, "wb") as fd:
|
107
|
-
tomli_w.dump(hosts, fd)
|
schemathesis/service/metadata.py
DELETED
@@ -1,46 +0,0 @@
|
|
1
|
-
"""Useful info to collect from CLI usage."""
|
2
|
-
from __future__ import annotations
|
3
|
-
import os
|
4
|
-
import platform
|
5
|
-
from dataclasses import dataclass, field
|
6
|
-
|
7
|
-
from ..constants import SCHEMATHESIS_VERSION
|
8
|
-
from .constants import DOCKER_IMAGE_ENV_VAR
|
9
|
-
|
10
|
-
|
11
|
-
@dataclass
|
12
|
-
class PlatformMetadata:
|
13
|
-
# System / OS name, e.g. "Linux" or "Windows".
|
14
|
-
system: str = field(default_factory=platform.system)
|
15
|
-
# System release, e.g. "5.14" or "NT".
|
16
|
-
release: str = field(default_factory=platform.release)
|
17
|
-
# Machine type, e.g. "i386".
|
18
|
-
machine: str = field(default_factory=platform.machine)
|
19
|
-
|
20
|
-
|
21
|
-
@dataclass
|
22
|
-
class InterpreterMetadata:
|
23
|
-
# The Python version as "major.minor.patch".
|
24
|
-
version: str = field(default_factory=platform.python_version)
|
25
|
-
# Python implementation, e.g. "CPython" or "PyPy".
|
26
|
-
implementation: str = field(default_factory=platform.python_implementation)
|
27
|
-
|
28
|
-
|
29
|
-
@dataclass
|
30
|
-
class CliMetadata:
|
31
|
-
# Schemathesis package version.
|
32
|
-
version: str = SCHEMATHESIS_VERSION
|
33
|
-
|
34
|
-
|
35
|
-
@dataclass
|
36
|
-
class Metadata:
|
37
|
-
"""CLI environment metadata."""
|
38
|
-
|
39
|
-
# Information about the host platform.
|
40
|
-
platform: PlatformMetadata = field(default_factory=PlatformMetadata)
|
41
|
-
# Python interpreter info.
|
42
|
-
interpreter: InterpreterMetadata = field(default_factory=InterpreterMetadata)
|
43
|
-
# CLI info itself.
|
44
|
-
cli: CliMetadata = field(default_factory=CliMetadata)
|
45
|
-
# Used Docker image if any
|
46
|
-
docker_image: str | None = field(default_factory=lambda: os.getenv(DOCKER_IMAGE_ENV_VAR))
|
schemathesis/service/models.py
DELETED
@@ -1,49 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
from dataclasses import dataclass
|
3
|
-
from enum import Enum
|
4
|
-
from typing import Any
|
5
|
-
|
6
|
-
|
7
|
-
class UploadSource(str, Enum):
|
8
|
-
DEFAULT = "default"
|
9
|
-
UPLOAD_COMMAND = "upload_command"
|
10
|
-
|
11
|
-
|
12
|
-
@dataclass
|
13
|
-
class ProjectDetails:
|
14
|
-
environments: list[ProjectEnvironment]
|
15
|
-
specification: Specification
|
16
|
-
|
17
|
-
@property
|
18
|
-
def default_environment(self) -> ProjectEnvironment | None:
|
19
|
-
return next((env for env in self.environments if env.is_default), None)
|
20
|
-
|
21
|
-
|
22
|
-
@dataclass
|
23
|
-
class ProjectEnvironment:
|
24
|
-
url: str
|
25
|
-
name: str
|
26
|
-
description: str
|
27
|
-
is_default: bool
|
28
|
-
|
29
|
-
|
30
|
-
@dataclass
|
31
|
-
class Specification:
|
32
|
-
schema: dict[str, Any]
|
33
|
-
|
34
|
-
|
35
|
-
@dataclass
|
36
|
-
class AuthResponse:
|
37
|
-
username: str
|
38
|
-
|
39
|
-
|
40
|
-
@dataclass
|
41
|
-
class UploadResponse:
|
42
|
-
message: str
|
43
|
-
next_url: str
|
44
|
-
correlation_id: str
|
45
|
-
|
46
|
-
|
47
|
-
@dataclass
|
48
|
-
class FailedUploadResponse:
|
49
|
-
detail: str
|
schemathesis/service/report.py
DELETED
@@ -1,255 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
import enum
|
3
|
-
import json
|
4
|
-
import os
|
5
|
-
import tarfile
|
6
|
-
import threading
|
7
|
-
import time
|
8
|
-
from contextlib import suppress
|
9
|
-
from dataclasses import asdict, dataclass, field
|
10
|
-
from io import BytesIO
|
11
|
-
from queue import Queue
|
12
|
-
from typing import Any, TYPE_CHECKING
|
13
|
-
|
14
|
-
import click
|
15
|
-
|
16
|
-
from ..cli.handlers import EventHandler
|
17
|
-
from ..runner.events import Initialized, InternalError, Interrupted
|
18
|
-
from . import ci, events, usage
|
19
|
-
from .constants import REPORT_FORMAT_VERSION, STOP_MARKER, WORKER_JOIN_TIMEOUT
|
20
|
-
from .hosts import HostData
|
21
|
-
from .metadata import Metadata
|
22
|
-
from .models import UploadResponse
|
23
|
-
from .serialization import serialize_event
|
24
|
-
|
25
|
-
|
26
|
-
if TYPE_CHECKING:
|
27
|
-
from .client import ServiceClient
|
28
|
-
from ..cli.context import ExecutionContext
|
29
|
-
from ..runner.events import ExecutionEvent
|
30
|
-
|
31
|
-
|
32
|
-
@dataclass
|
33
|
-
class ReportWriter:
|
34
|
-
"""Schemathesis.io test run report.
|
35
|
-
|
36
|
-
Simplifies adding new files to the archive.
|
37
|
-
"""
|
38
|
-
|
39
|
-
_tar: tarfile.TarFile
|
40
|
-
_events_count: int = 0
|
41
|
-
|
42
|
-
def add_json_file(self, name: str, data: Any) -> None:
|
43
|
-
buffer = BytesIO()
|
44
|
-
buffer.write(json.dumps(data, separators=(",", ":")).encode())
|
45
|
-
buffer.seek(0)
|
46
|
-
info = tarfile.TarInfo(name=name)
|
47
|
-
info.size = len(buffer.getbuffer())
|
48
|
-
info.mtime = int(time.time())
|
49
|
-
self._tar.addfile(info, buffer)
|
50
|
-
|
51
|
-
def add_metadata(
|
52
|
-
self,
|
53
|
-
*,
|
54
|
-
api_name: str | None,
|
55
|
-
location: str,
|
56
|
-
base_url: str | None,
|
57
|
-
started_at: str,
|
58
|
-
metadata: Metadata,
|
59
|
-
ci_environment: ci.Environment | None,
|
60
|
-
usage_data: dict[str, Any] | None,
|
61
|
-
) -> None:
|
62
|
-
data = {
|
63
|
-
# API identifier on the Schemathesis.io side (optional)
|
64
|
-
"api_name": api_name,
|
65
|
-
# The place, where the API schema is located
|
66
|
-
"location": location,
|
67
|
-
# The base URL against which the tests are running
|
68
|
-
"base_url": base_url,
|
69
|
-
# The time that the test run began
|
70
|
-
"started_at": started_at,
|
71
|
-
# Metadata about CLI environment
|
72
|
-
"environment": asdict(metadata),
|
73
|
-
# Environment variables specific for CI providers
|
74
|
-
"ci": ci_environment.asdict() if ci_environment is not None else None,
|
75
|
-
# CLI usage statistic
|
76
|
-
"usage": usage_data,
|
77
|
-
# Report format version
|
78
|
-
"version": REPORT_FORMAT_VERSION,
|
79
|
-
}
|
80
|
-
self.add_json_file("metadata.json", data)
|
81
|
-
|
82
|
-
def add_event(self, event: ExecutionEvent) -> None:
|
83
|
-
"""Add an execution event to the report."""
|
84
|
-
self._events_count += 1
|
85
|
-
filename = f"events/{self._events_count}-{event.__class__.__name__}.json"
|
86
|
-
self.add_json_file(filename, serialize_event(event))
|
87
|
-
|
88
|
-
|
89
|
-
class BaseReportHandler(EventHandler):
|
90
|
-
in_queue: Queue
|
91
|
-
worker: threading.Thread
|
92
|
-
|
93
|
-
def handle_event(self, context: ExecutionContext, event: ExecutionEvent) -> None:
|
94
|
-
self.in_queue.put(event)
|
95
|
-
|
96
|
-
def shutdown(self) -> None:
|
97
|
-
self._stop_worker()
|
98
|
-
|
99
|
-
def _stop_worker(self) -> None:
|
100
|
-
self.in_queue.put(STOP_MARKER)
|
101
|
-
self.worker.join(WORKER_JOIN_TIMEOUT)
|
102
|
-
|
103
|
-
|
104
|
-
@dataclass
|
105
|
-
class ServiceReportHandler(BaseReportHandler):
|
106
|
-
client: ServiceClient
|
107
|
-
host_data: HostData
|
108
|
-
api_name: str | None
|
109
|
-
location: str
|
110
|
-
base_url: str | None
|
111
|
-
started_at: str
|
112
|
-
telemetry: bool
|
113
|
-
out_queue: Queue
|
114
|
-
in_queue: Queue = field(default_factory=Queue)
|
115
|
-
worker: threading.Thread = field(init=False)
|
116
|
-
|
117
|
-
def __post_init__(self) -> None:
|
118
|
-
self.worker = threading.Thread(
|
119
|
-
target=write_remote,
|
120
|
-
kwargs={
|
121
|
-
"client": self.client,
|
122
|
-
"host_data": self.host_data,
|
123
|
-
"api_name": self.api_name,
|
124
|
-
"location": self.location,
|
125
|
-
"base_url": self.base_url,
|
126
|
-
"started_at": self.started_at,
|
127
|
-
"in_queue": self.in_queue,
|
128
|
-
"out_queue": self.out_queue,
|
129
|
-
"usage_data": usage.collect() if self.telemetry else None,
|
130
|
-
},
|
131
|
-
)
|
132
|
-
self.worker.start()
|
133
|
-
|
134
|
-
|
135
|
-
@enum.unique
|
136
|
-
class ConsumeResult(enum.Enum):
|
137
|
-
NORMAL = 1
|
138
|
-
INTERRUPT = 2
|
139
|
-
|
140
|
-
|
141
|
-
def consume_events(writer: ReportWriter, in_queue: Queue) -> ConsumeResult:
|
142
|
-
while True:
|
143
|
-
event = in_queue.get()
|
144
|
-
if event is STOP_MARKER or isinstance(event, (Interrupted, InternalError)):
|
145
|
-
# If the run is interrupted, or there is an internal error - do not send the report
|
146
|
-
return ConsumeResult.INTERRUPT
|
147
|
-
# Add every event to the report
|
148
|
-
if isinstance(event, Initialized):
|
149
|
-
writer.add_json_file("schema.json", event.schema)
|
150
|
-
writer.add_event(event)
|
151
|
-
if event.is_terminal:
|
152
|
-
break
|
153
|
-
return ConsumeResult.NORMAL
|
154
|
-
|
155
|
-
|
156
|
-
def write_remote(
|
157
|
-
client: ServiceClient,
|
158
|
-
host_data: HostData,
|
159
|
-
api_name: str | None,
|
160
|
-
location: str,
|
161
|
-
base_url: str | None,
|
162
|
-
started_at: str,
|
163
|
-
in_queue: Queue,
|
164
|
-
out_queue: Queue,
|
165
|
-
usage_data: dict[str, Any] | None,
|
166
|
-
) -> None:
|
167
|
-
"""Create a compressed ``tar.gz`` file during the run & upload it to Schemathesis.io when the run is finished."""
|
168
|
-
payload = BytesIO()
|
169
|
-
try:
|
170
|
-
with tarfile.open(mode="w:gz", fileobj=payload) as tar:
|
171
|
-
writer = ReportWriter(tar)
|
172
|
-
ci_environment = ci.environment()
|
173
|
-
writer.add_metadata(
|
174
|
-
api_name=api_name,
|
175
|
-
location=location,
|
176
|
-
base_url=base_url,
|
177
|
-
started_at=started_at,
|
178
|
-
metadata=Metadata(),
|
179
|
-
ci_environment=ci_environment,
|
180
|
-
usage_data=usage_data,
|
181
|
-
)
|
182
|
-
if consume_events(writer, in_queue) == ConsumeResult.INTERRUPT:
|
183
|
-
return
|
184
|
-
data = payload.getvalue()
|
185
|
-
out_queue.put(events.Metadata(size=len(data), ci_environment=ci_environment))
|
186
|
-
provider = ci_environment.provider if ci_environment is not None else None
|
187
|
-
response = client.upload_report(data, host_data.correlation_id, provider)
|
188
|
-
event: events.Event
|
189
|
-
if isinstance(response, UploadResponse):
|
190
|
-
host_data.store_correlation_id(response.correlation_id)
|
191
|
-
event = events.Completed(message=response.message, next_url=response.next_url)
|
192
|
-
else:
|
193
|
-
event = events.Failed(detail=response.detail)
|
194
|
-
out_queue.put(event)
|
195
|
-
except Exception as exc:
|
196
|
-
out_queue.put(events.Error(exc))
|
197
|
-
|
198
|
-
|
199
|
-
@dataclass
|
200
|
-
class FileReportHandler(BaseReportHandler):
|
201
|
-
file_handle: click.utils.LazyFile
|
202
|
-
api_name: str | None
|
203
|
-
location: str
|
204
|
-
base_url: str | None
|
205
|
-
started_at: str
|
206
|
-
telemetry: bool
|
207
|
-
out_queue: Queue
|
208
|
-
in_queue: Queue = field(default_factory=Queue)
|
209
|
-
worker: threading.Thread = field(init=False)
|
210
|
-
|
211
|
-
def __post_init__(self) -> None:
|
212
|
-
self.worker = threading.Thread(
|
213
|
-
target=write_file,
|
214
|
-
kwargs={
|
215
|
-
"file_handle": self.file_handle,
|
216
|
-
"api_name": self.api_name,
|
217
|
-
"location": self.location,
|
218
|
-
"base_url": self.base_url,
|
219
|
-
"started_at": self.started_at,
|
220
|
-
"in_queue": self.in_queue,
|
221
|
-
"out_queue": self.out_queue,
|
222
|
-
"usage_data": usage.collect() if self.telemetry else None,
|
223
|
-
},
|
224
|
-
)
|
225
|
-
self.worker.start()
|
226
|
-
|
227
|
-
|
228
|
-
def write_file(
|
229
|
-
file_handle: click.utils.LazyFile,
|
230
|
-
api_name: str | None,
|
231
|
-
location: str,
|
232
|
-
base_url: str | None,
|
233
|
-
started_at: str,
|
234
|
-
in_queue: Queue,
|
235
|
-
out_queue: Queue,
|
236
|
-
usage_data: dict[str, Any] | None,
|
237
|
-
) -> None:
|
238
|
-
with file_handle.open() as fileobj, tarfile.open(mode="w:gz", fileobj=fileobj) as tar:
|
239
|
-
writer = ReportWriter(tar)
|
240
|
-
ci_environment = ci.environment()
|
241
|
-
writer.add_metadata(
|
242
|
-
api_name=api_name,
|
243
|
-
location=location,
|
244
|
-
base_url=base_url,
|
245
|
-
started_at=started_at,
|
246
|
-
metadata=Metadata(),
|
247
|
-
ci_environment=ci_environment,
|
248
|
-
usage_data=usage_data,
|
249
|
-
)
|
250
|
-
result = consume_events(writer, in_queue)
|
251
|
-
if result == ConsumeResult.INTERRUPT:
|
252
|
-
with suppress(OSError):
|
253
|
-
os.remove(file_handle.name)
|
254
|
-
else:
|
255
|
-
out_queue.put(events.Metadata(size=os.path.getsize(file_handle.name), ci_environment=ci_environment))
|