schemathesis 3.25.5__py3-none-any.whl → 3.39.7__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 +6 -6
- schemathesis/_compat.py +2 -2
- schemathesis/_dependency_versions.py +4 -2
- schemathesis/_hypothesis.py +369 -56
- schemathesis/_lazy_import.py +1 -0
- schemathesis/_override.py +5 -4
- schemathesis/_patches.py +21 -0
- schemathesis/_rate_limiter.py +7 -0
- schemathesis/_xml.py +75 -22
- schemathesis/auths.py +78 -16
- schemathesis/checks.py +21 -9
- schemathesis/cli/__init__.py +793 -448
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/callbacks.py +58 -13
- schemathesis/cli/cassettes.py +233 -47
- schemathesis/cli/constants.py +8 -2
- schemathesis/cli/context.py +24 -4
- schemathesis/cli/debug.py +2 -1
- schemathesis/cli/handlers.py +4 -1
- schemathesis/cli/junitxml.py +103 -22
- schemathesis/cli/options.py +15 -4
- schemathesis/cli/output/default.py +286 -115
- schemathesis/cli/output/short.py +25 -6
- schemathesis/cli/reporting.py +79 -0
- schemathesis/cli/sanitization.py +6 -0
- schemathesis/code_samples.py +5 -3
- schemathesis/constants.py +1 -0
- schemathesis/contrib/openapi/__init__.py +1 -1
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
- schemathesis/contrib/openapi/formats/uuid.py +2 -1
- schemathesis/contrib/unique_data.py +3 -3
- schemathesis/exceptions.py +76 -65
- schemathesis/experimental/__init__.py +35 -0
- schemathesis/extra/_aiohttp.py +1 -0
- schemathesis/extra/_flask.py +4 -1
- schemathesis/extra/_server.py +1 -0
- schemathesis/extra/pytest_plugin.py +17 -25
- schemathesis/failures.py +77 -9
- schemathesis/filters.py +185 -8
- schemathesis/fixups/__init__.py +1 -0
- schemathesis/fixups/fast_api.py +2 -2
- schemathesis/fixups/utf8_bom.py +1 -2
- schemathesis/generation/__init__.py +20 -36
- schemathesis/generation/_hypothesis.py +59 -0
- schemathesis/generation/_methods.py +44 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/graphql.py +0 -1
- schemathesis/hooks.py +89 -12
- schemathesis/internal/checks.py +84 -0
- schemathesis/internal/copy.py +22 -3
- schemathesis/internal/deprecation.py +6 -2
- schemathesis/internal/diff.py +15 -0
- schemathesis/internal/extensions.py +27 -0
- schemathesis/internal/jsonschema.py +2 -1
- schemathesis/internal/output.py +68 -0
- schemathesis/internal/result.py +1 -1
- schemathesis/internal/transformation.py +11 -0
- schemathesis/lazy.py +138 -25
- schemathesis/loaders.py +7 -5
- schemathesis/models.py +323 -213
- schemathesis/parameters.py +4 -0
- schemathesis/runner/__init__.py +72 -22
- schemathesis/runner/events.py +86 -6
- schemathesis/runner/impl/context.py +104 -0
- schemathesis/runner/impl/core.py +447 -187
- schemathesis/runner/impl/solo.py +19 -29
- schemathesis/runner/impl/threadpool.py +70 -79
- schemathesis/{cli → runner}/probes.py +37 -25
- schemathesis/runner/serialization.py +150 -17
- schemathesis/sanitization.py +5 -1
- schemathesis/schemas.py +170 -102
- schemathesis/serializers.py +17 -4
- schemathesis/service/ci.py +1 -0
- schemathesis/service/client.py +39 -6
- schemathesis/service/events.py +5 -1
- schemathesis/service/extensions.py +224 -0
- schemathesis/service/hosts.py +6 -2
- schemathesis/service/metadata.py +25 -0
- schemathesis/service/models.py +211 -2
- schemathesis/service/report.py +6 -6
- schemathesis/service/serialization.py +60 -71
- schemathesis/service/usage.py +1 -0
- schemathesis/specs/graphql/_cache.py +26 -0
- schemathesis/specs/graphql/loaders.py +25 -5
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +2 -2
- schemathesis/specs/graphql/schemas.py +130 -100
- schemathesis/specs/graphql/validation.py +1 -2
- schemathesis/specs/openapi/__init__.py +1 -0
- schemathesis/specs/openapi/_cache.py +123 -0
- schemathesis/specs/openapi/_hypothesis.py +79 -61
- schemathesis/specs/openapi/checks.py +504 -25
- schemathesis/specs/openapi/converter.py +31 -4
- schemathesis/specs/openapi/definitions.py +10 -17
- schemathesis/specs/openapi/examples.py +143 -31
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +1 -1
- schemathesis/specs/openapi/expressions/extractors.py +26 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +29 -6
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +44 -0
- schemathesis/specs/openapi/links.py +125 -42
- schemathesis/specs/openapi/loaders.py +77 -36
- schemathesis/specs/openapi/media_types.py +34 -0
- schemathesis/specs/openapi/negative/__init__.py +6 -3
- schemathesis/specs/openapi/negative/mutations.py +21 -6
- schemathesis/specs/openapi/parameters.py +39 -25
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +37 -7
- schemathesis/specs/openapi/schemas.py +368 -242
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +1 -0
- schemathesis/specs/openapi/stateful/__init__.py +198 -70
- schemathesis/specs/openapi/stateful/statistic.py +198 -0
- schemathesis/specs/openapi/stateful/types.py +14 -0
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/specs/openapi/validation.py +1 -0
- schemathesis/stateful/__init__.py +35 -21
- schemathesis/stateful/config.py +97 -0
- schemathesis/stateful/context.py +135 -0
- schemathesis/stateful/events.py +274 -0
- schemathesis/stateful/runner.py +309 -0
- schemathesis/stateful/sink.py +68 -0
- schemathesis/stateful/state_machine.py +67 -38
- schemathesis/stateful/statistic.py +22 -0
- schemathesis/stateful/validation.py +100 -0
- schemathesis/targets.py +33 -1
- schemathesis/throttling.py +25 -5
- schemathesis/transports/__init__.py +354 -0
- schemathesis/transports/asgi.py +7 -0
- schemathesis/transports/auth.py +25 -2
- schemathesis/transports/content_types.py +3 -1
- schemathesis/transports/headers.py +2 -1
- schemathesis/transports/responses.py +9 -4
- schemathesis/types.py +9 -0
- schemathesis/utils.py +11 -16
- schemathesis-3.39.7.dist-info/METADATA +293 -0
- schemathesis-3.39.7.dist-info/RECORD +160 -0
- {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis-3.25.5.dist-info/METADATA +0 -356
- schemathesis-3.25.5.dist-info/RECORD +0 -134
- {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
schemathesis/parameters.py
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
These are basic entities that describe what data could be sent to the API.
|
|
4
4
|
"""
|
|
5
|
+
|
|
5
6
|
from __future__ import annotations
|
|
7
|
+
|
|
6
8
|
from dataclasses import dataclass, field
|
|
7
9
|
from typing import TYPE_CHECKING, Any, Generator, Generic, TypeVar
|
|
8
10
|
|
|
@@ -53,6 +55,8 @@ class ParameterSet(Generic[P]):
|
|
|
53
55
|
|
|
54
56
|
items: list[P] = field(default_factory=list)
|
|
55
57
|
|
|
58
|
+
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
|
59
|
+
|
|
56
60
|
def add(self, parameter: P) -> None:
|
|
57
61
|
"""Add a new parameter."""
|
|
58
62
|
self.items.append(parameter)
|
schemathesis/runner/__init__.py
CHANGED
|
@@ -1,34 +1,39 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from random import Random
|
|
4
|
-
from typing import Any, Callable, Generator, Iterable
|
|
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
|
-
from ..generation import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethod, GenerationConfig
|
|
9
7
|
from ..constants import (
|
|
10
8
|
DEFAULT_DEADLINE,
|
|
11
9
|
DEFAULT_STATEFUL_RECURSION_LIMIT,
|
|
12
10
|
HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER,
|
|
13
11
|
)
|
|
14
|
-
from ..
|
|
12
|
+
from ..exceptions import SchemaError
|
|
13
|
+
from ..generation import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethod, GenerationConfig
|
|
14
|
+
from ..internal.checks import CheckConfig
|
|
15
15
|
from ..internal.datetime import current_datetime
|
|
16
|
+
from ..internal.deprecation import deprecated_function
|
|
16
17
|
from ..internal.validation import file_exists
|
|
17
|
-
from ..transports.auth import get_requests_auth
|
|
18
|
-
from ..exceptions import SchemaError
|
|
19
18
|
from ..loaders import load_app
|
|
20
19
|
from ..specs.graphql import loaders as gql_loaders
|
|
21
20
|
from ..specs.openapi import loaders as oas_loaders
|
|
22
21
|
from ..targets import DEFAULT_TARGETS, Target
|
|
22
|
+
from ..transports import RequestConfig
|
|
23
|
+
from ..transports.auth import get_requests_auth
|
|
23
24
|
from ..types import Filter, NotSet, RawAuth, RequestCert
|
|
25
|
+
from . import events
|
|
26
|
+
from .probes import ProbeConfig
|
|
24
27
|
|
|
25
28
|
if TYPE_CHECKING:
|
|
26
|
-
|
|
29
|
+
import hypothesis
|
|
30
|
+
|
|
31
|
+
from .._override import CaseOverride
|
|
27
32
|
from ..models import CheckFunction
|
|
28
33
|
from ..schemas import BaseSchema
|
|
29
|
-
from .
|
|
34
|
+
from ..service.client import ServiceClient
|
|
30
35
|
from ..stateful import Stateful
|
|
31
|
-
import
|
|
36
|
+
from .impl import BaseRunner
|
|
32
37
|
|
|
33
38
|
|
|
34
39
|
@deprecated_function(removed_in="4.0", replacement="schemathesis.runner.from_schema")
|
|
@@ -75,6 +80,8 @@ def prepare(
|
|
|
75
80
|
hypothesis_report_multiple_bugs: bool | None = None,
|
|
76
81
|
hypothesis_suppress_health_check: list[hypothesis.HealthCheck] | None = None,
|
|
77
82
|
hypothesis_verbosity: hypothesis.Verbosity | None = None,
|
|
83
|
+
probe_config: ProbeConfig | None = None,
|
|
84
|
+
service_client: ServiceClient | None = None,
|
|
78
85
|
) -> Generator[events.ExecutionEvent, None, None]:
|
|
79
86
|
"""Prepare a generator that will run test cases against the given API definition."""
|
|
80
87
|
from ..checks import DEFAULT_CHECKS
|
|
@@ -128,6 +135,8 @@ def prepare(
|
|
|
128
135
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
129
136
|
count_operations=count_operations,
|
|
130
137
|
count_links=count_links,
|
|
138
|
+
probe_config=probe_config,
|
|
139
|
+
service_client=service_client,
|
|
131
140
|
)
|
|
132
141
|
|
|
133
142
|
|
|
@@ -188,6 +197,8 @@ def execute_from_schema(
|
|
|
188
197
|
stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT,
|
|
189
198
|
count_operations: bool = True,
|
|
190
199
|
count_links: bool = True,
|
|
200
|
+
probe_config: ProbeConfig | None = None,
|
|
201
|
+
service_client: ServiceClient | None,
|
|
191
202
|
) -> Generator[events.ExecutionEvent, None, None]:
|
|
192
203
|
"""Execute tests for the given schema.
|
|
193
204
|
|
|
@@ -237,6 +248,8 @@ def execute_from_schema(
|
|
|
237
248
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
238
249
|
count_operations=count_operations,
|
|
239
250
|
count_links=count_links,
|
|
251
|
+
probe_config=probe_config,
|
|
252
|
+
service_client=service_client,
|
|
240
253
|
).execute()
|
|
241
254
|
except SchemaError as error:
|
|
242
255
|
yield events.InternalError.from_schema_error(error)
|
|
@@ -267,7 +280,7 @@ def load_schema(
|
|
|
267
280
|
operation_id: Filter | None = None,
|
|
268
281
|
) -> BaseSchema:
|
|
269
282
|
"""Load schema via specified loader and parameters."""
|
|
270
|
-
loader_options = {
|
|
283
|
+
loader_options: dict[str, Any] = {
|
|
271
284
|
key: value
|
|
272
285
|
for key, value in (
|
|
273
286
|
("base_url", base_url),
|
|
@@ -333,18 +346,24 @@ def from_schema(
|
|
|
333
346
|
request_cert: RequestCert | None = None,
|
|
334
347
|
seed: int | None = None,
|
|
335
348
|
exit_first: bool = False,
|
|
349
|
+
no_failfast: bool = False,
|
|
336
350
|
max_failures: int | None = None,
|
|
337
351
|
started_at: str | None = None,
|
|
352
|
+
unique_data: bool = False,
|
|
338
353
|
dry_run: bool = False,
|
|
339
354
|
store_interactions: bool = False,
|
|
340
355
|
stateful: Stateful | None = None,
|
|
341
356
|
stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT,
|
|
342
357
|
count_operations: bool = True,
|
|
343
358
|
count_links: bool = True,
|
|
359
|
+
probe_config: ProbeConfig | None = None,
|
|
360
|
+
checks_config: CheckConfig | None = None,
|
|
361
|
+
service_client: ServiceClient | None = None,
|
|
344
362
|
) -> BaseRunner:
|
|
345
|
-
from starlette.applications import Starlette
|
|
346
363
|
import hypothesis
|
|
364
|
+
|
|
347
365
|
from ..checks import DEFAULT_CHECKS
|
|
366
|
+
from ..transports.asgi import is_asgi_app
|
|
348
367
|
from .impl import (
|
|
349
368
|
SingleThreadASGIRunner,
|
|
350
369
|
SingleThreadRunner,
|
|
@@ -355,9 +374,16 @@ def from_schema(
|
|
|
355
374
|
)
|
|
356
375
|
|
|
357
376
|
checks = checks or DEFAULT_CHECKS
|
|
377
|
+
checks_config = checks_config or CheckConfig()
|
|
378
|
+
probe_config = probe_config or ProbeConfig()
|
|
358
379
|
|
|
359
380
|
hypothesis_settings = hypothesis_settings or hypothesis.settings(deadline=DEFAULT_DEADLINE)
|
|
360
|
-
|
|
381
|
+
request_config = RequestConfig(
|
|
382
|
+
timeout=request_timeout,
|
|
383
|
+
tls_verify=request_tls_verify,
|
|
384
|
+
proxy=request_proxy,
|
|
385
|
+
cert=request_cert,
|
|
386
|
+
)
|
|
361
387
|
|
|
362
388
|
# Use the same seed for all tests unless `derandomize=True` is used
|
|
363
389
|
if seed is None and not hypothesis_settings.derandomize:
|
|
@@ -379,21 +405,23 @@ def from_schema(
|
|
|
379
405
|
headers=headers,
|
|
380
406
|
seed=seed,
|
|
381
407
|
workers_num=workers_num,
|
|
382
|
-
|
|
383
|
-
request_tls_verify=request_tls_verify,
|
|
384
|
-
request_proxy=request_proxy,
|
|
385
|
-
request_cert=request_cert,
|
|
408
|
+
request_config=request_config,
|
|
386
409
|
exit_first=exit_first,
|
|
410
|
+
no_failfast=no_failfast,
|
|
387
411
|
max_failures=max_failures,
|
|
388
412
|
started_at=started_at,
|
|
413
|
+
unique_data=unique_data,
|
|
389
414
|
dry_run=dry_run,
|
|
390
415
|
store_interactions=store_interactions,
|
|
391
416
|
stateful=stateful,
|
|
392
417
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
393
418
|
count_operations=count_operations,
|
|
394
419
|
count_links=count_links,
|
|
420
|
+
probe_config=probe_config,
|
|
421
|
+
checks_config=checks_config,
|
|
422
|
+
service_client=service_client,
|
|
395
423
|
)
|
|
396
|
-
if
|
|
424
|
+
if is_asgi_app(schema.app):
|
|
397
425
|
return ThreadPoolASGIRunner(
|
|
398
426
|
schema=schema,
|
|
399
427
|
checks=checks,
|
|
@@ -407,14 +435,19 @@ def from_schema(
|
|
|
407
435
|
headers=headers,
|
|
408
436
|
seed=seed,
|
|
409
437
|
exit_first=exit_first,
|
|
438
|
+
no_failfast=no_failfast,
|
|
410
439
|
max_failures=max_failures,
|
|
411
440
|
started_at=started_at,
|
|
441
|
+
unique_data=unique_data,
|
|
412
442
|
dry_run=dry_run,
|
|
413
443
|
store_interactions=store_interactions,
|
|
414
444
|
stateful=stateful,
|
|
415
445
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
416
446
|
count_operations=count_operations,
|
|
417
447
|
count_links=count_links,
|
|
448
|
+
probe_config=probe_config,
|
|
449
|
+
checks_config=checks_config,
|
|
450
|
+
service_client=service_client,
|
|
418
451
|
)
|
|
419
452
|
return ThreadPoolWSGIRunner(
|
|
420
453
|
schema=schema,
|
|
@@ -430,14 +463,19 @@ def from_schema(
|
|
|
430
463
|
seed=seed,
|
|
431
464
|
workers_num=workers_num,
|
|
432
465
|
exit_first=exit_first,
|
|
466
|
+
no_failfast=no_failfast,
|
|
433
467
|
max_failures=max_failures,
|
|
434
468
|
started_at=started_at,
|
|
469
|
+
unique_data=unique_data,
|
|
435
470
|
dry_run=dry_run,
|
|
436
471
|
store_interactions=store_interactions,
|
|
437
472
|
stateful=stateful,
|
|
438
473
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
439
474
|
count_operations=count_operations,
|
|
440
475
|
count_links=count_links,
|
|
476
|
+
probe_config=probe_config,
|
|
477
|
+
checks_config=checks_config,
|
|
478
|
+
service_client=service_client,
|
|
441
479
|
)
|
|
442
480
|
if not schema.app:
|
|
443
481
|
return SingleThreadRunner(
|
|
@@ -452,21 +490,23 @@ def from_schema(
|
|
|
452
490
|
override=override,
|
|
453
491
|
headers=headers,
|
|
454
492
|
seed=seed,
|
|
455
|
-
|
|
456
|
-
request_tls_verify=request_tls_verify,
|
|
457
|
-
request_proxy=request_proxy,
|
|
458
|
-
request_cert=request_cert,
|
|
493
|
+
request_config=request_config,
|
|
459
494
|
exit_first=exit_first,
|
|
495
|
+
no_failfast=no_failfast,
|
|
460
496
|
max_failures=max_failures,
|
|
461
497
|
started_at=started_at,
|
|
498
|
+
unique_data=unique_data,
|
|
462
499
|
dry_run=dry_run,
|
|
463
500
|
store_interactions=store_interactions,
|
|
464
501
|
stateful=stateful,
|
|
465
502
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
466
503
|
count_operations=count_operations,
|
|
467
504
|
count_links=count_links,
|
|
505
|
+
probe_config=probe_config,
|
|
506
|
+
checks_config=checks_config,
|
|
507
|
+
service_client=service_client,
|
|
468
508
|
)
|
|
469
|
-
if
|
|
509
|
+
if is_asgi_app(schema.app):
|
|
470
510
|
return SingleThreadASGIRunner(
|
|
471
511
|
schema=schema,
|
|
472
512
|
checks=checks,
|
|
@@ -480,14 +520,19 @@ def from_schema(
|
|
|
480
520
|
headers=headers,
|
|
481
521
|
seed=seed,
|
|
482
522
|
exit_first=exit_first,
|
|
523
|
+
no_failfast=no_failfast,
|
|
483
524
|
max_failures=max_failures,
|
|
484
525
|
started_at=started_at,
|
|
526
|
+
unique_data=unique_data,
|
|
485
527
|
dry_run=dry_run,
|
|
486
528
|
store_interactions=store_interactions,
|
|
487
529
|
stateful=stateful,
|
|
488
530
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
489
531
|
count_operations=count_operations,
|
|
490
532
|
count_links=count_links,
|
|
533
|
+
probe_config=probe_config,
|
|
534
|
+
checks_config=checks_config,
|
|
535
|
+
service_client=service_client,
|
|
491
536
|
)
|
|
492
537
|
return SingleThreadWSGIRunner(
|
|
493
538
|
schema=schema,
|
|
@@ -502,14 +547,19 @@ def from_schema(
|
|
|
502
547
|
headers=headers,
|
|
503
548
|
seed=seed,
|
|
504
549
|
exit_first=exit_first,
|
|
550
|
+
no_failfast=no_failfast,
|
|
505
551
|
max_failures=max_failures,
|
|
506
552
|
started_at=started_at,
|
|
553
|
+
unique_data=unique_data,
|
|
507
554
|
dry_run=dry_run,
|
|
508
555
|
store_interactions=store_interactions,
|
|
509
556
|
stateful=stateful,
|
|
510
557
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
511
558
|
count_operations=count_operations,
|
|
512
559
|
count_links=count_links,
|
|
560
|
+
probe_config=probe_config,
|
|
561
|
+
checks_config=checks_config,
|
|
562
|
+
service_client=service_client,
|
|
513
563
|
)
|
|
514
564
|
|
|
515
565
|
|
schemathesis/runner/events.py
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import enum
|
|
3
4
|
import threading
|
|
4
5
|
import time
|
|
5
6
|
from dataclasses import asdict, dataclass, field
|
|
6
|
-
from typing import
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
7
8
|
|
|
9
|
+
from ..exceptions import RuntimeErrorType, SchemaError, SchemaErrorType, format_exception
|
|
8
10
|
from ..internal.datetime import current_datetime
|
|
9
|
-
from ..
|
|
10
|
-
from ..exceptions import SchemaError, SchemaErrorType, format_exception, RuntimeErrorType
|
|
11
|
+
from ..internal.result import Err, Ok, Result
|
|
11
12
|
from .serialization import SerializedError, SerializedTestResult
|
|
12
13
|
|
|
13
|
-
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
|
+
from ..generation import DataGenerationMethod
|
|
15
16
|
from ..models import APIOperation, Status, TestResult, TestResultSet
|
|
16
|
-
from ..schemas import BaseSchema
|
|
17
|
+
from ..schemas import BaseSchema, Specification
|
|
18
|
+
from ..service.models import AnalysisResult
|
|
19
|
+
from ..stateful import events
|
|
20
|
+
from . import probes
|
|
17
21
|
|
|
18
22
|
|
|
19
23
|
@dataclass
|
|
@@ -35,6 +39,7 @@ class Initialized(ExecutionEvent):
|
|
|
35
39
|
"""Runner is initialized, settings are prepared, requests session is ready."""
|
|
36
40
|
|
|
37
41
|
schema: dict[str, Any]
|
|
42
|
+
specification: Specification
|
|
38
43
|
# Total number of operations in the schema
|
|
39
44
|
operations_count: int | None
|
|
40
45
|
# Total number of links in the schema
|
|
@@ -44,6 +49,8 @@ class Initialized(ExecutionEvent):
|
|
|
44
49
|
seed: int | None
|
|
45
50
|
# The base URL against which the tests are running
|
|
46
51
|
base_url: str
|
|
52
|
+
# The base path part of every operation
|
|
53
|
+
base_path: str
|
|
47
54
|
# API schema specification name
|
|
48
55
|
specification_name: str
|
|
49
56
|
# Monotonic clock value when the test run started. Used to properly calculate run duration, since this clock
|
|
@@ -60,22 +67,69 @@ class Initialized(ExecutionEvent):
|
|
|
60
67
|
schema: BaseSchema,
|
|
61
68
|
count_operations: bool = True,
|
|
62
69
|
count_links: bool = True,
|
|
70
|
+
start_time: float | None = None,
|
|
63
71
|
started_at: str | None = None,
|
|
64
72
|
seed: int | None,
|
|
65
73
|
) -> Initialized:
|
|
66
74
|
"""Computes all needed data from a schema instance."""
|
|
67
75
|
return cls(
|
|
68
76
|
schema=schema.raw_schema,
|
|
77
|
+
specification=schema.specification,
|
|
69
78
|
operations_count=schema.operations_count if count_operations else None,
|
|
70
79
|
links_count=schema.links_count if count_links else None,
|
|
71
80
|
location=schema.location,
|
|
72
81
|
base_url=schema.get_base_url(),
|
|
82
|
+
base_path=schema.base_path,
|
|
83
|
+
start_time=start_time or time.monotonic(),
|
|
73
84
|
started_at=started_at or current_datetime(),
|
|
74
85
|
specification_name=schema.verbose_name,
|
|
75
86
|
seed=seed,
|
|
76
87
|
)
|
|
77
88
|
|
|
78
89
|
|
|
90
|
+
@dataclass
|
|
91
|
+
class BeforeProbing(ExecutionEvent):
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class AfterProbing(ExecutionEvent):
|
|
97
|
+
probes: list[probes.ProbeRun] | None
|
|
98
|
+
|
|
99
|
+
def asdict(self, **kwargs: Any) -> dict[str, Any]:
|
|
100
|
+
probes = self.probes or []
|
|
101
|
+
return {"probes": [probe.serialize() for probe in probes], "events_type": self.__class__.__name__}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class BeforeAnalysis(ExecutionEvent):
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class AfterAnalysis(ExecutionEvent):
|
|
111
|
+
analysis: Result[AnalysisResult, Exception] | None
|
|
112
|
+
|
|
113
|
+
def _serialize(self) -> dict[str, Any]:
|
|
114
|
+
from ..service.models import AnalysisSuccess
|
|
115
|
+
|
|
116
|
+
data = {}
|
|
117
|
+
if isinstance(self.analysis, Ok):
|
|
118
|
+
result = self.analysis.ok()
|
|
119
|
+
if isinstance(result, AnalysisSuccess):
|
|
120
|
+
data["analysis_id"] = result.id
|
|
121
|
+
else:
|
|
122
|
+
data["error"] = result.message
|
|
123
|
+
elif isinstance(self.analysis, Err):
|
|
124
|
+
data["error"] = format_exception(self.analysis.err())
|
|
125
|
+
return data
|
|
126
|
+
|
|
127
|
+
def asdict(self, **kwargs: Any) -> dict[str, Any]:
|
|
128
|
+
data = self._serialize()
|
|
129
|
+
data["event_type"] = self.__class__.__name__
|
|
130
|
+
return data
|
|
131
|
+
|
|
132
|
+
|
|
79
133
|
class CurrentOperationMixin:
|
|
80
134
|
method: str
|
|
81
135
|
path: str
|
|
@@ -188,6 +242,9 @@ class InternalErrorType(str, enum.Enum):
|
|
|
188
242
|
OTHER = "other"
|
|
189
243
|
|
|
190
244
|
|
|
245
|
+
DEFAULT_INTERNAL_ERROR_MESSAGE = "An internal error occurred during the test run"
|
|
246
|
+
|
|
247
|
+
|
|
191
248
|
@dataclass
|
|
192
249
|
class InternalError(ExecutionEvent):
|
|
193
250
|
"""An error that happened inside the runner."""
|
|
@@ -226,7 +283,7 @@ class InternalError(ExecutionEvent):
|
|
|
226
283
|
type_=InternalErrorType.OTHER,
|
|
227
284
|
subtype=None,
|
|
228
285
|
title="Test Execution Error",
|
|
229
|
-
message=
|
|
286
|
+
message=DEFAULT_INTERNAL_ERROR_MESSAGE,
|
|
230
287
|
extras=[],
|
|
231
288
|
)
|
|
232
289
|
|
|
@@ -255,6 +312,29 @@ class InternalError(ExecutionEvent):
|
|
|
255
312
|
)
|
|
256
313
|
|
|
257
314
|
|
|
315
|
+
@dataclass
|
|
316
|
+
class StatefulEvent(ExecutionEvent):
|
|
317
|
+
"""Represents an event originating from the state machine runner."""
|
|
318
|
+
|
|
319
|
+
data: events.StatefulEvent
|
|
320
|
+
|
|
321
|
+
__slots__ = ("data",)
|
|
322
|
+
|
|
323
|
+
def asdict(self, **kwargs: Any) -> dict[str, Any]:
|
|
324
|
+
return {"data": self.data.asdict(**kwargs), "event_type": self.__class__.__name__}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@dataclass
|
|
328
|
+
class AfterStatefulExecution(ExecutionEvent):
|
|
329
|
+
"""Happens after the stateful test run."""
|
|
330
|
+
|
|
331
|
+
status: Status
|
|
332
|
+
result: SerializedTestResult
|
|
333
|
+
elapsed_time: float
|
|
334
|
+
data_generation_method: list[DataGenerationMethod]
|
|
335
|
+
thread_id: int = field(default_factory=threading.get_ident)
|
|
336
|
+
|
|
337
|
+
|
|
258
338
|
@dataclass
|
|
259
339
|
class Finished(ExecutionEvent):
|
|
260
340
|
"""The final event of the run.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from ...constants import NOT_SET
|
|
7
|
+
from ...internal.checks import CheckConfig
|
|
8
|
+
from ...models import TestResult, TestResultSet
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
import threading
|
|
12
|
+
|
|
13
|
+
from ..._override import CaseOverride
|
|
14
|
+
from ...exceptions import OperationSchemaError
|
|
15
|
+
from ...models import Case
|
|
16
|
+
from ...types import NotSet, RawAuth
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class RunnerContext:
|
|
21
|
+
"""Holds context shared for a test run."""
|
|
22
|
+
|
|
23
|
+
data: TestResultSet
|
|
24
|
+
auth: RawAuth | None
|
|
25
|
+
seed: int | None
|
|
26
|
+
stop_event: threading.Event
|
|
27
|
+
unique_data: bool
|
|
28
|
+
outcome_cache: dict[int, BaseException | None]
|
|
29
|
+
checks_config: CheckConfig
|
|
30
|
+
override: CaseOverride | None
|
|
31
|
+
no_failfast: bool
|
|
32
|
+
|
|
33
|
+
__slots__ = (
|
|
34
|
+
"data",
|
|
35
|
+
"auth",
|
|
36
|
+
"seed",
|
|
37
|
+
"stop_event",
|
|
38
|
+
"unique_data",
|
|
39
|
+
"outcome_cache",
|
|
40
|
+
"checks_config",
|
|
41
|
+
"override",
|
|
42
|
+
"no_failfast",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
*,
|
|
48
|
+
seed: int | None,
|
|
49
|
+
auth: RawAuth | None,
|
|
50
|
+
stop_event: threading.Event,
|
|
51
|
+
unique_data: bool,
|
|
52
|
+
checks_config: CheckConfig,
|
|
53
|
+
override: CaseOverride | None,
|
|
54
|
+
no_failfast: bool,
|
|
55
|
+
) -> None:
|
|
56
|
+
self.data = TestResultSet(seed=seed)
|
|
57
|
+
self.auth = auth
|
|
58
|
+
self.seed = seed
|
|
59
|
+
self.stop_event = stop_event
|
|
60
|
+
self.outcome_cache = {}
|
|
61
|
+
self.unique_data = unique_data
|
|
62
|
+
self.checks_config = checks_config
|
|
63
|
+
self.override = override
|
|
64
|
+
self.no_failfast = no_failfast
|
|
65
|
+
|
|
66
|
+
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def is_stopped(self) -> bool:
|
|
70
|
+
return self.stop_event.is_set()
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def has_all_not_found(self) -> bool:
|
|
74
|
+
"""Check if all responses are 404."""
|
|
75
|
+
has_not_found = False
|
|
76
|
+
for entry in self.data.results:
|
|
77
|
+
for check in entry.checks:
|
|
78
|
+
if check.response is not None:
|
|
79
|
+
if check.response.status_code == 404:
|
|
80
|
+
has_not_found = True
|
|
81
|
+
else:
|
|
82
|
+
# There are non-404 responses, no reason to check any other response
|
|
83
|
+
return False
|
|
84
|
+
# Only happens if all responses are 404, or there are no responses at all.
|
|
85
|
+
# In the first case, it returns True, for the latter - False
|
|
86
|
+
return has_not_found
|
|
87
|
+
|
|
88
|
+
def add_result(self, result: TestResult) -> None:
|
|
89
|
+
self.data.append(result)
|
|
90
|
+
|
|
91
|
+
def add_generic_error(self, error: OperationSchemaError) -> None:
|
|
92
|
+
self.data.generic_errors.append(error)
|
|
93
|
+
|
|
94
|
+
def add_warning(self, message: str) -> None:
|
|
95
|
+
self.data.add_warning(message)
|
|
96
|
+
|
|
97
|
+
def cache_outcome(self, case: Case, outcome: BaseException | None) -> None:
|
|
98
|
+
self.outcome_cache[hash(case)] = outcome
|
|
99
|
+
|
|
100
|
+
def get_cached_outcome(self, case: Case) -> BaseException | None | NotSet:
|
|
101
|
+
return self.outcome_cache.get(hash(case), NOT_SET)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
ALL_NOT_FOUND_WARNING_MESSAGE = "All API responses have a 404 status code. Did you specify the proper API location?"
|