src/eric7/Testing/Interfaces/UnittestExecutor.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 executor for the standard 'unittest' framework.
8 """
9
10 import contextlib
11 import json
12 import os
13 import re
14
15 from PyQt6.QtCore import pyqtSlot, QProcess
16
17 from EricNetwork.EricJsonStreamReader import EricJsonReader
18
19 from .TestExecutorBase import TestExecutorBase, TestResult, TestResultCategory
20
21
22 class UnittestExecutor(TestExecutorBase):
23 """
24 Class implementing the executor for the standard 'unittest' framework.
25 """
26
27 module = "unittest"
28 name = "unittest"
29
30 runner = os.path.join(os.path.dirname(__file__), "UnittestRunner.py")
31
32 def __init__(self, testWidget):
33 """
34 Constructor
35
36 @param testWidget reference to the unit test widget
37 @type TestingWidget
38 """
39 super().__init__(testWidget)
40
41 self.__statusCategoryMapping = {
42 "failure": TestResultCategory.FAIL,
43 "error": TestResultCategory.FAIL,
44 "skipped": TestResultCategory.SKIP,
45 "expected failure": TestResultCategory.OK,
46 "unexpected success": TestResultCategory.FAIL,
47 "success": TestResultCategory.OK,
48 }
49
50 self.__statusDisplayMapping = {
51 "failure": self.tr("Failure"),
52 "error": self.tr("Error"),
53 "skipped": self.tr("Skipped"),
54 "expected failure": self.tr("Expected Failure"),
55 "unexpected success": self.tr("Unexpected Success"),
56 "success": self.tr("Success"),
57 }
58
59 self.__testWidget = testWidget
60
61 def getVersions(self, interpreter):
62 """
63 Public method to get the test framework version and version information
64 of its installed plugins.
65
66 @param interpreter interpreter to be used for the test
67 @type str
68 @return dictionary containing the framework name and version and the
69 list of available plugins with name and version each
70 @rtype dict
71 """
72 proc = QProcess()
73 proc.start(interpreter, [UnittestExecutor.runner, "versions"])
74 if proc.waitForFinished(3000):
75 exitCode = proc.exitCode()
76 if exitCode == 0:
77 versionsStr = self.readAllOutput(proc)
78 with contextlib.suppress(json.JSONDecodeError):
79 return json.loads(versionsStr)
80
81 return {}
82
83 def hasCoverage(self, interpreter):
84 """
85 Public method to get the test framework version and version information
86 of its installed plugins.
87
88 @param interpreter interpreter to be used for the test
89 @type str
90 @return flag indicating the availability of coverage functionality
91 @rtype bool
92 """
93 return True
94
95 def createArguments(self, config):
96 """
97 Public method to create the arguments needed to start the test process.
98
99 @param config configuration for the test execution
100 @type TestConfig
101 @return list of process arguments
102 @rtype list of str
103 """
104 args = [
105 UnittestExecutor.runner,
106 "runtest",
107 self.reader.address(),
108 str(self.reader.port()),
109 ]
110
111 if config.discover:
112 args.extend(
113 [
114 "discover",
115 "--start-directory",
116 config.discoveryStart,
117 ]
118 )
119
120 if config.failFast:
121 args.append("--failfast")
122
123 if config.collectCoverage:
124 args.append("--cover")
125 if config.eraseCoverage:
126 args.append("--cover-erase")
127 if config.coverageFile:
128 args.append("--cover-file")
129 args.append(config.coverageFile)
130
131 if config.failedOnly:
132 args.append("--failed-only")
133 if config.testFilename:
134 args.append(config.testFilename)
135 args.extend(self.__testWidget.getFailedTests())
136 elif config.testFilename:
137 args.append(config.testFilename)
138 args.append(config.testName if config.testName else "suite")
139
140 return args
141
142 def start(self, config, pythonpath):
143 """
144 Public method to start the testing process.
145
146 @param config configuration for the test execution
147 @type TestConfig
148 @param pythonpath list of directories to be added to the Python path
149 @type list of str
150 """
151 self.reader = EricJsonReader(name="Unittest Reader", parent=self)
152 self.reader.dataReceived.connect(self.__processData)
153
154 super().start(config, pythonpath)
155
156 def finished(self):
157 """
158 Public method handling the unit test process been finished.
159
160 This method should read the results (if necessary) and emit the signal
161 testFinished.
162 """
163 self.reader.close()
164
165 output = self.readAllOutput()
166 self.testFinished.emit([], output)
167
168 @pyqtSlot(object)
169 def __processData(self, data):
170 """
171 Private slot to process the received data.
172
173 @param data data object received
174 @type dict
175 """
176 # error collecting tests
177 if data["event"] == "collecterror":
178 self.collectError.emit([("", data["error"])])
179
180 # tests collected
181 elif data["event"] == "collected":
182 self.collected.emit(
183 [(t["id"], t["name"], t["description"]) for t in data["tests"]]
184 )
185
186 # test started
187 elif data["event"] == "started":
188 self.startTest.emit((data["id"], data["name"], data["description"]))
189
190 # test result
191 elif data["event"] == "result":
192 filename, lineno = None, None
193 tracebackLines = data.get("traceback", "").splitlines()
194 if tracebackLines:
195 # find the last entry matching the pattern
196 for index in range(len(tracebackLines) - 1, -1, -1):
197 fmatch = re.search(
198 r'File "(.*?)", line (\d*?),.*', tracebackLines[index]
199 )
200 if fmatch:
201 break
202 if fmatch:
203 filename = fmatch.group(1)
204 lineno = int(fmatch.group(2))
205
206 message = data.get("shortmsg", "")
207 if not message and tracebackLines:
208 message = tracebackLines[-1].split(":", 1)[1].strip()
209
210 self.testResult.emit(
211 TestResult(
212 category=self.__statusCategoryMapping[data["status"]],
213 status=self.__statusDisplayMapping[data["status"]],
214 name=data["name"],
215 id=data["id"],
216 description=data["description"],
217 message=message,
218 extra=tracebackLines,
219 duration=data.get("duration_ms", None),
220 filename=filename,
221 lineno=lineno,
222 subtestResult=data.get("subtest", False),
223 )
224 )
225
226 # test run finished
227 elif data["event"] == "finished":
228 self.testRunFinished.emit(data["tests"], data["duration_s"])
229
230 # coverage data
231 elif data["event"] == "coverage":
232 self.coverageDataSaved.emit(data["file"])

eric ide

mercurial