-
Notifications
You must be signed in to change notification settings - Fork 225
/
Copy pathcucumber_json.py
149 lines (122 loc) · 5.52 KB
/
cucumber_json.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
"""Cucumber json output formatter."""
from __future__ import annotations
import json
import math
import os
import time
import typing
if typing.TYPE_CHECKING:
from typing import Any
from _pytest.config import Config
from _pytest.config.argparsing import Parser
from _pytest.reports import TestReport
from _pytest.terminal import TerminalReporter
def add_options(parser: Parser) -> None:
"""Add pytest-bdd options."""
group = parser.getgroup("bdd", "Cucumber JSON")
group.addoption(
"--cucumberjson",
"--cucumber-json",
action="store",
dest="cucumber_json_path",
metavar="path",
default=None,
help="create cucumber json style report file at given path.",
)
def configure(config: Config) -> None:
cucumber_json_path = config.option.cucumber_json_path
# prevent opening json log on worker nodes (xdist)
if cucumber_json_path and not hasattr(config, "workerinput"):
config._bddcucumberjson = LogBDDCucumberJSON(cucumber_json_path) # type: ignore[attr-defined]
config.pluginmanager.register(config._bddcucumberjson) # type: ignore[attr-defined]
def unconfigure(config: Config) -> None:
xml = getattr(config, "_bddcucumberjson", None) # type: ignore[attr-defined]
if xml is not None:
del config._bddcucumberjson # type: ignore[attr-defined]
config.pluginmanager.unregister(xml)
class LogBDDCucumberJSON:
"""Logging plugin for cucumber like json output."""
def __init__(self, logfile: str) -> None:
logfile = os.path.expanduser(os.path.expandvars(logfile))
self.logfile = os.path.normpath(os.path.abspath(logfile))
self.features: dict[str, dict] = {}
def _get_result(self, step: dict[str, Any], report: TestReport, error_message: bool = False) -> dict[str, Any]:
"""Get scenario test run result.
:param step: `Step` step we get result for
:param report: pytest `Report` object
:return: `dict` in form {"status": "<passed|failed|skipped>", ["error_message": "<error_message>"]}
"""
result: dict[str, Any] = {}
if report.skipped:
reason = report.longrepr[2][report.longrepr[2].find(":") + 2 :]
result = {"status": "skipped", "skipped_message": reason}
elif report.passed or not step["failed"]: # ignore setup/teardown
result = {"status": "passed"}
elif report.failed:
result = {"status": "failed", "error_message": str(report.longrepr) if error_message else ""}
result["duration"] = int(math.floor((10**9) * step["duration"])) # nanosec
return result
def _serialize_tags(self, item: dict[str, Any]) -> list[dict[str, Any]]:
"""Serialize item's tags.
:param item: json-serialized `Scenario` or `Feature`.
:return: `list` of `dict` in the form of:
[
{
"name": "<tag>",
"line": 2,
}
]
"""
return [{"name": tag, "line": item["line_number"] - 1} for tag in item["tags"]]
def pytest_runtest_logreport(self, report: TestReport) -> None:
try:
scenario = report.scenario
except AttributeError:
# skip reporting for non-bdd tests
return
if not scenario["steps"] or report.when != "call":
# skip if there isn't a result or scenario has no steps
return
def stepmap(step: dict[str, Any]) -> dict[str, Any]:
error_message = False
if step["failed"] and not scenario.setdefault("failed", False):
scenario["failed"] = True
error_message = True
step_name = step["name"]
return {
"keyword": step["keyword"],
"name": step_name,
"line": step["line_number"],
"match": {"location": ""},
"result": self._get_result(step, report, error_message),
}
if scenario["feature"]["filename"] not in self.features:
self.features[scenario["feature"]["filename"]] = {
"keyword": scenario["feature"]["keyword"],
"uri": scenario["feature"]["rel_filename"],
"name": scenario["feature"]["name"] or scenario["feature"]["rel_filename"],
"id": scenario["feature"]["rel_filename"].lower().replace(" ", "-"),
"line": scenario["feature"]["line_number"],
"description": scenario["feature"]["description"],
"tags": self._serialize_tags(scenario["feature"]),
"elements": [],
}
self.features[scenario["feature"]["filename"]]["elements"].append(
{
"keyword": scenario["keyword"],
"id": report.item["name"],
"name": scenario["name"],
"line": scenario["line_number"],
"description": "",
"tags": self._serialize_tags(scenario),
"type": "scenario",
"steps": [stepmap(step) for step in scenario["steps"]],
}
)
def pytest_sessionstart(self) -> None:
self.suite_start_time = time.time()
def pytest_sessionfinish(self) -> None:
with open(self.logfile, "w", encoding="utf-8") as logfile:
logfile.write(json.dumps(list(self.features.values())))
def pytest_terminal_summary(self, terminalreporter: TerminalReporter) -> None:
terminalreporter.write_sep("-", f"generated json file: {self.logfile}")