UnittestDialog: implemented the remote part fo 'discover only'.

Tue, 26 Mar 2019 19:29:30 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Tue, 26 Mar 2019 19:29:30 +0100
changeset 6901
f2c774c8db7e
parent 6900
060a30488316
child 6902
67d0ad66b59a

UnittestDialog: implemented the remote part fo 'discover only'.

DebugClients/Python/DebugClientBase.py file | annotate | diff | comparison | revisions
Debugger/DebugServer.py file | annotate | diff | comparison | revisions
Debugger/DebuggerInterfaceNone.py file | annotate | diff | comparison | revisions
Debugger/DebuggerInterfacePython.py file | annotate | diff | comparison | revisions
PyUnit/UnittestDialog.py file | annotate | diff | comparison | revisions
--- 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)

eric ide

mercurial