schemathesis 4.0.0a10__py3-none-any.whl → 4.0.0a12__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 +29 -30
- schemathesis/auths.py +65 -24
- schemathesis/checks.py +73 -39
- schemathesis/cli/commands/__init__.py +51 -3
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +163 -274
- schemathesis/cli/commands/run/context.py +8 -4
- schemathesis/cli/commands/run/events.py +11 -1
- schemathesis/cli/commands/run/executor.py +70 -78
- schemathesis/cli/commands/run/filters.py +15 -165
- schemathesis/cli/commands/run/handlers/cassettes.py +105 -104
- schemathesis/cli/commands/run/handlers/junitxml.py +5 -4
- schemathesis/cli/commands/run/handlers/output.py +195 -121
- schemathesis/cli/commands/run/loaders.py +35 -50
- schemathesis/cli/commands/run/validation.py +52 -162
- schemathesis/cli/core.py +5 -3
- schemathesis/cli/ext/fs.py +7 -5
- schemathesis/cli/ext/options.py +0 -21
- schemathesis/config/__init__.py +189 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +99 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +156 -0
- schemathesis/config/_generation.py +149 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +327 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +187 -0
- schemathesis/config/_projects.py +523 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +885 -0
- schemathesis/core/__init__.py +2 -0
- schemathesis/core/compat.py +16 -9
- schemathesis/core/errors.py +24 -4
- schemathesis/core/failures.py +6 -7
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/output/__init__.py +14 -37
- schemathesis/core/output/sanitization.py +3 -146
- schemathesis/core/transport.py +36 -1
- schemathesis/core/validation.py +16 -0
- schemathesis/engine/__init__.py +2 -4
- schemathesis/engine/context.py +42 -43
- schemathesis/engine/core.py +7 -5
- schemathesis/engine/errors.py +60 -1
- schemathesis/engine/events.py +10 -2
- schemathesis/engine/phases/__init__.py +10 -0
- schemathesis/engine/phases/probes.py +11 -8
- schemathesis/engine/phases/stateful/__init__.py +2 -1
- schemathesis/engine/phases/stateful/_executor.py +104 -46
- schemathesis/engine/phases/stateful/context.py +2 -2
- schemathesis/engine/phases/unit/__init__.py +23 -15
- schemathesis/engine/phases/unit/_executor.py +110 -21
- schemathesis/engine/phases/unit/_pool.py +1 -1
- schemathesis/errors.py +2 -0
- schemathesis/filters.py +2 -3
- schemathesis/generation/__init__.py +5 -33
- schemathesis/generation/case.py +6 -3
- schemathesis/generation/coverage.py +154 -124
- schemathesis/generation/hypothesis/builder.py +70 -20
- schemathesis/generation/meta.py +3 -3
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +0 -8
- schemathesis/generation/overrides.py +37 -1
- schemathesis/generation/stateful/__init__.py +4 -0
- schemathesis/generation/stateful/state_machine.py +9 -1
- schemathesis/graphql/loaders.py +159 -16
- schemathesis/hooks.py +62 -35
- schemathesis/openapi/checks.py +12 -8
- schemathesis/openapi/generation/filters.py +10 -8
- schemathesis/openapi/loaders.py +142 -17
- schemathesis/pytest/lazy.py +2 -5
- schemathesis/pytest/loaders.py +24 -0
- schemathesis/pytest/plugin.py +33 -2
- schemathesis/schemas.py +21 -66
- schemathesis/specs/graphql/scalars.py +37 -3
- schemathesis/specs/graphql/schemas.py +23 -18
- schemathesis/specs/openapi/_hypothesis.py +26 -28
- schemathesis/specs/openapi/checks.py +37 -36
- schemathesis/specs/openapi/examples.py +4 -3
- schemathesis/specs/openapi/formats.py +32 -5
- schemathesis/specs/openapi/media_types.py +44 -1
- schemathesis/specs/openapi/negative/__init__.py +2 -2
- schemathesis/specs/openapi/patterns.py +46 -16
- schemathesis/specs/openapi/references.py +2 -3
- schemathesis/specs/openapi/schemas.py +19 -22
- schemathesis/specs/openapi/stateful/__init__.py +12 -6
- schemathesis/transport/__init__.py +54 -16
- schemathesis/transport/prepare.py +38 -13
- schemathesis/transport/requests.py +12 -9
- schemathesis/transport/wsgi.py +11 -12
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/METADATA +50 -97
- schemathesis-4.0.0a12.dist-info/RECORD +164 -0
- schemathesis/cli/commands/run/checks.py +0 -79
- schemathesis/cli/commands/run/hypothesis.py +0 -78
- schemathesis/cli/commands/run/reports.py +0 -72
- schemathesis/cli/hooks.py +0 -36
- schemathesis/contrib/__init__.py +0 -9
- schemathesis/contrib/openapi/__init__.py +0 -9
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -20
- schemathesis/engine/config.py +0 -59
- schemathesis/experimental/__init__.py +0 -72
- schemathesis/generation/targets.py +0 -69
- schemathesis-4.0.0a10.dist-info/RECORD +0 -153
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/licenses/LICENSE +0 -0
@@ -6,16 +6,16 @@ import sys
|
|
6
6
|
import threading
|
7
7
|
from dataclasses import dataclass, field
|
8
8
|
from http.cookies import SimpleCookie
|
9
|
+
from pathlib import Path
|
9
10
|
from queue import Queue
|
10
11
|
from typing import IO, Callable, Iterator
|
11
12
|
from urllib.parse import parse_qsl, urlparse
|
12
13
|
|
13
14
|
import harfile
|
14
|
-
from click.utils import LazyFile
|
15
15
|
|
16
16
|
from schemathesis.cli.commands.run.context import ExecutionContext
|
17
17
|
from schemathesis.cli.commands.run.handlers.base import EventHandler
|
18
|
-
from schemathesis.
|
18
|
+
from schemathesis.config import ProjectConfig, ReportFormat, SchemathesisConfig
|
19
19
|
from schemathesis.core.output.sanitization import sanitize_url, sanitize_value
|
20
20
|
from schemathesis.core.transforms import deepclone
|
21
21
|
from schemathesis.core.transport import Response
|
@@ -33,17 +33,15 @@ class CassetteWriter(EventHandler):
|
|
33
33
|
"""Write network interactions to a cassette."""
|
34
34
|
|
35
35
|
format: ReportFormat
|
36
|
-
path:
|
37
|
-
|
38
|
-
preserve_bytes: bool = False
|
36
|
+
path: Path
|
37
|
+
config: ProjectConfig
|
39
38
|
queue: Queue = field(default_factory=Queue)
|
40
39
|
worker: threading.Thread = field(init=False)
|
41
40
|
|
42
41
|
def __post_init__(self) -> None:
|
43
42
|
kwargs = {
|
44
43
|
"path": self.path,
|
45
|
-
"
|
46
|
-
"preserve_bytes": self.preserve_bytes,
|
44
|
+
"config": self.config,
|
47
45
|
"queue": self.queue,
|
48
46
|
}
|
49
47
|
writer: Callable
|
@@ -55,7 +53,7 @@ class CassetteWriter(EventHandler):
|
|
55
53
|
self.worker.start()
|
56
54
|
|
57
55
|
def start(self, ctx: ExecutionContext) -> None:
|
58
|
-
self.queue.put(Initialize(seed=ctx.seed))
|
56
|
+
self.queue.put(Initialize(seed=ctx.config.seed))
|
59
57
|
|
60
58
|
def handle_event(self, ctx: ExecutionContext, event: events.EngineEvent) -> None:
|
61
59
|
if isinstance(event, events.ScenarioFinished):
|
@@ -103,7 +101,7 @@ def get_command_representation() -> str:
|
|
103
101
|
return f"st {args}"
|
104
102
|
|
105
103
|
|
106
|
-
def vcr_writer(path:
|
104
|
+
def vcr_writer(path: Path, config: ProjectConfig, queue: Queue) -> None:
|
107
105
|
"""Write YAML to a file in an incremental manner.
|
108
106
|
|
109
107
|
This implementation doesn't use `pyyaml` package and composes YAML manually as string due to the following reasons:
|
@@ -114,16 +112,15 @@ def vcr_writer(path: LazyFile, sanitize_output: bool, preserve_bytes: bool, queu
|
|
114
112
|
providing tags, anchors to have incremental writing, with primitive types it is much simpler.
|
115
113
|
"""
|
116
114
|
current_id = 1
|
117
|
-
stream = path.open()
|
118
115
|
|
119
116
|
def format_header_values(values: list[str]) -> str:
|
120
117
|
return "\n".join(f" - {json.dumps(v)}" for v in values)
|
121
118
|
|
122
|
-
if
|
119
|
+
if config.output.sanitization.enabled:
|
123
120
|
|
124
121
|
def format_headers(headers: dict[str, list[str]]) -> str:
|
125
122
|
headers = deepclone(headers)
|
126
|
-
sanitize_value(headers)
|
123
|
+
sanitize_value(headers, config=config.output.sanitization)
|
127
124
|
return "\n".join(f' "{name}":\n{format_header_values(values)}' for name, values in headers.items())
|
128
125
|
|
129
126
|
else:
|
@@ -145,7 +142,7 @@ def vcr_writer(path: LazyFile, sanitize_output: bool, preserve_bytes: bool, queu
|
|
145
142
|
checks:
|
146
143
|
{items}"""
|
147
144
|
|
148
|
-
if preserve_bytes:
|
145
|
+
if config.reports.preserve_bytes:
|
149
146
|
|
150
147
|
def format_request_body(output: IO, request: Request) -> None:
|
151
148
|
if request.encoded_body is not None:
|
@@ -188,102 +185,105 @@ def vcr_writer(path: LazyFile, sanitize_output: bool, preserve_bytes: bool, queu
|
|
188
185
|
)
|
189
186
|
write_double_quoted(output, string)
|
190
187
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
188
|
+
with open(path, "w", encoding="utf-8") as stream:
|
189
|
+
while True:
|
190
|
+
item = queue.get()
|
191
|
+
if isinstance(item, Initialize):
|
192
|
+
stream.write(
|
193
|
+
f"""command: '{get_command_representation()}'
|
196
194
|
recorded_with: 'Schemathesis {SCHEMATHESIS_VERSION}'
|
197
195
|
seed: {item.seed}
|
198
196
|
http_interactions:"""
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
197
|
+
)
|
198
|
+
elif isinstance(item, Process):
|
199
|
+
for case_id, interaction in item.recorder.interactions.items():
|
200
|
+
case = item.recorder.cases[case_id]
|
201
|
+
if interaction.response is not None:
|
202
|
+
if case_id in item.recorder.checks:
|
203
|
+
checks = item.recorder.checks[case_id]
|
204
|
+
status = Status.SUCCESS
|
205
|
+
for check in checks:
|
206
|
+
if check.status == Status.FAILURE:
|
207
|
+
status = check.status
|
208
|
+
break
|
209
|
+
else:
|
210
|
+
# NOTE: Checks recording could be skipped if Schemathesis start skipping just
|
211
|
+
# discovered failures in order to get past them and potentially discover more failures
|
212
|
+
checks = []
|
213
|
+
status = Status.SKIP
|
211
214
|
else:
|
212
|
-
# NOTE: Checks recording could be skipped if Schemathesis start skipping just
|
213
|
-
# discovered failures in order to get past them and potentially discover more failures
|
214
215
|
checks = []
|
215
|
-
status = Status.
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
# Body payloads are handled via separate `stream.write` calls to avoid some allocations
|
220
|
-
stream.write(
|
221
|
-
f"""\n- id: '{case_id}'
|
216
|
+
status = Status.ERROR
|
217
|
+
# Body payloads are handled via separate `stream.write` calls to avoid some allocations
|
218
|
+
stream.write(
|
219
|
+
f"""\n- id: '{case_id}'
|
222
220
|
status: '{status.name}'"""
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
221
|
+
)
|
222
|
+
meta = case.value.meta
|
223
|
+
if meta is not None:
|
224
|
+
# Start metadata block
|
225
|
+
stream.write(f"""
|
228
226
|
generation:
|
229
227
|
time: {meta.generation.time}
|
230
228
|
mode: {meta.generation.mode.value}
|
231
229
|
components:""")
|
232
230
|
|
233
|
-
|
234
|
-
|
235
|
-
|
231
|
+
# Write components
|
232
|
+
for kind, info in meta.components.items():
|
233
|
+
stream.write(f"""
|
236
234
|
{kind.value}:
|
237
235
|
mode: '{info.mode.value}'""")
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
236
|
+
# Write phase info
|
237
|
+
stream.write("\n phase:")
|
238
|
+
stream.write(f"\n name: '{meta.phase.name.value}'")
|
239
|
+
stream.write("\n data: ")
|
240
|
+
|
241
|
+
# Write phase-specific data
|
242
|
+
if isinstance(meta.phase.data, CoveragePhaseData):
|
243
|
+
stream.write("""
|
246
244
|
description: """)
|
247
|
-
|
248
|
-
|
245
|
+
write_double_quoted(stream, meta.phase.data.description)
|
246
|
+
stream.write("""
|
249
247
|
location: """)
|
250
|
-
|
251
|
-
|
248
|
+
write_double_quoted(stream, meta.phase.data.location)
|
249
|
+
stream.write("""
|
252
250
|
parameter: """)
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
251
|
+
if meta.phase.data.parameter is not None:
|
252
|
+
write_double_quoted(stream, meta.phase.data.parameter)
|
253
|
+
else:
|
254
|
+
stream.write("null")
|
255
|
+
stream.write("""
|
258
256
|
parameter_location: """)
|
259
|
-
|
260
|
-
|
257
|
+
if meta.phase.data.parameter_location is not None:
|
258
|
+
write_double_quoted(stream, meta.phase.data.parameter_location)
|
259
|
+
else:
|
260
|
+
stream.write("null")
|
261
261
|
else:
|
262
|
-
|
262
|
+
# Empty objects for these phases
|
263
|
+
stream.write("{}")
|
263
264
|
else:
|
264
|
-
|
265
|
-
stream.write("{}")
|
266
|
-
else:
|
267
|
-
stream.write("null")
|
265
|
+
stream.write("null")
|
268
266
|
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
267
|
+
if config.output.sanitization.enabled:
|
268
|
+
uri = sanitize_url(interaction.request.uri, config=config.output.sanitization)
|
269
|
+
else:
|
270
|
+
uri = interaction.request.uri
|
271
|
+
recorded_at = datetime.datetime.fromtimestamp(
|
272
|
+
interaction.timestamp, datetime.timezone.utc
|
273
|
+
).isoformat()
|
274
|
+
stream.write(
|
275
|
+
f"""
|
276
276
|
recorded_at: '{recorded_at}'{format_checks(checks)}
|
277
277
|
request:
|
278
278
|
uri: '{uri}'
|
279
279
|
method: '{interaction.request.method}'
|
280
280
|
headers:
|
281
281
|
{format_headers(interaction.request.headers)}"""
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
282
|
+
)
|
283
|
+
format_request_body(stream, interaction.request)
|
284
|
+
if interaction.response is not None:
|
285
|
+
stream.write(
|
286
|
+
f"""
|
287
287
|
response:
|
288
288
|
status:
|
289
289
|
code: '{interaction.response.status_code}'
|
@@ -292,19 +292,18 @@ http_interactions:"""
|
|
292
292
|
headers:
|
293
293
|
{format_headers(interaction.response.headers)}
|
294
294
|
"""
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
295
|
+
)
|
296
|
+
format_response_body(stream, interaction.response)
|
297
|
+
stream.write(
|
298
|
+
f"""
|
299
299
|
http_version: '{interaction.response.http_version}'"""
|
300
|
-
|
301
|
-
|
302
|
-
|
300
|
+
)
|
301
|
+
else:
|
302
|
+
stream.write("""
|
303
303
|
response: null""")
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
path.close()
|
304
|
+
current_id += 1
|
305
|
+
else:
|
306
|
+
break
|
308
307
|
|
309
308
|
|
310
309
|
def write_double_quoted(stream: IO, text: str | None) -> None:
|
@@ -350,14 +349,14 @@ def write_double_quoted(stream: IO, text: str | None) -> None:
|
|
350
349
|
stream.write('"')
|
351
350
|
|
352
351
|
|
353
|
-
def har_writer(path:
|
352
|
+
def har_writer(path: Path, config: SchemathesisConfig, queue: Queue) -> None:
|
354
353
|
with harfile.open(path) as har:
|
355
354
|
while True:
|
356
355
|
item = queue.get()
|
357
356
|
if isinstance(item, Process):
|
358
357
|
for interaction in item.recorder.interactions.values():
|
359
|
-
if
|
360
|
-
uri = sanitize_url(interaction.request.uri)
|
358
|
+
if config.output.sanitization.enabled:
|
359
|
+
uri = sanitize_url(interaction.request.uri, config=config.output.sanitization)
|
361
360
|
else:
|
362
361
|
uri = interaction.request.uri
|
363
362
|
query_params = urlparse(uri).query
|
@@ -365,7 +364,7 @@ def har_writer(path: LazyFile, sanitize_output: bool, preserve_bytes: bool, queu
|
|
365
364
|
post_data = harfile.PostData(
|
366
365
|
mimeType=interaction.request.headers.get("Content-Type", [""])[0],
|
367
366
|
text=interaction.request.encoded_body
|
368
|
-
if preserve_bytes
|
367
|
+
if config.reports.preserve_bytes
|
369
368
|
else interaction.request.body.decode("utf-8", "replace"),
|
370
369
|
)
|
371
370
|
else:
|
@@ -376,16 +375,18 @@ def har_writer(path: LazyFile, sanitize_output: bool, preserve_bytes: bool, queu
|
|
376
375
|
size=interaction.response.body_size or 0,
|
377
376
|
mimeType=content_type,
|
378
377
|
text=interaction.response.encoded_body
|
379
|
-
if preserve_bytes
|
378
|
+
if config.reports.preserve_bytes
|
380
379
|
else interaction.response.content.decode("utf-8", "replace")
|
381
380
|
if interaction.response.content is not None
|
382
381
|
else None,
|
383
|
-
encoding="base64"
|
382
|
+
encoding="base64"
|
383
|
+
if interaction.response.content is not None and config.reports.preserve_bytes
|
384
|
+
else None,
|
384
385
|
)
|
385
386
|
http_version = f"HTTP/{interaction.response.http_version}"
|
386
|
-
if
|
387
|
+
if config.output.sanitization.enabled:
|
387
388
|
headers = deepclone(interaction.response.headers)
|
388
|
-
sanitize_value(headers)
|
389
|
+
sanitize_value(headers, config=config.output.sanitization)
|
389
390
|
else:
|
390
391
|
headers = interaction.response.headers
|
391
392
|
response = harfile.Response(
|
@@ -405,9 +406,9 @@ def har_writer(path: LazyFile, sanitize_output: bool, preserve_bytes: bool, queu
|
|
405
406
|
time = 0
|
406
407
|
http_version = ""
|
407
408
|
|
408
|
-
if
|
409
|
+
if config.output.sanitization.enabled:
|
409
410
|
headers = deepclone(interaction.request.headers)
|
410
|
-
sanitize_value(headers)
|
411
|
+
sanitize_value(headers, config=config.output.sanitization)
|
411
412
|
else:
|
412
413
|
headers = interaction.request.headers
|
413
414
|
started_datetime = datetime.datetime.fromtimestamp(
|
@@ -2,9 +2,9 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import platform
|
4
4
|
from dataclasses import dataclass, field
|
5
|
+
from pathlib import Path
|
5
6
|
from typing import Iterable
|
6
7
|
|
7
|
-
from click.utils import LazyFile
|
8
8
|
from junit_xml import TestCase, TestSuite, to_xml_report_file
|
9
9
|
|
10
10
|
from schemathesis.cli.commands.run.context import ExecutionContext, GroupedFailures
|
@@ -15,7 +15,7 @@ from schemathesis.engine import Status, events
|
|
15
15
|
|
16
16
|
@dataclass
|
17
17
|
class JunitXMLHandler(EventHandler):
|
18
|
-
|
18
|
+
path: Path
|
19
19
|
test_cases: dict = field(default_factory=dict)
|
20
20
|
|
21
21
|
def handle_event(self, ctx: ExecutionContext, event: events.EngineEvent) -> None:
|
@@ -34,7 +34,8 @@ class JunitXMLHandler(EventHandler):
|
|
34
34
|
test_suites = [
|
35
35
|
TestSuite("schemathesis", test_cases=list(self.test_cases.values()), hostname=platform.node())
|
36
36
|
]
|
37
|
-
|
37
|
+
with open(self.path, "w") as fd:
|
38
|
+
to_xml_report_file(file_descriptor=fd, test_suites=test_suites, prettyprint=True)
|
38
39
|
|
39
40
|
def get_or_create_test_case(self, label: str) -> TestCase:
|
40
41
|
return self.test_cases.setdefault(label, TestCase(label, elapsed_sec=0.0, allow_multiple_subelements=True))
|
@@ -47,7 +48,7 @@ def add_failure(test_case: TestCase, checks: Iterable[GroupedFailures], context:
|
|
47
48
|
response=group.response,
|
48
49
|
failures=group.failures,
|
49
50
|
curl=group.code_sample,
|
50
|
-
config=context.
|
51
|
+
config=context.config.output,
|
51
52
|
)
|
52
53
|
for idx, group in enumerate(checks, 1)
|
53
54
|
]
|