pytest-bdd/pytest_bdd/reporting.py

181 lines
5.2 KiB
Python

"""Reporting functionality.
Collection of the scenario execution statuses, timing and other information
that enriches the pytest test reporting.
"""
from __future__ import annotations
import time
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Any, Callable
from _pytest.fixtures import FixtureRequest
from _pytest.nodes import Item
from _pytest.reports import TestReport
from _pytest.runner import CallInfo
from .parser import Feature, Scenario, Step
class StepReport:
"""Step execution report."""
failed = False
stopped = None
def __init__(self, step: Step) -> None:
"""Step report constructor.
:param pytest_bdd.parser.Step step: Step.
"""
self.step = step
self.started = time.perf_counter()
def serialize(self) -> dict[str, Any]:
"""Serialize the step execution report.
:return: Serialized step execution report.
:rtype: dict
"""
return {
"name": self.step.name,
"type": self.step.type,
"keyword": self.step.keyword,
"line_number": self.step.line_number,
"failed": self.failed,
"duration": self.duration,
}
def finalize(self, failed: bool) -> None:
"""Stop collecting information and finalize the report.
:param bool failed: Whether the step execution is failed.
"""
self.stopped = time.perf_counter()
self.failed = failed
@property
def duration(self) -> float:
"""Step execution duration.
:return: Step execution duration.
:rtype: float
"""
if self.stopped is None:
return 0
return self.stopped - self.started
class ScenarioReport:
"""Scenario execution report."""
def __init__(self, scenario: Scenario) -> None:
"""Scenario report constructor.
:param pytest_bdd.parser.Scenario scenario: Scenario.
:param node: pytest test node object
"""
self.scenario: Scenario = scenario
self.step_reports: list[StepReport] = []
@property
def current_step_report(self) -> StepReport:
"""Get current step report.
:return: Last or current step report.
:rtype: pytest_bdd.reporting.StepReport
"""
return self.step_reports[-1]
def add_step_report(self, step_report: StepReport) -> None:
"""Add new step report.
:param step_report: New current step report.
:type step_report: pytest_bdd.reporting.StepReport
"""
self.step_reports.append(step_report)
def serialize(self) -> dict[str, Any]:
"""Serialize scenario execution report in order to transfer reporting from nodes in the distributed mode.
:return: Serialized report.
:rtype: dict
"""
scenario = self.scenario
feature = scenario.feature
return {
"steps": [step_report.serialize() for step_report in self.step_reports],
"name": scenario.name,
"line_number": scenario.line_number,
"tags": sorted(scenario.tags),
"feature": {
"name": feature.name,
"filename": feature.filename,
"rel_filename": feature.rel_filename,
"line_number": feature.line_number,
"description": feature.description,
"tags": sorted(feature.tags),
},
}
def fail(self) -> None:
"""Stop collecting information and finalize the report as failed."""
self.current_step_report.finalize(failed=True)
remaining_steps = self.scenario.steps[len(self.step_reports) :]
# Fail the rest of the steps and make reports.
for step in remaining_steps:
report = StepReport(step=step)
report.finalize(failed=True)
self.add_step_report(report)
def runtest_makereport(item: Item, call: CallInfo, rep: TestReport) -> None:
"""Store item in the report object."""
try:
scenario_report: ScenarioReport = item.__scenario_report__
except AttributeError:
pass
else:
rep.scenario = scenario_report.serialize()
rep.item = {"name": item.name}
def before_scenario(request: FixtureRequest, feature: Feature, scenario: Scenario) -> None:
"""Create scenario report for the item."""
request.node.__scenario_report__ = ScenarioReport(scenario=scenario)
def step_error(
request: FixtureRequest,
feature: Feature,
scenario: Scenario,
step: Step,
step_func: Callable,
step_func_args: dict,
exception: Exception,
) -> None:
"""Finalize the step report as failed."""
request.node.__scenario_report__.fail()
def before_step(request: FixtureRequest, feature: Feature, scenario: Scenario, step: Step, step_func: Callable) -> None:
"""Store step start time."""
request.node.__scenario_report__.add_step_report(StepReport(step=step))
def after_step(
request: FixtureRequest,
feature: Feature,
scenario: Scenario,
step: Step,
step_func: Callable,
step_func_args: dict,
) -> None:
"""Finalize the step report as successful."""
request.node.__scenario_report__.current_step_report.finalize(failed=False)