src/eric7/Testing/Interfaces/TestExecutorBase.py

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

eric ide

mercurial