src/eric7/Testing/Interfaces/TestExecutorBase.py

branch
eric7-maintenance
changeset 10460
3b34efa2857c
parent 10079
0222a480e93d
parent 10439
21c28b0f9e41
child 10694
f46c1e224e8a
equal deleted inserted replaced
10366:411df92e881f 10460:3b34efa2857c
1 # -*- coding: utf-8 -*- 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (c) 2022 - 2023 Detlev Offenbach <detlev@die-offenbachs.de> 3 # Copyright (c) 2022 - 2024 Detlev Offenbach <detlev@die-offenbachs.de>
4 # 4 #
5 5
6 """ 6 """
7 Module implementing the executor base class for the various testing frameworks 7 Module implementing the executor base class for the various testing frameworks
8 and supporting classes. 8 and supporting classes.
9 """ 9 """
10 10
11 import os 11 import os
12 12
13 from dataclasses import dataclass 13 from dataclasses import dataclass, field
14 from enum import IntEnum 14 from enum import IntEnum
15 15
16 from PyQt6.QtCore import QObject, QProcess, QProcessEnvironment, pyqtSignal 16 from PyQt6.QtCore import QObject, QProcess, QProcessEnvironment, pyqtSignal
17 17
18 from eric7 import Preferences 18 from eric7 import Preferences
42 id: str # test id 42 id: str # test id
43 description: str = "" # short description of test 43 description: str = "" # short description of test
44 message: str = "" # short result message 44 message: str = "" # short result message
45 extra: list = None # additional information text 45 extra: list = None # additional information text
46 duration: float = None # test duration 46 duration: float = None # test duration
47 filename: str = None # file name of a failed test 47 filename: str = None # file name of a (failed) test
48 lineno: int = None # line number of a failed test 48 lineno: int = None # line number of a (failed) test
49 subtestResult: bool = False # flag indicating the result of a subtest 49 subtestResult: bool = False # flag indicating the result of a subtest
50 50
51 51
52 @dataclass 52 @dataclass
53 class TestConfig: 53 class TestConfig:
54 """ 54 """
55 Class containing the test run configuration. 55 Class containing the test run configuration.
56 """ 56 """
57 57
58 interpreter: str # path of the Python interpreter 58 interpreter: str # path of the Python interpreter
59 discover: bool # auto discovery flag 59 discover: bool = False # auto discovery flag
60 discoveryStart: str # start directory for auto discovery 60 discoveryStart: str = "" # start directory for auto discovery
61 testFilename: str # name of the test script 61 testCases: list = field(default_factory=list) # list of selected test cases
62 testName: str # name of the test function 62 testFilename: str = "" # name of the test script
63 testMarkerExpression: str # marker expression for test selection 63 testName: str = "" # name of the test function
64 testNamePattern: str # test name pattern expression or list 64 testMarkerExpression: str = "" # marker expression for test selection
65 failFast: bool # stop on first fail 65 testNamePattern: str = "" # test name pattern expression or list
66 failedOnly: bool # run failed tests only 66 failFast: bool = False # stop on first fail
67 collectCoverage: bool # coverage collection flag 67 failedOnly: bool = False # run failed tests only
68 eraseCoverage: bool # erase coverage data first 68 collectCoverage: bool = False # coverage collection flag
69 coverageFile: str # name of the coverage data file 69 eraseCoverage: bool = False # erase coverage data first
70 coverageFile: str = "" # name of the coverage data file
71 discoverOnly: bool = False # test discovery only
72 venvName: str = "" # name of the virtual environment
70 73
71 74
72 class TestExecutorBase(QObject): 75 class TestExecutorBase(QObject):
73 """ 76 """
74 Base class for test framework specific implementations. 77 Base class for test framework specific implementations.
75 78
76 @signal collected(list of tuple of (str, str, str)) emitted after all tests 79 @signal collected(list of tuple of (str, str, str, str, int, list)) emitted after
77 have been collected. Tuple elements are the test id, the test name and 80 all tests have been collected. Tuple elements are the test id, the test name,
78 a short description of the test. 81 a short description of the test, the test file name, the line number of
82 the test and the elements of the test path as a list.
79 @signal collectError(list of tuple of (str, str)) emitted when errors 83 @signal collectError(list of tuple of (str, str)) emitted when errors
80 are encountered during test collection. Tuple elements are the 84 are encountered during test collection. Tuple elements are the
81 test name and the error message. 85 test name and the error message.
82 @signal startTest(tuple of (str, str, str) emitted before tests are run. 86 @signal startTest(tuple of (str, str, str) emitted before tests are run.
83 Tuple elements are test id, test name and short description. 87 Tuple elements are test id, test name and short description.
86 The elements are the list of test results and the captured output 90 The elements are the list of test results and the captured output
87 of the test worker (if any). 91 of the test worker (if any).
88 @signal testRunAboutToBeStarted() emitted just before the test run will 92 @signal testRunAboutToBeStarted() emitted just before the test run will
89 be started. 93 be started.
90 @signal testRunFinished(int, float) emitted when the test run has finished. 94 @signal testRunFinished(int, float) emitted when the test run has finished.
91 The elements are the number of tests run and the duration in seconds 95 The elements are the number of tests run and the duration in seconds.
92 @signal stop() emitted when the test process is being stopped. 96 @signal stop() emitted when the test process is being stopped.
93 @signal coverageDataSaved(str) emitted after the coverage data was saved. 97 @signal coverageDataSaved(str) emitted after the coverage data was saved.
94 The element is the absolute path of the coverage data file. 98 The element is the absolute path of the coverage data file.
99 @signal discoveryAboutToBeStarted() emitted just before the test discovery
100 will be started
101 @signal discoveryFinished(int, float) emitted when the discovery has finished.
102 The elements are the number of discovered tests and the duration in seconds.
95 """ 103 """
96 104
97 collected = pyqtSignal(list) 105 collected = pyqtSignal(list)
98 collectError = pyqtSignal(list) 106 collectError = pyqtSignal(list)
99 startTest = pyqtSignal(tuple) 107 startTest = pyqtSignal(tuple)
101 testFinished = pyqtSignal(list, str) 109 testFinished = pyqtSignal(list, str)
102 testRunAboutToBeStarted = pyqtSignal() 110 testRunAboutToBeStarted = pyqtSignal()
103 testRunFinished = pyqtSignal(int, float) 111 testRunFinished = pyqtSignal(int, float)
104 stop = pyqtSignal() 112 stop = pyqtSignal()
105 coverageDataSaved = pyqtSignal(str) 113 coverageDataSaved = pyqtSignal(str)
114 discoveryAboutToBeStarted = pyqtSignal()
115 discoveryFinished = pyqtSignal(int, float)
106 116
107 module = "" 117 module = ""
108 name = "" 118 name = ""
109 runner = "" 119 runner = ""
110 120
116 @type TestingWidget 126 @type TestingWidget
117 """ 127 """
118 super().__init__(testWidget) 128 super().__init__(testWidget)
119 129
120 self.__process = None 130 self.__process = None
131 self.__debugger = None
132
133 self._language = "Python3"
121 134
122 @classmethod 135 @classmethod
123 def isInstalled(cls, interpreter): 136 def isInstalled(cls, interpreter):
124 """ 137 """
125 Class method to check whether a test framework is installed. 138 Class method to check whether a test framework is installed.
153 """ 166 """
154 return {} 167 return {}
155 168
156 def hasCoverage(self, interpreter): # noqa: U100 169 def hasCoverage(self, interpreter): # noqa: U100
157 """ 170 """
158 Public method to get the test framework version and version information 171 Public method to check, if the collection of coverage data is available.
159 of its installed plugins.
160 172
161 @param interpreter interpreter to be used for the test 173 @param interpreter interpreter to be used for the test
162 @type str 174 @type str
163 @return flag indicating the availability of coverage functionality 175 @return flag indicating the availability of coverage functionality
164 @rtype bool 176 @rtype bool
242 env.insert("PYTHONPATH", newPythonPath) 254 env.insert("PYTHONPATH", newPythonPath)
243 process.setProcessEnvironment(env) 255 process.setProcessEnvironment(env)
244 256
245 return process 257 return process
246 258
259 def discover(self, config, pythonpath):
260 """
261 Public method to start the test discovery process.
262
263 @param config configuration for the test discovery
264 @type TestConfig
265 @param pythonpath list of directories to be added to the Python path
266 @type list of str
267 @exception RuntimeError raised if the the test discovery process did not start
268 @exception ValueError raised if no start directory for the test discovery was
269 given
270 """
271 if not config.discoveryStart:
272 raise ValueError("No discovery start directory given.")
273
274 self.__process = self._prepareProcess(config.discoveryStart, pythonpath)
275 discoveryArgs = self.createArguments(config)
276 self.discoveryAboutToBeStarted.emit()
277 self.__process.start(config.interpreter, discoveryArgs)
278 running = self.__process.waitForStarted()
279 if not running:
280 raise RuntimeError("Test discovery process did not start.")
281
247 def start(self, config, pythonpath): 282 def start(self, config, pythonpath):
248 """ 283 """
249 Public method to start the testing process. 284 Public method to start the testing process.
250 285
251 @param config configuration for the test execution 286 @param config configuration for the test execution
265 self.__process.start(config.interpreter, testArgs) 300 self.__process.start(config.interpreter, testArgs)
266 running = self.__process.waitForStarted() 301 running = self.__process.waitForStarted()
267 if not running: 302 if not running:
268 raise RuntimeError("Test process did not start.") 303 raise RuntimeError("Test process did not start.")
269 304
305 def startDebug(self, config, pythonpath, debugger):
306 """
307 Public method to start the test run with debugger support.
308
309 @param config configuration for the test execution
310 @type TestConfig
311 @param pythonpath list of directories to be added to the Python path
312 @type list of str
313 @param debugger refference to the debugger interface
314 @type DebugUI
315 """
316 workDir = (
317 config.discoveryStart
318 if config.discover
319 else os.path.dirname(config.testFilename)
320 )
321 testArgs = self.createArguments(config)
322 if pythonpath:
323 currentPythonPath = os.environ.get("PYTHONPATH")
324 newPythonPath = os.pathsep.join(pythonpath)
325 if currentPythonPath:
326 newPythonPath += os.pathsep + currentPythonPath
327 environment = {"PYTHONPATH": newPythonPath}
328 else:
329 environment = {}
330
331 self.__debugger = debugger
332 self.__debugger.debuggingFinished.connect(self.finished)
333 self.testRunAboutToBeStarted.emit()
334
335 self.__debugger.debugInternalScript(
336 venvName=config.venvName,
337 scriptName=testArgs[0],
338 argv=testArgs[1:],
339 workDir=workDir,
340 environment=environment,
341 clientType=self._language,
342 forProject=False,
343 )
344
270 def finished(self): 345 def finished(self):
271 """ 346 """
272 Public method handling the unit test process been finished. 347 Public method handling the unit test process been finished.
273 348
274 This method should read the results (if necessary) and emit the signal 349 This method should read the results (if necessary) and emit the signal
275 testFinished. 350 testFinished.
276 351 """
277 @exception NotImplementedError this method needs to be implemented by 352 if self.__debugger is not None:
278 derived classes 353 self.__debugger.debuggingFinished.disconnect(self.finished)
279 """ 354 self.__debugger = None
280 raise NotImplementedError
281 355
282 def readAllOutput(self, process=None): 356 def readAllOutput(self, process=None):
283 """ 357 """
284 Public method to read all output of the test process. 358 Public method to read all output of the test process.
285 359

eric ide

mercurial