src/eric7/Testing/Interfaces/TestExecutorBase.py

branch
eric7
changeset 9221
bf71ee032bb4
parent 9209
b99e7fd55fd3
child 9264
18a7312cfdb3
child 9311
8e588f403fd9
equal deleted inserted replaced
9220:e9e7eca7efee 9221:bf71ee032bb4
19 19
20 class TestResultCategory(IntEnum): 20 class TestResultCategory(IntEnum):
21 """ 21 """
22 Class defining the supported result categories. 22 Class defining the supported result categories.
23 """ 23 """
24
24 RUNNING = 0 25 RUNNING = 0
25 FAIL = 1 26 FAIL = 1
26 OK = 2 27 OK = 2
27 SKIP = 3 28 SKIP = 3
28 PENDING = 4 29 PENDING = 4
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()

eric ide

mercurial