Skip to content

Commit bff1995

Browse files
committed
Start implementation of multiple example tables
1 parent 4d995b2 commit bff1995

File tree

5 files changed

+81
-40
lines changed

5 files changed

+81
-40
lines changed

src/pytest_bdd/generation.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def generate_code(features: list[Feature], scenarios: list[ScenarioTemplate], st
6262
"""Generate test code for the given filenames."""
6363
grouped_steps = group_steps(steps)
6464
template = template_lookup.get_template("test.py.mak")
65+
6566
code = template.render(
6667
features=features,
6768
scenarios=scenarios,
@@ -83,42 +84,39 @@ def show_missing_code(config: Config) -> int:
8384
def print_missing_code(scenarios: list[ScenarioTemplate], steps: list[Step]) -> None:
8485
"""Print missing code with TerminalWriter."""
8586
tw = TerminalWriter()
86-
scenario = step = None
8787

8888
for scenario in scenarios:
8989
tw.line()
9090
tw.line(
91-
'Scenario "{scenario.name}" is not bound to any test in the feature "{scenario.feature.name}"'
92-
" in the file {scenario.feature.filename}:{scenario.line_number}".format(scenario=scenario),
91+
f'Scenario "{scenario.name}" is not bound to any test in the feature "{scenario.feature.name}"'
92+
f" in the file {scenario.feature.filename}:{scenario.line_number}",
9393
red=True,
9494
)
9595

96-
if scenario:
97-
tw.sep("-", red=True)
96+
tw.sep("-", red=True)
9897

9998
for step in steps:
10099
tw.line()
101100
if step.scenario is not None:
102101
tw.line(
103-
"""Step {step} is not defined in the scenario "{step.scenario.name}" in the feature"""
104-
""" "{step.scenario.feature.name}" in the file"""
105-
""" {step.scenario.feature.filename}:{step.line_number}""".format(step=step),
102+
f"""Step {step} is not defined in the scenario "{step.scenario.name}" in the feature"""
103+
f""" "{step.scenario.feature.name}" in the file"""
104+
f" {step.scenario.feature.filename}:{step.line_number}",
106105
red=True,
107106
)
108107
elif step.background is not None:
109108
tw.line(
110-
"""Step {step} is not defined in the background of the feature"""
111-
""" "{step.background.feature.name}" in the file"""
112-
""" {step.background.feature.filename}:{step.line_number}""".format(step=step),
109+
f"""Step {step} is not defined in the background of the feature"""
110+
f""" "{step.background.feature.name}" in the file"""
111+
f" {step.background.feature.filename}:{step.line_number}",
113112
red=True,
114113
)
115114

116-
if step:
117-
tw.sep("-", red=True)
118-
119-
tw.line("Please place the code above to the test file(s):")
115+
tw.sep("-", red=True)
116+
tw.line("Please place the code above into the test file(s):")
120117
tw.line()
121118

119+
# Group features and generate the test code
122120
features = sorted(
123121
(scenario.feature for scenario in scenarios), key=lambda feature: feature.name or feature.filename
124122
)

src/pytest_bdd/gherkin_parser.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ class ExamplesTable:
107107
name: str | None = None
108108
table_header: Row | None = None
109109
table_body: list[Row] | None = field(default_factory=list)
110+
tags: list[str] = field(default_factory=set)
110111

111112
@classmethod
112113
def from_dict(cls, data: dict[str, Any]) -> Self:
@@ -115,6 +116,7 @@ def from_dict(cls, data: dict[str, Any]) -> Self:
115116
name=data.get("name"),
116117
table_header=Row.from_dict(data["tableHeader"]) if data.get("tableHeader") else None,
117118
table_body=[Row.from_dict(row) for row in data.get("tableBody", [])],
119+
tags=[Tag.from_dict(tag) for tag in data["tags"]]
118120
)
119121

120122

src/pytest_bdd/parser.py

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ class ScenarioTemplate:
124124
description: str | None = None
125125
tags: set[str] = field(default_factory=set)
126126
_steps: list[Step] = field(init=False, default_factory=list)
127-
examples: Examples | None = field(default_factory=Examples)
127+
examples: list[Examples] = field(default_factory=list)
128128

129129
def add_step(self, step: Step) -> None:
130130
"""Add a step to the scenario.
@@ -144,19 +144,20 @@ def steps(self) -> list[Step]:
144144
"""
145145
return (self.feature.background.steps if self.feature.background else []) + self._steps
146146

147-
def render(self, context: Mapping[str, Any]) -> Scenario:
147+
def render(self, context: Mapping[str, Any]) -> list[Scenario]:
148148
"""Render the scenario with the given context.
149149
150150
Args:
151151
context (Mapping[str, Any]): The context for rendering steps.
152152
153153
Returns:
154-
Scenario: A Scenario object with steps rendered based on the context.
154+
list[Scenario]: A list of Scenario objects with steps rendered based on the context.
155155
"""
156-
background_steps = self.feature.background.steps if self.feature.background else []
157-
scenario_steps = [
156+
scenarios = []
157+
base_context = context or {}
158+
base_steps = [
158159
Step(
159-
name=step.render(context),
160+
name=step.render(base_context),
160161
type=step.type,
161162
indent=step.indent,
162163
line_number=step.line_number,
@@ -166,16 +167,52 @@ def render(self, context: Mapping[str, Any]) -> Scenario:
166167
)
167168
for step in self._steps
168169
]
169-
steps = background_steps + scenario_steps
170-
return Scenario(
171-
feature=self.feature,
172-
keyword=self.keyword,
173-
name=self.name,
174-
line_number=self.line_number,
175-
steps=steps,
176-
tags=self.tags,
177-
description=self.description,
178-
)
170+
background_steps = self.feature.background.steps if self.feature.background else []
171+
172+
if not self.examples:
173+
# Render a single scenario without examples
174+
scenarios.append(
175+
Scenario(
176+
feature=self.feature,
177+
keyword=self.keyword,
178+
name=self.name,
179+
line_number=self.line_number,
180+
steps=background_steps + base_steps,
181+
tags=self.tags,
182+
description=self.description,
183+
)
184+
)
185+
else:
186+
# Render multiple scenarios with each example context
187+
for examples in self.examples:
188+
for example_context in examples.as_contexts():
189+
full_context = {**base_context, **example_context}
190+
example_steps = [
191+
Step(
192+
name=step.render(full_context),
193+
type=step.type,
194+
indent=step.indent,
195+
line_number=step.line_number,
196+
keyword=step.keyword,
197+
datatable=step.datatable,
198+
docstring=step.docstring,
199+
)
200+
for step in self._steps
201+
]
202+
example_tags = self.tags.union(examples.tags if hasattr(examples, "tags") else set())
203+
scenarios.append(
204+
Scenario(
205+
feature=self.feature,
206+
keyword=self.keyword,
207+
name=self.name,
208+
line_number=self.line_number,
209+
steps=background_steps + example_steps,
210+
tags=example_tags,
211+
description=self.description,
212+
)
213+
)
214+
215+
return scenarios
179216

180217

181218
@dataclass(eq=False)
@@ -401,6 +438,7 @@ def parse_scenario(self, scenario_data: GherkinScenario, feature: Feature) -> Sc
401438
for step in self.parse_steps(scenario_data.steps):
402439
scenario.add_step(step)
403440

441+
# Loop over multiple example tables if they exist
404442
for example_data in scenario_data.examples:
405443
examples = Examples(
406444
line_number=example_data.location.line,
@@ -413,7 +451,7 @@ def parse_scenario(self, scenario_data: GherkinScenario, feature: Feature) -> Sc
413451
for row in example_data.table_body:
414452
values = [cell.value or "" for cell in row.cells]
415453
examples.add_example(values)
416-
scenario.examples = examples
454+
scenario.examples.append(examples)
417455

418456
return scenario
419457

src/pytest_bdd/plugin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def _pytest_bdd_example() -> dict:
4949
5050
If no outline is used, we just return an empty dict to render
5151
the current template without any actual variable.
52-
Otherwise pytest_bdd will add all the context variables in this fixture
52+
Otherwise, pytest_bdd will add all the context variables in this fixture
5353
from the example definitions in the feature file.
5454
"""
5555
return {}

src/pytest_bdd/scenario.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,9 @@ def decorator(*args: Callable[P, T]) -> Callable[P, T]:
276276
@pytest.mark.usefixtures(*func_args)
277277
def scenario_wrapper(request: FixtureRequest, _pytest_bdd_example: dict[str, str]) -> Any:
278278
__tracebackhide__ = True
279-
scenario = templated_scenario.render(_pytest_bdd_example)
280-
_execute_scenario(feature, scenario, request)
279+
rendered_scenarios = templated_scenario.render(_pytest_bdd_example)
280+
for scenario in rendered_scenarios:
281+
_execute_scenario(feature, scenario, request)
281282
fixture_values = [request.getfixturevalue(arg) for arg in func_args]
282283
return fn(*fixture_values)
283284

@@ -289,7 +290,7 @@ def scenario_wrapper(request: FixtureRequest, _pytest_bdd_example: dict[str, str
289290
example_parametrizations,
290291
)(scenario_wrapper)
291292

292-
for tag in templated_scenario.tags.union(feature.tags):
293+
for tag in templated_scenario.tags | feature.tags:
293294
config = CONFIG_STACK[-1]
294295
config.hook.pytest_bdd_apply_tag(tag=tag, function=scenario_wrapper)
295296

@@ -303,12 +304,14 @@ def scenario_wrapper(request: FixtureRequest, _pytest_bdd_example: dict[str, str
303304
def collect_example_parametrizations(
304305
templated_scenario: ScenarioTemplate,
305306
) -> list[ParameterSet] | None:
306-
if templated_scenario.examples is None:
307+
if not templated_scenario.examples:
307308
return None
308-
if contexts := list(templated_scenario.examples.as_contexts()):
309-
return [pytest.param(context, id="-".join(context.values())) for context in contexts]
310-
else:
309+
contexts = []
310+
for _examples in templated_scenario.examples:
311+
contexts.extend(_examples.as_contexts())
312+
if not contexts:
311313
return None
314+
return [pytest.param(context, id="-".join(context.values())) for context in contexts]
312315

313316

314317
def scenario(

0 commit comments

Comments
 (0)