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
@@ -123,7 +123,7 @@ def worker_task(*, events_queue: Queue, producer: TaskProducer, ctx: EngineConte
|
|
123
123
|
else:
|
124
124
|
error = result.err()
|
125
125
|
if error.method:
|
126
|
-
label = f"{error.method.upper()} {error.
|
126
|
+
label = f"{error.method.upper()} {error.path}"
|
127
127
|
scenario_started = events.ScenarioStarted(
|
128
128
|
label=label, phase=PhaseName.UNIT_TESTING, suite_id=suite_id
|
129
129
|
)
|
@@ -149,12 +149,11 @@ def worker_task(*, events_queue: Queue, producer: TaskProducer, ctx: EngineConte
|
|
149
149
|
)
|
150
150
|
)
|
151
151
|
else:
|
152
|
-
assert error.full_path is not None
|
153
152
|
events_queue.put(
|
154
153
|
events.NonFatalError(
|
155
154
|
error=error,
|
156
155
|
phase=PhaseName.UNIT_TESTING,
|
157
|
-
label=error.
|
156
|
+
label=error.path,
|
158
157
|
related_to_operation=False,
|
159
158
|
)
|
160
159
|
)
|
@@ -70,6 +70,19 @@ def run_test(
|
|
70
70
|
error=error, phase=PhaseName.UNIT_TESTING, label=operation.label, related_to_operation=True
|
71
71
|
)
|
72
72
|
|
73
|
+
def scenario_finished(status: Status) -> events.ScenarioFinished:
|
74
|
+
return events.ScenarioFinished(
|
75
|
+
id=scenario_started.id,
|
76
|
+
suite_id=suite_id,
|
77
|
+
phase=PhaseName.UNIT_TESTING,
|
78
|
+
label=operation.label,
|
79
|
+
recorder=recorder,
|
80
|
+
status=status,
|
81
|
+
elapsed_time=time.monotonic() - test_start_time,
|
82
|
+
skip_reason=skip_reason,
|
83
|
+
is_final=False,
|
84
|
+
)
|
85
|
+
|
73
86
|
try:
|
74
87
|
setup_hypothesis_database_key(test_function, operation)
|
75
88
|
with catch_warnings(record=True) as warnings, ignore_hypothesis_output():
|
@@ -111,6 +124,7 @@ def run_test(
|
|
111
124
|
status = Status.ERROR
|
112
125
|
yield non_fatal_error(hypothesis.errors.Unsatisfiable("Failed to generate test cases for this API operation"))
|
113
126
|
except KeyboardInterrupt:
|
127
|
+
yield scenario_finished(Status.INTERRUPTED)
|
114
128
|
yield events.Interrupted(phase=PhaseName.UNIT_TESTING)
|
115
129
|
return
|
116
130
|
except AssertionError as exc: # May come from `hypothesis-jsonschema` or `hypothesis`
|
@@ -131,7 +145,6 @@ def run_test(
|
|
131
145
|
exc,
|
132
146
|
path=operation.path,
|
133
147
|
method=operation.method,
|
134
|
-
full_path=operation.schema.get_full_path(operation.path),
|
135
148
|
)
|
136
149
|
)
|
137
150
|
except HypothesisRefResolutionError:
|
@@ -194,20 +207,10 @@ def run_test(
|
|
194
207
|
if invalid_headers:
|
195
208
|
status = Status.ERROR
|
196
209
|
yield non_fatal_error(InvalidHeadersExample.from_headers(invalid_headers))
|
197
|
-
test_elapsed_time = time.monotonic() - test_start_time
|
198
210
|
for error in deduplicate_errors(errors):
|
199
211
|
yield non_fatal_error(error)
|
200
|
-
|
201
|
-
|
202
|
-
suite_id=suite_id,
|
203
|
-
phase=PhaseName.UNIT_TESTING,
|
204
|
-
label=operation.label,
|
205
|
-
recorder=recorder,
|
206
|
-
status=status,
|
207
|
-
elapsed_time=test_elapsed_time,
|
208
|
-
skip_reason=skip_reason,
|
209
|
-
is_final=False,
|
210
|
-
)
|
212
|
+
|
213
|
+
yield scenario_finished(status)
|
211
214
|
|
212
215
|
|
213
216
|
def setup_hypothesis_database_key(test: Callable, operation: APIOperation) -> None:
|
schemathesis/engine/recorder.py
CHANGED
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import base64
|
4
4
|
import time
|
5
|
-
import uuid
|
6
5
|
from dataclasses import dataclass
|
7
6
|
from typing import TYPE_CHECKING, Iterator, cast
|
8
7
|
|
@@ -24,7 +23,6 @@ class ScenarioRecorder:
|
|
24
23
|
Records test cases, their hierarchy, API interactions, and results of checks performed during execution.
|
25
24
|
"""
|
26
25
|
|
27
|
-
id: uuid.UUID
|
28
26
|
# Human-readable label
|
29
27
|
label: str
|
30
28
|
|
@@ -35,10 +33,9 @@ class ScenarioRecorder:
|
|
35
33
|
# Network interactions by test case ID
|
36
34
|
interactions: dict[str, Interaction]
|
37
35
|
|
38
|
-
__slots__ = ("
|
36
|
+
__slots__ = ("label", "status", "roots", "cases", "checks", "interactions")
|
39
37
|
|
40
38
|
def __init__(self, *, label: str) -> None:
|
41
|
-
self.id = uuid.uuid4()
|
42
39
|
self.label = label
|
43
40
|
self.cases = {}
|
44
41
|
self.checks = {}
|
@@ -96,30 +93,34 @@ class ScenarioRecorder:
|
|
96
93
|
return None
|
97
94
|
|
98
95
|
def find_related(self, *, case_id: str) -> Iterator[Case]:
|
99
|
-
"""Iterate over all
|
100
|
-
|
101
|
-
seen = {current_id}
|
96
|
+
"""Iterate over all cases in the tree, starting from the root."""
|
97
|
+
seen = {case_id}
|
102
98
|
|
99
|
+
# First, find the root by going up
|
100
|
+
current_id = case_id
|
103
101
|
while True:
|
104
102
|
current_node = self.cases.get(current_id)
|
105
103
|
if current_node is None or current_node.parent_id is None:
|
104
|
+
root_id = current_id
|
106
105
|
break
|
106
|
+
current_id = current_node.parent_id
|
107
107
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
if parent_id ==
|
108
|
+
# Then traverse the whole tree from root
|
109
|
+
def traverse(node_id: str) -> Iterator[Case]:
|
110
|
+
# Get all children
|
111
|
+
for case_id, node in self.cases.items():
|
112
|
+
if node.parent_id == node_id and case_id not in seen:
|
113
113
|
seen.add(case_id)
|
114
|
-
yield
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
114
|
+
yield node.value
|
115
|
+
# Recurse into children
|
116
|
+
yield from traverse(case_id)
|
117
|
+
|
118
|
+
# Start traversal from root
|
119
|
+
root_node = self.cases.get(root_id)
|
120
|
+
if root_node and root_id not in seen:
|
121
|
+
seen.add(root_id)
|
122
|
+
yield root_node.value
|
123
|
+
yield from traverse(root_id)
|
123
124
|
|
124
125
|
def find_response(self, *, case_id: str) -> Response | None:
|
125
126
|
"""Retrieve the API response for a given test case, if available."""
|
schemathesis/errors.py
CHANGED
@@ -1,18 +1,24 @@
|
|
1
1
|
"""Public Schemathesis errors."""
|
2
2
|
|
3
|
-
from schemathesis.core.errors import
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
3
|
+
from schemathesis.core.errors import (
|
4
|
+
IncorrectUsage,
|
5
|
+
InternalError,
|
6
|
+
InvalidHeadersExample,
|
7
|
+
InvalidRateLimit,
|
8
|
+
InvalidRegexPattern,
|
9
|
+
InvalidRegexType,
|
10
|
+
InvalidSchema,
|
11
|
+
InvalidStateMachine,
|
12
|
+
InvalidTransition,
|
13
|
+
LoaderError,
|
14
|
+
NoLinksFound,
|
15
|
+
OperationNotFound,
|
16
|
+
SchemathesisError,
|
17
|
+
SerializationError,
|
18
|
+
SerializationNotPossible,
|
19
|
+
TransitionValidationError,
|
20
|
+
UnboundPrefix,
|
21
|
+
)
|
16
22
|
|
17
23
|
__all__ = [
|
18
24
|
"IncorrectUsage",
|
@@ -22,10 +28,14 @@ __all__ = [
|
|
22
28
|
"InvalidRegexPattern",
|
23
29
|
"InvalidRegexType",
|
24
30
|
"InvalidSchema",
|
31
|
+
"InvalidStateMachine",
|
32
|
+
"InvalidTransition",
|
25
33
|
"LoaderError",
|
26
34
|
"OperationNotFound",
|
35
|
+
"NoLinksFound",
|
27
36
|
"SchemathesisError",
|
28
37
|
"SerializationError",
|
29
38
|
"SerializationNotPossible",
|
39
|
+
"TransitionValidationError",
|
30
40
|
"UnboundPrefix",
|
31
41
|
]
|
schemathesis/filters.py
CHANGED
@@ -268,6 +268,8 @@ class FilterSet:
|
|
268
268
|
# To match anything the regex should match the expected value, hence passing them together is useless
|
269
269
|
raise IncorrectUsage(ERROR_EXPECTED_AND_REGEX)
|
270
270
|
if expected is not None:
|
271
|
+
if attribute == "method":
|
272
|
+
expected = _normalize_method(expected)
|
271
273
|
matchers.append(Matcher.for_value(attribute, expected))
|
272
274
|
if regex is not None:
|
273
275
|
matchers.append(Matcher.for_regex(attribute, regex))
|
@@ -283,6 +285,12 @@ class FilterSet:
|
|
283
285
|
self._excludes.add(filter_)
|
284
286
|
|
285
287
|
|
288
|
+
def _normalize_method(value: FilterValue) -> FilterValue:
|
289
|
+
if isinstance(value, list):
|
290
|
+
return [item.upper() for item in value]
|
291
|
+
return value.upper()
|
292
|
+
|
293
|
+
|
286
294
|
def attach_filter_chain(
|
287
295
|
target: Callable,
|
288
296
|
attribute: str,
|
@@ -194,6 +194,10 @@ class CoverageContext:
|
|
194
194
|
re.compile(pattern)
|
195
195
|
except re.error:
|
196
196
|
raise Unsatisfiable from None
|
197
|
+
if "minLength" in schema or "maxLength" in schema:
|
198
|
+
min_length = schema.get("minLength")
|
199
|
+
max_length = schema.get("maxLength")
|
200
|
+
pattern = update_quantifier(pattern, min_length, max_length)
|
197
201
|
return cached_draw(st.from_regex(pattern))
|
198
202
|
if (keys == ["items", "type"] or keys == ["items", "minItems", "type"]) and isinstance(schema["items"], dict):
|
199
203
|
items = schema["items"]
|
@@ -433,7 +437,12 @@ def cover_schema_iter(
|
|
433
437
|
elif key == "required":
|
434
438
|
template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
|
435
439
|
yield from _negative_required(ctx, template, value)
|
436
|
-
elif
|
440
|
+
elif (
|
441
|
+
key == "additionalProperties"
|
442
|
+
and not value
|
443
|
+
and "pattern" not in schema
|
444
|
+
and schema.get("type") in ["object", None]
|
445
|
+
):
|
437
446
|
template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
|
438
447
|
yield NegativeValue(
|
439
448
|
{**template, UNKNOWN_PROPERTY_KEY: UNKNOWN_PROPERTY_VALUE},
|
@@ -514,11 +523,7 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
514
523
|
# Default positive value
|
515
524
|
yield PositiveValue(ctx.generate_from_schema(schema), description="Valid string")
|
516
525
|
elif "pattern" in schema:
|
517
|
-
# Without merging `maxLength` & `minLength` into a regex it is problematic
|
518
|
-
# to generate a valid value as the unredlying machinery will resort to filtering
|
519
|
-
# and it is unlikely that it will generate a string of that length
|
520
526
|
yield PositiveValue(ctx.generate_from_schema(schema), description="Valid string")
|
521
|
-
return
|
522
527
|
|
523
528
|
seen = set()
|
524
529
|
|
@@ -386,19 +386,22 @@ def _iter_coverage_cases(
|
|
386
386
|
container = template["query"]
|
387
387
|
for parameter in operation.query:
|
388
388
|
instant = Instant()
|
389
|
-
value
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
389
|
+
# Could be absent if value schema can't be negated
|
390
|
+
# I.e. contains just `default` value without any other keywords
|
391
|
+
value = container.get(parameter.name, NOT_SET)
|
392
|
+
if value is not NOT_SET:
|
393
|
+
yield operation.Case(
|
394
|
+
**{**template, "query": {**container, parameter.name: [value, value]}},
|
395
|
+
meta=CaseMetadata(
|
396
|
+
generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
|
397
|
+
components={},
|
398
|
+
phase=PhaseInfo.coverage(
|
399
|
+
description=f"Duplicate `{parameter.name}` query parameter",
|
400
|
+
parameter=parameter.name,
|
401
|
+
parameter_location="query",
|
402
|
+
),
|
399
403
|
),
|
400
|
-
)
|
401
|
-
)
|
404
|
+
)
|
402
405
|
# Generate missing required parameters
|
403
406
|
for parameter in operation.iter_parameters():
|
404
407
|
if parameter.is_required and parameter.location != "path":
|
@@ -10,7 +10,7 @@ from hypothesis.errors import InvalidDefinition
|
|
10
10
|
from hypothesis.stateful import RuleBasedStateMachine
|
11
11
|
|
12
12
|
from schemathesis.checks import CheckFunction
|
13
|
-
from schemathesis.core.errors import
|
13
|
+
from schemathesis.core.errors import NoLinksFound
|
14
14
|
from schemathesis.core.result import Result
|
15
15
|
from schemathesis.core.transport import Response
|
16
16
|
from schemathesis.generation.case import Case
|
@@ -22,16 +22,11 @@ if TYPE_CHECKING:
|
|
22
22
|
from schemathesis.schemas import BaseSchema
|
23
23
|
|
24
24
|
|
25
|
-
|
26
|
-
"Stateful testing requires at least one OpenAPI link in the schema, but no links detected. "
|
27
|
-
"Please add OpenAPI links to enable stateful testing or use stateless tests instead. \n"
|
28
|
-
"See https://schemathesis.readthedocs.io/en/stable/stateful.html#how-to-specify-connections for more information."
|
29
|
-
)
|
30
|
-
|
25
|
+
DEFAULT_STATEFUL_STEP_COUNT = 6
|
31
26
|
DEFAULT_STATE_MACHINE_SETTINGS = hypothesis.settings(
|
32
27
|
phases=[hypothesis.Phase.generate],
|
33
28
|
deadline=None,
|
34
|
-
stateful_step_count=
|
29
|
+
stateful_step_count=DEFAULT_STATEFUL_STEP_COUNT,
|
35
30
|
suppress_health_check=list(hypothesis.HealthCheck),
|
36
31
|
)
|
37
32
|
|
@@ -73,6 +68,52 @@ class ExtractedParam:
|
|
73
68
|
__slots__ = ("definition", "value")
|
74
69
|
|
75
70
|
|
71
|
+
@dataclass
|
72
|
+
class ExtractionFailure:
|
73
|
+
"""Represents a failure to extract data from a transition."""
|
74
|
+
|
75
|
+
# e.g., "GetUser"
|
76
|
+
id: str
|
77
|
+
case_id: str
|
78
|
+
# e.g., "POST /users"
|
79
|
+
source: str
|
80
|
+
# e.g., "GET /users/{userId}"
|
81
|
+
target: str
|
82
|
+
# e.g., "userId"
|
83
|
+
parameter_name: str
|
84
|
+
# e.g., "$response.body#/id"
|
85
|
+
expression: str
|
86
|
+
# Previous test cases in the chain, from newest to oldest
|
87
|
+
# Stored as a case + response pair
|
88
|
+
history: list[tuple[Case, Response]]
|
89
|
+
# The actual response that caused the failure
|
90
|
+
response: Response
|
91
|
+
error: Exception | None
|
92
|
+
|
93
|
+
__slots__ = ("id", "case_id", "source", "target", "parameter_name", "expression", "history", "response", "error")
|
94
|
+
|
95
|
+
def __eq__(self, other: object) -> bool:
|
96
|
+
assert isinstance(other, ExtractionFailure)
|
97
|
+
return (
|
98
|
+
self.source == other.source
|
99
|
+
and self.target == other.target
|
100
|
+
and self.id == other.id
|
101
|
+
and self.parameter_name == other.parameter_name
|
102
|
+
and self.expression == other.expression
|
103
|
+
)
|
104
|
+
|
105
|
+
def __hash__(self) -> int:
|
106
|
+
return hash(
|
107
|
+
(
|
108
|
+
self.source,
|
109
|
+
self.target,
|
110
|
+
self.id,
|
111
|
+
self.parameter_name,
|
112
|
+
self.expression,
|
113
|
+
)
|
114
|
+
)
|
115
|
+
|
116
|
+
|
76
117
|
@dataclass
|
77
118
|
class StepOutput:
|
78
119
|
"""Output from a single transition of a state machine."""
|
@@ -104,7 +145,11 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
104
145
|
super().__init__() # type: ignore
|
105
146
|
except InvalidDefinition as exc:
|
106
147
|
if "defines no rules" in str(exc):
|
107
|
-
|
148
|
+
if not self.schema.statistic.links.total:
|
149
|
+
message = "Schema contains no link definitions required for stateful testing"
|
150
|
+
else:
|
151
|
+
message = "All link definitions required for stateful testing are excluded by filters"
|
152
|
+
raise NoLinksFound(message) from None
|
108
153
|
raise
|
109
154
|
self.setup()
|
110
155
|
|
@@ -173,7 +218,7 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
173
218
|
kwargs = self.get_call_kwargs(input.case)
|
174
219
|
response = self.call(input.case, **kwargs)
|
175
220
|
self.after_call(response, input.case)
|
176
|
-
self.validate_response(response, input.case)
|
221
|
+
self.validate_response(response, input.case, **kwargs)
|
177
222
|
return StepOutput(response, input.case)
|
178
223
|
|
179
224
|
def before_call(self, case: Case) -> None:
|
@@ -267,7 +312,7 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
267
312
|
return {}
|
268
313
|
|
269
314
|
def validate_response(
|
270
|
-
self, response: Response, case: Case, additional_checks: list[CheckFunction] | None = None
|
315
|
+
self, response: Response, case: Case, additional_checks: list[CheckFunction] | None = None, **kwargs: Any
|
271
316
|
) -> None:
|
272
317
|
"""Validate an API response.
|
273
318
|
|
@@ -299,4 +344,4 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
299
344
|
all provided checks rather than only the first encountered exception.
|
300
345
|
"""
|
301
346
|
__tracebackhide__ = True
|
302
|
-
case.validate_response(response, additional_checks=additional_checks)
|
347
|
+
case.validate_response(response, additional_checks=additional_checks, transport_kwargs=kwargs)
|
schemathesis/pytest/lazy.py
CHANGED
@@ -168,7 +168,7 @@ class LazySchema:
|
|
168
168
|
for result in tests:
|
169
169
|
if isinstance(result, Ok):
|
170
170
|
operation, sub_test = result.ok()
|
171
|
-
subtests.item._nodeid = f"{node_id}[{operation.method.upper()} {operation.
|
171
|
+
subtests.item._nodeid = f"{node_id}[{operation.method.upper()} {operation.path}]"
|
172
172
|
run_subtest(operation, fixtures, sub_test, subtests)
|
173
173
|
else:
|
174
174
|
_schema_error(subtests, result.err(), node_id)
|
@@ -236,8 +236,7 @@ SEPARATOR = "\n===================="
|
|
236
236
|
def _schema_error(subtests: SubTests, error: InvalidSchema, node_id: str) -> None:
|
237
237
|
"""Run a failing test, that will show the underlying problem."""
|
238
238
|
sub_test = error.as_failing_test_function()
|
239
|
-
|
240
|
-
kwargs = {"path": error.full_path}
|
239
|
+
kwargs = {"path": error.path}
|
241
240
|
if error.method:
|
242
241
|
kwargs["method"] = error.method.upper()
|
243
242
|
subtests.item._nodeid = _get_partial_node_name(node_id, **kwargs)
|
schemathesis/pytest/plugin.py
CHANGED
@@ -149,11 +149,10 @@ class SchemathesisCase(PyCollector):
|
|
149
149
|
error = result.err()
|
150
150
|
funcobj = error.as_failing_test_function()
|
151
151
|
name = self.name
|
152
|
-
# `full_path` is always available in this case
|
153
152
|
if error.method:
|
154
|
-
name += f"[{error.method.upper()} {error.
|
153
|
+
name += f"[{error.method.upper()} {error.path}]"
|
155
154
|
else:
|
156
|
-
name += f"[{error.
|
155
|
+
name += f"[{error.path}]"
|
157
156
|
|
158
157
|
cls = self._get_class_parent()
|
159
158
|
definition: FunctionDefinition = FunctionDefinition.from_parent(
|
schemathesis/schemas.py
CHANGED
@@ -619,7 +619,7 @@ class APIOperation(Generic[P]):
|
|
619
619
|
|
620
620
|
def __post_init__(self) -> None:
|
621
621
|
if self.label is None:
|
622
|
-
self.label = f"{self.method.upper()} {self.
|
622
|
+
self.label = f"{self.method.upper()} {self.path}" # type: ignore
|
623
623
|
|
624
624
|
@property
|
625
625
|
def full_path(self) -> str:
|