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

Fix gherkin terminal report colouring #372

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/pytest_bdd/gherkin_terminal_reporter.py
Expand Up @@ -93,7 +93,10 @@ def pytest_runtest_logreport(self, report: TestReport) -> Any:
self._tw.write(report.scenario["name"], **scenario_markup)
self._tw.write("\n")
for step in report.scenario["steps"]:
self._tw.write(f" {step['keyword']} {step['name']}\n", **scenario_markup)
step_markup = (
{"red": True} if step["failed"] else ({"yellow": True} if step["skipped"] else {"green": True})
)
self._tw.write(f" {step['keyword']} {step['name']}\n", **step_markup)
self._tw.write(" " + word, **word_markup)
self._tw.write("\n\n")
else:
Expand Down
4 changes: 4 additions & 0 deletions src/pytest_bdd/hooks.py
Expand Up @@ -25,6 +25,10 @@ def pytest_bdd_after_step(request, feature, scenario, step, step_func, step_func
"""Called after step function is successfully executed."""


def pytest_bdd_step_skip(request, feature, scenario, step, step_func, step_func_args, exception):
"""Called when step function is skipped."""


def pytest_bdd_step_error(request, feature, scenario, step, step_func, step_func_args, exception):
"""Called when step function failed to execute."""

Expand Down
2 changes: 2 additions & 0 deletions src/pytest_bdd/parser.py
Expand Up @@ -296,6 +296,7 @@ class Step:
indent: int
keyword: str
failed: bool = field(init=False, default=False)
skipped: bool = field(init=False, default=False)
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)
Expand All @@ -308,6 +309,7 @@ def __init__(self, name: str, type: str, indent: int, line_number: int, keyword:
self.keyword = keyword

self.failed = False
self.skipped = False
self.scenario = None
self.background = None
self.lines = []
Expand Down
13 changes: 13 additions & 0 deletions src/pytest_bdd/plugin.py
Expand Up @@ -87,6 +87,19 @@ def pytest_bdd_before_scenario(request: FixtureRequest, feature: Feature, scenar
reporting.before_scenario(request, feature, scenario)


@pytest.hookimpl(tryfirst=True)
def pytest_bdd_step_skip(
request: FixtureRequest,
feature: Feature,
scenario: Scenario,
step: Step,
step_func: Callable,
step_func_args: dict,
exception: Exception,
) -> None:
reporting.step_skip(request, feature, scenario, step, step_func, step_func_args, exception)


@pytest.hookimpl(tryfirst=True)
def pytest_bdd_step_error(
request: FixtureRequest,
Expand Down
30 changes: 29 additions & 1 deletion src/pytest_bdd/reporting.py
Expand Up @@ -22,6 +22,7 @@
class StepReport:
"""Step execution report."""

skipped = False
failed = False
stopped = None

Expand All @@ -44,16 +45,19 @@ def serialize(self) -> dict[str, Any]:
"type": self.step.type,
"keyword": self.step.keyword,
"line_number": self.step.line_number,
"skipped": self.skipped,
"failed": self.failed,
"duration": self.duration,
}

def finalize(self, failed: bool) -> None:
def finalize(self, failed: bool, skipped=False) -> None:
"""Stop collecting information and finalize the report.

:param bool failed: Whether the step execution is failed.
:param bool skipped: Indicates if the step execution is skipped.
"""
self.stopped = time.perf_counter()
self.skipped = skipped
self.failed = failed

@property
Expand Down Expand Up @@ -133,6 +137,17 @@ def fail(self) -> None:
report.finalize(failed=True)
self.add_step_report(report)

def skip(self):
"""Stop collecting information and finalize the report as skipped."""
self.current_step_report.finalize(failed=False, skipped=True)
remaining_steps = self.scenario.steps[len(self.step_reports) :]

# Skip the rest of the steps and make reports.
for step in remaining_steps:
report = StepReport(step=step)
report.finalize(failed=False, skipped=True)
self.add_step_report(report)


def runtest_makereport(item: Item, call: CallInfo, rep: TestReport) -> None:
"""Store item in the report object."""
Expand All @@ -150,6 +165,19 @@ def before_scenario(request: FixtureRequest, feature: Feature, scenario: Scenari
request.node.__scenario_report__ = ScenarioReport(scenario=scenario)


def step_skip(
request: FixtureRequest,
feature: Feature,
scenario: Scenario,
step: Step,
step_func: Callable,
step_func_args: dict,
exception: Exception,
) -> None:
"""Finalize the step report as skipped."""
request.node.__scenario_report__.skip()


def step_error(
request: FixtureRequest,
feature: Feature,
Expand Down
4 changes: 4 additions & 0 deletions src/pytest_bdd/scenario.py
Expand Up @@ -21,6 +21,7 @@
import pytest
from _pytest.fixtures import FixtureDef, FixtureManager, FixtureRequest, call_fixture_func
from _pytest.nodes import iterparentnodeids
from _pytest.outcomes import Skipped

from . import exceptions
from .feature import get_feature, get_features
Expand Down Expand Up @@ -160,6 +161,9 @@ def _execute_step_function(
except Exception as exception:
request.config.hook.pytest_bdd_step_error(exception=exception, **kw)
raise
except Skipped as exception:
request.config.hook.pytest_bdd_step_skip(exception=exception, **kw)
raise

if context.target_fixture is not None:
inject_fixture(request, context.target_fixture, return_value)
Expand Down
92 changes: 79 additions & 13 deletions tests/feature/test_report.py
Expand Up @@ -25,6 +25,7 @@ def test_step_trace(pytester):
feature-tag
scenario-passing-tag
scenario-failing-tag
scenario-skipping-tag
"""
),
)
Expand All @@ -33,7 +34,7 @@ def test_step_trace(pytester):
test=textwrap.dedent(
"""
@feature-tag
Feature: One passing scenario, one failing scenario
Feature: One passing scenario, one failing scenario, one skipping scenario

@scenario-passing-tag
Scenario: Passing
Expand All @@ -45,6 +46,12 @@ def test_step_trace(pytester):
Given a passing step
And a failing step

@scenario-skipping-tag
Scenario: Skipping
Given a passing step
And a skipping step
And a passing step

Scenario Outline: Outlined
Given there are <start> cucumbers
When I eat <eat> cucumbers
Expand Down Expand Up @@ -76,6 +83,10 @@ def _():
def _():
raise Exception('Error')

@given('a skipping step')
def _():
pytest.skip()

@given(parsers.parse('there are {start:d} cucumbers'), target_fixture="cucumbers")
def _(start):
assert isinstance(start, int)
Expand Down Expand Up @@ -106,7 +117,7 @@ def _(cucumbers, left):
"description": "",
"filename": str(feature),
"line_number": 2,
"name": "One passing scenario, one failing scenario",
"name": "One passing scenario, one failing scenario, one skipping scenario",
"rel_filename": str(relpath),
"tags": ["feature-tag"],
},
Expand All @@ -119,6 +130,7 @@ def _(cucumbers, left):
"keyword": "Given",
"line_number": 6,
"name": "a passing step",
"skipped": False,
"type": "given",
},
{
Expand All @@ -127,6 +139,7 @@ def _(cucumbers, left):
"keyword": "And",
"line_number": 7,
"name": "some other passing step",
"skipped": False,
"type": "given",
},
],
Expand All @@ -141,7 +154,7 @@ def _(cucumbers, left):
"description": "",
"filename": str(feature),
"line_number": 2,
"name": "One passing scenario, one failing scenario",
"name": "One passing scenario, one failing scenario, one skipping scenario",
"rel_filename": str(relpath),
"tags": ["feature-tag"],
},
Expand All @@ -154,6 +167,7 @@ def _(cucumbers, left):
"keyword": "Given",
"line_number": 11,
"name": "a passing step",
"skipped": False,
"type": "given",
},
{
Expand All @@ -162,48 +176,97 @@ def _(cucumbers, left):
"keyword": "And",
"line_number": 12,
"name": "a failing step",
"skipped": False,
"type": "given",
},
],
"tags": ["scenario-failing-tag"],
}
assert report == expected

report = result.matchreport("test_skipping", when="call").scenario
expected = {
"feature": {
"description": "",
"filename": str(feature),
"line_number": 2,
"name": "One passing scenario, one failing scenario, one skipping scenario",
"rel_filename": str(relpath),
"tags": ["feature-tag"],
},
"line_number": 15,
"name": "Skipping",
"steps": [
{
"duration": OfType(float),
"failed": False,
"keyword": "Given",
"line_number": 16,
"name": "a passing step",
"skipped": False,
"type": "given",
},
{
"duration": OfType(float),
"failed": False,
"keyword": "And",
"line_number": 17,
"name": "a skipping step",
"skipped": True,
"type": "given",
},
{
"duration": OfType(float),
"failed": False,
"keyword": "And",
"line_number": 18,
"name": "a passing step",
"skipped": True,
"type": "given",
},
],
"tags": ["scenario-skipping-tag"],
}
assert report == expected

report = result.matchreport("test_outlined[12-5-7]", when="call").scenario
expected = {
"feature": {
"description": "",
"filename": str(feature),
"line_number": 2,
"name": "One passing scenario, one failing scenario",
"name": "One passing scenario, one failing scenario, one skipping scenario",
"rel_filename": str(relpath),
"tags": ["feature-tag"],
},
"line_number": 14,
"line_number": 20,
"name": "Outlined",
"steps": [
{
"duration": OfType(float),
"failed": False,
"keyword": "Given",
"line_number": 15,
"line_number": 21,
"name": "there are 12 cucumbers",
"skipped": False,
"type": "given",
},
{
"duration": OfType(float),
"failed": False,
"keyword": "When",
"line_number": 16,
"line_number": 22,
"name": "I eat 5 cucumbers",
"skipped": False,
"type": "when",
},
{
"duration": OfType(float),
"failed": False,
"keyword": "Then",
"line_number": 17,
"line_number": 23,
"name": "I should have 7 cucumbers",
"skipped": False,
"type": "then",
},
],
Expand All @@ -217,35 +280,38 @@ def _(cucumbers, left):
"description": "",
"filename": str(feature),
"line_number": 2,
"name": "One passing scenario, one failing scenario",
"name": "One passing scenario, one failing scenario, one skipping scenario",
"rel_filename": str(relpath),
"tags": ["feature-tag"],
},
"line_number": 14,
"line_number": 20,
"name": "Outlined",
"steps": [
{
"duration": OfType(float),
"failed": False,
"keyword": "Given",
"line_number": 15,
"line_number": 21,
"name": "there are 5 cucumbers",
"skipped": False,
"type": "given",
},
{
"duration": OfType(float),
"failed": False,
"keyword": "When",
"line_number": 16,
"line_number": 22,
"name": "I eat 4 cucumbers",
"skipped": False,
"type": "when",
},
{
"duration": OfType(float),
"failed": False,
"keyword": "Then",
"line_number": 17,
"line_number": 23,
"name": "I should have 1 cucumbers",
"skipped": False,
"type": "then",
},
],
Expand Down