schemathesis 4.0.0a2__py3-none-any.whl → 4.0.0a4__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/cli/__init__.py +15 -4
- schemathesis/cli/commands/run/__init__.py +148 -94
- schemathesis/cli/commands/run/context.py +72 -2
- schemathesis/cli/commands/run/events.py +22 -2
- schemathesis/cli/commands/run/executor.py +35 -12
- schemathesis/cli/commands/run/filters.py +1 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +27 -46
- schemathesis/cli/commands/run/handlers/junitxml.py +1 -1
- schemathesis/cli/commands/run/handlers/output.py +180 -87
- schemathesis/cli/commands/run/hypothesis.py +30 -19
- schemathesis/cli/commands/run/reports.py +72 -0
- schemathesis/cli/commands/run/validation.py +18 -12
- schemathesis/cli/ext/groups.py +42 -13
- schemathesis/cli/ext/options.py +15 -8
- schemathesis/core/errors.py +85 -9
- schemathesis/core/failures.py +2 -1
- schemathesis/core/transforms.py +1 -1
- schemathesis/engine/core.py +1 -1
- schemathesis/engine/errors.py +17 -6
- schemathesis/engine/phases/stateful/__init__.py +1 -0
- schemathesis/engine/phases/stateful/_executor.py +9 -12
- schemathesis/engine/phases/unit/__init__.py +2 -3
- schemathesis/engine/phases/unit/_executor.py +16 -13
- schemathesis/engine/recorder.py +22 -21
- schemathesis/errors.py +23 -13
- schemathesis/filters.py +8 -0
- schemathesis/generation/coverage.py +10 -5
- schemathesis/generation/hypothesis/builder.py +15 -12
- schemathesis/generation/stateful/state_machine.py +57 -12
- schemathesis/pytest/lazy.py +2 -3
- schemathesis/pytest/plugin.py +2 -3
- schemathesis/schemas.py +1 -1
- schemathesis/specs/openapi/checks.py +77 -37
- schemathesis/specs/openapi/expressions/__init__.py +22 -6
- schemathesis/specs/openapi/expressions/nodes.py +15 -21
- schemathesis/specs/openapi/expressions/parser.py +1 -1
- schemathesis/specs/openapi/parameters.py +0 -2
- schemathesis/specs/openapi/patterns.py +170 -2
- schemathesis/specs/openapi/schemas.py +67 -39
- schemathesis/specs/openapi/stateful/__init__.py +207 -84
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/{links.py → stateful/links.py} +72 -14
- {schemathesis-4.0.0a2.dist-info → schemathesis-4.0.0a4.dist-info}/METADATA +1 -1
- {schemathesis-4.0.0a2.dist-info → schemathesis-4.0.0a4.dist-info}/RECORD +47 -45
- {schemathesis-4.0.0a2.dist-info → schemathesis-4.0.0a4.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a2.dist-info → schemathesis-4.0.0a4.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.0a2.dist-info → schemathesis-4.0.0a4.dist-info}/licenses/LICENSE +0 -0
schemathesis/cli/__init__.py
CHANGED
@@ -1,17 +1,28 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
from schemathesis.cli.commands import Group, run, schemathesis
|
4
|
+
from schemathesis.cli.commands.run.context import ExecutionContext
|
5
|
+
from schemathesis.cli.commands.run.events import LoadingFinished, LoadingStarted
|
4
6
|
from schemathesis.cli.commands.run.executor import handler
|
5
7
|
from schemathesis.cli.commands.run.handlers import EventHandler
|
6
|
-
from schemathesis.cli.ext.groups import GROUPS
|
8
|
+
from schemathesis.cli.ext.groups import GROUPS, OptionGroup
|
7
9
|
|
8
|
-
__all__ = [
|
10
|
+
__all__ = [
|
11
|
+
"schemathesis",
|
12
|
+
"run",
|
13
|
+
"EventHandler",
|
14
|
+
"ExecutionContext",
|
15
|
+
"LoadingStarted",
|
16
|
+
"LoadingFinished",
|
17
|
+
"add_group",
|
18
|
+
"handler",
|
19
|
+
]
|
9
20
|
|
10
21
|
|
11
22
|
def add_group(name: str, *, index: int | None = None) -> Group:
|
12
23
|
"""Add a custom options group to `st run`."""
|
13
24
|
if index is not None:
|
14
|
-
GROUPS
|
25
|
+
GROUPS[name] = OptionGroup(name=name, order=index)
|
15
26
|
else:
|
16
|
-
GROUPS
|
27
|
+
GROUPS[name] = OptionGroup(name=name)
|
17
28
|
return Group(name)
|
@@ -1,16 +1,17 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
from pathlib import Path
|
3
4
|
from random import Random
|
4
5
|
from typing import Any, Sequence
|
5
6
|
|
6
7
|
import click
|
8
|
+
from click.utils import LazyFile
|
7
9
|
|
8
10
|
from schemathesis import contrib, experimental
|
9
11
|
from schemathesis.checks import CHECKS
|
10
12
|
from schemathesis.cli.commands.run import executor, validation
|
11
13
|
from schemathesis.cli.commands.run.checks import CheckArguments
|
12
14
|
from schemathesis.cli.commands.run.filters import FilterArguments, with_filters
|
13
|
-
from schemathesis.cli.commands.run.handlers.cassettes import CassetteConfig, CassetteFormat
|
14
15
|
from schemathesis.cli.commands.run.hypothesis import (
|
15
16
|
HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER,
|
16
17
|
HealthCheck,
|
@@ -19,6 +20,7 @@ from schemathesis.cli.commands.run.hypothesis import (
|
|
19
20
|
prepare_phases,
|
20
21
|
prepare_settings,
|
21
22
|
)
|
23
|
+
from schemathesis.cli.commands.run.reports import DEFAULT_REPORT_DIRECTORY, ReportConfig, ReportFormat
|
22
24
|
from schemathesis.cli.constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS
|
23
25
|
from schemathesis.cli.core import ensure_color
|
24
26
|
from schemathesis.cli.ext.groups import group, grouped_option
|
@@ -48,26 +50,15 @@ DEFAULT_PHASES = ("unit", "stateful")
|
|
48
50
|
@click.argument("schema", type=str) # type: ignore[misc]
|
49
51
|
@group("Options")
|
50
52
|
@grouped_option(
|
51
|
-
"--
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
metavar="",
|
56
|
-
)
|
57
|
-
@grouped_option(
|
58
|
-
"--base-url",
|
59
|
-
"-b",
|
60
|
-
help="Base URL of the API, required when schema is provided as a file",
|
53
|
+
"--url",
|
54
|
+
"-u",
|
55
|
+
"base_url",
|
56
|
+
help="API base URL (required for file-based schemas)",
|
57
|
+
metavar="URL",
|
61
58
|
type=str,
|
62
59
|
callback=validation.validate_base_url,
|
63
60
|
envvar="SCHEMATHESIS_BASE_URL",
|
64
61
|
)
|
65
|
-
@grouped_option(
|
66
|
-
"--suppress-health-check",
|
67
|
-
help="A comma-separated list of Schemathesis health checks to disable",
|
68
|
-
type=CsvEnumChoice(HealthCheck),
|
69
|
-
metavar="",
|
70
|
-
)
|
71
62
|
@grouped_option(
|
72
63
|
"--workers",
|
73
64
|
"-w",
|
@@ -82,6 +73,26 @@ DEFAULT_PHASES = ("unit", "stateful")
|
|
82
73
|
callback=validation.convert_workers,
|
83
74
|
metavar="",
|
84
75
|
)
|
76
|
+
@grouped_option(
|
77
|
+
"--phases",
|
78
|
+
help="A comma-separated list of test phases to run",
|
79
|
+
type=CsvChoice(["unit", "stateful"]),
|
80
|
+
default=",".join(DEFAULT_PHASES),
|
81
|
+
metavar="",
|
82
|
+
)
|
83
|
+
@grouped_option(
|
84
|
+
"--suppress-health-check",
|
85
|
+
help="A comma-separated list of Schemathesis health checks to disable",
|
86
|
+
type=CsvEnumChoice(HealthCheck),
|
87
|
+
metavar="",
|
88
|
+
)
|
89
|
+
@grouped_option(
|
90
|
+
"--wait-for-schema",
|
91
|
+
help="Maximum duration, in seconds, to wait for the API schema to become available. Disabled by default",
|
92
|
+
type=click.FloatRange(1.0),
|
93
|
+
default=None,
|
94
|
+
envvar="SCHEMATHESIS_WAIT_FOR_SCHEMA",
|
95
|
+
)
|
85
96
|
@group("API validation options")
|
86
97
|
@grouped_option(
|
87
98
|
"--checks",
|
@@ -106,11 +117,6 @@ DEFAULT_PHASES = ("unit", "stateful")
|
|
106
117
|
show_default=True,
|
107
118
|
metavar="",
|
108
119
|
)
|
109
|
-
@grouped_option(
|
110
|
-
"--max-response-time",
|
111
|
-
help="Time limit in seconds for API response times. The test will fail if a response time exceeds this limit",
|
112
|
-
type=click.FloatRange(min=0.0, min_open=True),
|
113
|
-
)
|
114
120
|
@grouped_option(
|
115
121
|
"-x",
|
116
122
|
"--exitfirst",
|
@@ -127,42 +133,52 @@ DEFAULT_PHASES = ("unit", "stateful")
|
|
127
133
|
help="Terminate the test suite after reaching a specified number of failures or errors",
|
128
134
|
show_default=True,
|
129
135
|
)
|
130
|
-
@
|
136
|
+
@grouped_option(
|
137
|
+
"--max-response-time",
|
138
|
+
help="Maximum allowed API response time in seconds",
|
139
|
+
type=click.FloatRange(min=0.0, min_open=True),
|
140
|
+
metavar="SECONDS",
|
141
|
+
)
|
142
|
+
@group(
|
143
|
+
"Filtering options",
|
144
|
+
description=(
|
145
|
+
"Filter operations by path, method, name, tag, or operation-id using:\n\n"
|
146
|
+
"--include-TYPE VALUE Match operations with exact VALUE\n"
|
147
|
+
"--include-TYPE-regex PATTERN Match operations using regular expression\n"
|
148
|
+
"--exclude-TYPE VALUE Exclude operations with exact VALUE\n"
|
149
|
+
"--exclude-TYPE-regex PATTERN Exclude operations using regular expression"
|
150
|
+
),
|
151
|
+
)
|
131
152
|
@with_filters
|
132
153
|
@grouped_option(
|
133
154
|
"--include-by",
|
134
155
|
"include_by",
|
135
156
|
type=str,
|
136
|
-
|
157
|
+
metavar="EXPR",
|
158
|
+
help="Include using custom expression",
|
137
159
|
)
|
138
160
|
@grouped_option(
|
139
161
|
"--exclude-by",
|
140
162
|
"exclude_by",
|
141
163
|
type=str,
|
142
|
-
|
164
|
+
metavar="EXPR",
|
165
|
+
help="Exclude using custom expression",
|
143
166
|
)
|
144
167
|
@grouped_option(
|
145
168
|
"--exclude-deprecated",
|
146
|
-
help="
|
169
|
+
help="Skip deprecated operations",
|
147
170
|
is_flag=True,
|
148
171
|
is_eager=True,
|
149
172
|
default=False,
|
150
173
|
show_default=True,
|
151
174
|
)
|
152
|
-
@group("Loader options")
|
153
|
-
@grouped_option(
|
154
|
-
"--wait-for-schema",
|
155
|
-
help="Maximum duration, in seconds, to wait for the API schema to become available. Disabled by default",
|
156
|
-
type=click.FloatRange(1.0),
|
157
|
-
default=None,
|
158
|
-
envvar="SCHEMATHESIS_WAIT_FOR_SCHEMA",
|
159
|
-
)
|
160
175
|
@group("Network requests options")
|
161
176
|
@grouped_option(
|
162
177
|
"--header",
|
163
178
|
"-H",
|
164
179
|
"headers",
|
165
|
-
help=r"Add a custom HTTP header to all API requests
|
180
|
+
help=r"Add a custom HTTP header to all API requests",
|
181
|
+
metavar="NAME:VALUE",
|
166
182
|
multiple=True,
|
167
183
|
type=str,
|
168
184
|
callback=validation.validate_headers,
|
@@ -170,29 +186,40 @@ DEFAULT_PHASES = ("unit", "stateful")
|
|
170
186
|
@grouped_option(
|
171
187
|
"--auth",
|
172
188
|
"-a",
|
173
|
-
help="
|
189
|
+
help="Authenticate all API requests with basic authentication",
|
190
|
+
metavar="USER:PASS",
|
174
191
|
type=str,
|
175
192
|
callback=validation.validate_auth,
|
176
193
|
)
|
177
194
|
@grouped_option(
|
178
|
-
"--
|
179
|
-
|
180
|
-
type=click.FloatRange(min=0.0, min_open=True),
|
181
|
-
default=DEFAULT_RESPONSE_TIMEOUT,
|
182
|
-
)
|
183
|
-
@grouped_option(
|
184
|
-
"--request-proxy",
|
195
|
+
"--proxy",
|
196
|
+
"request_proxy",
|
185
197
|
help="Set the proxy for all network requests",
|
198
|
+
metavar="URL",
|
186
199
|
type=str,
|
187
200
|
)
|
188
201
|
@grouped_option(
|
189
|
-
"--
|
190
|
-
|
202
|
+
"--tls-verify",
|
203
|
+
"request_tls_verify",
|
204
|
+
help="Path to CA bundle for TLS verification, or 'false' to disable",
|
191
205
|
type=str,
|
192
206
|
default="true",
|
193
207
|
show_default=True,
|
194
208
|
callback=validation.convert_boolean_string,
|
195
209
|
)
|
210
|
+
@grouped_option(
|
211
|
+
"--rate-limit",
|
212
|
+
help="Specify a rate limit for test requests in '<limit>/<duration>' format. "
|
213
|
+
"Example - `100/m` for 100 requests per minute",
|
214
|
+
type=str,
|
215
|
+
callback=validation.validate_rate_limit,
|
216
|
+
)
|
217
|
+
@grouped_option(
|
218
|
+
"--request-timeout",
|
219
|
+
help="Timeout limit, in seconds, for each network request during tests",
|
220
|
+
type=click.FloatRange(min=0.0, min_open=True),
|
221
|
+
default=DEFAULT_RESPONSE_TIMEOUT,
|
222
|
+
)
|
196
223
|
@grouped_option(
|
197
224
|
"--request-cert",
|
198
225
|
help="File path of unencrypted client certificate for authentication. "
|
@@ -210,45 +237,56 @@ DEFAULT_PHASES = ("unit", "stateful")
|
|
210
237
|
show_default=False,
|
211
238
|
callback=validation.validate_request_cert_key,
|
212
239
|
)
|
240
|
+
@group("Output options")
|
213
241
|
@grouped_option(
|
214
|
-
"--
|
215
|
-
|
216
|
-
"
|
217
|
-
type=
|
218
|
-
|
242
|
+
"--report",
|
243
|
+
"report_formats",
|
244
|
+
help="Generate test reports in specified formats",
|
245
|
+
type=CsvEnumChoice(ReportFormat),
|
246
|
+
is_eager=True,
|
247
|
+
metavar="FORMAT",
|
219
248
|
)
|
220
|
-
@group("Output options")
|
221
249
|
@grouped_option(
|
222
|
-
"--
|
223
|
-
help="
|
250
|
+
"--report-dir",
|
251
|
+
help="Directory to store all report files",
|
252
|
+
type=click.Path(file_okay=False, dir_okay=True),
|
253
|
+
default=DEFAULT_REPORT_DIRECTORY,
|
254
|
+
show_default=True,
|
255
|
+
)
|
256
|
+
@grouped_option(
|
257
|
+
"--report-junit-path",
|
258
|
+
help="Custom path for JUnit XML report",
|
224
259
|
type=click.File("w", encoding="utf-8"),
|
260
|
+
is_eager=True,
|
225
261
|
)
|
226
262
|
@grouped_option(
|
227
|
-
"--
|
228
|
-
help="
|
263
|
+
"--report-vcr-path",
|
264
|
+
help="Custom path for VCR cassette",
|
229
265
|
type=click.File("w", encoding="utf-8"),
|
230
266
|
is_eager=True,
|
231
267
|
)
|
232
268
|
@grouped_option(
|
233
|
-
"--
|
234
|
-
help="
|
235
|
-
type=click.
|
236
|
-
|
237
|
-
callback=validation.convert_cassette_format,
|
238
|
-
metavar="",
|
269
|
+
"--report-har-path",
|
270
|
+
help="Custom path for HAR file",
|
271
|
+
type=click.File("w", encoding="utf-8"),
|
272
|
+
is_eager=True,
|
239
273
|
)
|
240
274
|
@grouped_option(
|
241
|
-
"--
|
275
|
+
"--report-preserve-bytes",
|
242
276
|
help="Retain exact byte sequence of payloads in cassettes, encoded as base64",
|
277
|
+
type=bool,
|
243
278
|
is_flag=True,
|
244
|
-
|
279
|
+
default=False,
|
280
|
+
callback=validation.validate_preserve_bytes,
|
245
281
|
)
|
246
282
|
@grouped_option(
|
247
283
|
"--output-sanitize",
|
248
|
-
type=
|
249
|
-
default=
|
284
|
+
type=str,
|
285
|
+
default="true",
|
250
286
|
show_default=True,
|
251
287
|
help="Enable or disable automatic output sanitization to obscure sensitive data",
|
288
|
+
metavar="BOOLEAN",
|
289
|
+
callback=validation.convert_boolean_string,
|
252
290
|
)
|
253
291
|
@grouped_option(
|
254
292
|
"--output-truncate",
|
@@ -256,6 +294,7 @@ DEFAULT_PHASES = ("unit", "stateful")
|
|
256
294
|
type=str,
|
257
295
|
default="true",
|
258
296
|
show_default=True,
|
297
|
+
metavar="BOOLEAN",
|
259
298
|
callback=validation.convert_boolean_string,
|
260
299
|
)
|
261
300
|
@group("Experimental options")
|
@@ -306,10 +345,10 @@ DEFAULT_PHASES = ("unit", "stateful")
|
|
306
345
|
)
|
307
346
|
@group("Data generation options")
|
308
347
|
@grouped_option(
|
309
|
-
"--
|
348
|
+
"--mode",
|
349
|
+
"-m",
|
310
350
|
"generation_modes",
|
311
|
-
help="
|
312
|
-
"Use 'positive' for valid data, 'negative' for invalid data, or 'all' for both",
|
351
|
+
help="Test data generation mode",
|
313
352
|
type=click.Choice([item.value for item in GenerationMode] + ["all"]),
|
314
353
|
default=GenerationMode.default().value,
|
315
354
|
callback=validation.convert_generation_mode,
|
@@ -317,14 +356,23 @@ DEFAULT_PHASES = ("unit", "stateful")
|
|
317
356
|
metavar="",
|
318
357
|
)
|
319
358
|
@grouped_option(
|
320
|
-
"--
|
321
|
-
|
359
|
+
"--max-examples",
|
360
|
+
"-n",
|
361
|
+
"generation_max_examples",
|
362
|
+
help="Maximum number of test cases per API operation",
|
363
|
+
type=click.IntRange(1),
|
364
|
+
)
|
365
|
+
@grouped_option(
|
366
|
+
"--seed",
|
367
|
+
"generation_seed",
|
368
|
+
help="Random seed for reproducible test runs",
|
322
369
|
type=int,
|
323
370
|
)
|
324
371
|
@grouped_option(
|
325
|
-
"--
|
326
|
-
|
327
|
-
|
372
|
+
"--no-shrink",
|
373
|
+
"generation_no_shrink",
|
374
|
+
help="Disable test case shrinking. Makes test failures harder to debug but improves performance",
|
375
|
+
is_flag=True,
|
328
376
|
)
|
329
377
|
@grouped_option(
|
330
378
|
"--generation-deterministic",
|
@@ -336,10 +384,11 @@ DEFAULT_PHASES = ("unit", "stateful")
|
|
336
384
|
)
|
337
385
|
@grouped_option(
|
338
386
|
"--generation-allow-x00",
|
339
|
-
help="Whether to allow the generation of
|
387
|
+
help="Whether to allow the generation of 'NULL' bytes within strings",
|
340
388
|
type=str,
|
341
389
|
default="true",
|
342
390
|
show_default=True,
|
391
|
+
metavar="BOOLEAN",
|
343
392
|
callback=validation.convert_boolean_string,
|
344
393
|
)
|
345
394
|
@grouped_option(
|
@@ -350,8 +399,8 @@ DEFAULT_PHASES = ("unit", "stateful")
|
|
350
399
|
callback=validation.validate_generation_codec,
|
351
400
|
)
|
352
401
|
@grouped_option(
|
353
|
-
"--generation-
|
354
|
-
"
|
402
|
+
"--generation-maximize",
|
403
|
+
"generation_maximize",
|
355
404
|
multiple=True,
|
356
405
|
help="Guide input generation to values more likely to expose bugs via targeted property-based testing",
|
357
406
|
type=RegistryChoice(TARGETS),
|
@@ -470,8 +519,8 @@ def run(
|
|
470
519
|
negative_data_rejection_allowed_statuses: list[str],
|
471
520
|
included_check_names: Sequence[str],
|
472
521
|
excluded_check_names: Sequence[str],
|
473
|
-
phases: Sequence[str] = DEFAULT_PHASES,
|
474
522
|
max_response_time: float | None = None,
|
523
|
+
phases: Sequence[str] = DEFAULT_PHASES,
|
475
524
|
exit_first: bool = False,
|
476
525
|
max_failures: int | None = None,
|
477
526
|
include_path: Sequence[str] = (),
|
@@ -499,18 +548,20 @@ def run(
|
|
499
548
|
exclude_deprecated: bool = False,
|
500
549
|
workers_num: int = DEFAULT_WORKERS,
|
501
550
|
base_url: str | None = None,
|
551
|
+
wait_for_schema: float | None = None,
|
552
|
+
rate_limit: str | None = None,
|
502
553
|
suppress_health_check: list[HealthCheck] | None = None,
|
503
554
|
request_timeout: int | None = None,
|
504
555
|
request_tls_verify: bool = True,
|
505
556
|
request_cert: str | None = None,
|
506
557
|
request_cert_key: str | None = None,
|
507
558
|
request_proxy: str | None = None,
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
559
|
+
report_formats: list[ReportFormat] | None = None,
|
560
|
+
report_dir: Path = DEFAULT_REPORT_DIRECTORY,
|
561
|
+
report_junit_path: LazyFile | None = None,
|
562
|
+
report_vcr_path: LazyFile | None = None,
|
563
|
+
report_har_path: LazyFile | None = None,
|
564
|
+
report_preserve_bytes: bool = False,
|
514
565
|
output_sanitize: bool = True,
|
515
566
|
output_truncate: bool = True,
|
516
567
|
contrib_openapi_fill_missing_examples: bool = False,
|
@@ -519,7 +570,7 @@ def run(
|
|
519
570
|
generation_modes: tuple[GenerationMode, ...] = DEFAULT_GENERATOR_MODES,
|
520
571
|
generation_seed: int | None = None,
|
521
572
|
generation_max_examples: int | None = None,
|
522
|
-
|
573
|
+
generation_maximize: Sequence[str] | None = None,
|
523
574
|
generation_deterministic: bool | None = None,
|
524
575
|
generation_database: str | None = None,
|
525
576
|
generation_unique_inputs: bool = False,
|
@@ -527,6 +578,7 @@ def run(
|
|
527
578
|
generation_graphql_allow_null: bool = True,
|
528
579
|
generation_with_security_parameters: bool = True,
|
529
580
|
generation_codec: str = "utf-8",
|
581
|
+
generation_no_shrink: bool = False,
|
530
582
|
force_color: bool = False,
|
531
583
|
no_color: bool = False,
|
532
584
|
**__kwargs: Any,
|
@@ -541,7 +593,7 @@ def run(
|
|
541
593
|
|
542
594
|
validation.validate_schema(schema, base_url)
|
543
595
|
|
544
|
-
_hypothesis_phases = prepare_phases(hypothesis_phases, hypothesis_no_phases)
|
596
|
+
_hypothesis_phases = prepare_phases(hypothesis_phases, hypothesis_no_phases, generation_no_shrink)
|
545
597
|
_hypothesis_suppress_health_check = prepare_health_checks(suppress_health_check)
|
546
598
|
|
547
599
|
for experiment in experiments:
|
@@ -591,13 +643,16 @@ def run(
|
|
591
643
|
if exit_first and max_failures is None:
|
592
644
|
max_failures = 1
|
593
645
|
|
594
|
-
|
595
|
-
if
|
596
|
-
|
597
|
-
|
598
|
-
|
646
|
+
report_config = None
|
647
|
+
if report_formats or report_junit_path or report_vcr_path or report_har_path:
|
648
|
+
report_config = ReportConfig(
|
649
|
+
formats=report_formats,
|
650
|
+
directory=Path(report_dir),
|
651
|
+
junit_path=report_junit_path if report_junit_path else None,
|
652
|
+
vcr_path=report_vcr_path if report_vcr_path else None,
|
653
|
+
har_path=report_har_path if report_har_path else None,
|
654
|
+
preserve_bytes=report_preserve_bytes,
|
599
655
|
sanitize_output=output_sanitize,
|
600
|
-
preserve_exact_body_bytes=cassette_preserve_exact_body_bytes,
|
601
656
|
)
|
602
657
|
|
603
658
|
# Use the same seed for all tests unless `derandomize=True` is used
|
@@ -616,7 +671,7 @@ def run(
|
|
616
671
|
execution=ExecutionConfig(
|
617
672
|
phases=phases_,
|
618
673
|
checks=selected_checks,
|
619
|
-
targets=TARGETS.get_by_names(
|
674
|
+
targets=TARGETS.get_by_names(generation_maximize or []),
|
620
675
|
hypothesis_settings=prepare_settings(
|
621
676
|
database=generation_database,
|
622
677
|
derandomize=generation_deterministic,
|
@@ -654,8 +709,7 @@ def run(
|
|
654
709
|
wait_for_schema=wait_for_schema,
|
655
710
|
rate_limit=rate_limit,
|
656
711
|
output=OutputConfig(sanitize=output_sanitize, truncate=output_truncate),
|
657
|
-
|
658
|
-
junit_xml=junit_xml,
|
712
|
+
report=report_config,
|
659
713
|
args=ctx.args,
|
660
714
|
params=ctx.params,
|
661
715
|
)
|
@@ -1,13 +1,19 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
from dataclasses import dataclass, field
|
4
|
-
from typing import Generator
|
4
|
+
from typing import TYPE_CHECKING, Generator
|
5
5
|
|
6
6
|
from schemathesis.core.failures import Failure
|
7
7
|
from schemathesis.core.output import OutputConfig
|
8
|
+
from schemathesis.core.result import Err, Ok
|
9
|
+
from schemathesis.core.transforms import UNRESOLVABLE
|
8
10
|
from schemathesis.core.transport import Response
|
9
11
|
from schemathesis.engine import Status, events
|
10
|
-
from schemathesis.engine.recorder import ScenarioRecorder
|
12
|
+
from schemathesis.engine.recorder import CaseNode, ScenarioRecorder
|
13
|
+
from schemathesis.generation.case import Case
|
14
|
+
|
15
|
+
if TYPE_CHECKING:
|
16
|
+
from schemathesis.generation.stateful.state_machine import ExtractionFailure
|
11
17
|
|
12
18
|
|
13
19
|
@dataclass
|
@@ -17,6 +23,8 @@ class Statistic:
|
|
17
23
|
outcomes: dict[Status, int]
|
18
24
|
failures: dict[str, dict[str, GroupedFailures]]
|
19
25
|
|
26
|
+
extraction_failures: set[ExtractionFailure]
|
27
|
+
|
20
28
|
tested_operations: set[str]
|
21
29
|
|
22
30
|
total_cases: int
|
@@ -26,6 +34,7 @@ class Statistic:
|
|
26
34
|
__slots__ = (
|
27
35
|
"outcomes",
|
28
36
|
"failures",
|
37
|
+
"extraction_failures",
|
29
38
|
"tested_operations",
|
30
39
|
"total_cases",
|
31
40
|
"cases_with_failures",
|
@@ -35,6 +44,7 @@ class Statistic:
|
|
35
44
|
def __init__(self) -> None:
|
36
45
|
self.outcomes = {}
|
37
46
|
self.failures = {}
|
47
|
+
self.extraction_failures = set()
|
38
48
|
self.tested_operations = set()
|
39
49
|
self.total_cases = 0
|
40
50
|
self.cases_with_failures = 0
|
@@ -42,10 +52,25 @@ class Statistic:
|
|
42
52
|
|
43
53
|
def record_checks(self, recorder: ScenarioRecorder) -> None:
|
44
54
|
"""Update statistics and store failures from a new batch of checks."""
|
55
|
+
from schemathesis.generation.stateful.state_machine import ExtractionFailure
|
56
|
+
|
45
57
|
failures = self.failures.get(recorder.label, {})
|
46
58
|
|
47
59
|
self.total_cases += len(recorder.cases)
|
48
60
|
|
61
|
+
extraction_failures = set()
|
62
|
+
|
63
|
+
def collect_history(node: CaseNode, response: Response) -> list[tuple[Case, Response]]:
|
64
|
+
history = [(node.value, response)]
|
65
|
+
current = node
|
66
|
+
while current.parent_id is not None:
|
67
|
+
current_response = recorder.find_response(case_id=current.parent_id)
|
68
|
+
# We need a response to get there, so it should be present
|
69
|
+
assert current_response is not None
|
70
|
+
current = recorder.cases[current.parent_id]
|
71
|
+
history.append((current.value, current_response))
|
72
|
+
return history
|
73
|
+
|
49
74
|
for case_id, case in recorder.cases.items():
|
50
75
|
checks = recorder.checks.get(case_id, [])
|
51
76
|
|
@@ -71,11 +96,56 @@ class Statistic:
|
|
71
96
|
failures[case_id].failures.append(check.failure_info.failure)
|
72
97
|
if has_failures:
|
73
98
|
self.cases_with_failures += 1
|
99
|
+
|
100
|
+
if case.transition is None:
|
101
|
+
continue
|
102
|
+
transition = case.transition
|
103
|
+
parent = recorder.cases[transition.parent_id]
|
104
|
+
response = recorder.find_response(case_id=parent.value.id)
|
105
|
+
# We need a response to get there, so it should be present
|
106
|
+
assert response is not None
|
107
|
+
|
108
|
+
for params in transition.parameters.values():
|
109
|
+
for parameter, extracted in params.items():
|
110
|
+
if isinstance(extracted.value, Ok) and extracted.value.ok() is UNRESOLVABLE:
|
111
|
+
history = collect_history(parent, response)
|
112
|
+
extraction_failures.add(
|
113
|
+
ExtractionFailure(
|
114
|
+
id=transition.id,
|
115
|
+
case_id=case_id,
|
116
|
+
source=parent.value.operation.label,
|
117
|
+
target=case.value.operation.label,
|
118
|
+
parameter_name=parameter,
|
119
|
+
expression=extracted.definition,
|
120
|
+
history=history,
|
121
|
+
response=response,
|
122
|
+
error=None,
|
123
|
+
)
|
124
|
+
)
|
125
|
+
elif isinstance(extracted.value, Err):
|
126
|
+
history = collect_history(parent, response)
|
127
|
+
extraction_failures.add(
|
128
|
+
ExtractionFailure(
|
129
|
+
id=transition.id,
|
130
|
+
case_id=case_id,
|
131
|
+
source=parent.value.operation.label,
|
132
|
+
target=case.value.operation.label,
|
133
|
+
parameter_name=parameter,
|
134
|
+
expression=extracted.definition,
|
135
|
+
history=history,
|
136
|
+
response=response,
|
137
|
+
error=extracted.value.err(),
|
138
|
+
)
|
139
|
+
)
|
140
|
+
|
74
141
|
if failures:
|
75
142
|
for group in failures.values():
|
76
143
|
group.failures = sorted(set(group.failures))
|
77
144
|
self.failures[recorder.label] = failures
|
78
145
|
|
146
|
+
if extraction_failures:
|
147
|
+
self.extraction_failures.update(extraction_failures)
|
148
|
+
|
79
149
|
|
80
150
|
@dataclass
|
81
151
|
class GroupedFailures:
|
@@ -16,10 +16,28 @@ class LoadingStarted(events.EngineEvent):
|
|
16
16
|
|
17
17
|
|
18
18
|
class LoadingFinished(events.EngineEvent):
|
19
|
-
__slots__ = (
|
19
|
+
__slots__ = (
|
20
|
+
"id",
|
21
|
+
"timestamp",
|
22
|
+
"location",
|
23
|
+
"duration",
|
24
|
+
"base_url",
|
25
|
+
"base_path",
|
26
|
+
"specification",
|
27
|
+
"statistic",
|
28
|
+
"schema",
|
29
|
+
)
|
20
30
|
|
21
31
|
def __init__(
|
22
|
-
self,
|
32
|
+
self,
|
33
|
+
*,
|
34
|
+
location: str,
|
35
|
+
start_time: float,
|
36
|
+
base_url: str,
|
37
|
+
base_path: str,
|
38
|
+
specification: Specification,
|
39
|
+
statistic: ApiStatistic,
|
40
|
+
schema: dict,
|
23
41
|
) -> None:
|
24
42
|
self.id = uuid.uuid4()
|
25
43
|
self.timestamp = time.time()
|
@@ -28,3 +46,5 @@ class LoadingFinished(events.EngineEvent):
|
|
28
46
|
self.base_url = base_url
|
29
47
|
self.specification = specification
|
30
48
|
self.statistic = statistic
|
49
|
+
self.schema = schema
|
50
|
+
self.base_path = base_path
|