schemathesis 4.0.0a4__py3-none-any.whl → 4.0.0a6__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/commands/run/__init__.py +15 -45
- schemathesis/cli/commands/run/checks.py +2 -3
- schemathesis/cli/commands/run/context.py +30 -17
- schemathesis/cli/commands/run/executor.py +1 -0
- schemathesis/cli/commands/run/handlers/output.py +168 -88
- schemathesis/cli/commands/run/hypothesis.py +7 -45
- schemathesis/core/__init__.py +7 -1
- schemathesis/engine/config.py +2 -2
- schemathesis/engine/core.py +11 -1
- schemathesis/engine/events.py +7 -0
- schemathesis/engine/phases/__init__.py +16 -4
- schemathesis/engine/phases/unit/__init__.py +77 -52
- schemathesis/engine/phases/unit/_executor.py +14 -12
- schemathesis/engine/phases/unit/_pool.py +8 -0
- schemathesis/experimental/__init__.py +0 -6
- schemathesis/generation/hypothesis/builder.py +222 -97
- schemathesis/openapi/checks.py +3 -1
- schemathesis/pytest/lazy.py +41 -2
- schemathesis/pytest/plugin.py +2 -1
- schemathesis/specs/openapi/checks.py +1 -1
- schemathesis/specs/openapi/examples.py +39 -25
- schemathesis/specs/openapi/patterns.py +39 -7
- schemathesis/specs/openapi/serialization.py +14 -0
- schemathesis/transport/requests.py +10 -1
- {schemathesis-4.0.0a4.dist-info → schemathesis-4.0.0a6.dist-info}/METADATA +47 -91
- {schemathesis-4.0.0a4.dist-info → schemathesis-4.0.0a6.dist-info}/RECORD +29 -29
- {schemathesis-4.0.0a4.dist-info → schemathesis-4.0.0a6.dist-info}/licenses/LICENSE +1 -1
- {schemathesis-4.0.0a4.dist-info → schemathesis-4.0.0a6.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a4.dist-info → schemathesis-4.0.0a6.dist-info}/entry_points.txt +0 -0
@@ -11,11 +11,12 @@ import warnings
|
|
11
11
|
from queue import Queue
|
12
12
|
from typing import TYPE_CHECKING, Any
|
13
13
|
|
14
|
+
from schemathesis.core.errors import InvalidSchema
|
14
15
|
from schemathesis.core.result import Ok
|
15
16
|
from schemathesis.engine import Status, events
|
16
17
|
from schemathesis.engine.phases import PhaseName, PhaseSkipReason
|
17
18
|
from schemathesis.engine.recorder import ScenarioRecorder
|
18
|
-
from schemathesis.generation.hypothesis.builder import HypothesisTestConfig
|
19
|
+
from schemathesis.generation.hypothesis.builder import HypothesisTestConfig, HypothesisTestMode
|
19
20
|
from schemathesis.generation.hypothesis.reporting import ignore_hypothesis_output
|
20
21
|
|
21
22
|
from ._pool import TaskProducer, WorkerPool
|
@@ -34,6 +35,12 @@ def execute(engine: EngineContext, phase: Phase) -> events.EventGenerator:
|
|
34
35
|
Implemented as a producer-consumer pattern via a task queue.
|
35
36
|
The main thread provides an iterator over API operations and worker threads create test functions and run them.
|
36
37
|
"""
|
38
|
+
if phase.name == PhaseName.EXAMPLES:
|
39
|
+
mode = HypothesisTestMode.EXAMPLES
|
40
|
+
elif phase.name == PhaseName.COVERAGE:
|
41
|
+
mode = HypothesisTestMode.COVERAGE
|
42
|
+
else:
|
43
|
+
mode = HypothesisTestMode.FUZZING
|
37
44
|
producer = TaskProducer(engine)
|
38
45
|
workers_num = engine.config.execution.workers_num
|
39
46
|
|
@@ -45,7 +52,13 @@ def execute(engine: EngineContext, phase: Phase) -> events.EventGenerator:
|
|
45
52
|
is_executed = False
|
46
53
|
|
47
54
|
with WorkerPool(
|
48
|
-
workers_num=workers_num,
|
55
|
+
workers_num=workers_num,
|
56
|
+
producer=producer,
|
57
|
+
worker_factory=worker_task,
|
58
|
+
ctx=engine,
|
59
|
+
mode=mode,
|
60
|
+
phase=phase.name,
|
61
|
+
suite_id=suite_started.id,
|
49
62
|
) as pool:
|
50
63
|
try:
|
51
64
|
while True:
|
@@ -74,7 +87,7 @@ def execute(engine: EngineContext, phase: Phase) -> events.EventGenerator:
|
|
74
87
|
except KeyboardInterrupt:
|
75
88
|
engine.stop()
|
76
89
|
status = Status.INTERRUPTED
|
77
|
-
yield events.Interrupted(phase=
|
90
|
+
yield events.Interrupted(phase=phase.name)
|
78
91
|
|
79
92
|
if not is_executed:
|
80
93
|
phase.skip_reason = PhaseSkipReason.NOTHING_TO_TEST
|
@@ -86,13 +99,52 @@ def execute(engine: EngineContext, phase: Phase) -> events.EventGenerator:
|
|
86
99
|
yield events.PhaseFinished(phase=phase, status=status, payload=None)
|
87
100
|
|
88
101
|
|
89
|
-
def worker_task(
|
90
|
-
|
102
|
+
def worker_task(
|
103
|
+
*,
|
104
|
+
events_queue: Queue,
|
105
|
+
producer: TaskProducer,
|
106
|
+
ctx: EngineContext,
|
107
|
+
mode: HypothesisTestMode,
|
108
|
+
phase: PhaseName,
|
109
|
+
suite_id: uuid.UUID,
|
110
|
+
) -> None:
|
111
|
+
from hypothesis.errors import HypothesisWarning, InvalidArgument
|
91
112
|
|
92
113
|
from schemathesis.generation.hypothesis.builder import create_test
|
93
114
|
|
94
115
|
from ._executor import run_test, test_func
|
95
116
|
|
117
|
+
def on_error(error: Exception, *, method: str | None = None, path: str | None = None) -> None:
|
118
|
+
if method and path:
|
119
|
+
label = f"{method.upper()} {path}"
|
120
|
+
scenario_started = events.ScenarioStarted(label=label, phase=phase, suite_id=suite_id)
|
121
|
+
events_queue.put(scenario_started)
|
122
|
+
|
123
|
+
events_queue.put(events.NonFatalError(error=error, phase=phase, label=label, related_to_operation=True))
|
124
|
+
|
125
|
+
events_queue.put(
|
126
|
+
events.ScenarioFinished(
|
127
|
+
id=scenario_started.id,
|
128
|
+
suite_id=suite_id,
|
129
|
+
phase=phase,
|
130
|
+
label=label,
|
131
|
+
status=Status.ERROR,
|
132
|
+
recorder=ScenarioRecorder(label="Error"),
|
133
|
+
elapsed_time=0.0,
|
134
|
+
skip_reason=None,
|
135
|
+
is_final=True,
|
136
|
+
)
|
137
|
+
)
|
138
|
+
else:
|
139
|
+
events_queue.put(
|
140
|
+
events.NonFatalError(
|
141
|
+
error=error,
|
142
|
+
phase=phase,
|
143
|
+
label=path or "-",
|
144
|
+
related_to_operation=False,
|
145
|
+
)
|
146
|
+
)
|
147
|
+
|
96
148
|
warnings.filterwarnings("ignore", message="The recursion limit will not be reset", category=HypothesisWarning)
|
97
149
|
with ignore_hypothesis_output():
|
98
150
|
try:
|
@@ -104,61 +156,34 @@ def worker_task(*, events_queue: Queue, producer: TaskProducer, ctx: EngineConte
|
|
104
156
|
if isinstance(result, Ok):
|
105
157
|
operation = result.ok()
|
106
158
|
as_strategy_kwargs = get_strategy_kwargs(ctx, operation)
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
159
|
+
try:
|
160
|
+
test_function = create_test(
|
161
|
+
operation=operation,
|
162
|
+
test_func=test_func,
|
163
|
+
config=HypothesisTestConfig(
|
164
|
+
modes=[mode],
|
165
|
+
settings=ctx.config.execution.hypothesis_settings,
|
166
|
+
seed=ctx.config.execution.seed,
|
167
|
+
generation=ctx.config.execution.generation,
|
168
|
+
as_strategy_kwargs=as_strategy_kwargs,
|
169
|
+
),
|
170
|
+
)
|
171
|
+
except (InvalidSchema, InvalidArgument) as exc:
|
172
|
+
on_error(exc, method=operation.method, path=operation.path)
|
173
|
+
continue
|
117
174
|
|
118
175
|
# The test is blocking, meaning that even if CTRL-C comes to the main thread, this tasks will continue
|
119
176
|
# executing. However, as we set a stop event, it will be checked before the next network request.
|
120
177
|
# However, this is still suboptimal, as there could be slow requests and they will block for longer
|
121
|
-
for event in run_test(
|
178
|
+
for event in run_test(
|
179
|
+
operation=operation, test_function=test_function, ctx=ctx, phase=phase, suite_id=suite_id
|
180
|
+
):
|
122
181
|
events_queue.put(event)
|
123
182
|
else:
|
124
183
|
error = result.err()
|
125
|
-
|
126
|
-
label = f"{error.method.upper()} {error.path}"
|
127
|
-
scenario_started = events.ScenarioStarted(
|
128
|
-
label=label, phase=PhaseName.UNIT_TESTING, suite_id=suite_id
|
129
|
-
)
|
130
|
-
events_queue.put(scenario_started)
|
131
|
-
|
132
|
-
events_queue.put(
|
133
|
-
events.NonFatalError(
|
134
|
-
error=error, phase=PhaseName.UNIT_TESTING, label=label, related_to_operation=True
|
135
|
-
)
|
136
|
-
)
|
137
|
-
|
138
|
-
events_queue.put(
|
139
|
-
events.ScenarioFinished(
|
140
|
-
id=scenario_started.id,
|
141
|
-
suite_id=suite_id,
|
142
|
-
phase=PhaseName.UNIT_TESTING,
|
143
|
-
label=label,
|
144
|
-
status=Status.ERROR,
|
145
|
-
recorder=ScenarioRecorder(label="Error"),
|
146
|
-
elapsed_time=0.0,
|
147
|
-
skip_reason=None,
|
148
|
-
is_final=True,
|
149
|
-
)
|
150
|
-
)
|
151
|
-
else:
|
152
|
-
events_queue.put(
|
153
|
-
events.NonFatalError(
|
154
|
-
error=error,
|
155
|
-
phase=PhaseName.UNIT_TESTING,
|
156
|
-
label=error.path,
|
157
|
-
related_to_operation=False,
|
158
|
-
)
|
159
|
-
)
|
184
|
+
on_error(error, method=error.method, path=error.path)
|
160
185
|
except KeyboardInterrupt:
|
161
|
-
events_queue.put(events.Interrupted(phase=
|
186
|
+
events_queue.put(events.Interrupted(phase=phase))
|
162
187
|
|
163
188
|
|
164
189
|
def get_strategy_kwargs(ctx: EngineContext, operation: APIOperation) -> dict[str, Any]:
|
@@ -52,13 +52,17 @@ if TYPE_CHECKING:
|
|
52
52
|
|
53
53
|
|
54
54
|
def run_test(
|
55
|
-
*,
|
55
|
+
*,
|
56
|
+
operation: APIOperation,
|
57
|
+
test_function: Callable,
|
58
|
+
ctx: EngineContext,
|
59
|
+
phase: PhaseName,
|
60
|
+
suite_id: uuid.UUID,
|
56
61
|
) -> events.EventGenerator:
|
57
62
|
"""A single test run with all error handling needed."""
|
58
63
|
import hypothesis.errors
|
59
64
|
|
60
|
-
|
61
|
-
scenario_started = events.ScenarioStarted(label=operation.label, phase=PhaseName.UNIT_TESTING, suite_id=suite_id)
|
65
|
+
scenario_started = events.ScenarioStarted(label=operation.label, phase=phase, suite_id=suite_id)
|
62
66
|
yield scenario_started
|
63
67
|
errors: list[Exception] = []
|
64
68
|
skip_reason = None
|
@@ -66,15 +70,13 @@ def run_test(
|
|
66
70
|
recorder = ScenarioRecorder(label=operation.label)
|
67
71
|
|
68
72
|
def non_fatal_error(error: Exception) -> events.NonFatalError:
|
69
|
-
return events.NonFatalError(
|
70
|
-
error=error, phase=PhaseName.UNIT_TESTING, label=operation.label, related_to_operation=True
|
71
|
-
)
|
73
|
+
return events.NonFatalError(error=error, phase=phase, label=operation.label, related_to_operation=True)
|
72
74
|
|
73
75
|
def scenario_finished(status: Status) -> events.ScenarioFinished:
|
74
76
|
return events.ScenarioFinished(
|
75
77
|
id=scenario_started.id,
|
76
78
|
suite_id=suite_id,
|
77
|
-
phase=
|
79
|
+
phase=phase,
|
78
80
|
label=operation.label,
|
79
81
|
recorder=recorder,
|
80
82
|
status=status,
|
@@ -125,7 +127,7 @@ def run_test(
|
|
125
127
|
yield non_fatal_error(hypothesis.errors.Unsatisfiable("Failed to generate test cases for this API operation"))
|
126
128
|
except KeyboardInterrupt:
|
127
129
|
yield scenario_finished(Status.INTERRUPTED)
|
128
|
-
yield events.Interrupted(phase=
|
130
|
+
yield events.Interrupted(phase=phase)
|
129
131
|
return
|
130
132
|
except AssertionError as exc: # May come from `hypothesis-jsonschema` or `hypothesis`
|
131
133
|
status = Status.ERROR
|
@@ -178,7 +180,7 @@ def run_test(
|
|
178
180
|
yield non_fatal_error(exc)
|
179
181
|
if (
|
180
182
|
status == Status.SUCCESS
|
181
|
-
and ctx.config.execution.
|
183
|
+
and ctx.config.execution.continue_on_failure
|
182
184
|
and any(check.status == Status.FAILURE for checks in recorder.checks.values() for check in checks)
|
183
185
|
):
|
184
186
|
status = Status.FAILURE
|
@@ -283,7 +285,7 @@ def test_func(*, ctx: EngineContext, case: Case, recorder: ScenarioRecorder) ->
|
|
283
285
|
ctx=ctx.get_check_context(recorder),
|
284
286
|
checks=ctx.config.execution.checks,
|
285
287
|
response=response,
|
286
|
-
|
288
|
+
continue_on_failure=ctx.config.execution.continue_on_failure,
|
287
289
|
recorder=recorder,
|
288
290
|
)
|
289
291
|
|
@@ -294,7 +296,7 @@ def validate_response(
|
|
294
296
|
ctx: CheckContext,
|
295
297
|
checks: Iterable[CheckFunction],
|
296
298
|
response: Response,
|
297
|
-
|
299
|
+
continue_on_failure: bool,
|
298
300
|
recorder: ScenarioRecorder,
|
299
301
|
) -> None:
|
300
302
|
failures = set()
|
@@ -321,5 +323,5 @@ def validate_response(
|
|
321
323
|
on_success=on_success,
|
322
324
|
)
|
323
325
|
|
324
|
-
if failures and not
|
326
|
+
if failures and not continue_on_failure:
|
325
327
|
raise FailureGroup(list(failures)) from None
|
@@ -7,9 +7,11 @@ from types import TracebackType
|
|
7
7
|
from typing import TYPE_CHECKING, Callable
|
8
8
|
|
9
9
|
from schemathesis.core.result import Result
|
10
|
+
from schemathesis.engine.phases import PhaseName
|
10
11
|
|
11
12
|
if TYPE_CHECKING:
|
12
13
|
from schemathesis.engine.context import EngineContext
|
14
|
+
from schemathesis.generation.hypothesis.builder import HypothesisTestMode
|
13
15
|
|
14
16
|
|
15
17
|
class TaskProducer:
|
@@ -34,12 +36,16 @@ class WorkerPool:
|
|
34
36
|
producer: TaskProducer,
|
35
37
|
worker_factory: Callable,
|
36
38
|
ctx: EngineContext,
|
39
|
+
mode: HypothesisTestMode,
|
40
|
+
phase: PhaseName,
|
37
41
|
suite_id: uuid.UUID,
|
38
42
|
) -> None:
|
39
43
|
self.workers_num = workers_num
|
40
44
|
self.producer = producer
|
41
45
|
self.worker_factory = worker_factory
|
42
46
|
self.ctx = ctx
|
47
|
+
self.mode = mode
|
48
|
+
self.phase = phase
|
43
49
|
self.suite_id = suite_id
|
44
50
|
self.workers: list[threading.Thread] = []
|
45
51
|
self.events_queue: Queue = Queue()
|
@@ -51,6 +57,8 @@ class WorkerPool:
|
|
51
57
|
target=self.worker_factory,
|
52
58
|
kwargs={
|
53
59
|
"ctx": self.ctx,
|
60
|
+
"mode": self.mode,
|
61
|
+
"phase": self.phase,
|
54
62
|
"events_queue": self.events_queue,
|
55
63
|
"producer": self.producer,
|
56
64
|
"suite_id": self.suite_id,
|
@@ -64,12 +64,6 @@ class ExperimentSet:
|
|
64
64
|
|
65
65
|
ENV_PREFIX = "SCHEMATHESIS_EXPERIMENTAL"
|
66
66
|
GLOBAL_EXPERIMENTS = ExperimentSet()
|
67
|
-
COVERAGE_PHASE = GLOBAL_EXPERIMENTS.create_experiment(
|
68
|
-
name="Coverage phase",
|
69
|
-
env_var="COVERAGE_PHASE",
|
70
|
-
description="Generate covering test cases",
|
71
|
-
discussion_url="https://github.com/schemathesis/schemathesis/discussions/2418",
|
72
|
-
)
|
73
67
|
POSITIVE_DATA_ACCEPTANCE = GLOBAL_EXPERIMENTS.create_experiment(
|
74
68
|
name="Positive Data Acceptance",
|
75
69
|
env_var="POSITIVE_DATA_ACCEPTANCE",
|