schemathesis 3.35.1__py3-none-any.whl → 3.35.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/_hypothesis.py +12 -14
- schemathesis/cli/__init__.py +424 -412
- schemathesis/cli/context.py +7 -3
- schemathesis/cli/output/default.py +13 -2
- schemathesis/generation/coverage.py +57 -14
- schemathesis/models.py +23 -2
- schemathesis/runner/events.py +6 -1
- schemathesis/runner/impl/core.py +1 -2
- schemathesis/runner/serialization.py +10 -2
- schemathesis/schemas.py +3 -9
- schemathesis/specs/graphql/loaders.py +2 -1
- schemathesis/specs/openapi/links.py +11 -2
- schemathesis/specs/openapi/loaders.py +3 -1
- schemathesis/specs/openapi/parameters.py +16 -9
- schemathesis/stateful/context.py +3 -0
- schemathesis/stateful/runner.py +8 -1
- schemathesis/types.py +8 -0
- {schemathesis-3.35.1.dist-info → schemathesis-3.35.3.dist-info}/METADATA +69 -168
- {schemathesis-3.35.1.dist-info → schemathesis-3.35.3.dist-info}/RECORD +22 -22
- {schemathesis-3.35.1.dist-info → schemathesis-3.35.3.dist-info}/WHEEL +0 -0
- {schemathesis-3.35.1.dist-info → schemathesis-3.35.3.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.35.1.dist-info → schemathesis-3.35.3.dist-info}/licenses/LICENSE +0 -0
schemathesis/cli/context.py
CHANGED
|
@@ -4,7 +4,7 @@ import os
|
|
|
4
4
|
import shutil
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
from queue import Queue
|
|
7
|
-
from typing import TYPE_CHECKING
|
|
7
|
+
from typing import TYPE_CHECKING, Generator
|
|
8
8
|
|
|
9
9
|
from ..code_samples import CodeSampleStyle
|
|
10
10
|
from ..internal.deprecation import deprecated_property
|
|
@@ -60,11 +60,15 @@ class ExecutionContext:
|
|
|
60
60
|
analysis: Result[AnalysisResult, Exception] | None = None
|
|
61
61
|
output_config: OutputConfig = field(default_factory=OutputConfig)
|
|
62
62
|
state_machine_sink: StateMachineSink | None = None
|
|
63
|
-
|
|
63
|
+
initialization_lines: list[str | Generator[str, None, None]] = field(default_factory=list)
|
|
64
|
+
summary_lines: list[str | Generator[str, None, None]] = field(default_factory=list)
|
|
64
65
|
|
|
65
66
|
@deprecated_property(removed_in="4.0", replacement="show_trace")
|
|
66
67
|
def show_errors_tracebacks(self) -> bool:
|
|
67
68
|
return self.show_trace
|
|
68
69
|
|
|
69
|
-
def
|
|
70
|
+
def add_initialization_line(self, line: str | Generator[str, None, None]) -> None:
|
|
71
|
+
self.initialization_lines.append(line)
|
|
72
|
+
|
|
73
|
+
def add_summary_line(self, line: str | Generator[str, None, None]) -> None:
|
|
70
74
|
self.summary_lines.append(line)
|
|
@@ -7,6 +7,7 @@ import textwrap
|
|
|
7
7
|
import time
|
|
8
8
|
from importlib import metadata
|
|
9
9
|
from queue import Queue
|
|
10
|
+
from types import GeneratorType
|
|
10
11
|
from typing import TYPE_CHECKING, Any, Generator, Literal, cast
|
|
11
12
|
|
|
12
13
|
import click
|
|
@@ -776,6 +777,8 @@ def handle_initialized(context: ExecutionContext, event: events.Initialized) ->
|
|
|
776
777
|
click.secho(f"Collected API links: {links_count}", bold=True)
|
|
777
778
|
if isinstance(context.report, ServiceReportContext):
|
|
778
779
|
click.secho("Report to Schemathesis.io: ENABLED", bold=True)
|
|
780
|
+
if context.initialization_lines:
|
|
781
|
+
_print_lines(context.initialization_lines)
|
|
779
782
|
|
|
780
783
|
|
|
781
784
|
def handle_before_probing(context: ExecutionContext, event: events.BeforeProbing) -> None:
|
|
@@ -852,12 +855,20 @@ def handle_finished(context: ExecutionContext, event: events.Finished) -> None:
|
|
|
852
855
|
display_statistic(context, event)
|
|
853
856
|
if context.summary_lines:
|
|
854
857
|
click.echo()
|
|
855
|
-
|
|
856
|
-
click.echo(line)
|
|
858
|
+
_print_lines(context.summary_lines)
|
|
857
859
|
click.echo()
|
|
858
860
|
display_summary(event)
|
|
859
861
|
|
|
860
862
|
|
|
863
|
+
def _print_lines(lines: list[str | Generator[str, None, None]]) -> None:
|
|
864
|
+
for entry in lines:
|
|
865
|
+
if isinstance(entry, str):
|
|
866
|
+
click.echo(entry)
|
|
867
|
+
elif isinstance(entry, GeneratorType):
|
|
868
|
+
for line in entry:
|
|
869
|
+
click.echo(line)
|
|
870
|
+
|
|
871
|
+
|
|
861
872
|
def handle_interrupted(context: ExecutionContext, event: events.Interrupted) -> None:
|
|
862
873
|
click.echo()
|
|
863
874
|
_handle_interrupted(context)
|
|
@@ -201,12 +201,12 @@ def cover_schema_iter(ctx: CoverageContext, schema: dict | bool) -> Generator[Ge
|
|
|
201
201
|
seen.add(value)
|
|
202
202
|
elif key == "multipleOf":
|
|
203
203
|
yield from _negative_multiple_of(ctx, schema, value)
|
|
204
|
-
elif key == "minLength" and 0 < value < BUFFER_SIZE
|
|
204
|
+
elif key == "minLength" and 0 < value < BUFFER_SIZE:
|
|
205
205
|
with suppress(InvalidArgument):
|
|
206
206
|
yield NegativeValue(
|
|
207
207
|
ctx.generate_from_schema({**schema, "minLength": value - 1, "maxLength": value - 1})
|
|
208
208
|
)
|
|
209
|
-
elif key == "maxLength" and value < BUFFER_SIZE
|
|
209
|
+
elif key == "maxLength" and value < BUFFER_SIZE:
|
|
210
210
|
with suppress(InvalidArgument):
|
|
211
211
|
yield NegativeValue(
|
|
212
212
|
ctx.generate_from_schema({**schema, "minLength": value + 1, "maxLength": value + 1})
|
|
@@ -234,6 +234,17 @@ def cover_schema_iter(ctx: CoverageContext, schema: dict | bool) -> Generator[Ge
|
|
|
234
234
|
yield from cover_schema_iter(nctx, sub_schema)
|
|
235
235
|
|
|
236
236
|
|
|
237
|
+
def _get_properties(schema: dict | bool) -> dict | bool:
|
|
238
|
+
if isinstance(schema, dict):
|
|
239
|
+
if "example" in schema:
|
|
240
|
+
return {"const": schema["example"]}
|
|
241
|
+
if "examples" in schema and schema["examples"]:
|
|
242
|
+
return {"enum": schema["examples"]}
|
|
243
|
+
if schema.get("type") == "object":
|
|
244
|
+
return _get_template_schema(schema, "object")
|
|
245
|
+
return schema
|
|
246
|
+
|
|
247
|
+
|
|
237
248
|
def _get_template_schema(schema: dict, ty: str) -> dict:
|
|
238
249
|
if ty == "object":
|
|
239
250
|
properties = schema.get("properties")
|
|
@@ -242,10 +253,7 @@ def _get_template_schema(schema: dict, ty: str) -> dict:
|
|
|
242
253
|
**schema,
|
|
243
254
|
"required": list(properties),
|
|
244
255
|
"type": ty,
|
|
245
|
-
"properties": {
|
|
246
|
-
k: _get_template_schema(v, "object") if isinstance(v, dict) and v.get("type") == "object" else v
|
|
247
|
-
for k, v in properties.items()
|
|
248
|
-
},
|
|
256
|
+
"properties": {k: _get_properties(v) for k, v in properties.items()},
|
|
249
257
|
}
|
|
250
258
|
return {**schema, "type": ty}
|
|
251
259
|
|
|
@@ -255,14 +263,21 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
|
255
263
|
# Boundary and near boundary values
|
|
256
264
|
min_length = schema.get("minLength")
|
|
257
265
|
max_length = schema.get("maxLength")
|
|
258
|
-
|
|
259
|
-
|
|
266
|
+
example = schema.get("example")
|
|
267
|
+
examples = schema.get("examples")
|
|
268
|
+
if example or examples:
|
|
269
|
+
if example:
|
|
270
|
+
yield PositiveValue(example)
|
|
271
|
+
if examples:
|
|
272
|
+
for example in examples:
|
|
273
|
+
yield PositiveValue(example)
|
|
274
|
+
elif not min_length and not max_length:
|
|
260
275
|
# Default positive value
|
|
261
276
|
yield PositiveValue(ctx.generate_from_schema(schema))
|
|
262
277
|
|
|
263
278
|
seen = set()
|
|
264
279
|
|
|
265
|
-
if min_length is not None and min_length < BUFFER_SIZE
|
|
280
|
+
if min_length is not None and min_length < BUFFER_SIZE:
|
|
266
281
|
# Exactly the minimum length
|
|
267
282
|
yield PositiveValue(ctx.generate_from_schema({**schema, "maxLength": min_length}))
|
|
268
283
|
seen.add(min_length)
|
|
@@ -273,7 +288,7 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
|
273
288
|
yield PositiveValue(ctx.generate_from_schema({**schema, "minLength": larger, "maxLength": larger}))
|
|
274
289
|
seen.add(larger)
|
|
275
290
|
|
|
276
|
-
if max_length is not None
|
|
291
|
+
if max_length is not None:
|
|
277
292
|
# Exactly the maximum length
|
|
278
293
|
if max_length < BUFFER_SIZE and max_length not in seen:
|
|
279
294
|
yield PositiveValue(ctx.generate_from_schema({**schema, "minLength": max_length}))
|
|
@@ -310,8 +325,16 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
|
310
325
|
if exclusive_maximum is not None:
|
|
311
326
|
maximum = exclusive_maximum - 1
|
|
312
327
|
multiple_of = schema.get("multipleOf")
|
|
313
|
-
|
|
314
|
-
|
|
328
|
+
example = schema.get("example")
|
|
329
|
+
examples = schema.get("examples")
|
|
330
|
+
|
|
331
|
+
if example or examples:
|
|
332
|
+
if example:
|
|
333
|
+
yield PositiveValue(example)
|
|
334
|
+
if examples:
|
|
335
|
+
for example in examples:
|
|
336
|
+
yield PositiveValue(example)
|
|
337
|
+
elif not minimum and not maximum:
|
|
315
338
|
# Default positive value
|
|
316
339
|
yield PositiveValue(ctx.generate_from_schema(schema))
|
|
317
340
|
|
|
@@ -357,7 +380,17 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
|
357
380
|
|
|
358
381
|
def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Generator[GeneratedValue, None, None]:
|
|
359
382
|
seen = set()
|
|
360
|
-
|
|
383
|
+
example = schema.get("example")
|
|
384
|
+
examples = schema.get("examples")
|
|
385
|
+
|
|
386
|
+
if example or examples:
|
|
387
|
+
if example:
|
|
388
|
+
yield PositiveValue(example)
|
|
389
|
+
if examples:
|
|
390
|
+
for example in examples:
|
|
391
|
+
yield PositiveValue(example)
|
|
392
|
+
else:
|
|
393
|
+
yield PositiveValue(template)
|
|
361
394
|
seen.add(len(template))
|
|
362
395
|
|
|
363
396
|
# Boundary and near-boundary sizes
|
|
@@ -390,7 +423,17 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
|
|
|
390
423
|
|
|
391
424
|
|
|
392
425
|
def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Generator[GeneratedValue, None, None]:
|
|
393
|
-
|
|
426
|
+
example = schema.get("example")
|
|
427
|
+
examples = schema.get("examples")
|
|
428
|
+
|
|
429
|
+
if example or examples:
|
|
430
|
+
if example:
|
|
431
|
+
yield PositiveValue(example)
|
|
432
|
+
if examples:
|
|
433
|
+
for example in examples:
|
|
434
|
+
yield PositiveValue(example)
|
|
435
|
+
else:
|
|
436
|
+
yield PositiveValue(template)
|
|
394
437
|
# Only required properties
|
|
395
438
|
properties = schema.get("properties", {})
|
|
396
439
|
if set(properties) != set(schema.get("required", {})):
|
schemathesis/models.py
CHANGED
|
@@ -71,6 +71,14 @@ if TYPE_CHECKING:
|
|
|
71
71
|
from .transports.responses import GenericResponse, WSGIResponse
|
|
72
72
|
|
|
73
73
|
|
|
74
|
+
@dataclass
|
|
75
|
+
class TransitionId:
|
|
76
|
+
name: str
|
|
77
|
+
status_code: str
|
|
78
|
+
|
|
79
|
+
__slots__ = ("name", "status_code")
|
|
80
|
+
|
|
81
|
+
|
|
74
82
|
@dataclass
|
|
75
83
|
class CaseSource:
|
|
76
84
|
"""Data sources, used to generate a test case."""
|
|
@@ -79,6 +87,7 @@ class CaseSource:
|
|
|
79
87
|
response: GenericResponse
|
|
80
88
|
elapsed: float
|
|
81
89
|
overrides_all_parameters: bool
|
|
90
|
+
transition_id: TransitionId
|
|
82
91
|
|
|
83
92
|
def partial_deepcopy(self) -> CaseSource:
|
|
84
93
|
return self.__class__(
|
|
@@ -86,6 +95,7 @@ class CaseSource:
|
|
|
86
95
|
response=self.response,
|
|
87
96
|
elapsed=self.elapsed,
|
|
88
97
|
overrides_all_parameters=self.overrides_all_parameters,
|
|
98
|
+
transition_id=self.transition_id,
|
|
89
99
|
)
|
|
90
100
|
|
|
91
101
|
|
|
@@ -214,9 +224,20 @@ class Case:
|
|
|
214
224
|
def app(self) -> Any:
|
|
215
225
|
return self.operation.app
|
|
216
226
|
|
|
217
|
-
def set_source(
|
|
227
|
+
def set_source(
|
|
228
|
+
self,
|
|
229
|
+
response: GenericResponse,
|
|
230
|
+
case: Case,
|
|
231
|
+
elapsed: float,
|
|
232
|
+
overrides_all_parameters: bool,
|
|
233
|
+
transition_id: TransitionId,
|
|
234
|
+
) -> None:
|
|
218
235
|
self.source = CaseSource(
|
|
219
|
-
case=case,
|
|
236
|
+
case=case,
|
|
237
|
+
response=response,
|
|
238
|
+
elapsed=elapsed,
|
|
239
|
+
overrides_all_parameters=overrides_all_parameters,
|
|
240
|
+
transition_id=transition_id,
|
|
220
241
|
)
|
|
221
242
|
|
|
222
243
|
@property
|
schemathesis/runner/events.py
CHANGED
|
@@ -14,7 +14,7 @@ from .serialization import SerializedError, SerializedTestResult
|
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
16
16
|
from ..models import APIOperation, Status, TestResult, TestResultSet
|
|
17
|
-
from ..schemas import BaseSchema
|
|
17
|
+
from ..schemas import BaseSchema, Specification
|
|
18
18
|
from ..service.models import AnalysisResult
|
|
19
19
|
from ..stateful import events
|
|
20
20
|
from . import probes
|
|
@@ -39,6 +39,7 @@ class Initialized(ExecutionEvent):
|
|
|
39
39
|
"""Runner is initialized, settings are prepared, requests session is ready."""
|
|
40
40
|
|
|
41
41
|
schema: dict[str, Any]
|
|
42
|
+
specification: Specification
|
|
42
43
|
# Total number of operations in the schema
|
|
43
44
|
operations_count: int | None
|
|
44
45
|
# Total number of links in the schema
|
|
@@ -48,6 +49,8 @@ class Initialized(ExecutionEvent):
|
|
|
48
49
|
seed: int | None
|
|
49
50
|
# The base URL against which the tests are running
|
|
50
51
|
base_url: str
|
|
52
|
+
# The base path part of every operation
|
|
53
|
+
base_path: str
|
|
51
54
|
# API schema specification name
|
|
52
55
|
specification_name: str
|
|
53
56
|
# Monotonic clock value when the test run started. Used to properly calculate run duration, since this clock
|
|
@@ -71,10 +74,12 @@ class Initialized(ExecutionEvent):
|
|
|
71
74
|
"""Computes all needed data from a schema instance."""
|
|
72
75
|
return cls(
|
|
73
76
|
schema=schema.raw_schema,
|
|
77
|
+
specification=schema.specification,
|
|
74
78
|
operations_count=schema.operations_count if count_operations else None,
|
|
75
79
|
links_count=schema.links_count if count_links else None,
|
|
76
80
|
location=schema.location,
|
|
77
81
|
base_url=schema.get_base_url(),
|
|
82
|
+
base_path=schema.base_path,
|
|
78
83
|
start_time=start_time or time.monotonic(),
|
|
79
84
|
started_at=started_at or current_datetime(),
|
|
80
85
|
specification_name=schema.verbose_name,
|
schemathesis/runner/impl/core.py
CHANGED
|
@@ -306,13 +306,12 @@ class BaseRunner:
|
|
|
306
306
|
if isinstance(stateful_event, stateful_events.SuiteFinished):
|
|
307
307
|
if stateful_event.failures and status != Status.error:
|
|
308
308
|
status = Status.failure
|
|
309
|
-
for failure in stateful_event.failures:
|
|
310
|
-
result.checks.append(failure)
|
|
311
309
|
elif isinstance(stateful_event, stateful_events.RunStarted):
|
|
312
310
|
test_start_time = stateful_event.timestamp
|
|
313
311
|
elif isinstance(stateful_event, stateful_events.RunFinished):
|
|
314
312
|
test_elapsed_time = stateful_event.timestamp - cast(float, test_start_time)
|
|
315
313
|
elif isinstance(stateful_event, stateful_events.StepFinished):
|
|
314
|
+
result.checks.extend(stateful_event.checks)
|
|
316
315
|
on_step_finished(stateful_event)
|
|
317
316
|
elif isinstance(stateful_event, stateful_events.Errored):
|
|
318
317
|
status = Status.error
|
|
@@ -28,7 +28,7 @@ from ..exceptions import (
|
|
|
28
28
|
make_unique_by_key,
|
|
29
29
|
)
|
|
30
30
|
from ..generation import DataGenerationMethod
|
|
31
|
-
from ..models import Case, Check, Interaction, Request, Response, Status, TestPhase, TestResult
|
|
31
|
+
from ..models import Case, Check, Interaction, Request, Response, Status, TestPhase, TestResult, TransitionId
|
|
32
32
|
from ..transports import deserialize_payload, serialize_payload
|
|
33
33
|
|
|
34
34
|
if TYPE_CHECKING:
|
|
@@ -52,7 +52,9 @@ class SerializedCase:
|
|
|
52
52
|
method: str
|
|
53
53
|
url: str
|
|
54
54
|
path_template: str
|
|
55
|
+
full_path: str
|
|
55
56
|
verbose_name: str
|
|
57
|
+
transition_id: TransitionId | None
|
|
56
58
|
# Transport info
|
|
57
59
|
verify: bool
|
|
58
60
|
# Headers coming from sources outside data generation
|
|
@@ -78,7 +80,9 @@ class SerializedCase:
|
|
|
78
80
|
method=case.method,
|
|
79
81
|
url=request_data.url,
|
|
80
82
|
path_template=case.path,
|
|
83
|
+
full_path=case.full_path,
|
|
81
84
|
verbose_name=case.operation.verbose_name,
|
|
85
|
+
transition_id=case.source.transition_id if case.source is not None else None,
|
|
82
86
|
verify=verify,
|
|
83
87
|
extra_headers=request_data.headers,
|
|
84
88
|
)
|
|
@@ -174,7 +178,11 @@ class SerializedCheck:
|
|
|
174
178
|
|
|
175
179
|
|
|
176
180
|
def _get_headers(headers: dict[str, Any] | CaseInsensitiveDict) -> dict[str, str]:
|
|
177
|
-
return {
|
|
181
|
+
return {
|
|
182
|
+
key: value[0] if isinstance(value, list) else value
|
|
183
|
+
for key, value in headers.items()
|
|
184
|
+
if key not in get_excluded_headers()
|
|
185
|
+
}
|
|
178
186
|
|
|
179
187
|
|
|
180
188
|
@dataclass
|
schemathesis/schemas.py
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
"""Schema objects provide a convenient interface to raw schemas.
|
|
2
|
-
|
|
3
|
-
Their responsibilities:
|
|
4
|
-
- Provide a unified way to work with different types of schemas
|
|
5
|
-
- Give all paths / methods combinations that are available directly from the schema;
|
|
6
|
-
|
|
7
|
-
They give only static definitions of paths.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
1
|
from __future__ import annotations
|
|
11
2
|
|
|
12
3
|
from collections.abc import Mapping
|
|
@@ -69,6 +60,7 @@ from .types import (
|
|
|
69
60
|
NotSet,
|
|
70
61
|
PathParameters,
|
|
71
62
|
Query,
|
|
63
|
+
Specification,
|
|
72
64
|
)
|
|
73
65
|
from .utils import PARAMETRIZE_MARKER, GivenInput, given_proxy
|
|
74
66
|
|
|
@@ -89,6 +81,7 @@ def get_full_path(base_path: str, path: str) -> str:
|
|
|
89
81
|
class BaseSchema(Mapping):
|
|
90
82
|
raw_schema: dict[str, Any]
|
|
91
83
|
transport: Transport
|
|
84
|
+
specification: Specification
|
|
92
85
|
location: str | None = None
|
|
93
86
|
base_url: str | None = None
|
|
94
87
|
filter_set: FilterSet = field(default_factory=FilterSet)
|
|
@@ -408,6 +401,7 @@ class BaseSchema(Mapping):
|
|
|
408
401
|
|
|
409
402
|
return self.__class__(
|
|
410
403
|
self.raw_schema,
|
|
404
|
+
specification=self.specification,
|
|
411
405
|
location=self.location,
|
|
412
406
|
base_url=base_url, # type: ignore
|
|
413
407
|
app=app,
|
|
@@ -21,7 +21,7 @@ from ...internal.validation import require_relative_url
|
|
|
21
21
|
from ...loaders import load_schema_from_url
|
|
22
22
|
from ...throttling import build_limiter
|
|
23
23
|
from ...transports.headers import setup_default_headers
|
|
24
|
-
from ...types import PathLike
|
|
24
|
+
from ...types import PathLike, Specification
|
|
25
25
|
|
|
26
26
|
if TYPE_CHECKING:
|
|
27
27
|
from graphql import DocumentNode
|
|
@@ -254,6 +254,7 @@ def from_dict(
|
|
|
254
254
|
rate_limiter = build_limiter(rate_limit)
|
|
255
255
|
instance = GraphQLSchema(
|
|
256
256
|
raw_schema,
|
|
257
|
+
specification=Specification.GRAPHQL,
|
|
257
258
|
location=location,
|
|
258
259
|
base_url=base_url,
|
|
259
260
|
app=app,
|
|
@@ -14,7 +14,7 @@ from jsonschema import RefResolver
|
|
|
14
14
|
|
|
15
15
|
from ...constants import NOT_SET
|
|
16
16
|
from ...internal.copy import fast_deepcopy
|
|
17
|
-
from ...models import APIOperation, Case
|
|
17
|
+
from ...models import APIOperation, Case, TransitionId
|
|
18
18
|
from ...parameters import ParameterSet
|
|
19
19
|
from ...stateful import ParsedData, StatefulTest, UnresolvableLink
|
|
20
20
|
from ...stateful.state_machine import Direction
|
|
@@ -226,7 +226,16 @@ class OpenAPILink(Direction):
|
|
|
226
226
|
if parameter.name not in overrides.get(parameter.location, []):
|
|
227
227
|
overrides_all_parameters = False
|
|
228
228
|
break
|
|
229
|
-
case.set_source(
|
|
229
|
+
case.set_source(
|
|
230
|
+
context.response,
|
|
231
|
+
context.case,
|
|
232
|
+
elapsed,
|
|
233
|
+
overrides_all_parameters,
|
|
234
|
+
transition_id=TransitionId(
|
|
235
|
+
name=self.name,
|
|
236
|
+
status_code=self.status_code,
|
|
237
|
+
),
|
|
238
|
+
)
|
|
230
239
|
|
|
231
240
|
def set_parameters(
|
|
232
241
|
self, case: Case, context: expressions.ExpressionContext
|
|
@@ -26,7 +26,7 @@ from ...loaders import load_schema_from_url, load_yaml
|
|
|
26
26
|
from ...throttling import build_limiter
|
|
27
27
|
from ...transports.content_types import is_json_media_type, is_yaml_media_type
|
|
28
28
|
from ...transports.headers import setup_default_headers
|
|
29
|
-
from ...types import Filter, NotSet, PathLike
|
|
29
|
+
from ...types import Filter, NotSet, PathLike, Specification
|
|
30
30
|
from . import definitions, validation
|
|
31
31
|
|
|
32
32
|
if TYPE_CHECKING:
|
|
@@ -349,6 +349,7 @@ def from_dict(
|
|
|
349
349
|
_maybe_validate_schema(raw_schema, definitions.SWAGGER_20_VALIDATOR, validate_schema)
|
|
350
350
|
instance = SwaggerV20(
|
|
351
351
|
raw_schema,
|
|
352
|
+
specification=Specification.OPENAPI,
|
|
352
353
|
app=app,
|
|
353
354
|
base_url=base_url,
|
|
354
355
|
filter_set=filter_set,
|
|
@@ -388,6 +389,7 @@ def from_dict(
|
|
|
388
389
|
_maybe_validate_schema(raw_schema, validator, validate_schema)
|
|
389
390
|
instance = OpenApi30(
|
|
390
391
|
raw_schema,
|
|
392
|
+
specification=Specification.OPENAPI,
|
|
391
393
|
app=app,
|
|
392
394
|
base_url=base_url,
|
|
393
395
|
filter_set=filter_set,
|
|
@@ -48,11 +48,20 @@ class OpenAPIParameter(Parameter):
|
|
|
48
48
|
|
|
49
49
|
@property
|
|
50
50
|
def is_header(self) -> bool:
|
|
51
|
-
|
|
51
|
+
return self.location in ("header", "cookie")
|
|
52
52
|
|
|
53
53
|
def as_json_schema(self, operation: APIOperation) -> dict[str, Any]:
|
|
54
54
|
"""Convert parameter's definition to JSON Schema."""
|
|
55
|
+
# JSON Schema allows `examples` as an array
|
|
56
|
+
if self.examples_field in self.definition:
|
|
57
|
+
examples = [
|
|
58
|
+
example["value"] for example in self.definition[self.examples_field].values() if "value" in example
|
|
59
|
+
]
|
|
60
|
+
else:
|
|
61
|
+
examples = None
|
|
55
62
|
schema = self.from_open_api_to_json_schema(operation, self.definition)
|
|
63
|
+
if examples is not None:
|
|
64
|
+
schema["examples"] = examples
|
|
56
65
|
return self.transform_keywords(schema)
|
|
57
66
|
|
|
58
67
|
def transform_keywords(self, schema: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -117,12 +126,10 @@ class OpenAPI20Parameter(OpenAPIParameter):
|
|
|
117
126
|
"uniqueItems",
|
|
118
127
|
"enum",
|
|
119
128
|
"multipleOf",
|
|
129
|
+
"example",
|
|
130
|
+
"examples",
|
|
120
131
|
)
|
|
121
132
|
|
|
122
|
-
@property
|
|
123
|
-
def is_header(self) -> bool:
|
|
124
|
-
return self.location == "header"
|
|
125
|
-
|
|
126
133
|
|
|
127
134
|
@dataclass(eq=False)
|
|
128
135
|
class OpenAPI30Parameter(OpenAPIParameter):
|
|
@@ -163,12 +170,10 @@ class OpenAPI30Parameter(OpenAPIParameter):
|
|
|
163
170
|
"properties",
|
|
164
171
|
"additionalProperties",
|
|
165
172
|
"format",
|
|
173
|
+
"example",
|
|
174
|
+
"examples",
|
|
166
175
|
)
|
|
167
176
|
|
|
168
|
-
@property
|
|
169
|
-
def is_header(self) -> bool:
|
|
170
|
-
return self.location in ("header", "cookie")
|
|
171
|
-
|
|
172
177
|
def from_open_api_to_json_schema(self, operation: APIOperation, open_api_schema: dict[str, Any]) -> dict[str, Any]:
|
|
173
178
|
open_api_schema = get_parameter_schema(operation, open_api_schema)
|
|
174
179
|
return super().from_open_api_to_json_schema(operation, open_api_schema)
|
|
@@ -217,6 +222,8 @@ class OpenAPI20Body(OpenAPIBody, OpenAPI20Parameter):
|
|
|
217
222
|
"allOf",
|
|
218
223
|
"properties",
|
|
219
224
|
"additionalProperties",
|
|
225
|
+
"example",
|
|
226
|
+
"examples",
|
|
220
227
|
)
|
|
221
228
|
# NOTE. For Open API 2.0 bodies, we still give `x-example` precedence over the schema-level `example` field to keep
|
|
222
229
|
# the precedence rules consistent.
|
schemathesis/stateful/context.py
CHANGED
|
@@ -48,6 +48,8 @@ class RunnerContext:
|
|
|
48
48
|
current_response: GenericResponse | None = None
|
|
49
49
|
# Total number of failures
|
|
50
50
|
failures_count: int = 0
|
|
51
|
+
# The total number of completed test scenario
|
|
52
|
+
completed_scenarios: int = 0
|
|
51
53
|
# Metrics collector for targeted testing
|
|
52
54
|
metric_collector: TargetMetricCollector = field(default_factory=lambda: TargetMetricCollector(targets=[]))
|
|
53
55
|
|
|
@@ -64,6 +66,7 @@ class RunnerContext:
|
|
|
64
66
|
return events.ScenarioStatus.REJECTED
|
|
65
67
|
|
|
66
68
|
def reset_scenario(self) -> None:
|
|
69
|
+
self.completed_scenarios += 1
|
|
67
70
|
self.current_step_status = None
|
|
68
71
|
self.current_response = None
|
|
69
72
|
|
schemathesis/stateful/runner.py
CHANGED
|
@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any, Generator, Iterator, Type
|
|
|
9
9
|
import hypothesis
|
|
10
10
|
import requests
|
|
11
11
|
from hypothesis.control import current_build_context
|
|
12
|
-
from hypothesis.errors import Flaky
|
|
12
|
+
from hypothesis.errors import Flaky, Unsatisfiable
|
|
13
13
|
from hypothesis.stateful import Rule
|
|
14
14
|
|
|
15
15
|
from ..exceptions import CheckFailed
|
|
@@ -265,6 +265,13 @@ def _execute_state_machine_loop(
|
|
|
265
265
|
ctx.mark_current_suite_as_seen_in_run()
|
|
266
266
|
continue
|
|
267
267
|
except Exception as exc:
|
|
268
|
+
if isinstance(exc, Unsatisfiable) and ctx.completed_scenarios > 0:
|
|
269
|
+
# Sometimes Hypothesis randomly gives up on generating some complex cases. However, if we know that
|
|
270
|
+
# values are possible to generate based on the previous observations, we retry the generation
|
|
271
|
+
if ctx.completed_scenarios >= config.hypothesis_settings.max_examples:
|
|
272
|
+
# Avoid infinite restarts
|
|
273
|
+
break
|
|
274
|
+
continue
|
|
268
275
|
# Any other exception is an inner error and the test run should be stopped
|
|
269
276
|
suite_status = events.SuiteStatus.ERROR
|
|
270
277
|
event_queue.put(events.Errored(exception=exc))
|
schemathesis/types.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import enum
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Set, Tuple, Union
|
|
3
4
|
|
|
@@ -34,3 +35,10 @@ Hook = Union[
|
|
|
34
35
|
RawAuth = Tuple[str, str]
|
|
35
36
|
# Generic test with any arguments and no return
|
|
36
37
|
GenericTest = Callable[..., None]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Specification(str, enum.Enum):
|
|
41
|
+
"""Specification of the given schema."""
|
|
42
|
+
|
|
43
|
+
OPENAPI = "openapi"
|
|
44
|
+
GRAPHQL = "graphql"
|