diff -r 411df92e881f -r 3b34efa2857c src/eric7/Testing/Interfaces/TestExecutorBase.py --- a/src/eric7/Testing/Interfaces/TestExecutorBase.py Sun Dec 03 14:54:00 2023 +0100 +++ b/src/eric7/Testing/Interfaces/TestExecutorBase.py Mon Jan 01 11:10:45 2024 +0100 @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2022 - 2023 Detlev Offenbach <detlev@die-offenbachs.de> +# Copyright (c) 2022 - 2024 Detlev Offenbach <detlev@die-offenbachs.de> # """ @@ -10,7 +10,7 @@ import os -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import IntEnum from PyQt6.QtCore import QObject, QProcess, QProcessEnvironment, pyqtSignal @@ -44,8 +44,8 @@ message: str = "" # short result message extra: list = None # additional information text duration: float = None # test duration - filename: str = None # file name of a failed test - lineno: int = None # line number of a failed test + filename: str = None # file name of a (failed) test + lineno: int = None # line number of a (failed) test subtestResult: bool = False # flag indicating the result of a subtest @@ -56,26 +56,30 @@ """ interpreter: str # path of the Python interpreter - discover: bool # auto discovery flag - discoveryStart: str # start directory for auto discovery - testFilename: str # name of the test script - testName: str # name of the test function - testMarkerExpression: str # marker expression for test selection - testNamePattern: str # test name pattern expression or list - failFast: bool # stop on first fail - failedOnly: bool # run failed tests only - collectCoverage: bool # coverage collection flag - eraseCoverage: bool # erase coverage data first - coverageFile: str # name of the coverage data file + discover: bool = False # auto discovery flag + discoveryStart: str = "" # start directory for auto discovery + testCases: list = field(default_factory=list) # list of selected test cases + testFilename: str = "" # name of the test script + testName: str = "" # name of the test function + testMarkerExpression: str = "" # marker expression for test selection + testNamePattern: str = "" # test name pattern expression or list + failFast: bool = False # stop on first fail + failedOnly: bool = False # run failed tests only + collectCoverage: bool = False # coverage collection flag + eraseCoverage: bool = False # erase coverage data first + coverageFile: str = "" # name of the coverage data file + discoverOnly: bool = False # test discovery only + venvName: str = "" # name of the virtual environment class TestExecutorBase(QObject): """ Base class for test framework specific implementations. - @signal collected(list of tuple of (str, str, str)) emitted after all tests - have been collected. Tuple elements are the test id, the test name and - a short description of the test. + @signal collected(list of tuple of (str, str, str, str, int, list)) emitted after + all tests have been collected. Tuple elements are the test id, the test name, + a short description of the test, the test file name, the line number of + the test and the elements of the test path as a list. @signal collectError(list of tuple of (str, str)) emitted when errors are encountered during test collection. Tuple elements are the test name and the error message. @@ -88,10 +92,14 @@ @signal testRunAboutToBeStarted() emitted just before the test run will be started. @signal testRunFinished(int, float) emitted when the test run has finished. - The elements are the number of tests run and the duration in seconds + The elements are the number of tests run and the duration in seconds. @signal stop() emitted when the test process is being stopped. @signal coverageDataSaved(str) emitted after the coverage data was saved. The element is the absolute path of the coverage data file. + @signal discoveryAboutToBeStarted() emitted just before the test discovery + will be started + @signal discoveryFinished(int, float) emitted when the discovery has finished. + The elements are the number of discovered tests and the duration in seconds. """ collected = pyqtSignal(list) @@ -103,6 +111,8 @@ testRunFinished = pyqtSignal(int, float) stop = pyqtSignal() coverageDataSaved = pyqtSignal(str) + discoveryAboutToBeStarted = pyqtSignal() + discoveryFinished = pyqtSignal(int, float) module = "" name = "" @@ -118,6 +128,9 @@ super().__init__(testWidget) self.__process = None + self.__debugger = None + + self._language = "Python3" @classmethod def isInstalled(cls, interpreter): @@ -155,8 +168,7 @@ def hasCoverage(self, interpreter): # noqa: U100 """ - Public method to get the test framework version and version information - of its installed plugins. + Public method to check, if the collection of coverage data is available. @param interpreter interpreter to be used for the test @type str @@ -244,6 +256,29 @@ return process + def discover(self, config, pythonpath): + """ + Public method to start the test discovery process. + + @param config configuration for the test discovery + @type TestConfig + @param pythonpath list of directories to be added to the Python path + @type list of str + @exception RuntimeError raised if the the test discovery process did not start + @exception ValueError raised if no start directory for the test discovery was + given + """ + if not config.discoveryStart: + raise ValueError("No discovery start directory given.") + + self.__process = self._prepareProcess(config.discoveryStart, pythonpath) + discoveryArgs = self.createArguments(config) + self.discoveryAboutToBeStarted.emit() + self.__process.start(config.interpreter, discoveryArgs) + running = self.__process.waitForStarted() + if not running: + raise RuntimeError("Test discovery process did not start.") + def start(self, config, pythonpath): """ Public method to start the testing process. @@ -267,17 +302,56 @@ if not running: raise RuntimeError("Test process did not start.") + def startDebug(self, config, pythonpath, debugger): + """ + Public method to start the test run with debugger support. + + @param config configuration for the test execution + @type TestConfig + @param pythonpath list of directories to be added to the Python path + @type list of str + @param debugger refference to the debugger interface + @type DebugUI + """ + workDir = ( + config.discoveryStart + if config.discover + else os.path.dirname(config.testFilename) + ) + testArgs = self.createArguments(config) + if pythonpath: + currentPythonPath = os.environ.get("PYTHONPATH") + newPythonPath = os.pathsep.join(pythonpath) + if currentPythonPath: + newPythonPath += os.pathsep + currentPythonPath + environment = {"PYTHONPATH": newPythonPath} + else: + environment = {} + + self.__debugger = debugger + self.__debugger.debuggingFinished.connect(self.finished) + self.testRunAboutToBeStarted.emit() + + self.__debugger.debugInternalScript( + venvName=config.venvName, + scriptName=testArgs[0], + argv=testArgs[1:], + workDir=workDir, + environment=environment, + clientType=self._language, + forProject=False, + ) + def finished(self): """ Public method handling the unit test process been finished. This method should read the results (if necessary) and emit the signal testFinished. - - @exception NotImplementedError this method needs to be implemented by - derived classes """ - raise NotImplementedError + if self.__debugger is not None: + self.__debugger.debuggingFinished.disconnect(self.finished) + self.__debugger = None def readAllOutput(self, process=None): """