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

Implement localisation support #725

Merged
merged 6 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Unreleased
- Dropped support for python 3.8. Supported python versions: 3.9, 3.10, 3.11, 3.12, 3.13.
- Text after the `#` character is no longer stripped from the Scenario and Feature name.
- Gherkin keyword aliases can now be used and correctly reported in json and terminal output (see `Keywords <https://cucumber.io/docs/gherkin/reference/#keywords>` for permitted list).
- Added localization support. The language of the feature file can be specified using the `# language: <language>` directive at the beginning of the file.

8.0.0b2
----------
Expand Down
1 change: 1 addition & 0 deletions src/pytest_bdd/cucumber_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def stepmap(step: dict[str, Any]) -> dict[str, Any]:
"id": scenario["feature"]["rel_filename"].lower().replace(" ", "-"),
"line": scenario["feature"]["line_number"],
"description": scenario["feature"]["description"],
"language": scenario["feature"]["language"],
"tags": self._serialize_tags(scenario["feature"]),
"elements": [],
}
Expand Down
2 changes: 2 additions & 0 deletions src/pytest_bdd/gherkin_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ def from_dict(cls, data: dict[str, Any]) -> Self:
@dataclass
class Feature:
location: Location
language: str
keyword: str
tags: list[Tag]
name: str
Expand All @@ -279,6 +280,7 @@ class Feature:
def from_dict(cls, data: dict[str, Any]) -> Self:
return cls(
location=Location.from_dict(data["location"]),
language=data["language"],
keyword=data["keyword"],
tags=[Tag.from_dict(tag) for tag in data["tags"]],
name=data["name"],
Expand Down
12 changes: 6 additions & 6 deletions src/pytest_bdd/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from .gherkin_parser import Step as GherkinStep
from .gherkin_parser import Tag as GherkinTag
from .gherkin_parser import get_gherkin_document
from .types import STEP_TYPES
from .types import STEP_TYPE_BY_PARSER_KEYWORD

STEP_PARAM_RE = re.compile(r"<(.+?)>")

Expand All @@ -40,6 +40,7 @@ class Feature:
scenarios: OrderedDict[str, ScenarioTemplate]
filename: str
rel_filename: str
language: str
keyword: str
name: str | None
tags: set[str]
Expand Down Expand Up @@ -352,7 +353,7 @@ def parse_steps(self, steps_data: list[GherkinStep]) -> list[Step]:
return []

first_step = steps_data[0]
if first_step.keyword.lower() not in STEP_TYPES:
if first_step.keyword_type not in STEP_TYPE_BY_PARSER_KEYWORD:
raise StepError(
message=f"First step in a scenario or background must start with 'Given', 'When' or 'Then', but got {first_step.keyword}.",
line=first_step.location.line,
Expand All @@ -361,11 +362,9 @@ def parse_steps(self, steps_data: list[GherkinStep]) -> list[Step]:
)

steps = []
current_type = first_step.keyword.lower()
current_type = STEP_TYPE_BY_PARSER_KEYWORD[first_step.keyword_type]
for step in steps_data:
keyword = step.keyword.lower()
if keyword in STEP_TYPES:
current_type = keyword
current_type = STEP_TYPE_BY_PARSER_KEYWORD.get(step.keyword_type, current_type)
steps.append(
Step(
name=step.text,
Expand Down Expand Up @@ -449,6 +448,7 @@ def parse(self) -> Feature:
background=None,
line_number=feature_data.location.line,
description=textwrap.dedent(feature_data.description),
language=feature_data.language,
)

for child in feature_data.children:
Expand Down
1 change: 1 addition & 0 deletions src/pytest_bdd/reporting.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ def serialize(self) -> dict[str, Any]:
"name": feature.name,
"filename": feature.filename,
"rel_filename": feature.rel_filename,
"language": feature.language,
"line_number": feature.line_number,
"description": feature.description,
"tags": sorted(feature.tags),
Expand Down
6 changes: 6 additions & 0 deletions src/pytest_bdd/types.py
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is another type to map: Conjunction (And, But and *)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know, but that's just a "tokenizer" type, not a semantic one, that's why I didn't include it here

Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,9 @@
THEN: Literal["then"] = "then"

STEP_TYPES = (GIVEN, WHEN, THEN)

STEP_TYPE_BY_PARSER_KEYWORD = {
"Context": GIVEN,
"Action": WHEN,
"Outcome": THEN,
}
1 change: 1 addition & 0 deletions tests/feature/test_cucumber_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ def test_passing_outline():
],
"id": os.path.join("test_step_trace0", "test.feature"),
"keyword": "Feature",
"language": "en",
"line": 2,
"name": "One passing scenario, one failing scenario",
"tags": [{"name": "feature-tag", "line": 1}],
Expand Down
4 changes: 4 additions & 0 deletions tests/feature/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ def _(cucumbers, left):
"feature": {
"description": "",
"keyword": "Feature",
"language": "en",
"filename": str(feature),
"line_number": 2,
"name": "One passing scenario, one failing scenario",
Expand Down Expand Up @@ -144,6 +145,7 @@ def _(cucumbers, left):
"feature": {
"description": "",
"keyword": "Feature",
"language": "en",
"filename": str(feature),
"line_number": 2,
"name": "One passing scenario, one failing scenario",
Expand Down Expand Up @@ -180,6 +182,7 @@ def _(cucumbers, left):
"feature": {
"description": "",
"keyword": "Feature",
"language": "en",
"filename": str(feature),
"line_number": 2,
"name": "One passing scenario, one failing scenario",
Expand Down Expand Up @@ -224,6 +227,7 @@ def _(cucumbers, left):
"feature": {
"description": "",
"keyword": "Feature",
"language": "en",
"filename": str(feature),
"line_number": 2,
"name": "One passing scenario, one failing scenario",
Expand Down
84 changes: 84 additions & 0 deletions tests/feature/test_scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import textwrap

from pytest_bdd.utils import collect_dumped_objects


def test_scenario_not_found(pytester, pytest_params):
"""Test the situation when scenario is not found."""
Expand Down Expand Up @@ -195,3 +197,85 @@ def _():
)
result = pytester.runpytest()
result.assert_outcomes(passed=2)


def test_multilanguage_support(pytester):
"""Test multilanguage support."""
pytester.makefile(
".feature",
simple="""
# language: it

Funzionalità: Funzionalità semplice

Contesto:
Dato che uso uno step nel contesto
Allora va tutto bene

Scenario: Scenario semplice
Dato che uso uno step con "Dato"
E che uso uno step con "E"
Ma che uso uno step con "Ma"
* che uso uno step con "*"
Allora va tutto bene

Schema dello scenario: Scenario con schema
Dato che uso uno step con "<nome esempio>"
Allora va tutto bene

Esempi:
| nome esempio |
| esempio 1 |
| esempio 2 |
""",
)
pytester.makepyfile(
"""
from pytest_bdd import scenario, given, then, parsers
from pytest_bdd.utils import dump_obj

@scenario("simple.feature", "Scenario semplice")
def test_scenario_semplice():
pass

@scenario("simple.feature", "Scenario con schema")
def test_scenario_con_schema():
pass

@given("che uso uno step nel contesto")
def _():
return dump_obj(("given", "che uso uno step nel contesto"))

@given(parsers.parse('che uso uno step con "{step_name}"'))
def _(step_name):
return dump_obj(("given", "che uso uno step con ", step_name))

@then("va tutto bene")
def _():
dump_obj(("then", "va tutto bene"))
"""
)
result = pytester.runpytest("-s")
result.assert_outcomes(passed=3)

assert collect_dumped_objects(result) == [
# 1st scenario
("given", "che uso uno step nel contesto"),
("then", "va tutto bene"),
("given", "che uso uno step con ", "Dato"),
("given", "che uso uno step con ", "E"),
("given", "che uso uno step con ", "Ma"),
("given", "che uso uno step con ", "*"),
("then", "va tutto bene"),
# 2nd scenario
# 1st example
("given", "che uso uno step nel contesto"),
("then", "va tutto bene"),
("given", "che uso uno step con ", "esempio 1"),
("then", "va tutto bene"),
# 2nd example
("given", "che uso uno step nel contesto"),
("then", "va tutto bene"),
("given", "che uso uno step con ", "esempio 2"),
("then", "va tutto bene"),
]
1 change: 1 addition & 0 deletions tests/parser/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def test_parser():
tags=[],
name="User login",
description=" As a registered user\n I want to be able to log in\n So that I can access my account",
language="en",
children=[
Child(
background=Background(
Expand Down
Loading