--- a/PyUnit/UnittestDialog.py Sat Mar 02 11:17:15 2019 +0100 +++ b/PyUnit/UnittestDialog.py Fri Apr 05 19:06:39 2019 +0200 @@ -18,7 +18,7 @@ from PyQt5.QtCore import pyqtSignal, QEvent, Qt, pyqtSlot from PyQt5.QtGui import QColor from PyQt5.QtWidgets import QWidget, QDialog, QApplication, QDialogButtonBox, \ - QListWidgetItem, QComboBox + QListWidgetItem, QComboBox, QTreeWidgetItem from E5Gui.E5Application import e5App from E5Gui import E5MessageBox @@ -37,26 +37,33 @@ """ Class implementing the UI to the pyunit package. - @signal unittestFile(str, int, int) emitted to show the source of a + @signal unittestFile(str, int, bool) emitted to show the source of a unittest file @signal unittestStopped() emitted after a unit test was run """ - unittestFile = pyqtSignal(str, int, int) + unittestFile = pyqtSignal(str, int, bool) unittestStopped = pyqtSignal() - def __init__(self, prog=None, dbs=None, ui=None, fromEric=False, - parent=None, name=None): + TestCaseNameRole = Qt.UserRole + TestCaseFileRole = Qt.UserRole + 1 + + ErrorsInfoRole = Qt.UserRole + + def __init__(self, prog=None, dbs=None, ui=None, parent=None, name=None): """ Constructor @param prog filename of the program to open + @type str @param dbs reference to the debug server object. It is an indication - whether we were called from within the eric6 IDE + whether we were called from within the eric6 IDE. + @type DebugServer @param ui reference to the UI object - @param fromEric flag indicating an instantiation from within the - eric IDE (boolean) - @param parent parent widget of this dialog (QWidget) - @param name name of this dialog (string) + @type UserInterface + @param parent parent widget of this dialog + @type QWidget + @param name name of this dialog + @type str """ super(UnittestDialog, self).__init__(parent) if name: @@ -68,6 +75,18 @@ self.testsuitePicker.setSizeAdjustPolicy( QComboBox.AdjustToMinimumContentsLength) + self.discoveryPicker.setMode(E5PathPickerModes.DirectoryMode) + self.discoveryPicker.setInsertPolicy(QComboBox.InsertAtTop) + self.discoveryPicker.setSizeAdjustPolicy( + QComboBox.AdjustToMinimumContentsLength) + + self.discoverButton = self.buttonBox.addButton( + self.tr("Discover"), QDialogButtonBox.ActionRole) + self.discoverButton.setToolTip(self.tr( + "Discover tests")) + self.discoverButton.setWhatsThis(self.tr( + """<b>Discover</b>""" + """<p>This button starts a discovery of available tests.</p>""")) self.startButton = self.buttonBox.addButton( self.tr("Start"), QDialogButtonBox.ActionRole) self.startButton.setToolTip(self.tr( @@ -89,12 +108,13 @@ self.stopButton.setWhatsThis(self.tr( """<b>Stop Test</b>""" """<p>This button stops a running unittest.</p>""")) + self.discoverButton.setEnabled(False) self.stopButton.setEnabled(False) self.startButton.setDefault(True) self.startFailedButton.setEnabled(False) - self.dbs = dbs - self.__fromEric = fromEric + self.__dbs = dbs + self.__forProject = False self.setWindowFlags( self.windowFlags() | Qt.WindowFlags( @@ -103,17 +123,36 @@ self.setWindowTitle(self.tr("Unittest")) if dbs: self.ui = ui + + self.debuggerCheckBox.setChecked(True) + + # virtual environment manager is only used in the integrated + # variant + self.__venvManager = e5App().getObject("VirtualEnvManager") + self.__populateVenvComboBox() + self.__venvManager.virtualEnvironmentAdded.connect( + self.__populateVenvComboBox) + self.__venvManager.virtualEnvironmentRemoved.connect( + self.__populateVenvComboBox) + self.__venvManager.virtualEnvironmentChanged.connect( + self.__populateVenvComboBox) else: - self.localCheckBox.hide() + self.__venvManager = None + self.debuggerCheckBox.setVisible(False) + self.venvComboBox.setVisible(bool(self.__venvManager)) + self.venvLabel.setVisible(bool(self.__venvManager)) + self.__setProgressColor("green") self.progressLed.setDarkFactor(150) self.progressLed.off() + self.discoverHistory = [] self.fileHistory = [] self.testNameHistory = [] self.running = False self.savedModulelist = None self.savedSysPath = sys.path + self.savedCwd = os.getcwd() if prog: self.insertProg(prog) @@ -128,16 +167,17 @@ self.__failedTests = [] # now connect the debug server signals if called from the eric6 IDE - if self.dbs: - self.dbs.utPrepared.connect(self.__UTPrepared) - self.dbs.utFinished.connect(self.__setStoppedMode) - self.dbs.utStartTest.connect(self.testStarted) - self.dbs.utStopTest.connect(self.testFinished) - self.dbs.utTestFailed.connect(self.testFailed) - self.dbs.utTestErrored.connect(self.testErrored) - self.dbs.utTestSkipped.connect(self.testSkipped) - self.dbs.utTestFailedExpected.connect(self.testFailedExpected) - self.dbs.utTestSucceededUnexpected.connect( + if self.__dbs: + self.__dbs.utDiscovered.connect(self.__UTDiscovered) + self.__dbs.utPrepared.connect(self.__UTPrepared) + self.__dbs.utFinished.connect(self.__setStoppedMode) + self.__dbs.utStartTest.connect(self.testStarted) + self.__dbs.utStopTest.connect(self.testFinished) + self.__dbs.utTestFailed.connect(self.testFailed) + self.__dbs.utTestErrored.connect(self.testErrored) + self.__dbs.utTestSkipped.connect(self.testSkipped) + self.__dbs.utTestFailedExpected.connect(self.testFailedExpected) + self.__dbs.utTestSucceededUnexpected.connect( self.testSucceededUnexpected) self.__editors = [] @@ -148,9 +188,23 @@ @param evt key press event to handle (QKeyEvent) """ - if evt.key() == Qt.Key_Escape and self.__fromEric: + if evt.key() == Qt.Key_Escape and self.__dbs: self.close() + def __populateVenvComboBox(self): + """ + Private method to (re-)populate the virtual environments selector. + """ + currentText = self.venvComboBox.currentText() + 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 __setProgressColor(self, color): """ Private methode to set the color of the progress color label. @@ -158,7 +212,44 @@ @param color colour to be shown (string) """ self.progressLed.setColor(QColor(color)) + + def setProjectMode(self, forProject): + """ + Public method to set the project mode of the dialog. + @param forProject flag indicating to run for the open project + @type bool + """ + self.__forProject = forProject + if forProject: + project = e5App().getObject("Project") + if project.isOpen(): + self.insertDiscovery(project.getProjectPath()) + else: + self.insertDiscovery("") + else: + self.insertDiscovery("") + + self.discoveryList.clear() + self.tabWidget.setCurrentIndex(0) + + def insertDiscovery(self, start): + """ + Public slot to insert the discovery start directory into the + discoveryPicker object. + + @param start start directory name to be inserted + @type str + """ + # prepend the given directory to the discovery picker + if start is None: + start = "" + if start in self.discoverHistory: + self.discoverHistory.remove(start) + self.discoverHistory.insert(0, start) + self.discoveryPicker.clear() + self.discoveryPicker.addItems(self.discoverHistory) + def insertProg(self, prog): """ Public slot to insert the filename prog into the testsuitePicker @@ -174,7 +265,7 @@ self.fileHistory.insert(0, prog) self.testsuitePicker.clear() self.testsuitePicker.addItems(self.fileHistory) - + def insertTestName(self, testName): """ Public slot to insert a test name into the testComboBox object. @@ -189,19 +280,19 @@ self.testNameHistory.insert(0, testName) self.testComboBox.clear() self.testComboBox.addItems(self.testNameHistory) - + @pyqtSlot() def on_testsuitePicker_aboutToShowPathPickerDialog(self): """ Private slot called before the test suite selection dialog is shown. """ - if self.dbs: + if self.__dbs: py2Extensions = \ ' '.join(["*{0}".format(ext) - for ext in self.dbs.getExtensions('Python2')]) + for ext in self.__dbs.getExtensions('Python2')]) py3Extensions = \ ' '.join(["*{0}".format(ext) - for ext in self.dbs.getExtensions('Python3')]) + for ext in self.__dbs.getExtensions('Python3')]) fileFilter = self.tr( "Python3 Files ({1});;Python2 Files ({0});;All Files (*)")\ .format(py2Extensions, py3Extensions) @@ -209,6 +300,15 @@ fileFilter = self.tr("Python Files (*.py);;All Files (*)") self.testsuitePicker.setFilters(fileFilter) + defaultDirectory = Preferences.getMultiProject("Workspace") + if not defaultDirectory: + defaultDirectory = os.path.expanduser("~") + if self.__dbs: + project = e5App().getObject("Project") + if self.__forProject and project.isOpen(): + defaultDirectory = project.getProjectPath() + self.testsuitePicker.setDefaultDirectory(defaultDirectory) + @pyqtSlot(str) def on_testsuitePicker_pathSelected(self, suite): """ @@ -218,103 +318,114 @@ @type str """ self.insertProg(suite) - + @pyqtSlot(str) - def on_testsuitePicker_editTextChanged(self, txt): + def on_testsuitePicker_editTextChanged(self, path): """ - Private slot to handle changes of the test file name. + Private slot handling changes of the test suite path. - @param txt name of the test file (string) + @param path path of the test suite file + @type str + """ + self.startFailedButton.setEnabled(False) + + @pyqtSlot(bool) + def on_discoverCheckBox_toggled(self, checked): """ - if self.dbs: - exts = self.dbs.getExtensions("Python2") - flags = Utilities.extractFlagsFromFile(txt) - if txt.endswith(exts) or \ - ("FileType" in flags and - flags["FileType"] in ["Python", "Python2"]): - self.coverageCheckBox.setChecked(False) - self.coverageCheckBox.setEnabled(False) - self.localCheckBox.setChecked(False) - self.localCheckBox.setEnabled(False) - return + Private slot handling state changes of the 'discover' checkbox. + + @param checked state of the checkbox + @type bool + """ + self.discoverButton.setEnabled(checked) + self.discoveryList.clear() - self.coverageCheckBox.setEnabled(True) - self.localCheckBox.setEnabled(True) - + if not bool(self.discoveryPicker.currentText()): + if self.__forProject: + project = e5App().getObject("Project") + if project.isOpen(): + self.insertDiscovery(project.getProjectPath()) + return + + self.insertDiscovery(Preferences.getMultiProject("Workspace")) + def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @param button button that was clicked (QAbstractButton) """ - if button == self.startButton: - self.on_startButton_clicked() + if button == self.discoverButton: + self.__discover() + elif button == self.startButton: + self.startTests() elif button == self.stopButton: - self.on_stopButton_clicked() + self.__stopTests() elif button == self.startFailedButton: - self.on_startButton_clicked(failedOnly=True) - + self.startTests(failedOnly=True) + @pyqtSlot() - def on_startButton_clicked(self, failedOnly=False): + def __discover(self): """ - Private slot to start the test. - - @keyparam failedOnly flag indicating to run only failed tests (boolean) + Private slot to discover unit test but don't run them. """ if self.running: return - prog = self.testsuitePicker.currentText() - if not prog: - E5MessageBox.critical( - self, - self.tr("Unittest"), - self.tr("You must enter a test suite file.")) - return + self.discoveryList.clear() - # prepend the selected file to the testsuite combobox - self.insertProg(prog) - self.sbLabel.setText(self.tr("Preparing Testsuite")) + discoveryStart = self.discoveryPicker.currentText() + self.sbLabel.setText(self.tr("Discovering Tests")) QApplication.processEvents() - testFunctionName = self.testComboBox.currentText() - if testFunctionName: - self.insertTestName(testFunctionName) - else: - testFunctionName = "suite" - - # build the module name from the filename without extension - self.testName = os.path.splitext(os.path.basename(prog))[0] - - if self.dbs and not self.localCheckBox.isChecked(): + self.testName = self.tr("Unittest with auto-discovery") + if self.__dbs: + venvName = self.venvComboBox.currentText() + # we are cooperating with the eric6 IDE project = e5App().getObject("Project") - if project.isOpen() and project.isProjectSource(prog): - mainScript = project.getMainScript(True) + if self.__forProject: + mainScript = os.path.abspath(project.getMainScript(True)) clientType = project.getProjectLanguage() - else: - mainScript = os.path.abspath(prog) - flags = Utilities.extractFlagsFromFile(mainScript) - if mainScript.endswith( - tuple(Preferences.getPython("PythonExtensions"))) or \ - ("FileType" in flags and - flags["FileType"] in ["Python", "Python2"]): - clientType = "Python2" + if mainScript: + workdir = os.path.dirname(mainScript) else: - clientType = "" - if failedOnly and self.__failedTests: - failed = [t.split(".", 1)[1] for t in self.__failedTests] + workdir = project.getProjectPath() + sysPath = [workdir] + if not discoveryStart: + discoveryStart = workdir else: - failed = [] - self.__failedTests = [] - self.dbs.remoteUTPrepare( - prog, self.testName, testFunctionName, failed, - self.coverageCheckBox.isChecked(), mainScript, - self.coverageEraseCheckBox.isChecked(), clientType=clientType) + if not discoveryStart: + E5MessageBox.critical( + self, + self.tr("Unittest"), + self.tr("You must enter a start directory for" + " auto-discovery.")) + return + + workdir = "" + clientType = \ + self.__venvManager.getVirtualenvVariant(venvName) + if not clientType: + # assume Python 3 + clientType = "Python3" + sysPath = [] + self.__dbs.remoteUTDiscover(clientType, self.__forProject, + workdir, venvName, sysPath, + discoveryStart) else: - # we are running as an application or in local mode - sys.path = [os.path.dirname(os.path.abspath(prog))] + \ - self.savedSysPath + # we are running as an application + if not discoveryStart: + E5MessageBox.critical( + self, + self.tr("Unittest"), + self.tr("You must enter a start directory for" + " auto-discovery.")) + return + + if discoveryStart: + sys.path = [os.path.abspath(discoveryStart)] + \ + self.savedSysPath # clean up list of imported modules to force a reimport upon # running the test @@ -325,56 +436,438 @@ del(sys.modules[modname]) self.savedModulelist = sys.modules.copy() - # now try to generate the testsuite + # now try to discover the testsuite + os.chdir(discoveryStart) try: - module = __import__(self.testName) - try: - if failedOnly and self.__failedTests: - test = unittest.defaultTestLoader.loadTestsFromNames( - [t.split(".", 1)[1] for t in self.__failedTests], - module) - else: - test = unittest.defaultTestLoader.loadTestsFromName( - testFunctionName, module) - except AttributeError: - test = unittest.defaultTestLoader.loadTestsFromModule( - module) + testLoader = unittest.TestLoader() + test = testLoader.discover(discoveryStart) + if hasattr(testLoader, "errors") and \ + bool(testLoader.errors): + E5MessageBox.critical( + self, + self.tr("Unittest"), + self.tr( + "<p>Unable to discover tests.</p>" + "<p>{0}</p>" + ).format("<br/>".join(testLoader.errors) + .replace("\n", "<br/>")) + ) + self.sbLabel.clear() + else: + testsList = self.__assembleTestCasesList( + test, discoveryStart) + self.__populateDiscoveryResults(testsList) + self.sbLabel.setText( + self.tr("Discovered %n Test(s)", "", + len(testsList))) + self.tabWidget.setCurrentIndex(0) except Exception: exc_type, exc_value, exc_tb = sys.exc_info() E5MessageBox.critical( self, self.tr("Unittest"), self.tr( - "<p>Unable to run test <b>{0}</b>.<br>" - "{1}<br>{2}</p>") + "<p>Unable to discover tests.</p>" + "<p>{0}<br/>{1}</p>") + .format(str(exc_type), + str(exc_value).replace("\n", "<br/>")) + ) + self.sbLabel.clear() + + sys.path = self.savedSysPath + + def __assembleTestCasesList(self, suite, start): + """ + Private method to assemble a list of test cases included in a test + suite. + + @param suite test suite to be inspected + @type unittest.TestSuite + @param start name of directory discovery was started at + @type str + @return list of tuples containing the test case ID, a short description + and the path of the test file name + @rtype list of tuples of (str, str, str) + """ + testCases = [] + for test in suite: + if isinstance(test, unittest.TestSuite): + testCases.extend(self.__assembleTestCasesList(test, start)) + else: + testId = test.id() + if "ModuleImportFailure" not in testId and \ + "LoadTestsFailure" not in testId and \ + "_FailedTest" not in testId: + filename = os.path.join( + start, + test.__module__.replace(".", os.sep) + ".py") + testCases.append( + (test.id(), test.shortDescription(), filename) + ) + return testCases + + def __findDiscoveryItem(self, modulePath): + """ + Private method to find an item given the module path. + + @param modulePath path of the module in dotted notation + @type str + @return reference to the item or None + @rtype QTreeWidgetItem or None + """ + itm = self.discoveryList.topLevelItem(0) + while itm is not None: + if itm.data(0, UnittestDialog.TestCaseNameRole) == modulePath: + return itm + itm = self.discoveryList.itemBelow(itm) + + return None + + def __populateDiscoveryResults(self, tests): + """ + Private method to populate the test discovery results list. + + @param tests list of tuples containing the discovery results + @type list of tuples of (str, str, str) + """ + for test, _testDescription, filename in tests: + testPath = test.split(".") + pitm = None + for index in range(1, len(testPath) + 1): + modulePath = ".".join(testPath[:index]) + itm = self.__findDiscoveryItem(modulePath) + if itm is not None: + pitm = itm + else: + if pitm is None: + itm = QTreeWidgetItem(self.discoveryList, + [testPath[index - 1]]) + else: + itm = QTreeWidgetItem(pitm, + [testPath[index - 1]]) + pitm.setExpanded(True) + itm.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) + itm.setCheckState(0, Qt.Unchecked) + itm.setData(0, UnittestDialog.TestCaseNameRole, modulePath) + if os.path.splitext(os.path.basename(filename))[0] == \ + itm.text(0): + itm.setData(0, UnittestDialog.TestCaseFileRole, + filename) + elif pitm: + fn = pitm.data(0, UnittestDialog.TestCaseFileRole) + if fn: + itm.setData(0, UnittestDialog.TestCaseFileRole, fn) + pitm = itm + + def __selectedTestCases(self, parent=None): + """ + Private method to assemble the list of selected test cases and suites. + + @param parent reference to the parent item + @type QTreeWidgetItem + @return list of selected test cases + @rtype list of str + """ + selectedTests = [] + if parent is None: + # top level + for index in range(self.discoveryList.topLevelItemCount()): + itm = self.discoveryList.topLevelItem(index) + if itm.checkState(0) == Qt.Checked: + selectedTests.append( + itm.data(0, UnittestDialog.TestCaseNameRole)) + # ignore children because they are included implicitly + elif itm.childCount(): + # recursively check children + selectedTests.extend(self.__selectedTestCases(itm)) + + else: + # parent item with children + for index in range(parent.childCount()): + itm = parent.child(index) + if itm.checkState(0) == Qt.Checked: + selectedTests.append( + itm.data(0, UnittestDialog.TestCaseNameRole)) + # ignore children because they are included implicitly + elif itm.childCount(): + # recursively check children + selectedTests.extend(self.__selectedTestCases(itm)) + + return selectedTests + + def __UTDiscovered(self, testCases, exc_type, exc_value): + """ + Private slot to handle the utDiscovered signal. + + If the unittest suite was loaded successfully, we ask the + client to run the test suite. + + @param testCases list of detected test cases + @type str + @param exc_type exception type occured during discovery + @type str + @param exc_value value of exception occured during discovery + @type str + """ + if testCases: + self.__populateDiscoveryResults(testCases) + self.sbLabel.setText( + self.tr("Discovered %n Test(s)", "", + len(testCases))) + self.tabWidget.setCurrentIndex(0) + else: + E5MessageBox.critical( + self, + self.tr("Unittest"), + self.tr("<p>Unable to discover tests.</p>" + "<p>{0}<br/>{1}</p>") + .format(exc_type, exc_value.replace("\n", "<br/>")) + ) + + @pyqtSlot(QTreeWidgetItem, int) + def on_discoveryList_itemDoubleClicked(self, item, column): + """ + Private slot handling the user double clicking an item. + + @param item reference to the item + @type QTreeWidgetItem + @param column column of the double click + @type int + """ + if item: + filename = item.data(0, UnittestDialog.TestCaseFileRole) + if filename: + if self.__dbs: + # running as part of eric IDE + self.unittestFile.emit(filename, 1, False) + else: + self.__openEditor(filename, 1) + + @pyqtSlot() + def startTests(self, failedOnly=False): + """ + Public slot to start the test. + + @keyparam failedOnly flag indicating to run only failed tests (boolean) + """ + if self.running: + return + + discover = self.discoverCheckBox.isChecked() + if discover: + discoveryStart = self.discoveryPicker.currentText() + testFileName = "" + testName = "" + else: + discoveryStart = "" + testFileName = self.testsuitePicker.currentText() + testName = self.testComboBox.currentText() + if testName: + self.insertTestName(testName) + if testFileName and not testName: + testName = "suite" + + if not discover and not testFileName and not testName: + E5MessageBox.critical( + self, + self.tr("Unittest"), + self.tr("You must select auto-discovery or enter a test suite" + " file or a dotted test name.")) + return + + # prepend the selected file to the testsuite combobox + self.insertProg(testFileName) + self.sbLabel.setText(self.tr("Preparing Testsuite")) + QApplication.processEvents() + + if discover: + self.testName = self.tr("Unittest with auto-discovery") + else: + # build the module name from the filename without extension + if testFileName: + self.testName = os.path.splitext( + os.path.basename(testFileName))[0] + elif testName: + self.testName = testName + else: + self.testName = self.tr("<Unnamed Test>") + + if failedOnly: + testCases = [] + else: + testCases = self.__selectedTestCases() + + if not testCases and self.discoveryList.topLevelItemCount(): + ok = E5MessageBox.yesNo( + self, + self.tr("Unittest"), + self.tr("""No test case has been selected. Shall all""" + """ test cases be run?""")) + if not ok: + return + + if self.__dbs: + venvName = self.venvComboBox.currentText() + + # we are cooperating with the eric6 IDE + project = e5App().getObject("Project") + if self.__forProject: + mainScript = os.path.abspath(project.getMainScript(True)) + clientType = project.getProjectLanguage() + if mainScript: + workdir = os.path.dirname(mainScript) + else: + workdir = project.getProjectPath() + sysPath = [workdir] + coverageFile = os.path.splitext(mainScript)[0] + if discover and not discoveryStart: + discoveryStart = workdir + else: + if discover: + if not discoveryStart: + E5MessageBox.critical( + self, + self.tr("Unittest"), + self.tr("You must enter a start directory for" + " auto-discovery.")) + return + + coverageFile = os.path.join(discoveryStart, "unittest") + workdir = "" + clientType = \ + self.__venvManager.getVirtualenvVariant(venvName) + if not clientType: + # assume Python 3 + clientType = "Python3" + elif testFileName: + mainScript = os.path.abspath(testFileName) + flags = Utilities.extractFlagsFromFile(mainScript) + workdir = os.path.dirname(mainScript) + if mainScript.endswith( + tuple(Preferences.getPython("PythonExtensions"))) or \ + ("FileType" in flags and + flags["FileType"] in ["Python", "Python2"]): + clientType = "Python2" + else: + # if it is not Python2 it must be Python3! + clientType = "Python3" + coverageFile = os.path.splitext(mainScript)[0] + else: + coverageFile = os.path.abspath("unittest") + workdir = "" + clientType = \ + self.__venvManager.getVirtualenvVariant(venvName) + if not clientType: + # assume Python 3 + clientType = "Python3" + sysPath = [] + if failedOnly and self.__failedTests: + failed = self.__failedTests[:] + if discover: + workdir = discoveryStart + discover = False + else: + failed = [] + self.__failedTests = [] + self.__dbs.remoteUTPrepare( + testFileName, self.testName, testName, failed, + self.coverageCheckBox.isChecked(), coverageFile, + self.coverageEraseCheckBox.isChecked(), clientType=clientType, + forProject=self.__forProject, workdir=workdir, + venvName=venvName, syspath=sysPath, + discover=discover, discoveryStart=discoveryStart, + testCases=testCases, debug=self.debuggerCheckBox.isChecked()) + else: + # we are running as an application + if discover and not discoveryStart: + E5MessageBox.critical( + self, + self.tr("Unittest"), + self.tr("You must enter a start directory for" + " auto-discovery.")) + return + + if testFileName: + sys.path = [os.path.dirname(os.path.abspath(testFileName))] + \ + self.savedSysPath + elif discoveryStart: + sys.path = [os.path.abspath(discoveryStart)] + \ + self.savedSysPath + + # clean up list of imported modules to force a reimport upon + # running the test + if self.savedModulelist: + for modname in list(sys.modules.keys()): + if modname not in self.savedModulelist: + # delete it + del(sys.modules[modname]) + self.savedModulelist = sys.modules.copy() + + os.chdir(self.savedCwd) + + # now try to generate the testsuite + try: + testLoader = unittest.TestLoader() + if failedOnly and self.__failedTests: + failed = self.__failedTests[:] + if discover: + os.chdir(discoveryStart) + discover = False + else: + failed = [] + if discover: + if testCases: + test = testLoader.loadTestsFromNames(testCases) + else: + test = testLoader.discover(discoveryStart) + else: + if testFileName: + module = __import__(self.testName) + else: + module = None + if failedOnly and self.__failedTests: + if module: + failed = [t.split(".", 1)[1] + for t in self.__failedTests] + else: + failed = self.__failedTests[:] + test = testLoader.loadTestsFromNames( + failed, module) + else: + test = testLoader.loadTestsFromName( + testName, module) + except Exception: + exc_type, exc_value, exc_tb = sys.exc_info() + E5MessageBox.critical( + self, + self.tr("Unittest"), + self.tr( + "<p>Unable to run test <b>{0}</b>.</p>" + "<p>{1}<br/>{2}</p>") .format(self.testName, str(exc_type), - str(exc_value))) + str(exc_value).replace("\n", "<br/>")) + ) return # now set up the coverage stuff if self.coverageCheckBox.isChecked(): - if self.dbs: - # we are cooperating with the eric6 IDE - project = e5App().getObject("Project") - if project.isOpen() and project.isProjectSource(prog): - mainScript = project.getMainScript(True) - if not mainScript: - mainScript = os.path.abspath(prog) - else: - mainScript = os.path.abspath(prog) + if discover: + covname = os.path.join(discoveryStart, "unittest") + elif testFileName: + covname = \ + os.path.splitext(os.path.abspath(testFileName))[0] else: - mainScript = os.path.abspath(prog) + covname = "unittest" from DebugClients.Python.coverage import coverage - cover = coverage( - data_file="{0}.coverage".format( - os.path.splitext(mainScript)[0])) + cover = coverage(data_file="{0}.coverage".format(covname)) if self.coverageEraseCheckBox.isChecked(): cover.erase() else: cover = None - self.testResult = QtTestResult(self) + self.testResult = QtTestResult( + self, self.failfastCheckBox.isChecked()) self.totalTests = test.countTestCases() self.__failedTests = [] self.__setRunningMode() @@ -386,7 +879,7 @@ cover.save() self.__setStoppedMode() sys.path = self.savedSysPath - + def __UTPrepared(self, nrTests, exc_type, exc_value): """ Private slot to handle the utPrepared signal. @@ -403,24 +896,28 @@ self, self.tr("Unittest"), self.tr( - "<p>Unable to run test <b>{0}</b>.<br>{1}<br>{2}</p>") - .format(self.testName, exc_type, exc_value)) + "<p>Unable to run test <b>{0}</b>.</p>" + "<p>{1}<br/>{2}</p>") + .format(self.testName, exc_type, + exc_value.replace("\n", "<br/>")) + ) return - + self.totalTests = nrTests self.__setRunningMode() - self.dbs.remoteUTRun() - + self.__dbs.remoteUTRun(debug=self.debuggerCheckBox.isChecked(), + failfast=self.failfastCheckBox.isChecked()) + @pyqtSlot() - def on_stopButton_clicked(self): + def __stopTests(self): """ Private slot to stop the test. """ - if self.dbs and not self.localCheckBox.isChecked(): - self.dbs.remoteUTStop() + if self.__dbs: + self.__dbs.remoteUTStop() elif self.testResult: self.testResult.stop() - + def on_errorsListWidget_currentTextChanged(self, text): """ Private slot to handle the highlighted signal. @@ -437,12 +934,13 @@ itm = foundItems[0] self.testsListWidget.setCurrentItem(itm) self.testsListWidget.scrollToItem(itm) - + def __setRunningMode(self): """ Private method to set the GUI in running mode. """ self.running = True + self.tabWidget.setCurrentIndex(1) # reset counters and error infos self.runCount = 0 @@ -452,7 +950,7 @@ self.expectedFailureCount = 0 self.unexpectedSuccessCount = 0 self.remainingCount = self.totalTests - + # reset the GUI self.progressCounterRunCount.setText(str(self.runCount)) self.progressCounterRemCount.setText(str(self.remainingCount)) @@ -463,20 +961,25 @@ str(self.expectedFailureCount)) self.progressCounterUnexpectedSuccessCount.setText( str(self.unexpectedSuccessCount)) + self.errorsListWidget.clear() self.testsListWidget.clear() + self.progressProgressBar.setRange(0, self.totalTests) self.__setProgressColor("green") self.progressProgressBar.reset() + self.stopButton.setEnabled(True) self.startButton.setEnabled(False) + self.startFailedButton.setEnabled(False) self.stopButton.setDefault(True) + self.sbLabel.setText(self.tr("Running")) self.progressLed.on() QApplication.processEvents() self.startTime = time.time() - + def __setStoppedMode(self): """ Private method to set the GUI in stopped mode. @@ -485,27 +988,26 @@ self.timeTaken = float(self.stopTime - self.startTime) self.running = False + failedAvailable = bool(self.__failedTests) self.startButton.setEnabled(True) - self.startFailedButton.setEnabled(bool(self.__failedTests)) + self.startFailedButton.setEnabled(failedAvailable) self.stopButton.setEnabled(False) - if self.__failedTests: + if failedAvailable: self.startFailedButton.setDefault(True) self.startButton.setDefault(False) else: self.startFailedButton.setDefault(False) self.startButton.setDefault(True) - if self.runCount == 1: - self.sbLabel.setText( - self.tr("Ran {0} test in {1:.3f}s") - .format(self.runCount, self.timeTaken)) - else: - self.sbLabel.setText( - self.tr("Ran {0} tests in {1:.3f}s") - .format(self.runCount, self.timeTaken)) + self.sbLabel.setText( + self.tr("Ran %n test(s) in {0:.3f}s", "", self.runCount) + .format(self.timeTaken)) self.progressLed.off() self.unittestStopped.emit() - + + self.raise_() + self.activateWindow() + def testFailed(self, test, exc, testId): """ Public method called if a test fails. @@ -517,10 +1019,10 @@ self.failCount += 1 self.progressCounterFailureCount.setText(str(self.failCount)) itm = QListWidgetItem(self.tr("Failure: {0}").format(test)) - itm.setData(Qt.UserRole, (test, exc)) + itm.setData(UnittestDialog.ErrorsInfoRole, (test, exc)) self.errorsListWidget.insertItem(0, itm) self.__failedTests.append(testId) - + def testErrored(self, test, exc, testId): """ Public method called if a test errors. @@ -532,10 +1034,10 @@ self.errorCount += 1 self.progressCounterErrorCount.setText(str(self.errorCount)) itm = QListWidgetItem(self.tr("Error: {0}").format(test)) - itm.setData(Qt.UserRole, (test, exc)) + itm.setData(UnittestDialog.ErrorsInfoRole, (test, exc)) self.errorsListWidget.insertItem(0, itm) self.__failedTests.append(testId) - + def testSkipped(self, test, reason, testId): """ Public method called if a test was skipped. @@ -549,7 +1051,7 @@ itm = QListWidgetItem(self.tr(" Skipped: {0}").format(reason)) itm.setForeground(Qt.blue) self.testsListWidget.insertItem(1, itm) - + def testFailedExpected(self, test, exc, testId): """ Public method called if a test fails expectedly. @@ -564,7 +1066,7 @@ itm = QListWidgetItem(self.tr(" Expected Failure")) itm.setForeground(Qt.blue) self.testsListWidget.insertItem(1, itm) - + def testSucceededUnexpected(self, test, testId): """ Public method called if a test succeeds unexpectedly. @@ -578,7 +1080,7 @@ itm = QListWidgetItem(self.tr(" Unexpected Success")) itm.setForeground(Qt.red) self.testsListWidget.insertItem(1, itm) - + def testStarted(self, test, doc): """ Public method called if a test is about to be run. @@ -589,9 +1091,9 @@ if doc: self.testsListWidget.insertItem(0, " {0}".format(doc)) self.testsListWidget.insertItem(0, test) - if self.dbs is None or self.localCheckBox.isChecked(): + if self.__dbs is None: QApplication.processEvents() - + def testFinished(self): """ Public method called if a test has finished. @@ -610,7 +1112,7 @@ elif self.failCount: self.__setProgressColor("orange") self.progressProgressBar.setValue(self.runCount) - + def on_errorsListWidget_itemDoubleClicked(self, lbitem): """ Private slot called by doubleclicking an errorlist entry. @@ -625,10 +1127,10 @@ self.errListIndex = self.errorsListWidget.row(lbitem) text = lbitem.text() self.on_errorsListWidget_currentTextChanged(text) - + # get the error info - test, tracebackText = lbitem.data(Qt.UserRole) - + test, tracebackText = lbitem.data(UnittestDialog.ErrorsInfoRole) + # now build the dialog from .Ui_UnittestStacktraceDialog import Ui_UnittestStacktraceDialog self.dlg = QDialog(self) @@ -649,7 +1151,7 @@ # and now fire it up self.dlg.show() self.dlg.exec_() - + def __showSource(self): """ Private slot to show the source of a traceback in an eric6 editor. @@ -664,9 +1166,9 @@ break if fmatch: fn, ln = fmatch.group(1, 2) - if self.dbs: + if self.__dbs: # running as part of eric IDE - self.unittestFile.emit(fn, int(ln), 1) + self.unittestFile.emit(fn, int(ln), True) else: self.__openEditor(fn, int(ln)) @@ -720,15 +1222,19 @@ For more details see pyunit.py of the standard Python distribution. """ - def __init__(self, parent): + def __init__(self, parent, failfast): """ Constructor - @param parent The parent widget. + @param parent reference to the parent widget + @type UnittestDialog + @param failfast flag indicating to stop at the first error + @type bool """ super(QtTestResult, self).__init__() self.parent = parent - + self.failfast = failfast + def addFailure(self, test, err): """ Public method called if a test failed. @@ -739,7 +1245,7 @@ super(QtTestResult, self).addFailure(test, err) tracebackLines = self._exc_info_to_string(err, test) self.parent.testFailed(str(test), tracebackLines, test.id()) - + def addError(self, test, err): """ Public method called if a test errored. @@ -750,7 +1256,7 @@ super(QtTestResult, self).addError(test, err) tracebackLines = self._exc_info_to_string(err, test) self.parent.testErrored(str(test), tracebackLines, test.id()) - + def addSkip(self, test, reason): """ Public method called if a test was skipped. @@ -760,7 +1266,7 @@ """ super(QtTestResult, self).addSkip(test, reason) self.parent.testSkipped(str(test), reason, test.id()) - + def addExpectedFailure(self, test, err): """ Public method called if a test failed expected. @@ -771,7 +1277,7 @@ super(QtTestResult, self).addExpectedFailure(test, err) tracebackLines = self._exc_info_to_string(err, test) self.parent.testFailedExpected(str(test), tracebackLines, test.id()) - + def addUnexpectedSuccess(self, test): """ Public method called if a test succeeded expectedly. @@ -780,7 +1286,7 @@ """ super(QtTestResult, self).addUnexpectedSuccess(test) self.parent.testSucceededUnexpected(str(test), test.id()) - + def startTest(self, test): """ Public method called at the start of a test. @@ -812,7 +1318,7 @@ @param parent reference to the parent widget (QWidget) """ super(UnittestWindow, self).__init__(parent) - self.cw = UnittestDialog(prog=prog, parent=self) + self.cw = UnittestDialog(prog, parent=self) self.cw.installEventFilter(self) size = self.cw.size() self.setCentralWidget(self.cw) @@ -820,6 +1326,9 @@ 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): """