src/eric7/Testing/Interfaces/PytestExecutor.py

branch
eric7
changeset 9221
bf71ee032bb4
parent 9209
b99e7fd55fd3
child 9264
18a7312cfdb3
child 9311
8e588f403fd9
equal deleted inserted replaced
9220:e9e7eca7efee 9221:bf71ee032bb4
20 20
21 class PytestExecutor(TestExecutorBase): 21 class PytestExecutor(TestExecutorBase):
22 """ 22 """
23 Class implementing the executor for the 'pytest' framework. 23 Class implementing the executor for the 'pytest' framework.
24 """ 24 """
25
25 module = "pytest" 26 module = "pytest"
26 name = "pytest" 27 name = "pytest"
27 28
28 runner = os.path.join(os.path.dirname(__file__), "PytestRunner.py") 29 runner = os.path.join(os.path.dirname(__file__), "PytestRunner.py")
29 30
30 def __init__(self, testWidget): 31 def __init__(self, testWidget):
31 """ 32 """
32 Constructor 33 Constructor
33 34
34 @param testWidget reference to the unit test widget 35 @param testWidget reference to the unit test widget
35 @type TestingWidget 36 @type TestingWidget
36 """ 37 """
37 super().__init__(testWidget) 38 super().__init__(testWidget)
38 39
39 self.__statusDisplayMapping = { 40 self.__statusDisplayMapping = {
40 "failed": self.tr("Failure"), 41 "failed": self.tr("Failure"),
41 "skipped": self.tr("Skipped"), 42 "skipped": self.tr("Skipped"),
42 "xfailed": self.tr("Expected Failure"), 43 "xfailed": self.tr("Expected Failure"),
43 "xpassed": self.tr("Unexpected Success"), 44 "xpassed": self.tr("Unexpected Success"),
44 "passed": self.tr("Success"), 45 "passed": self.tr("Success"),
45 } 46 }
46 47
47 self.__config = None 48 self.__config = None
48 49
49 def getVersions(self, interpreter): 50 def getVersions(self, interpreter):
50 """ 51 """
51 Public method to get the test framework version and version information 52 Public method to get the test framework version and version information
52 of its installed plugins. 53 of its installed plugins.
53 54
54 @param interpreter interpreter to be used for the test 55 @param interpreter interpreter to be used for the test
55 @type str 56 @type str
56 @return dictionary containing the framework name and version and the 57 @return dictionary containing the framework name and version and the
57 list of available plugins with name and version each 58 list of available plugins with name and version each
58 @rtype dict 59 @rtype dict
65 outputLines = self.readAllOutput(proc).splitlines() 66 outputLines = self.readAllOutput(proc).splitlines()
66 for line in outputLines: 67 for line in outputLines:
67 if line.startswith("{") and line.endswith("}"): 68 if line.startswith("{") and line.endswith("}"):
68 with contextlib.suppress(json.JSONDecodeError): 69 with contextlib.suppress(json.JSONDecodeError):
69 return json.loads(line) 70 return json.loads(line)
70 71
71 return {} 72 return {}
72 73
73 def hasCoverage(self, interpreter): 74 def hasCoverage(self, interpreter):
74 """ 75 """
75 Public method to get the test framework version and version information 76 Public method to get the test framework version and version information
76 of its installed plugins. 77 of its installed plugins.
77 78
78 @param interpreter interpreter to be used for the test 79 @param interpreter interpreter to be used for the test
79 @type str 80 @type str
80 @return flag indicating the availability of coverage functionality 81 @return flag indicating the availability of coverage functionality
81 @rtype bool 82 @rtype bool
82 """ 83 """
83 versions = self.getVersions(interpreter) 84 versions = self.getVersions(interpreter)
84 if "plugins" in versions: 85 if "plugins" in versions:
85 return any(plugin["name"] == "pytest-cov" 86 return any(plugin["name"] == "pytest-cov" for plugin in versions["plugins"])
86 for plugin in versions["plugins"]) 87
87
88 return False 88 return False
89 89
90 def createArguments(self, config): 90 def createArguments(self, config):
91 """ 91 """
92 Public method to create the arguments needed to start the test process. 92 Public method to create the arguments needed to start the test process.
93 93
94 @param config configuration for the test execution 94 @param config configuration for the test execution
95 @type TestConfig 95 @type TestConfig
96 @return list of process arguments 96 @return list of process arguments
97 @rtype list of str 97 @rtype list of str
98 """ 98 """
106 "runtest", 106 "runtest",
107 self.reader.address(), 107 self.reader.address(),
108 str(self.reader.port()), 108 str(self.reader.port()),
109 "--quiet", 109 "--quiet",
110 ] 110 ]
111 111
112 if config.failFast: 112 if config.failFast:
113 args.append("--exitfirst") 113 args.append("--exitfirst")
114 114
115 if config.failedOnly: 115 if config.failedOnly:
116 args.append("--last-failed") 116 args.append("--last-failed")
117 else: 117 else:
118 args.append("--cache-clear") 118 args.append("--cache-clear")
119 119
120 if config.collectCoverage: 120 if config.collectCoverage:
121 args.extend([ 121 args.extend(["--cov=.", "--cov-report="])
122 "--cov=.",
123 "--cov-report="
124 ])
125 if not config.eraseCoverage: 122 if not config.eraseCoverage:
126 args.append("--cov-append") 123 args.append("--cov-append")
127 124
128 if config.testFilename: 125 if config.testFilename:
129 if config.testName: 126 if config.testName:
130 args.append("{0}::{1}".format( 127 args.append(
131 config.testFilename, 128 "{0}::{1}".format(
132 config.testName.replace(".", "::") 129 config.testFilename, config.testName.replace(".", "::")
133 )) 130 )
131 )
134 else: 132 else:
135 args.append(config.testFilename) 133 args.append(config.testFilename)
136 134
137 return args 135 return args
138 136
139 def start(self, config, pythonpath): 137 def start(self, config, pythonpath):
140 """ 138 """
141 Public method to start the testing process. 139 Public method to start the testing process.
142 140
143 @param config configuration for the test execution 141 @param config configuration for the test execution
144 @type TestConfig 142 @type TestConfig
145 @param pythonpath list of directories to be added to the Python path 143 @param pythonpath list of directories to be added to the Python path
146 @type list of str 144 @type list of str
147 """ 145 """
148 self.reader = EricJsonReader(name="Unittest Reader", parent=self) 146 self.reader = EricJsonReader(name="Unittest Reader", parent=self)
149 self.reader.dataReceived.connect(self.__processData) 147 self.reader.dataReceived.connect(self.__processData)
150 148
151 self.__config = config 149 self.__config = config
152 150
153 if config.discoveryStart: 151 if config.discoveryStart:
154 pythonpath.insert(0, os.path.abspath(config.discoveryStart)) 152 pythonpath.insert(0, os.path.abspath(config.discoveryStart))
155 elif config.testFilename: 153 elif config.testFilename:
156 pythonpath.insert( 154 pythonpath.insert(0, os.path.abspath(os.path.dirname(config.testFilename)))
157 0, os.path.abspath(os.path.dirname(config.testFilename))) 155
158
159 if config.discover: 156 if config.discover:
160 self.__rootdir = config.discoveryStart 157 self.__rootdir = config.discoveryStart
161 elif config.testFilename: 158 elif config.testFilename:
162 self.__rootdir = os.path.dirname(config.testFilename) 159 self.__rootdir = os.path.dirname(config.testFilename)
163 else: 160 else:
164 self.__rootdir = "" 161 self.__rootdir = ""
165 162
166 super().start(config, pythonpath) 163 super().start(config, pythonpath)
167 164
168 def finished(self): 165 def finished(self):
169 """ 166 """
170 Public method handling the unit test process been finished. 167 Public method handling the unit test process been finished.
171 168
172 This method should read the results (if necessary) and emit the signal 169 This method should read the results (if necessary) and emit the signal
173 testFinished. 170 testFinished.
174 """ 171 """
175 if self.__config.collectCoverage: 172 if self.__config.collectCoverage:
176 self.coverageDataSaved.emit( 173 self.coverageDataSaved.emit(os.path.join(self.__rootdir, ".coverage"))
177 os.path.join(self.__rootdir, ".coverage")) 174
178
179 self.__config = None 175 self.__config = None
180 176
181 self.reader.close() 177 self.reader.close()
182 178
183 output = self.readAllOutput() 179 output = self.readAllOutput()
184 self.testFinished.emit([], output) 180 self.testFinished.emit([], output)
185 181
186 @pyqtSlot(object) 182 @pyqtSlot(object)
187 def __processData(self, data): 183 def __processData(self, data):
188 """ 184 """
189 Private slot to process the received data. 185 Private slot to process the received data.
190 186
191 @param data data object received 187 @param data data object received
192 @type dict 188 @type dict
193 """ 189 """
194 # test configuration 190 # test configuration
195 if data["event"] == "config": 191 if data["event"] == "config":
196 self.__rootdir = data["root"] 192 self.__rootdir = data["root"]
197 193
198 # error collecting tests 194 # error collecting tests
199 elif data["event"] == "collecterror": 195 elif data["event"] == "collecterror":
200 name = self.__normalizeModuleName(data["nodeid"]) 196 name = self.__normalizeModuleName(data["nodeid"])
201 self.collectError.emit([(name, data["report"])]) 197 self.collectError.emit([(name, data["report"])])
202 198
203 # tests collected 199 # tests collected
204 elif data["event"] == "collected": 200 elif data["event"] == "collected":
205 self.collected.emit([ 201 self.collected.emit(
206 (data["nodeid"], 202 [(data["nodeid"], self.__nodeid2testname(data["nodeid"]), "")]
207 self.__nodeid2testname(data["nodeid"]), 203 )
208 "") 204
209 ])
210
211 # test started 205 # test started
212 elif data["event"] == "starttest": 206 elif data["event"] == "starttest":
213 self.startTest.emit( 207 self.startTest.emit(
214 (data["nodeid"], 208 (data["nodeid"], self.__nodeid2testname(data["nodeid"]), "")
215 self.__nodeid2testname(data["nodeid"]),
216 "")
217 ) 209 )
218 210
219 # test result 211 # test result
220 elif data["event"] == "result": 212 elif data["event"] == "result":
221 if data["status"] in ("failed", "xpassed") or data["with_error"]: 213 if data["status"] in ("failed", "xpassed") or data["with_error"]:
222 category = TestResultCategory.FAIL 214 category = TestResultCategory.FAIL
223 elif data["status"] in ("passed", "xfailed"): 215 elif data["status"] in ("passed", "xfailed"):
224 category = TestResultCategory.OK 216 category = TestResultCategory.OK
225 else: 217 else:
226 category = TestResultCategory.SKIP 218 category = TestResultCategory.SKIP
227 219
228 status = ( 220 status = (
229 self.tr("Error") 221 self.tr("Error")
230 if data["with_error"] else 222 if data["with_error"]
231 self.__statusDisplayMapping[data["status"]] 223 else self.__statusDisplayMapping[data["status"]]
232 ) 224 )
233 225
234 message = data.get("message", "") 226 message = data.get("message", "")
235 extraText = data.get("report", "") 227 extraText = data.get("report", "")
236 reportPhase = data.get("report_phase") 228 reportPhase = data.get("report_phase")
237 if reportPhase in ("setup", "teardown"): 229 if reportPhase in ("setup", "teardown"):
238 message = ( 230 message = self.tr("ERROR at {0}: {1}", "phase, message").format(
239 self.tr("ERROR at {0}: {1}", "phase, message") 231 reportPhase, message
240 .format(reportPhase, message)
241 ) 232 )
242 extraText = ( 233 extraText = self.tr("ERROR at {0}: {1}", "phase, extra text").format(
243 self.tr("ERROR at {0}: {1}", "phase, extra text") 234 reportPhase, extraText
244 .format(reportPhase, extraText)
245 ) 235 )
246 sections = data.get("sections", []) 236 sections = data.get("sections", [])
247 if sections: 237 if sections:
248 extraText += "\n" 238 extraText += "\n"
249 for heading, text in sections: 239 for heading, text in sections:
250 extraText += "----- {0} -----\n{1}".format(heading, text) 240 extraText += "----- {0} -----\n{1}".format(heading, text)
251 241
252 duration = data.get("duration_s", None) 242 duration = data.get("duration_s", None)
253 if duration: 243 if duration:
254 # convert to ms 244 # convert to ms
255 duration *= 1000 245 duration *= 1000
256 246
257 filename = data["filename"] 247 filename = data["filename"]
258 if self.__rootdir: 248 if self.__rootdir:
259 filename = os.path.join(self.__rootdir, filename) 249 filename = os.path.join(self.__rootdir, filename)
260 250
261 self.testResult.emit(TestResult( 251 self.testResult.emit(
262 category=category, 252 TestResult(
263 status=status, 253 category=category,
264 name=self.__nodeid2testname(data["nodeid"]), 254 status=status,
265 id=data["nodeid"], 255 name=self.__nodeid2testname(data["nodeid"]),
266 description="", 256 id=data["nodeid"],
267 message=message, 257 description="",
268 extra=extraText.rstrip().splitlines(), 258 message=message,
269 duration=duration, 259 extra=extraText.rstrip().splitlines(),
270 filename=filename, 260 duration=duration,
271 lineno=data.get("linenumber", 0) + 1, 261 filename=filename,
272 # pytest reports 0-based line numbers 262 lineno=data.get("linenumber", 0) + 1,
273 )) 263 # pytest reports 0-based line numbers
274 264 )
265 )
266
275 # test run finished 267 # test run finished
276 elif data["event"] == "finished": 268 elif data["event"] == "finished":
277 self.testRunFinished.emit(data["tests"], data["duration_s"]) 269 self.testRunFinished.emit(data["tests"], data["duration_s"])
278 270
279 def __normalizeModuleName(self, name): 271 def __normalizeModuleName(self, name):
280 r""" 272 r"""
281 Private method to convert a module name reported by pytest to Python 273 Private method to convert a module name reported by pytest to Python
282 conventions. 274 conventions.
283 275
284 This method strips the extensions '.pyw' and '.py' first and replaces 276 This method strips the extensions '.pyw' and '.py' first and replaces
285 '/' and '\' thereafter. 277 '/' and '\' thereafter.
286 278
287 @param name module name reported by pytest 279 @param name module name reported by pytest
288 @type str 280 @type str
289 @return module name iaw. Python conventions 281 @return module name iaw. Python conventions
290 @rtype str 282 @rtype str
291 """ 283 """
292 return (name 284 return (
293 .replace(".pyw", "") 285 name.replace(".pyw", "")
294 .replace(".py", "") 286 .replace(".py", "")
295 .replace("/", ".") 287 .replace("/", ".")
296 .replace("\\", ".")) 288 .replace("\\", ".")
297 289 )
290
298 def __nodeid2testname(self, nodeid): 291 def __nodeid2testname(self, nodeid):
299 """ 292 """
300 Private method to convert a nodeid to a test name. 293 Private method to convert a nodeid to a test name.
301 294
302 @param nodeid nodeid to be converted 295 @param nodeid nodeid to be converted
303 @type str 296 @type str
304 @return test name 297 @return test name
305 @rtype str 298 @rtype str
306 """ 299 """

eric ide

mercurial