eric7/Unittest/Interfaces/UTExecutorBase.py

branch
unittest
changeset 9059
e7fd342f8bfc
child 9062
7f27bf3b50c3
equal deleted inserted replaced
9057:ddc46e93ccc4 9059:e7fd342f8bfc
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 ResultCategory(IntEnum):
21 """
22 Class defining the supported result categories.
23 """
24 FAIL = 1
25 OK = 2
26 SKIP = 3
27 PENDING = 4
28
29
30 @dataclass
31 class UTTestResult:
32 """
33 Class containing the test result data.
34 """
35 category: int # result category
36 status: str # test status
37 name: str # test name
38 message: str # short result message
39 extra: str # additional information text
40 duration: float # test duration
41 filename: str # file name of a failed test
42 lineno: int # line number of a failed test
43
44
45 @dataclass
46 class UTTestConfig:
47 """
48 Class containing the test run configuration.
49 """
50 interpreter: str # path of the Python interpreter
51 discover: bool # auto discovery flag
52 discoveryStart: str # start directory for auto discovery
53 testFilename: str # name of the test script
54 testName: str # name of the test function
55 failFast: bool # stop on first fail
56 collectCoverage: bool # coverage collection flag
57 eraseCoverage: bool # erase coverage data first
58
59
60 class UTExecutorBase(QObject):
61 """
62 Base class for test framework specific implementations.
63
64 @signal collected(list of str) emitted after all tests have been
65 collected
66 @signal collectError(list of tuple of (str, str)) emitted when errors
67 are encountered during test collection. Tuple elements are the
68 test name and the error message.
69 @signal startTest(list of str) emitted before tests are run
70 @signal testResult(UTTestResult) emitted when a test result is ready
71 @signal testFinished(list, str) emitted when the test has finished.
72 The elements are the list of test results and the captured output
73 of the test worker (if any).
74 @signal stop() emitted when the test process is being stopped.
75 """
76 collected = pyqtSignal(list)
77 collectError = pyqtSignal(list)
78 startTest = pyqtSignal(list)
79 testResult = pyqtSignal(UTTestResult)
80 testFinished = pyqtSignal(list, str)
81 stop = pyqtSignal()
82
83 module = ""
84 name = ""
85 runner = ""
86
87 def __init__(self, testWidget, logfile=None):
88 """
89 Constructor
90
91 @param testWidget reference to the unit test widget
92 @type UnittestWidget
93 @param logfile file name to log test results to (defaults to None)
94 @type str (optional)
95 """
96 super().__init__(testWidget)
97
98 self.__process = None
99 self._logfile = logfile
100 # TODO: add log file creation
101
102 @classmethod
103 def isInstalled(cls, interpreter):
104 """
105 Class method to check whether a test framework is installed.
106
107 The test is performed by checking, if a module loader can found.
108
109 @param interpreter interpreter to be used for the test
110 @type str
111 @return flag indicating the test framework module is installed
112 @rtype bool
113 """
114 if cls.runner:
115 proc = QProcess()
116 proc.start(interpreter, [cls.runner, "installed"])
117 if proc.waitForFinished(3000):
118 exitCode = proc.exitCode()
119 return exitCode == 0
120
121 return False
122
123 def getVersions(self, interpreter):
124 """
125 Public method to get the test framework version and version information
126 of its installed plugins.
127
128 @param interpreter interpreter to be used for the test
129 @type str
130 @return dictionary containing the framework name and version and the
131 list of available plugins with name and version each
132 @rtype dict
133 @exception NotImplementedError this method needs to be implemented by
134 derived classes
135 """
136 raise NotImplementedError
137
138 return {}
139
140 def createArguments(self, config):
141 """
142 Public method to create the arguments needed to start the test process.
143
144 @param config configuration for the test execution
145 @type UTTestConfig
146 @return list of process arguments
147 @rtype list of str
148 @exception NotImplementedError this method needs to be implemented by
149 derived classes
150 """
151 raise NotImplementedError
152
153 return []
154
155 def _prepareProcess(self, workDir, pythonpath):
156 """
157 Protected method to prepare a process object to be started.
158
159 @param workDir working directory
160 @type str
161 @param pythonpath list of directories to be added to the Python path
162 @type list of str
163 @return prepared process object
164 @rtype QProcess
165 """
166 process = QProcess(self)
167 process.setProcessChannelMode(
168 QProcess.ProcessChannelMode.MergedChannels)
169 process.setWorkingDirectory(workDir)
170 process.finished.connect(self.finished)
171 if pythonpath:
172 env = QProcessEnvironment.systemEnvironment()
173 currentPythonPath = env.value('PYTHONPATH', None)
174 newPythonPath = os.pathsep.join(pythonpath)
175 if currentPythonPath:
176 newPythonPath += os.pathsep + currentPythonPath
177 env.insert('PYTHONPATH', newPythonPath)
178 process.setProcessEnvironment(env)
179
180 return process
181
182 def start(self, config, pythonpath):
183 """
184 Public method to start the testing process.
185
186 @param config configuration for the test execution
187 @type UTTestConfig
188 @param pythonpath list of directories to be added to the Python path
189 @type list of str
190 @exception RuntimeError raised if the the testing process did not start
191 """
192 workDir = (
193 config.discoveryStart
194 if config.discover else
195 os.path.dirname(config.testFilename)
196 )
197 self.__process = self._prepareProcess(workDir, pythonpath)
198 testArgs = self.createArguments(config)
199 self.__process.start(config.interpreter, testArgs)
200 running = self.__process.waitForStarted()
201 if not running:
202 raise RuntimeError
203
204 def finished(self):
205 """
206 Public method handling the unit test process been finished.
207
208 This method should read the results (if necessary) and emit the signal
209 testFinished.
210
211 @exception NotImplementedError this method needs to be implemented by
212 derived classes
213 """
214 raise NotImplementedError
215
216 def readAllOutput(self, process=None):
217 """
218 Public method to read all output of the test process.
219
220 @param process reference to the process object
221 @type QProcess
222 @return test process output
223 @rtype str
224 """
225 if process is None:
226 process = self.__process
227 output = (
228 str(process.readAllStandardOutput(),
229 Preferences.getSystem("IOEncoding"),
230 'replace').strip()
231 if process else
232 ""
233 )
234 return output
235
236 def stopIfRunning(self):
237 """
238 Public method to stop the testing process, if it is running.
239 """
240 if (
241 self.__process and
242 self.__process.state() == QProcess.ProcessState.Running
243 ):
244 self.__process.terminate()
245 self.__process.waitForFinished(2000)
246 self.__process.kill()
247 self.__process.waitForFinished(3000)
248
249 self.stop.emit()

eric ide

mercurial