31 @dataclass |
32 @dataclass |
32 class TestResult: |
33 class TestResult: |
33 """ |
34 """ |
34 Class containing the test result data. |
35 Class containing the test result data. |
35 """ |
36 """ |
36 category: TestResultCategory # result category |
37 |
37 status: str # test status |
38 category: TestResultCategory # result category |
38 name: str # test name |
39 status: str # test status |
39 id: str # test id |
40 name: str # test name |
40 description: str = "" # short description of test |
41 id: str # test id |
41 message: str = "" # short result message |
42 description: str = "" # short description of test |
42 extra: list = None # additional information text |
43 message: str = "" # short result message |
43 duration: float = None # test duration |
44 extra: list = None # additional information text |
44 filename: str = None # file name of a failed test |
45 duration: float = None # test duration |
45 lineno: int = None # line number of a failed test |
46 filename: str = None # file name of a failed test |
46 subtestResult: bool = False # flag indicating the result of a subtest |
47 lineno: int = None # line number of a failed test |
|
48 subtestResult: bool = False # flag indicating the result of a subtest |
47 |
49 |
48 |
50 |
49 @dataclass |
51 @dataclass |
50 class TestConfig: |
52 class TestConfig: |
51 """ |
53 """ |
52 Class containing the test run configuration. |
54 Class containing the test run configuration. |
53 """ |
55 """ |
54 interpreter: str # path of the Python interpreter |
56 |
55 discover: bool # auto discovery flag |
57 interpreter: str # path of the Python interpreter |
56 discoveryStart: str # start directory for auto discovery |
58 discover: bool # auto discovery flag |
57 testFilename: str # name of the test script |
59 discoveryStart: str # start directory for auto discovery |
58 testName: str # name of the test function |
60 testFilename: str # name of the test script |
59 failFast: bool # stop on first fail |
61 testName: str # name of the test function |
60 failedOnly: bool # run failed tests only |
62 failFast: bool # stop on first fail |
61 collectCoverage: bool # coverage collection flag |
63 failedOnly: bool # run failed tests only |
62 eraseCoverage: bool # erase coverage data first |
64 collectCoverage: bool # coverage collection flag |
63 coverageFile: str # name of the coverage data file |
65 eraseCoverage: bool # erase coverage data first |
|
66 coverageFile: str # name of the coverage data file |
64 |
67 |
65 |
68 |
66 class TestExecutorBase(QObject): |
69 class TestExecutorBase(QObject): |
67 """ |
70 """ |
68 Base class for test framework specific implementations. |
71 Base class for test framework specific implementations. |
69 |
72 |
70 @signal collected(list of tuple of (str, str, str)) emitted after all tests |
73 @signal collected(list of tuple of (str, str, str)) emitted after all tests |
71 have been collected. Tuple elements are the test id, the test name and |
74 have been collected. Tuple elements are the test id, the test name and |
72 a short description of the test. |
75 a short description of the test. |
73 @signal collectError(list of tuple of (str, str)) emitted when errors |
76 @signal collectError(list of tuple of (str, str)) emitted when errors |
74 are encountered during test collection. Tuple elements are the |
77 are encountered during test collection. Tuple elements are the |
85 The elements are the number of tests run and the duration in seconds |
88 The elements are the number of tests run and the duration in seconds |
86 @signal stop() emitted when the test process is being stopped. |
89 @signal stop() emitted when the test process is being stopped. |
87 @signal coverageDataSaved(str) emitted after the coverage data was saved. |
90 @signal coverageDataSaved(str) emitted after the coverage data was saved. |
88 The element is the absolute path of the coverage data file. |
91 The element is the absolute path of the coverage data file. |
89 """ |
92 """ |
|
93 |
90 collected = pyqtSignal(list) |
94 collected = pyqtSignal(list) |
91 collectError = pyqtSignal(list) |
95 collectError = pyqtSignal(list) |
92 startTest = pyqtSignal(tuple) |
96 startTest = pyqtSignal(tuple) |
93 testResult = pyqtSignal(TestResult) |
97 testResult = pyqtSignal(TestResult) |
94 testFinished = pyqtSignal(list, str) |
98 testFinished = pyqtSignal(list, str) |
95 testRunAboutToBeStarted = pyqtSignal() |
99 testRunAboutToBeStarted = pyqtSignal() |
96 testRunFinished = pyqtSignal(int, float) |
100 testRunFinished = pyqtSignal(int, float) |
97 stop = pyqtSignal() |
101 stop = pyqtSignal() |
98 coverageDataSaved = pyqtSignal(str) |
102 coverageDataSaved = pyqtSignal(str) |
99 |
103 |
100 module = "" |
104 module = "" |
101 name = "" |
105 name = "" |
102 runner = "" |
106 runner = "" |
103 |
107 |
104 def __init__(self, testWidget): |
108 def __init__(self, testWidget): |
105 """ |
109 """ |
106 Constructor |
110 Constructor |
107 |
111 |
108 @param testWidget reference to the unit test widget |
112 @param testWidget reference to the unit test widget |
109 @type TestingWidget |
113 @type TestingWidget |
110 """ |
114 """ |
111 super().__init__(testWidget) |
115 super().__init__(testWidget) |
112 |
116 |
113 self.__process = None |
117 self.__process = None |
114 |
118 |
115 @classmethod |
119 @classmethod |
116 def isInstalled(cls, interpreter): |
120 def isInstalled(cls, interpreter): |
117 """ |
121 """ |
118 Class method to check whether a test framework is installed. |
122 Class method to check whether a test framework is installed. |
119 |
123 |
120 The test is performed by checking, if a module loader can found. |
124 The test is performed by checking, if a module loader can found. |
121 |
125 |
122 @param interpreter interpreter to be used for the test |
126 @param interpreter interpreter to be used for the test |
123 @type str |
127 @type str |
124 @return flag indicating the test framework module is installed |
128 @return flag indicating the test framework module is installed |
125 @rtype bool |
129 @rtype bool |
126 """ |
130 """ |
128 proc = QProcess() |
132 proc = QProcess() |
129 proc.start(interpreter, [cls.runner, "installed"]) |
133 proc.start(interpreter, [cls.runner, "installed"]) |
130 if proc.waitForFinished(3000): |
134 if proc.waitForFinished(3000): |
131 exitCode = proc.exitCode() |
135 exitCode = proc.exitCode() |
132 return exitCode == 0 |
136 return exitCode == 0 |
133 |
137 |
134 return False |
138 return False |
135 |
139 |
136 def getVersions(self, interpreter): |
140 def getVersions(self, interpreter): |
137 """ |
141 """ |
138 Public method to get the test framework version and version information |
142 Public method to get the test framework version and version information |
139 of its installed plugins. |
143 of its installed plugins. |
140 |
144 |
141 @param interpreter interpreter to be used for the test |
145 @param interpreter interpreter to be used for the test |
142 @type str |
146 @type str |
143 @return dictionary containing the framework name and version and the |
147 @return dictionary containing the framework name and version and the |
144 list of available plugins with name and version each |
148 list of available plugins with name and version each |
145 @rtype dict |
149 @rtype dict |
146 @exception NotImplementedError this method needs to be implemented by |
150 @exception NotImplementedError this method needs to be implemented by |
147 derived classes |
151 derived classes |
148 """ |
152 """ |
149 raise NotImplementedError |
153 raise NotImplementedError |
150 |
154 |
151 return {} |
155 return {} |
152 |
156 |
153 def hasCoverage(self, interpreter): |
157 def hasCoverage(self, interpreter): |
154 """ |
158 """ |
155 Public method to get the test framework version and version information |
159 Public method to get the test framework version and version information |
156 of its installed plugins. |
160 of its installed plugins. |
157 |
161 |
158 @param interpreter interpreter to be used for the test |
162 @param interpreter interpreter to be used for the test |
159 @type str |
163 @type str |
160 @return flag indicating the availability of coverage functionality |
164 @return flag indicating the availability of coverage functionality |
161 @rtype bool |
165 @rtype bool |
162 @exception NotImplementedError this method needs to be implemented by |
166 @exception NotImplementedError this method needs to be implemented by |
163 derived classes |
167 derived classes |
164 """ |
168 """ |
165 raise NotImplementedError |
169 raise NotImplementedError |
166 |
170 |
167 return False |
171 return False |
168 |
172 |
169 def createArguments(self, config): |
173 def createArguments(self, config): |
170 """ |
174 """ |
171 Public method to create the arguments needed to start the test process. |
175 Public method to create the arguments needed to start the test process. |
172 |
176 |
173 @param config configuration for the test execution |
177 @param config configuration for the test execution |
174 @type TestConfig |
178 @type TestConfig |
175 @return list of process arguments |
179 @return list of process arguments |
176 @rtype list of str |
180 @rtype list of str |
177 @exception NotImplementedError this method needs to be implemented by |
181 @exception NotImplementedError this method needs to be implemented by |
178 derived classes |
182 derived classes |
179 """ |
183 """ |
180 raise NotImplementedError |
184 raise NotImplementedError |
181 |
185 |
182 return [] |
186 return [] |
183 |
187 |
184 def _prepareProcess(self, workDir, pythonpath): |
188 def _prepareProcess(self, workDir, pythonpath): |
185 """ |
189 """ |
186 Protected method to prepare a process object to be started. |
190 Protected method to prepare a process object to be started. |
187 |
191 |
188 @param workDir working directory |
192 @param workDir working directory |
189 @type str |
193 @type str |
190 @param pythonpath list of directories to be added to the Python path |
194 @param pythonpath list of directories to be added to the Python path |
191 @type list of str |
195 @type list of str |
192 @return prepared process object |
196 @return prepared process object |
193 @rtype QProcess |
197 @rtype QProcess |
194 """ |
198 """ |
195 process = QProcess(self) |
199 process = QProcess(self) |
196 process.setProcessChannelMode( |
200 process.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels) |
197 QProcess.ProcessChannelMode.MergedChannels) |
|
198 process.setWorkingDirectory(workDir) |
201 process.setWorkingDirectory(workDir) |
199 process.finished.connect(self.finished) |
202 process.finished.connect(self.finished) |
200 if pythonpath: |
203 if pythonpath: |
201 env = QProcessEnvironment.systemEnvironment() |
204 env = QProcessEnvironment.systemEnvironment() |
202 currentPythonPath = env.value('PYTHONPATH', None) |
205 currentPythonPath = env.value("PYTHONPATH", None) |
203 newPythonPath = os.pathsep.join(pythonpath) |
206 newPythonPath = os.pathsep.join(pythonpath) |
204 if currentPythonPath: |
207 if currentPythonPath: |
205 newPythonPath += os.pathsep + currentPythonPath |
208 newPythonPath += os.pathsep + currentPythonPath |
206 env.insert('PYTHONPATH', newPythonPath) |
209 env.insert("PYTHONPATH", newPythonPath) |
207 process.setProcessEnvironment(env) |
210 process.setProcessEnvironment(env) |
208 |
211 |
209 return process |
212 return process |
210 |
213 |
211 def start(self, config, pythonpath): |
214 def start(self, config, pythonpath): |
212 """ |
215 """ |
213 Public method to start the testing process. |
216 Public method to start the testing process. |
214 |
217 |
215 @param config configuration for the test execution |
218 @param config configuration for the test execution |
216 @type TestConfig |
219 @type TestConfig |
217 @param pythonpath list of directories to be added to the Python path |
220 @param pythonpath list of directories to be added to the Python path |
218 @type list of str |
221 @type list of str |
219 @exception RuntimeError raised if the the testing process did not start |
222 @exception RuntimeError raised if the the testing process did not start |
220 """ |
223 """ |
221 workDir = ( |
224 workDir = ( |
222 config.discoveryStart |
225 config.discoveryStart |
223 if config.discover else |
226 if config.discover |
224 os.path.dirname(config.testFilename) |
227 else os.path.dirname(config.testFilename) |
225 ) |
228 ) |
226 self.__process = self._prepareProcess(workDir, pythonpath) |
229 self.__process = self._prepareProcess(workDir, pythonpath) |
227 testArgs = self.createArguments(config) |
230 testArgs = self.createArguments(config) |
228 self.testRunAboutToBeStarted.emit() |
231 self.testRunAboutToBeStarted.emit() |
229 self.__process.start(config.interpreter, testArgs) |
232 self.__process.start(config.interpreter, testArgs) |
230 running = self.__process.waitForStarted() |
233 running = self.__process.waitForStarted() |
231 if not running: |
234 if not running: |
232 raise RuntimeError |
235 raise RuntimeError |
233 |
236 |
234 def finished(self): |
237 def finished(self): |
235 """ |
238 """ |
236 Public method handling the unit test process been finished. |
239 Public method handling the unit test process been finished. |
237 |
240 |
238 This method should read the results (if necessary) and emit the signal |
241 This method should read the results (if necessary) and emit the signal |
239 testFinished. |
242 testFinished. |
240 |
243 |
241 @exception NotImplementedError this method needs to be implemented by |
244 @exception NotImplementedError this method needs to be implemented by |
242 derived classes |
245 derived classes |
243 """ |
246 """ |
244 raise NotImplementedError |
247 raise NotImplementedError |
245 |
248 |
246 def readAllOutput(self, process=None): |
249 def readAllOutput(self, process=None): |
247 """ |
250 """ |
248 Public method to read all output of the test process. |
251 Public method to read all output of the test process. |
249 |
252 |
250 @param process reference to the process object |
253 @param process reference to the process object |
251 @type QProcess |
254 @type QProcess |
252 @return test process output |
255 @return test process output |
253 @rtype str |
256 @rtype str |
254 """ |
257 """ |
255 if process is None: |
258 if process is None: |
256 process = self.__process |
259 process = self.__process |
257 output = ( |
260 output = ( |
258 str(process.readAllStandardOutput(), |
261 str( |
|
262 process.readAllStandardOutput(), |
259 Preferences.getSystem("IOEncoding"), |
263 Preferences.getSystem("IOEncoding"), |
260 'replace').strip() |
264 "replace", |
261 if process else |
265 ).strip() |
262 "" |
266 if process |
|
267 else "" |
263 ) |
268 ) |
264 return output |
269 return output |
265 |
270 |
266 def stopIfRunning(self): |
271 def stopIfRunning(self): |
267 """ |
272 """ |
268 Public method to stop the testing process, if it is running. |
273 Public method to stop the testing process, if it is running. |
269 """ |
274 """ |
270 if ( |
275 if self.__process and self.__process.state() == QProcess.ProcessState.Running: |
271 self.__process and |
|
272 self.__process.state() == QProcess.ProcessState.Running |
|
273 ): |
|
274 self.__process.terminate() |
276 self.__process.terminate() |
275 self.__process.waitForFinished(2000) |
277 self.__process.waitForFinished(2000) |
276 self.__process.kill() |
278 self.__process.kill() |
277 self.__process.waitForFinished(3000) |
279 self.__process.waitForFinished(3000) |
278 |
280 |
279 self.stop.emit() |
281 self.stop.emit() |