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