eric7/Testing/Interfaces/PytestRunner.py

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

eric ide

mercurial