src/eric7/Testing/Interfaces/PytestRunner.py

branch
eric7-maintenance
changeset 9264
18a7312cfdb3
parent 9192
a763d57e23bc
parent 9221
bf71ee032bb4
child 9371
1da8bc75946f
equal deleted inserted replaced
9241:d23e9854aea4 9264:18a7312cfdb3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the test runner script for the 'pytest' framework.
8 """
9
10 import json
11 import os
12 import sys
13 import time
14
15 sys.path.insert(2, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
16
17
18 class GetPluginVersionsPlugin:
19 """
20 Class implementing a pytest plugin to extract the version info of all
21 installed plugins.
22 """
23
24 def __init__(self):
25 """
26 Constructor
27 """
28 super().__init__()
29
30 self.versions = []
31
32 def pytest_cmdline_main(self, config):
33 """
34 Public method called for performing the main command line action.
35
36 @param config pytest config object
37 @type Config
38 """
39 pluginInfo = config.pluginmanager.list_plugin_distinfo()
40 if pluginInfo:
41 for _plugin, dist in pluginInfo:
42 self.versions.append(
43 {"name": dist.project_name, "version": dist.version}
44 )
45
46 def getVersions(self):
47 """
48 Public method to get the assembled list of plugin versions.
49
50 @return list of collected plugin versions
51 @rtype list of dict
52 """
53 return self.versions
54
55
56 class EricPlugin:
57 """
58 Class implementing a pytest plugin which reports the data in a format
59 suitable for the PytestExecutor.
60 """
61
62 def __init__(self, writer):
63 """
64 Constructor
65
66 @param writer reference to the object to write the results to
67 @type EricJsonWriter
68 """
69 self.__writer = writer
70
71 self.__testsRun = 0
72
73 def __initializeReportData(self):
74 """
75 Private method to initialize attributes for data collection.
76 """
77 self.__status = "---"
78 self.__duration = 0
79 self.__report = []
80 self.__reportPhase = ""
81 self.__sections = []
82 self.__hadError = False
83 self.__wasSkipped = False
84 self.__wasXfail = False
85
86 def pytest_report_header(self, config, startdir):
87 """
88 Public method called by pytest before any reporting.
89
90 @param config reference to the configuration object
91 @type Config
92 @param startdir starting directory
93 @type LocalPath
94 """
95 self.__writer.write({"event": "config", "root": str(config.rootdir)})
96
97 def pytest_collectreport(self, report):
98 """
99 Public method called by pytest after the tests have been collected.
100
101 @param report reference to the report object
102 @type CollectReport
103 """
104 if report.outcome == "failed":
105 self.__writer.write(
106 {
107 "event": "collecterror",
108 "nodeid": report.nodeid,
109 "report": str(report.longrepr),
110 }
111 )
112
113 def pytest_itemcollected(self, item):
114 """
115 Public malled by pytest after a test item has been collected.
116
117 @param item reference to the collected test item
118 @type Item
119 """
120 self.__writer.write(
121 {
122 "event": "collected",
123 "nodeid": item.nodeid,
124 "name": item.name,
125 }
126 )
127
128 def pytest_runtest_logstart(self, nodeid, location):
129 """
130 Public method called by pytest before running a test.
131
132 @param nodeid node id of the test item
133 @type str
134 @param location tuple containing the file name, the line number and
135 the test name
136 @type tuple of (str, int, str)
137 """
138 self.__testsRun += 1
139
140 self.__writer.write(
141 {
142 "event": "starttest",
143 "nodeid": nodeid,
144 }
145 )
146
147 self.__initializeReportData()
148
149 def pytest_runtest_logreport(self, report):
150 """
151 Public method called by pytest when a test phase (setup, call and
152 teardown) has been completed.
153
154 @param report reference to the test report object
155 @type TestReport
156 """
157 if report.when == "call":
158 self.__status = report.outcome
159 self.__duration = report.duration
160 else:
161 if report.outcome == "failed":
162 self.__hadError = True
163 elif report.outcome == "skipped":
164 self.__wasSkipped = True
165
166 if hasattr(report, "wasxfail"):
167 self.__wasXfail = True
168 self.__report.append(report.wasxfail)
169 self.__reportPhase = report.when
170
171 self.__sections = report.sections
172
173 if report.longrepr:
174 self.__reportPhase = report.when
175 if (
176 hasattr(report.longrepr, "reprcrash")
177 and report.longrepr.reprcrash is not None
178 ):
179 self.__report.append(report.longrepr.reprcrash.message)
180 if isinstance(report.longrepr, tuple):
181 self.__report.append(report.longrepr[2])
182 elif isinstance(report.longrepr, str):
183 self.__report.append(report.longrepr)
184 else:
185 self.__report.append(str(report.longrepr))
186
187 def pytest_runtest_logfinish(self, nodeid, location):
188 """
189 Public method called by pytest after a test has been completed.
190
191 @param nodeid node id of the test item
192 @type str
193 @param location tuple containing the file name, the line number and
194 the test name
195 @type tuple of (str, int, str)
196 """
197 if self.__wasXfail:
198 self.__status = "xpassed" if self.__status == "passed" else "xfailed"
199 elif self.__wasSkipped:
200 self.__status = "skipped"
201
202 data = {
203 "event": "result",
204 "status": self.__status,
205 "with_error": self.__hadError,
206 "sections": self.__sections,
207 "duration_s": self.__duration,
208 "nodeid": nodeid,
209 "filename": location[0],
210 "linenumber": location[1],
211 "report_phase": self.__reportPhase,
212 }
213 if self.__report:
214 messageLines = self.__report[0].rstrip().splitlines()
215 data["message"] = messageLines[0]
216 data["report"] = "\n".join(self.__report)
217
218 self.__writer.write(data)
219
220 def pytest_sessionstart(self, session):
221 """
222 Public method called by pytest before performing collection and
223 entering the run test loop.
224
225 @param session reference to the session object
226 @type Session
227 """
228 self.__totalStartTime = time.monotonic_ns()
229 self.__testsRun = 0
230
231 def pytest_sessionfinish(self, session, exitstatus):
232 """
233 Public method called by pytest after the whole test run finished.
234
235 @param session reference to the session object
236 @type Session
237 @param exitstatus exit status
238 @type int or ExitCode
239 """
240 stopTime = time.monotonic_ns()
241 duration = (stopTime - self.__totalStartTime) / 1_000_000_000 # s
242
243 self.__writer.write(
244 {
245 "event": "finished",
246 "duration_s": duration,
247 "tests": self.__testsRun,
248 }
249 )
250
251
252 def getVersions():
253 """
254 Function to determine the framework version and versions of all available
255 plugins.
256 """
257 try:
258 import pytest
259
260 versions = {
261 "name": "pytest",
262 "version": pytest.__version__,
263 "plugins": [],
264 }
265
266 # --capture=sys needed on Windows to avoid
267 # ValueError: saved filedescriptor not valid anymore
268 plugin = GetPluginVersionsPlugin()
269 pytest.main(["--version", "--capture=sys"], plugins=[plugin])
270 versions["plugins"] = plugin.getVersions()
271 except ImportError:
272 versions = {}
273
274 print(json.dumps(versions))
275 sys.exit(0)
276
277
278 if __name__ == "__main__":
279 command = sys.argv[1]
280 if command == "installed":
281 try:
282 import pytest # __IGNORE_WARNING__
283
284 sys.exit(0)
285 except ImportError:
286 sys.exit(1)
287
288 elif command == "versions":
289 getVersions()
290
291 elif command == "runtest":
292 import pytest
293 from EricNetwork.EricJsonStreamWriter import EricJsonWriter
294
295 writer = EricJsonWriter(sys.argv[2], int(sys.argv[3]))
296 pytest.main(sys.argv[4:], plugins=[EricPlugin(writer)])
297 writer.close()
298 sys.exit(0)
299
300 sys.exit(42)
301
302 #
303 # eflag: noqa = M801

eric ide

mercurial