Fix typing in `reporting.py` and `cucumber_json.py`
I managed to remove all occurrences of `Any`, and use proper typed dicts instead
This commit is contained in:
parent
4ccb683ecd
commit
ad221becd1
|
@ -6,11 +6,11 @@ import json
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from typing import TYPE_CHECKING, Any, Literal, TypedDict
|
from typing import TYPE_CHECKING, Literal, TypedDict
|
||||||
|
|
||||||
from typing_extensions import NotRequired
|
from typing_extensions import NotRequired
|
||||||
|
|
||||||
from .reporting import test_report_context_registry
|
from .reporting import FeatureDict, ScenarioReportDict, StepReportDict, test_report_context_registry
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
|
@ -19,6 +19,56 @@ if TYPE_CHECKING:
|
||||||
from _pytest.terminal import TerminalReporter
|
from _pytest.terminal import TerminalReporter
|
||||||
|
|
||||||
|
|
||||||
|
class ResultElementDict(TypedDict):
|
||||||
|
status: Literal["passed", "failed", "skipped"]
|
||||||
|
duration: int # in nanoseconds
|
||||||
|
error_message: NotRequired[str]
|
||||||
|
|
||||||
|
|
||||||
|
class TagElementDict(TypedDict):
|
||||||
|
name: str
|
||||||
|
line: int
|
||||||
|
|
||||||
|
|
||||||
|
class MatchElementDict(TypedDict):
|
||||||
|
location: str
|
||||||
|
|
||||||
|
|
||||||
|
class StepElementDict(TypedDict):
|
||||||
|
keyword: str
|
||||||
|
name: str
|
||||||
|
line: int
|
||||||
|
match: MatchElementDict
|
||||||
|
result: ResultElementDict
|
||||||
|
|
||||||
|
|
||||||
|
class ScenarioElementDict(TypedDict):
|
||||||
|
keyword: str
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
line: int
|
||||||
|
description: str
|
||||||
|
tags: list[TagElementDict]
|
||||||
|
type: Literal["scenario"]
|
||||||
|
steps: list[StepElementDict]
|
||||||
|
|
||||||
|
|
||||||
|
class FeatureElementDict(TypedDict):
|
||||||
|
keyword: str
|
||||||
|
uri: str
|
||||||
|
name: str
|
||||||
|
id: str
|
||||||
|
line: int
|
||||||
|
description: str
|
||||||
|
language: str
|
||||||
|
tags: list[TagElementDict]
|
||||||
|
elements: list[ScenarioElementDict]
|
||||||
|
|
||||||
|
|
||||||
|
class FeaturesDict(TypedDict):
|
||||||
|
features: dict[str, FeatureElementDict]
|
||||||
|
|
||||||
|
|
||||||
def add_options(parser: Parser) -> None:
|
def add_options(parser: Parser) -> None:
|
||||||
"""Add pytest-bdd options."""
|
"""Add pytest-bdd options."""
|
||||||
group = parser.getgroup("bdd", "Cucumber JSON")
|
group = parser.getgroup("bdd", "Cucumber JSON")
|
||||||
|
@ -48,21 +98,15 @@ def unconfigure(config: Config) -> None:
|
||||||
config.pluginmanager.unregister(xml)
|
config.pluginmanager.unregister(xml)
|
||||||
|
|
||||||
|
|
||||||
class Result(TypedDict):
|
|
||||||
status: Literal["passed", "failed", "skipped"]
|
|
||||||
duration: int # in nanoseconds
|
|
||||||
error_message: NotRequired[str]
|
|
||||||
|
|
||||||
|
|
||||||
class LogBDDCucumberJSON:
|
class LogBDDCucumberJSON:
|
||||||
"""Logging plugin for cucumber like json output."""
|
"""Logging plugin for cucumber like json output."""
|
||||||
|
|
||||||
def __init__(self, logfile: str) -> None:
|
def __init__(self, logfile: str) -> None:
|
||||||
logfile = os.path.expanduser(os.path.expandvars(logfile))
|
logfile = os.path.expanduser(os.path.expandvars(logfile))
|
||||||
self.logfile = os.path.normpath(os.path.abspath(logfile))
|
self.logfile = os.path.normpath(os.path.abspath(logfile))
|
||||||
self.features: dict[str, dict] = {}
|
self.features: dict[str, FeatureElementDict] = {}
|
||||||
|
|
||||||
def _get_result(self, step: dict[str, Any], report: TestReport, error_message: bool = False) -> Result:
|
def _get_result(self, step: StepReportDict, report: TestReport, error_message: bool = False) -> ResultElementDict:
|
||||||
"""Get scenario test run result.
|
"""Get scenario test run result.
|
||||||
|
|
||||||
:param step: `Step` step we get result for
|
:param step: `Step` step we get result for
|
||||||
|
@ -80,12 +124,12 @@ class LogBDDCucumberJSON:
|
||||||
status = "skipped"
|
status = "skipped"
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown test outcome {report.outcome}")
|
raise ValueError(f"Unknown test outcome {report.outcome}")
|
||||||
res: Result = {"status": status, "duration": int(math.floor((10**9) * step["duration"]))} # nanosec
|
res: ResultElementDict = {"status": status, "duration": int(math.floor((10**9) * step["duration"]))} # nanosec
|
||||||
if res_message is not None:
|
if res_message is not None:
|
||||||
res["error_message"] = res_message
|
res["error_message"] = res_message
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def _serialize_tags(self, item: dict[str, Any]) -> list[dict[str, Any]]:
|
def _serialize_tags(self, item: FeatureDict | ScenarioReportDict) -> list[TagElementDict]:
|
||||||
"""Serialize item's tags.
|
"""Serialize item's tags.
|
||||||
|
|
||||||
:param item: json-serialized `Scenario` or `Feature`.
|
:param item: json-serialized `Scenario` or `Feature`.
|
||||||
|
@ -110,7 +154,7 @@ class LogBDDCucumberJSON:
|
||||||
# skip if there isn't a result or scenario has no steps
|
# skip if there isn't a result or scenario has no steps
|
||||||
return
|
return
|
||||||
|
|
||||||
def stepmap(step: dict[str, Any]) -> dict[str, Any]:
|
def stepmap(step: StepReportDict) -> StepElementDict:
|
||||||
error_message = False
|
error_message = False
|
||||||
if step["failed"] and not scenario.setdefault("failed", False):
|
if step["failed"] and not scenario.setdefault("failed", False):
|
||||||
scenario["failed"] = True
|
scenario["failed"] = True
|
||||||
|
|
|
@ -43,10 +43,10 @@ def configure(config: Config) -> None:
|
||||||
raise Exception("gherkin-terminal-reporter is not compatible with 'xdist' plugin.")
|
raise Exception("gherkin-terminal-reporter is not compatible with 'xdist' plugin.")
|
||||||
|
|
||||||
|
|
||||||
class GherkinTerminalReporter(TerminalReporter): # type: ignore
|
class GherkinTerminalReporter(TerminalReporter): # type: ignore[misc]
|
||||||
def __init__(self, config: Config) -> None:
|
def __init__(self, config: Config) -> None:
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
self.current_rule = None
|
self.current_rule: str | None = None
|
||||||
|
|
||||||
def pytest_runtest_logreport(self, report: TestReport) -> None:
|
def pytest_runtest_logreport(self, report: TestReport) -> None:
|
||||||
rep = report
|
rep = report
|
||||||
|
|
|
@ -64,7 +64,7 @@ class Feature:
|
||||||
scenarios (OrderedDict[str, ScenarioTemplate]): A dictionary of scenarios in the feature.
|
scenarios (OrderedDict[str, ScenarioTemplate]): A dictionary of scenarios in the feature.
|
||||||
filename (str): The absolute path of the feature file.
|
filename (str): The absolute path of the feature file.
|
||||||
rel_filename (str): The relative path of the feature file.
|
rel_filename (str): The relative path of the feature file.
|
||||||
name (Optional[str]): The name of the feature.
|
name (str): The name of the feature.
|
||||||
tags (set[str]): A set of tags associated with the feature.
|
tags (set[str]): A set of tags associated with the feature.
|
||||||
background (Optional[Background]): The background steps for the feature, if any.
|
background (Optional[Background]): The background steps for the feature, if any.
|
||||||
line_number (int): The line number where the feature starts in the file.
|
line_number (int): The line number where the feature starts in the file.
|
||||||
|
@ -76,7 +76,7 @@ class Feature:
|
||||||
rel_filename: str
|
rel_filename: str
|
||||||
language: str
|
language: str
|
||||||
keyword: str
|
keyword: str
|
||||||
name: str | None
|
name: str
|
||||||
tags: set[str]
|
tags: set[str]
|
||||||
background: Background | None
|
background: Background | None
|
||||||
line_number: int
|
line_number: int
|
||||||
|
|
|
@ -8,12 +8,12 @@ from __future__ import annotations
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Callable, TypedDict
|
||||||
from weakref import WeakKeyDictionary
|
from weakref import WeakKeyDictionary
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
from typing_extensions import NotRequired
|
||||||
from typing import Any, Callable
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
from _pytest.fixtures import FixtureRequest
|
from _pytest.fixtures import FixtureRequest
|
||||||
from _pytest.nodes import Item
|
from _pytest.nodes import Item
|
||||||
from _pytest.reports import TestReport
|
from _pytest.reports import TestReport
|
||||||
|
@ -25,6 +25,44 @@ scenario_reports_registry: WeakKeyDictionary[Item, ScenarioReport] = WeakKeyDict
|
||||||
test_report_context_registry: WeakKeyDictionary[TestReport, ReportContext] = WeakKeyDictionary()
|
test_report_context_registry: WeakKeyDictionary[TestReport, ReportContext] = WeakKeyDictionary()
|
||||||
|
|
||||||
|
|
||||||
|
class FeatureDict(TypedDict):
|
||||||
|
keyword: str
|
||||||
|
name: str
|
||||||
|
filename: str
|
||||||
|
rel_filename: str
|
||||||
|
language: str
|
||||||
|
line_number: int
|
||||||
|
description: str
|
||||||
|
tags: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
class RuleDict(TypedDict):
|
||||||
|
keyword: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
tags: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
class StepReportDict(TypedDict):
|
||||||
|
name: str
|
||||||
|
type: str
|
||||||
|
keyword: str
|
||||||
|
line_number: int
|
||||||
|
failed: bool
|
||||||
|
duration: float
|
||||||
|
|
||||||
|
|
||||||
|
class ScenarioReportDict(TypedDict):
|
||||||
|
steps: list[StepReportDict]
|
||||||
|
keyword: str
|
||||||
|
name: str
|
||||||
|
line_number: int
|
||||||
|
tags: list[str]
|
||||||
|
feature: FeatureDict
|
||||||
|
rule: NotRequired[RuleDict]
|
||||||
|
failed: NotRequired[bool]
|
||||||
|
|
||||||
|
|
||||||
class StepReport:
|
class StepReport:
|
||||||
"""Step execution report."""
|
"""Step execution report."""
|
||||||
|
|
||||||
|
@ -39,11 +77,10 @@ class StepReport:
|
||||||
self.step = step
|
self.step = step
|
||||||
self.started = time.perf_counter()
|
self.started = time.perf_counter()
|
||||||
|
|
||||||
def serialize(self) -> dict[str, object]:
|
def serialize(self) -> StepReportDict:
|
||||||
"""Serialize the step execution report.
|
"""Serialize the step execution report.
|
||||||
|
|
||||||
:return: Serialized step execution report.
|
:return: Serialized step execution report.
|
||||||
:rtype: dict
|
|
||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
"name": self.step.name,
|
"name": self.step.name,
|
||||||
|
@ -103,16 +140,15 @@ class ScenarioReport:
|
||||||
"""
|
"""
|
||||||
self.step_reports.append(step_report)
|
self.step_reports.append(step_report)
|
||||||
|
|
||||||
def serialize(self) -> dict[str, object]:
|
def serialize(self) -> ScenarioReportDict:
|
||||||
"""Serialize scenario execution report in order to transfer reporting from nodes in the distributed mode.
|
"""Serialize scenario execution report in order to transfer reporting from nodes in the distributed mode.
|
||||||
|
|
||||||
:return: Serialized report.
|
:return: Serialized report.
|
||||||
:rtype: dict
|
|
||||||
"""
|
"""
|
||||||
scenario = self.scenario
|
scenario = self.scenario
|
||||||
feature = scenario.feature
|
feature = scenario.feature
|
||||||
|
|
||||||
serialized = {
|
serialized: ScenarioReportDict = {
|
||||||
"steps": [step_report.serialize() for step_report in self.step_reports],
|
"steps": [step_report.serialize() for step_report in self.step_reports],
|
||||||
"keyword": scenario.keyword,
|
"keyword": scenario.keyword,
|
||||||
"name": scenario.name,
|
"name": scenario.name,
|
||||||
|
@ -131,12 +167,13 @@ class ScenarioReport:
|
||||||
}
|
}
|
||||||
|
|
||||||
if scenario.rule:
|
if scenario.rule:
|
||||||
serialized["rule"] = {
|
rule_dict: RuleDict = {
|
||||||
"keyword": scenario.rule.keyword,
|
"keyword": scenario.rule.keyword,
|
||||||
"name": scenario.rule.name,
|
"name": scenario.rule.name,
|
||||||
"description": scenario.rule.description,
|
"description": scenario.rule.description,
|
||||||
"tags": scenario.rule.tags,
|
"tags": sorted(scenario.rule.tags),
|
||||||
}
|
}
|
||||||
|
serialized["rule"] = rule_dict
|
||||||
|
|
||||||
return serialized
|
return serialized
|
||||||
|
|
||||||
|
@ -154,7 +191,7 @@ class ScenarioReport:
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ReportContext:
|
class ReportContext:
|
||||||
scenario: dict[str, Any]
|
scenario: ScenarioReportDict
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
@ -191,7 +228,7 @@ def before_step(
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
scenario: Scenario,
|
scenario: Scenario,
|
||||||
step: Step,
|
step: Step,
|
||||||
step_func: Callable[..., Any],
|
step_func: Callable[..., object],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Store step start time."""
|
"""Store step start time."""
|
||||||
scenario_reports_registry[request.node].add_step_report(StepReport(step=step))
|
scenario_reports_registry[request.node].add_step_report(StepReport(step=step))
|
||||||
|
|
Loading…
Reference in New Issue