eric7/Testing/Interfaces/UnittestExecutor.py

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

eric ide

mercurial