Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Peo 7674 add datatables #596

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
53 changes: 38 additions & 15 deletions src/pytest_bdd/parser.py
Expand Up @@ -107,11 +107,11 @@ def parse_feature(basedir: str, filename: str, encoding: str = "utf-8") -> Featu
description: list[str] = []
step = None
multiline_step = False
prev_line = None

with open(abs_filename, encoding=encoding) as f:
content = f.read()

all_lines = content.splitlines()
for line_number, line in enumerate(content.splitlines(), start=1):
unindented_line = line.lstrip()
line_indent = len(line) - len(unindented_line)
Expand Down Expand Up @@ -140,7 +140,7 @@ def parse_feature(basedir: str, filename: str, encoding: str = "utf-8") -> Featu
if prev_mode is None or prev_mode == types.TAG:
_, feature.name = parse_line(clean_line)
feature.line_number = line_number
feature.tags = get_tags(prev_line)
feature.tags = get_tags(all_lines, line_number)
elif prev_mode == types.FEATURE:
description.append(clean_line)
else:
Expand All @@ -157,7 +157,7 @@ def parse_feature(basedir: str, filename: str, encoding: str = "utf-8") -> Featu
keyword, parsed_line = parse_line(clean_line)

if mode in [types.SCENARIO, types.SCENARIO_OUTLINE]:
tags = get_tags(prev_line)
tags = get_tags(all_lines, line_number)
scenario = ScenarioTemplate(
feature=feature,
name=parsed_line,
Expand All @@ -171,6 +171,7 @@ def parse_feature(basedir: str, filename: str, encoding: str = "utf-8") -> Featu
elif mode == types.EXAMPLES:
mode = types.EXAMPLES_HEADERS
scenario.examples.line_number = line_number
scenario.examples.tags = get_tags(all_lines, line_number)
elif mode == types.EXAMPLES_HEADERS:
scenario.examples.set_param_names([l for l in split_line(parsed_line) if l])
mode = types.EXAMPLE_LINE
Expand All @@ -183,7 +184,6 @@ def parse_feature(basedir: str, filename: str, encoding: str = "utf-8") -> Featu
else:
scenario = cast(ScenarioTemplate, scenario)
scenario.add_step(step)
prev_line = clean_line

feature.description = "\n".join(description).strip()
return feature
Expand Down Expand Up @@ -264,6 +264,7 @@ class Step:
scenario: ScenarioTemplate | None = field(init=False, default=None)
background: Background | None = field(init=False, default=None)
lines: list[str] = field(init=False, default_factory=list)
datatable: list[str] = field(init=False, default_factory=list)

def __init__(self, name: str, type: str, indent: int, line_number: int, keyword: str) -> None:
self.name = name
Expand All @@ -276,13 +277,20 @@ def __init__(self, name: str, type: str, indent: int, line_number: int, keyword:
self.scenario = None
self.background = None
self.lines = []
self.datatable = []

def add_line(self, line: str) -> None:
"""Add line to the multiple step.
"""Add line to the multiple step or add a line to the datatable.

:param str line: Line of text - the continuation of the step name.
:param str line: Line of text - the continuation of the step name or a datatable row.
"""
self.lines.append(line)
clean_line = line.strip()
if clean_line.startswith("|") and clean_line.endswith("|"):
# datatable line
clean_line = clean_line[1:-1]
self.datatable.append(clean_line.split("|"))
else:
self.lines.append(line)

@property
def name(self) -> str:
Expand Down Expand Up @@ -337,37 +345,52 @@ class Examples:

line_number: int | None = field(default=None)
name: str | None = field(default=None)
tags: set[str] = field(default_factory=set)

example_params: list[str] = field(init=False, default_factory=list)
example_tags: list[str] = field(init=False, default_factory=list)
examples: list[Sequence[str]] = field(init=False, default_factory=list)

def set_param_names(self, keys: Iterable[str]) -> None:
self.example_params = [str(key) for key in keys]

def add_example(self, values: Sequence[str]) -> None:
self.examples.append(values)
self.example_tags.append(self.tags)

def as_contexts(self) -> Iterable[dict[str, Any]]:
def as_contexts(self) -> Iterable[dict[str, Any], list[str]]:
if not self.examples:
return
header, rows, tags = self.example_params, self.examples, self.example_tags

header, rows = self.example_params, self.examples

for row in rows:
for index, row in enumerate(rows):
assert len(header) == len(row)
yield dict(zip(header, row))
yield dict(zip(header, row)), tags[index]

def __bool__(self) -> bool:
return bool(self.examples)


def get_tags(line: str | None) -> set[str]:
def get_tags(all_lines: list[str] | str | None, line_number: int = 0) -> set[str]:
"""Get tags out of the given line.

:param str line: Feature file text line.
:param int line_number: Starting line.

:return: List of tags.
"""
if not line or not line.strip().startswith("@"):
if isinstance(all_lines, str) or not all_lines:
if not all_lines or not all_lines.strip().startswith("@"):
return set()
return {tag.lstrip("@") for tag in all_lines.strip().split(" @") if len(tag) > 1}

total_tags = set[str]
line_offset = 2 # Used to denote the line containing the tags, above the current line
if not all_lines[line_number - line_offset] or not all_lines[line_number - line_offset].strip().startswith("@"):
return set()
return {tag.lstrip("@") for tag in line.strip().split(" @") if len(tag) > 1}
else:
while (line_number - 1) > 0 and all_lines[line_number - line_offset].strip().startswith("@"):
line_tags = {tag.lstrip("@") for tag in all_lines[line_number - 2].strip().split(" @") if len(tag) > 1}
total_tags = total_tags.union(line_tags)
line_number -= 1
return total_tags
10 changes: 8 additions & 2 deletions src/pytest_bdd/scenario.py
Expand Up @@ -243,14 +243,20 @@ def scenario_wrapper(request: FixtureRequest, _pytest_bdd_example: dict[str, str

def collect_example_parametrizations(
templated_scenario: ScenarioTemplate,
) -> list[ParameterSet] | None:
) -> tuple[list[ParameterSet], set[str]] | None:
# We need to evaluate these iterators and store them as lists, otherwise
# we won't be able to do the cartesian product later (the second iterator will be consumed)
contexts = list(templated_scenario.examples.as_contexts())
if not contexts:
return None
return [
pytest.param(context[0], id="-".join(context[0].values()), marks=_register_tags_as_marks(context[1]))
for context in contexts
]

return [pytest.param(context, id="-".join(context.values())) for context in contexts]

def _register_tags_as_marks(tags) -> list[pytest.mark]:
return [getattr(pytest.mark, marks) for marks in tags]


def scenario(
Expand Down
172 changes: 172 additions & 0 deletions tests/feature/test_tags.py
Expand Up @@ -67,6 +67,178 @@ def _():
assert result["deselected"] == 2


def test_multiline_tags(pytester):
"""Test tests selection by multiline tags."""
pytester.makefile(
".ini",
pytest=textwrap.dedent(
"""
[pytest]
markers =
feature_tag_1
feature_tag_2
scenario_tag_01
scenario_tag_02
scenario_tag_10
scenario_tag_20
scenario_tag_30
"""
),
)
pytester.makefile(
".feature",
test="""
@feature_tag_1
@feature_tag_2
Feature: Tags

@scenario_tag_01 @scenario_tag_02
Scenario: Tags
Given I have a bar

@scenario_tag_10
@scenario_tag_20
@scenario_tag_30
Scenario: Tags 2
Given I have a bar

""",
)
pytester.makepyfile(
"""
import pytest
from pytest_bdd import given, scenarios

@given('I have a bar')
def _():
return 'bar'

scenarios('test.feature')
"""
)
result = pytester.runpytest("-m", "scenario_tag_10 and not scenario_tag_01", "-vv")
outcomes = result.parseoutcomes()
assert outcomes["passed"] == 1
assert outcomes["deselected"] == 1

result = pytester.runpytest("-m", "scenario_tag_01 and not scenario_tag_10", "-vv").parseoutcomes()
assert result["passed"] == 1
assert result["deselected"] == 1

result = pytester.runpytest("-m", "feature_tag_1", "-vv").parseoutcomes()
assert result["passed"] == 2

result = pytester.runpytest("-m", "feature_tag_10", "-vv").parseoutcomes()
assert result["deselected"] == 2

result = pytester.runpytest("-m", " scenario_tag_02", "-vv").parseoutcomes()
assert result["passed"] == 1

result = pytester.runpytest("-m", " scenario_tag_10", "-vv").parseoutcomes()
assert result["passed"] == 1

result = pytester.runpytest("-m", " scenario_tag_20", "-vv").parseoutcomes()
assert result["passed"] == 1

result = pytester.runpytest("-m", " scenario_tag_30", "-vv").parseoutcomes()
assert result["passed"] == 1


def test_example_tags(pytester):
"""Test example selection by tags."""
pytester.makefile(
".ini",
pytest=textwrap.dedent(
"""
[pytest]
markers =
feature_tag_1
scenario_tag_01
example_tag_01
example_tag_02
example_tag_03
"""
),
)
pytester.makefile(
".feature",
test="""
@feature_tag_1
Feature: Tags
@scenario_tag_01
Scenario Outline: Outlined with empty example values
Given there are <start> cucumbers
When I eat <eat> cucumbers
Then I should have <left> cucumbers

@example_tag_01
Examples:
| start | eat | left |
| 1 | 1 | 0 |

@example_tag_02
@example_tag_03
Examples:
| start | eat | left |
| 3 | 2 | 1 |

""",
)
pytester.makepyfile(
"""
import pytest
from pytest_bdd import given, when, then, scenario
from pytest_bdd.parsers import parse

@scenario("test.feature", "Outlined with empty example values")
def test_outline():
pass

@given(parse('there are {start} cucumbers'))
def _():
pass

@when(parse('I eat {eat} cucumbers'))
def _():
pass

@then(parse('I should have {left} cucumbers'))
def _():
pass
"""
)
result = pytester.runpytest("-m", "example_tag_01", "-vv").parseoutcomes()
assert result["passed"] == 1

result = pytester.runpytest("-m", "example_tag_02", "-vv").parseoutcomes()
assert result["passed"] == 1

result = pytester.runpytest("-m", "example_tag_01 or example_tag_02", "-vv").parseoutcomes()
assert result["passed"] == 2

result = pytester.runpytest("-m", "example_tag_01 and example_tag_02", "-vv").parseoutcomes()
assert result["deselected"] == 2

result = pytester.runpytest("-m", "scenario_tag_01 and example_tag_03", "-vv").parseoutcomes()
assert result["passed"] == 1
assert result["deselected"] == 1

result = pytester.runpytest("-m", "scenario_tag_01 and example_tag_02", "-vv").parseoutcomes()
assert result["passed"] == 1
assert result["deselected"] == 1

result = pytester.runpytest("-m", "feature_tag_1", "-vv").parseoutcomes()
assert result["passed"] == 2

result = pytester.runpytest("-m", "feature_tag_1 and not example_tag_02", "-vv").parseoutcomes()
assert result["passed"] == 1
assert result["deselected"] == 1

result = pytester.runpytest("-m", "scenario_tag_01 and not example_tag_01", "-vv").parseoutcomes()
assert result["passed"] == 1
assert result["deselected"] == 1


def test_tags_after_background_issue_160(pytester):
"""Make sure using a tag after background works."""
pytester.makefile(
Expand Down