eric7/Unittest/UnittestWidget.py

Fri, 13 May 2022 17:23:21 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Fri, 13 May 2022 17:23:21 +0200
branch
unittest
changeset 9062
7f27bf3b50c3
parent 9059
e7fd342f8bfc
child 9063
f1d7dd7ae471
permissions
-rw-r--r--

Implemented most of the 'unittest' executor and runner.

# -*- coding: utf-8 -*-

# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing a widget to orchestrate unit test execution.
"""

import enum
import locale
import os

from PyQt6.QtCore import pyqtSlot, Qt, QEvent, QCoreApplication
from PyQt6.QtWidgets import (
    QAbstractButton, QComboBox, QDialogButtonBox, QWidget
)

from EricWidgets import EricMessageBox
from EricWidgets.EricApplication import ericApp
from EricWidgets.EricMainWindow import EricMainWindow
from EricWidgets.EricPathPicker import EricPathPickerModes

from .Ui_UnittestWidget import Ui_UnittestWidget

from .UTTestResultsTree import TestResultsModel, TestResultsTreeView
from .Interfaces import Frameworks
from .Interfaces.UTExecutorBase import (
    UTTestConfig, UTTestResult, ResultCategory
)
from .Interfaces.UTFrameworkRegistry import UTFrameworkRegistry

import Preferences
import UI.PixmapCache

from Globals import (
    recentNameUnittestDiscoverHistory, recentNameUnittestFileHistory,
    recentNameUnittestTestnameHistory, recentNameUnittestFramework,
    recentNameUnittestEnvironment
)


class UnittestWidgetModes(enum.Enum):
    """
    Class defining the various modes of the unittest widget.
    """
    IDLE = 0            # idle, no test were run yet
    RUNNING = 1         # test run being performed
    STOPPED = 2         # test run finished


# TODO: add a "Show Coverage" function using PyCoverageDialog

class UnittestWidget(QWidget, Ui_UnittestWidget):
    """
    Class implementing a widget to orchestrate unit test execution.
    """
    def __init__(self, testfile=None, parent=None):
        """
        Constructor
        
        @param testfile file name of the test to load
        @type str
        @param parent reference to the parent widget (defaults to None)
        @type QWidget (optional)
        """
        super().__init__(parent)
        self.setupUi(self)
        
        self.__resultsModel = TestResultsModel(self)
        self.__resultsTree = TestResultsTreeView(self)
        self.__resultsTree.setModel(self.__resultsModel)
        self.resultsGroupBox.layout().addWidget(self.__resultsTree)
        
        self.versionsButton.setIcon(
            UI.PixmapCache.getIcon("info"))
        self.clearHistoriesButton.setIcon(
            UI.PixmapCache.getIcon("clearPrivateData"))
        
        self.testsuitePicker.setMode(EricPathPickerModes.OPEN_FILE_MODE)
        self.testsuitePicker.setInsertPolicy(
            QComboBox.InsertPolicy.InsertAtTop)
        self.testsuitePicker.setSizeAdjustPolicy(
            QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon)
        
        self.discoveryPicker.setMode(EricPathPickerModes.DIRECTORY_MODE)
        self.discoveryPicker.setInsertPolicy(
            QComboBox.InsertPolicy.InsertAtTop)
        self.discoveryPicker.setSizeAdjustPolicy(
            QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon)
        
        self.testComboBox.lineEdit().setClearButtonEnabled(True)
        
        # create some more dialog buttons for orchestration
        self.__startButton = self.buttonBox.addButton(
            self.tr("Start"), QDialogButtonBox.ButtonRole.ActionRole)
        
        self.__startButton.setToolTip(self.tr(
            "Start the selected testsuite"))
        self.__startButton.setWhatsThis(self.tr(
            """<b>Start Test</b>"""
            """<p>This button starts the selected testsuite.</p>"""))
        
        # TODO: implement "Rerun Failed"
##        self.__startFailedButton = self.buttonBox.addButton(
##            self.tr("Rerun Failed"), QDialogButtonBox.ButtonRole.ActionRole)
##        self.__startFailedButton.setToolTip(
##            self.tr("Reruns failed tests of the selected testsuite"))
##        self.__startFailedButton.setWhatsThis(self.tr(
##            """<b>Rerun Failed</b>"""
##            """<p>This button reruns all failed tests of the selected"""
##            """ testsuite.</p>"""))
##        
        self.__stopButton = self.buttonBox.addButton(
            self.tr("Stop"), QDialogButtonBox.ButtonRole.ActionRole)
        self.__stopButton.setToolTip(self.tr("Stop the running unittest"))
        self.__stopButton.setWhatsThis(self.tr(
            """<b>Stop Test</b>"""
            """<p>This button stops a running unittest.</p>"""))
        
        self.__stopButton.setEnabled(False)
        self.__startButton.setDefault(True)
        self.__startButton.setEnabled(False)
##        self.__startFailedButton.setEnabled(False)
        
        self.setWindowFlags(
            self.windowFlags() |
            Qt.WindowType.WindowContextHelpButtonHint
        )
        self.setWindowIcon(UI.PixmapCache.getIcon("eric"))
        self.setWindowTitle(self.tr("Unittest"))
        
        from VirtualEnv.VirtualenvManager import VirtualenvManager
        self.__venvManager = VirtualenvManager(self)
        self.__venvManager.virtualEnvironmentAdded.connect(
            self.__populateVenvComboBox)
        self.__venvManager.virtualEnvironmentRemoved.connect(
            self.__populateVenvComboBox)
        self.__venvManager.virtualEnvironmentChanged.connect(
            self.__populateVenvComboBox)
        
        # TODO: implement project mode
        self.__forProject = False
        
        self.__discoverHistory = []
        self.__fileHistory = []
        self.__testNameHistory = []
        self.__recentFramework = ""
        self.__recentEnvironment = ""
        
        self.__failedTests = set()
        
        self.__editors = []
        self.__testExecutor = None
        
        # connect some signals
        self.frameworkComboBox.currentIndexChanged.connect(
            self.__updateButtonBoxButtons)
        self.discoverCheckBox.toggled.connect(
            self.__updateButtonBoxButtons)
        self.discoveryPicker.editTextChanged.connect(
            self.__updateButtonBoxButtons)
        self.testsuitePicker.editTextChanged.connect(
            self.__updateButtonBoxButtons)
        
        self.__frameworkRegistry = UTFrameworkRegistry()
        for framework in Frameworks:
            self.__frameworkRegistry.register(framework)
        
        self.__setIdleMode()
        
        self.__loadRecent()
        self.__populateVenvComboBox()
        
        if self.__forProject:
            project = ericApp().getObject("Project")
            if project.isOpen():
                self.__insertDiscovery(project.getProjectPath())
            else:
                self.__insertDiscovery("")
        else:
            self.__insertDiscovery("")
        self.__insertTestFile(testfile)
        self.__insertTestName("")
        
        self.clearHistoriesButton.clicked.connect(self.clearRecent)
        
        self.tabWidget.setCurrentIndex(0)
    
    def __populateVenvComboBox(self):
        """
        Private method to (re-)populate the virtual environments selector.
        """
        currentText = self.venvComboBox.currentText()
        if not currentText:
            currentText = self.__recentEnvironment
        
        self.venvComboBox.clear()
        self.venvComboBox.addItem("")
        self.venvComboBox.addItems(
            sorted(self.__venvManager.getVirtualenvNames()))
        index = self.venvComboBox.findText(currentText)
        if index < 0:
            index = 0
        self.venvComboBox.setCurrentIndex(index)
    
    def __populateTestFrameworkComboBox(self):
        """
        Private method to (re-)populate the test framework selector.
        """
        currentText = self.frameworkComboBox.currentText()
        if not currentText:
            currentText = self.__recentFramework
        
        self.frameworkComboBox.clear()
        
        if bool(self.venvComboBox.currentText()):
            interpreter = self.__venvManager.getVirtualenvInterpreter(
                self.venvComboBox.currentText())
            self.frameworkComboBox.addItem("")
            for index, (name, executor) in enumerate(
                sorted(self.__frameworkRegistry.getFrameworks().items()),
                start=1
            ):
                isInstalled = executor.isInstalled(interpreter)
                entry = (
                    name
                    if isInstalled else
                    self.tr("{0} (not available)").format(name)
                )
                self.frameworkComboBox.addItem(entry)
                self.frameworkComboBox.model().item(index).setEnabled(
                    isInstalled)
            
            self.frameworkComboBox.setCurrentText(self.__recentFramework)
    
    @pyqtSlot(str)
    def __insertHistory(self, widget, history, item):
        """
        Private slot to insert an item into a history object.
        
        @param widget reference to the widget
        @type QComboBox or EricComboPathPicker
        @param history array containing the history
        @type list of str
        @param item item to be inserted
        @type str
        """
        current = widget.currentText()
        
        # prepend the given directory to the discovery picker
        if item is None:
            item = ""
        if item in history:
            history.remove(item)
        history.insert(0, item)
        widget.clear()
        widget.addItems(history)
        
        if current:
            widget.setEditText(current)
    
    @pyqtSlot(str)
    def __insertDiscovery(self, start):
        """
        Private slot to insert the discovery start directory into the
        discoveryPicker object.
        
        @param start start directory name to be inserted
        @type str
        """
        self.__insertHistory(self.discoveryPicker, self.__discoverHistory,
                             start)
    
    @pyqtSlot(str)
    def __insertTestFile(self, prog):
        """
        Private slot to insert a test file name into the testsuitePicker
        object.
        
        @param prog test file name to be inserted
        @type str
        """
        self.__insertHistory(self.testsuitePicker, self.__fileHistory,
                             prog)
    
    @pyqtSlot(str)
    def __insertTestName(self, testName):
        """
        Private slot to insert a test name into the testComboBox object.
        
        @param testName name of the test to be inserted
        @type str
        """
        self.__insertHistory(self.testComboBox, self.__testNameHistory,
                             testName)
    
    def __loadRecent(self):
        """
        Private method to load the most recently used lists.
        """
        Preferences.Prefs.rsettings.sync()
        
        # 1. recently selected test framework and virtual environment
        self.__recentEnvironment = Preferences.Prefs.rsettings.value(
            recentNameUnittestEnvironment, "")
        self.__recentFramework = Preferences.Prefs.rsettings.value(
            recentNameUnittestFramework, "")
        
        # 2. discovery history
        self.__discoverHistory = []
        rs = Preferences.Prefs.rsettings.value(
            recentNameUnittestDiscoverHistory)
        if rs is not None:
            recent = [f for f in Preferences.toList(rs) if os.path.exists(f)]
            self.__discoverHistory = recent[
                :Preferences.getDebugger("RecentNumber")]
        
        # 3. test file history
        self.__fileHistory = []
        rs = Preferences.Prefs.rsettings.value(
            recentNameUnittestFileHistory)
        if rs is not None:
            recent = [f for f in Preferences.toList(rs) if os.path.exists(f)]
            self.__fileHistory = recent[
                :Preferences.getDebugger("RecentNumber")]
        
        # 4. test name history
        self.__testNameHistory = []
        rs = Preferences.Prefs.rsettings.value(
            recentNameUnittestTestnameHistory)
        if rs is not None:
            recent = [n for n in Preferences.toList(rs) if n]
            self.__testNameHistory = recent[
                :Preferences.getDebugger("RecentNumber")]
    
    def __saveRecent(self):
        """
        Private method to save the most recently used lists.
        """
        Preferences.Prefs.rsettings.setValue(
            recentNameUnittestEnvironment, self.__recentEnvironment)
        Preferences.Prefs.rsettings.setValue(
            recentNameUnittestFramework, self.__recentFramework)
        Preferences.Prefs.rsettings.setValue(
            recentNameUnittestDiscoverHistory, self.__discoverHistory)
        Preferences.Prefs.rsettings.setValue(
            recentNameUnittestFileHistory, self.__fileHistory)
        Preferences.Prefs.rsettings.setValue(
            recentNameUnittestTestnameHistory, self.__testNameHistory)
        
        Preferences.Prefs.rsettings.sync()
    
    @pyqtSlot()
    def clearRecent(self):
        """
        Public slot to clear the recently used lists.
        """
        # clear histories
        self.__discoverHistory = []
        self.__fileHistory = []
        self.__testNameHistory = []
        
        # clear widgets with histories
        self.discoveryPicker.clear()
        self.testsuitePicker.clear()
        self.testComboBox.clear()
        
        # sync histories
        self.__saveRecent()
    
    def __updateButtonBoxButtons(self):
        """
        Private method to update the state of the buttons of the button box.
        """
        failedAvailable = bool(self.__failedTests)
        
        # Start button
        if self.__mode in (
            UnittestWidgetModes.IDLE, UnittestWidgetModes.STOPPED
        ):
            self.__startButton.setEnabled(
                bool(self.venvComboBox.currentText()) and
                bool(self.frameworkComboBox.currentText()) and
                (
                    (self.discoverCheckBox.isChecked() and
                     bool(self.discoveryPicker.currentText())) or
                    bool(self.testsuitePicker.currentText())
                )
            )
            self.__startButton.setDefault(
                self.__mode == UnittestWidgetModes.IDLE or
                not failedAvailable
            )
        else:
            self.__startButton.setEnabled(False)
            self.__startButton.setDefault(False)
        
        # Start Failed button
        # TODO: not implemented yet (Start Failed button)
        
        # Stop button
        self.__stopButton.setEnabled(
            self.__mode == UnittestWidgetModes.RUNNING)
        self.__stopButton.setDefault(
            self.__mode == UnittestWidgetModes.RUNNING)
        
        # Close button
        self.buttonBox.button(
            QDialogButtonBox.StandardButton.Close
        ).setEnabled(self.__mode in (
            UnittestWidgetModes.IDLE, UnittestWidgetModes.STOPPED
        ))
    
    def __updateProgress(self):
        """
        Private method update the progress indicators.
        """
        self.progressCounterRunCount.setText(
            str(self.__runCount))
        self.progressCounterRemCount.setText(
            str(self.__totalCount - self.__runCount))
        self.progressProgressBar.setMaximum(self.__totalCount)
        self.progressProgressBar.setValue(self.__runCount)
    
    def __setIdleMode(self):
        """
        Private method to switch the widget to idle mode.
        """
        self.__mode = UnittestWidgetModes.IDLE
        self.__updateButtonBoxButtons()
        self.tabWidget.setCurrentIndex(0)
    
    def __setRunningMode(self):
        """
        Private method to switch the widget to running mode.
        """
        self.__mode = UnittestWidgetModes.RUNNING
        
        self.__totalCount = 0
        self.__runCount = 0
        
        self.__coverageFile = ""
        # TODO: implement the handling of the 'Show Coverage' button
        
        self.sbLabel.setText(self.tr("Running"))
        self.tabWidget.setCurrentIndex(1)
        self.__updateButtonBoxButtons()
        self.__updateProgress()
        
        self.__resultsModel.clear()
    
    def __setStoppedMode(self):
        """
        Private method to switch the widget to stopped mode.
        """
        self.__mode = UnittestWidgetModes.STOPPED
        
        self.__updateButtonBoxButtons()
        
        self.raise_()
        self.activateWindow()
    
    @pyqtSlot(QAbstractButton)
    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked
        @type QAbstractButton
        """
        if button == self.__startButton:
            self.startTests()
            self.__saveRecent()
        elif button == self.__stopButton:
            self.__stopTests()
#        elif button == self.__startFailedButton:
#            self.startTests(failedOnly=True)
    
    @pyqtSlot(int)
    def on_venvComboBox_currentIndexChanged(self, index):
        """
        Private slot handling the selection of a virtual environment.
        
        @param index index of the selected environment
        @type int
        """
        self.__populateTestFrameworkComboBox()
        self.__updateButtonBoxButtons()
        
        self.versionsButton.setEnabled(bool(self.venvComboBox.currentText()))
    
    @pyqtSlot()
    def on_versionsButton_clicked(self):
        """
        Private slot to show the versions of available plugins.
        """
        venvName = self.venvComboBox.currentText()
        if venvName:
            headerText = self.tr("<h3>Versions of Frameworks and their"
                                 " Plugins</h3>")
            versionsText = ""
            interpreter = self.__venvManager.getVirtualenvInterpreter(venvName)
            for framework in sorted(
                self.__frameworkRegistry.getFrameworks().keys()
            ):
                executor = self.__frameworkRegistry.createExecutor(
                    framework, self)
                versions = executor.getVersions(interpreter)
                if versions:
                    txt = "<p><strong>{0} {1}</strong>".format(
                        versions["name"], versions["version"])
                    
                    if versions["plugins"]:
                        txt += "<table>"
                        for pluginVersion in versions["plugins"]:
                            txt += self.tr(
                                "<tr><td>{0}</td><td>{1}</td></tr>"
                            ).format(
                                pluginVersion["name"], pluginVersion["version"]
                            )
                        txt += "</table>"
                    txt += "</p>"
                    
                    versionsText += txt
            
            if not versionsText:
                versionsText = self.tr("No version information available.")
            
            EricMessageBox.information(
                self,
                self.tr("Versions"),
                headerText + versionsText
            )
    
    @pyqtSlot()
    def startTests(self, failedOnly=False):
        """
        Public slot to start the test run.
        
        @param failedOnly flag indicating to run only failed tests
        @type bool
        """
        if self.__mode == UnittestWidgetModes.RUNNING:
            return
        
        self.__recentEnvironment = self.venvComboBox.currentText()
        self.__recentFramework = self.frameworkComboBox.currentText()
        
        discover = self.discoverCheckBox.isChecked()
        if discover:
            discoveryStart = self.discoveryPicker.currentText()
            testFileName = ""
            testName = ""
            
            if discoveryStart:
                self.__insertDiscovery(discoveryStart)
        else:
            discoveryStart = ""
            testFileName = self.testsuitePicker.currentText()
            if testFileName:
                self.__insertTestFile(testFileName)
            testName = self.testComboBox.currentText()
            if testName:
                self.__insertTestName(testName)
            if testFileName and not testName:
                testName = "suite"
        
        self.sbLabel.setText(self.tr("Preparing Testsuite"))
        QCoreApplication.processEvents()
        
        interpreter = self.__venvManager.getVirtualenvInterpreter(
            self.__recentEnvironment)
        config = UTTestConfig(
            interpreter=interpreter,
            discover=self.discoverCheckBox.isChecked(),
            discoveryStart=discoveryStart,
            testFilename=testFileName,
            testName=testName,
            failFast=self.failfastCheckBox.isChecked(),
            collectCoverage=self.coverageCheckBox.isChecked(),
            eraseCoverage=self.coverageEraseCheckBox.isChecked(),
        )
        
        self.__resultsModel.clear()
        self.__testExecutor = self.__frameworkRegistry.createExecutor(
            self.__recentFramework, self)
        self.__testExecutor.collected.connect(self.__testsCollected)
        self.__testExecutor.collectError.connect(self.__testsCollectError)
        self.__testExecutor.startTest.connect(self.__testStarted)
        self.__testExecutor.testResult.connect(self.__processTestResult)
        self.__testExecutor.testFinished.connect(self.__testProcessFinished)
        self.__testExecutor.testRunFinished.connect(self.__testRunFinished)
        self.__testExecutor.stop.connect(self.__testsStopped)
        self.__testExecutor.coverageDataSaved.connect(self.__coverageData)
        
        self.__setRunningMode()
        self.__testExecutor.start(config, [])
    
    @pyqtSlot()
    def __stopTests(self):
        """
        Private slot to stop the current test run.
        """
        self.__testExecutor.stopIfRunning()
    
    @pyqtSlot(list)
    def __testsCollected(self, testNames):
        """
        Private slot handling the 'collected' signal of the executor.
        
        @param testNames list of tuples containing the test id and test name
            of collected tests
        @type list of tuple of (str, str)
        """
        testResults = [
            UTTestResult(
                category=ResultCategory.PENDING,
                status=self.tr("pending"),
                name=name,
                id=id,
                message=desc,
            ) for id, name, desc in testNames
        ]
        self.__resultsModel.setTestResults(testResults)
        
        self.__totalCount = len(testResults)
        self.__updateProgress()
    
    @pyqtSlot(list)
    def __testsCollectError(self, errors):
        """
        Private slot handling the 'collectError' signal of the executor.
        
        @param errors list of tuples containing the test name and a description
            of the error
        @type list of tuple of (str, str)
        """
        testResults = []
        
        for testFile, error in errors:
            if testFile:
                testResults.append(UTTestResult(
                    category=ResultCategory.FAIL,
                    status=self.tr("Failure"),
                    name=testFile,
                    id=testFile,
                    message=self.tr("Collection Error"),
                    extra=error.splitlines()
                ))
            else:
                EricMessageBox.critical(
                    self,
                    self.tr("Collection Error"),
                    self.tr(
                        "<p>There was an error while collecting unit tests."
                        "</p><p>{0}</p>"
                    ).format("<br/>".join(error.splitlines()))
                )
        
        if testResults:
            self.__resultsModel.addTestResults(testResults)
    
    @pyqtSlot(tuple)
    def __testStarted(self, test):
        """
        Private slot handling the 'startTest' signal of the executor.
        
        @param test tuple containing the id, name and short description of the
            tests about to be run
        @type tuple of (str, str, str)
        """
        self.__resultsModel.updateTestResults([
            UTTestResult(
                category=ResultCategory.RUNNING,
                status=self.tr("running"),
                id=test[0],
                name=test[1],
                message="" if test[2] is None else test[2],
            )
        ])
    
    @pyqtSlot(UTTestResult)
    def __processTestResult(self, result):
        """
        Private slot to handle the receipt of a test result object.
        
        @param result test result object
        @type UTTestResult
        """
        self.__runCount += 1
        self.__updateProgress()
        
        self.__resultsModel.updateTestResults([result])
    
    @pyqtSlot(list, str)
    def __testProcessFinished(self, results, output):
        """
        Private slot to handle the 'testFinished' signal of the executor.
        
        @param results list of test result objects (if not sent via the
            'testResult' signal
        @type list of UTTestResult
        @param output string containing the test process output (if any)
        @type str
        """
        self.__setStoppedMode()
        self.__testExecutor = None
    
    @pyqtSlot(int, float)
    def __testRunFinished(self, noTests, duration):
        """
        Private slot to handle the 'testRunFinished' signal of the executor.
        
        @param noTests number of tests run by the executor
        @type int
        @param duration time needed in seconds to run the tests
        @type float
        """
        self.sbLabel.setText(
            self.tr("Ran %n test(s) in {0}s", "", noTests).format(
                locale.format_string("%.3f", duration, grouping=True)
            )
        )
        
        self.__setStoppedMode()
    
    @pyqtSlot()
    def __testsStopped(self):
        """
        Private slot to handle the 'stop' signal of the executor.
        """
        self.sbLabel.setText(self.tr("Ran %n test(s)", "", self.__runCount))
        
        self.__setStoppedMode()
    
    @pyqtSlot(str)
    def __coverageData(self, coverageFile):
        """
        Private slot to handle the 'coverageData' signal of the executor.
        
        @param coverageFile file containing the coverage data
        @type str
        """
        self.__coverageFile = coverageFile
        
        # TODO: implement the handling of the 'Show Coverage' button


class UnittestWindow(EricMainWindow):
    """
    Main window class for the standalone dialog.
    """
    def __init__(self, testfile=None, parent=None):
        """
        Constructor
        
        @param testfile file name of the test script to open
        @type str
        @param parent reference to the parent widget
        @type QWidget
        """
        super().__init__(parent)
        self.__cw = UnittestWidget(testfile=testfile, parent=self)
        self.__cw.installEventFilter(self)
        size = self.__cw.size()
        self.setCentralWidget(self.__cw)
        self.resize(size)
        
        self.setStyle(Preferences.getUI("Style"),
                      Preferences.getUI("StyleSheet"))
        
        self.__cw.buttonBox.accepted.connect(self.close)
        self.__cw.buttonBox.rejected.connect(self.close)
    
    def eventFilter(self, obj, event):
        """
        Public method to filter events.
        
        @param obj reference to the object the event is meant for (QObject)
        @param event reference to the event object (QEvent)
        @return flag indicating, whether the event was handled (boolean)
        """
        if event.type() == QEvent.Type.Close:
            QCoreApplication.exit(0)
            return True
        
        return False


def clearSavedHistories(self):
    """
    Function to clear the saved history lists.
    """
    Preferences.Prefs.rsettings.setValue(
        recentNameUnittestDiscoverHistory, [])
    Preferences.Prefs.rsettings.setValue(
        recentNameUnittestFileHistory, [])
    Preferences.Prefs.rsettings.setValue(
        recentNameUnittestTestnameHistory, [])
    
    Preferences.Prefs.rsettings.sync()

eric ide

mercurial