|
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 |