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.
Files changed (29) hide show
  1. schemathesis/cli/commands/run/__init__.py +15 -45
  2. schemathesis/cli/commands/run/checks.py +2 -3
  3. schemathesis/cli/commands/run/context.py +30 -17
  4. schemathesis/cli/commands/run/executor.py +1 -0
  5. schemathesis/cli/commands/run/handlers/output.py +168 -88
  6. schemathesis/cli/commands/run/hypothesis.py +7 -45
  7. schemathesis/core/__init__.py +7 -1
  8. schemathesis/engine/config.py +2 -2
  9. schemathesis/engine/core.py +11 -1
  10. schemathesis/engine/events.py +7 -0
  11. schemathesis/engine/phases/__init__.py +16 -4
  12. schemathesis/engine/phases/unit/__init__.py +77 -52
  13. schemathesis/engine/phases/unit/_executor.py +14 -12
  14. schemathesis/engine/phases/unit/_pool.py +8 -0
  15. schemathesis/experimental/__init__.py +0 -6
  16. schemathesis/generation/hypothesis/builder.py +222 -97
  17. schemathesis/openapi/checks.py +3 -1
  18. schemathesis/pytest/lazy.py +41 -2
  19. schemathesis/pytest/plugin.py +2 -1
  20. schemathesis/specs/openapi/checks.py +1 -1
  21. schemathesis/specs/openapi/examples.py +39 -25
  22. schemathesis/specs/openapi/patterns.py +39 -7
  23. schemathesis/specs/openapi/serialization.py +14 -0
  24. schemathesis/transport/requests.py +10 -1
  25. {schemathesis-4.0.0a4.dist-info → schemathesis-4.0.0a6.dist-info}/METADATA +47 -91
  26. {schemathesis-4.0.0a4.dist-info → schemathesis-4.0.0a6.dist-info}/RECORD +29 -29
  27. {schemathesis-4.0.0a4.dist-info → schemathesis-4.0.0a6.dist-info}/licenses/LICENSE +1 -1
  28. {schemathesis-4.0.0a4.dist-info → schemathesis-4.0.0a6.dist-info}/WHEEL +0 -0
  29. {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, producer=producer, worker_factory=worker_task, ctx=engine, suite_id=suite_started.id
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=PhaseName.UNIT_TESTING)
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(*, events_queue: Queue, producer: TaskProducer, ctx: EngineContext, suite_id: uuid.UUID) -> None:
90
- from hypothesis.errors import HypothesisWarning
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
- test_function = create_test(
108
- operation=operation,
109
- test_func=test_func,
110
- config=HypothesisTestConfig(
111
- settings=ctx.config.execution.hypothesis_settings,
112
- seed=ctx.config.execution.seed,
113
- generation=ctx.config.execution.generation,
114
- as_strategy_kwargs=as_strategy_kwargs,
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(operation=operation, test_function=test_function, ctx=ctx, suite_id=suite_id):
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
- if error.method:
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=PhaseName.UNIT_TESTING))
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
- *, operation: APIOperation, test_function: Callable, ctx: EngineContext, suite_id: uuid.UUID
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
- # To simplify connecting `before` and `after` events in external systems
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=PhaseName.UNIT_TESTING,
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=PhaseName.UNIT_TESTING)
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.no_failfast
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
- no_failfast=ctx.config.execution.no_failfast,
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
- no_failfast: bool,
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 no_failfast:
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",