Tue, 26 Mar 2019 19:29:30 +0100
UnittestDialog: implemented the remote part fo 'discover only'.
--- a/DebugClients/Python/DebugClientBase.py Mon Mar 25 20:18:47 2019 +0100 +++ b/DebugClients/Python/DebugClientBase.py Tue Mar 26 19:29:30 2019 +0100 @@ -801,6 +801,47 @@ elif method == "RequestCompletion": self.__completionList(params["text"]) + elif method == "RequestUTDiscover": + if params["syspath"]: + sys.path = params["syspath"] + sys.path + + discoveryStart = params["discoverystart"] + if not discoveryStart: + discoveryStart = params["workdir"] + + os.chdir(params["discoverystart"]) + + # set the system exception handling function to ensure, that + # we report on all unhandled exceptions + sys.excepthook = self.__unhandled_exception + self.__interceptSignals() + + try: + import unittest + testLoader = unittest.TestLoader() + test = testLoader.discover(discoveryStart) + if hasattr(testLoader, "errors") and \ + bool(testLoader.errors): + self.sendJsonCommand("ResponseUTDiscover", { + "testCasesList": [], + "exception": "DiscoveryError", + "message": "\n\n".join(testLoader.errors), + }) + else: + testsList = self.__assembleTestCasesList(test) + self.sendJsonCommand("ResponseUTDiscover", { + "testCasesList": testsList, + "exception": "", + "message": "", + }) + except Exception: + exc_type, exc_value, exc_tb = sys.exc_info() + self.sendJsonCommand("ResponseUTDiscover", { + "testCasesList": [], + "exception": exc_type.__name__, + "message": str(exc_value), + }) + elif method == "RequestUTPrepare": if params["syspath"]: sys.path = params["syspath"] + sys.path @@ -818,12 +859,16 @@ try: import unittest + testLoader = unittest.TestLoader() if params["discover"]: discoveryStart = params["discoverystart"] if not discoveryStart: discoveryStart = params["workdir"] - self.test = unittest.defaultTestLoader.discover( - discoveryStart) + if params["testcases"]: + self.test = testLoader.loadTestsFromNames( + params["testcases"]) + else: + self.test = testLoader.discover(discoveryStart) else: if params["filename"]: utModule = imp.load_source( @@ -836,18 +881,17 @@ for t in params["failed"]] else: failed = params["failed"][:] - self.test = unittest.defaultTestLoader\ - .loadTestsFromNames(failed, utModule) + self.test = testLoader.loadTestsFromNames( + failed, utModule) else: - self.test = unittest.defaultTestLoader\ - .loadTestsFromName(params["testfunctionname"], - utModule) + self.test = testLoader.loadTestsFromName( + params["testfunctionname"], utModule) except Exception: exc_type, exc_value, exc_tb = sys.exc_info() self.sendJsonCommand("ResponseUTPrepared", { "count": 0, "exception": exc_type.__name__, - "message": str(exc_value) + "<br/>" + str(params), + "message": str(exc_value), }) return @@ -888,6 +932,30 @@ self.fork_child = (params["target"] == 'child') self.eventExit = True + def __assembleTestCasesList(self, suite): + """ + Private method to assemble a list of test cases included in a test + suite. + + @param suite test suite to be inspected + @type unittest.TestSuite + @return list of tuples containing the test case ID and short + description + @rtype list of tuples of (str, str) + """ + import unittest + testCases = [] + for test in suite: + if isinstance(test, unittest.TestSuite): + testCases.extend(self.__assembleTestCasesList(test)) + else: + testId = test.id() + if "ModuleImportFailure" not in testId and \ + "LoadTestsFailure" not in testId and \ + "_FailedTest" not in testId: + testCases.append((test.id(), test.shortDescription())) + return testCases + def sendJsonCommand(self, method, params): """ Public method to send a single command or response to the IDE.
--- a/Debugger/DebugServer.py Mon Mar 25 20:18:47 2019 +0100 +++ b/Debugger/DebugServer.py Tue Mar 26 19:29:30 2019 +0100 @@ -94,6 +94,8 @@ unplanned) @signal clientInterpreterChanged(str) emitted to signal a change of the client interpreter + @signal utDiscovered(testCases, exc_type, exc_value) emitted after the + client has performed a test case discovery action @signal utPrepared(nrTests, exc_type, exc_value) emitted after the client has loaded a unittest suite @signal utFinished() emitted after the client signalled the end of the @@ -142,6 +144,7 @@ clientCapabilities = pyqtSignal(int, str, str) clientCompletionList = pyqtSignal(list, str) clientInterpreterChanged = pyqtSignal(str) + utDiscovered = pyqtSignal(list, str, str) utPrepared = pyqtSignal(int, str, str) utStartTest = pyqtSignal(str, str) utStopTest = pyqtSignal() @@ -1311,11 +1314,52 @@ @param text the text to be completed (string) """ self.debuggerInterface.remoteCompletion(text) - + + def remoteUTDiscover(self, clientType, forProject, venvName, syspath, + workdir, discoveryStart): + """ + Public method to perform a test case discovery. + + @param clientType client type to be used + @type str + @param forProject flag indicating a project related action + @type bool + @param venvName name of a virtual environment + @type str + @param syspath list of directories to be added to sys.path on the + remote side + @type list of str + @param workdir path name of the working directory + @type str + @param discoveryStart directory to start auto-discovery at + @type str + """ + if clientType and clientType not in self.getSupportedLanguages(): + # a not supported client language was requested + E5MessageBox.critical( + None, + self.tr("Start Debugger"), + self.tr( + """<p>The debugger type <b>{0}</b> is not supported""" + """ or not configured.</p>""").format(clientType) + ) + return + + # Restart the client if there is already a program loaded. + try: + if clientType: + self.__setClientType(clientType) + except KeyError: + self.__setClientType('Python3') # assume it is a Python3 file + self.startClient(False, forProject=forProject, venvName=venvName) + + self.debuggerInterface.remoteUTDiscover( + syspath, workdir, discoveryStart) + def remoteUTPrepare(self, fn, tn, tfn, failed, cov, covname, coverase, clientType="", forProject=False, venvName="", syspath=None, workdir="", discover=False, - discoveryStart=""): + discoveryStart="", testCases=None): """ Public method to prepare a new unittest run. @@ -1349,6 +1393,8 @@ @type bool @param discoveryStart directory to start auto-discovery at @type str + @param testCases list of test cases to be loaded + @type list of str """ if clientType and clientType not in self.getSupportedLanguages(): # a not supported client language was requested @@ -1374,7 +1420,7 @@ self.debuggerInterface.remoteUTPrepare( fn, tn, tfn, failed, cov, covname, coverase, syspath, workdir, - discover, discoveryStart) + discover, discoveryStart, testCases) self.debugging = False self.running = True @@ -1634,6 +1680,19 @@ isCall, fromFile, fromLine, fromFunction, toFile, toLine, toFunction) + def clientUtDiscovered(self, testCases, exceptionType, exceptionValue): + """ + Public method to process the client unittest discover info. + + @param testCases list of detected test cases + @type str + @param exceptionType exception type + @type str + @param exceptionValue exception message + @type str + """ + self.utDiscovered.emit(testCases, exceptionType, exceptionValue) + def clientUtPrepared(self, result, exceptionType, exceptionValue): """ Public method to process the client unittest prepared info.
--- a/Debugger/DebuggerInterfaceNone.py Mon Mar 25 20:18:47 2019 +0100 +++ b/Debugger/DebuggerInterfaceNone.py Tue Mar 26 19:29:30 2019 +0100 @@ -408,8 +408,22 @@ """ return + def remoteUTDiscover(self, syspath, workdir, discoveryStart): + """ + Public method to perform a test case discovery. + + @param syspath list of directories to be added to sys.path on the + remote side + @type list of str + @param workdir path name of the working directory + @type str + @param discoveryStart directory to start auto-discovery at + @type str + """ + return + def remoteUTPrepare(self, fn, tn, tfn, failed, cov, covname, coverase, - syspath, workdir, discover, discoveryStart): + syspath, workdir, discover, discoveryStart, testCases): """ Public method to prepare a new unittest run. @@ -437,6 +451,8 @@ @type bool @param discoveryStart directory to start auto-discovery at @type str + @param testCases list of test cases to be loaded + @type list of str """ return
--- a/Debugger/DebuggerInterfacePython.py Mon Mar 25 20:18:47 2019 +0100 +++ b/Debugger/DebuggerInterfacePython.py Tue Mar 26 19:29:30 2019 +0100 @@ -927,8 +927,26 @@ "text": text, }) + def remoteUTDiscover(self, syspath, workdir, discoveryStart): + """ + Public method to perform a test case discovery. + + @param syspath list of directories to be added to sys.path on the + remote side + @type list of str + @param workdir path name of the working directory + @type str + @param discoveryStart directory to start auto-discovery at + @type str + """ + self.__sendJsonCommand("RequestUTDiscover", { + "syspath": [] if syspath is None else syspath, + "workdir": workdir, + "discoverystart": discoveryStart, + }) + def remoteUTPrepare(self, fn, tn, tfn, failed, cov, covname, coverase, - syspath, workdir, discover, discoveryStart): + syspath, workdir, discover, discoveryStart, testCases): """ Public method to prepare a new unittest run. @@ -956,6 +974,8 @@ @type bool @param discoveryStart directory to start auto-discovery at @type str + @param testCases list of test cases to be loaded + @type list of str """ if fn: self.__scriptName = os.path.abspath(fn) @@ -976,6 +996,7 @@ "workdir": workdir, "discover": discover, "discoverystart": discoveryStart, + "testcases": [] if testCases is None else testCases, }) def remoteUTRun(self): @@ -1193,6 +1214,11 @@ self.debugServer.signalClientCompletionList( params["completions"], params["text"]) + elif method == "ResponseUTDiscover": + self.debugServer.clientUtDiscovered( + params["testCasesList"], params["exception"], + params["message"]) + elif method == "ResponseUTPrepared": self.debugServer.clientUtPrepared( params["count"], params["exception"], params["message"])
--- a/PyUnit/UnittestDialog.py Mon Mar 25 20:18:47 2019 +0100 +++ b/PyUnit/UnittestDialog.py Tue Mar 26 19:29:30 2019 +0100 @@ -75,7 +75,6 @@ self.discoveryPicker.setSizeAdjustPolicy( QComboBox.AdjustToMinimumContentsLength) - # TODO: add a "Discover" button enabled upon selection of 'auto-discovery' self.discoverButton = self.buttonBox.addButton( self.tr("Discover"), QDialogButtonBox.ActionRole) self.discoverButton.setToolTip(self.tr( @@ -161,6 +160,7 @@ # now connect the debug server signals if called from the eric6 IDE 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) @@ -221,6 +221,9 @@ self.insertDiscovery("") else: self.insertDiscovery("") + + self.discoveryList.clear() + self.tabWidget.setCurrentIndex(0) def insertDiscovery(self, start): """ @@ -369,8 +372,38 @@ self.testName = self.tr("Unittest with auto-discovery") if self.__dbs: - # TODO: implement this later - pass + 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] + if not discoveryStart: + discoveryStart = workdir + else: + 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" + self.__dbs.remoteUTDiscover(clientType, self.__forProject, + workdir, venvName, sysPath, + discoveryStart) else: # we are running as an application if not discoveryStart: @@ -399,26 +432,25 @@ try: testLoader = unittest.TestLoader() test = testLoader.discover(discoveryStart) - if test: - 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) - self.__populateDiscoveryResults(testsList) - self.sbLabel.setText( - self.tr("Discovered %n Test(s)", "", - len(testsList))) - self.tabWidget.setCurrentIndex(0) + 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) + 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( @@ -502,6 +534,69 @@ itm.setData(0, Qt.UserRole, modulePath) 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, Qt.UserRole)) + # 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, Qt.UserRole)) + # 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 utPrepared 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() def startTests(self, failedOnly=False): """ @@ -551,6 +646,11 @@ else: self.testName = self.tr("<Unnamed Test>") + if failedOnly: + testCases = [] + else: + testCases = self.__selectedTestCases() + if self.__dbs: venvName = self.venvComboBox.currentText() @@ -620,7 +720,8 @@ self.coverageEraseCheckBox.isChecked(), clientType=clientType, forProject=self.__forProject, workdir=workdir, venvName=venvName, syspath=sysPath, - discover=discover, discoveryStart=discoveryStart) + discover=discover, discoveryStart=discoveryStart, + testCases=testCases) else: # we are running as an application if discover and not discoveryStart: @@ -660,7 +761,10 @@ else: failed = [] if discover: - test = testLoader.discover(discoveryStart) + if testCases: + test = testLoader.loadTestsFromNames(testCases) + else: + test = testLoader.discover(discoveryStart) else: if testFileName: module = __import__(self.testName)